Published on

LangChain LlamaIndex RAG에서 답변이 반복되고 환각될 때 리랭커와 청킹 전략 토큰 예산으로 정확도 2배 올리는 디버깅 체크리스트

Authors

서빙 중이던 RAG가 어느 날부터 같은 문장을 반복하거나, 문서에 없는 내용을 그럴듯하게 지어내는(환각) 패턴이 나타나면 보통 "모델이 바뀌었나?"부터 의심합니다. 하지만 현업에서 더 자주 맞닥뜨리는 원인은 따로 있습니다.

  • 검색 결과가 비슷한 청크로만 채워져 컨텍스트가 단조로워짐
  • 청킹/오버랩이 어긋나 근거가 잘려 들어옴
  • top-k가 늘면서 컨텍스트 윈도우를 초과해 중요한 근거가 잘림
  • 리트리버가 가져온 후보를 LLM에 그대로 던져 관련도 순서가 망가짐
  • 다양성을 확보하지 못해 중복 근거 → 반복 답변으로 이어짐

이 글은 LangChain/LlamaIndex 기반 RAG에서 이런 증상이 발생했을 때, 리랭커(Cohere/Jina)·chunk overlap·MMR·토큰 예산을 중심으로 정확도를 빠르게 끌어올리는 실전 체크리스트입니다.


1) 증상 분류부터 하자: 반복 vs 환각 vs 근거 누락

디버깅은 “증상을 정확히 이름 붙이는 것”에서 절반이 끝납니다.

A. 반복(Looping) 패턴

  • 같은 문장/구문을 변형하며 2~5회 반복
  • 결론을 내지 못하고 “요약하면…”을 계속 말함

주요 원인

  • 검색 결과 청크들이 상호 중복(near-duplicate)
  • top-k가 너무 크고, chunk overlap이 과도해 컨텍스트가 단조로움
  • 프롬프트에 “반복하지 마라” 같은 지시가 약하거나, 출력 제한/stop 조건이 없음

B. 환각(Hallucination) 패턴

  • 문서에 없는 제품명/정책/숫자를 생성
  • “~라고 명시되어 있습니다” 같은 근거형 문장인데 실제로는 없음

주요 원인

  • 리트리버 recall이 낮아 정작 필요한 근거가 안 들어옴
  • 컨텍스트 윈도우 초과로 핵심 근거가 잘려나감
  • 질문이 멀티홉인데 single-shot으로 처리

C. 근거 누락/부분 정답

  • 답은 맞는 듯한데 중요한 조건/예외가 빠짐

주요 원인

  • 청크가 너무 작아 문맥이 끊김(특히 표/정책 문서)
  • overlap이 부족해 문장 경계에서 의미가 손실

이제부터는 “원인 후보 → 빠른 측정 → 처방” 순서로 접근합니다.


2) 가장 먼저 확인할 것: 컨텍스트 윈도우 토큰 예산(잘림이 모든 걸 망친다)

RAG가 갑자기 망가지는 흔한 계기:

  • top_k를 올림
  • chunk_size를 키움
  • 시스템 프롬프트/가드레일을 추가함
  • 대화 히스토리를 더 많이 넣기 시작함

이때 모델 컨텍스트 한도를 넘기면, 많은 프레임워크가 조용히 앞/뒤를 잘라냅니다. 문제는 “어느 부분이 잘렸는지”가 로그에 안 남는 경우가 많다는 점입니다.

토큰 예산을 숫자로 고정하라

아래처럼 입력 토큰을 측정하고, 문서 컨텍스트에 배정할 예산을 고정합니다.

# pip install tiktoken
import tiktoken

enc = tiktoken.get_encoding("cl100k_base")

def count_tokens(text: str) -> int:
    return len(enc.encode(text))

MODEL_CTX = 8192
RESERVED_FOR_OUTPUT = 900   # 답변 길이 예산
RESERVED_FOR_SYSTEM = 700   # 시스템/가드레일
RESERVED_FOR_HISTORY = 1200 # 대화 히스토리

BUDGET_FOR_CONTEXT = MODEL_CTX - (RESERVED_FOR_OUTPUT + RESERVED_FOR_SYSTEM + RESERVED_FOR_HISTORY)
print("context budget:", BUDGET_FOR_CONTEXT)

컨텍스트 조립 시 “예산 초과 시 자르기”가 아니라 “예산 내에서 선택”해야 한다

  • 나쁜 방식: top_k=20을 넣고 마지막에 토큰 초과분을 잘라냄 → 중요한 근거가 잘릴 확률 증가
  • 좋은 방식: 리랭킹/스코어 기반으로 우선순위 정렬 후 예산 내에서 채움

이 단계만 제대로 해도 환각이 눈에 띄게 줄어듭니다.

