RAG 성능이 안 오르는 진짜 이유: HyDE + Reranking + Query Expansion을 “같이” 최적화하는 법 (2026년 3월 기준)
들어가며
프로덕션 RAG에서 “임베딩 모델 바꾸면 좋아지겠지”는 보통 착각입니다. 실패 패턴을 뜯어보면 대개 (1) query–document asymmetry(질문은 짧고 문서는 길다), (2) 1차 검색의 recall 부족, (3) 상위 k 안에서의 precision 부족 세 가지로 귀결됩니다.
2026년 3월 시점에도 업계/연구의 흐름은 크게 변하지 않았고, 이 문제를 가장 실전적으로 푸는 조합이 HyDE로 recall을 끌어올리고 → Reranking으로 precision을 고정 → Query Expansion으로 커버리지를 넓히는 3단 구조입니다. HyDE는 원래 “relevance label 없이도” zero-shot dense retrieval을 강하게 만드는 아이디어로 제안됐고, 실제 RAG 최적화 가이드들에서도 “HyDE + reranker”를 강력 추천하는 패턴이 반복됩니다. (arxiv.org)
🔧 핵심 개념
1) HyDE (Hypothetical Document Embeddings)
정의: 유저 query를 그대로 embed해서 검색하지 않고, LLM으로 “이 질문에 대한 이상적인 답변/문서(가짜 문서)”를 먼저 생성한 뒤 그 텍스트를 embedding해서 검색하는 기법입니다. (arxiv.org)
왜 통하나(원리):
- query는 짧고 정보가 덜 들어있어서 embedding 공간에서 “어떤 문서랑 가까워야 하는지” 신호가 약합니다.
- HyDE는 답변 형태의 텍스트(=문서 스타일) 를 만들어 embedding 입력을 문서 분포에 맞춥니다. 즉, embedding 모델이 잘하는 형태로 query를 변환해 semantic gap을 줄입니다. (emergentmind.com)
리스크: HyDE가 “그럴듯한 헛소리”를 생성하면 엉뚱한 방향으로 검색이 당겨질 수 있어, 후단에서 reranker로 post-validate 하는 게 사실상 세트입니다. (emergentmind.com)
2) Reranking (Cross-Encoder / Late Interaction)
정의: 1차 retrieval(topN)은 빠르게 “후보를 많이” 뽑고, reranker가 query–doc을 jointly encode해서 더 정확히 점수화한 뒤 topK를 재정렬합니다. BGE 문서도 reranker를 embedding 모델과 구분해 “직접 relevance를 출력”한다고 정리합니다. (bge-model.com)
어떤 계열이 있나:
- Cross-Encoder: 정확하지만 비용이 큼(후보 N이 늘수록 선형으로 느려짐).
- Late interaction(예: ColBERT): 후보를 많이 다루면서도 효율/정확 타협점. (arxiv.org)
3) Query Expansion / Multi-Query
정의: 원 query를 여러 변형으로 확장(동의어, 하위 질문, 관점 분해)해 검색 커버리지를 넓히는 전략. 최근 RAG 계열 논문들도 “query rewriting/augmentation”을 별도 레이어로 다루고, 다양성 있는 multi-query가 retrieval과 최종 품질을 올린다고 보고합니다. (aclanthology.org)
핵심 포인트: expansion은 recall을 올리지만 노이즈도 같이 올립니다. 그래서 실전에서는
- expansion으로 후보 pool을 키우고
- reranker로 precision을 다시 잠그는 구조가 안정적입니다.
💻 실전 코드
아래 코드는 “Multi-Query + HyDE + (Vector Search) + Cross-Encoder Rerank”의 뼈대입니다.
외부 벡터DB 대신 로컬에서 faiss를 쓰고, reranker는 HuggingFace cross-encoder 계열(예: BGE reranker)을 붙이는 형태로 작성했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# pip install -U sentence-transformers faiss-cpu numpy
from sentence_transformers import SentenceTransformer, CrossEncoder
import faiss
import numpy as np
# ---------------------------
# 1) 준비: 코퍼스(문서 chunk)와 인덱스 구축
# ---------------------------
docs = [
"HyDE는 LLM이 생성한 hypothetical document를 embed해서 dense retrieval 성능을 개선한다.",
"Cross-encoder reranker는 query와 document를 함께 넣고 직접 relevance score를 출력한다.",
"Multi-query rewriting은 서로 다른 관점의 query를 만들어 recall을 높이지만 노이즈도 늘린다.",
"ColBERT는 late interaction을 사용해 효율적인 passage search를 제공한다.",
"RAG 최적화는 retrieve-many-then-rerank가 기본 패턴이다."
]
embed_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
doc_emb = embed_model.encode(docs, normalize_embeddings=True).astype("float32")
dim = doc_emb.shape[1]
index = faiss.IndexFlatIP(dim) # cosine 유사도는 normalize + inner product로 처리
index.add(doc_emb)
# ---------------------------
# 2) Query Expansion (간단 버전)
# - 실전에서는 LLM으로 N개 생성 + 다양성 제약(중복 제거) 권장
# ---------------------------
def expand_queries(q: str) -> list[str]:
# 데모용: 규칙 기반. 실제로는 LLM prompt로 "서로 다른 관점 3~8개" 생성 추천
return [
q,
f"{q} 성능 병목 원인",
f"{q} reranking 적용 방법",
f"{q} HyDE query rewriting 비교"
]
# ---------------------------
# 3) HyDE (데모용)
# - 실전에서는 LLM 호출로 "가짜 문서" 생성
# ---------------------------
def hyde_generate(q: str) -> str:
# NOTE: 여기서는 LLM 대신 템플릿. 실제로는:
# - "답변을 단정하지 말고 키워드/정의/절차 위주로 기술"
# - 길이 제한(예: 120~200 tokens)
return (
f"이 문서는 '{q}'에 대한 기술적 설명이다. "
"핵심은 retrieval recall을 늘리고, reranker로 precision을 고정하며, "
"query expansion으로 검색 커버리지를 넓히는 것이다."
)
# ---------------------------
# 4) 1차 Retrieval: Multi-query + HyDE 임베딩으로 후보 풀 수집
# ---------------------------
def retrieve_candidates(user_query: str, top_n: int = 5) -> list[tuple[int, float]]:
candidates = {}
for q in expand_queries(user_query):
hyde_doc = hyde_generate(q)
q_emb = embed_model.encode([hyde_doc], normalize_embeddings=True).astype("float32")
scores, ids = index.search(q_emb, top_n)
for doc_id, score in zip(ids[0], scores[0]):
# Rank-fusion의 아주 단순 버전: 최대 score로 통합
candidates[doc_id] = max(candidates.get(doc_id, -1e9), float(score))
# 후보를 score로 정렬
return sorted(candidates.items(), key=lambda x: x[1], reverse=True)
# ---------------------------
# 5) Reranking: Cross-Encoder로 최종 정렬
# - BGE reranker 등으로 교체 가능
# ---------------------------
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
def rerank(user_query: str, cand_ids: list[int], top_k: int = 3):
pairs = [(user_query, docs[i]) for i in cand_ids]
ce_scores = reranker.predict(pairs)
ranked = sorted(zip(cand_ids, ce_scores), key=lambda x: x[1], reverse=True)
return ranked[:top_k]
# ---------------------------
# 6) 실행
# ---------------------------
q = "RAG에서 HyDE와 reranking을 같이 쓰는 최적 패턴"
cands = retrieve_candidates(q, top_n=4)
cand_ids = [doc_id for doc_id, _ in cands][:8] # 후보 풀 크기 조절
final_top = rerank(q, cand_ids, top_k=3)
print("== Candidates ==")
for doc_id, s in cands:
print(doc_id, round(s, 3), docs[doc_id])
print("\n== Reranked TopK ==")
for doc_id, s in final_top:
print(doc_id, round(float(s), 3), docs[doc_id])
⚡ 실전 팁
- HyDE는 “항상 on”이 아니라 “조건부 on”이 효율적입니다. 예: 1차 retrieval score 분포가 평평하거나, top1~topk가 특정 임계치 아래면 HyDE를 켜는 식(비용/지연 최적화). (emergentmind.com)
- Retrieve-many-then-rerank의 N/K를 분리하세요. 경험적으로
topN=30~200(cheap) →rerank topK=5~10(expensive) 구조가 튜닝하기 좋습니다. (maxpool.dev) - Query Expansion은 “다양성”이 핵심입니다. 동의어만 늘리면 중복만 증가합니다. 관점(정의/비교/절차/제약/실패 사례)을 강제로 분해해 multi-query를 만들고, 결과는 RRF 같은 rank fusion으로 합치는 패턴이 안정적입니다(논문/실무 모두 이 방향). (arxiv.org)
- Reranker 선택: 다국어/한글이 섞이면 multilingual cross-encoder 계열(BGE reranker 등)을 후보로 두고, latency 예산이 작으면 작은 cross-encoder부터 시작하세요. BGE는 reranker를 “embedding이 아닌 direct scoring”으로 명확히 구분해 문서화합니다. (bge-model.com)
- HyDE 환각 방지 프롬프트: “사실 단정 금지, 키워드/정의/체크리스트 위주, 길이 제한”을 걸면 drift가 줄고 reranker의 부담도 감소합니다(특히 사내 문서/정책 검색).
🚀 마무리
HyDE, Reranking, Query Expansion은 각각 “좋은 트릭”이 아니라 서로의 부작용을 상쇄하는 역할 분담입니다.
- HyDE: query–document asymmetry를 줄여 recall을 끌어올림
- Query Expansion: 검색 관점을 넓혀 coverage를 확장
- Reranking: 늘어난 후보/노이즈를 정리해 precision을 고정
다음 단계로는 (1) 조건부 HyDE 라우팅, (2) fusion(RRF) 정교화, (3) 도메인별 hard negative로 reranker 미세조정, (4) multi-hop query rewriting(계획 기반) 같은 방향을 추천합니다. (arxiv.org)