Published on

RAG 환각 줄이기 - LlamaIndex+Weaviate 리랭킹 튜닝

Authors

RAG에서 환각(hallucination)은 보통 모델이 “없던 근거를 만들어내는 문제”로 설명되지만, 실무에서 더 자주 마주치는 형태는 검색 결과는 있었는데도 (1) 관련 없는 문서가 섞이거나 (2) 중요한 근거가 상위에 못 올라오거나 (3) 컨텍스트가 너무 길어 핵심이 희석되면서 답변이 틀어지는 케이스입니다.

이 글은 LlamaIndex + Weaviate 조합에서 환각을 줄이기 위해 가장 효과가 컸던 축인 리랭킹(reranking) 튜닝을 중심으로, “어디를 어떻게 계측하고 어떤 파라미터를 어떤 순서로 조정할지”를 실전 관점에서 정리합니다.

참고로 검색 품질이 급락하는 원인을 진단하는 체크리스트는 아래 글도 같이 보면 좋습니다.

환각을 줄이는 핵심: 검색이 아니라 “정렬”

Weaviate가 벡터 검색으로 잘 가져오더라도, LLM이 답을 만들 때는 결국 상위 N개 컨텍스트에 크게 의존합니다. 즉 환각을 줄이는 가장 직접적인 방법 중 하나는:

  • 후보군(recall)을 충분히 넓게 가져오고
  • 그 후보군을 질문 의도에 맞게 정확히 재정렬해서
  • LLM에 들어가는 컨텍스트를 “짧고, 정답 근거가 상단에 오게” 만드는 것

입니다.

여기서 리랭킹은 단순히 “점수 다시 계산”이 아니라, 다음을 동시에 해결합니다.

  • 근접하지만 다른 주제(near-miss) 문서가 상위에 끼는 문제
  • 질문이 복합 조건일 때 한 조건만 맞는 문서가 상위에 오는 문제
  • chunk가 길거나 헤더/푸터가 많아 embedding이 흐려진 문서가 상위에 오는 문제

전체 파이프라인: Retrieve → Rerank → Synthesize

LlamaIndex 관점에서 최소 구성은 보통 아래 흐름입니다.

  1. Weaviate에서 top k 후보를 뽑는다(벡터/하이브리드)
  2. 리랭커가 후보를 재정렬하고 top n을 남긴다
  3. 답변 생성(요약/인용/근거 포함)

환각을 줄이려면 2번 리랭킹을 “형식적으로 붙이는 수준”이 아니라, k/n/하이브리드 비율/필터/스코어 임계값을 함께 튜닝해야 합니다.

Weaviate 쿼리 전략: 후보군을 넓히되, 쓰레기를 줄인다

리랭킹이 강력하다고 해도 입력 후보군이 너무 오염되면(무관 문서 다수) 리랭커가 상단을 제대로 세우기 어렵습니다. 반대로 후보군이 너무 좁으면(정답 근거가 후보에 없음) 리랭킹은 아무것도 못 합니다.

권장 기본값(출발점)

  • 후보군 k: 30~100
  • 최종 컨텍스트 n: 5~12
  • chunk 크기: 300~800 토큰(도메인에 따라)

여기서 중요한 건 kn의 비율입니다.

  • k가 너무 작으면 “정답 근거 누락”으로 환각이 증가
  • n이 너무 크면 컨텍스트가 길어져 “핵심 희석”으로 환각이 증가

하이브리드 검색을 쓰는 이유

도메인 용어/에러 코드/식별자 같은 것들은 벡터만으로는 놓치기 쉽습니다. Weaviate의 하이브리드(키워드+벡터)로 recall을 올리고, 리랭커로 precision을 회복하는 구성이 실전에서 안정적입니다.

LlamaIndex + Weaviate + 리랭커 예제 코드

아래 코드는 “Weaviate에서 넓게 가져오고(top k), 리랭커로 정렬한 뒤(top n) 답변 생성”의 뼈대입니다. (버전에 따라 import 경로가 달라질 수 있으니, 사용하는 LlamaIndex 버전에 맞춰 조정하세요.)

import os
import weaviate

from llama_index.core import VectorStoreIndex, Settings
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.vector_stores.weaviate import WeaviateVectorStore

