Day to_day

Basic RAG를 실습 코드와 함께 알아보기 (feat. llamaIndex, langchain) 본문

카테고리 없음

Basic RAG를 실습 코드와 함께 알아보기 (feat. llamaIndex, langchain)

m_inglet 2024. 11. 10. 17:21
728x90
반응형

들어가며

  • RAG를 사용해야 하는 이유
  • RAG의 패러다임 (RAG 변천사)
  • Naive RAG의 구조
  • langchain과 llamaIndex를 활용한 chromaDB로 Naive RAG 구현하기

 

What is RAG(Retrieval-Augmented Generation)?

전통적인 LLM들은 특정 시점의 데이터로만 훈련되어 훈련 이후의 정보는 반영하지 못한다. 특히 최신 정보를 답변하지 못하거나 hallucination 문제로 인해 구글, OpenAI, Facebook 등 여러 연구 기관과 기업들이 검색 증강 생성(Retrieval-Augmented Generation, RAG) 구조를 도입하게 되었다. 이에 따라 다른 여러 기업에서도 특히 환각 현상을 해결하기 위해 RAG에 대한 주목이 커지기 시작했다.

LLM만 사용하는 것과 달리 RAG를 추가하면 특정 도메인이나 조직 내부 데이터를 활용함으로써 더욱 정밀한 답변을 생성할 수 있다. Finetuning으로 LLM에 직접 지식 주입을 할 수도 있겠지만 시간과 비용이 많이 들고 데이터가 업데이트됨에 따라 매번 finetuning을 진행하는 것은 한계가 있다. 또한 RAG를 사용하면 출처를 기반으로 답변을 생성하기 때문에 답변의 근거를 제시할 수 있다는 장점도 있다. 그리고 초기 LLM에 비해 점차 더 많은 token을 처리할 수 있게 되면서 context를 LLM에 넣어줄 수 있게되어 RAG를 활용하는 사례도 계속해서 발전해 나아가고 있다.

 

 

RAG 변천사

 

RAG는 위의 같은 패러다임을 거쳐서 계속해서 발전하고 있다. Naive RAG가장 간단하고 기본적인 RAG이고, 오늘은 이 Naive RAG에 대해서 간단하게 살펴보고자 한다.

그리고 이후 Advanced RAGpre-retrieval과 post-retrieval이 붙은 구조이다. 이는 더 정교한 검색을 할 수 있도록 더욱 발전된 형태의 RAG이다. 그리고 세번째 Modular RAG는 여러 모듈들로 이루어져 있고, 각각의 기능 별로 나누어져 있고, 이 여러 모듈을 레고처럼 조합해서 검색과 생성을 하는 RAG로 발전하게 되었다.

 

Naive RAG

Naive RAG의 구조를 보면 단순한 RAG 패러다임으로 주요 구성 요소는 인덱싱(Indexing), 검색(Retrieve), 생성(Generation)이 있다. Naive RAG를 보며 RAG가 동작하는 기본 개념을 먼저 이해하면 RAG 구조의 큰 틀을 확인하고 갈 수 있다. 세부적인 내용은 뒤에서 더 소개할 텐데 우선 이 방법은 검색의 정확도가 낮고, 그로 인한 낮은 품질의 응답을 생성한다는 한계가 있다.

 

Advanced RAG

이런 부족한 점을 개선하기 위해서 크게 Pre-Retrieval Process, Post-Retrieval Process을 추가하면서 더 다양한 기법을 통해 검색과 생성의 질을 크게 향상했다. Naive RAG에 비해서는 더 많이 개선되기는 했지만 애플리케이션에서의 사용 사례는 실제로는 더욱 복잡하기 때문에 다양한 케이스를 모두 커버하는데 한계가 존재했다. 즉, 한 방향의 RAG 흐름에서는 다양한 데이터를 모두 통합하는데에 파이프라인을 제어하기 어렵다는 것이 한계점으로 지적되었다.

 

Modular RAG

그래서 Modular RAG는 여러 기능을 모두 모듈화를 시켜 재구성이 용이하고 유연한 흐름을 만들 수 있도록 고안된 방법이다. 특히 동적으로 레고처럼 구성할 수 있다는 것에서 다양한 플로우를 만들어 낼 수 있다.

 

 

Naive RAG

이제 가장 기본이 되는 Naive RAG에 대해서 자세하게 들어가보자. 

위의 RAG 워크플로우를 간단하게 설명하면 원하는 문서를 검색하기 좋은 형태로 vectordb에 한번에 모두 저장한다. 이후에 유저가 보내는 쿼리와 가장 관련된 콘텐츠들을 검색, 그리고 그 콘텐츠와 유저 쿼리를 llm에 함께 넣어 답변을 생성하게 된다. 크게 보면 문서를 DB에 저장하는 단계, 유저가 사용할 때 일어나는 단계로 나눠볼 수 있다. 그 과정에 대해 자세히 알아보자.

 

1. 인덱싱(Indexing)

인덱싱은 데이터 소스(source)에서 문서를 파싱하여 데이터를 얻고 인덱스를 생성하는 과정으로 데이터 정제, 청크(chunk) 분할, 벡터 인코딩 및 인덱스 생성을 포함한다.

 

0-1. 데이터 로드(load data)

