问题与目标
“问了几次感觉不错”无法判断 RAG 是否真的改进。最终答案可能碰巧正确但没有检索到依据,也可能检索正确却被模型曲解。评估必须把组件拆开,并保留不可回答问题和权限边界样本。
本篇构建自创问题集,分别计算检索 Recall@k、MRR、答案关键词正确性和引用完整性,再说明忠实性需要逐条核对答案断言是否由来源支持。
核心概念
- 检索相关性:相关 Chunk 是否进入候选以及排在什么位置;
- 答案相关性:回答是否真正回应问题,而不是只复述背景;
- 忠实性:答案中的事实是否能由检索证据支持;
- 正确性:答案是否符合参考答案或领域专家判断;
- 引用质量:引用是否存在、是否覆盖关键断言、是否指向支持它的片段;
- 运行指标:延迟、Token、成本、超时与失败率。
忠实不等于正确:答案可以忠实复述一份过时文档。正确也不等于忠实:模型可能凭参数知识猜对,但系统无法证明依据。
可运行实现
python
import re
cases = [
{
"question": "关键告警通知谁?",
"relevant": {"duty:v1:002"},
"required_terms": {"负责人"},
},
{
"question": "普通告警需要停机吗?",
"relevant": set(),
"required_terms": {"无法确认"},
},
]
def retrieval_scores(ranked_ids: list[str], relevant: set[str], k: int = 3) -> dict:
if not relevant:
return {"recall_at_k": None, "mrr": None}
top = ranked_ids[:k]
recall = len(set(top) & relevant) / len(relevant)
ranks = [index for index, doc_id in enumerate(ranked_ids, 1) if doc_id in relevant]
return {"recall_at_k": recall, "mrr": 1 / min(ranks) if ranks else 0.0}
def answer_scores(answer: str, required_terms: set[str], source_ids: set[str]) -> dict:
cited = set(re.findall(r"\[(S\d+)\]", answer))
return {
"required_terms": int(all(term in answer for term in required_terms)),
"citation_valid": int(bool(cited) and cited <= source_ids),
}
print(retrieval_scores(["duty:v1:002", "other:v1:000"], cases[0]["relevant"]))
print(answer_scores("关键告警应通知负责人 [S1]。", {"负责人"}, {"S1", "S2"}))
关键词检查只适合稳定规则,不等于开放问题正确性。忠实性评估可以先把答案拆成原子断言,再由人工或评审模型逐条对照来源;评审 Prompt、模型与重复次数也要版本化,并定期与人工标注校准。
测试集至少包含:直接事实、跨片段问题、同义表达、精确编号、时间版本、不可回答、提示注入和无权限问题。训练调参集与最终验收集分开,避免对少量问题过拟合。
常见问题与排查
只评最终答案
同时保存 Top-k ID、分数、来源、最终上下文和答案。检索失败与生成失败需要不同修复手段。
用 LLM-as-judge 当唯一真相
先用确定性规则检查格式、引用和拒答,再用人工样本校准评审模型。对高风险领域保留专家复核。
数据集全是可回答问题
加入证据不足与权限拒绝样本,否则系统通过“总能回答”反而获得高表面分数。
平均分提高就上线
查看失败样本和分位数,设置关键子集门槛,例如安全与权限样本必须全部通过,同时监控 P95 延迟与失败率。
小结
RAG 评估要回答“错在召回、排序、生成还是引用”,而不是只给最终答案一个总分。固定数据、分层指标、失败样本和运行成本共同构成可比较的实验基线。
License: CC BY-NC 4.0
Updated 2 hours ago
Was this article helpful? Give it a like.
0 comments


