Published on

RAG 환각 줄이기 - HyDE+rerank 튜닝 실전

Authors

RAG에서 환각이 줄지 않는 가장 흔한 이유는 모델이 “없는 근거를 만들어내서”가 아니라, 애초에 검색 단계에서 근거가 부실하거나, 근거가 있어도 상위 컨텍스트에 못 올라오거나, 근거를 읽고도 답변 형식이 과감해서입니다. 즉 환각은 생성 모델만의 문제가 아니라 retrieve -> select -> answer 파이프라인 전체의 품질 문제입니다.

이 글은 그중에서도 실무에서 효과가 큰 조합인 HyDE(Hypothetical Document Embeddings) + rerank를 중심으로, “어디를 어떻게 튜닝해야 환각이 실제로 줄어드는지”를 단계별로 정리합니다. 마지막에는 임계값, 거절 응답, 평가 지표까지 함께 엮어 재현 가능한 튜닝 루프를 제시합니다.

또한 RAG 답변에서 불필요한 추론 노출을 통제하는 프롬프트 방어는 별도 주제로도 중요합니다. 필요하면 Chain-of-Thought 누출 막는 프롬프트 방어 7선도 함께 참고하세요.

RAG 환각의 3가지 실패 지점

현장에서 로그를 보면 환각은 대체로 아래 3가지로 분류됩니다.

  1. Retrieval miss: 정답 근거가 인덱스에 있는데도 검색이 못 찾음(질의가 짧거나 모호, 어휘 불일치)
  2. Context selection miss: 검색은 했지만 상위 k에 정답 근거가 못 올라옴(벡터 유사도만으로는 미세한 차이를 못 가름)
  3. Answering overreach: 근거가 부족한데도 모델이 단정적으로 답함(출처 인용 없음, 거절 규칙 없음)

HyDE는 1번을, rerank는 2번을 크게 개선합니다. 그리고 3번은 “근거 부족 시 거절”과 “근거 인용” 같은 답변 정책으로 마무리합니다.

HyDE란 무엇이고, 왜 환각을 줄이나

HyDE는 사용자 질문을 그대로 임베딩해 검색하는 대신, LLM이 **가상의 정답 문서(hypothetical document)**를 먼저 작성하고 그 문서를 임베딩해 검색하는 기법입니다.

핵심 효과는 두 가지입니다.

  • 질의 확장(Query expansion): 짧은 질문을 “문서 형태”로 확장해 검색 신호를 풍부하게 만듭니다.
  • 어휘/표현 정규화: 사용자가 쓰지 않은 동의어, 관련 키워드가 가상 문서에 등장하면서 벡터 검색의 recall이 올라갑니다.

주의할 점도 있습니다. HyDE가 만든 가상 문서는 사실일 필요가 없습니다. 오히려 “사실처럼 보이는 거짓”이 섞일 수 있습니다. 그래서 HyDE는 정답 생성용이 아니라 검색용으로만 쓰고, 최종 답변은 반드시 실제 근거 문서로만 작성해야 합니다.

HyDE 프롬프트: 검색용 문서로 제한하기

HyDE 프롬프트는 길고 창의적일수록 좋지 않습니다. 검색용 문서는 다음 속성이 좋습니다.

  • 질문의 핵심 엔티티/조건을 빠짐없이 포함
  • 과도한 서사 제거
  • 단정적 주장 최소화(하지만 키워드는 풍부하게)

아래는 실무에서 무난한 형태입니다.

System: You write a hypothetical reference passage for retrieval.
Rules:
- Do not cite sources.
- Do not mention that this is hypothetical.
- Focus on technical terms, constraints, and likely wording in documentation.
- 120-200 words.

User: {question}

여기서 120200 단어는 예시 범위입니다. 한국어라면 “문단 12개” 정도로 제한하는 편이 안전합니다. 너무 길면 임베딩이 평균화되어 오히려 검색이 흐려질 수 있습니다.

파이프라인 설계: HyDE + 다중 쿼리 + rerank

추천하는 기본 구조는 다음입니다.

  1. 사용자 질문 q
  2. HyDE로 가상 문서 h
  3. 검색 쿼리 구성
    • q 자체
    • h 임베딩
    • (선택) q를 키워드 쿼리로도 변환해 BM25 병행
  4. 후보 문서 N개 넉넉히 수집(예: 50~200)
  5. reranker로 상위 k 선별(예: 5~12)
  6. 근거 기반 답변(인용 필수, 부족하면 거절)

여기서 환각 감소에 가장 직결되는 튜닝 포인트는 Nk, 그리고 reranker 임계값입니다.

구현 예시(Python): HyDE 생성 -> 벡터 검색 -> rerank

아래 코드는 특정 벡터DB에 종속되지 않도록 “인터페이스 형태”로 작성했습니다. 중요한 건 데이터 흐름입니다.

from dataclasses import dataclass
from typing import List, Tuple

@dataclass
class Doc:
    id: str
    text: str
    meta: dict

class Embedder:
    def embed(self, text: str) -> List[float]:
        raise NotImplementedError

