RAG 심화 #6 RAG 평가 파이프라인 만들기
1편에서 골든셋과 기준선 두 숫자로 시작했고, 매 편 그 숫자로 개선을 판단해 왔습니다. 이번 글에서는 그 임시 측정을 제대로 된 평가 파이프라인으로 키웁니다. 검색과 생성을 각각의 지표로 재고, 키워드 매칭의 한계를 LLM 판정자로 넘어서고, 환각률까지 한 번에 측정하는 체계입니다. 한 번 만들어 두면 청킹이든 모델이든 프롬프트든, 무엇을 바꾸더라도 같은 시험지로 검증할 수 있습니다.
검색 평가 — recall@k와 MRR #
1편의 검색 적중률은 “상위 k개 안에 정답 문서가 있는가"였습니다. 이것이 바로 표준 지표인 recall@k(리콜 앳 케이, 상위 k개 결과 안에 정답이 포함된 비율)입니다. 여기에 하나를 더하면 좋습니다. 정답이 몇 등으로 검색됐는지를 반영하는 MRR(Mean Reciprocal Rank, 정답 순위의 역수 평균)입니다.
def eval_retrieval(golden: list, top_k: int = 5) -> dict:
recall_hits, rr_sum = 0, 0.0
for case in golden:
chunks = search(case["question"], top_k=top_k)
rank = next(
(i for i, c in enumerate(chunks, 1) if c["metadata"]["source"] == case["source_doc"]),
None,
)
if rank is not None:
recall_hits += 1
rr_sum += 1 / rank
n = len(golden)
return {"recall@k": recall_hits / n, "mrr": rr_sum / n}두 지표는 역할이 다릅니다. recall@k가 같아도 MRR이 오르면 정답이 더 위로 올라왔다는 뜻이고, 이는 4편의 리랭킹처럼 순서를 다듬는 개선의 효과를 잡아냅니다. 검색만 따로 평가할 수 있다는 점도 중요합니다. 생성까지 돌리지 않으니 빠르고 싸서, 검색 쪽 실험은 이 지표만으로 수십 번 반복할 수 있습니다.
생성 평가 — 키워드 매칭의 한계와 LLM 판정자 #
1편에서는 답에 키워드가 들어 있는지로 채점했습니다. 빠르지만 한계가 분명합니다. “수수료는 10%입니다"와 “수수료는 10%가 아닙니다"를 구분하지 못합니다. 그래서 생성 평가에는 LLM 판정자(LLM-as-a-judge, 모델로 답의 품질을 채점하는 방법)를 씁니다. LLM 앱 개발 실전 12편에서 소개한 방법을 RAG 채점에 맞게 구성하면 이렇습니다.
import json
JUDGE_SYSTEM = """너는 Q&A 시스템의 채점자다. 질문, 모범 답안, 시스템의 답을 보고 JSON으로 채점하라.
- correct: 시스템의 답이 모범 답안과 사실관계가 일치하면 true
- grounded: 답이 제공된 문서 조각의 내용 범위 안에 있으면 true (조각에 없는 내용을 단정하면 false)
- reason: 판정 이유 한 문장
"""
def judge(case: dict, answer: str, chunks: list) -> dict:
context = "\n---\n".join(c["text"] for c in chunks)
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=300,
system=JUDGE_SYSTEM,
messages=[{
"role": "user",
"content": (
f"질문: {case['question']}\n"
f"모범 답안: {case['reference_answer']}\n"
f"문서 조각:\n{context}\n"
f"시스템의 답: {answer}\n"
'JSON만 출력: {"correct": bool, "grounded": bool, "reason": str}'
),
}],
)
text = next(b.text for b in response.content if b.type == "text")
return json.loads(text)골든셋에는 키워드 대신 모범 답안(reference_answer)을 적어 둡니다. 판정 항목을 둘로 나눈 점에 주목해 주세요. correct는 정확도를, grounded는 근거 충실성을 잽니다. grounded가 false인 비율이 곧 환각률입니다. 5편의 인용 게이트가 실시간 방어선이라면, 이 지표는 변경할 때마다 환각 경향을 재는 정기 검진입니다.
LLM 판정자 자체도 모델이므로 검증이 필요합니다. 도입 초기에 판정 결과 20〜30건을 직접 읽어 보고, 사람의 판단과 어긋나는 패턴이 있으면 판정 프롬프트를 고칩니다. 판정자를 한 번 믿을 수 있게 만들어 두면 이후의 모든 평가가 자동화됩니다.
한 번에 돌리는 평가 스크립트 #
검색 지표와 생성 지표를 묶으면 평가 파이프라인이 완성됩니다.
def evaluate(golden: list, top_k: int = 5) -> dict:
retrieval = eval_retrieval(golden, top_k=top_k)
correct, grounded = 0, 0
for case in golden:
chunks = search(case["question"], top_k=top_k)
answer = rag_answer(case["question"], chunks)
verdict = judge(case, answer, chunks)
correct += verdict["correct"]
grounded += verdict["grounded"]
n = len(golden)
return {
**retrieval,
"accuracy": correct / n,
"hallucination_rate": 1 - grounded / n,
}
print(evaluate(GOLDEN))
# {'recall@k': 0.87, 'mrr': 0.71, 'accuracy': 0.80, 'hallucination_rate': 0.03}이 네 숫자가 RAG의 건강 진단표입니다. recall@k가 낮으면 2〜4편(청킹·검색)으로, recall은 높은데 accuracy가 낮으면 5편(생성)으로, hallucination_rate가 오르면 프롬프트와 인용 게이트로 돌아가면 됩니다. 어디를 고칠지가 숫자에서 바로 읽힙니다.
회귀 테스트로 운영하기 #
평가 파이프라인의 진짜 가치는 반복에 있습니다. 운영 요령 세 가지입니다.
- 변경마다 돌립니다. 청킹, 모델 버전, 프롬프트, top_k 등 무엇이든 바꾸면 전후를 비교합니다. 코드의 회귀 테스트와 같은 위치입니다.
- 결과를 기록합니다. 날짜, 변경 내용, 네 지표를 한 줄씩 쌓아 두면 어떤 변경이 효과 있었는지의 역사가 됩니다.
- 골든셋을 키웁니다. 운영 중 발견한 실패 사례를 골든셋에 추가합니다. 한 번 틀렸던 질문이 다시 틀리지 않는지 영구히 감시하게 됩니다.
비용 걱정은 규모를 보면 풀립니다. 골든셋 30건 평가에 드는 호출은 판정 포함 60회 수준이라, 캐싱과 작은 판정 모델을 쓰면 한 번 돌리는 비용은 가볍습니다. 잘못된 변경을 배포해서 치르는 비용과 비교하면 더욱 그렇습니다.
흔히 걸려 넘어지는 곳 #
- 판정자를 검증 없이 믿는다 — LLM 판정자도 틀립니다. 초기에 사람 판정과 대조해 보정하고, 판정 이유(
reason)를 같이 받아 어긋난 판정을 추적합니다. - 평가 직전에 골든셋을 고친다 — 측정 대상에 맞춰 시험지를 바꾸면 비교가 무의미해집니다. 골든셋 변경과 파이프라인 변경은 따로 합니다.
- 평균만 본다 — 전체 평균이 같아도 질문군별로는 오르내림이 있습니다. 3편에서 한 것처럼 질문 유형별로 나눠 보면 가려진 회귀를 잡을 수 있습니다.
마무리 #
이번 글에서는 RAG 평가를 체계화했습니다.
- 검색은 recall@k와 MRR로 따로, 빠르게 평가합니다. 순서 개선은 MRR이 잡아냅니다.
- 생성은 LLM 판정자로 정확도와 근거 충실성을 채점하고, 근거 충실성의 반대가 환각률입니다.
- 네 지표를 한 스크립트로 묶어 모든 변경의 회귀 테스트로 돌리고, 실패 사례를 골든셋에 계속 추가합니다.
도구와 측정이 모두 준비됐습니다. 마지막 글인 “RAG 심화 #7 실전 프로젝트: 문서 Q&A 봇 업그레이드"에서는 LLM 앱 개발 실전 13편의 봇을 이 시리즈의 기법으로 단계별로 개선하면서, 지표가 실제로 어떻게 움직이는지 확인하고 시리즈를 마칩니다.