GPT-4와 Claude 같은 대규모 언어 모델(LLM)은 놀라울 정도로 강력하지만, 근본적인 한계가 있습니다: 학습 시점의 지식이 고정되어 있다는 것입니다. 내부 문서, 데이터베이스, 실시간 정보에 접근할 수 없습니다. **검색 증강 생성(RAG)**은 LLM의 생성 능력과 외부 소스에서 정보를 검색하는 능력을 결합하여 바로 이 문제를 해결합니다.
문제점: LLM의 한계
RAG에 대해 이야기하기 전에, 왜 이것이 필요한지 이해하는 것이 중요합니다.
- 정적 지식: LLM은 학습 중에 본 것만 알고 있습니다. 컷오프 이후에 발생한 사건에 대해 물어보면 답할 수 없습니다.
- 환각: LLM이 답을 모를 때, 그럴듯하지만 완전히 거짓된 정보를 생성하여 답을 지어내는 경향이 있습니다.
- 비공개 데이터에 대한 접근 불가: 일반적인 LLM은 회사의 내부 문서, 티켓 또는 코드베이스에 접근할 수 없습니다.
RAG는 쿼리 시점에 외부 소스에서 검색된 관련 컨텍스트를 모델에 제공함으로써 이 세 가지 문제를 모두 해결합니다.
RAG란 무엇인가?
검색 증강 생성은 외부 지식 베이스에서 검색된 정보로 LLM에 전송되는 프롬프트를 풍부하게 하는 아키텍처입니다. 모델의 파라메트릭 지식에만 의존하는 대신, RAG는 먼저 관련 정보를 검색한 다음 프롬프트에 주입하여 모델이 정확하고 근거 있는 응답을 생성할 수 있게 합니다.
RAG의 상세 작동 방식
RAG 아키텍처는 두 가지 주요 단계로 구성됩니다: 인덱싱 (오프라인)과 검색 + 생성 (온라인).
단계 1: 인덱싱 (문서 수집)
인덱싱 단계는 시맨틱 검색을 위해 문서를 준비합니다. 네 가지 단계로 구성됩니다.
1. 문서 로딩
문서는 어떤 소스에서든 올 수 있습니다: PDF 파일, 웹 페이지, 데이터베이스, Markdown 파일, API. Document Loader는 이러한 문서를 읽고 구조화된 텍스트로 변환합니다.
2. 텍스트 분할 (Chunking)
LLM은 제한된 컨텍스트 윈도우를 가지고 있으며, 문서는 매우 길 수 있습니다. Text Splitter는 문서를 chunks라고 불리는 작은 조각으로 나눕니다. 청킹의 품질은 매우 중요합니다: 너무 작은 chunks는 컨텍스트를 잃고, 너무 큰 chunks는 관련성을 희석시킵니다.
가장 일반적인 전략은 다음과 같습니다:
- 재귀적 문자 분할:
\n\n,\n,.같은 구분자를 사용하여 문서 구조를 존중하면서 텍스트를 재귀적으로 분할합니다. - 시맨틱 분할: embeddings를 사용하여 텍스트에서 자연스러운 분기점을 찾습니다.
- Chunk 오버랩: 경계에서 컨텍스트를 보존하기 위해 연속된 chunks 간에 겹침을 포함합니다.
3. Embedding
각 chunk는 embedding 모델(예: OpenAI의 text-embedding-3-small)을 통해 숫자 벡터 (embedding)로 변환됩니다. 이 벡터는 텍스트의 의미적 의미를 포착합니다: 비슷한 의미를 가진 문장은 다차원 공간에서 가까운 벡터를 갖게 됩니다.
4. Vector Store
벡터는 ChromaDB, Pinecone, Weaviate 또는 FAISS와 같은 Vector Store (또는 벡터 데이터베이스)에 저장됩니다. 이 데이터베이스는 유사도 검색에 최적화되어 있습니다: 주어진 쿼리에 대해 가장 유사한 벡터(따라서 가장 관련성 높은 텍스트 chunks)를 찾습니다.
단계 2: 검색 + 생성
사용자가 질문을 할 때:
- 질문은 동일한 embedding 모델을 사용하여 embedding으로 변환됩니다.
- Vector Store가 유사도 검색 (일반적으로 코사인 유사도 또는 유클리드 거리)을 통해 가장 유사한 chunks를 찾습니다.
- 검색된 chunks가 컨텍스트로 프롬프트에 삽입됩니다.
- LLM이 제공된 컨텍스트를 기반으로 응답을 생성합니다.
LangChain으로 RAG 파이프라인 구축하기
LangChain은 LLM 기반 애플리케이션을 구축하기 위한 가장 인기 있는 Python (및 JavaScript) 프레임워크입니다. RAG 파이프라인의 모든 구성 요소에 대한 높은 수준의 추상화를 제공합니다.
설치
pip install langchain langchain-openai langchain-community chromadb
1단계: 문서 로드
LangChain은 다양한 데이터 소스를 위한 수십 개의 Document Loader를 제공합니다.
from langchain_community.document_loaders import ( PyPDFLoader, WebBaseLoader, DirectoryLoader, TextLoader, ) # Load a PDF pdf_loader = PyPDFLoader("docs/manual.pdf") pdf_docs = pdf_loader.load() # Load a web page web_loader = WebBaseLoader("https://docs.example.com/guide") web_docs = web_loader.load() # Load all .md files from a directory dir_loader = DirectoryLoader("./knowledge_base", glob="**/*.md", loader_cls=TextLoader) md_docs = dir_loader.load() all_docs = pdf_docs + web_docs + md_docs
2단계: 문서를 Chunks로 분할
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", ". ", " ", ""], ) chunks = text_splitter.split_documents(all_docs) print(f"Original documents: {len(all_docs)}, Chunks: {len(chunks)}")
chunk_overlap 파라미터는 매우 중요합니다: 연속된 chunks 간에 겹침을 만들어 경계에서 컨텍스트가 손실되지 않도록 합니다.
3단계: Embeddings와 Vector Store 생성
from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma embedding_model = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )
4단계: Retriever 생성
Retriever는 주어진 쿼리에 대해 vector store에서 가장 관련성 높은 chunks를 가져오는 구성 요소입니다.
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4}, ) relevant_docs = retriever.invoke("How does authentication work?") for doc in relevant_docs: print(doc.page_content[:200]) print("---")
5단계: RAG 체인 구축
이제 LLM과 프롬프트 템플릿으로 모든 것을 결합해 봅시다.
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser llm = ChatOpenAI(model="gpt-4o", temperature=0) prompt = ChatPromptTemplate.from_template(""" Answer the question based only on the provided context. If the context does not contain enough information, say you don't know. Context: {context} Question: {question} Answer: """) def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) response = rag_chain.invoke("How does authentication work in the system?") print(response)
고급 RAG 기법
기본 파이프라인은 잘 작동하지만, 응답 품질을 크게 향상시키는 여러 기법이 있습니다.
멀티 쿼리 검색
때때로 사용자의 쿼리가 모호하거나 문서에서 사용되는 언어와 일치하지 않습니다. Multi-Query Retriever는 여러 관점을 포착하기 위해 원래 질문의 변형을 자동으로 생성합니다.
from langchain.retrievers import MultiQueryRetriever multi_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(), llm=llm, ) docs = multi_retriever.invoke("What are the security best practices?")
컨텍스트 압축
chunk의 모든 내용이 쿼리와 관련된 것은 아닙니다. Contextual Compression Retriever는 검색된 각 chunk에서 관련 부분만 추출하기 위해 LLM을 사용합니다.
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor compressor = LLMChainExtractor.from_llm(llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=retriever, )
하이브리드 검색
순수한 시맨틱 검색이 항상 최적은 아닙니다. 하이브리드 검색은 더 나은 결과를 얻기 위해 시맨틱 검색(embeddings)과 어휘 검색(BM25, 키워드 매칭)을 결합합니다.
from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 4 semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) hybrid_retriever = EnsembleRetriever( retrievers=[bm25_retriever, semantic_retriever], weights=[0.4, 0.6], )
대화형 RAG (메모리 포함)
대화 컨텍스트를 기억하는 RAG 챗봇을 구축하려면, 대화 기록을 고려하여 사용자의 질문을 재구성하는 메모리를 추가해야 합니다.
from langchain.chains import create_history_aware_retriever from langchain_core.prompts import MessagesPlaceholder contextualize_prompt = ChatPromptTemplate.from_messages([ ("system", "Given the chat history and the user's latest question, " "reformulate the question so it is understandable without the history."), MessagesPlaceholder("chat_history"), ("human", "{input}"), ]) history_aware_retriever = create_history_aware_retriever( llm, retriever, contextualize_prompt )
모범 사례
- 적절한 chunk 크기 선택: 다양한 크기(500-1500 토큰)로 실험하세요. 정확한 답변을 위한 작은 chunks, 넓은 컨텍스트를 위한 큰 chunks.
- 문서 메타데이터 사용: chunks에 소스, 날짜, 카테고리를 메타데이터로 추가하세요. 이를 통해 검색 시 결과를 필터링할 수 있습니다.
- 품질 평가: faithfulness, relevancy, context precision 같은 메트릭을 측정하기 위해 RAGAS 같은 프레임워크를 사용하세요.
- 문서 업데이트 관리: 데이터 소스와 vector store의 동기화를 유지하기 위한 재수집 파이프라인을 구현하세요.
- re-ranker 추가: 초기 검색 후, 실제 관련성에 기반하여 결과를 재정렬하기 위해 re-ranking 모델(예: Cohere Rerank)을 사용하세요.
결론
RAG는 특정하고 최신의 지식에 접근해야 하는 AI 애플리케이션을 구축하기 위한 표준 아키텍처가 되었습니다. LangChain은 파이프라인의 모든 구성 요소에 대한 추상화를 제공하여 구현을 크게 단순화합니다.
다음 단계:
- 로컬에서 실험하기: 파이프라인에 익숙해지기 위해 ChromaDB와 몇 개의 문서로 시작하세요.
- LangSmith 탐색하기: 프로덕션에서 체인을 모니터링하고 디버깅하기 위해 LangSmith를 사용하세요.
- 다양한 embedding 모델 시도:
text-embedding-3-small,text-embedding-3-large, 그리고 Sentence Transformers의 오픈소스 모델을 비교해 보세요. - 문서 확인: LangChain 문서는 훌륭하고 지속적으로 업데이트되는 리소스입니다.