class VectorIndex:
    def search(self, query_vec: List[float], top_n: int) -> List[Tuple[Doc, float]]:
        raise NotImplementedError

class Reranker:
    def score(self, query: str, doc_texts: List[str]) -> List[float]:
        """Return relevance scores aligned with doc_texts."""
        raise NotImplementedError

class LLM:
    def generate(self, prompt: str) -> str:
        raise NotImplementedError


def build_hyde_prompt(question: str) -> str:
    return (
        "System: You write a hypothetical reference passage for retrieval.\n"
        "Rules:\n"
        "- Do not cite sources.\n"
        "- Focus on technical terms, constraints, and likely wording in documentation.\n"
        "- Keep it concise.\n\n"
        f"User: {question}\n"
    )


def retrieve_with_hyde(
    question: str,
    llm: LLM,
    embedder: Embedder,
    index: VectorIndex,
    reranker: Reranker,
    top_n: int = 100,
    top_k: int = 8,
    min_rerank_score: float = 0.2,
) -> List[Doc]:
    # 1) HyDE
    hyde = llm.generate(build_hyde_prompt(question))

    # 2) Candidate retrieval using HyDE embedding
    qvec = embedder.embed(hyde)
    candidates = index.search(qvec, top_n=top_n)

    # 3) Rerank using original question (not HyDE)
    docs = [d for d, _ in candidates]
    scores = reranker.score(question, [d.text for d in docs])

    ranked = sorted(zip(docs, scores), key=lambda x: x[1], reverse=True)

    # 4) Threshold + top_k selection
    filtered = [d for d, s in ranked if s >= min_rerank_score][:top_k]
    return filtered

포인트는 2가지입니다.

  • rerank 입력 쿼리는 hyde가 아니라 원 질문을 쓰는 편이 안전합니다. HyDE가 만든 “그럴듯한 허구”가 rerank를 오염시킬 수 있기 때문입니다.
  • min_rerank_score를 두어 근거가 약한 경우 아예 컨텍스트를 비우고 거절로 보내는 경로를 만들 수 있습니다.

rerank 모델 선택과 튜닝 포인트

rerank는 크게 두 계열이 있습니다.

  • Cross-encoder: querydoc를 함께 넣고 관련도를 직접 예측. 정확도가 좋지만 비용이 큼.
  • LLM-as-reranker: LLM으로 “이 문서가 질문에 답하는가”를 평가. 비용과 변동성이 커서 운영 난이도가 있음.

실무에서는 cross-encoder 계열(예: bge-reranker, jina reranker 등)이 가장 재현성이 좋습니다.

튜닝에서 중요한 건 “모델 선택”보다도 다음 파라미터입니다.

1) 후보 수 N을 충분히 키우기

벡터 검색만으로 상위 10개를 뽑아 rerank하면, rerank는 없는 정답을 만들 수 없습니다. rerank는 후보 재정렬기일 뿐입니다.

  • 권장 시작점: N=100, k=8
  • 문서가 길고 유사 문서가 많으면 N=200까지도 고려

2) k를 줄여 컨텍스트 오염을 막기

환각은 “근거 없음”뿐 아니라 “근거가 너무 많아 상충”할 때도 늘어납니다.

  • 운영에서 자주 쓰는 범위: k=5~12
  • 답변이 자주 흔들리면 k를 먼저 줄여보는 것이 효과적입니다.

3) rerank 임계값으로 거절 라우팅하기

min_rerank_score(또는 top1-top2 margin)를 두면, 애매한 질문에 대해 모델이 단정적으로 말하는 비율이 확 떨어집니다.

실전 규칙 예시:

  • top1_score가 0.25 미만이면 “근거 부족”
  • 또는 (top1_score - top2_score)가 너무 작으면 “문서가 비슷비슷해서 확신 낮음”

임계값은 데이터셋으로 보정해야 합니다. 뒤에서 평가 루프를 설명합니다.

HyDE와 rerank를 같이 쓸 때의 흔한 함정

HyDE가 특정 방향으로 편향된 문서를 만들어 recall이 떨어지는 경우

예를 들어 질문이 “A와 B 비교”인데 HyDE가 A 위주로만 작성하면 B 관련 문서가 밀릴 수 있습니다.

대응:

  • HyDE 프롬프트에 “대안/비교 축 포함” 규칙 추가
  • HyDE를 2개 생성해 앙상블(비용 증가)
Rules:
- If the question implies comparison, include both sides with symmetric terminology.
- Include common alternatives and their keywords.

rerank가 긴 문서에 유리하게 점수를 주는 경우

문서 길이가 길면 질문 키워드가 우연히 더 많이 포함되어 점수가 올라갈 수 있습니다.

대응:

  • chunking을 더 촘촘히(예: 300~800 토큰)
  • rerank 입력에 title + chunk처럼 구조화
  • 같은 원문에서 여러 chunk가 상위권을 점유하면 “문서 다양성” 패널티 적용

답변 단계 튜닝: 인용 강제 + 근거 없으면 거절

HyDE+rerank로도 환각이 0이 되지는 않습니다. 마지막 방어선은 답변 정책입니다.

