RAG 파이프라인 개선의 핵심
Retrieval-Augmented Generation(RAG)의 품질은 검색 정확도에 크게 좌우됩니다. 문서를 어떻게 쪼개고(청킹), 검색 결과를 어떻게 재정렬하느냐(리랭킹)가 최종 답변 품질을 결정합니다.
청킹 전략 비교
| 전략 | 방법 | 장점 | 단점 |
|---|---|---|---|
| 고정 크기 | 토큰/글자 수 기준 | 단순, 균일 | 의미 단위 파괴 |
| 문단 기반 | 줄바꿈 구분 | 자연스러운 단위 | 크기 불균일 |
| 재귀 분할 | 계층적 구분자 | 유연한 크기 조절 | 구현 복잡 |
| 시맨틱 | 임베딩 유사도 | 의미 보존 최적 | 처리 비용 높음 |
LangChain 청킹 구현
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 재귀 분할 — 가장 범용적
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""],
length_function=len,
)
chunks = splitter.split_text(document_text)
# 시맨틱 청킹 — 의미 단위 보존
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
semantic_splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95,
)
semantic_chunks = semantic_splitter.split_text(document_text)
청킹 최적화 팁
# 메타데이터 보존 청킹
def chunk_with_metadata(doc, chunk_size=500, overlap=50):
chunks = []
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=overlap,
)
for i, chunk in enumerate(splitter.split_text(doc['content'])):
chunks.append({
'text': chunk,
'metadata': {
'source': doc['source'],
'title': doc['title'],
'chunk_index': i,
'total_chunks': None,
}
})
for c in chunks:
c['metadata']['total_chunks'] = len(chunks)
return chunks
# 부모-자식 청킹 (Parent Document Retriever)
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=InMemoryStore(),
parent_splitter=parent_splitter,
child_splitter=child_splitter,
)
리랭킹 구현
# Cross-Encoder 리랭킹
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
def rerank_results(query, documents, top_k=5):
pairs = [(query, doc['text']) for doc in documents]
scores = reranker.predict(pairs)
ranked = sorted(
zip(documents, scores),
key=lambda x: x[1],
reverse=True
)
return [doc for doc, score in ranked[:top_k]]
# Cohere Rerank API 활용
import cohere
co = cohere.Client('your-api-key')
results = co.rerank(
model='rerank-multilingual-v3.0',
query=query,
documents=[doc['text'] for doc in candidates],
top_n=5,
)
최적 파이프라인 구성
- 청크 크기: 300-500 토큰이 일반적 최적 범위, 도메인에 따라 조절
- 오버랩: 청크 크기의 10-20%가 적절 (문맥 연결 유지)
- 하이브리드 검색: BM25(키워드) + 벡터 검색 결합으로 recall 향상
- 리랭킹 배치: 초기 검색 20-50개 → 리랭킹 → 상위 3-5개를 LLM에 전달
- 평가: RAGAS 프레임워크로 faithfulness, relevancy 측정
RAG 품질 개선은 단일 기법이 아닌 파이프라인 전체의 최적화입니다. 청킹 → 임베딩 → 검색 → 리랭킹 각 단계를 독립적으로 실험하고 측정하여 최적 조합을 찾아야 합니다.
댓글 0