- Published on
Pinecone·Milvus RAG 할루시네이션 줄이는 임베딩 평가법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
RAG에서 할루시네이션이 터질 때 많은 팀이 프롬프트를 먼저 의심합니다. 하지만 운영에서 반복적으로 관찰되는 패턴은 “모델이 거짓말을 했다”가 아니라 “검색이 틀린 근거를 가져왔다” 혹은 “가져올 근거가 없는데도 답하도록 강제했다”에 가깝습니다. 특히 Pinecone·Milvus 같은 벡터DB를 쓰는 구성에서는 임베딩 모델 선택, 청킹, 메타데이터 필터, 인덱스 파라미터, top-k/리랭킹 전략이 검색 품질을 좌우하고, 그 결과가 그대로 할루시네이션 확률로 전이됩니다.
이 글은 Pinecone·Milvus에 공통으로 적용 가능한 “임베딩 평가(embedding evaluation)”를 중심으로, RAG 할루시네이션을 체계적으로 줄이는 실험 설계와 지표를 정리합니다. 핵심은 감으로 튜닝하지 않고, 재현 가능한 데이터셋과 메트릭으로 “검색이 맞는지”를 먼저 검증하는 것입니다.
1) RAG 할루시네이션을 만드는 검색 실패 4가지
임베딩 평가를 하려면, 무엇을 실패로 볼지 먼저 정의해야 합니다. 실무에서 자주 만나는 검색 실패는 다음 네 가지로 정리됩니다.
리콜 부족(Recall failure)
- 정답 문서가 코퍼스에 있는데 top-
k에 못 들어옴 - 원인: 임베딩 모델 부적합, 청킹 과도/부족, 인덱스 근사 오차, 필터 조건 과도
- 정답 문서가 코퍼스에 있는데 top-
정밀도 부족(Precision failure)
- top-
k는 나오지만 대부분이 잡음(비슷한 단어만 공유) - 원인: 도메인 용어 중의성, chunk에 컨텍스트 부족, 하이브리드/리랭킹 부재
- top-
컨텍스트 누락(Context gap)
- 문서는 맞는데 chunk 단위가 잘려 필요한 근거가 없음
- 원인: chunk size/overlap 설계 문제, 구조화 문서(표/코드) 파싱 실패
질문-문서 불일치(Query mismatch)
- 사용자의 질문이 사실상 “정의/절차/정책”인데, 임베딩이 “키워드 유사도”로만 끌어옴
- 원인: 쿼리 리라이트 부재, 멀티쿼리 전략 부재, 임베딩 학습 목적과 태스크 불일치
할루시네이션은 보통 위 실패가 1개 이상 겹칠 때 폭발합니다. 따라서 임베딩 평가는 “정답이 top-k에 들어오는가(리콜)”와 “top-k가 쓸만한가(정밀도)”를 분리해서 봐야 합니다.
2) 평가 데이터셋: Q&A만으로는 부족하다
임베딩 평가를 제대로 하려면 최소한 아래 3종의 라벨이 필요합니다.
- Query(질문)
- Positive context(정답 근거가 포함된 문서/구간)
- Hard negatives(헷갈리기 쉬운 오답 근거)
여기서 hard negative가 없으면, 평가가 지나치게 낙관적으로 나옵니다. 예를 들어 “환불 정책”을 묻는 질문에 “배송 정책” 문서가 자주 섞이는 환경이라면, 배송 정책 chunk를 hard negative로 의도적으로 포함해야 실제 운영과 유사한 난이도를 만들 수 있습니다.
2.1 데이터셋을 만드는 현실적인 방법
- 로그 기반
- 운영 쿼리 로그에서 “사용자가 만족/불만족”을 남긴 케이스를 샘플링
- 불만족 케이스는 hard negative 후보가 풍부합니다
- 문서 기반 합성
- 문서 chunk를 보고 LLM으로 질문을 생성
- 단, 합성 질문은 분포가 단순해지기 쉬우므로 hard negative를 함께 생성해야 합니다
- 휴먼 라벨 최소화
- “정답 문서 ID”까지만 라벨링하고, chunk 단위 매칭은 자동화
- 이후 평가에서 문서 ID 기반 리콜과 chunk 기반 리콜을 분리해 측정
3) 핵심 메트릭: Recall@k만 보면 망한다
RAG 검색 평가에서 자주 쓰는 지표와, 각 지표가 놓치는 함정을 함께 정리합니다.
3.1 Recall@k
- 의미: top-
k안에 정답이 1개라도 있으면 성공 - 장점: RAG에서 가장 중요한 “정답이 들어오기라도 했는가”를 직접 측정
- 함정: 정답이
k=20에 겨우 들어오면, LLM 컨텍스트 제한/리랭킹 부재에서 사실상 실패일 수 있음
권장: Recall@1, Recall@3, Recall@5, Recall@10을 같이 보고, 운영 컨텍스트 윈도우에 맞춰 k를 제한합니다.
3.2 MRR(Mean Reciprocal Rank)
- 의미: 정답이 몇 번째에 등장했는지에 민감
- 장점: “정답이 위로 올라왔는가”를 반영
- 함정: 정답이 여러 개(여러 chunk)인 경우 정의가 애매해질 수 있음
3.3 nDCG@k
- 의미: 관련도(relevance)가 다중 레벨일 때 유용
- 장점: “완전 정답/부분 정답/약간 관련”을 구분 가능
- 함정: 관련도 라벨링 비용이 큼
3.4 Precision@k, HitRate@k
- 의미: top-
k중 관련 문서 비율 - 장점: 잡음이 많은 환경에서 중요
- 함정: RAG는 “하나만 제대로 가져와도” 답이 가능한 경우가 있어, Precision만 보면 과도하게 비관적일 수 있음
3.5 Contextual metrics(컨텍스트 품질)
문서 ID가 맞아도 chunk가 잘려 근거가 없으면 LLM은 추론으로 메꾸려다 할루시네이션을 냅니다. 그래서 아래를 추가로 봅니다.
- Evidence coverage: 정답 근거 문장(또는 키 구절)이 chunk에 포함되는 비율
- Chunk completeness: 정답 근거가 chunk 경계에서 잘리는 빈도
이 두 지표는 “청킹 설계”를 평가하는 데 특히 강력합니다.
4) Pinecone·Milvus 공통 실험 설계: 변수를 분리하라
벡터 검색 품질은 변수가 너무 많아 한 번에 바꾸면 원인 추적이 불가능합니다. 아래 순서로 분리 실험을 권장합니다.
- 임베딩 모델 고정, 청킹만 튜닝
- 청킹 고정, 인덱스/검색 파라미터 튜닝
- 그 다음에 하이브리드(키워드+벡터), 리랭커 추가
특히 Pinecone와 Milvus는 인덱스 타입/근사 탐색 파라미터가 다르지만, “근사 오차로 인한 리콜 손실”이라는 현상은 동일합니다. 따라서 평가셋을 동일하게 두고, 파라미터가 리콜 곡선을 어떻게 바꾸는지 비교해야 합니다.
5) 임베딩 평가 파이프라인 예제(Python)
아래 코드는 “질문-정답 문서 ID”가 있는 평가셋을 기준으로, 벡터DB에서 top-k를 가져와 Recall@k와 MRR을 계산하는 최소 예제입니다. Pinecone·Milvus 모두 “query embedding 생성”과 “top-k 검색 결과의 ID 리스트”만 맞추면 동일하게 쓸 수 있습니다.
from dataclasses import dataclass
from typing import List, Dict, Sequence
@dataclass
class EvalSample:
qid: str
query: str
positive_doc_ids: List[str] # 정답 문서 ID(복수 가능)
def recall_at_k(retrieved: Sequence[str], positives: set, k: int) -> float:
topk = retrieved[:k]
return 1.0 if any(doc_id in positives for doc_id in topk) else 0.0
def mrr(retrieved: Sequence[str], positives: set) -> float:
for i, doc_id in enumerate(retrieved, start=1):
if doc_id in positives:
return 1.0 / i
return 0.0
def evaluate(samples: List[EvalSample], search_fn, ks=(1,3,5,10)) -> Dict[str, float]:
# search_fn(query) -> List[doc_id] (ranked)
totals = {f"recall@{k}": 0.0 for k in ks}
totals["mrr"] = 0.0
for s in samples:
retrieved = search_fn(s.query)
pos = set(s.positive_doc_ids)
for k in ks:
totals[f"recall@{k}"] += recall_at_k(retrieved, pos, k)
totals["mrr"] += mrr(retrieved, pos)
n = max(len(samples), 1)
return {k: v / n for k, v in totals.items()}
5.1 search 함수 어댑터(개념)
- Pinecone:
index.query(vector=..., top_k=..., filter=...) - Milvus:
collection.search(data=[...], anns_field=..., param=..., limit=...)
중요한 건 SDK 차이가 아니라, 아래를 동일 조건으로 맞추는 것입니다.
- 동일한 임베딩 모델과 동일한 전처리
- 동일한 chunk 스키마(문서 ID, chunk ID, 섹션 등)
- 동일한 필터 조건
6) 할루시네이션을 줄이는 “임베딩 평가” 체크리스트
6.1 청킹(Chunking)을 지표로 평가하기
청킹은 감으로 정하면 안 됩니다. 아래 케이스를 평가셋에 반드시 포함하세요.
- 정의형 질문: “
X가 무엇인가” - 절차형 질문: “어떻게 설정하는가”
- 예외/제약 질문: “언제 안 되는가”
- 비교 질문: “
A와B차이”
그리고 chunk size/overlap을 바꿔가며 Recall@k뿐 아니라 evidence coverage를 같이 봅니다. 정의형은 작은 chunk가 유리한 반면, 절차형은 문맥이 길어야 성공하는 경우가 많습니다.
6.2 Hard negative로 “그럴듯한 오답”을 막기
할루시네이션은 “정답 부재”보다 “그럴듯한 오답 근거”에서 더 자주 발생합니다. 따라서 평가셋에 아래를 hard negative로 넣고, Precision@k 또는 nDCG@k로 감시합니다.
- 같은 제품군이지만 다른 버전 문서
- 비슷한 용어를 쓰지만 정책이 다른 문서
- 오래된 문서(Deprecated)
이때 메타데이터(버전, 유효기간, 서비스명) 필터가 실제로 작동하는지도 같이 검증해야 합니다.
6.3 임베딩 모델 비교는 “오프라인 + 온라인”으로
오프라인에서 Recall@k가 높아도, 온라인에서 사용자 질문 분포가 다르면 성능이 쉽게 무너집니다. 권장 흐름은 다음과 같습니다.
- 오프라인: 평가셋으로 모델 A/B의 Recall@k, MRR, nDCG 비교
- 온라인: 트래픽의 일부에만 적용해 클릭/만족도/재질문율 비교
재질문율은 “검색이 틀려서 다시 묻는” 신호일 때가 많아, RAG 품질 모니터링에 유용합니다.
7) Pinecone·Milvus에서 자주 터지는 운영 이슈와 평가의 연결
벡터 검색은 네트워크/타임아웃/리트라이 정책에 따라 결과가 달라질 수 있습니다. 예를 들어 검색 요청이 타임아웃으로 실패하면, 애플리케이션이 fallback으로 “검색 없이 답변”을 시도하면서 할루시네이션이 급증할 수 있습니다. 이런 경우는 검색 품질 이전에 “요청 신뢰성” 문제입니다.
- 타임아웃/데드라인 설계는 분산 환경에서 특히 중요합니다. gRPC 기반으로 벡터 검색을 감싸는 MSA라면 데드라인 전파 패턴을 점검해야 합니다. 관련해서는 gRPC MSA에서 Deadline Exceeded 원인과 패턴도 함께 참고하면 좋습니다.
- LLM Tool 호출로 검색을 붙였다면, 파라미터 스키마 불일치로 검색 자체가 실패하는 경우도 많습니다. 이때는 에러를 “조용히 무시”하지 말고, 실패 시 답변을 중단하거나 추가 질문으로 전환하는 가드레일이 필요합니다. 실전 오류 패턴은 LangChain Tool Calling 400 invalid_request 오류 9가지에 정리되어 있습니다.
- 에이전트가 재검색을 반복하다 컨텍스트를 오염시키는 문제(무한 루프, 근거 빈약한 반복)도 할루시네이션을 키웁니다. 종료조건/가드레일은 AutoGPT 무한루프 막는 종료조건·가드레일 설계 관점으로 같이 설계하는 편이 안전합니다.
8) 실무 권장 아키텍처: “검색 평가”를 CI에 넣기
임베딩 모델을 바꾸거나 청킹 로직을 수정할 때마다 검색 품질이 조용히 퇴화하는 경우가 많습니다. 이를 막으려면 평가를 CI에서 자동으로 돌려 “품질 게이트”를 걸어야 합니다.
- PR마다 오프라인 평가 실행
- 최소 기준 예시
Recall@5는 기존 대비-1%p이상 하락하면 실패MRR은 기존 대비 하락하면 경고- 특정 카테고리(정책/보안/결제) 쿼리는 별도 기준 적용
아래는 매우 단순화한 형태의 “기준 미달 시 실패” 예시입니다.
BASELINE = {
"recall@5": 0.86,
"mrr": 0.62,
}
THRESHOLDS = {
"recall@5_drop": 0.01, # 1%p 이상 하락 금지
"mrr_drop": 0.00,
}
metrics = {
"recall@5": 0.85,
"mrr": 0.62,
}
if BASELINE["recall@5"] - metrics["recall@5"] > THRESHOLDS["recall@5_drop"]:
raise SystemExit("Recall@5 regression")
if BASELINE["mrr"] - metrics["mrr"] > THRESHOLDS["mrr_drop"]:
raise SystemExit("MRR regression")
이렇게 해두면 “문서 파서 변경”, “chunk 분할 로직 변경”, “메타데이터 필터 추가” 같은 사소한 변경이 실제로는 할루시네이션을 키우는 방향으로 가는 것을 초기에 차단할 수 있습니다.
9) 결론: 할루시네이션은 검색 품질로 먼저 잡힌다
Pinecone·Milvus 같은 벡터DB를 쓴 RAG에서 할루시네이션을 줄이는 가장 빠른 길은, 프롬프트 엔지니어링보다 먼저 “임베딩과 검색을 평가 가능한 시스템”으로 만드는 것입니다.
- Recall@k와 MRR로 “정답이 들어오는가/위로 오는가”를 측정하고
- hard negative로 “그럴듯한 오답”을 억제하며
- evidence coverage로 “청킹이 근거를 담는가”를 확인하고
- 이 평가를 CI와 온라인 실험으로 연결하면
RAG의 품질 문제를 감이 아니라 데이터로 다룰 수 있습니다. 그 결과로 모델이 답을 지어내는 상황 자체가 줄어들고, 남는 할루시네이션은 “답변 중단, 추가 질문, 근거 부족 고지” 같은 제품 레벨 가드레일로 다루기 쉬워집니다.