第 15 章 验证循环:Agent 怎么检查自己的作业
格式对,不等于内容对
第 14 章解决了"输出符合 schema"。但有个更扎心的问题:符合 schema 只保证格式对,不保证内容对。 一个 Agent 写出来的文案可能结构完整、字段齐全,但读起来空洞无物;一个 Agent 写的 UI 代码能编译、能过单测,但渲染出来是一坨残缺的界面;一个 Agent 改的代码能跑通,但根本不知道哪个模块编译不过。
这一章讲三个模式,共同主题是验证循环:让 Agent 在交付之前,真正"检查自己的作业"。
- Deterministic Grader in the Loop(确定性评分器):把"让模型打分"换成"确定性评分器",给 Agent 可执行的反馈。
- Rendered UI Finish Gate(渲染 UI 完成门):UI 改动合并前,对照设计契约检查渲染结果。
- Subagent Compilation Checker(子 Agent 编译检查):派专门的编译子 Agent 去验证代码模块。
模式一:确定性评分器(Deterministic Grader in the Loop)
问题
会产出文本的 Agent,交付前经常被要求"给自己的作品打分":论证是否严密?文案是否通用?读起来像不像机器写的?默认做法是再问一个模型(LLM-as-judge)或用一个现成分类器。
但这两者在同一个地方坑 Agent:裁决不可行动,也不可复现。
- 分类器返回"87% 可能是 AI 生成"。这个数字里没有下一步动作。Agent 唯一的出路是盲目重写、再掷一次骰子。
- LLM 裁判对同样的输入,每次跑分数都不一样,还会随模型版本漂移。它没法做流水线的门禁,也没法在测试里断言。
- 两者都在用"风格"证据下"作者归属"的结论,两个方向都可能是错的。
循环停滞,因为反馈没有梯度。
方案
在循环里放一个确定性评分器,而不是裁判。 评分器统计可观察、可枚举的特征,返回每个子分数背后的原始计数,而不只是总分。
四个核心属性:
- 确定性:相同输入,相同输出,永远。无采样、无模型调用。
- 可分解:总分由命名的子分数构成,每个都能追溯到一次计数。
- 本地且免费:无 API 调用,每次迭代都能跑,没有成本或延迟压力。
- 诚实说明测的是什么:它报告的是风格测量,绝不是作者归属裁决。
Agent 循环变成有真实梯度的爬山:
draft = generate()
loop until score >= threshold or budget exhausted:
report = score(draft) // 确定性、可分解
worst = argmax(report.counts) // 例: {"hype": 5, "em_dash": 9, "specifics": 0}
draft = revise(draft, target=worst)"hype": 5 告诉 Agent 删掉 5 个具体的词。87% 可能是 AI 什么也没告诉它。这个差别就是整个模式。
这其实就是编码 Agent 从 linter、类型检查器、失败的测试里免费拿到的东西:确定性、可分解、可行动。这个模式只是注意到,非代码输出也值得同样的待遇,而伸手去叫一个模型来打分通常是降级。
证据
- 证据等级:低(
low)。 - 一个有价值的发现:用一个这样的评分器扫了 239 个真实产品落地页,得到稳定分布(中位数 79/100),并暴露了一个反直觉的主导特征:82%(195/239)的 hero 区一个具体数字都没有。而大家以为很常见的"空洞热词"只在 7% 上触发,em-dash 密度也类似。可分解的评分器能发现这个;一个不透明概率做不到。
- 同一批语料还说明为什么"风格,不是作者归属"这个框架是承重的:
stripe.com得分 61,但明显是专业人类写的。低分的意思是"读起来像通用模板",绝不是"这是生成的"。 - 未验证:闭环是否真的能提升人类评判的输出质量,还是主要教会 Agent 去满足指标(见下面的 Goodhart 风险)。还没做过对 LLM-judge 循环的受控对比。
怎么用
- 枚举特征:把"好/坏"这个属性,在你所能容忍的最窄领域里枚举出来。这是真正的功夫,而且依赖领域。
- 写纯函数:从产物到
{score, sub_scores, counts},无网络、无模型。 - 暴露计数,不只暴露总分。计数是可行动的部分,总分只用于门禁。
- 把它暴露成工具:MCP server、CLI、库函数,让 Agent 能在任务中途调用,闭环不需要人。
- 在它上面做门禁:因为它是确定性的,阈值可以放进测试或 CI 检查。
- 对照真实语料校准并发布分布,分数才有参照系。一个没有基线的裸 0-100 不可解释。
适合:风格和可读性检查、格式和 schema 一致性、术语和含糊密度、必需元素是否存在、结构重复。 不适合:事实准确性、新颖性、受众契合度,这些留给人或模型。
取舍
- 好处:可复现,能在测试里断言、能当 CI 门禁、跨运行跨时间可比;可行动,指明具体要改什么,给修订循环一个梯度;便宜且离线,无 token 成本和延迟,每次迭代都能跑;可审计,任何人都能读规则、明确反对它。
- 代价:只测量你想到要数的东西,枚举特征集之外的模式它看不见;Goodhart 风险真实且直接:Agent 优化一个它能读的指标,会学会去满足指标。要限制迭代次数,把阈值放在远低于上限的地方;需要校准语料才可解释,这是前期功夫;对意义、真相、受众契合度一概不管,满分也可能是垃圾散文;规则要随惯例变化维护。
模式二:渲染 UI 完成门(Rendered UI Finish Gate)
问题
编码 Agent 能产出能编译、能过单测的前端代码,但交付的界面依然通用、残缺、误导人。常见失败:缺 loading 和 error 状态、看起来能点但实际没反应的控件、token 漂移、不可达的焦点行为、从没在目标宽度渲染过的响应式布局。纯源码审查检测不出这些。
方案
在实现之后、合并之前,加一个有界的完成门。门把渲染出来的界面和一个小型设计契约做对比,同时检查行为和视觉证据:
- 契约检查:确认预期的层级、内容模型、token、响应式规则、平台特定约束。
- 状态检查:枚举并演练该功能能到达的 loading、空、error、disabled、focus、success、权限状态。
- 交互检查:验证可见控件有真实的事件处理器、键盘路径、标签、焦点处理,导航或提交行为正确。
- 渲染检查:在代表性的视口尺寸检查页面,和契约对比,而不是和一个通用模板对比。
- 完成裁决:报告带证据、严重级别的具体发现,以及解决每个阻塞问题所需的最小改动。
contract = read_design_contract(change)
states = enumerate_required_states(change)
implementation = inspect_source(change)
rendered = render_at_viewports(change, contract.viewports)
findings = []
findings += check_tokens_and_hierarchy(implementation, contract)
findings += check_state_coverage(implementation, states)
findings += check_interactions(rendered, contract)
findings += check_responsive_rendered_output(rendered, contract)
return verdict(findings, evidence=findings.evidence)这个门刻意和产品特定的视觉品味分开。它先拦截缺失的行为和站不住脚的声明,然后把视觉质量问题记录下来给人审。
证据
- 证据等级:中(
medium)。 - 有价值的发现:源码检查能抓住惰性控件和缺失分支;渲染检查能暴露源码检查抓不到的布局和状态失败;书面契约让评审标准可重复。
- 未验证:最佳视口集合、严重级别阈值、人工评审量因产品和平台而异。
怎么用
- 改动用户面向的 Web 或原生 UI 的 PR,要求过这道门。
- 当层级、状态、响应式行为不清楚时,让实现 Agent 在写代码前先写或更新设计契约。
- 第一遍保持确定性:检查改动的文件、枚举可达状态、渲染代表性路径、把发现挂到改动上。
- 缺失状态、惰性控件、坏掉的键盘路径、token 违规,这些当合并阻塞项。主观打磨当评审项,除非契约把它写明了。
- 项目需要审计轨迹时,把渲染证据和裁决存到 PR 上。
已知实现: UIZZE 的 anti-ui-slop Skill,把契约、状态、交互、渲染输出检查应用到编码 Agent 的 UI 改动上。
取舍
- 好处:找到编译期检查和纯源码评审发现不了的问题;让 UI 评审可重复;让必需状态和交互语义保持可见;产出可行动的证据,而不是一个模糊的质量分。
- 代价:加渲染和评审时间;需要代表性测试数据和视口选择;视觉对比仍可能带上评审者的偏见;不是每个问题都能压成确定性规则。
模式三:子 Agent 编译检查(Subagent Compilation Checker)
问题
大型编码任务往往涉及多个独立组件(微服务、库)。让主 Agent 在上下文里处理每个组件的编译和错误检查:
- 上下文爆炸:把完整构建日志或字节码塞进提示词不现实。
- 推理变慢:发完整构建命令、解析啰嗦的输出,烧大量 token。
另外,当 Agent 唯一的"编译加运行"步骤失败时,没有更细的粒度,很难定位是哪个子模块出的错。
方案
派专门的"编译子 Agent"去独立构建、验证每个代码子模块,只回报:
- 错误摘要:文件路径、行号、错误消息。
- 二进制产物(如需):引用 ID(编译后对象文件的路径),而不是原始二进制。
工作流:
主 Agent 请求: "编译模块 auth-service"
→ 派发 CompileSubagent(auth-service)
→ 子 Agent 跑 mvn clean install 或 go build ./auth-service
→ 返回结构化错误列表或编译产物位置
→ 主 Agent 更新上下文:
[{file: "auth_controller.go", line: 85, error: "undefined: UserModel"}]证据
- 证据等级:中(
medium)。 - 有价值发现:Self-Refine(Shinn 等人,2023)通过迭代反馈带来 15%-45% 质量提升;CaMeL(Debenedetti 等人,2025)验证了"执行前静态验证"作为安全模式;行业工具(Cursor、Aider、OpenHands)实现了类似的编译/测试检查工作流。
- 未验证:对主 Agent 上下文效率的量化影响;最优子 Agent 并行策略。
怎么用
- 子 Agent 定义:每个子 Agent 是带相应运行时(JVM 跑 Java、Node 跑 JavaScript)的轻量容器或进程。
- 集成进 RL 循环:把每次子 Agent 调用当 RL 环境里的一次工具调用。
- 错误驱动奖励:错误列表非空就给负奖励,与错误数成正比(如
reward = -len(error_list)),推动 Agent 尽快修编译错误。
取舍
- 好处:模块隔离,主 Agent 从不需要把完整构建日志载入上下文;并行构建,多个子 Agent 同时编译不同模块,加快端到端流程。
- 代价:基础设施开销,要能拉起和销毁多个构建环境;子 Agent 同步,如果一个模块依赖另一个的构建产物,协调策略要保证正确的构建顺序。
三个模式怎么选
| 场景 | 推荐模式 |
|---|---|
| 文本产出,想让 Agent 交付前自检 | 确定性评分器 |
| UI 改动,编译过但界面可能残缺 | 渲染 UI 完成门 |
| 大编码任务,编译错误难定位 | 子 Agent 编译检查 |
三个模式是验证循环的三条路径:确定性评分器验证"文本质量"(风格、可读性),渲染完成门验证"界面行为"(状态、交互、渲染),编译检查验证"代码正确性"(模块、构建)。它们都有一个共同点:用确定性的检查代替模型的主观判断,让 Agent 拿到的是"具体改哪里"而不是"我觉得不行"。
实践清单
- 文本产出:枚举风格特征,写纯函数评分器,暴露原始计数(不只总分)
- 评分器做成工具(MCP/CLI),让 Agent 任务中途能调用,闭环免人工
- 阈值放进测试/CI 门禁,并对照真实语料校准、发布分布
- UI 改动:合并前过完成门,契约 / 状态 / 交互 / 渲染四查
- 缺失状态、惰性控件、坏键盘路径、token 违规当合并阻塞项
- 大编码任务:派编译子 Agent 独立验证模块,只回报错误摘要
- 子 Agent 返回结构化错误列表(文件 / 行号 / 消息),别塞整个构建日志
- 多模块可并行编译,注意模块间的构建顺序依赖
- 检查:Agent 拿到的是"具体改哪里",还是"我觉得不行"?
本章小结
- 确定性评分器:用纯函数数特征,给 Agent 可行动、可复现的梯度,风格检查不再靠模型打分了。
- 渲染 UI 完成门:合并前对照设计契约,检查状态、交互、渲染,拦住"编译过但界面残"。
- 子 Agent 编译检查:专门的编译子 Agent 验证模块,主 Agent 只收错误摘要,上下文不爆炸。
- 验证循环的共同原则:确定性代替主观,可行动代替模糊。
下一章讲评测基建:单次验证之外,怎么系统性地评测 Agent?Mock 工具评测、事故转评测、编排器基准、动作缓存回放。