RAG 심화 #1 RAG가 틀리는 지점부터 찾기
LLM 앱 개발 실전 8편에서 기본 RAG 파이프라인을 만들었고, 13편에서는 그걸로 사내 문서 Q&A 봇까지 완성했습니다. 그런데 실제로 운영해 보면 어딘가 아쉽습니다. 어떤 질문에는 정확한데 어떤 질문에는 엉뚱한 답을 하고, 고치려고 프롬프트를 만지면 다른 질문이 나빠집니다. 이 시리즈는 그 “아쉬운 RAG"를 체계적으로 끌어올리는 7편입니다.
순서는 이렇습니다. 실패 진단과 기준선(1편), 청킹 전략(2편), 하이브리드 검색(3편), 쿼리 변환과 리랭킹(4편), 인용으로 환각 줄이기(5편), 평가 파이프라인(6편)을 거쳐, 마지막 7편에서 13편의 Q&A 봇을 단계별로 업그레이드하며 수치 변화를 확인합니다.
첫 편의 주제는 개선 기법이 아니라 진단입니다. 어디가 부러졌는지 모른 채 고치기 시작하면, 좋아졌는지조차 알 수 없기 때문입니다.
RAG의 실패는 두 곳에서 납니다 #
RAG의 답이 이상할 때 원인은 크게 두 갈래입니다.
| 실패 유형 | 증상 | 고치는 곳 |
|---|---|---|
| 검색 실패 | 정답이 담긴 조각을 가져오지 못했다 | 청킹, 검색(2~4편) |
| 생성 실패 | 정답 조각은 가져왔는데 답이 틀렸다 | 프롬프트, 인용(5편) |
이 구분이 중요한 이유는 처방이 정반대이기 때문입니다. 검색 실패인데 프롬프트를 고치는 것은 헛수고이고, 생성 실패인데 임베딩 모델을 바꾸는 것도 마찬가지입니다. 그리고 현장에서 만나는 실패의 다수는 검색 실패입니다. 8편에서도 한 줄로 짚었지만, 엉뚱한 조각을 가져오면 Claude가 아무리 잘 답해도 소용이 없습니다.
첫 번째 진단 도구는 눈입니다 #
거창한 도구 전에, 가장 효과적인 진단은 검색 결과를 직접 보는 것입니다. 틀린 답이 나온 질문에 대해, 검색이 가져온 조각을 그대로 출력해 봅니다.
def inspect(question: str, top_k: int = 5):
chunks = search(question, top_k=top_k) # 8편의 검색 함수
print(f"질문: {question}\n")
for i, c in enumerate(chunks, 1):
print(f"--- 조각 {i} (유사도 {c.score:.3f}) ---")
print(c.text[:200])
print()
inspect("환불 수수료가 얼마야?")조각들 안에 정답이 있는지 없는지만 보면, 검색 실패인지 생성 실패인지가 바로 갈립니다. 정답이 없다면 검색 쪽을, 있는데 답이 틀렸다면 생성 쪽을 파면 됩니다. 틀린 질문 열 개만 이렇게 살펴봐도 패턴이 보이기 시작합니다. 고유 명사 질문에서만 검색이 빗나간다든지, 표 안에 있던 내용만 누락된다든지 하는 식입니다.
골든셋 만들기 #
눈으로 보는 진단은 빠르지만, 개선이 진짜 개선인지 확인하려면 같은 시험지로 반복해서 재야 합니다. 그 시험지가 골든셋(golden set, 정답을 미리 확정해 둔 평가용 데이터 묶음)입니다. RAG에서는 질문, 정답, 그리고 정답이 들어 있는 조각의 출처를 함께 적습니다.
GOLDEN = [
{
"question": "환불 수수료가 얼마야?",
"answer_keywords": ["10%", "수수료"], # 답에 반드시 들어가야 할 내용
"source_doc": "refund-policy.md", # 정답 조각이 있는 문서
},
{
"question": "연차는 입사 첫해에 며칠이야?",
"answer_keywords": ["11일"],
"source_doc": "hr-handbook.md",
},
# 실제 사용자 질문에서 고른 20~30개면 시작하기에 충분하다
]질문은 머리로 지어내지 말고 실제 사용자 질문 로그에서 고르는 것이 중요합니다. 지어낸 질문은 문서의 표현을 닮아서 검색이 너무 쉽게 성공하고, 실제 질문은 문서와 다른 어휘를 써서 어렵습니다. 어려운 시험지가 좋은 시험지입니다.
기준선 재기 #
골든셋이 있으면 두 가지 숫자를 잴 수 있습니다. 검색이 정답 문서를 가져왔는지(검색 적중률), 답에 정답 내용이 들어 있는지(답변 정확도)입니다.
def measure(golden: list, top_k: int = 5) -> tuple:
retrieval_hits, answer_hits = 0, 0
for case in golden:
chunks = search(case["question"], top_k=top_k)
if any(c.source == case["source_doc"] for c in chunks):
retrieval_hits += 1
answer = rag_answer(case["question"]) # 8편의 RAG 응답 함수
if all(kw in answer for kw in case["answer_keywords"]):
answer_hits += 1
n = len(golden)
return retrieval_hits / n, answer_hits / n
retrieval, answer = measure(GOLDEN)
print(f"검색 적중률: {retrieval:.0%} 답변 정확도: {answer:.0%}")키워드 포함 여부로 답을 채점하는 것은 거친 방법이지만, 기준선 용도로는 충분합니다. 더 정교한 채점(LLM 판정자)은 6편에서 다룹니다. 지금 중요한 것은 숫자 두 개가 생겼다는 사실입니다. 예를 들어 “검색 적중률 70%, 답변 정확도 55%“라는 기준선이 있으면, 앞으로의 모든 개선을 이 숫자와 비교해서 판단할 수 있습니다.
두 숫자의 간격도 정보입니다. 검색 적중률은 높은데 답변 정확도가 크게 낮다면 생성 쪽 문제가 크다는 뜻이고, 둘 다 낮다면 검색부터 고쳐야 한다는 뜻입니다.
흔히 걸려 넘어지는 곳 #
- 느낌으로 개선을 판단한다 — 몇 개 질문을 던져 보고 “좋아진 것 같다"고 결론 내리면, 보이지 않는 곳에서 나빠진 것을 놓칩니다. 같은 골든셋으로 전후를 비교합니다.
- 쉬운 질문으로 골든셋을 채운다 — 문서 문장을 거의 그대로 옮긴 질문은 항상 통과해서 변화를 감지하지 못합니다. 실제 로그에서, 특히 틀렸던 질문에서 고릅니다.
- 검색과 생성을 한 덩어리로 본다 — “RAG가 틀렸다"까지만 보고 멈추면 어디를 고칠지 정할 수 없습니다. 반드시 검색 결과를 열어서 실패 지점을 가릅니다.
마무리 #
이번 글에서는 RAG 개선의 출발점인 진단과 측정을 다뤘습니다.
- 실패는 검색 실패와 생성 실패로 갈리고, 처방이 서로 다릅니다. 검색 결과를 직접 열어 보는 것이 첫 진단입니다.
- 실제 사용자 질문으로 골든셋을 만들고, 검색 적중률과 답변 정확도 두 숫자로 기준선을 잽니다.
- 이후의 모든 개선은 이 기준선과의 비교로 판단합니다.
기준선이 생겼으니 이제 고치기 시작합니다. 검색 실패의 가장 흔한 뿌리는 검색 알고리즘이 아니라 그 전 단계에 있습니다. 다음 글인 “RAG 심화 #2 검색 품질을 좌우하는 청킹 전략"에서 문서를 쪼개는 방법부터 손봅니다.