Published on

RAG 환각 줄이기 - 하이브리드 검색+Rerank 튜닝

Authors

RAG에서 환각을 줄이려면 모델을 “더 똑똑하게” 만들기보다, 모델이 참고할 근거를 더 정확히 제공하는 편이 훨씬 비용 대비 효과가 큽니다. 특히 실무에서 보이는 환각의 상당수는 LLM 자체의 문제가 아니라 아래 두 가지에서 시작합니다.

  • 못 찾음: 질문에 필요한 근거 문서를 검색 단계에서 가져오지 못함
  • 잘못 올림: 가져오긴 했지만, 상위 컨텍스트에 엉뚱한 문서를 올려 모델이 그걸 근거로 답함

이 글은 이 두 문제를 동시에 겨냥하는 하이브리드 검색(lexical+vector)rerank(재정렬) 를 어떻게 튜닝하면 환각이 줄어드는지, 그리고 어떤 지표로 개선을 확인할지 정리합니다.

아래 글도 함께 보면 RAG 운영 품질을 더 빨리 끌어올릴 수 있습니다.

환각을 “검색 문제”로 재정의하기

환각을 줄이기 위한 실전 프레임은 간단합니다.

  1. Recall을 올린다: 정답 문서가 후보군에 들어오게 만든다
  2. Precision을 올린다: 후보군에서 정답 문서를 상위로 올린다
  3. Context를 절제한다: 상위 몇 개만, 필요한 부분만 LLM에 준다

하이브리드 검색은 1번을, rerank는 2번을, 컨텍스트 구성은 3번을 주로 담당합니다.

하이브리드 검색이 필요한 이유

벡터 검색만 쓰면 의미적으로 비슷한 문서를 잘 찾지만, 다음 케이스에서 자주 미끄러집니다.

  • 고유명사/코드/에러코드/약어: 예를 들어 EKS InvalidIdentityToken 같은 문자열은 lexical이 강함
  • 정확한 키워드 매칭이 중요한 정책/규정: “반드시”, “금지” 같은 문구가 의미를 바꿈
  • 질문이 짧고 애매한 경우: 벡터가 넓게 퍼져 엉뚱한 문서가 섞임

반대로 BM25 같은 lexical 검색만 쓰면 동의어, 표현 차이, 문장 구조가 바뀐 질문에서 recall이 떨어집니다.

그래서 실무 RAG는 보통 아래 구조가 안정적입니다.

  • 1차: 하이브리드로 후보를 넓게 가져오기
  • 2차: rerank로 상위를 정확히 정렬하기
  • 3차: 상위 k개만 컨텍스트로 구성

하이브리드 검색 설계: 후보군을 “넓고 안전하게”

하이브리드 검색의 핵심은 “벡터로 넓게, 키워드로 정확히”를 섞되, 합치는 방식이 환각에 직결된다는 점입니다.

합치는 방법 3가지

1) 단순 합집합(Union)

  • 벡터 top N + BM25 top M을 합쳐 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: 정답 문서가 후보 top K에 들어왔는가
  • 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는 환각을 줄이지만 비용과 지연을 올릴 수 있습니다. 아래 순서로 최적화하면 안전합니다.

  1. 캐싱: 동일/유사 질의 캐시(정규화, 토큰화 기반)
  2. rerank 대상 축소: 하이브리드 결과에서 중복 제거, 메타데이터 필터
  3. 2단 rerank: cheap rerank로 100개를 30개로 줄이고, strong rerank로 top 10

또한 데이터베이스나 검색 인덱스가 느리면 전체 파이프라인이 흔들립니다. 병목이 DB라면 auto_explain 같은 도구로 원인을 좁히는 접근이 유용합니다. 자세한 방법은 PostgreSQL 쿼리 느림? auto_explain으로 추적을 참고하세요.

정리: 환각을 줄이는 가장 현실적인 조합

  • 하이브리드 검색으로 정답 문서가 후보군에 들어올 확률(Recall) 을 올린다
  • rerank로 정답 문서를 상위에 올릴 확률(MRR, NDCG) 을 올린다
  • 최종 컨텍스트는 짧고 정확하게, 출처 라벨을 포함한다
  • “근거 없으면 중단”을 프롬프트와 스키마로 강제한다

이 조합은 모델을 바꾸지 않고도 환각을 눈에 띄게 줄일 수 있고, 문제 발생 시에도 로그로 원인 분리가 쉬워 운영 난이도가 내려갑니다. 다음 단계로는 도메인별 하드 케이스(고유명사, 버전, 정책 문서)를 모아 평가셋을 만들고, 하이브리드 파라미터와 rerank 입력 길이를 고정된 실험으로 튜닝하는 것을 권장합니다.