- Published on
LlamaIndex+Qdrant RAG 재랭킹으로 환각 30%↓
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
RAG(Retrieval-Augmented Generation)를 붙였는데도 답이 그럴듯하게 틀리는 경우가 생각보다 자주 나옵니다. 특히 벡터 검색 결과가 “주제는 비슷하지만 질문의 핵심을 직접 답하지 못하는 문서”로 채워지면, LLM은 그 빈틈을 자연스럽게 메우며 환각을 만듭니다.
이 글에서는 LlamaIndex + Qdrant 조합에서 재랭킹(reranking) 을 추가해, 실제로 “정답 근거가 되는 청크”가 컨텍스트 상단에 오도록 만들고, 그 결과 환각을 체감상 30% 수준으로 낮추는 접근을 설명합니다. 핵심은 단순히 재랭커를 붙이는 게 아니라, 검색 단계의 후보군(recall)과 재랭킹 단계의 정밀도(precision)를 분리해서 튜닝하는 것입니다.
관련해서 RAG 환각을 줄이는 더 넓은 전략(하이브리드 검색, 재랭커 튜닝)은 아래 글도 같이 보면 이해가 빨라집니다.
왜 Qdrant 벡터 검색만으로는 부족한가
Qdrant의 ANN(Approximate Nearest Neighbor) 벡터 검색은 빠르고 실용적이지만, 다음 한계가 있습니다.
- 임베딩 유사도는 “정답성”을 보장하지 않음
- 질문과 문서가 비슷한 주제를 말해도, 질문이 요구하는 조건(기간, 수치, 예외, 버전)을 만족하지 않을 수 있습니다.
- 청크 분할/노이즈에 취약
- 비슷한 문장 하나 때문에 청크 전체가 상위로 올라오기도 합니다.
- 상위
k결과의 순서가 LLM 답변 품질을 좌우- LLM은 보통 컨텍스트 앞부분에 더 강하게 끌립니다. 상단이 “그럴듯한 오답 근거”면 환각이 늘어납니다.
재랭킹은 여기서 “LLM이 읽을 상위 컨텍스트”를 더 정확히 정렬해, 틀린 근거를 앞쪽에서 치우는 역할을 합니다.
재랭킹이 환각을 줄이는 메커니즘
구조를 단순화하면 아래 파이프라인입니다.
- 후보 검색(Recall): Qdrant에서
top_k = 20~50정도로 넓게 가져오기 - 재랭킹(Precision): 질문-문서 쌍을 더 강한 모델로 점수화해 재정렬
- 컨텍스트 구성: 상위
n = 3~8개만 LLM에 넣기 - 생성: “근거 기반” 프롬프트로 답변
여기서 환각이 줄어드는 이유는:
- 정답 근거가 상단에 오면 LLM이 그 근거를 중심으로 답을 구성
- 주제만 비슷한 문서가 밀려나면 LLM이 빈틈을 메울 기회가 줄어듦
- 재랭커 점수로 임계값(threshold) 을 적용하면 “근거 부족” 상황을 감지해
모름/추가 질문으로 전환 가능
LlamaIndex + Qdrant 구성(기본)
아래 예시는 Python 기준이며, Qdrant를 벡터 스토어로 쓰는 전형적인 형태입니다. 환경에 따라 패키지명/버전이 다를 수 있으니, 코드의 구조를 중심으로 보시면 됩니다.
# pip install llama-index qdrant-client
from qdrant_client import QdrantClient
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.qdrant import QdrantVectorStore
client = QdrantClient(url="http://localhost:6333")
vector_store = QdrantVectorStore(
client=client,
collection_name="docs",
)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_vector_store(
vector_store=vector_store,
storage_context=storage_context,
)
query_engine = index.as_query_engine(similarity_top_k=20)
resp = query_engine.query("장애 발생 시 재시도 정책은 어떻게 설계해야 해?")
print(resp)
이 상태에서도 RAG는 동작하지만, similarity_top_k 결과의 순서가 “유사도 중심”이라 질문의 핵심과 어긋나는 컨텍스트가 상단을 차지할 수 있습니다.
재랭킹 추가: 후보는 넓게, 컨텍스트는 좁게
재랭킹을 붙일 때 가장 중요한 운영 원칙은 아래 2가지입니다.
- Qdrant 검색
top_k는 넓게 가져와서 놓치지 않기 - LLM에 넣는 컨텍스트는 재랭킹 후 상위만 엄선하기
재랭커 선택지
재랭커는 크게 두 부류로 나뉩니다.
- Cross-encoder 계열
- 질문과 문서를 한 번에 넣고 관련도를 직접 점수화
- 정확도가 높지만 비용/지연이 증가
- LLM 기반 rerank
- LLM에게 “질문에 가장 답이 되는 문서 순서”를 매기게 함
- 품질은 좋을 수 있으나 비용/변동성이 큼
실무에서는 보통 cross-encoder를 1차로 쓰고, 정말 중요한 질의에만 LLM rerank를 추가하는 방식이 안정적입니다.
아래는 “후보를 30개 뽑고, 재랭킹 후 상위 5개만 컨텍스트로 쓰는” 형태의 예시입니다.
# 예시용 의사 코드(사용하는 reranker 구현체에 맞게 조정)
from llama_index.core import VectorStoreIndex
# 1) 후보는 넓게
candidate_k = 30
# 2) 최종 컨텍스트는 좁게
final_n = 5
retriever = index.as_retriever(similarity_top_k=candidate_k)
nodes = retriever.retrieve("프로덕션에서 서킷 브레이커 임계값은 어떻게 잡아?")
# rerank(query, nodes) -> nodes를 관련도 순으로 재정렬해 반환한다고 가정
reranked_nodes = rerank(
query="프로덕션에서 서킷 브레이커 임계값은 어떻게 잡아?",
nodes=nodes,
)
context_nodes = reranked_nodes[:final_n]
answer = synthesize_answer(
query="프로덕션에서 서킷 브레이커 임계값은 어떻게 잡아?",
context_nodes=context_nodes,
)
print(answer)
핵심은 candidate_k와 final_n을 분리하는 것입니다. 많은 팀이 top_k=5로 바로 LLM에 넣는데, 이러면 “정답 후보가 6~20위에 숨어있던 케이스”를 영영 못 살립니다.
임계값(threshold)로 “근거 부족”을 감지하기
환각은 “근거가 약한데도 답을 만들 때” 폭발합니다. 재랭킹 점수(혹은 유사도 점수)를 이용해 아래 정책을 넣으면 효과가 큽니다.
- 상위 1위 점수가
min_score미만이면 답변 대신:근거가 부족합니다. 관련 문서를 더 제공해 주세요또는질문을 더 구체화해 주세요로 전환
min_score = 0.35 # 예시: 재랭커 점수 스케일에 맞춰 조정
best = reranked_nodes[0]
if best.score < min_score:
print("근거가 부족해 답변을 보류합니다. 질문을 더 구체화해 주세요.")
else:
context_nodes = reranked_nodes[:5]
print(synthesize_answer(query, context_nodes))
이 한 줄 정책이 “그럴듯한 헛소리”를 “정직한 모름”으로 바꾸는 데 매우 강력합니다.
프롬프트: 재랭킹만으로 부족하면 ‘근거 강제’를 넣어라
재랭킹이 컨텍스트 품질을 올려도, 모델이 여전히 상상으로 메우는 경우가 있습니다. 이때는 생성 프롬프트에 아래 제약을 추가합니다.
- 컨텍스트에 없는 내용은
모름으로 답할 것 - 답변에 인용(출처 청크 ID 또는 요약 근거)을 포함할 것
규칙:
1) 아래 제공된 컨텍스트에 근거한 내용만 답변한다.
2) 컨텍스트에 없는 정보는 "모르겠습니다"라고 답한다.
3) 답변 마지막에 근거로 사용한 청크 ID를 나열한다.
질문: ...
컨텍스트:
- [chunk:123] ...
- [chunk:456] ...
재랭킹과 근거 강제 프롬프트는 궁합이 좋습니다. 재랭킹이 “좋은 근거를 위로” 올리고, 프롬프트가 “근거 밖으로 나가지 않게” 막습니다.
튜닝 포인트 5가지(환각 30%↓에 기여하는 부분)
아래 항목은 실제로 환각이 줄어드는 데 기여도가 큰 순서로 정리했습니다.
1) 후보 top_k를 늘리고, 최종 컨텍스트는 줄이기
- 추천 시작값:
candidate_k=30,final_n=5 - 증상별 조정:
- “아예 엉뚱한 문서만 나옴”이면
candidate_k를 올리기 - “컨텍스트가 길어 답이 흐림”이면
final_n을 내리기
- “아예 엉뚱한 문서만 나옴”이면
2) 청크 크기와 오버랩
- 너무 작으면 문맥이 끊겨 재랭커도 판단이 어려움
- 너무 크면 잡음이 섞여 관련도 판단이 흔들림
- 시작 가이드: 300
800 토큰 범위에서 실험, 오버랩 1020%
3) 메타데이터 필터로 후보군 정제
Qdrant는 페이로드 기반 필터가 강점입니다. 예를 들어:
- 버전별 문서(
version) 필터 - 제품/서비스별(
service) 필터 - 최신 문서 우선(
updated_at) 가중
이걸 재랭킹 전에 적용하면 “비슷하지만 다른 제품 문서”가 섞이는 일이 확 줄어듭니다.
4) 재랭킹 점수 임계값과 폴백
min_score미만이면 답변 보류- 폴백 전략:
- 하이브리드 검색으로 재시도
- 키워드 기반 BM25 후보를 섞기
5) 평가셋을 만들고 숫자로 본다
“환각 30%↓” 같은 개선은 측정 없이는 재현이 어렵습니다. 최소한 아래를 준비합니다.
- 질문 50~200개
- 정답 근거가 되는 문서/청크 라벨(가능하면)
- 지표:
hit@k: 정답 근거가 상위 k에 포함되는 비율faithfulness: 답변 문장이 컨텍스트에 의해 뒷받침되는 비율(샘플링 리뷰라도)
운영에서 자주 터지는 함정
재랭커가 “길이”에 끌리는 문제
긴 청크가 정보량이 많아 보여 점수가 올라가는 경우가 있습니다.
- 해결: 청크 길이 제한, 요약 청크를 별도로 만들기, 혹은 길이 정규화
지연(latency) 증가
재랭킹은 후보 문서 수만큼 점수화를 하므로 비용이 늘어납니다.
- 해결:
candidate_k를 무작정 키우지 말고, 메타데이터 필터로 후보를 줄이기- 캐시 적용(질문 정규화 후 rerank 결과 캐시)
- 트래픽 구간별로 재랭킹 on/off 또는 단계적 적용
“정답이 문서에 없다” 케이스
재랭킹을 아무리 해도 근거가 없으면 답변은 위험합니다.
- 해결: 임계값 기반 보류 + 추가 질문 유도
마무리: 재랭킹은 RAG의 ‘정렬 레이어’다
LlamaIndex + Qdrant 조합에서 재랭킹은 단순 옵션이 아니라, RAG 품질을 한 단계 끌어올리는 정렬 레이어입니다. 후보 검색은 넓게(Recall), 재랭킹으로 좁게(Precision), 그리고 임계값으로 정직하게(모름 처리) 가져가면 “그럴듯한 환각”이 눈에 띄게 줄어듭니다.
다음 단계로는 하이브리드 검색(BM25+벡터)과 재랭커 튜닝을 함께 적용해, 질문 유형별로 최적 파이프라인을 만드는 것을 추천합니다. 위 내부 링크 글의 체크리스트를 같이 적용하면 시행착오를 더 줄일 수 있습니다.