第 11 章 把评测做成回归:测试集、断言与流水线
难度 ★4 | 约 4 学时 | 6 节
这一章把评测接进流水线。它的核心不是「配什么工具」,而是三个决定:拦什么、门槛多高、红了之后人能读懂什么。
学习目标:把评测接进流水线,并明确「什么该拦、什么只报警」。
本章交付物:一份接入流水线的评测配置 + 一次「故意改坏」的红灯记录
11.1 门禁与看板分工不同——但边界不在「确定性」,在「阈值」
这一节的第一件事是分账:哪句话是本书的,哪句话可以照抄官方。
本书的切法:「门禁与看板分工不同」。门禁回答「这次合并能不能进」,看板回答「质量在往哪个方向走」。这两个问题的读者不同、时效不同、容忍的噪声也不同。
官方的口径:非确定性断言照样可以进流水线,只是不给它 100% 的门槛——用通过率下限,并有一个专门的门槛参数。
把两者放在一起,你就看到了本节最重要的一处修正:
边界不在「确定性与非确定性」,而在「门槛配多少」。
因为「把随机指标当门禁,最后一定会被绕过」这个现象,成因不是「它进了流水线」,而是「它被要求每次都全过」。要求非确定性断言 100% 通过,得到的必然是「一跑就红、重跑就绿」的构建——而一个随机变红的门禁,团队的第一反应不是修 bug,是点一次重跑。从那一刻起,门禁就名存实亡了。
所以准确的表述是:不是「随机指标不进流水线」,而是「随机指标不能配 100% 的门槛」。
原文那条直觉因此要保留、但换一个理由:它会被绕过,不是因为它是随机的,是因为它的门槛配错了。
还有一条可以直接照抄的细节,很多人不知道:退出码是分层的——测试失败返回 100,配置错误之类的运行性问题返回 1。
这个区分为什么重要?因为它把两种完全不同的红灯分开了:「评测没通过」与「评测没跑起来」。 前者要去看用例,后者要去看配置——如果你的流水线把它们都当成「失败」,团队会先学会忽略它。
第三处要补的是「同一组断言喂两处」:门禁与看板的读者不同,但断言可以是同一批。 不必为「进流水线」与「上墙」各写一套——两套断言一定会漂移,而漂移之后,被信任的那一套永远不是你在改的那一套。
最后一条接口,也是本节最容易被忽略的:门禁被绕过的第二种成因,不是指标随机,而是「红了之后没人知道该改什么」。
所以 11.4 的归因能力其实是门禁能不能守住的前提——一个报不出「哪条用例、哪个断言」的门禁,它的最终命运一定是被加进忽略列表。
11.2 要用通过率阈值,而不是「全对」
这一节只需要一条机制句,但它是整章的地基:
模型评分类断言本身是非确定性的,所以要求每次运行都 100% 会得到不稳定的构建。 正确做法是定一个反映真实可接受质量的下限,并允许少量噪声通过。
这句话可以压成一句更好记的:阈值不是宽容,是对测量精度的一次承认。
官方示例里给的起步门槛就是 100,并明确说明:「等你的用例集大到一条不稳定的用例不该挡住合并时,再把它降下来」(示例给出的下一个数字是 90)。
请注意这个顺序:先 100,再降。 它不是「先松再紧」,而是「先确认你的用例都稳,再承认噪声」。反过来做(先 90 再往上加)会让你永远不知道那 10% 里有多少是噪声、有多少是真 bug。
接下来是三条定界,没有它们,「定个阈值」就会变成拍脑袋:
① 阈值要与用例规模一起动。
用例少时,噪声占比大,阈值必须高(否则门禁形同虚设——50 条里允许错 5 条,等于没设门槛);用例多时,单条波动占比小,阈值可以降。所以阈值是一个随规模调整的参数,不是一次定死的常数。
这句话有一个很实用的推论:「我们的阈值是 90」这句话本身没有信息量,得配上「在多少条用例上」。
② 断言可以分组,组本身带通过比例。
官方配置里有一种「断言组」写法:组内断言按权重计,组有一个通过比例(例如两条断言里过一条即算组通过)。这正好用来表达「格式必须全对,但风格类的可以由一即可」。
这一条解决的是一个真实的矛盾:一个提示词可能让格式更稳、同时让语气更松。 如果所有断言平铺在一张表里按同一个比例算,你就无法表达「这两类断言的严格程度不同」。
③ 阈值必须钉在模型快照上。
如果你没钉快照,阈值会随上游更新漂移——那时候红绿灯的含义就变了:你不知道红灯是「你的回归」还是「上游的改动」。(12.2 会把这件事拆得更细。)
收口:阈值是对测量精度的承认,不是宽容。 这句话能挡掉两种误读——把它当「放松要求」的人会越降越低;把它当「一次定死」的人会永远不调。
11.3 提示词、模型、参数三者都要版本化——但要给出成本边界
先给一句比「三者都要版本化」更准确的话,它是本节的锚:
提示词版本 + 模型快照,合起来构成部署单元。
这一句解释了大纲那句「只版本化代码是不够的」到底在说什么:不够的原因不是「还有两样没版本化」,而是「部署单元本来就不是代码一个东西」。 你发上线的不是一个仓库,是一段文本 + 一个权重 + 一组参数的组合。
配套的纪律也是现成的:任何一次要出厂的提示词,都必须挂着一个提交号和一次评测运行。
现在说这一节真正要补的东西:「任何一项变化都该触发一轮评测」这句话,如果不加成本边界,会得到一个跑不起的评测。
三条边界都是现成的做法:
① 让评审参与评分,会让每个用例的成本大约翻倍。(10.4 那条,乘法不是加法。)
② 在流水线里开缓存,让「提示词与输入都没变」的用例不被重复计费。
③ 用路径过滤,只在提示词文件或配置真正变化时才触发这次评测——不要让无关的提交去花模型调用费。
三条合起来,才让「每次变化都测」从一句口号变成一笔付得起的账。没有②③,你会得到一个「因为太贵所以只在发版前跑」的评测——而那等于没有回归。
第二处要补的是版本化的对象比「三样」更多:
| 要版本化的东西 | 为什么 | 不版本化的后果 |
|---|---|---|
| 提示词 | 它决定行为 | 改了什么说不清 |
| 模型快照 | 它决定权重 | 上游一改,结果就漂(11.2 的红绿灯失效) |
| 参数 | 它们决定采样行为 | 「同一版」跑出两种结果 |
| 评测集 | 用例变了,通过率就不可比 | 92% 与 88% 可能量的不是同一件事 |
| 评审模型 | 换评审等于换了一把尺子 | 你的分变了,但你不知道是质量变了还是尺子变了 |
后两行最容易被漏,因为它们不「在应用里」。判断标准很简单:这个对象变了之后,「92%」这个数字的含义会不会变?会变,就要版本化。
收口是一句可以把整章串起来的话:
通过率只有在「同一批用例 + 同一把尺子 + 同一个快照」这三个条件同时成立时才可比。
三个条件,任何一个变了,你得到的就不是「变好/变差」,而是「两个不同的测量」。 这也解释了本节为什么排在第 11 章中间——它前面是阈值(11.2 要知道阈值钉在哪),后面是归因(11.4 要知道差值可不可信),再往后是变更管理(12 章要知道一次只动什么)。
11.4 评测结果要能归因:报到能指认一行
这一节有一个现成的范例,可以直接照着抄形状:
官方那个流水线动作在拉取请求里做的事是——跑一遍评测,并与基线做前后对比,把结果直接评论在拉取请求上。评论里给出一份记分卡:「这些用例变好了、这些变差了、差值是多少」,并附一个可以交互查看前后对比的链接。
请注意它做的事的性质:「归因」不是让人去翻日志,而是让流水线主动把差值摆到评审者面前。
官方对这条动作的定位也说得很清楚:它的意义是**「评审者不必再『相信』一次改写过的提示词没问题」**——差异自带记分卡。
这背后是一个很具体的产品判断:在 AI 功能里,「我相信这个改动没问题」是一句无法验证的话。 而记分卡把「相信」换成了「看这里」。
现在补这一节的反面。大纲说「只报通过率从 92% 掉到 88% 帮不上任何忙」——这句话的反面不是「报更多数字」,而是「报到能指认一行」。
要能指认三件事:
| 要素 | 具体是什么 |
|---|---|
| 哪一条用例 | 输入是哪一条(能定位到那一行数据) |
| 哪一个断言 | 是格式没过,还是内容没过 |
| 差在哪里 | 前后两版的输出放在一起 |
三个都要。少第一件,你不知道去重放哪条;少第二件,你不知道去改提示词还是改解析;少第三件,你甚至无法判断这次「变差」是不是你的断言写得太苛刻。
第二处要补:梯度必须保留。
记分卡的价值在于它同时显示「变好的」与「变差的」。只看总分会漏掉一类非常常见的失败:一批变好换一批变差——总分没动,体验却换了个形状。 这在提示词调整里极其常见(你让格式更稳,往往代价是别处更啰嗦)。
总分是一个会把「交换」隐身掉的量。 两个版本总分一样,不代表它们对用户一样。
第三处:归因要有基线。
「可比」的前提是 11.3 那三个条件同时成立。基线不稳,记分卡上的差值就没有意义——你以为在看变化,其实在看两把不同的尺子。一份不可比的记分卡,比没有记分卡更危险:因为它看起来是证据。
收口:一份好的评测报告,读者看完不需要问「然后呢」。 他知道该去改哪一行。
11.5 成本与延迟也是断言
这一节有最直接的支撑:在官方评测工具的断言目录里,成本与延迟就是两种断言类型——延迟按毫秒阈值判、成本按每次响应的美元阈值判,写法与其他断言完全一致(也能放进断言组按权重参与)。
也就是说,大纲那句「质量没掉、账单翻倍」不是一个担忧,是一个可配置项。
而且反模式清单里明确列了「没有成本 / 延迟上限」,后果写得很直白:「每次拉取请求的评测开销膨胀。」 注意这句话说的是评测自己的开销——一个不设上限的评测,会先把评测本身变成成本问题。
更值得写进正文的是它的位置:成本与延迟和格式断言在同一张清单上。 它们是同一套门禁的一部分,不是运维的事后统计。
这个位置上的差别很实际:事后统计只能解释账单,门禁能拦住账单。
三条定界,缺一条就会把这个好机制用歪:
① 阈值必须来自你真实愿意付的钱与愿意等的时长,不是抄一个数字。
延迟这条尤其要按形态分开定:流式与非流式的合理上限不同(流式的「首字」与「整段」是两个数,见 B1 第 5 章),有排队与没排队也不同。同一个模型在两种形态下的合理上限可以差一个数量级——用一个数管两种形态,等于两种都没管住。
② 成本断言要在「有缓存的流水线」里解读。
缓存会让未变的用例不再计费,所以流水线里的成本数字低于生产,两者不能直接比。 如果把流水线的成本当成生产成本,你会得到一个过于乐观的结论——而这个结论会在你放量的时候,以账单的形式到齐。
③ 评审类断言本身也吃成本与延迟。
评审调用会同时抬高这两项,所以「改用了模型评分」这件事会在成本断言上留下痕迹。
这条看起像一个副作用,其实是一个自检收益:它让「评审变多了」这件事变得可见。否则你会在一堆断言里悄悄多加十几个评审调用,而直到账单来了才知道。
收口:成本与延迟写进断言,防住的不是「贵」,是「无人察觉的退化」。 后者才是这类问题的真正形状——它不报警,它只是每个月多花一点。
11.6 一个可观测的验证:故意改坏一版提示词
这一节是本书构造的验证动作,先说清身份。它有一个很像的官方对照实验可以借鉴:流水线动作的文档里给了一个看起来像「改进」的改动——让模型「先解释推理,再给 JSON」;然后观察检查项变红,以及评论里的前后差异。
这个例子的好处是它不像破坏:它不是把提示词写成乱码,而是做了一个听起来合理的改动。 而真实世界里的回归,绝大多数正是这样产生的。
这一节与 10.6 是一对,而且分工很清楚:
10.6 验的是「这套断言能不能红」;11.6 验的是「红了之后,流水线给出的理由够不够精确」。 两者串起来,正好覆盖「门禁是否真的能拦住东西」这个问题的两半。
三个观察点,一个都不能省:
① 是否变红(门禁有没有生效——与「被绕过」的风险直接对应)。 ② 红的理由是否精确指向那条用例(归因能力,接 11.4)。这一条是本节的重点:只红不说,等于把一个 bug 换成了一个谜。 ③ 前后对比里有没有把「变好的」也显示出来(避免只看退化——这同时是对记分卡的一次检验)。
还有一条执行纪律,很多人只会做一半:这条验证要定期做,而且要做两遍。
「把那一行改回来,检查就绿了」是另一半。 为什么两遍都必要?——只验红不验绿,你不知道门禁是不是永久卡死了;只验一次,你不知道它是不是只在某一次碰巧红了。
第二句尤其重要:一个偶尔才红的门禁,和一个从不变红的门禁,在单次实验里长得一模一样。
最后交代它与第 12 章的关系,这也是本节最容易忽略的一层:这条验证用的「改坏一版提示词」,正好就是 12.1 要求的「能 diff、能回滚」的现场。
也就是说:如果提示词不在版本库里,这条验证根本做不了。 你没法「改坏一版」,因为你没有「一版」这个概念——只有一个正在跑的字符串。这是版本库最不抽象的一个好处:它让你能做回归实验。