- Published on
RAG 환각 줄이기 - 하이브리드검색+재랭커 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
RAG에서 환각은 모델이 거짓말을 해서라기보다, 모델이 참고할 근거를 제대로 받지 못했거나(검색 누락), 엉뚱한 근거를 받았거나(컨텍스트 오염), 근거가 있어도 답변 생성 단계에서 근거를 무시했을 때 자주 발생합니다. 이 글은 그중에서도 가장 비용 대비 효과가 큰 축인 검색 품질을 올리는 방법, 특히 하이브리드 검색 + 재랭커 조합을 어떻게 튜닝해야 환각이 유의미하게 줄어드는지에 집중합니다.
핵심 메시지는 단순합니다.
- 임베딩 기반 벡터 검색만으로는 키워드 희소성, 숫자, 코드, 고유명사에 취약합니다.
- 키워드 기반 BM25만으로는 의미적 동의어, 문장형 질문, 장문 문서 매칭에 취약합니다.
- 그래서 1차 후보군은 하이브리드로 넓게 잡고, 2차에서 재랭커로 정확도를 끌어올리는 구조가 환각 억제에 가장 안정적입니다.
환각을 줄이는 관점에서 본 검색 실패 유형
환각을 검색 관점으로 분해하면 대략 아래 케이스로 떨어집니다.
1) Recall 부족: 필요한 근거가 아예 안 들어옴
- 동의어, 표현 차이, 문서가 길어 chunk가 분산된 경우
- 필터링이 과도하거나,
top_k가 너무 작거나, 인덱스가 부실한 경우
증상은 간단합니다. 답변이 자신감 있게 나오지만, 인용할 문장이 없거나, 인용이 질문과 직접 관련이 없습니다.
2) Precision 부족: 엉뚱한 근거가 들어옴
- 벡터 유사도가 높은데 실제로는 다른 의미인 경우
- BM25가 키워드만 맞춰서 비슷한 문서를 끌고 오는 경우
이때 모델은 들어온 컨텍스트를 근거로 답을 구성하려고 하면서, 결과적으로 그럴듯한 오답을 생성합니다.
3) 컨텍스트 오염: 후보가 많아도 섞여 들어옴
- 관련 chunk와 무관 chunk가 함께 들어와 모델이 헷갈림
- chunk 크기, overlap, 문서 구조가 나쁜 경우
하이브리드 검색과 재랭커는 1)과 2)를 직접적으로 줄이고, 3)은 chunking과 컨텍스트 구성 전략까지 같이 손봐야 효과가 극대화됩니다.
권장 아키텍처: 하이브리드 1차 검색 + 재랭커 2차 정제
일반적인 파이프라인은 다음 순서가 안정적입니다.
- 쿼리 전처리(정규화, 동의어 확장, 필터 추출)
- 1차 후보 생성
- BM25(키워드)
- Vector ANN(임베딩)
- 결과를 합치고 점수 정규화
- 2차 재랭킹
- Cross-encoder 계열 재랭커로
query-document쌍을 직접 점수화
- Cross-encoder 계열 재랭커로
- 컨텍스트 구성
- 상위 N개 chunk를 넣되, 중복 제거, 문서 다양성 확보
- 생성 단계 제약
- 인용 기반 답변, 근거 없으면 모른다고 답하기
여기서 환각 억제의 실전 포인트는 후보군을 넓게 잡되, 최종 컨텍스트는 좁고 정확하게 입니다.
하이브리드 검색 튜닝: BM25와 벡터의 균형 잡기
1) 합치는 방식: union 후 점수 정규화가 기본
BM25 점수와 벡터 유사도는 스케일이 다릅니다. 단순 합산은 튜닝을 어렵게 만듭니다. 실무에서는 아래 중 하나를 씁니다.
- Rank fusion: 순위 기반으로 합치기
- Score normalization: 점수를 0에서 1로 정규화한 뒤 가중합
가장 구현이 쉬운 것은 RRF 입니다.
RRF는 각 검색기의 순위를 이용해 합칩니다.
- 장점: 점수 스케일 문제에서 자유로움
- 단점: 절대 점수 기반 임계값 컷이 어려움
2) 후보군 크기: 1차는 넉넉하게, 2차에서 줄이기
재랭커가 있으면 1차 top_k를 키우는 것이 보통 유리합니다.
- 벡터
top_k예: 50에서 200 - BM25
top_k예: 50에서 200 - 합친 뒤 유니크 후보 예: 100에서 400
- 재랭커 후 최종 컨텍스트 예: 5에서 15
다만 후보가 커질수록 재랭커 비용이 증가합니다. 따라서 재랭커 입력 길이와 동시 처리량을 함께 고려해야 합니다.
3) BM25 튜닝 포인트: 형태소, 숫자, 고유명사
한국어 문서라면 BM25 품질이 형태소 분석과 토크나이저에 크게 좌우됩니다.
- 제품명, 에러코드, 버전, 숫자 토큰이 쪼개지지 않도록 설정
- 불용어 제거가 과하면 오히려 recall이 떨어짐
- 제목, 헤더, 코드 블록에 가중치를 주는 필드 부스팅
예를 들어 문서에 503, OOMKilled, OutOfSync 같은 토큰이 중요하다면, 해당 토큰이 인덱싱 과정에서 손실되지 않게 해야 합니다.
4) 벡터 검색 튜닝 포인트: chunking이 성능의 절반
벡터 검색은 chunk 품질에 민감합니다.
- chunk 크기: 300에서 800 토큰 사이에서 실험
- overlap: 50에서 150 토큰
- 문서 구조 기반 분할: 헤더 단위, 코드 블록 단위
너무 작은 chunk는 의미가 약해지고, 너무 큰 chunk는 잡음이 섞여 precision이 떨어집니다.
재랭커 튜닝: precision을 끌어올려 환각을 억제
재랭커는 query와 candidate chunk를 함께 보고 관련성을 판정합니다. 즉, 1차 검색에서 섞여 들어온 잡음을 최종 컨텍스트에서 제거하는 역할을 합니다.
1) 재랭커 모델 선택: cross-encoder 계열이 기본
- 경량: MiniLM 계열
- 고성능: bge-reranker, e5 기반 reranker 등
실무에서는 지연시간과 비용 때문에 경량 모델로 시작하고, 특정 도메인에서만 고성능 모델을 쓰는 식으로 계층화하기도 합니다.
2) 입력 구성: chunk만 넣지 말고 메타데이터를 섞기
재랭커 입력에 아래 정보를 함께 넣으면 도움이 됩니다.
- 문서 제목
- 섹션 헤더 경로
- 작성일, 버전
다만 메타데이터가 과하면 편향이 생길 수 있어, 일관된 템플릿을 쓰는 것이 좋습니다.
3) 임계값 컷: 근거 부족 시 답변을 멈추게 만들기
환각을 줄이려면 재랭커 점수 임계값을 적극적으로 활용해야 합니다.
- 상위 1개 점수가 낮으면
근거 부족으로 간주 - 상위 1개와 2개 점수 차이가 너무 작으면
애매한 질문으로 간주
이때 생성 단계에서 근거가 없으면 모른다고 답하기를 강제하면 환각이 급감합니다.
4) 다양성 제약: 한 문서가 상위 N개를 독점하지 않게
같은 문서에서 연속 chunk가 상위권을 독점하면 컨텍스트가 편향될 수 있습니다.
- 문서 단위 max quota 예: 문서당 최대 2개 chunk
- MMR류의 다양성 선택
실전 코드 예제: 하이브리드 검색 + RRF + 재랭커
아래는 개념을 보여주는 파이썬 예제입니다. 실제 검색 엔진은 Elasticsearch, OpenSearch, Vespa, Weaviate 등으로 대체하면 됩니다.
from dataclasses import dataclass
from typing import List, Dict, Tuple
@dataclass
class Hit:
doc_id: str
chunk_id: str
text: str
score: float
source: str # "bm25" or "vector"
def rrf_fusion(ranked_lists: List[List[Hit]], k: int = 60) -> List[Tuple[str, float]]:
"""Reciprocal Rank Fusion. Returns list of (key, fused_score)."""
scores: Dict[str, float] = {}
for hits in ranked_lists:
for rank, h in enumerate(hits, start=1):
key = f"{h.doc_id}:{h.chunk_id}"
scores[key] = scores.get(key, 0.0) + 1.0 / (k + rank)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
def rerank(query: str, candidates: List[Hit]) -> List[Hit]:
"""Stub reranker. Replace with a real cross-encoder call."""
# 예: sentence-transformers CrossEncoder, 또는 외부 reranker API
# 여기서는 데모로 기존 score를 그대로 사용
return sorted(candidates, key=lambda h: h.score, reverse=True)
def build_context(reranked: List[Hit], max_chunks: int = 8, per_doc_quota: int = 2) -> List[Hit]:
picked: List[Hit] = []
per_doc: Dict[str, int] = {}
for h in reranked:
if len(picked) >= max_chunks:
break
if per_doc.get(h.doc_id, 0) >= per_doc_quota:
continue
picked.append(h)
per_doc[h.doc_id] = per_doc.get(h.doc_id, 0) + 1
return picked
def hybrid_search_then_rerank(query: str, bm25_hits: List[Hit], vector_hits: List[Hit]) -> List[Hit]:
fused = rrf_fusion([bm25_hits, vector_hits], k=60)
hit_map = {f"{h.doc_id}:{h.chunk_id}": h for h in (bm25_hits + vector_hits)}
candidates = [hit_map[key] for key, _ in fused[:200] if key in hit_map]
reranked = rerank(query, candidates)
context = build_context(reranked, max_chunks=8, per_doc_quota=2)
return context
위 코드에서 환각 억제와 직결되는 튜닝 포인트는 다음입니다.
fused[:200]후보 수max_chunks최종 컨텍스트 크기per_doc_quota다양성 제한- 재랭커 점수 임계값 기반의
no_answer처리
평가 지표와 실험 설계: 환각은 검색 지표로 먼저 잡는다
환각을 줄이려면 생성 품질 평가만으로는 느립니다. 검색 단계에서 빠르게 피드백을 돌릴 수 있는 지표를 먼저 잡는 것이 효율적입니다.
1) 검색 단계 오프라인 지표
- Recall@K: 정답 문서 또는 정답 chunk가 K 안에 들어오는가
- MRR: 정답이 얼마나 상위에 위치하는가
- nDCG: 관련도 등급이 있을 때 순위 품질
정답 라벨이 없다면, 최소한 질문-근거 쌍을 50에서 200개라도 수작업으로 만들면 튜닝 속도가 크게 올라갑니다.
2) 재랭커 평가
- 재랭커 적용 전후 MRR 비교
- hard negative에 대한 분리 능력
hard negative는 키워드는 맞는데 의미가 다른 문서, 또는 의미는 비슷하지만 질문의 조건을 만족하지 않는 문서입니다.
3) 온라인 지표
- 답변에 포함된 인용 근거의 적합성
- no_answer 비율과 사용자 재질문 비율
- 환각 리포트율
운영 팁: 관측성과 장애 대응까지 고려하기
하이브리드 검색과 재랭커를 붙이면 구성 요소가 늘어나고, 그만큼 디버깅 포인트도 늘어납니다. 실무에서는 아래 로그가 필수입니다.
- 쿼리 원문과 정규화 결과
- BM25 상위 N개와 벡터 상위 N개
- fusion 결과 상위 N개
- 재랭커 입력 텍스트 길이, 점수 분포
- 최종 컨텍스트와 생성 답변의 인용 매핑
비동기 호출로 검색과 재랭킹을 병렬화하면 지연시간을 줄일 수 있는데, 파이썬에서 비동기 로깅을 잘 설계하면 트러블슈팅이 쉬워집니다. 관련해서는 Python 데코레이터+ContextVar로 async 로그 추적 글의 패턴이 RAG 파이프라인에도 그대로 적용됩니다.
또한 스트리밍 응답을 붙인 경우, 재시도 로직이 중복 토큰이나 중복 문단을 만들면서 품질 이슈로 이어질 수 있습니다. 이때는 OpenAI SSE 스트리밍 끊김·중복 토큰 재시도 패턴에서 소개한 멱등 처리 아이디어가 도움이 됩니다.
체크리스트: 환각을 줄이는 튜닝 순서
아래 순서대로 하면 시행착오가 줄어듭니다.
- chunking 점검
- 헤더 기반 분할, 코드 블록 보존, 적절한 overlap
- 하이브리드 1차 후보 확장
- BM25
top_k와 벡터top_k를 늘리고 fusion 적용
- BM25
- 재랭커 도입
- 후보 100에서 400개를 재랭킹, 최종 5에서 15개만 컨텍스트로
- 임계값 기반 no_answer
- 재랭커 점수 또는 상위 점수 격차로 근거 부족 차단
- 다양성 제약
- 문서 쿼터, 중복 chunk 제거
- 지표 루프 구축
- Recall@K, MRR, 온라인 환각 리포트율을 함께 추적
마무리
RAG 환각을 줄이는 가장 현실적인 방법은 모델 프롬프트를 계속 만지는 것이 아니라, 검색이 정답 근거를 안정적으로 가져오게 만들고, 재랭커로 잡음을 제거한 뒤, 근거가 없으면 멈추게 하는 것입니다.
하이브리드 검색은 recall을, 재랭커는 precision을 담당합니다. 이 둘을 분리해서 튜닝하면 어디서 문제가 생기는지 명확해지고, 결과적으로 환각은 자연스럽게 줄어듭니다.