
快速阅读(30 秒速览)
- 调了参数不知道效果变好还是变坏?你需要一套可重复的量化评测
- EasyRAG 的 GoldenSetEvaluator:准备一份"黄金问答集",系统逐条跑,输出混淆矩阵 + 三大指标
- 三大指标:幻觉率(该拒答时硬答的比例)、覆盖率(回答了多少)、忠实度(回答的正确率)
- 黄金集里要故意放"应该拒答"的问题——护栏的价值只有在拒答题上才能被量化
- 评测器通过 Queryable trait 解耦:可以评真实引擎,也可以评 mock
前情提要:上一篇讲完查询引擎,自然要回答下一个问题:这些设计到底有没有让效果变好?不能凭感觉,要拿数字。
"感觉变好了"不是评测
一个常见的调参循环:
改了分块参数 → 问了几个问题 → “好像更准了” → 上线 → 用户投诉变差了
问题出在哪?样本太小、没有基线、没有对照组。你记住的永远是那几个答对了的问题,答错的早就忘了。
科学的做法是把评测变成一段可以反复执行的代码:固定问题集、固定指标、一键出报告。
第一步:构建黄金问答集
黄金集(Golden Set)是一组人工标注的问答对,EasyRAG 的模型定义在 easyrag-core:
pub struct GoldenItem {
pub query: String,
pub expected_answer: Option<String>, // None = 这个问题应该拒答!
pub query_type: Option<String>, // 可选分类:single_hop / multi_hop…
}
注意 expected_answer 是 Option——这是整套设计里最关键的细节。黄金集分两类:
# testdata/golden/ 示例(示意)
{ "query": "产品保修期多久?", "expected_answer": "主要部件保修 2 年" }
{ "query": "公司食堂中午几点开饭?", "expected_answer": null }
第二类问题就是幻觉检测器:如果系统对"食堂几点开饭"给出了具体时间的回答——幻觉实锤。
构建建议:
- 从真实用户问题里挑(别自己编,真实问题的表述方式有你想不到的多样性);
- 应答题与拒答题比例约 2:1;
- 每道题标注到"答案在哪份文档"——答错时能直接定位是检索问题还是生成问题。
第二步:评测器的混淆矩阵思维
easyrag-pipeline/src/evaluation.rs 的 GoldenSetEvaluator 把每道题归入四种情况:
let (classification, correct) = match (should_abstain, abstained) {
(false, false) => { // 应回答,回答了 → TP,再查答得对不对
let correct = is_roughly_correct(&response, item.expected_answer.as_deref());
matrix.true_positive += 1;
("tp".to_string(), correct)
}
(true, true) => { // 应拒答,拒答了 → TN,正确
matrix.true_negative += 1; ("tn".to_string(), true)
}
(true, false) => { // 应拒答,却回答了 → FP,幻觉!
matrix.false_positive += 1; ("fp".to_string(), false)
}
(false, true) => { // 应回答,却拒答了 → FN,漏答
matrix.false_negative += 1; ("fn".to_string(), false)
}
};

"是否拒答"怎么判断?系统拒答时返回统一的 FAIL_RESPONSE 话术(第 5 篇讲过),评测器识别这个标记即可——护栏与评测共用同一套协议,这是系统设计上的呼应。
第三步:三大指标
// 覆盖率:系统愿意回答多少问题
let coverage = answered as f32 / total as f32;
// 幻觉率:回答了的问题里,多少是"本该拒答却硬答"
let hallucination_rate = matrix.false_positive as f32 / answered as f32;
// 忠实度:应答题里,实际答对的比例
let faithfulness = correct_answers as f32 / matrix.true_positive as f32;
三个指标必须一起看,因为它们互相牵制:
| 覆盖率 | 是 | 全都回答(幻觉率爆炸) |
| 幻觉率(越低越好) | 越低越好 | 全都拒答(覆盖率归零) |
| 忠实度 | 是 | 只回答最有把握的题 |
单看任何一个指标都会被系统"应试"刷分。 护栏阈值往严调,幻觉率降、覆盖率降;往松调反之——评测报告就是帮你找到那个平衡点。
此外报告还包含 risk-coverage 曲线:按置信度排序逐步放宽拒答阈值,画出"幻觉风险随覆盖率上升"的曲线,你可以直接读图选工作点。
工程细节:评测器怎么不绑死实现
#[async_trait]
pub trait Queryable: Send + Sync {
async fn eval_query(&self, query: &str, param: &QueryParam)
-> easyrag_core::Result<String>;
}
pub struct GoldenSetEvaluator {
queryable: Arc<dyn Queryable>, // 可以是真实引擎,也可以是 mock
param: QueryParam,
}
Queryable trait 让评测器与查询实现解耦:
- 生产评测:接真实 QueryEngine,通过 /api/eval/golden-set 路由在线跑(另有 /api/eval/risk-coverage 单独出曲线);
- 单元测试:接 mock,验证指标计算本身(混淆矩阵逻辑的测试不需要真模型)。
查询失败也算拒答——评测循环里 Err 被归入 FAIL_RESPONSE,一次网络抖动不会让整份报告作废。
评测驱动开发:真实工作流
1. 基线报告:默认配置跑一遍,存档
2. 改一个变量(分块策略 / top_k / 护栏阈值 / 换 embedding 模型)
3. 重跑,diff 三个指标
4. 指标升 → 保留;降 → 回滚,换下一个变量
一次只改一个变量——这是评测能给出因果结论的前提。同时改三个参数等于没评测。
踩坑记录
坑 1:黄金集全是应答题。 幻觉率永远是 0(分母里没有拒答题),护栏调了个寂寞。拒答题至少占 1/3。
坑 2:期望答案写得太"标准"。 is_roughly_correct 做的是粗匹配,期望答案写成论文式长句会导致"答对了但判错"。期望答案写关键事实短语(“2 年”“3.2 亿元”),判对率立刻合理。
坑 3:评测用流式模式。 流式输出拼字符串有开销且不稳定,评测统一走非流式接口,结果可复现。
写在最后
| 调参 | 凭感觉 | 看指标 |
| 上线 | 赌运气 | 基线对比 |
| 换模型 | 心里没底 | 一键回归 |
| 向老板汇报 | “应该更好了” | “幻觉率从 12% 降到 3%” |
评测体系是 RAG 项目里投入产出比最高的基础设施——它不提升任何一次回答的质量,但它让每一次优化都可信。
下一篇进入工程实战篇:同一个系统,本地用 JSON 文件、生产用 PostgreSQL+Neo4j,代码一行不改——Trait 可插拔存储是怎么做到的。
你的团队怎么评测 RAG 效果?有黄金集吗?评论区聊聊。
本文为《手撸一个生产级 RAG:EasyRAG 实战系列》第 7 篇。上一篇:7 种检索模式 | 下一篇:Trait 可插拔存储
开源仓库:haibingzhao/easyrag —— 欢迎 Star、Fork、提 Issue 和 PR。
网硕互联帮助中心





评论前必须登录!
注册