> 검색 품질 자체가 갑자기 떨어졌다면 벡터DB/정규화/인덱스 설정 문제일 수도 있습니다. 이 경우 PostgreSQL pgvector RAG 검색 품질 급락 원인과 해결 체크리스트를 먼저 점검하세요.


3) chunk size와 chunk overlap: “정답 근거가 잘리는” 전형적인 함정

청킹이 잘못되면 리트리버가 아무리 똑똑해도 답이 흔들립니다.

실무 권장 시작점(문서 유형별)

  • 정책/약관/가이드: chunk_size 8001200 tokens, overlap 100200
  • 기술 문서(섹션이 명확): chunk_size 500900, overlap 80150
  • Q&A/짧은 문단: chunk_size 300600, overlap 50100

overlap이 너무 작을 때

  • 문장/표의 핵심 조건이 경계에서 잘림
  • “예외/단서”가 다음 청크로 넘어가 누락

overlap이 너무 클 때

  • 검색 결과가 비슷한 청크로 도배됨
  • LLM은 근거가 다양하지 않으니 같은 말을 반복하기 쉬움

빠른 진단법: top-5 청크를 눈으로 비교

  • 같은 문단이 70% 이상 겹치면 overlap 과다 또는 chunk_size 과대
  • 질문에 필요한 정의/조건이 청크 경계에서 끊기면 overlap 부족

LlamaIndex 예시: SentenceSplitter로 청킹 튜닝

from llama_index.core.node_parser import SentenceSplitter

splitter = SentenceSplitter(
    chunk_size=900,
    chunk_overlap=120,
)
# 문서를 nodes로 변환하는 파이프라인에 splitter를 적용

4) MMR로 중복을 줄여 반복 답변을 끊는다

반복 답변은 “모델이 멍청해졌다”기보다, 컨텍스트가 단조로워졌을 때 훨씬 잘 발생합니다. 특히 동일 문서에서 유사한 청크가 여러 개 뽑히면 LLM은 안전하게 같은 표현을 재사용합니다.

이때 **MMR(Maximal Marginal Relevance)**가 즉효입니다.

MMR이 해결하는 것

  • 관련도만 최적화(top-k cosine)하면 유사 청크가 몰림
  • MMR은 관련도 + 다양성(중복 페널티)을 같이 최적화

LangChain 예시: MMR 검색

# 예: vectorstore가 FAISS/Chroma/PGVector 등일 때
retriever = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={
        "k": 8,          # 최종 반환
        "fetch_k": 30,   # 후보 풀
        "lambda_mult": 0.6  # 0~1, 낮을수록 다양성↑
    },
)

results = retriever.get_relevant_documents("질문 텍스트")

튜닝 팁

  • 반복이 심하면: lambda_mult를 0.3~0.6으로 낮춰 다양성 확보
  • 정답이 자주 빗나가면: lambda_mult를 0.7~0.9로 올려 관련도 우선

5) 리랭커(Cohere/Jina)로 “가져온 후보”를 제대로 정렬하라

RAG에서 가장 흔한 구조적 문제는 이것입니다.

  1. 벡터 검색은 recall은 괜찮지만 precision이 낮음(비슷한 문서 많이 섞임)
  2. top-k를 늘려 커버하려고 함
  3. 컨텍스트가 길어지고 중복이 늘고 토큰 예산 초과
  4. 환각/반복이 폭발

해결책은 top-k를 무작정 늘리는 게 아니라, fetch_k는 넉넉히 뽑고 리랭커로 정밀하게 top_n을 고르는 것입니다.

Cohere Rerank (개념 예시)

  • fetch_k=30~100 후보를 뽑고
  • rerank top_n=5~10으로 줄여서 컨텍스트 예산 내에 넣기
# 개념 코드(실제 패키지/클래스명은 사용 환경에 맞게 조정)
# pip install cohere
import cohere

co = cohere.Client("COHERE_API_KEY")

query = "질문"
docs = [d.page_content for d in candidates]  # vector search 결과

reranked = co.rerank(
    model="rerank-multilingual-v3.0",
    query=query,
    documents=docs,
    top_n=8,
)

top_docs = [docs[r.index] for r in reranked.results]

Jina Reranker (개념 예시)

Jina 계열 리랭커도 같은 방식으로 적용합니다. 포인트는 “리랭킹 후 컨텍스트에 넣는 문서 수를 줄이고, 대신 관련도를 올린다”입니다.

리랭커 적용 Best Practice

  • 리랭커는 비용/지연이 있으니 캐시(query+doc_hash 기반) 적용
  • 리랭커 입력은 너무 긴 문서보다 청크 단위가 유리
  • 상위 문서가 한 소스에 과도하게 쏠리면 source diversity 규칙 추가(예: 동일 문서 최대 2청크)

6) 컨텍스트 구성 전략: “한 번에 많이”가 아니라 “근거 중심으로 압축”

리랭킹까지 했는데도 토큰이 빡빡하면, 컨텍스트를 그대로 넣지 말고 근거 요약(압축) 단계를 추가합니다.

