RAG 上线后最常见的困境是"感觉答得还行",但产品、测试、老板问"到底行不行"时拿不出数字。这套评测方案把 RAG 质量拆成两层可量化指标:检索层 (hit rate、MRR、context precision/recall)衡量"有没有把对的东西捞上来",生成层(faithfulness 忠实度、answer relevancy、噪声敏感度)衡量"有没有基于捞上来的东西好好回答",统一交给 LLM-as-a-Judge 打分,配一个 200 条左右的 golden set 做回归。选型上指标计算用 ragas(业界通用、指标全),回归门禁用 promptfoo(YAML 配置、接 CI 成本低),judge 模型固定用 GPT-4o 且 temperature=0。跑了两个月,线上 52 个 query 的 faithfulness 从 0.71 提到 0.86,人工抽检一致率 84%。下面讲清楚指标怎么定义、judge 怎么校准、以及踩过的坑。
指标分层:先分清楚"没检索到"还是"没答好"
RAG 的错误一半以上出在检索层,但很多人只盯着最终回答看,导致定位问题全靠猜。把指标拆两层,出问题先看检索层指标,再看生成层指标,定位成本低一个数量级。
| 层级 | 指标 | 衡量什么 | 怎么算 |
|---|---|---|---|
| 检索层 | Hit Rate@k | 正确答案是否在 top-k 里 | 每个 query 有 golden chunk 标注,命中即 1 |
| 检索层 | MRR | 正确答案排得多靠前 | 1/rank 取平均 |
| 检索层 | Context Precision@k | 捞上来的 chunk 里有用的比例 | 有用 chunk 的 precision 按位置加权 |
| 检索层 | Context Recall@k | 该捞的 chunk 捞上来多少 | 命中 golden chunk 数 / golden 总数 |
| 生成层 | Faithfulness | 回答里的 claim 有多少能被检索上下文支撑 | 逐 claim 判定,支撑数 / 总数 |
| 生成层 | Answer Relevancy | 回答是否切题、没答非所问 | judge 对 question↔answer 相关性打分 |
| 生成层 | Noise Sensitivity | 混入无关上下文后回答漂移多少 | 注入 top-k 外的噪声 chunk,对比回答变化 |
实际经验:先修检索层再谈生成层。context recall 低于 0.8 时,生成层指标再好看也是假象------答案可能来自模型参数记忆而非检索结果,上线后换知识库就翻车。检索层达标后,生成层的 faithfulnes 才是真正的"有没有忠实于资料"。
LLM-as-a-Judge:别整体打分,拆 claim 再判
judge 最容易犯的错是让 LLM 对"整个回答"打一个分。整体打分有两个毛病:一是 LLM 对长回答的局部错误不敏感,二是容易受"看起来专业"的措辞影响。正确做法是两步走:先让 judge 把回答拆成原子 claim,再逐条判定"该 claim 是否能被给定上下文支撑",最后算支撑比例。
judge prompt 模板(faithfulness 判定):
你是一个严谨的评测员。任务:判断回答中的每个事实性陈述(claim)是否能被"给定上下文"支撑。
第一步:把回答拆成原子 claim,每个 claim 只包含一个可验证的事实。
第二步:对每个 claim 输出 JSON:{"claim": "...", "supported": true/false, "reason": "..."}
判定规则:
- 只有上下文明确包含的信息才算 supported,模型自身知识不算。
- 上下文没有提及的 claim 判 false(哪怕它是对的)。
- 上下文与 claim 矛盾判 false。
只输出 JSON 数组,不要额外解释。
代码侧用 ragas 跑全套指标:
from ragas import EvaluationDataset, evaluate
from ragas.metrics import (faithfulness, answer_relevancy,
context_precision, context_recall)
from ragas.llms import LangchainLLMWrapper
from langchain_openai import ChatOpenAI
judge = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o", temperature=0))
dataset = EvaluationDataset.from_dict({
"user_input": ["合同里违约金比例是多少?", "公司2025年营收多少?"],
"retrieved_contexts": [["<chunk 1>", "<chunk 2>"], ["<chunk 1>"]],
"response": ["合同约定违约金为合同总额的10%......", "根据检索资料,未找到相关数据。"],
"reference": ["违约金为合同总额的10%。", "资料中未披露2025年营收。"],
})
result = evaluate(dataset, metrics=[
faithfulness, answer_relevancy, context_precision, context_recall,
], llm=judge)
df = result.to_pandas()
print(df[["faithfulness", "answer_relevancy",
"context_precision", "context_recall"]].describe())
三个关键点:temperature=0 必须写死,否则同一条数据两次跑分能差 0.1;retrieved_contexts 和 response 必须来自同一次运行快照 ,分开采集会导致指标对不上;golden set 的 reference 要人工反复校对,因为 faithfulness 的判定基准是检索上下文,reference 本身有幻觉会带偏整个分数。
持续回归:promptfoo 接 CI,改完就测
离线批跑指标解决"这个版本行不行",解决不了"这次改动有没有把之前修好的问题弄回去"。用 promptfoo 把评测固化进 CI,每次改 prompt、改切分策略、改重排都跑一遍 golden set。
# promptfooconfig.yaml
prompts:
- file://prompts/rag_answer.txt
providers:
- id: openai:gpt-4o
config:
temperature: 0
defaultTest:
options:
# 每个 test 都会先跑检索器取上下文,保证上下文是当前版本的真实输出
setup: file://setup/retrieve.py
tests:
- vars:
question: "合同里违约金比例是多少?"
assert:
- type: llm-rubric
value: "回答必须只基于给定上下文;上下文不含该信息时必须明确说不知道"
- type: contains-all
value: ["10%"]
- vars:
question: "公司2025年营收是多少?"
assert:
- type: llm-rubric
value: "若上下文无营收数据,回答必须明确拒绝,禁止编造"
- vars:
question: "合同有效期多长?"
assert:
- type: llm-rubric
value: "回答必须与上下文一致,不得引入上下文之外的事实"
llm-rubric 类型的断言就是 LLM-as-a-Judge 的工程化封装,语义化断言比字符串匹配抗"说法变了但意思没变"的情况。CI 里 promptfoo eval --config promptfooconfig.yaml 输出 JSON 报告,配合 promptfoo assertions 的阈值参数,分数跌破阈值直接让流水线失败。
踩坑记录
坑 1:judge 的位置偏差和自褒偏差。 同一份回答,把上下文顺序调换,faithfulness 能差 0.05~0.1;judge 还倾向给"结构完整、语气自信"的回答加分。对策:关键版本跑两遍(上下文顺序打乱各一次)取均值;rubric 里明确禁止"well-structured"之类的评价维度,只许判事实支撑关系。
坑 2:reference 污染。 早期 golden set 的参考答案是让另一个 LLM 生成的,没人工核对,结果 faithfulness 虚高------judge 看到参考答案里也有该 claim,倾向判 supported。后来对 200 条 reference 做了两轮人工校对,faithfulness 整体掉了 0.06,但和人工抽检的一致率从 71% 涨到 84%。指标的可信度比数值好看更重要。
坑 3:噪声敏感度必须主动构造。 真实用户 query 里"上下文混入无关内容"是小概率事件,被动采样测不出来。做法:评测时把检索结果第 k+1 到 k+3 的 chunk 强制塞进上下文,看回答漂移。某次改完重排后漂移率从 18% 降到 6%,但只测 faithfulness 完全看不出来------这就是噪声敏感度单独成指标的原因。
坑 4:阈值拍脑袋。 一开始定 faithfulness ≥ 0.85 才算过,结果小模型+简单场景根本到不了,大模型+难场景轻松超,阈值形同虚设。改成按场景分档:标准问答场景 ≥ 0.85,长文档综合场景 ≥ 0.75,低于档位禁止上线。阈值要和业务场景绑定,不能全局一个数。
工具对比与使用建议
| 工具 | 定位 | 强项 | 适合场景 |
|---|---|---|---|
| ragas | RAG 专项指标库 | 忠实度/检索指标全面,离线批跑 | 版本迭代的指标基线 |
| promptfoo | prompt/agent 测试框架 | llm-rubric 断言、CI 集成、diff 对比 | 回归门禁、prompt 调优 |
| Opik | 评测+追踪一体 | 线上 trace 与离线评测打通 | 线上问题回溯定位 |
| Giskard | 测试+红队 | agent 测试、漏洞扫描 | 安全与边界场景 |
组合建议:离线指标用 ragas 定基线,线上回归用 promptfoo 接 CI,线上观测用 Opik 接 trace。小团队先上 ragas + golden set 就够,别一上来铺四个工具,评测链路本身也是要维护的成本。
小结
RAG 质量评测的落地顺序:先建 200 条左右 golden set(覆盖各业务场景、含边界 case)→ 用 ragas 跑出检索层+生成层基线 → 修检索层(切分、embedding、重排)到 recall 达标 → 再调生成层(prompt、模型)→ 用 promptfoo 把 golden set 固化进 CI 当回归门禁。评测不是一次性工程,是跟着提示词、切分策略、重排一起迭代的闭环。进阶方向:用 LLM 合成数据自动扩充 golden set、把 judge 蒸馏成小模型降推理成本、按 diff 增量触发评测而不是全量跑。
如果你也在做 AI 应用(RAG / Agent / LLM),不知道质量怎么测------我最近在给 AI 应用做免费质量体检,出一份可执行的测评报告(检索命中率、回答忠实度、噪声敏感度等维度),感兴趣可以直接私信我。