RAG (Retrieval-Augmented Generation) 완전 정복
LangChain에서 RAG 사용하기
관련 컴포넌트
- Document Loader: 데이터 소스에서 문서를 읽어들임
- Document Transformer: 문서에 어떤 변환을 가함
- Embedding Model: 문서를 벡터화함
- Vector Store: 벡터화한 문서의 저장소
- Retriever: 입력 텍스트와 관련된 문서를 검색함
1. 필요한 파일 읽어오기
!pip install langchain-community==0.3.0 GitPython==3.1.43
from langchain_community.document_loaders import GitLoader
def file_filter(file_path: str) -> bool:
return file_path.endswith(".mdx")
# langchain 문서는 md, mdx, ipynb 등의 형식으로 작성되어 있고, 문서의 빌드 처리를 위해서 이 형식의 문서만 읽어야 한다
loader = GitLoader(
clone_url = "https://github.com/langchain-ai/langchain",
repo_path = "./langchain",
branch = "master",
file_filter = file_filter
)
raw_docs = loader.load()
여러가지 로더 소개
- 파일 시스템 로더: CSV, JSON, PDF, Text, Directory, MS Office …
- 웹 기반 로더: 웹 페이지, 여러 웹 페이지에 대한 재귀적 로드, 사이트맵 기반 웹 페이지 로드, 유튜브 비디오 트랜스크립트 …
- 클라우드 스토리지
- 데이터베이스
- 협업 도구, 소셜 미디어, 오디오 …
대부분의 로더는 문서와 함께 메타데이터를 제공합니다.
2. 문서를 변환하기
Data Transformer: 문서를 청킹하기
!pip install langchain-text-splitters==0.3.0
from langchain_text_splitters import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
chunk_size = 1000,
chunk_overlap = 0
)
docs = text_splitter.split_documents(raw_docs)
단순 청킹 이외에도 다양한 변환 처리가 가능합니다:
- HTML을 일반 텍스트로 변환
- 메타데이터 추출
- 문서 번역
- 사용자 질문과의 관련성을 높이기 위해 문서에서 Q&A 생성
3. 임베딩하기
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
model = "text-embedding-3-small"
)
query = "What is LangChain?"
vector = embeddings.embed_query(query)
4. Vector Store 구축
여기서는 Chroma를 사용합니다:
# pip install langchain-chroma==0.1.4
from langchain_chroma import Chroma
db = Chroma.from_documents(
docs,
embeddings
)
retreiver = db.as_retriever()
query = "What is LangChain?"
context = retreiver.invoke(query)
first_doc = context[0]
print(first_doc.metadata)
print(first_doc.page_content)
LCEL 활용하기
# LCEL을 활용한 RAG chain 구현
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
prompt = ChatPromptTemplate.from_template(
'''
Answer the question based on the context provided.
Context: {context}
Question: {question}
'''
)
model = ChatOpenAI(
model = "gpt-4o-mini",
temperature = 0
)
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| model
| StrOutputParser()
)
chain.invoke({"question": "What is LangChain?"})
📚 추가로 알아볼 내용
1. 고급 Document Loaders
AsyncWebCrawler:
from langchain_community.document_loaders import AsyncWebCrawler
import asyncio
async def crawl_websites():
loader = AsyncWebCrawler(urls=["https://example1.com", "https://example2.com"])
docs = await loader.aload()
return docs
# 비동기 처리로 속도 향상
UnstructuredFileLoader (다양한 파일 형식):
from langchain_community.document_loaders import UnstructuredFileLoader
# PDF, DOCX, PPTX, HTML, TXT 등 자동 처리
loader = UnstructuredFileLoader("mixed_documents/")
docs = loader.load()
DatabaseLoader:
from langchain_community.document_loaders import SQLDatabaseLoader
loader = SQLDatabaseLoader(
"postgresql://user:pass@localhost/db",
"SELECT content, metadata FROM documents WHERE active = true"
)
docs = loader.load()
2. 정교한 텍스트 분할 전략
RecursiveCharacterTextSplitter (권장):
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200, # 겹침으로 컨텍스트 보존
separators=["\n\n", "\n", " ", ""] # 우선순위별 구분자
)
TokenTextSplitter (토큰 기반):
from langchain_text_splitters import TokenTextSplitter
# 정확한 토큰 수 기반 분할
splitter = TokenTextSplitter(
encoding_name="gpt2", # 또는 "cl100k_base" for GPT-4
chunk_size=512,
chunk_overlap=50
)
SemanticChunker (의미 기반 분할):
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
semantic_chunker = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile" # 의미 변화 지점에서 분할
)
3. 벡터 스토어 최적화
메타데이터 필터링:
# 문서 저장 시 메타데이터 추가
docs_with_metadata = [
Document(
page_content="내용",
metadata={"source": "document1.pdf", "page": 1, "category": "technical"}
)
]
db = Chroma.from_documents(docs_with_metadata, embeddings)
# 필터 검색
retriever = db.as_retriever(
search_kwargs={"filter": {"category": "technical"}}
)
하이브리드 검색 설정:
# 유사도 + 키워드 검색 조합
retriever = db.as_retriever(
search_type="mmr", # Maximal Marginal Relevance
search_kwargs={
"k": 10,
"lambda_mult": 0.7 # 다양성과 관련성 균형 조절
}
)
4. 성능 최적화 전략
배치 임베딩:
# 한 번에 여러 문서 임베딩 (효율적)
embeddings = OpenAIEmbeddings()
texts = [doc.page_content for doc in docs]
vectors = embeddings.embed_documents(texts) # 배치 처리
인덱스 최적화:
# FAISS 사용 (대용량 데이터에 적합)
from langchain_community.vectorstores import FAISS
db = FAISS.from_documents(docs, embeddings)
db.save_local("faiss_index") # 인덱스 저장
# 로드 시 빠른 시작
db = FAISS.load_local("faiss_index", embeddings)
🔍 학습하면서 알게 된 점
1. 청킹 전략의 실제 영향
잘못된 청킹의 문제:
# 너무 작은 청크 - 컨텍스트 부족
small_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)
# 너무 큰 청크 - 노이즈 증가
large_splitter = CharacterTextSplitter(chunk_size=5000, chunk_overlap=0)
# 최적화된 청킹
optimal_splitter = RecursiveCharacterTextSplitter(
chunk_size=800, # 충분한 컨텍스트
chunk_overlap=100, # 20% 겹침으로 경계 정보 보존
separators=["\n\n", "\n", ". ", " "] # 자연스러운 구분점
)
청킹 품질 평가:
def evaluate_chunks(chunks):
avg_length = sum(len(chunk.page_content) for chunk in chunks) / len(chunks)
print(f"평균 청크 길이: {avg_length}")
print(f"총 청크 수: {len(chunks)}")
# 너무 짧거나 긴 청크 확인
short_chunks = [c for c in chunks if len(c.page_content) < 200]
long_chunks = [c for c in chunks if len(c.page_content) > 2000]
print(f"짧은 청크 수: {len(short_chunks)}")
print(f"긴 청크 수: {len(long_chunks)}")
2. 임베딩 모델 선택의 중요성
모델별 특성:
# OpenAI - 범용성 좋음, 비용 있음
openai_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# Sentence Transformers - 무료, 한국어 지원 좋음
from langchain_community.embeddings import HuggingFaceEmbeddings
huggingface_embeddings = HuggingFaceEmbeddings(
model_name="jhgan/ko-sroberta-multitask"
)
# 모델 성능 비교 필요
def compare_embeddings(text, embedding_models):
for name, model in embedding_models.items():
vector = model.embed_query(text)
print(f"{name}: 벡터 차원 {len(vector)}")
3. 검색 품질 향상 기법
컨텍스트 압축:
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
)
Self-Query 검색:
from langchain.retrievers.self_query.base import SelfQueryRetriever
# 자연어 질문을 메타데이터 필터로 변환
retriever = SelfQueryRetriever.from_llm(
llm,
vectorstore,
document_content_description="기술 문서",
metadata_field_info=[
{"name": "source", "description": "문서 출처", "type": "string"},
{"name": "date", "description": "작성 날짜", "type": "string"}
]
)
4. RAG 파이프라인 평가 및 디버깅
검색 결과 평가:
def debug_retrieval(question, retriever, k=3):
docs = retriever.get_relevant_documents(question)
print(f"질문: {question}")
print(f"검색된 문서 수: {len(docs)}")
for i, doc in enumerate(docs[:k]):
print(f"\n문서 {i+1}:")
print(f"내용: {doc.page_content[:200]}...")
print(f"메타데이터: {doc.metadata}")
답변 품질 모니터링:
def evaluate_rag_response(question, context_docs, answer):
# 컨텍스트 관련성 체크
context_text = "\n".join([doc.page_content for doc in context_docs])
relevance_prompt = f"""
질문: {question}
컨텍스트: {context_text}
답변: {answer}
답변이 컨텍스트를 기반으로 하고 있는지 1-10점으로 평가하세요.
"""
# LLM으로 답변 품질 평가
score = model.invoke(relevance_prompt)
return score
5. 프로덕션 환경에서의 고려사항
벡터DB 선택 기준:
# 소규모 프로토타입 - Chroma
chroma_db = Chroma.from_documents(docs, embeddings)
# 중규모 서비스 - Pinecone
from langchain_pinecone import Pinecone
pinecone_db = Pinecone.from_documents(docs, embeddings, index_name="my-index")
# 대규모 엔터프라이즈 - Weaviate
from langchain_community.vectorstores import Weaviate
weaviate_db = Weaviate.from_documents(docs, embeddings)
캐싱 전략:
from functools import lru_cache
@lru_cache(maxsize=100)
def cached_retrieval(question: str):
return retriever.get_relevant_documents(question)
# Redis 캐싱
import redis
import pickle
redis_client = redis.Redis()
def redis_cached_retrieval(question):
cached = redis_client.get(f"rag:{question}")
if cached:
return pickle.loads(cached)
result = retriever.get_relevant_documents(question)
redis_client.setex(f"rag:{question}", 3600, pickle.dumps(result))
return result
모니터링 메트릭:
- 검색 지연시간
- 답변 생성 시간
- 토큰 사용량
- 검색 정확도 (사용자 피드백 기반)
- 컨텍스트 활용률