검색 품질, 평가, 그리고 실무 파이프라인
RAG를 만들다 보면 어느 순간 이런 벽에 부딪힌다.
"잘 되는 건 진짜 잘 되는데, 안 되는 건 죽어도 안 됨"
"평균 정답률이 60~70%에서 더 안 올라감"
이게 RAG의 보편적인 한계 지대다. 이번 글에서는 왜 그런 일이 일어나는지, 그리고 그걸 측정하고 개선하기 위한 평가 체계와 실무 파이프라인을 정리한다.
RAG 품질이 60~70%에서 멈추는 9가지 이유
1. Chunking 품질 문제
청크 자르기가 검색의 절반이다.
나쁜 예: 1000자씩 무조건 자르기
→ 한 청크 안에 여러 토픽이 섞이고
한 토픽이 두 청크에 걸쳐 잘림
좋은 예: 의미 단위로 자르기
→ 제목/소제목/문단 단위
→ 표는 통째로
→ 코드 블록은 통째로
추천 시작점:
- 마크다운/HTML이면 헤더 단위로 자름
- 일반 텍스트는 500~1000자 + 100자 오버랩
- 표/코드는 절대 자르지 말 것
- 메타데이터로 "어느 문서의 몇 번째 청크"를 박아둘 것
2. Query Rewrite 실패
사용자 질문 그대로 검색하면 안 잡히는 경우가 많다.
사용자: "이거 왜 안 돼?" ← 검색하면 아무것도 안 나옴
Rewrite 노드가 필요하다.
입력: "이거 왜 안 돼?" + 직전 대화 맥락
출력: "결제 한도 초과 4023 에러 원인"
Rewrite를 LLM으로 하든, 규칙으로 하든, 안 하면 60% 천장에 자주 부딪힌다.
3. Embedding 모델의 한계
영어 임베딩 모델을 한국어에 그대로 쓰면 품질이 떨어진다. 도메인 특화 용어(의료, 법률, 금융)도 일반 임베딩으론 약하다.
대응:
- 한국어 특화 모델 (KURE, BGE-m3 multilingual)
- 도메인이 특수하면 fine-tuning 검토
- 그래도 안 되면 Hybrid에서 Sparse 비중을 올림
4. 문서 자체에 답이 없는 경우
이건 검색의 문제가 아니다.
사용자: "주차 가능해?"
문서: 회사 위키에 주차 얘기가 어디에도 없음
→ 검색이 아무리 잘 돼도 답이 안 나옴
이런 케이스는 "문서에 없습니다" 라고 답하게 만드는 게 중요하다. 환각보다 모른다고 답하는 게 천 배 낫다.
SYSTEM = """
주어진 문서에 답이 명확히 있으면 그 내용으로 답한다.
없으면 반드시 "문서에 답이 없습니다"라고만 답한다.
추측하지 않는다.
"""5. 권한 필터에 좋은 문서가 걸러지는 경우
권한 설계가 너무 엄격하면 의외로 자주 일어난다.
사용자: "휴가 며칠이야?"
답이 있는 문서: 인사팀 전용으로 잠겨 있음
→ 검색에서 제외됨 → 답 못함
해결은 정책 쪽이다.
- 공용으로 풀 수 있는 문서를 추출해서 별도 인덱스로
- 사용자 그룹에 맞는 "공개 가능 요약본"을 따로 운영
6. top-k가 너무 작거나 너무 큰 경우
top-k = 3: 답 있는 문서가 4등이면 못 봄
top-k = 30: 노이즈 문서가 컨텍스트를 흐림
→ LLM이 헷갈리거나 비용 폭발
추천 시작점: Hybrid top-20 → Reranker → top-5 LLM에 주입.
7. Reranker 부재
Hybrid만으로는 한 끗이 부족한 경우가 많다.
Hybrid top-20 안에는 답이 있는데
top-5에 못 들어와서 LLM이 못 봄
→ Reranker로 top-5에 끌어올려야 함
8. Context Window에 넣는 방식
같은 문서를 넣어도 어떻게 넣냐에 따라 결과가 다르다.
나쁜 예: 그냥 본문 다 이어 붙임
좋은 예:
[문서 1: "결제 한도 정책"]
{내용}
[문서 2: ...]
질문: ...
형식: 답은 [문서 X]를 인용해서 답한다.
문서 경계와 인용 형식을 명시하면 LLM이 인용하기 쉽고, 사용자도 출처를 추적할 수 있다.
9. Evaluation Dataset이 없는 경우
이게 가장 흔한 진짜 원인.
"어디가 문제인지 모르겠다"
→ 평가셋이 없어서 개선 방향을 정량적으로 볼 수 없다
평가셋 없으면 어떤 변경이 좋아진 건지 모른다. 60%에서 70%로 올랐는지, 80%인 케이스 일부가 50%로 떨어진 건지 구분이 안 된다.
평가 지표
검색 품질은 보통 다음 지표로 본다.
Recall@k
정답 문서가 top-k 안에 들어왔는가
가장 기본. 일단 답이 있는 문서가 후보 안에 들어와야 그 다음을 따질 수 있다.
Precision@k
top-k 중에 진짜 관련 있는 문서 비율
노이즈 정도를 보는 지표.
MRR (Mean Reciprocal Rank)
정답 문서가 몇 등이었나 (1등 = 1.0, 2등 = 0.5, ...)
답이 위쪽에 잘 올라오는지 보는 지표.
nDCG@k
순위에 따른 누적 점수
순위가 중요할 때(top-5를 LLM에 주입할 때) 가장 쓸모 있다.
LLM-as-Judge
LLM에게 "이 답이 정답이냐?"를 물어서 점수화
자동 평가에 자주 쓴다. 단, 사람 평가와 LLM 평가의 일치도를 한 번은 측정해야 한다.
Evaluation Dataset 만드는 법
처음부터 완벽한 셋을 만들려면 못 만든다. 순서는 이렇게.
1. 사용자 실제 질문 로그에서 100건 샘플링
2. 각 질문에 대해 "정답이 들어 있는 문서 id" 라벨링
3. 라벨링은 운영팀/도메인 전문가가 함
4. LLM-as-Judge로 자동 채점기 구성
5. Langfuse나 LangSmith로 결과 기록
6. 주기적으로 (주 1회) 평가 → 회귀 발견
100건이면 시작으로 충분하다. 분기마다 50건씩 추가하면 1년 뒤 300건이 된다.
실무 검색 프로세스 13단계
회사에서 RAG를 운영할 때 실제로 일어나는 흐름.
큰 흐름을 압축하면 이렇게 생겼다.
세부 단계는 이렇다.
1. User Query 수신
2. Query Normalization
(소문자화, 특수문자 제거, 동의어 치환)
3. Query Rewrite
(대화 맥락 반영, 검색 키워드 추출)
4. Sparse 검색 (BM25) + Metadata Filter (권한/날짜/타입)
5. Dense 검색 (Vector) + 같은 Metadata Filter
6. Score Fusion (RRF로 후보 통합)
7. Reranking (bge / Cohere / LLM)
8. Top-k Context 선택
9. Context Packing
(문서 경계, 인용 라벨, 토큰 예산 관리)
10. LLM 답변 생성
11. Citation 생성
(답에 [문서 1] 같은 인용을 박아넣음)
12. 사용자 피드백 수집
(👍 / 👎, 별점, 정정 요청)
13. Offline / Online Evaluation
(평가셋 회귀 + A/B 테스트)
→ 결과를 다시 1번부터 개선
각 단계마다 흔히 만나는 고민.
2번 : "강민호"와 "강 민호"가 다른 토큰이 되면 BM25에서 미스
3번 : Rewrite를 너무 공격적으로 하면 원 의도가 사라짐
4-5번: 권한 필터를 둘 다 안 걸면 유출 사고
6번 : RRF의 k값을 너무 작게 잡으면 점수 차이가 안 남
7번 : Reranker latency가 SLA를 깨면 운영 의문 제기됨
9번 : 토큰 예산 안 잡으면 비용 폭발
11번 : Citation을 강제하지 않으면 사용자가 답을 못 믿음
12번 : 피드백 UI가 없으면 평가셋이 늘지 않음
디렉토리 구조: FastAPI + Hybrid Retriever + Reranker
실무에서 이런 구조를 추천한다.
app/
main.py FastAPI 엔트리포인트
agent/
graph.py LangGraph StateGraph 정의
state.py State TypedDict
nodes.py query_rewrite / retrieve / answer 노드
retriever/
base.py Retriever 인터페이스 (Protocol)
elasticsearch.py BM25 + Metadata Filter
vector.py pgvector / Pinecone 어댑터
hybrid.py RRF 합산
reranker.py Cohere or bge-reranker
llm/
provider.py Claude / OpenAI / Gemini 어댑터
prompt_builder.py 캐시 친화적 프롬프트 조립
observability/
langfuse.py CallbackHandler 설정, 표준 metadata
evaluation/
dataset.py 평가 데이터 로딩
metrics.py Recall/MRR/nDCG 계산
rag_eval.py 평가 파이프라인 진입점
각 컴포넌트 책임을 한 줄로.
agent/ : 흐름 제어. 노드 책임만 알고, 어떤 retriever인지 모름
retriever/ : "검색"이라는 단일 책임. Reranker도 여기 둠
llm/ : LLM provider 추상화 + 프롬프트 조립
observability/: trace, metadata, feedback 수집
evaluation/ : 정량 평가. CI에 붙여서 회귀 감지
이렇게 책임을 분리해두면 다음 같은 시도가 쉬워진다.
- pgvector → Pinecone으로 교체
- Cohere Rerank → bge-reranker로 교체
- gpt-4o-mini → Claude Haiku로 교체
- 같은 질문을 두 retriever로 동시 평가 (A/B)
정리
- 60~70% 천장의 9가지 원인: chunking, rewrite, embedding, 문서, 권한, top-k, reranker, context, 평가셋 없음
- 지표는 Recall@k, MRR, nDCG로 시작, 자동 채점은 LLM-as-Judge
- 평가셋은 실제 질문 로그에서 100건 정도부터 시작
- 운영 파이프라인은 13단계로 추상화 가능
- 디렉토리는 agent / retriever / llm / observability / evaluation으로 책임 분리
다음 시리즈에서는 이걸 다 관찰 가능하게 만드는 도구인 Langfuse를 본격적으로 다룬다. "검색 실패인지 생성 실패인지 어떻게 가려내나"부터 풀어볼 거다.