- Published on
LangChain LlamaIndex RAG에서 답변이 반복되고 환각될 때 리랭커와 청킹 전략 토큰 예산으로 정확도 2배 올리는 디버깅 체크리스트
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 중이던 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 800
1200 tokens, overlap 100200 - 기술 문서(섹션이 명확): chunk_size 500
900, overlap 80150 - Q&A/짧은 문단: chunk_size 300
600, 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에서 가장 흔한 구조적 문제는 이것입니다.
- 벡터 검색은 recall은 괜찮지만 precision이 낮음(비슷한 문서 많이 섞임)
- top-k를 늘려 커버하려고 함
- 컨텍스트가 길어지고 중복이 늘고 토큰 예산 초과
- 환각/반복이 폭발
해결책은 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/레이트리밋으로 품질이 출렁임
- 리랭커/LLM 호출이 몰리면 재시도 로직이 컨텍스트 구성을 망치거나 지연이 폭증할 수 있습니다.
- 토큰 예산과 큐잉/백오프를 함께 설계하세요: OpenAI API 429 폭탄 대응 실전 가이드 지수 백오프 큐잉 토큰 버짓으로 비용과 지연을 함께 줄이기
9) Best Practice 조합 예시: 정확도와 안정성을 같이 잡는 기본 세트
현업에서 무난하게 먹히는 조합은 아래입니다.
- 청킹: chunk_size 700
1000, overlap 100150에서 시작 - 리트리버: fetch_k 40~80
- 다양성: MMR로 후보를 1차 정리 또는 source diversity 제한
- 리랭커: top_n 6~10
- 컨텍스트 예산: 고정(시스템/히스토리/출력) + 컨텍스트 예산 내에서만 조립
- 근거 압축: 필요 시 청크당 3~6문장으로 압축
- 회귀 테스트: 실패 질문 세트로 전/후 비교
이 세트를 적용하면 “갑자기 반복/환각이 늘어난다” 같은 운영 이슈의 대부분이 검색-컨텍스트 레이어에서 해결됩니다.
결론: 모델 탓하기 전에 RAG 파이프라인을 예산과 순서로 재정렬하라
RAG의 반복·환각은 대개 모델의 성능 문제가 아니라,
- 컨텍스트 윈도우 토큰 예산 붕괴
- 청킹/오버랩 설계 미스
- 중복 제거 실패(MMR 부재)
- 리랭킹 부재로 인한 낮은 precision
에서 시작됩니다.
오늘 바로 할 일은 간단합니다.
- 입력 토큰을 계측해 컨텍스트 예산을 고정하고, 2) fetch_k/rerank top_n 구조로 바꾸고, 3) MMR과 overlap을 조정해 중복을 줄이세요. 그리고 4) 실패 질문 회귀 테스트로 효과를 수치로 확인해보면, “정확도 2배”가 과장이 아니라는 걸 체감하게 됩니다.