- Published on
RAG 환각 줄이기 - 하이브리드 검색+Rerank 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
RAG에서 환각을 줄이려면 모델을 “더 똑똑하게” 만들기보다, 모델이 참고할 근거를 더 정확히 제공하는 편이 훨씬 비용 대비 효과가 큽니다. 특히 실무에서 보이는 환각의 상당수는 LLM 자체의 문제가 아니라 아래 두 가지에서 시작합니다.
- 못 찾음: 질문에 필요한 근거 문서를 검색 단계에서 가져오지 못함
- 잘못 올림: 가져오긴 했지만, 상위 컨텍스트에 엉뚱한 문서를 올려 모델이 그걸 근거로 답함
이 글은 이 두 문제를 동시에 겨냥하는 하이브리드 검색(lexical+vector) 과 rerank(재정렬) 를 어떻게 튜닝하면 환각이 줄어드는지, 그리고 어떤 지표로 개선을 확인할지 정리합니다.
아래 글도 함께 보면 RAG 운영 품질을 더 빨리 끌어올릴 수 있습니다.
- 벡터 품질이 시간이 지나며 나빠지는 문제: Rust+Qdrant RAG에서 벡터 드리프트 잡는 법
- 스키마 기반으로 모델의 “헛소리 출력”을 제어하는 방법: CoT 프롬프트 유출 막기 - JSON 스키마+툴콜
- 검색/리랭크/LLM 호출이 느릴 때 병목 찾기: PostgreSQL 쿼리 느림? auto_explain으로 추적
환각을 “검색 문제”로 재정의하기
환각을 줄이기 위한 실전 프레임은 간단합니다.
- Recall을 올린다: 정답 문서가 후보군에 들어오게 만든다
- Precision을 올린다: 후보군에서 정답 문서를 상위로 올린다
- Context를 절제한다: 상위 몇 개만, 필요한 부분만 LLM에 준다
하이브리드 검색은 1번을, rerank는 2번을, 컨텍스트 구성은 3번을 주로 담당합니다.
하이브리드 검색이 필요한 이유
벡터 검색만 쓰면 의미적으로 비슷한 문서를 잘 찾지만, 다음 케이스에서 자주 미끄러집니다.
- 고유명사/코드/에러코드/약어: 예를 들어
EKS InvalidIdentityToken같은 문자열은 lexical이 강함 - 정확한 키워드 매칭이 중요한 정책/규정: “반드시”, “금지” 같은 문구가 의미를 바꿈
- 질문이 짧고 애매한 경우: 벡터가 넓게 퍼져 엉뚱한 문서가 섞임
반대로 BM25 같은 lexical 검색만 쓰면 동의어, 표현 차이, 문장 구조가 바뀐 질문에서 recall이 떨어집니다.
그래서 실무 RAG는 보통 아래 구조가 안정적입니다.
- 1차: 하이브리드로 후보를 넓게 가져오기
- 2차: rerank로 상위를 정확히 정렬하기
- 3차: 상위
k개만 컨텍스트로 구성
하이브리드 검색 설계: 후보군을 “넓고 안전하게”
하이브리드 검색의 핵심은 “벡터로 넓게, 키워드로 정확히”를 섞되, 합치는 방식이 환각에 직결된다는 점입니다.
합치는 방법 3가지
1) 단순 합집합(Union)
- 벡터 top
N+ BM25 topM을 합쳐 rerank에 넘김 - 구현이 쉽고, rerank가 강하면 이 방식이 가장 무난합니다.
2) 점수 가중 합(Weighted sum)
score = alpha * bm25 + (1 - alpha) * vector- 검색 단계에서 이미 순위를 만들기 때문에, rerank 비용을 줄이고 싶을 때 유리
- 단점: 점수 스케일 정규화가 필요합니다.
3) RRF(Reciprocal Rank Fusion)
- 서로 다른 랭킹을 “순위 기반”으로 합쳐 스케일 문제를 피합니다.
- 실무에서 튜닝 난이도 대비 성능이 좋아 자주 씁니다.
RRF 예시는 아래처럼 구현합니다.
def rrf_fusion(rankings, k=60):
"""rankings: list[list[doc_id]] where each list is ordered best->worst"""
scores = {}
for ranking in rankings:
for i, doc_id in enumerate(ranking):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + i + 1)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
후보군 크기: rerank를 전제로 잡기
환각을 줄이는 목적이라면 후보군을 너무 줄이면 안 됩니다.
- 1차 후보군: 보통
50~200에서 시작 - rerank 입력: 보통
20~100사이에서 비용과 성능 타협
중요한 건 “top k 컨텍스트”가 아니라 rerank 전 후보군에서 정답 문서가 살아남는지 입니다.
Rerank 튜닝: 환각을 줄이는 가장 직접적인 레버
rerank는 “질문과 문서의 관련도”를 더 정교하게 평가해 순서를 재정렬합니다. 하이브리드가 recall을 올려도, 상위에 오답 문서가 올라오면 LLM은 그걸 근거로 답을 만들어 환각이 발생합니다.
rerank 모델 선택 기준
- Cross-encoder 계열: 정확도 좋지만 느림. 상위 후보
50개 정도에 적용하는 식으로 사용 - LLM rerank: 품질은 좋을 수 있으나 비용과 지연이 커서 운영 난이도가 올라감
대부분의 팀은 “하이브리드로 넓게 가져온 뒤 cross-encoder rerank” 조합을 기본으로 깔고, 특정 도메인에서만 LLM rerank를 제한적으로 씁니다.
rerank 입력 문서 길이 제한이 핵심
rerank는 입력 길이에 민감합니다. 문서 chunk가 너무 길면:
- 질문과 무관한 부분이 섞여 점수가 흐려짐
- rerank 모델의 최대 토큰 제한에 걸려 뒤가 잘림
따라서 rerank에는 “문서 전체”가 아니라 질문과 가까운 창(window) 을 넣는 전략이 효과적입니다.
- chunk 자체를 짧게(예:
300~800토큰) - 또는 문서에서 질의 키워드 주변 문장만 추출
실전 파이프라인 예시(하이브리드 + rerank)
아래는 개념을 보여주는 간단한 형태입니다.
from dataclasses import dataclass
@dataclass
class Doc:
id: str
title: str
text: str
bm25_score: float = 0.0
vec_score: float = 0.0
rerank_score: float = 0.0
def hybrid_candidates(query: str, bm25_index, vector_index, n_bm25=50, n_vec=50):
bm25_hits = bm25_index.search(query, top_k=n_bm25) # list[Doc]
vec_hits = vector_index.search(query, top_k=n_vec) # list[Doc]
# union by id
merged = {}
for d in bm25_hits:
merged[d.id] = d
for d in vec_hits:
if d.id in merged:
merged[d.id].vec_score = d.vec_score
else:
merged[d.id] = d
return list(merged.values())
def rerank(query: str, docs: list[Doc], reranker, top_k=10):
pairs = [(query, d.text) for d in docs]
scores = reranker.score(pairs) # list[float]
for d, s in zip(docs, scores):
d.rerank_score = s
docs.sort(key=lambda x: x.rerank_score, reverse=True)
return docs[:top_k]
def build_context(docs: list[Doc]):
# 컨텍스트는 짧고 명확하게, 출처 식별자를 포함
parts = []
for i, d in enumerate(docs, start=1):
parts.append(f"[source:{i} id={d.id} title={d.title}]\n{d.text}")
return "\n\n".join(parts)
여기서 환각을 줄이는 포인트는 build_context 단계에서 출처 라벨을 강제로 넣는 것입니다. 모델이 답변에 근거를 붙이도록 유도할 수 있고, 사후 디버깅도 쉬워집니다.
튜닝 체크리스트: 무엇을 바꾸면 환각이 줄어드나
환각 감소를 목표로 할 때는 “정확도”를 추상적으로 보지 말고, 아래 레버를 순서대로 만져보는 편이 빠릅니다.
1) chunk 전략부터 재점검
- 너무 긴 chunk는 rerank 품질을 깎고, 컨텍스트에 노이즈를 넣습니다.
- 너무 짧은 chunk는 문맥이 끊겨 답을 만들 근거가 부족해집니다.
권장 시작점:
- 문서 성격이 텍스트 위주면
400~800토큰 - 표/정책/FAQ면 더 짧게 쪼개고(예:
200~400), 메타데이터를 풍부하게
2) 하이브리드 가중치 또는 RRF 파라미터
- weighted sum이면
alpha를 조정합니다. - RRF면
k를 조정합니다.k가 작을수록 상위 순위의 영향이 커집니다.
튜닝 방법은 간단히 시작할 수 있습니다.
- 질문 세트
100개 정도를 만들고 - 정답 문서 id를 라벨링한 뒤
Recall@50,MRR@10,NDCG@10을 비교합니다.
3) rerank 후보 수를 늘리고, 최종 컨텍스트 수는 줄이기
환각은 “상위 컨텍스트가 오염”될 때 크게 늘어납니다.
- rerank 입력 후보:
20에서50으로 늘리면 정답이 상위로 올라올 확률이 증가 - 최종 컨텍스트:
8에서4로 줄이면 노이즈가 줄어듭니다.
즉, 앞단은 넓게, 뒷단은 좁게 가 기본 원칙입니다.
4) 메타데이터 필터와 부스팅
의외로 환각을 크게 줄이는 방법이 “검색 공간 자체를 줄이는 것”입니다.
예:
- 제품 버전(예:
v1,v2) - 지역/리전
- 문서 타입(런북, 정책, 릴리즈 노트)
- 최신성
질문에서 버전을 파싱해 필터링하면, 구버전 문서를 근거로 답하는 환각이 급감합니다.
평가: “정답을 맞췄나”가 아니라 “근거를 찾았나”
RAG는 생성 모델이 포함되어 있어 최종 답변 정확도만 보면 원인 분리가 어렵습니다. 환각을 줄이는 튜닝에는 검색 평가 지표가 더 직접적입니다.
추천 지표:
Recall@K: 정답 문서가 후보 topK에 들어왔는가MRR@K: 정답 문서가 얼마나 위에 있었는가Context precision: 최종 컨텍스트에 포함된 문서 중 실제로 근거가 되는 비율
그리고 운영에서는 아래 로그가 중요합니다.
- 질의, top 후보 목록, rerank 점수, 최종 컨텍스트 id
- 답변에 인용된
source라벨
이 로그가 있어야 “검색이 문제인지, rerank가 문제인지, 컨텍스트가 문제인지”를 분리할 수 있습니다.
프롬프트/출력 제약으로 환각을 추가로 누르기
검색과 rerank를 튜닝해도, 모델이 문장으로 매끄럽게 “추론”하면서 근거 밖 내용을 섞는 경우가 있습니다. 이때는 출력 제약이 유효합니다.
- 답변은 반드시
source를 인용하게 하기 - 근거가 없으면
모르겠다로 답하게 하기 - 구조화 출력(JSON)로 강제하기
특히 JSON 스키마 기반 출력은 환각을 “형태” 차원에서 줄이는 데 도움이 됩니다. 관련 접근은 CoT 프롬프트 유출 막기 - JSON 스키마+툴콜에서 더 자세히 다룹니다.
간단한 예시는 아래처럼 구성할 수 있습니다.
시스템 규칙:
- 제공된 sources에 있는 내용만 사용해 답한다.
- 답변에는 반드시 source 번호를 포함한다.
- sources로부터 답을 만들 수 없으면 "insufficient_evidence"를 반환한다.
출력(JSON):
{
"status": "ok" | "insufficient_evidence",
"answer": string,
"citations": ["source:1", "source:3"]
}
여기서도 | 문자가 싫다면 문서 스타일에 맞게 바꿔도 됩니다. 중요한 건 “근거 없으면 중단”을 시스템 규칙으로 못 박는 것입니다.
운영 팁: 성능과 비용을 같이 잡기
하이브리드+rerank는 환각을 줄이지만 비용과 지연을 올릴 수 있습니다. 아래 순서로 최적화하면 안전합니다.
- 캐싱: 동일/유사 질의 캐시(정규화, 토큰화 기반)
- rerank 대상 축소: 하이브리드 결과에서 중복 제거, 메타데이터 필터
- 2단 rerank: cheap rerank로
100개를30개로 줄이고, strong rerank로 top10
또한 데이터베이스나 검색 인덱스가 느리면 전체 파이프라인이 흔들립니다. 병목이 DB라면 auto_explain 같은 도구로 원인을 좁히는 접근이 유용합니다. 자세한 방법은 PostgreSQL 쿼리 느림? auto_explain으로 추적을 참고하세요.
정리: 환각을 줄이는 가장 현실적인 조합
- 하이브리드 검색으로 정답 문서가 후보군에 들어올 확률(Recall) 을 올린다
- rerank로 정답 문서를 상위에 올릴 확률(MRR, NDCG) 을 올린다
- 최종 컨텍스트는 짧고 정확하게, 출처 라벨을 포함한다
- “근거 없으면 중단”을 프롬프트와 스키마로 강제한다
이 조합은 모델을 바꾸지 않고도 환각을 눈에 띄게 줄일 수 있고, 문제 발생 시에도 로그로 원인 분리가 쉬워 운영 난이도가 내려갑니다. 다음 단계로는 도메인별 하드 케이스(고유명사, 버전, 정책 문서)를 모아 평가셋을 만들고, 하이브리드 파라미터와 rerank 입력 길이를 고정된 실험으로 튜닝하는 것을 권장합니다.