1. 为什么 RAG 必须单独评估
RAG 系统由检索器与生成器串联,端到端效果差时无法定位问题在哪一段。常见的两种误判:
- 检索对了但生成幻觉 → 误以为是检索问题,去调 embedding 模型,白费力气。
- 检索错了但答案蒙对 → 端到端准确率看起来不错,掩盖了检索缺陷。
所以评估必须分层:先测检索,再测生成,最后测端到端。这与传统 ML 里"分开评估特征与模型"的思路一致,可参考 模型评估方法 。
另一个现实动机:RAG 调优的参数极多(chunk size、top-k、reranker 阈值、embedding 模型、混合检索权重)。没有量化指标,调优就是玄学。
2. 评估的四个对象与数据三元组
RAG 评估的最小数据单元是一个四元组:
(question, retrieved_contexts, answer, ground_truth)
| 字段 | 说明 | 是否必需 |
|---|---|---|
| question | 用户问题 | 必需 |
| retrieved_contexts | 系统检索到的上下文列表 | 必需 |
| answer | 系统生成的回答 | 必需 |
| ground_truth | 标准答案 | 可选(有则能测召回与正确性) |
依据是否有 ground_truth,指标分成两类:
| 类别 | 需要标准答案 | 指标 |
|---|---|---|
| 有参考(reference-based) | 是 | Context Recall、Answer Correctness、EM/F1 |
| 无参考(reference-free) | 否 | Faithfulness、Answer Relevance、Context Precision |
无参考指标的价值在于可以用真实线上流量评估,不需要人工标注。
3. 检索质量指标
检索是 RAG 的地基。先测检索,再谈生成。
3.1 核心指标
| 指标 | 含义 | 关注点 |
|---|---|---|
| Recall@k | Top-k 里包含相关文档的比例 | 有没有漏 |
| Precision@k | Top-k 里相关文档的占比 | 有没有噪声 |
| MRR | 第一个相关文档排名的倒数均值 | 排得够不够前 |
| NDCG@k | 考虑位置权重的排序质量 | 整体排序 |
| Hit Rate@k | 至少命中一个相关文档的查询比例 | 覆盖率 |
import numpy as np
def recall_at_k(retrieved: list[str], relevant: set[str], k: int) -> float:
if not relevant:
return 0.0
return len(set(retrieved[:k]) & relevant) / len(relevant)
def precision_at_k(retrieved: list[str], relevant: set[str], k: int) -> float:
top = retrieved[:k]
return len(set(top) & relevant) / k if k else 0.0
def mrr(retrieved: list[str], relevant: set[str]) -> float:
for rank, doc_id in enumerate(retrieved, start=1):
if doc_id in relevant:
return 1.0 / rank
return 0.0
def ndcg_at_k(retrieved: list[str], relevant: set[str], k: int) -> float:
dcg = sum(
1.0 / np.log2(rank + 1)
for rank, doc_id in enumerate(retrieved[:k], start=1)
if doc_id in relevant
)
idcg = sum(1.0 / np.log2(rank + 1) for rank in range(1, min(len(relevant), k) + 1))
return dcg / idcg if idcg else 0.0
3.2 怎么选 k
| k | Recall | Precision | 延迟 | 适用 |
|---|---|---|---|---|
| 3 | 低 | 高 | 低 | 简单问答 |
| 5 | 中 | 中 | 中 | 通用默认 |
| 10 | 高 | 中低 | 中 | 配 Reranker |
| 20+ | 很高 | 低 | 高 | 仅用于召回阶段,必须重排 |
标准做法:召回阶段用 k=2050 保证 Recall,再用 Reranker 精排到 k=35 保证 Precision。重排序的机制见 /llm-embedding-reranker/。
3.3 检索指标的常见陷阱
- 用 chunk 级还是文档级标注:用户问"退款政策",相关的是"某个文档的某一节"。若标注是文档级,而系统返回 chunk 级,需先做 chunk→doc 映射再算指标。
- 相关性非二值:很多场景是分级相关(完全相关 / 部分相关 / 不相关),此时应算 NDCG 而非 Recall。
- 同一问题多个正确来源:相关文档集要覆盖全部正确来源,否则会低估 Recall。
4. 生成质量指标
4.1 Faithfulness(忠实度)
回答是否完全由检索到的上下文支撑,即有没有幻觉。这是 RAG 最重要的生成指标。
计算方式:把回答拆成若干断言(claim),逐条判断能否从上下文推出。
Faithfulness = 能被上下文支撑的断言数 / 总断言数
示例:
上下文:本产品支持 7 天无理由退货,运费由买家承担。
回答:本产品支持 7 天无理由退货,运费由卖家承担。
断言 1「支持 7 天无理由退货」→ 被支撑 ✓
断言 2「运费由卖家承担」 → 与上下文矛盾 ✗
Faithfulness = 1/2 = 0.5
4.2 Answer Relevance(答案相关性)
回答是否切题。注意它不检查正确性,只检查是否在回答所问。
Answer Relevance = 用回答反向生成问题的相似度均值
做法:让 LLM 根据回答反推若干个"这答案可能在回答的问题",再与原问题算 embedding 相似度取均值。如果回答跑题,反推的问题会与原问题不相似。
4.3 Context Precision / Recall
| 指标 | 含义 | 依赖 |
|---|---|---|
| Context Precision | 检索到的上下文中,真正有用的占比,且按排名加权 | ground_truth |
| Context Recall | 标准答案中的信息,有多少能在上下文中找到 | ground_truth |
Context Recall 的直观算法:把标准答案拆成句子,逐句判断"能否在检索上下文中找到依据"。
def context_recall(contexts: list[str], ground_truth: str, judge) -> float:
sentences = split_sentences(ground_truth)
if not sentences:
return 0.0
hits = sum(
1 for s in sentences
if judge.binary(f"下面的上下文能否支撑这句话?\n上下文:{contexts}\n句子:{s}")
)
return hits / len(sentences)
4.4 指标总览与定位
| 症状 | 可能原因 | 该看的指标 |
|---|---|---|
| 答非所问 | 检索没找到相关内容 | Context Recall ↓ |
| 回答有幻觉 | 生成器没约束好 | Faithfulness ↓ |
| 回答太啰嗦/偏题 | Prompt 问题 | Answer Relevance ↓ |
| 上下文噪声大 | top-k 太大 | Context Precision ↓ |
定位流程:端到端差 → 先看 Context Recall(检索是否兜住)→ 再看 Faithfulness(生成是否忠实)→ 最后看 Answer Relevance。
5. Ragas 实战
Ragas 是目前最成熟的 RAG 评估框架,内置上述指标并支持自定义。
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
answer_correctness,
)
from datasets import Dataset
data = Dataset.from_dict({
"question": [
"退款需要几天到账?",
"支持哪些支付方式?",
],
"answer": [
"退款一般在 3~5 个工作日到账。",
"支持微信、支付宝和银行卡。",
],
"contexts": [
["退款将在审核通过后 3~5 个工作日内退回原支付渠道。"],
["支持微信支付、支付宝、银行卡三种方式。"],
],
"ground_truth": [
"审核通过后 3~5 个工作日退回原渠道。",
"支持微信、支付宝和银行卡。",
],
})
result = evaluate(
dataset=data,
metrics=[
faithfulness,
answer_relevancy,
context_precision,
context_recall,
answer_correctness,
],
)
print(result)
# {'faithfulness': 0.95, 'answer_relevancy': 0.91,
# 'context_precision': 0.88, 'context_recall': 0.90, 'answer_correctness': 0.84}
df = result.to_pandas()
df.to_csv("rag_eval_report.csv", index=False)
5.1 用 LangSmith / 自建管线跑批量评估
import asyncio
async def evaluate_rag_system(rag, golden: list[dict]) -> dict:
rows = []
for case in golden:
retrieved = await rag.retrieve(case["question"])
answer = await rag.generate(case["question"], retrieved)
rows.append({
"question": case["question"],
"answer": answer,
"contexts": [c["text"] for c in retrieved],
"ground_truth": case["ground_truth"],
})
ds = Dataset.from_list(rows)
return evaluate(ds, metrics=[faithfulness, answer_relevancy,
context_precision, context_recall])
5.2 报告要按维度切片
整体平均分会掩盖问题。必须按问题类型切片:
| 切片 | Faithfulness | Context Recall | 结论 |
|---|---|---|---|
| 事实型 | 0.96 | 0.94 | 健康 |
| 多跳推理 | 0.82 | 0.65 | 检索漏链,需多跳检索 |
| 对比型 | 0.88 | 0.71 | 需增加召回数 |
| 否定型 | 0.70 | 0.60 | 上下文约束不足 |
多跳场景的改进方案(查询分解、迭代检索)见 /llm-rag-advanced-optimization/。
6. LLM-as-Judge 的偏差与校准
多数指标依赖 LLM 打分,因此 Judge 的质量决定了评估的可信度。
6.1 已知偏差
| 偏差 | 现象 | 缓解 |
|---|---|---|
| 位置偏差 | 偏好第一个候选 | 交换顺序跑两次取平均 |
| 长度偏差 | 偏好更长的回答 | 显式要求"长度不影响评分" |
| 自我偏好 | 偏好同族模型的输出 | 换不同家族的 Judge |
| 分数聚集 | 大量 4/5 分,区分度低 | 用 1~5 分并给锚点定义 |
6.2 提高可靠性的做法
JUDGE_PROMPT = """你是一个严格的事实核查员。
任务:判断「回答」中的每一条事实性断言是否被「上下文」支持。
规则:
1. 只依据上下文判断,不使用你的先验知识。
2. 上下文未提及的内容,一律判为「不支持」。
3. 回答长度与评分无关。
上下文:
{context}
回答:
{answer}
以 JSON 输出:{"claims": [{"claim": "...", "supported": true|false}], "score": 0.0~1.0}
"""
三个要点:
- 给锚点定义(1 分是什么、5 分是什么),避免分数聚集。
- 要求先给理由再给分(CoT),一致性显著提升。
- 用 JSON 结构化输出约束格式,便于程序解析——这正是受限解码的用武之地。
6.3 与人工标注对齐
Judge 上线前必须做一次对齐验证:抽 50~100 条人工标注,算 Judge 与人的一致率(Cohen’s Kappa)。
| Kappa | 一致性 |
|---|---|
| < 0.4 | 差,不可用 |
| 0.4~0.6 | 中,仅供参考 |
| 0.6~0.8 | 好,可用于迭代 |
| > 0.8 | 优,可用于上线门禁 |
7. 组件级 vs 端到端
| 层次 | 测什么 | 优点 | 缺点 |
|---|---|---|---|
| 组件级 | 检索 / 重排 / 生成各自指标 | 定位精准 | 忽略误差累积 |
| 端到端 | 最终答案质量 | 贴近真实体验 | 无法定位 |
两者都要有。日常迭代看组件级(快、便宜),上线门禁看端到端(准、慢)。
端到端还要关注非质量指标:
P50/P95 延迟、Token 成本/次、检索命中缓存率、拒答率、用户追问率
“用户追问率"是一个极好的隐式反馈信号——用户追问通常意味着上一轮没答好。
8. 评估集构建
评估集的质量决定了评估的上限。三种来源:
| 来源 | 规模 | 成本 | 特点 |
|---|---|---|---|
| 人工标注 | 100~500 | 高 | 最可靠,作为黄金集 |
| 线上真实问题 | 1000+ | 低 | 分布真实,需补标准答案 |
| LLM 合成 | 数千 | 极低 | 覆盖广,需过滤 |
8.1 LLM 合成评估集
GEN_PROMPT = """基于下面的文档片段,生成 {n} 个用户可能提出的问题及其标准答案。
要求:
1. 问题必须能仅凭该片段回答。
2. 问题要口语化,模拟真实用户。
3. 覆盖不同类型的提问:事实查询、对比、条件判断。
文档片段:
{chunk}
以 JSON 输出:[{{"question": "...", "ground_truth": "..."}}]
"""
def build_eval_set(chunks: list[str], n_per_chunk: int = 3) -> list[dict]:
cases = []
for chunk in chunks:
raw = llm.complete(GEN_PROMPT.format(n=n_per_chunk, chunk=chunk))
cases.extend(json.loads(raw))
return dedup(cases)
合成后必须人工抽检 10%:LLM 生成的问题常有"答案已在问题里"或"文档其实答不了"的缺陷。
8.2 评估集要覆盖的四类问题
- 事实型:单一事实查询(“退款几天到账”)。
- 多跳型:需要串联多个片段(“A 产品的退货政策和 B 产品有什么区别”)。
- 否定/边界型:答案应该是"文档里没说”(考验拒答能力)。
- 对抗型:问题措辞与文档用词不匹配,考验语义检索。
9. 在线评估与 A/B
离线指标涨了,线上不一定涨。最终要靠 A/B 验证。
9.1 指标设计
| 类型 | 指标 | 说明 |
|---|---|---|
| 质量 | 点赞/点踩率、追问率 | 直接反馈 |
| 行为 | 会话时长、任务完成率 | 间接反馈 |
| 成本 | 平均 Token/次、检索次数 | 成本控制 |
| 性能 | P95 延迟 | 体验 |
9.2 分流与显著性
import hashlib
def assign_bucket(user_id: str, salt: str = "rag-v2") -> str:
"""稳定分流:同一用户始终进同一组。"""
h = hashlib.md5(f"{user_id}:{salt}".encode()).hexdigest()
return "treatment" if int(h[:8], 16) % 100 < 50 else "control"
注意:RAG 的 A/B 必须按用户分流而非按请求,否则同一用户的会话会在两组间跳变,污染体验。
样本量估算:点踩率这类低基率指标(如 3%)要检测 10% 的相对提升,通常需要每组数千次会话。小流量场景建议先用离线评估与灰度小样本的定性反馈。
9.3 回归门禁
把评估接进 CI,每次改动检索参数或 Prompt 都自动跑黄金集:
# .github/workflows/rag-eval.yml
name: RAG Evaluation
on:
pull_request:
paths: ["src/rag/**", "configs/retrieval.yaml"]
jobs:
evaluate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: python scripts/eval_rag.py --golden data/golden.jsonl --out report.json
- run: python scripts/check_gate.py --report report.json --max-drop 0.02
门禁阈值建议:Faithfulness 下降超过 0.02 或 Context Recall 下降超过 0.03 即 fail。阈值太严会被噪声频繁误伤,太松则形同虚设。
10. 落地检查清单
- 有分层指标:检索侧 + 生成侧 + 端到端
- 有版本化的黄金评估集(含四类问题)
- Judge 与人工标注做过一致性校验(Kappa > 0.6)
- 评估报告按问题类型切片,而非只看平均分
- CI 中跑回归,指标下降超阈值即阻断
- 线上有隐式反馈指标(追问率)作为兜底信号
小结
RAG 评估的核心是分层:先用 Recall@k / NDCG 确认检索兜住了相关信息,再用 Faithfulness 确认生成没有幻觉,最后用端到端指标与 A/B 确认用户体验真的变好。
工具上首选 Ragas 覆盖标准指标,但评估集的质量比框架更重要——一个覆盖四类问题、经过人工校验的 200 条黄金集,价值远超一万条自动合成的低质样本。同时记住 LLM-as-Judge 有位置、长度与自我偏好三类偏差,必须做顺序交换、锚点定义与人工对齐。
指标体系的持续运营属于 MLOps 范畴,与模型上线流程的配合见 /llm-evaluation-llmops/。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。