압축(Compression) 패턴

  • 각 청크에서 질문과 무관한 문장을 제거
  • 표/리스트는 필요한 행만 남김

LangChain에서는 contextual compression retriever 같은 패턴으로 구현할 수 있고, LlamaIndex도 유사한 postprocessor로 구성 가능합니다.

실무에서 효과가 큰 규칙:

  • “정의/조건/예외/수치”만 남기기
  • 문장 수를 강제로 제한(예: 청크당 3~6문장)

7) 반복·환각 디버깅 체크리스트(현업용)

아래 순서대로 하면 보통 30~60분 안에 원인 범위를 좁힐 수 있습니다.

1) 로그/관측부터: 검색 결과와 최종 컨텍스트를 저장

  • query
  • top-k 후보의 (doc_id, chunk_id, score, text 앞 200자)
  • 최종 LLM에 들어간 컨텍스트 전문
  • 최종 입력 토큰 수 / 잘림 여부

2) 토큰 예산 초과 여부

  • 초과라면: top_k 줄이기보다 fetch_k↑ + rerank top_n↓로 전환
  • 시스템/히스토리/출력 예산을 고정하고 컨텍스트 예산을 지키기

3) 중복도 측정

  • 최종 컨텍스트 청크 간 Jaccard/SimHash로 중복률 측정
  • 중복률이 높으면: MMR 적용 + overlap 축소 + source diversity

4) 청킹 점검

  • 경계에서 의미가 끊기면 overlap↑
  • 유사 청크가 너무 많으면 overlap↓ 또는 chunk_size↓

5) 리랭커 적용

  • 후보 풀(fetch_k) 30~100
  • top_n 5~12 (토큰 예산에 맞춰)

6) 프롬프트 최소화

  • RAG가 흔들릴 때 프롬프트를 복잡하게 하면 오히려 근거가 묻힘
  • “근거 밖은 모른다” + “근거 인용” 정도로 단순화 후 다시 측정

7) 실패 케이스를 고정한 회귀 테스트 만들기

  • 실패 질문 20~50개를 고정
  • 변경(청킹/MMR/리랭커) 전후의 정확도/근거 인용률 비교

8) 트러블슈팅: 자주 만나는 함정과 처방

함정 A. top_k를 올렸더니 정확도가 떨어짐

  • 원인: 컨텍스트 단조로움 + 토큰 예산 초과
  • 처방: MMR + rerank + top_n 축소 + 예산 기반 컨텍스트 조립

함정 B. overlap을 키웠더니 반복이 심해짐

  • 원인: near-duplicate 청크 증가
  • 처방: overlap을 80~150 범위로 되돌리고, 동일 문서에서 뽑는 청크 수 제한

함정 C. 리랭커 붙였더니 지연이 커짐

  • 처방:
    • rerank 대상 문서 수를 30~60으로 제한
    • 캐시 도입
    • 비동기 처리/배치 처리
    • 타임아웃 시 fallback(리랭커 없이 MMR)

함정 D. 운영 중 429/레이트리밋으로 품질이 출렁임


9) Best Practice 조합 예시: 정확도와 안정성을 같이 잡는 기본 세트

현업에서 무난하게 먹히는 조합은 아래입니다.

  1. 청킹: chunk_size 7001000, overlap 100150에서 시작
  2. 리트리버: fetch_k 40~80
  3. 다양성: MMR로 후보를 1차 정리 또는 source diversity 제한
  4. 리랭커: top_n 6~10
  5. 컨텍스트 예산: 고정(시스템/히스토리/출력) + 컨텍스트 예산 내에서만 조립
  6. 근거 압축: 필요 시 청크당 3~6문장으로 압축
  7. 회귀 테스트: 실패 질문 세트로 전/후 비교

이 세트를 적용하면 “갑자기 반복/환각이 늘어난다” 같은 운영 이슈의 대부분이 검색-컨텍스트 레이어에서 해결됩니다.


결론: 모델 탓하기 전에 RAG 파이프라인을 예산과 순서로 재정렬하라

RAG의 반복·환각은 대개 모델의 성능 문제가 아니라,

  • 컨텍스트 윈도우 토큰 예산 붕괴
  • 청킹/오버랩 설계 미스
  • 중복 제거 실패(MMR 부재)
  • 리랭킹 부재로 인한 낮은 precision

에서 시작됩니다.

오늘 바로 할 일은 간단합니다.

  1. 입력 토큰을 계측해 컨텍스트 예산을 고정하고, 2) fetch_k/rerank top_n 구조로 바꾸고, 3) MMR과 overlap을 조정해 중복을 줄이세요. 그리고 4) 실패 질문 회귀 테스트로 효과를 수치로 확인해보면, “정확도 2배”가 과장이 아니라는 걸 체감하게 됩니다.