# 1) Weaviate 연결
client = weaviate.Client(
    url=os.environ["WEAVIATE_URL"],
    additional_headers={
        "X-OpenAI-Api-Key": os.environ.get("OPENAI_API_KEY", "")
    },
)

# 2) VectorStore / Index 구성
vector_store = WeaviateVectorStore(
    weaviate_client=client,
    index_name="Docs",
    text_key="text",
)
index = VectorStoreIndex.from_vector_store(vector_store)

# 3) Retriever (후보군 k를 넓게)
retriever = index.as_retriever(similarity_top_k=50)

# 4) Reranker (상위 n만 남김)
reranker = SentenceTransformerRerank(
    model="cross-encoder/ms-marco-MiniLM-L-6-v2",
    top_n=8,
)

query_engine = RetrieverQueryEngine(
    retriever=retriever,
    node_postprocessors=[reranker],
)

response = query_engine.query("배포 중 ImagePullBackOff가 뜨는 원인과 확인 순서 알려줘")
print(str(response))

위 구성에서 환각 감소에 직접 영향을 주는 레버는 다음입니다.

  • similarity_top_k(후보군 크기)
  • 리랭커 모델 선택(크로스 인코더 계열 권장)
  • top_n(최종 컨텍스트 개수)

또한 답변 생성 단계에서 “근거 없으면 모른다고 말하기”를 강제하는 프롬프트/정책도 중요합니다. 정확도 검증 패턴은 아래 글의 접근이 사고방식 측면에서 도움이 됩니다.

리랭커 튜닝 체크리스트

1) k를 올렸는데 환각이 느는 경우: 후보군 오염

k를 20에서 100으로 올렸더니 오히려 답이 헛소리가 되는 경우가 있습니다. 이는 리랭커가 충분히 강하지 않거나, 데이터가 서로 비슷한 표현으로 뭉쳐 있어 상위에 잡음이 올라오는 패턴입니다.

해결 순서:

  1. 하이브리드 검색을 쓰고 있다면 alpha(벡터 비중)를 조정해 키워드 기반의 정밀도를 보강
  2. 메타데이터 필터(문서 타입, 제품, 버전, 날짜, 언어)를 먼저 적용해 후보군을 “깨끗하게” 만들기
  3. 리랭커 top_n을 줄여 컨텍스트 길이를 줄이기

핵심은 “리랭커로 해결하기 전에, 후보군의 분포를 정상화”하는 것입니다.

2) n이 큰데 근거 인용이 약해지는 경우: 컨텍스트 희석

top_n을 15~20으로 늘리면 답변이 길어지면서, 모델이 근거를 정확히 집지 못하고 일반론으로 흐르는 일이 잦습니다.

권장:

  • top_n을 5~12 범위에서 시작
  • 대신 k를 올려 후보군에서 정답 근거를 찾을 확률을 높이고, 리랭커로 상단에 올리기

3) 리랭커 모델 선택: “크로스 인코더”가 체감이 크다

환각을 줄이는 목적이라면, 임베딩 기반의 단순 재정렬보다 크로스 인코더(cross-encoder) 계열이 효과가 큰 편입니다. 이유는 질문-문서 쌍을 함께 보고 “정답성”을 평가하기 때문입니다.

  • 장점: 상위 몇 개 문서를 매우 정확히 고름
  • 단점: 비용/지연 증가(후보군 k에 비례)

실전 팁:

  • 온라인 서비스라면 k를 30~60으로 제한하고 캐시를 적극 사용
  • 배치/오프라인이라면 k를 100 이상으로 늘려도 됨

4) 스코어 임계값(컷오프)로 “근거 부족”을 감지

리랭커를 붙였는데도 환각이 남는 가장 현실적인 이유는 “정답 근거가 애초에 없다”입니다. 이 경우는 답을 만들지 말아야 합니다.

전략:

  • 리랭커 상위 1~3개 점수가 특정 임계값 미만이면
    • “관련 문서를 찾지 못했다”로 응답
    • 또는 사용자에게 추가 정보를 요청

코드(개념 예시):

# rerank 결과에서 상위 점수들을 확인한 뒤, 임계값 미만이면 거절/질문 유도
MIN_RERANK_SCORE = 0.25