아래는 “근거 인용”과 “모르면 모른다”를 시스템 레벨에서 강제하는 템플릿 예시입니다.

System:
You are a QA assistant. Use only the provided context.
If the context does not contain enough information, say you don't know.
Cite sources by doc id for every factual claim.
Do not add external knowledge.

User:
Question: {question}

Context:
{context_blocks}

이때 “인용”이 실제 환각 감소에 매우 중요합니다. 인용을 강제하면 모델이 근거가 없는 문장을 만들기 어려워지고, 운영자가 근거-답변 매핑을 감사할 수 있습니다.

프롬프트 보안/추론 노출까지 같이 고민한다면 앞서 언급한 Chain-of-Thought 누출 막는 프롬프트 방어 7선에서 “근거만 요약하고 추론은 숨기는” 패턴도 함께 적용할 수 있습니다.

오프라인 평가: “환각”을 수치로 줄이는 방법

튜닝이 어려운 이유는 “환각”이 모호한 단어라서입니다. 실무에서는 아래처럼 분해해 측정합니다.

1) Retrieval 지표

  • Recall@N: 정답 근거가 후보 N에 포함되는 비율
  • MRR: 정답 근거가 얼마나 상위에 오는지

HyDE의 성과는 주로 Recall@N에서 확인됩니다.

2) Rerank 지표

  • Precision@k: 상위 k에 실제 답변에 필요한 근거가 얼마나 들어오는지
  • nDCG@k: 상위에 더 중요한 근거가 오는지

3) Answer 지표(환각 근사)

정교한 fact-checker가 없더라도, 운영에서 유용한 근사 지표가 있습니다.

  • Attribution rate: 답변 문장 중 인용이 붙은 비율
  • Unsupported claim rate: 인용은 있으나 문서에 없는 내용을 말하는 비율(샘플링 리뷰)
  • Abstain accuracy: 근거 부족 질문에서 거절을 잘했는지

여기서 min_rerank_score 같은 임계값은 “정답률 vs 거절률” 트레이드오프를 만들기 때문에, 반드시 오프라인 세트로 튜닝해야 합니다.

튜닝 루프(권장 순서)

  1. chunking 점검: 너무 길거나 너무 짧으면 rerank가 흔들립니다.
  2. HyDE 도입: Recall@100이 오르는지 확인합니다.
  3. 후보 수 N 확대: rerank에 먹일 후보를 늘립니다.
  4. rerank 적용: Precision@k가 오르는지 확인합니다.
  5. k 축소/조정: 컨텍스트 오염을 줄입니다.
  6. 임계값 도입: min_rerank_score로 거절 라우팅을 만듭니다.
  7. 답변 프롬프트: 인용 강제, 근거 없으면 거절.

이 순서를 추천하는 이유는, 앞 단계가 무너지면 뒤 단계가 아무리 좋아도 환각이 줄지 않기 때문입니다.

운영 팁: 지연시간과 비용을 줄이는 현실적 방법

HyDE와 rerank는 품질을 올리지만 비용도 올립니다. 운영에서 자주 쓰는 절충안은 다음입니다.

  • HyDE는 캐시하기: 같은 질문/유사 질문에 대해 HyDE 결과를 캐시하면 비용이 크게 줄어듭니다.
  • rerank는 후보가 많을 때만: 벡터 검색 점수가 충분히 높고 분리가 잘 되면 rerank를 생략하는 규칙을 둘 수 있습니다.
  • 로컬 LLM/경량 모델로 HyDE 생성: HyDE는 “정답 생성”이 아니라 “검색용 문서 생성”이라 비교적 작은 모델도 실용적입니다. 로컬 모델 최적화는 Transformers 로컬 LLM 느림·OOM, 4bit+FlashAttn2 같은 최적화 포인트가 도움이 됩니다.

체크리스트: 환각이 계속 나오면 여기부터 보자

  • HyDE를 최종 답변에 섞고 있지 않은가(절대 금지)
  • N이 너무 작지 않은가(후보 부족)
  • k가 너무 크지 않은가(컨텍스트 오염)
  • rerank 임계값이 없는가(애매한 질문이 단정 답변으로 감)
  • “인용 강제”가 없는가(감사 불가능)
  • chunk가 문맥 단위로 잘렸는가(표/코드/절차가 분리되면 검색이 약해짐)

마무리

HyDE는 “질문을 문서처럼 만들어 검색 recall을 끌어올리는 장치”이고, rerank는 “상위 컨텍스트를 정밀하게 정렬해 근거 품질을 올리는 장치”입니다. 두 개를 결합하면 RAG 환각의 큰 축인 retrieval misscontext selection miss를 동시에 줄일 수 있습니다.

하지만 마지막 10%는 운영 정책(임계값, 거절, 인용, 평가 루프)에서 결정됩니다. HyDE+rerank를 넣었는데도 환각이 줄지 않는다면, 생성 모델을 바꾸기 전에 먼저 N/k/threshold와 “근거 기반 답변 규칙”을 계측 가능한 형태로 고정해 보세요. 그 순간부터 튜닝이 감이 아니라 엔지니어링이 됩니다.