- 다양한 타입(web page, text, csv, pdf etc..)의 데이터를 로드하는 단계로 문서를 바로 읽어오기도, 혹은 파서를 이용해 전처리 작업을 거쳐 원하는 형태의 데이터로 변환하는 단계이다.

- 예) pdf 파일을 읽어서 마크다운으로 변환, 캡쳐된 문서를 읽어와서 OCR로 텍스트로 변환

 

0-2. 텍스트 분할(chunk split)

- 불러온 데이터를 작은 크기의 단위(chunk)로 분할하는 작업이다. 텍스트가 분리되는 기준에 따라 ‘청크마다 독립적인 의미를 갖도록’ 나눌 수도 있고, 혹은 문장, 구절, 단락 등의 문장 구조를 기준으로 나눌 수 있다.

- 청크 크기 역시 직접 조정하여 제한하거나 단어 수, 문자 수로 기준으로 나눌 수 있다.

 

0-3. 임베딩

- 위의 워크플로우에는 빠져있지만 우리는 이렇게 분할된 텍스트를 검색 가능한 형태로 만드는 단계로 만들어야한다.

- 문자 자체로 비교하는 것보다 언어를 벡터 형태로 만들어서 유사도를 비교하는 게 더 쉽다.

출처: https://www.nvidia.com/en-us/glossary/vector-database/

- 즉, 텍스트를 임베딩으로 변환하고, 이를 저장한 후 저장된 임베딩을 기반으로 유사성 검색을 수행하는 과정이다.

- 이때 청크를 인덱싱하는 과정에서 window의 제한이 있고, 그 제한된 사이즈 안에서 해당 chunk를 벡터화시켜서 압축을 시켜 저장한다.

 

2. 검색 (Retrival)

- 청크를 벡터로 만들고 그것을 vectorDB에 저장했다면 저장된 벡터들 중에서 사용자의 쿼리(question)에 가장 유사한 벡터를 빠르게 찾아내는 작업이 필요하다.

- 청크를 벡터로 변환할 때 사용했던 임베딩 모델과 같은 모델을 사용해 사용자의 쿼리도 임베딩 시킨다.

- 이 검색하는 방법으로는 코사인 유사도, 유클리드 거리, 맨해튼 거리 등 다양한 유사도 측정 방법을 사용 가능하지만 주로 코사인 유사도를 텍스트 임베딩 검색에 자주 사용한다.

- 사용자의 쿼리에 대해 계산된 유사도 점수를 기반으로 가장 유사한 항목들을 순서대로 top k개를 사용자에게 반환한다.

 

3. 생성 (Generation)

주어진 질문과 RAG를 통해 검색된 관련 context를 결합하여 새로운 프롬프트를 생성하고, llm은 이 정보를 기반으로 질문에 답변하는 과정이다.

 

 

코드 구현하기

LLM을 활용한 애플리케이션 개발을 간편하게 도와주는 도구로는 대표적으로 langchainllamaIndex가 있다. 현재 업무에서는 llamaIndex를 사용하고 있는데 개인적으로는 langchain이 좀 더 프로젝트 단위로 관리하기가 편한 것 같다. 물론 두 도구 모두 공식 문서가 잘 되어있기 때문에 참고해서 쉽게 구현이 가능한데 이 글을 읽게 되는 분들이 어떤 것을 사용할지 모르니 두 개의 예시 모두 적어두려 한다.

코드는 문서를 Chroma DB에 저장하고, 쿼리를 보내보는 실습 코드를 가져왔다.

 

langchain을 활용한 코드 예시

langchain== 0.3.7 버전으로 작성된 코드입니다. 

 

1. 문서 로드 후 splitter로 청킹, 모든 chunk를 ChromaDB에 저장하기 (경로를 지정해 주어 디스크에 저장)

 

 

2. ChromaDB 로드해서 retriever로 만들기

 

 

3. Naive RAG로 문서 검색 후 chain 구성해서 답변 생성

chain 구조: retriever를 통해 검색된 chunk를 format_docs 함수에 넣어 content만 가져와서 이어 붙인다. 

그 내용은 모두 template에 context 자리에 들어가게 된다. 

입력 쿼리는 RunnablePassthrough를 통해서 그대로 template의 question 자리에 전달된다. 

이후 template은 llm의 입력으로 전달되고 output parser를 통해 response만 얻게 된다. 

 

llamaIndex를 활용한 코드 예시

llama-index == 0.11.22 버전으로 작성된 코드입니다.

 

1. ChromaDB에 문서를 저장

문서를 가져와서 VectorStoreIndex의 from_documents로 청크를 분할하고 데이터 저장한다. 

 

2. query_engine을 이용해서 답변을 생성

ChromaDB를 불러와서 as_query_engine으로 query_engine을 생성한 뒤에 입력 쿼리 전달한다. 

 

query_engine을 사용하지 않고 OpenAI API를 직접 호출해서 사용하는 방법도 있는데 그것은 github으로 업로드해 두겠다. 

 

 

단계별로 자세한 코드는 아래 github을 통해서 확인할 수 있습니다. 

 

RAG_practice/Naive RAG at main · minglet/RAG_practice

Contribute to minglet/RAG_practice development by creating an account on GitHub.

github.com

 

Reference

RAG from scratch

대규모 언어 모델을 위한 검색-증강 생성(RAG) 기술 현황 - 1/2편

https://wikidocs.net/231431

https://python.langchain.com/docs/tutorials/rag/

728x90
반응형
Comments