- Published on
RAG 환각 줄이기 - HyDE+Rerank 실전 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서치 기반 RAG는 모델이 아는 척하는 환각을 줄이는 데 효과적이지만, 운영에 들어가면 여전히 두 가지 이유로 환각이 발생합니다.
- 검색이 빗나가서 근거 없는 답을 생성한다
- 검색은 맞았는데 컨텍스트 선택이 엉켜서 잘못 요약한다
이 글은 이 두 문제를 각각 HyDE(Hypothetical Document Embeddings)와 Rerank로 다루는 튜닝 레시피를 설명합니다. 핵심은 간단합니다.
- HyDE로
recall을 올려서 “찾아오지 못해 생기는 환각”을 줄이고 - Rerank로
precision을 잠가서 “가져왔지만 섞여서 생기는 환각”을 줄입니다
아래는 실전에서 바로 적용 가능한 구조, 프롬프트, 파라미터, 평가 방법, 그리고 운영 체크리스트입니다.
환각을 “검색 실패”와 “선정 실패”로 분해하기
RAG 환각을 줄이려면 먼저 실패 지점을 분리해야 합니다.
1) 검색 실패(retrieval miss)
- 쿼리가 짧거나 모호해서 벡터 검색이 엉뚱한 문서를 가져옴
- 도메인 용어가 사용자의 표현과 달라서 임베딩 공간에서 멀어짐
- 질문이 복합 질문인데 단일 쿼리로 검색해서 일부만 맞음
2) 선정 실패(context selection miss)
- Top
k안에는 정답 문서가 있는데도, 상위에 노이즈가 섞여 LLM이 오답을 합성 - 유사하지만 다른 규정/버전/지역 문서를 함께 넣어 충돌
- chunk가 너무 작거나 너무 커서 근거가 끊기거나 과다 포함
HyDE는 1)을, Rerank는 2)를 주로 개선합니다. 둘을 같이 쓰면 “재현율을 올린 뒤 정밀도를 회수하는” 형태가 됩니다.
HyDE 개념과 왜 환각을 줄이는가
HyDE는 사용자의 질문에서 바로 임베딩을 뽑지 않고, 먼저 “정답처럼 보이는 가짜 문서”를 LLM으로 생성한 다음 그 가짜 문서를 임베딩해 검색 쿼리로 사용합니다.
- 질문 임베딩은 짧고 정보가 부족해 검색이 흔들릴 수 있음
- HyDE 문서는 질문을 도메인 용어로 확장하고, 키워드를 풍부하게 포함
- 결과적으로 벡터 검색에서 관련 chunk를 더 잘 끌어옴
중요한 점은 HyDE 문서가 사실일 필요가 없다는 것입니다. HyDE는 답을 만드는 게 아니라 “검색을 위한 서치 쿼리 확장”에 가깝습니다.
HyDE 프롬프트: 사실 생성이 아니라 “용어 확장”에 최적화
HyDE를 잘못 쓰면 오히려 특정 결론을 강하게 유도해 편향된 검색을 만들 수 있습니다. 그래서 HyDE 프롬프트는 다음 원칙을 지키는 것이 좋습니다.
- 단정적 결론을 피하고, 가능한 표현을 폭넓게 나열
- 숫자/정책/버전처럼 민감한 값은 임의로 만들지 않게 제약
- 문서 톤을 “내부 위키/매뉴얼”처럼 만들어 용어를 풍부하게
아래는 실전용 HyDE 프롬프트 예시입니다.
역할: 검색 쿼리 확장기(HyDE)
목표: 아래 질문과 관련된 내부 문서에서 나올 법한 설명문을 1개 작성하라.
주의:
- 사실 여부는 중요하지 않다. 단, 특정 숫자/정책/버전/날짜를 임의로 단정하지 말고 범주형 표현을 사용하라.
- 가능한 관련 키워드, 동의어, 약어, API/설정 항목명을 풍부하게 포함하라.
- 결론을 단정하지 말고, 문제 정의/원인 후보/점검 항목 중심으로 작성하라.
질문:
{user_question}
출력:
- 120~220단어 분량의 설명문 1개
HyDE 출력 길이 튜닝
- 너무 짧으면 원래 질문과 차이가 없어 효과가 약함
- 너무 길면 불필요한 키워드가 섞여 검색이 퍼짐
경험적으로 120220단어, 또는 한국어 기준 6001200자 정도가 안정적입니다.
HyDE 검색 파이프라인 구성: dual query가 안전하다
HyDE만 쓰면 편향이 생길 수 있으니, “원 질문 임베딩”과 “HyDE 임베딩”을 함께 쓰는 듀얼 쿼리를 추천합니다.
q1: user question embeddingq2: HyDE document embedding- 검색은 둘을 합쳐 후보군을 넓힘
예시 의사코드입니다.
def retrieve_candidates(user_question: str, top_k: int = 40):
hyde_doc = llm_generate_hyde(user_question)
q1 = embed(user_question)
q2 = embed(hyde_doc)
# 각각 넉넉히 뽑아서 합치고 dedup
c1 = vector_search(q1, top_k=top_k)
c2 = vector_search(q2, top_k=top_k)
merged = dedup_by_chunk_id(c1 + c2)
return merged
여기서 top k는 최종 컨텍스트 개수가 아니라 “rerank 전 후보군 크기”입니다. HyDE로 recall을 올리는 대신, rerank가 처리할 후보군이 늘어나므로 비용과 지연이 증가합니다.
벡터 DB 레벨에서 지연을 줄이는 튜닝은 별도 최적화 포인트가 많습니다. Milvus를 쓰는 경우 인덱스 선택과 파라미터가 체감 성능을 크게 바꿉니다. 관련해서는 Milvus IVF_FLAT·HNSW 튜닝으로 지연 50% 줄이기도 함께 참고하면 좋습니다.
Rerank: “상위 컨텍스트”를 정확히 고르는 마지막 잠금장치
Rerank는 후보 문서들을 질문과 함께 넣고, 질문-문서 관련도를 다시 점수화해 상위 N개를 고르는 단계입니다.
- 벡터 검색은 근사 유사도 기반이라 상위가 흔들릴 수 있음
- Rerank는 cross-encoder 계열 또는 LLM scoring으로 더 정밀하게 정렬
- 결과적으로 LLM에 들어가는 컨텍스트 품질이 올라가 환각이 줄어듦
Rerank 입력 단위: chunk가 핵심
Rerank는 “문서”가 아니라 “답에 직접 쓰일 근거 chunk”를 고르는 작업입니다.
- chunk가 너무 작으면 근거가 불충분해 오히려 오답 합성
- chunk가 너무 크면 관련 없는 문장이 함께 들어가 혼선
실전에서는 다음이 무난합니다.
- 기술 문서: 300
800 토큰 chunk, 50120 토큰 overlap - FAQ/정책: 섹션 단위 chunk, 표/리스트는 깨지지 않게
HyDE+Rerank 조합의 표준 파이프라인
아래는 운영에서 가장 흔히 쓰는 형태입니다.
- HyDE 생성
- 벡터 검색 듀얼 쿼리로 후보
k=30~80확보 - Rerank로 상위
n=4~10선별 - 컨텍스트를 “근거 중심”으로 재구성
- LLM 답변 생성
- 인용/근거 검사 및 거절 전략 적용
의사코드 예시입니다.
def answer(user_question: str):
candidates = retrieve_candidates(user_question, top_k=60)
# rerank: 질문과 chunk를 함께 넣고 관련도 점수화
scored = rerank(user_question, [c.text for c in candidates])
top = select_top_n(scored, n=6)
context = build_context_with_citations(top)
prompt = f"""
너는 근거 기반 어시스턴트다.
- 제공된 컨텍스트에 없는 내용은 추측하지 말고 '모르겠다'고 답하라.
- 답변에는 근거 chunk id를 인용하라.
질문:
{user_question}
컨텍스트:
{context}
"""
return llm_generate(prompt)
MDX 렌더링 환경에서는 인용 형식을 chunk_id처럼 안전한 텍스트로 두고, 원문 링크를 별도 필드로 관리하는 편이 빌드 이슈와도 분리되어 좋습니다.
튜닝 포인트 1: 후보군 k와 최종 n의 균형
HyDE를 넣으면 후보군 k를 더 크게 잡아야 효과가 납니다. 하지만 k가 커지면 rerank 비용이 증가합니다.
권장 시작점:
- 후보군
k: 40~80 - 최종 컨텍스트
n: 4~8
튜닝 방법:
k를 늘렸는데도 정답 근거가 안 들어오면 chunking 또는 임베딩 모델 문제일 가능성이 큼k를 늘리면 정답이 들어오는데 답이 흔들리면 rerank 또는 컨텍스트 구성 문제일 가능성이 큼
튜닝 포인트 2: Rerank 스코어 임계값으로 “답변 거절” 만들기
환각을 줄이는 가장 강력한 방법 중 하나는 “근거가 약하면 답하지 않기”입니다.
- 최상위 rerank score가 낮으면 컨텍스트가 질문과 잘 안 맞는 상태
- 이때는 답변을 생성해도 환각 확률이 급증
예시 로직:
def should_abstain(rerank_scores, threshold: float = 0.35):
best = max(rerank_scores)
return best < threshold
임계값은 모델과 스케일에 따라 달라서, 반드시 오프라인 평가로 잡아야 합니다. 운영에서는 다음과 같은 UX가 좋습니다.
- “현재 문서에서 근거를 찾지 못했습니다. 관련 키워드로 다시 질문해 주세요.”
- “다음 중 어떤 의미인가요” 형태로 질문을 분해해 재질문 유도
튜닝 포인트 3: HyDE 편향 줄이기 위한 멀티 HyDE
질문이 복합적이거나 애매하면 HyDE 1개로는 특정 방향으로 치우칠 수 있습니다. 이때는 HyDE를 2~3개 샘플링하고 후보를 합치는 방식이 효과적입니다.
temperature를 약간 올려 다양성 확보- 각 HyDE 문서는 짧게 유지
def multi_hyde_docs(user_question: str, m: int = 3):
return [llm_generate_hyde(user_question, temperature=0.7) for _ in range(m)]
def retrieve_with_multi_hyde(user_question: str, top_k: int = 40):
q_base = embed(user_question)
docs = multi_hyde_docs(user_question, m=3)
qs = [embed(d) for d in docs]
results = vector_search(q_base, top_k=top_k)
for q in qs:
results += vector_search(q, top_k=top_k)
return dedup_by_chunk_id(results)
비용이 늘어나므로, “rerank score가 낮을 때만 멀티 HyDE 재시도” 같은 조건부 전략이 현실적입니다.
튜닝 포인트 4: 컨텍스트 재구성은 “요약”보다 “근거 보존”
RAG에서 흔한 실수는, rerank 후 컨텍스트를 다시 LLM으로 요약해 넣는 것입니다. 요약은 정보 손실을 만들고, 그 손실을 LLM이 메우며 환각이 생깁니다.
권장 방식:
- 상위 chunk 원문을 그대로 넣되
- 각 chunk 앞에 짧은 메타데이터를 붙여 충돌을 줄임
[chunk_id=docA-12 | source=runbook | updated=2025-01]
...원문...
[chunk_id=docB-03 | source=policy | updated=2024-11]
...원문...
이때 updated 같은 필드는 “최신 문서 우선” 같은 후속 규칙을 만들 때도 유용합니다.
평가: 환각을 수치로 잡는 최소 지표 세트
튜닝은 감으로 하면 끝이 없습니다. 최소한 아래 3가지는 측정해야 합니다.
1) Retrieval recall@k
- 정답 근거 chunk가 후보군
k안에 들어왔는가
2) Rerank hit@1 또는 hit@n
- 정답 근거가 rerank 상위
n에 들어왔는가
3) Grounded answer rate
- 답변 문장들이 컨텍스트에 의해 뒷받침되는가
간단한 평가 데이터는 다음처럼 만듭니다.
- 질문
- 정답 근거 chunk id(들)
- 기대 답(짧은 형태)
그리고 파이프라인 변경 시 위 지표가 어떻게 변하는지 비교합니다.
운영 팁: 장애 시 환각이 폭증하는 구간을 막기
실제 운영에서는 모델 문제가 아니라 “외부 API 오류”나 “타임아웃”이 환각을 유발하기도 합니다. 예를 들어 rerank 호출이 실패했는데도 그대로 생성 단계로 넘어가면, 컨텍스트 품질이 급락해 환각이 늘어납니다.
따라서 다음을 권장합니다.
- rerank 실패 시 답변 생성 금지 또는 보수적 폴백
- LLM API 500/503에는 재시도, 서킷브레이커, 폴백 모델 적용
관련 실전 패턴은 OpenAI Responses API 500·503 대응 재시도 폴백 서킷브레이커에서 더 자세히 다뤘습니다.
또한 캐시를 쓰는 서비스라면, 잘못된 컨텍스트 조합이 캐시되어 환각이 반복되는 문제가 생길 수 있습니다. App Router 기반이라면 캐시 무효화 전략을 점검해 두는 편이 안전합니다. Next.js App Router 캐시 무효화 7가지 정리도 함께 참고하세요.
실전 체크리스트
아래 체크리스트는 HyDE+Rerank 튜닝을 “한 번에” 점검하기 위한 항목입니다.
HyDE
- HyDE 문서가 단정적 결론을 강요하지 않는가
- 숫자/버전/정책을 임의로 만들어내지 않게 제약했는가
- 길이가 과도하게 길지 않은가
- 듀얼 쿼리로 원 질문 임베딩도 함께 검색하는가
- rerank score가 낮을 때만 멀티 HyDE 재시도를 거는가
Retrieval
- 후보군
k가 충분한가(보통 40~80) - chunking이 답변 단위와 맞는가(표/리스트/섹션 보존)
- 도메인 용어 사전, 동의어 매핑이 필요한가
Rerank
- 상위
n이 너무 크지 않은가(보통 4~8) - 스코어 임계값 기반 거절이 있는가
- 상충하는 문서(버전/지역/정책)가 섞일 때 우선순위 규칙이 있는가
Generation
- “컨텍스트 밖 추측 금지”와 “근거 인용”이 프롬프트에 포함됐는가
- 컨텍스트를 요약해서 넣고 있진 않은가(정보 손실 위험)
마무리: HyDE로 넓히고 Rerank로 잠그기
HyDE는 검색을 “넓히는” 도구이고, Rerank는 컨텍스트를 “잠그는” 도구입니다. 둘을 결합하면 RAG의 대표적 환각 원인인 검색 미스와 컨텍스트 선정 미스를 동시에 줄일 수 있습니다.
실전에서는 다음 순서로 접근하는 것이 가장 빠릅니다.
- 먼저
recall@k를 올려 정답 근거가 후보군에 들어오게 만들고 - 그 다음 rerank로 상위 컨텍스트 품질을 고정한 뒤
- 마지막으로 임계값 거절과 운영 폴백으로 “근거 없을 때 답하지 않기”를 완성합니다
이 3단계를 갖추면, 환각은 프롬프트 요령이 아니라 시스템 설계로 안정적으로 줄어듭니다.