안녕하세요! 이번 벨로그 시리즈에서는 최근 LLM(Large Language Model) 애플리케이션 개발의 핵심 기술 중 하나인 RAG(Retrieval-Augmented Generation)에 대해 깊이 있게 다뤄보겠습니다.

최근 LLM은 놀라운 성능을 보여주며 다양한 분야에서 활용되고 있습니다. 하지만 LLM은 학습된 데이터에 기반하여 답변을 생성하기 때문에 다음과 같은 한계를 가집니다.
환각(Hallucination) 현상: 사실과 다른 정보를 마치 사실인 것처럼 생성할 수 있습니다.
최신 정보 부족: 학습 시점 이후의 최신 정보나 실시간 데이터에 접근하기 어렵습니다.
도메인 특화 지식 부족: 특정 산업이나 기업의 내부 문서와 같은 전문적인 지식에 대해서는 답변하기 어렵습니다.
이러한 한계를 극복하기 위해 등장한 것이 바로 RAG입니다. RAG는 LLM이 외부의 신뢰할 수 있는 정보원에서 필요한 지식을 '검색(Retrieval)'하여 답변을 '생성(Generation)'하는 과정을 '증강(Augmented)'시키는 기술입니다. 마치 시험을 치르기 전에 참고 자료를 찾아보고 답변하는 똑똑한 학생과 같다고 할 수 있습니다.
RAG는 크게 검색(Retrieval)과 생성(Generation) 두 단계로 나눌 수 있으며, 이를 위해 여러 구성 요소가 유기적으로 작동합니다.
(위 이미지는 RAG 아키텍처를 시각적으로 보여주는 다이어그램이 들어갈 자리입니다. 실제 벨로그 게시 시에는 적절한 이미지로 대체해주세요.)
가장 먼저, LLM에 제공할 외부 지식 소스(문서)를 준비합니다. 이는 PDF, TXT, Markdown, 웹 페이지 등 다양한 형태가 될 수 있습니다.
방대한 문서를 LLM이 처리하기 쉬운 작은 단위(청크)로 분할합니다. 이때 청크의 크기와 오버랩(겹치는 부분) 전략이 검색 성능에 큰 영향을 미칩니다. 너무 작으면 문맥이 끊기고, 너무 크면 불필요한 정보가 많아질 수 있습니다.
분할된 텍스트 청크들을 벡터 공간의 숫자 표현(임베딩)으로 변환합니다. 임베딩은 텍스트의 의미를 함축하고 있어, 의미적으로 유사한 텍스트는 벡터 공간에서 가깝게 위치하게 됩니다. OpenAI의 text-embedding-ada-002나 Sentence Transformers와 같은 임베딩 모델이 사용됩니다.
생성된 임베딩 벡터들을 저장하고 효율적으로 검색할 수 있도록 인덱싱하는 데이터베이스입니다. Chroma, Pinecone, FAISS, Weaviate, PGVector(PostgreSQL 확장) 등 다양한 벡터 스토어가 있습니다.
사용자의 질문(쿼리)이 들어오면, 이 질문 또한 임베딩으로 변환합니다. 그리고 이 임베딩을 벡터 스토어에서 검색하여 질문과 가장 유사한(가장 관련성 높은) 문서 청크들을 찾아냅니다.
검색된 관련성 높은 문서 청크들을 사용자의 원래 질문과 함께 LLM에게 전달할 새로운 프롬프트로 구성합니다. 이 과정에서 프롬프트 엔지니어링이 중요하며, LLM이 검색된 정보를 효과적으로 활용하도록 안내합니다.
증강된 프롬프트를 LLM에 입력하여 최종 답변을 생성합니다. LLM은 이제 외부 지식을 참고하여 더 정확하고 신뢰할 수 있는 답변을 제공할 수 있게 됩니다.
파이썬에서 RAG를 구현할 때는 LangChain이나 LlamaIndex와 같은 프레임워크를 활용하면 편리합니다. 다음은 개념적인 구현 흐름을 보여주는 코드입니다.
# 필요한 라이브러리 임포트 (예시)
# from langchain.document_loaders import TextLoader
# from langchain.text_splitter import CharacterTextSplitter
# from langchain.embeddings import OpenAIEmbeddings
# from langchain.vectorstores import Chroma
# from langchain.chat_models import ChatOpenAI
# from langchain.chains import RetrievalQA
# 1. 문서 로딩
# loader = TextLoader("./my_document.txt")
# documents = loader.load()
# 2. 텍스트 분할
# text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
# docs = text_splitter.split_documents(documents)
# 3. 임베딩 및 벡터 스토어 저장
# embeddings = OpenAIEmbeddings()
# vectorstore = Chroma.from_documents(docs, embeddings)
# 4. 검색 및 생성 체인 구성
# llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
# qa_chain = RetrievalQA.from_chain_type(llm, retriever=vectorstore.as_retriever())
# 5. 질문 및 답변 생성
# query = "RAG는 무엇인가요?"
# response = qa_chain.run(query)
# print(response)
(위 코드는 개념적인 예시이며, 실제 동작하는 코드를 위해서는 각 라이브러리의 설치 및 API 키 설정 등이 필요합니다.)
RAG는 LLM 기반 애플리케이션에 다음과 같은 중요한 이점을 제공합니다.
환각 감소: 외부의 정확한 정보를 기반으로 답변하여 잘못된 정보 생성을 줄입니다.
최신성 및 정확성: 실시간 데이터나 최신 정보를 반영하여 답변의 정확성을 높입니다.
도메인 특화: 특정 분야의 전문 지식을 LLM에 주입하여 활용할 수 있습니다.
출처 명시 가능: 검색된 문서 청크를 통해 답변의 근거를 제시할 수 있어 신뢰도를 높입니다.
모델 재학습 불필요: 새로운 정보가 추가될 때마다 LLM을 재학습시킬 필요 없이 벡터 스토어만 업데이트하면 됩니다.
RAG는 강력한 기술이지만, 최적의 성능을 위해서는 몇 가지 고려 사항이 있습니다.
RAG는 LLM의 지식 한계를 보완하고, 더욱 정확하고 신뢰할 수 있는 AI 애플리케이션을 구축하는 데 필수적인 기술입니다. 문서 기반 질의응답 시스템, 챗봇, 지식 관리 시스템 등 다양한 분야에서 RAG의 활용 가능성은 무궁무진합니다. 다음 시리즈에서는 RAG의 각 구성 요소를 더 깊이 파고들거나, 실제 프로젝트에 적용하는 예시를 다뤄보겠습니다.