RAG 评测指标:别只看答案像不像
从一个真实的评测翻车说起
我在做企业知识库问答系统时,曾经用 BLEU 和 ROUGE 跑了一轮评测,综合得分 0.72,看起来还不错。上线第一天,客服团队反馈:系统给了一个「看起来很像那么回事」的答案,但引用的内部制度文件根本不存在。
问题出在我把评测当成了「生成质量」问题。RAG 系统是一个多阶段流水线——查询改写、文档检索、上下文排序、答案生成——每一层都可能出问题,而最终答案的「语言流畅度」和「事实可靠性」是两个完全独立的维度。BLEU 只能衡量词汇重叠,它分不清「基于证据的正确回答」和「一本正经的胡说八道」。
这次翻车让我重新审视了整个评测体系。这篇文章记录了我在实践中逐步建立的一套 RAG 评测方法论,核心观点是:评测必须拆开链路看,每个环节有独立的指标,最后还需要端到端的综合判断。
RAG 评测的理论框架
RAG 链路的分层评测思路
RAGAS 框架(Es et al., 2023)提出了一种「无需参考答案」(reference-free)的评测范式,将 RAG 系统的评测拆成两个独立维度:检索质量和生成质量。这个拆分很关键,因为它让定位问题变得可操作——如果检索得分低,去优化向量库和切分策略;如果生成得分低,去调 Prompt 或换模型。
Mistral AI 在工程博客中进一步提炼了「RAG 三元组」(RAG Triad)的概念(Mistral AI, 2025),将核心评测压缩到三个维度:
| 维度 | 评测对象 | 核心问题 |
|---|---|---|
| Context Relevance | 检索器 | 召回的文档和用户问题相关吗? |
| Groundedness | 生成器 | 答案是否忠实于召回的文档? |
| Answer Relevance | 生成器 | 答案是否回应了用户原始问题? |
Patronus AI 在此基础上扩展为五个指标(Patronus AI, 2025),增加了 Context Sufficiency(上下文是否充足)和 Answer Hallucination(是否有幻觉),使评测更贴近生产环境需求。
组件级评测 vs 端到端评测
一个容易混淆的边界是:什么时候评组件,什么时候评整体?
| 对比维度 | 组件级评测 | 端到端评测 |
|---|---|---|
| 评测对象 | 检索器、生成器独立打分 | 整个流水线最终输出 |
| 优势 | 精确定位故障层 | 反映用户真实体验 |
| 劣势 | 各层指标好不代表整体好 | 分数低时不知道哪里出了问题 |
| 适用场景 | 迭代调优、A/B 测试 | 上线验收、定期巡检 |
| 典型指标 | Context Precision、Faithfulness | LLM-as-Judge 综合分、任务完成率 |
我实际的做法是:日常迭代看组件指标,发版前看端到端指标。两者缺一不可。
案例一:检索阶段就失败了,后面全白搭
场景
一个内部技术文档问答系统,用户问「Kubernetes 集群如何做滚动更新?」,系统返回了 5 个 chunk,其中 3 个是关于 Docker Compose 的部署文档,只有 2 个和 Kubernetes 相关——而且排在第 4、5 位。
翻车
用 BLEU 评分,答案看着还行,因为生成器很擅长从有限上下文里「凑」出一个像样的回答。但检索阶段就已经引入了大量噪声,答案的准确性完全依赖生成器的脑补能力。
修复:引入 Context Precision 和 Context Recall
Context Precision 衡量的是「相关文档是否排在前面」,它本质上是 Mean Average Precision(MAP)的 LLM-as-Judge 版本。Context Recall 衡量的是「该召回的文档是否都召回了」。
| 指标 | 公式逻辑 | 关注点 |
|---|---|---|
| Context Precision | 相关 chunk 的 Precision@k 均值 | 排序质量——好东西是否在顶部 |
| Context Recall | ground_truth 中被召回上下文覆盖的比例 | 覆盖度——该找的是否都找到了 |
| Recall@K | Top-K 结果中相关文档占全部相关文档的比例 | 数量——召回了多少正确答案 |
| Hit Rate | Top-K 中是否至少包含一个相关文档 | 存在性——有没有找到一点相关的 |
修复后的评测代码:
# ✅ 好做法:同时评估检索排序质量和覆盖度
from ragas.llms import llm_factory
from ragas.metrics import ContextPrecision, ContextRecall
llm = llm_factory("gpt-4o-mini", client=client)
# Context Precision: 相关 chunk 是否排在前面
context_precision = ContextPrecision(llm=llm)
# Context Recall: 是否召回了所有需要的信息
context_recall = ContextRecall(llm=llm)
results = await evaluate(
dataset=eval_dataset,
metrics=[context_precision, context_recall],
)
print(f"Context Precision: {results['context_precision']}")
print(f"Context Recall: {results['context_recall']}")对比只看最终答案相似度的做法:
# ❌ 坏做法:只看答案和标准答案的语义相似度
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer("all-MiniLM-L6-v2")
emb_pred = model.encode(predicted_answer)
emb_truth = model.encode(ground_truth_answer)
score = util.cos_sim(emb_pred, emb_truth).item()
# 问题:语义相似度高不代表事实正确,也不代表检索质量好
# 一个「流畅的错误答案」可能比「生硬但正确的答案」得分更高这两段代码的核心差异在于:好做法把检索环节单独拉出来评测,精确定位到「是检索没找到」还是「找到了但排序不对」还是「找到了也排对了但生成器没用好」;坏做法把所有问题都混在一个分数里,出了问题完全不知道改哪里。
案例二:Faithfulness 揭露的幻觉问题
场景
一个金融合规知识库,用户问「反洗钱报告的时间要求是什么?」。系统给出了一个结构清晰的答案:「根据《金融机构大额交易和可疑交易报告管理办法》,可疑交易报告需在发现后 5 个工作日内提交。」
翻车
这个答案读起来很专业,但实际上「5 个工作日」这个数字在检索到的法规原文里并不存在。正确要求是「10 个工作日」。生成器在上下文不够充分时,选择了一个「看起来合理」的数字来填充。
如果用传统的 ROUGE-L 评测,因为答案和标准答案有大量词汇重叠,得分会很高。但事实上,答案中的关键数字是错的——在金融合规场景下,这种错误的后果不是「用户体验差」,而是「合规风险」。
修复:Faithfulness 指标
Faithfulness 的计算分三步(RAGAS, Es et al., 2023):
- 将生成的答案拆解为独立的事实声明(claims)
- 对每个声明,用 NLI(自然语言推理)判断它是否能从检索上下文中推导出来
- 计算比例:被上下文支持的声明数 / 总声明数
| Faithfulness 分数 | 含义 | 行动建议 |
|---|---|---|
| 1.0 | 所有声明都有据可查 | 通过 |
| 0.7-0.9 | 大部分有依据,少量超出 | 检查是否需要在 Prompt 中强化引用约束 |
| 0.4-0.6 | 大量声明缺乏上下文支持 | 检索质量和 Prompt 都需要优化 |
| < 0.4 | 大部分内容是生成的,不是检索的 | 系统基本不可用 |
# ✅ 好做法:用 Faithfulness 检测答案是否有据可查
from ragas.metrics import Faithfulness
faithfulness = Faithfulness(llm=llm)
result = await faithfulness.ascore(
user_input="反洗钱报告的时间要求是什么?",
response="可疑交易报告需在发现后5个工作日内提交。",
retrieved_contexts=[
"《金融机构大额交易和可疑交易报告管理办法》第十五条:金融机构应当在发现可疑交易之日起10个工作日内,向中国反洗钱监测分析中心提交可疑交易报告。"
]
)
# result.score ≈ 0.0 — "5个工作日" 无法从上下文中推导出来
# 这个信号明确告诉我们:生成器在编造信息
print(f"Faithfulness: {result.score}")对比只看答案相似度的做法:
# ❌ 坏做法:用 ROUGE 评分,词汇重叠高但关键数字错了
import evaluate
rouge = evaluate.load("rouge")
score = rouge.compute(
predictions=["可疑交易报告需在发现后5个工作日内提交"],
references=["可疑交易报告需在发现后10个工作日内提交"]
)
# score["rouge_l"] 可能在 0.7 以上,因为大部分词汇重叠
# 但 "5" 和 "10" 的差异在合规场景下是不可接受的
# ROUGE 看不到这个语义层面的关键差异
print(f"ROUGE-L: {score['rouge_l']}")Faithfulness 的价值在于它直接回答了一个问题:答案里的每一句话,是否都能在检索到的文档里找到依据? 这是传统 N-gram 匹配完全做不到的。
评测流水线的整体流程
在实际工程中,我把 RAG 评测组织成下面这个流水线。每一步对应具体的指标,每一层的输出是下一层的输入:
这张图的关键在于:检索指标和生成指标是并行计算的,最终汇聚到诊断报告。根据得分分布,可以精确定位问题出在哪一层,而不是对着一个总分猜来猜去。
案例三:系统该拒答的时候没拒答
场景
一个医疗设备售后知识库,用户问「XX 型号的心脏起搏器和 YY 型号的兼容性如何?」。这个问题涉及两个具体型号的配合使用,但知识库里只有各自的技术手册,没有交叉兼容性说明。
翻车
系统没有拒答,而是从两个型号的文档里各抽了一段信息,拼凑出一个「看起来合理」的答案:「XX 型号采用蓝牙 5.0 协议,YY 型号支持蓝牙 5.0,因此两者可以兼容。」这个推理过程在技术上是错误的——通信协议兼容不代表设备兼容,但在语言层面非常流畅。
在医疗设备场景下,这种「自信的胡说八道」可能导致严重后果。
修复:拒答评测 + Answer Relevancy 联合判断
拒答能力不是一个独立指标,而是多个指标的综合表现。当 Context Sufficiency 低(上下文不够回答问题)且 Faithfulness 低(答案缺乏上下文支持)时,系统就应该选择拒答或追问。
| 拒答场景 | Context Sufficiency | Faithfulness | 系统行为 |
|---|---|---|---|
| 上下文充足且答案有据 | 高 | 高 | 正常回答 |
| 上下文充足但答案偏离 | 高 | 低 | 重新生成或标注不确定 |
| 上下文不足但强行回答 | 低 | 低 | 应该拒答但没有 |
| 上下文不足且正确拒答 | 低 | N/A | 理想拒答行为 |
# ✅ 好做法:在 Prompt 中明确拒答条件,并用多维度指标检测
from ragas.metrics import Faithfulness, AnswerRelevancy
# Prompt 模板中明确约束
PROMPT_TEMPLATE = """基于以下上下文回答用户问题。
如果上下文中的信息不足以回答问题,请明确说明「当前资料不足以回答此问题」,不要推测。
上下文:
{context}
用户问题:{question}
"""
# 评测时联合检查多个信号
faithfulness = Faithfulness(llm=llm)
answer_relevancy = AnswerRelevancy(llm=llm)
faith_score = await faithfulness.ascore(
user_input=question,
response=response,
retrieved_contexts=contexts
)
relevancy_score = await answer_relevancy.ascore(
user_input=question,
response=response
)
# 当 faithfulness 低于阈值时,说明系统在缺乏依据的情况下仍然给出了断言
# 这就是需要触发拒答的场景
if faith_score < 0.5:
print("⚠️ 系统给出了缺乏依据的回答,应优化拒答逻辑")对比不做拒答控制的做法:
# ❌ 坏做法:没有拒答机制,什么问题都给答案
async def generate_answer(question: str, contexts: list[str]):
prompt = f"基于以下上下文回答问题。\n上下文:{contexts}\n问题:{question}"
# 没有「信息不足时拒答」的指令
# 模型会尽力从有限上下文中拼凑答案
response = await llm.generate(prompt)
return response
# 问题:模型天然倾向于「有问必答」
# 如果 Prompt 中没有拒答指令,模型会利用任何可用信息来构造答案
# 即使这些信息和问题没有直接关系核心指标的实操对比
在理解了上面的案例之后,我把 RAG 评测的核心指标整理成一张完整的对照表:
| 指标 | 评测对象 | 输入 | 是否需要 ground_truth | 计算核心 |
|---|---|---|---|---|
| Faithfulness | 生成器 | answer + contexts | 否 | 答案声明能否从上下文推导 |
| Context Precision | 检索器 | question + contexts + ground_truth | 是 | 相关 chunk 是否排在前面 |
| Context Recall | 检索器 | contexts + ground_truth | 是 | ground_truth 信息是否被覆盖 |
| Answer Relevancy | 生成器 | question + answer | 否 | 答案是否回应了原始问题 |
| Answer Correctness | 生成器 | answer + ground_truth | 是 | 答案和标准答案的事实一致性 |
| Context Relevance | 检索器 | question + contexts | 否 | 召回文档和问题的语义相关性 |
这里面有一个实际选择时的关键区分:需要 ground_truth 的指标(Context Precision、Context Recall、Answer Correctness)评测更准确,但构建标注数据集的成本高;无需 ground_truth 的指标(Faithfulness、Answer Relevancy、Context Relevance)构建成本低,但依赖 LLM-as-Judge 的判断质量。
我的实践建议是:初期用无参考指标快速建立基线,随着数据积累逐步引入有参考指标做精确评测。
LLM-as-Judge 与传统指标的对比
RAGAS 的核心创新在于用 LLM 本身来做评测。这引发了一个值得讨论的问题:用 LLM 来评价 LLM 的输出,靠谱吗?
| 对比维度 | 传统指标(BLEU/ROUGE) | LLM-as-Judge(RAGAS) |
|---|---|---|
| 评测方式 | 词汇/子串重叠 | 语义理解 + 逻辑推理 |
| 是否需要参考答案 | 是 | 可选 |
| 能否检测幻觉 | 否 | 是(Faithfulness) |
| 能否评测拒答 | 否 | 是 |
| 计算成本 | 低(本地模型推理) | 高(需调用 LLM API) |
| 一致性 | 高(确定性算法) | 中(受温度和模型版本影响) |
| 适用场景 | 翻译、摘要等短文本生成 | RAG、对话等长文本开放生成 |
Evidently AI 在其评测指南中给出了务实的建议(Evidently AI, 2025):「不要用传统的词汇匹配指标,用语义相似度或 LLM-as-Judge 框架来捕捉更细微的质量差异。」
不过 LLM-as-Judge 也有明显的局限。Reddit 社区讨论中有人指出(Reddit/r/LangChain, 2024),RAGAS 缺少正式的技术报告来验证各指标和人类判断的一致性。在实际使用中,我也观察到评估 LLM 自身版本升级会导致分数漂移——GPT-4o 做 Judge 和 GPT-4o-mini 做 Judge,同一个答案的 Faithfulness 得分可能差 0.1-0.2。
我的处理方式:固定评估 LLM 的版本,把 Judge 模型版本作为评测配置的一部分管理。
端到端评测的完整代码示例
把上面的指标组合起来,一个完整的评测流程是这样的:
# ✅ 好做法:多维度评测流水线
from ragas import evaluate
from ragas.metrics import (
Faithfulness,
AnswerRelevancy,
ContextPrecision,
ContextRecall,
)
from ragas.llms import llm_factory
llm = llm_factory("gpt-4o", client=client)
# 定义完整指标集
metrics = [
# 检索层指标
ContextPrecision(llm=llm), # 检索排序质量
ContextRecall(llm=llm), # 检索覆盖度
# 生成层指标
Faithfulness(llm=llm), # 答案忠实度(是否幻觉)
AnswerRelevancy(llm=llm), # 答案相关性(是否回应问题)
]
# 执行评测
results = await evaluate(
dataset=eval_dataset, # 包含 question, ground_truth, contexts, answer
metrics=metrics,
)
# 分层输出诊断报告
print("=== 检索层 ===")
print(f"Context Precision: {results['context_precision']:.3f}")
print(f"Context Recall: {results['context_recall']:.3f}")
print("=== 生成层 ===")
print(f"Faithfulness: {results['faithfulness']:.3f}")
print(f"Answer Relevancy: {results['answer_relevancy']:.3f}")对比只关注单一指标的做法:
# ❌ 坏做法:只看一个综合分数
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer("all-MiniLM-L6-v2")
scores = []
for item in eval_data:
emb_pred = model.encode(item["answer"])
emb_truth = model.encode(item["ground_truth"])
scores.append(util.cos_sim(emb_pred, emb_truth).item())
avg_score = sum(scores) / len(scores)
print(f"Average similarity: {avg_score:.3f}")
# 问题:
# 1. 无法区分检索失败和生成失败
# 2. 语义相似度高不等于事实正确
# 3. 无法发现幻觉问题
# 4. 无法评估拒答能力构建评测数据集的要点
指标选对了,没有好的评测数据集也白搭。这方面踩过的坑比选指标时更多。
数据从哪里来
| 数据来源 | 优势 | 劣势 | 适用阶段 |
|---|---|---|---|
| 线上真实 query 日志 | 覆盖真实分布 | 需要人工标注 ground_truth | 成熟期 |
| 专家手动构造 | 质量高,覆盖边界场景 | 成本高,数量少 | 初期基线 |
| LLM 合成 + 人工审核 | 量大,成本低 | 可能有偏差,必须人工审核 | 扩展期 |
| 从文档自动生成 QA 对 | 和知识库对齐 | 容易产生简单问题 | 检索层评测 |
Evidently AI 的建议很实在(Evidently AI, 2025):「从真实数据开始,审核自动生成的数据集,实验中坚持使用同一份测试集。」
容易忽视的数据质量问题
标注一致性:如果让多个人标注 ground_truth,需要先校准标注标准。我见过两个人对同一个问题写出完全不同但都正确的答案——这种情况下用字符串匹配算 Answer Correctness 就会出错。
难度分布:评测集里不能全是简单的事实查询。一个好的评测集应该包含:简单事实查询(30%)、需要跨文档推理的问题(30%)、知识库里不存在答案的问题(20%)、模糊或歧义问题(20%)。
时效性:知识库会更新,评测集也要同步。过期的评测集测出来的分数没有参考价值。
上线后的持续评测
评测不是一次性的事情。模型升级、知识库更新、Prompt 调整——任何一个环节变动都可能影响质量。
我把持续评测分为三个层次:
| 层次 | 频率 | 评测内容 | 触发条件 |
|---|---|---|---|
| 冒烟测试 | 每次部署 | 50-100 条核心 query 的 Faithfulness + Answer Relevancy | CI/CD 触发 |
| 回归测试 | 每周 | 完整评测集的所有指标 | 定时或知识库更新 |
| 深度评测 | 每月 | 含边界场景、拒答测试、对抗测试 | 模型升级或架构变更 |
Patronus AI 的建议是「定义明确的质量阈值,当指标下降超过阈值时自动触发告警」(Patronus AI, 2025)。我在实践中设置的阈值是:Faithfulness 下降超过 0.05 就触发排查,Context Recall 下降超过 0.08 就暂停发布。
上线前检查清单
数据准备阶段
- 评测集覆盖至少 200 条 query,包含简单查询、推理查询、无答案查询、模糊查询
- ground_truth 经过至少两人交叉审核
- 评测集和训练集无重叠
- 已标注每条 query 对应的 ground_truth 引用来源(context)
指标选择与实现阶段
- 至少包含 Faithfulness、Context Precision、Answer Relevancy 三个核心指标
- 已固定 Judge LLM 的版本和温度参数
- 针对拒答场景定义了独立的评测逻辑
- 检索层和生成层指标分开计算和展示
- 已建立各指标的质量阈值和告警规则
持续监控阶段
- CI/CD 流水线集成冒烟测试,每次部署自动运行
- 评测结果持久化存储,支持趋势对比
- 知识库更新后自动触发回归测试
- 定期(每月)更新评测集,淘汰过期 query
- 有明确的「分数下降 → 排查 → 修复」工作流
参考资料
-
Es, S., James, J., Espinosa-Anke, L., & Scarlatos, S. (2023). RAGAS: Automated Evaluation of Retrieval Augmented Generation. arXiv:2309.15217. https://arxiv.org/abs/2309.15217
-
Mistral AI. (2025). Evaluating RAG with LLM as a Judge. https://mistral.ai/news/llm-as-rag-judge/
-
Patronus AI. (2025). RAG Evaluation Metrics: Best Practices for Evaluating RAG Systems. https://www.patronus.ai/llm-testing/rag-evaluation-metrics
-
RAGAS Official Documentation. Available Metrics. https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/
-
Evidently AI. (2025). A Complete Guide to RAG Evaluation: Metrics, Testing and Best Practices. https://www.evidentlyai.com/llm-guide/rag-evaluation
-
IBM. (2024). Result Evaluation for RAG: Metrics & Best Practices. https://www.ibm.com/think/architectures/rag-cookbook/result-evaluation
-
Microsoft. (2025). 开发 RAG 解决方案——大型语言模型端到端评估阶段. https://learn.microsoft.com/zh-cn/azure/architecture/ai-ml/guide/rag/rag-llm-evaluation-phase
-
Databricks. (2023). Best Practices for LLM Evaluation of RAG Applications. https://www.databricks.com/blog/LLM-auto-eval-best-practices-RAG