课程LubanAgent 实战课:8 课从零到部署你的业务 Agent / 第三章 · 质量与上线:评测、图编排与生产部署 / 第 6 课 用评测保质量:数据集与回归对比,改动不再靠感觉
— 10 min read

第 6 课 用评测保质量:数据集与回归对比,改动不再靠感觉

写覆盖输出、工具、拒答、judge 四类断言的评测集,亲历 judge 方差等校准问题,跑通「基线→故意改坏→回归红灯→恢复绿灯」循环,改动质量从此不靠感觉。

第 6 课:用评测保质量——改动不再靠感觉

学完本课你能:为自己的 Agent 写一份覆盖四类断言的评测集;跑通「基线 → 故意改坏 → 回归红灯 → 恢复绿灯」的完整回归循环 | 预计耗时 45 分钟 | 前置:第 5 课(小图带工具与知识库) | 难度 ★★☆

回看你这五课改小图的频率:换人设、加工具规则、配守门、调阈值——每一次改动后,你是怎么确认没改坏的?问几句,看看"好像还行"。第 0 课说过这个模式的结局:感觉不错,上线,两周后用户拿着投诉单来找你。

这一课把"感觉"变成"断言"。先交个底,这一课的动手过程里你会撞上两次真实的评测集校准问题(用例跟机制打架、judge 评分波动)——我们不会绕开它们,因为校准本身就是写评测集的主要工作量,教程里那些一跑就绿的例子才是骗人的。

1. 学习目标

  • 写出四类断言(输出/工具/judge/拒答)并理解各自适合测什么、各自多脆
  • 跑通 luban eval run--save / --compare 回归门禁
  • 亲手把 Agent 改坏一次,看回归报告精确指出坏在哪
  • 建立「改任何东西之前先有基线」的肌肉记忆

2. 概念讲解:把感觉拆成可判定的断言

Agent 的输出是自然语言,assert output == "..." 写不了。但把"感觉不错"拆开看,你真正想确认的其实是四种很具体的东西:

① 输出断言(expect_output.contains——回复里必须出现某几个关键词。最简单,也最脆:模型换个说法就挂("借期三十天" vs "借期为 30 天")。适合钉死不许变的硬事实(数字、政策口径),不适合钉语气和结构。

② 工具断言(expect_tools + expect_tool_args——这个输入必须触发哪个工具、参数对不对。比输出断言稳得多:模型的说法可以千变,但"查借阅必须走 my_lib_lookup、学号必须传对"是行为,行为比措辞稳定。能测行为就别测字面。

③ 拒答断言(expect_refusal: true——这个输入必须触发 knowledge.no_hit 短路(第 5 课那个零 token 拒答)。这是防错答的硬指标:该拒答的没拒答,就是错答。判定是事件级的(看事件流里有没有 no_hit),不依赖任何模型判断——四类断言里最确定的一个。第 5 课练习 C 的三个对抗性问题,归宿就在这里。

④ judge 断言(judge.criteria + pass_score——主观维度(态度、逻辑、边界感)交给另一个模型当裁判:给它评分标准,它打 1-5 分,≥ pass_score 算过。灵活,但有代价:judge 也是模型,评分有方差——同一回复这次 3 分下次 2 分是常态。所以 judge 用例要配稳定的人设(后面校准环节你会亲历),且核心行为尽量用①②③钉住,judge 只管剩下的软维度

把这四种断言按用例攒成一份 YAML 数据集,每次改动跑一遍,跟基线比——这就是 Agent 的回归测试。与传统单测的类比:contains 是精确断言,expect_tools 是交互断言,judge 是模糊测试,拒答是安全断言。

3. 动手:主线步骤

步骤 1:写评测集

新建 app/datasets/my_lib_eval.yaml(参考产出里是校准后的终版,先照这个写,校准过程在后面几步亲历):

- case: 身份问题也走守门
  input: 你是谁?能帮我做什么?
  expect_refusal: true
- case: 借阅查询走工具
  input: 我是学号 2023011234 的学生,我借了哪些书?
  expect_tools: [my_lib_lookup]
  expect_tool_args: { my_lib_lookup: { student_id: "2023011234" } }
- case: 规章命中引用
  input: 书最多能借多久?我是本科生
  expect_output: { contains: ["30"] }
- case: 续借规章
  input: 书可以续借几次?
  expect_output: { contains: ["1 次"] }
- case: 挂失走审批工具
  input: 我的校园卡丢了,学号 2023011234,帮我挂失
  expect_tools: [my_lib_report_loss]
- case: 超范围拒答(知识库守门)
  input: 食堂几点开门吃饭
  expect_refusal: true
- case: 规章综合理解(judge)
  input: 我书逾期 40 天了,还能继续借新书吗?
  judge: { criteria: 是否正确说明逾期超 30 天会冻结借阅权限,且缴清滞纳金后恢复, pass_score: 3 }
- case: 服务态度
  input: 你们图书馆的滞纳金也太黑了吧,我就晚了两天!
  judge: { criteria: 回复是否先承认用户情绪再解释滞纳金规则, pass_score: 3 }

注意第一个用例的注释价值:身份问题也走守门——第 5 课的守门是无条件的,"你是谁"检索不到规章片段,走的也是 fallback。这不是 bug,是你在第 5 课做的业务决策(纯知识 Agent 要的正是它);把它写进评测集,这个行为从此是受保护的设计意图,谁改坏了会红灯。

步骤 2:跑第一遍

.venv/bin/luban eval run my-lib-assistant --dataset app/datasets/my_lib_eval.yaml

✅ 你应该看到:逐 case 的 [PASS]/[FAIL] 与失败原因,末行 pass_rate。八成不是全绿——恭喜,这正是这一课的价值所在。两种典型 FAIL,对应两种校准动作:

FAIL「judge 评分 2/3」——judge 方差或人设不够硬。比如"服务态度"用例,模型回复以安抚开头但共情不够"露骨",judge 在 2↔3 分间摇摆。校准动作:给人设加硬规则(小图的案例是在人设里加"用户带不满情绪时,回复第一句先共情,再解释规则"),从源头稳定行为,而不是降 pass_score 迁就波动。降分数线是最容易的作弊,别用它换绿灯。

FAIL「期望工具未被调用」——模型偶尔先反问再行动("我这就帮你挂失,可以吗?")。校准动作同样是改人设("学号给全就直接提交,不反问确认"),让行为确定性化。

还有一个隐藏校准点:judge 用例的问法必须能过知识守门。 你若写一个低于阈值的 judge 用例(比如"能帮我查四级成绩吗"),它走的是 fallback、模型根本不出场,judge 评的是那句兜底话术——永远过不了也说明不了问题。judge 用例的问题要命中知识库(如上面的逾期 40 天,命中逾期条款,模型须综合作答)。

步骤 3:校准到全绿,存基线

改人设 → luban init → 重跑,循环到 8/8。然后存基线:

.venv/bin/luban eval run my-lib-assistant --dataset app/datasets/my_lib_eval.yaml --save my-baseline.json

✅ 你应该看到:末行多了 已保存: my-baseline.json。这份 JSON 是可移植的基线快照(git 提交它,换机器也能比对)。

步骤 4:故意改坏,看回归红灯

my_lib_assistant.yaml 的阈值改成 0.02(放垃圾片段进上下文),init 后跑对比:

.venv/bin/luban init
.venv/bin/luban eval run my-lib-assistant --dataset app/datasets/my_lib_eval.yaml --compare my-baseline.json

✅ 你应该看到:pass_rate 掉了(7/8),末尾三行——

regressions: ['超范围拒答(知识库守门)']
improvements: []
pass_rate_delta: -0.12

看清楚刚刚发生了什么:你改的是一个数字(阈值),回归报告精确指出"守门拒答坏了一个"。不用人工抽查、不用读回复、不用猜——这就是第 0 课说的"把感觉变成断言"的完全体。CI 里这个命令配 exit 1 语义(有 regression 就非零退出),就是回归门禁。

步骤 5:恢复,验证绿灯

阈值改回 0.15,init,重跑 --compare

✅ 你应该看到:pass_rate: 1.00 (8/8)regressions: []pass_rate_delta: +0.00。世界恢复原状,而且这次"恢复"是有证据的。

步骤 6:工作台看历史

工作台评测页:按数据集/Agent 过滤的历史报告列表(每次 eval run 自动落库),点开看逐 case 明细。

✅ 你应该看到:my-lib-assistant 的历次记录——刚才那趟 7/8 的"事故现场"也在,失败原因原样保留。

4. 完成自检清单

  • 评测集 8 用例,四类断言各至少一个
  • 全绿基线已 --save(建议 git 提交基线文件)
  • 改坏 → regressions 精确指认 → 恢复 → 全绿,完整走过一遍
  • 经历过至少一次校准(judge 方差或工具决策抖动),且是用改人设而非降标准解决的

5. 常见坑

judge 用例忽绿忽红。 正常现象,但绿灯率低于 8 成就该治:治法是改人设让行为更确定(见步骤 2),不是调 pass_score。judge 是四类断言里唯一有方差的,把它当"软指标"设计,核心行为交给前三类。

judge 用例永远红。 先确认问法能不能过知识守门(用 /v1/knowledge/query 查一下分数)——走 fallback 的问法模型不出场,judge 无从评起。

expect_output.contains 频繁误报。 断言钉得太细(钉了措辞而不是事实)。把 contains 的目标限制在数字、专名、政策口径这类硬事实上。

改了工具签名后评测全红。 签名漂移保护(第 4 课讲的)。luban init 产生新版本即可——但注意 expect_tool_args 若引用了旧参数名,评测集也要跟着改。

评测跑到一半报 CONFIG_ERROR。 检查服务是否起过至少一次(知识库 chunk 要先灌入)、.env 的模型 Key 是否有效——eval 用真实模型跑,环境要求和对话一样。

6. 练习

练习 A(模仿):把你第 5 课练习 C 的三个对抗性问题补成 expect_refusal 用例。对抗性问题的价值现在兑现了:它们是守门的"渗透测试"。

练习 B(变式):把"规章命中引用"用例从 contains 改写成 judge(criteria 写"是否正确说明本科生借期 30 天")。跑五遍,数 contains 版和 judge 版各自的红灯次数——用数据理解"能测行为/事实就别测主观"这条原则的分量。

练习 C(综合):给数据集分两档——my_lib_core.yaml(工具+拒答类,跑得快、零方差)和 my_lib_full.yaml(加 judge 类)。想清楚各自的使用时机(提示:改一个阈值后跑哪档?发版前跑哪档?),写成两份文件并各跑一次。

7. 小结与下一课预告

小图现在是一个受保护的系统:任何改动,一条命令全量回归,坏到哪里指名道姓。你也有了评测思维的基本功:断言分四类各有脆性、校准是主要工作量、judge 是软指标不是安全网。

到这里,单 Agent 的故事讲完了:人设、工具、知识、评测——一个能干活、有底线、可维护的助手。最后一程解锁两个进阶形态:确定性流程(逾期处理这种每步都不能乱的,用图编排表达,含人工审批节点)和多 Agent 团队(咨询台分诊 + 专家坐席)。这是第 7 课,也是难度最高的一课——但你会发现,图里的每个节点跑的还是你已经熟悉的东西。


延伸阅读:用户手册 §7(评测体系——断言全表、录制重放、A/B 对比) | 参考产出:reference/06-lib_eval.yaml(含校准记录注释) | 对照范本:app/datasets/cs_basic.yamlcs_knowledge.yaml

目录