Retrieval이 RAG 품질의 상한을 결정한다
- 년, RAG 프로젝트에서 '생성 모델을 GPT-4에서 Claude Opus 4.7로 바꾸었더니 갑자기 똑똑해졌다'는 경우는 드뭅니다. 대부분의 과제는 Retrieval 단계에서 필요한 정보를 가져오지 못하거나, 노이즈를 가져와 LLM을 혼란시키는 것입니다. KGA에서 과거 18개월간 지원한 RAG 프로젝트를 되돌아보면, Retrieval 개선만으로 사용자 체감 점수가 30~60% 향상된 반면, LLM 모델 변경으로 인한 향상은 평균 8% 수준이었습니다.
본고는 2026년 시점의 RAG Retrieval 정석을 5개의 레이어로 정리합니다: (1) 하이브리드 검색, (2) 재순위 지정(reranker), (3) 쿼리 재작성, (4) 청크 전략, (5) 평가 지표.
레이어 1: Hybrid Search (BM25 + Dense)
단일 dense 검색은 형번('ZX-450B'), 약어('TLS1.3'), 인명, 고유한 수치 조건에서 자주 실패합니다. BM25는 반대로 의미론적 바꾸어 쓰기에 약합니다. 이 두 가지를 Reciprocal Rank Fusion (RRF) 으로 융합하는 것이 업계 표준이 되었습니다.
RRF는 매우 단순합니다: 각 검색 결과의 순위를 `1 / (k + rank)`으로 변환하여 합산합니다(k=60이 기본값). 점수의 스케일 차이를 신경 쓰지 않고 융합할 수 있어 다른 모델 간의 조합에 만능으로 효과적입니다.
```python from collections import defaultdict
def rrf_fusion(ranked_lists: list[list[str]], k: int = 60) -> list[tuple[str, float]]: scores = defaultdict(float) for results in ranked_lists: for rank, doc_id in enumerate(results, start=1): scores[doc_id] += 1.0 / (k + rank) return sorted(scores.items(), key=lambda x: -x[1])
bm25_top = bm25_index.search(query, k=50) dense_top = vector_db.search(query_embed, k=50) fused = rrf_fusion([bm25_top, dense_top])[:20] ```
KGA의 법무 검색 프로젝트에서는 BM25 단독 nDCG@10=0.52, Dense 단독 0.61, RRF 하이브리드로 0.73까지 향상되었습니다. 단순하지만 강력합니다.
레이어 2: Reranker(Cohere Rerank-3, Voyage Rerank-2)
하이브리드 검색으로 상위 50~200개를 가져온 후, 크로스 인코더형 재순위 지정기로 정밀하게 점수를 매기는 2단계 설계가 2026년의 사실상 표준입니다. Dense 검색은 쿼리와 문서를 독립적으로 벡터화하는 bi-encoder 구조로, 쿼리-문서 간의 상호작용을 잃습니다. 재순위 지정기는 양쪽을 동시에 Transformer에 통과시켜 진정한 관련도를 출력합니다.
- Cohere Rerank-3 (2025/09 출시): 100개 언어 지원, 128k 컨텍스트, $2/1k queries
- Voyage Rerank-2: 32k 컨텍스트, MTEB Reranking 최상위급, $0.5/1k queries
- Jina Reranker v2: 오픈 소스 지향 옵션, 일본어 성능은 Cohere에 한발 뒤짐
- BGE-Reranker-v2-m3: MIT 라이선스 오픈 소스, 자체 호스팅 운용의 첫 번째 선택지
```python import cohere
co = cohere.Client() results = co.rerank( model="rerank-3", query="데이터 보호법에서의 국경 간 이전 요건", documents=[d.text for d in top50], top_n=10, ) reranked = [top50[r.index] for r in results.results] ```
재순위 지정기의 효과는 극적으로, KGA에서의 평균으로는 nDCG@10이 0.65 → 0.82 수준까지 뛰어오릅니다. 다만 레이턴시는 상위 50개에서 +200~400ms가 추가되므로, 검색 UX에서는 비동기화나 1단계만으로 빠른 응답을 하는 설계가 필요해집니다.
레이어 3: 쿼리 재작성 (HyDE, Query2Doc, Multi-Query)
사용자의 쿼리는 짧고 모호한 경우가 많습니다('계약 해제', '메모리 사용률'). 이것을 LLM으로 확장한 후 검색하는 기법이 2026년의 Retrieval 설계에 통합되었습니다.
- HyDE (Hypothetical Document Embeddings): LLM에 가상적인 답변 문서를 작성하게 하고, 그 임베딩을 검색 키로 사용
- Query2Doc: 쿼리와 가상 문서를 결합하여 임베딩을 취함(HyDE의 변형, 많은 경우 더 안정적)
- Multi-Query: LLM에 같은 의도의 다른 표현을 3~5개 생성하게 하여 병렬 검색 후 RRF 융합
```python def hyde_search(query: str, vector_db, llm): hypothetical = llm.generate( f"다음 질문에 대한 가상의 답변을 3문장으로 작성해 주세요: {query}" ) combined = query + " " + hypothetical # Query2Doc화 embed = embedding_model.encode(combined) return vector_db.search(embed, k=50) ```
Multi-Query가 가장 가성비가 좋으며, KGA의 사내 FAQ RAG에서는 단일 쿼리 검색 대비 Recall@20이 12% 향상되었습니다. 비용은 1쿼리당 LLM 호출이 1회 늘어나지만, Claude Haiku / GPT-4.1-mini로 실행하면 0.1엔 미만입니다.
레이어 4: 청크 전략
'1,000자로 분할'만으로 운용하는 RAG는 2026년 시점에서 시대에 뒤떨어진 것입니다. 청크 방식이 그대로 Retrieval 품질을 결정합니다.
- Fixed-size: 고전적인 N자 분할. 오버랩은 10~20%
- Recursive Character Splitting: 단락 → 문장 → 어절 순으로 재귀적으로 구분. LangChain `RecursiveCharacterTextSplitter`의 기본값
- Semantic Chunking: 연속 문장을 임베딩하여 코사인 유사도의 급변점에서 구분. 의미론적으로 응집된 청크를 만듦
- Late Chunking (Jina 2024/2025): 장문 전체를 먼저 임베딩 모델에 통과시키고, 토큰별 임베딩을 취한 후 평균화하여 청크를 만듦. 청크가 주변 문맥을 보유하므로 대명사나 조응을 잃지 않음
- Hierarchical / Parent-Child: 소 청크로 검색하고, 부모 청크로 생성에 넘김. 정확도와 문맥량을 양립
Late Chunking의 구현 예:
```python from transformers import AutoModel, AutoTokenizer import torch
model = AutoModel.from_pretrained("jinaai/jina-embeddings-v3", trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained("jinaai/jina-embeddings-v3")
def late_chunking(long_text: str, chunk_boundaries: list[tuple[int, int]]): inputs = tokenizer(long_text, return_tensors="pt", truncation=True, max_length=8192) with torch.no_grad(): token_embeds = model(**inputs).last_hidden_state[0] chunk_embeds = [] for start, end in chunk_boundaries: chunk_embeds.append(token_embeds[start:end].mean(dim=0)) return torch.stack(chunk_embeds) ```
KGA에서 계약서 RAG에 Late Chunking을 도입한 결과, '제3조에 규정된'과 같은 상호 참조가 많은 문서에서 Recall@10이 71% → 88%까지 향상되었습니다.
레이어 5: 평가 지표와 오프라인 평가
Retrieval은 '동작했다'만으로는 평가할 수 없습니다. 정량 지표와 회귀 테스트가 필수입니다.
- Recall@k: 정답 문서가 상위 k개에 포함되는 비율. RAG의 상한을 결정하는 가장 중요한 지표
- nDCG@10 (Normalized Discounted Cumulative Gain): 순위의 질까지 고려. 상업 검색의 사실상 표준
- MRR (Mean Reciprocal Rank): 첫 번째 정답까지의 순위의 역수 평균. 사용자 체감에 가까움
- Hit Rate@k: 단순한 '상위 k개에 정답이 있었는가'의 이진값
평가 세트는 최소 100개 쿼리, 이상적으로는 500개 쿼리로 만듭니다. 쿼리와 정답 문서 ID의 쌍을 CSV로 보유하고, CI에서 회귀 테스트를 실행합니다.
```python import numpy as np
def ndcg_at_k(ranked_ids: list[str], relevance: dict[str, int], k: int = 10) -> float: gains = [relevance.get(doc_id, 0) for doc_id in ranked_ids[:k]] dcg = sum((2 g - 1) / np.log2(idx + 2) for idx, g in enumerate(gains)) ideal_gains = sorted(relevance.values(), reverse=True)[:k] idcg = sum((2 g - 1) / np.log2(idx + 2) for idx, g in enumerate(ideal_gains)) return dcg / idcg if idcg > 0 else 0.0 ```
LlamaIndex의 `RetrieverEvaluator`, RAGAS, Trulens 등의 프레임워크가 이러한 계산을 표준화하고 있으므로, 직접 구현하기보다 먼저 RAGAS를 설치하는 것이 2026년의 정석입니다.
실서비스 참조 구성
KGA가 2026년에 제안하는 실서비스 RAG의 표준 구성은 다음과 같습니다.
- Ingest: 문서 → Late Chunking(Jina v3) → 부모/자식 저장
- Index: pgvector 또는 Qdrant에 dense + sparse 양쪽 저장
- Query: Multi-Query(LLM으로 3가지 변형 생성)
- 1단계: 하이브리드 BM25 + Dense → RRF 융합으로 상위 100개
- 2단계: Cohere Rerank-3으로 상위 10개로 압축
- Generation: 상위 10개를 Claude Opus 4.7에 넘겨 최종 답변 생성
- Eval: 500개 쿼리의 회귀 테스트를 매주 CI에서 실행하여 nDCG@10 / MRR / Recall@20의 임계값 하락을 차단
이 구성을 엄밀하게 구성하면, 'LLM을 바꾸지 않아도 RAG가 똑똑해지는' 경험이 실제로 나타납니다. Retrieval이야말로 RAG의 핵심이며, 2026년 프로젝트 투자의 최우선 레이어입니다.