第 3 课:确定性验证器:能跑出 pass/fail 的检查才算数
学习目标:
- 按「最快、最可靠、最可扩展」给候选验证器排序,为一个具体产出挑出合适的那个
- 写出一个能返回 pass/fail 的确定性验证脚本,并让它的输出能被 Agent 在对话里读懂、拿去迭代
- 认出「验证器过严把对的判错」这类假阴性,用归一化修好它,同时知道确定性检查在哪里到顶
前置要求:读完第 1、2 课(没有可跑的检查时「看起来做完了」是唯一信号;验终态优先、过程兜底,成功标准要可测量) | 上一课 第 2 课 << | 下一课 第 4 课 >>
从「验什么」到「用什么验」
第 2 课结束时,你手上应该已经有一份写死的成功标准了。拿本课要用的例子说:Agent 读一批销售 CSV,汇总成一个 report.json,里面有标题、分渠道的条目、以及一个总数。第 2 课教你把标准写成终态的样子——文件存在、字段齐全、总数等于各条目之和——而不是「先读文件再算总和再写文件」这种逐步核对。
标准有了,下一个问题很实在:拿什么去检查这份标准?
你可以人眼看一遍;可以让另一个模型读一读、给个评价;也可以写十几行 Node 脚本,把 JSON 读进来算一遍总和,对不上就退出码非零。三条路都能得到结论,代价和可靠程度却差得很远。
官方文档给了一条排序原则:挑最快、最可靠、最可扩展的那种判分方式1。按这条尺子量下来,三类方法的位置很清楚:
- 代码判分——最快、最可靠,极易扩展;短板是对那些需要弹性、不适合用规则硬卡的复杂判断力不从心1。
- LLM 判分——快且灵活,可扩展,能处理复杂判断,但要先验证裁判本身靠不靠谱再放量用1。(第 4 课讲。)
- 人工判分——最灵活、质量最高,但慢又贵,能免则免1。
这里的「可靠」指的是同一个输入进去,每次给出同一个结论。代码判分排第一不是因为它聪明,恰恰因为它笨——它不会今天心情好放你过关,明天换个措辞就把你毙了。在计算机里,确定性系统指的是相同输入每次产出相同输出,而 Agent 这样的非确定性系统即使起点一样也可能给出不同回答2。本课说的「确定性验证器」就是这种笨得可预测的检查:用一个确定的东西去卡一个不确定的东西。
设计评测题目时还有条相关建议:尽量把问题设计成能自动判分的形式,比如选择题、字符串匹配、代码判分1。有机会自动判的地方,别浪费。
验证器菜单:任何能返回一个信号的东西
「验证器」听起来像个专门的框架,其实门槛低得多。官方文档的定义直白得几乎有点粗糙:检查可以是任何能返回一个信号、而且这个信号能被模型在对话里读到的东西——一个测试套件、一个构建的退出码、一个 linter、一个把输出和基准文件做 diff 的脚本,或者一张和设计稿比对的浏览器截图3。
拆开看看这份清单:跑 npm test 全绿就是 pass,适合代码类产出——代码方案本来就是可以用自动化测试来验证的4;构建退出码最省事,工具链已经替你写好了断言;linter 单独用不够,但作为「不该犯的错」的地板很好使;diff 脚本把这次的输出和一份事先备好的基准文件(fixture,就是一份已知正确的样本)比一比,适合输出稳定、格式固定的场景;截图对比则用在前端改样式的时候。
顺带说一句工程常识:编译器和静态类型检查器(比如 tsc --noEmit)在实际项目里也常被当成这类检查用,因为它们同样吐退出码、同样给出可读的错误位置。这条是我的补充,不在上面那份清单里,别当成官方推荐。
这些手段之间不是互斥的。验证器其实是一条光谱——一头是「和基准做精确字符串比对」这种最简单的形式,另一头是「请 Claude 来判」这种最高级的形式2。本课讲左半边,第 4 课去右半边。
最小形态:output == golden_answer
光谱最左端长这样1:
就这样,一个等号。这叫精确匹配。它衡量的是模型输出跟一个预先定好的正确答案对不对得上,而且通常要先做空白与大小写的归一化;这是个简单、没有歧义的指标,特别适合那些答案清晰、可以归类的任务,比如情感分析的三分类(正面、负面、中性)1。
「归一化」这个词的意思很朴素:比较之前,先把那些不承载语义的差异抹掉。写成代码就是:
第二个调用的结果值得盯一会儿。trim() 和 toLowerCase() 救得了换行和大小写,救不了那个句号。归一化该做到哪一步,取决于你的任务里哪些差异是无关的——这个判断没法外包给库,只能你自己下。本课后面的陷阱小节,讲的就是这个判断做浅了会怎么样。
让验证器的输出能被读懂
上面那句定义里有半句常被忽略:信号得能被模型在对话里读到3。这半句决定了验证脚本该怎么写输出。对比两种失败信息:
第一种只告诉 Agent「你错了」,它拿到之后只能瞎猜;第二种告诉它错在哪一项、期望是什么、实际是什么,下一轮就能直接去改那个数。同一个 pass/fail,信息量差一个数量级。官方文档的另一条建议说的是同一件事:让 Claude 拿出证据,而不是断言成功——测试输出、它跑了什么命令、命令返回了什么,或者一张结果截图;复核证据比你自己重跑一遍验证快,对你没盯着的那些会话同样有效3。你的验证脚本就是那份证据的生产者,它写得含糊,证据就含糊。
实操两条:退出码用 0 表示通过、非零表示失败(CI 和 shell 里的 && 都能直接用);stdout 里每条失败单独一行,写清「哪一项、期望什么、实际什么」。
有了 pass/fail,Agent 的行为会变
Agent 在执行过程中,每一步都需要从环境里拿到真实反馈(ground truth,比如工具调用的结果或者代码执行的结果)来评估自己的进展4。没有验证器的时候,它唯一能拿到的「进展信号」就是自己写下的那段话——它觉得写完了,那就是写完了。有了验证器,环境里多了一个不受它主观意愿影响的事实来源。
于是行为链条变了:给 Claude 一个能产出通过或失败的东西,这个循环就能自己转完一圈——干活、跑检查、读结果,一直迭代到检查通过为止3。换个说法:Agent 能把测试结果当反馈来迭代自己的方案4。
在你本系列第 7 门课手写的那种 harness 循环里,落地方式就是把检查做成一个工具:
验证器的输出通过 tool_result 回到对话里,模型读到 total 对不上:声明 48,各 count 之和 50,下一轮就去改。你没有参与任何一步。
两个顺手的延伸。一、检查不必只放在终点。 第 2 课讲过,复杂流程可以拆成若干离散的验证检查点,在那些「本该发生某个状态变化」的位置上验一次,而不是核对每一个中间步骤5。这些验证检查点正好是确定性检查的落脚处——比如「CSV 全部读完之后,行数应该等于各文件行数之和」。同一篇复盘也提到,实践中会把 Agent 的灵活性和确定性的保障(例如重试逻辑、定期检查点——注意这个「检查点」指的是本系列第 9 门课那种存现场的恢复检查点)组合起来用5。
二、有一类验证可以前移到 API 层。 工具定义里加上 strict: true,可以确保 Claude 的工具调用严格符合你的 schema6。schema 就是你对参数结构的声明。
加上这一行,「字段名拼错」「count 传成字符串」这类结构问题就从「你要写代码去检查的东西」变成了平台层面的保证。工具本来就是确定性系统和非确定性 Agent 之间的一份契约2,strict 是把这份契约写进合同的方式。
但它管得住结构,管不住语义。total 是不是真等于各 count 之和,schema 一个字都说不上来——那部分仍然得你自己验。
陷阱:验证器过严,会把对的判错
这是确定性检查最常翻的跟头,而且翻的时候你往往察觉不到。
官方给工具评测写的那条建议一针见血:避免过于严格的验证器,它们会因为格式、标点、合法的另一种措辞这类无关差异,把正确的回答判成失败2。
「无关差异」这个词是钥匙。同一个正确答案,可能带着一个尾随空格、可能把 positive 写成 Positive、可能在两个词之间多打了一个空格。这些差异对任务来说毫无意义,对一个逐字节比对的验证器来说却是灭顶之灾。判错的方向也很坑:它不会放过错的,它会冤枉对的——这叫假阴性。
来看一个具体的翻车与修复过程。基准标题是 2026 年 Q1 渠道汇总,Agent 产出的 report.json 里数据全对,只是标题前后多了空白、两个词之间多打了两个空格。天真的严格版验证器是这么写的:
跑一下,真实输出:
一份内容完全正确的报告,被一个尾随换行毙了。如果这个结果回传给 Agent,它接下来会去折腾标题的空白——一个跟任务毫无关系的方向。
修复只要一个函数:
replace(/\s+/g, " ") 把连续空白折成一个,trim() 去掉首尾,toLowerCase() 统一大小写。改完之后同一份文件就通过了——本课练习 Level 2 的完整脚本和真实运行结果在后面。
反过来也要提醒一句:归一化不是越狠越好。如果你把标点也一并抹掉,那么 total: 48 和 total: 4.8 这种真正的错误也可能被你自己抹平。判断标准始终是那一条——这个差异承载语义吗? 承载就该严,不承载就该归一化。
确定性检查到顶的地方
确定性验证器的适用范围有边界,而且边界很清楚。
第一条边界是自由文本。 研究类的产出很难用程序来评判,因为它们是自由格式的文字,而且很少有唯一正确答案5。你没法给一段摘要写 output == golden_answer——同样一份材料,两个写得都好的摘要可以用完全不同的措辞。这类判断交给谁,是第 4 课的题目。
第二条边界是「符合更大的系统要求」。 自动化测试能验证功能是否正常,但要确保方案跟更大的系统要求对得上,人工审查仍然不可少4。一个补丁可以全绿通过测试,同时是个会让整个模块难以维护的糟糕设计。多 Agent 的工程复盘里也有同样的意思:即使自动化评测已经很成熟,人工测试仍然必需5。
确定性检查负责的是「不该错的地方没错」这个地板;地板之上的部分,得换工具。
分寸:不是每件事都值得配验证器
反方向也有个常见毛病:给一次性脚本配全套验证器。
Agent 写一段只跑一次、跑完就删的数据迁移脚本,你却给它搭上结构校验、基准比对、回归样例——写验证器的时间比亲眼看一遍输出还长。
判断的尺子还是那句老话:只有当复杂度能带来可测量的改进时,才值得加4。落到验证器上,挑一个问题问自己就够了:
- 这个检查会被跑几次?只跑一次的东西,你亲自看一眼可能更快。
- 没有它,错误多久才会被发现?「立刻,我就在旁边看着」和「等下游报错,两天后」,结论完全不同。
- 这个任务你打算改几版提示词?只要超过一版,你就需要一把能比较前后的稳定尺子,不然「这次是不是变好了」永远只能靠感觉。
至于什么时候必须写,官方文档给的原则很硬——始终提供验证手段(测试、脚本、截图);验不了的东西,别发3。
💻 练习
小结
- 挑判分方式的排序原则是「最快、最可靠、最可扩展」;代码判分在这三项上都排第一,代价是对需要弹性的复杂判断力不从心1。
- 检查可以是任何能返回一个信号、且信号能被模型在对话里读到的东西:测试套件、构建退出码、linter、把输出和基准文件 diff 的脚本、和设计稿比对的截图3。
- 验证器是一条光谱,最左端是和基准做精确字符串比对,最右端是请 Claude 来判2;最小形态就是
output == golden_answer,通常先归一化空白与大小写,适合答案清晰、可归类的任务1。
- 有了 pass/fail,循环能自己转完一圈:干活、跑检查、读结果、迭代到通过3;这背后是 Agent 每一步都需要从环境拿到真实反馈来评估进展4,也是它能把测试结果当反馈来迭代的原因4。
- 复杂流程可以拆成离散的验证检查点,在该发生状态变化的位置验一次,而不是核对每个中间步骤5;一部分结构校验还能前移到 API 层,用
strict: true 让工具调用严格符合 schema6。
- 最大的陷阱是验证器过严:它会因为格式、标点、合法的另一种措辞这类无关差异,把正确回答判成失败2。修法是先归一化再比,把严格留给真正承载语义的部分。
- 确定性检查有天花板:自由文本很难用程序评判5;自动化测试验证功能,但要确保方案符合更大的系统要求,人工审查仍不可少4,人工测试在自动化评测之外也依然必需5。
- 别给一次性脚本配全套验证器——复杂度只在能带来可测量改进时才值得加4;但反过来,验不了的东西就别发3。
>> 第 4 课:LLM 当裁判:量表、格式与不该让它裁的事