핵심 요약
RAG 파이프라인은 문서 → 청킹 → 임베딩 → 저장 → 검색 → 리랭킹 → LLM 생성 7단계다. 각 단계의 선택이 품질에 누적 영향을 준다.
1. 청킹 전략
- 고정 크기(500~1000 토큰): 단순, 오버랩 10~20%
- 문장 경계: 의미 단위 유지
- 계층적: 섹션 요약 + 하위 청크 (대형 문서)
- 코드/표: 구조 경계 존중 (함수 단위, 표 전체)
# LangChain 예시
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=800, chunk_overlap=120,
separators=["\n## ", "\n\n", "\n", ". "]
)
2. 임베딩 모델 선택
| 모델 | 차원 | 언어 | 비고 |
|---|---|---|---|
| OpenAI text-embedding-3-large | 3072 | 다국어 | 고품질, 유료 |
| Cohere embed-v3 | 1024 | 다국어 | 리랭커와 세트 |
| BGE-M3 | 1024 | 다국어 | OSS, 한국어 우수 |
| ko-sroberta | 768 | 한국어 | 경량 |
3. 벡터 DB 선택
- Redis 8: 수백만 벡터까지 충분, 운영 친숙
- pgvector: Postgres 단일 스택 유지
- Qdrant: 오픈소스, 페이로드 필터 강력
- Pinecone: 매니지드, 대규모·다중 테넌트
- Weaviate: 하이브리드(BM25 + vector) 기본
4. 하이브리드 검색
벡터 단독보다 BM25 + 벡터를 RRF(Reciprocal Rank Fusion)로 합치는 편이 품질이 좋다. 특히 고유명사·약어 쿼리에서 차이가 크다.
# 의사 코드
scores = {}
for rank, doc in enumerate(bm25_results):
scores[doc.id] = scores.get(doc.id, 0) + 1 / (60 + rank)
for rank, doc in enumerate(vector_results):
scores[doc.id] = scores.get(doc.id, 0) + 1 / (60 + rank)
top = sorted(scores.items(), key=lambda x: -x[1])[:10]
5. 리랭킹
검색 top-50을 가져와 크로스 인코더로 top-5를 재정렬. Cohere Rerank, BGE-Reranker가 무난.
6. 프롬프트 설계
system: "제공된 컨텍스트 안에서만 답변. 근거가 없으면 '모른다'고 답."
user: "컨텍스트:
[1] ...
[2] ...
질문: ..."
답변에 반드시 [번호] 인용을 요구하면 환각이 줄어든다.
7. 평가
- Retrieval 지표: Recall@k, MRR
- Generation 지표: Faithfulness, Answer Relevance
- 도구: Ragas, TruLens, LangSmith 평가셋
실무 함정
- 청크 크기만 바꾸면 절반 이상 문제 해결되는 경우 많음
- 메타데이터 필터(날짜·문서타입) 적극 활용
- 한국어는 임베딩 차원 + 리랭커 유무가 품질에 결정적
자주 묻는 질문
파인튜닝 vs RAG?
지식 주입은 RAG가 빠르고 저렴하다. 스타일·형식 학습은 파인튜닝이 유리. 대부분 RAG 먼저, 필요시 병행.
컨텍스트 길이가 길어져도 괜찮나?
관련도 낮은 청크가 끼면 오히려 성능이 떨어진다(Lost in the Middle). top-k를 5~8로 제한.
댓글 0