nodes = query_engine.retrieve("질문")
# nodes가 (node, score) 형태로 나오지 않는 구현도 있어, 사용 중인 버전에 맞게 조정 필요

top_score = nodes[0].score if nodes else 0.0
if top_score < MIN_RERANK_SCORE:
    print("근거를 찾지 못했습니다. 대상 시스템/버전/에러 로그를 더 알려주세요.")
else:
    resp = query_engine.query("질문")
    print(resp)

임계값은 데이터와 리랭커 모델에 따라 분포가 달라서, 반드시 로그를 쌓아 히스토그램을 보고 정해야 합니다.

Weaviate에서 리랭킹 효과를 끌어올리는 데이터 설계

리랭킹은 “문서가 질문에 답할 수 있는 형태”일 때 성능이 급상승합니다. 아래는 환각 감소에 직결되는 데이터 측면 팁입니다.

1) chunk에 “질문에 답이 되는 문장”이 들어가게 쪼개기

  • 헤더/푸터/네비게이션이 chunk를 오염시키면 리랭커가 헷갈립니다.
  • 표/코드 블록은 텍스트화 규칙을 정해 일관되게 저장하세요.

2) 메타데이터를 적극적으로 저장하고 필터로 사용

예:

  • product, module, version, lang, doc_type, created_at

질문이 “특정 버전”을 포함할 때 필터가 없으면, 리랭커가 버전 간 충돌 문서를 상단에 올려 환각이 늘어납니다.

3) 중복 문서 제거 또는 군집화

비슷한 문서가 여러 개 있으면 리랭커 상위가 “같은 말 반복”으로 채워져, 정작 필요한 다른 관점의 근거가 컨텍스트에 못 들어옵니다.

  • 인덱싱 전에 near-duplicate 제거
  • 혹은 retrieval 이후 MMR(Maximal Marginal Relevance) 같은 다양성 확보를 섞고 리랭킹

평가/계측: 환각을 “튜닝 가능한 숫자”로 만들기

리랭킹 튜닝은 감으로 하면 끝이 없습니다. 최소한 아래 3가지는 숫자로 봐야 합니다.

  1. Answer faithfulness: 답변 문장들이 제공된 컨텍스트로부터 지지되는가
  2. Context precision: 상위 n개 컨텍스트 중 실제로 도움이 된 비율
  3. Context recall: 정답 근거가 후보군 k 안에 들어왔는가

실전에서는 간단히 다음 로그를 남겨도 효과가 큽니다.

  • query
  • retrieve top k 문서 ID와 점수
  • rerank 후 top n 문서 ID와 점수
  • 최종 답변
  • (가능하면) 사용자 피드백 또는 정답 라벨

이 데이터를 쌓으면,

  • “retrieve 단계에서 이미 누락”인지
  • “rerank가 잘못 올림”인지
  • “synthesis가 컨텍스트를 무시”하는지

원인을 분해할 수 있어 환각 대응이 빨라집니다.

실전 튜닝 순서(추천)

  1. 메타데이터 필터부터 적용해 후보군 오염 줄이기
  2. Weaviate 검색을 하이브리드로 바꾸고 k를 30~60으로 확대
  3. 크로스 인코더 리랭커 적용, top_n을 8 전후로 시작
  4. 컨텍스트가 길면 top_n을 줄이고 chunk를 재조정
  5. 리랭커 점수 임계값으로 “근거 부족”을 감지해 답변 거절/추가 질문
  6. 실패 케이스를 모아 chunking/중복 제거/필터/alpha를 재조정

마무리

LlamaIndex+Weaviate RAG에서 환각을 줄이는 가장 빠른 길은 “더 똑똑한 LLM”이 아니라, 후보군을 넓게 가져오되 리랭킹으로 상단을 정확히 세우고, 근거가 없으면 답을 만들지 않게 하는 것입니다.

특히 ktop_n의 균형, 크로스 인코더 리랭커 선택, 메타데이터 필터링, 그리고 리랭커 점수 임계값 기반의 거절 정책은 환각을 체감 수준으로 낮춰줍니다. 다음 단계로는 실패 로그를 기반으로 “어떤 질문 유형에서 어떤 문서가 반복적으로 잘못 올라오는지”를 분석해, chunking과 문서 정규화를 함께 개선해보세요.