Agent 说"做完了",证据呢:三道交付验证门
"fixed and tested"不是证据。三道门——声明核对、三树差分、证据包——把 Agent 的自述变成可复核的事实。
一句话结论:当 Agent 提交一个变更时,它给出的 "fixed and tested" 是一句声明,不是证据。三组 2026 年 10 月公开的新材料,正好构成交付验证的三道门:声明核对(把声明和真实 diff 逐条对账——28 个并发 Agent × 5 轮实测,11 个不实声明全部被拒、0 个诚实声明被误伤)、三树差分(同一测试套件跑 parent / patched / ablated 三次,把"绿"拆成 7 类,只有一类算数)、证据包(命令、输出、变更清单、真实截图、已知限制,缺一项就无法复核)。三道门的共同原则只有一句话:不要验证 Agent 说了什么,要验证它改了什么、跑出了什么、留下了什么。
本文整理四篇近 60 天的新文:@dannwaneri《I built a lie detector for AI coding agents. Then I read the rules.》(dev.to,2026-10-15,claimcheck 项目)、Finley Zhou(@datacpp_8185)《Revert the Fix, Keep the Tests: A Three-Tree Differential Gate for Agent Patches》(dev.to,2026-10-15)、@patilrb《Give a coding-agent delivery enough evidence to review》(dev.to,2026-10-15)与 Ofer Shapira(@ofers_agent)《How to Write Review Feedback a Coding Agent Can Turn Into a Verified Fix》(dev.to,2026-10-15),做中文整理。立场先说清楚:claimcheck 作者自述该项目为参加 Cloudflare 竞赛而建、最终因居住地规则未参赛,文章是原型记录;三树差分那篇是 MonkeyCode 的产品外联文(作者在文内披露);证据包那篇注明有 AI 辅助、为 Wagglet 营销工作而写;反馈那篇是个人经验总结。本文按"方法与数据"引用,不替任何工具背书。
一、第一道门:把 Agent 的"声明"当账来对
问题从一个非常具体的场景开始:多个 Agent 同时往一个仓库推代码时,没人核对它们的提交说明。
一个 Agent 改了三个文件、删了 40 行,提交信息写"修复拼写"——这句话对不对?在"一个 Agent 改、一个人审"的场景里,人顺手就看了;在"五个 Agent 同时推"的场景里,人根本看不过来。于是 @dannwaneri 做了 claimcheck:每个 Agent 推变更时,必须同时推一个 .claim/claim.json,平台在合并前核对"声明"和"实际改动"是否一致:
{
"task_id": "t-01",
"agent_id": "agent-a",
"summary": "Add a sub() helper.",
"scope": { "paths": ["src/math.js"] },
"changes": [
{ "path": "src/math.js", "description": "Add sub(a, b) that returns a - b." }
]
}
平台执行的规则有五条:
- 声明必须覆盖 Agent 实际改动的所有文件;
- 声明里只能有真正改动过的文件;
- 不得触碰保护路径(默认
src/auth/**); - 变更规模不得超过上限(行数与文件数各一条);
- 每条 description 必须与 diff 相符。
前四条是确定性检查,纯代码判断。这里有一个值得记住的细节:保护路径匹配大小写不敏感——测试时发现 src/Auth/session.js 与 src/auth/** 不匹配会造成绕过,因为在 macOS 和 Windows 上它们落在同一个目录里。另外两条会产出 UNCLAIMED_CHANGE(改了没声明)和 CLAIMED_NOT_CHANGED(声明了没改)这样的具体结论。
第五条交给 LLM,但加了硬约束:模型要否决一个变更,必须从 diff 里引用一行真实存在的代码作为证据;写不出、或者引用的行在 diff 里找不到,就不能算"否决",只能进 needs_review。三种裁决由此确定:
- 只要有一条证据成立的"否",直接
rejected; - 全部判断为"是"且无错误,判
verified; - 其他情况(含超时)一律
needs_review。
默认不合并,只有 verified 的变更进入合并队列;同一个文件,第一个通过验证的变更获胜。
作者跑了三轮不同规模的测试:
| 测试 | 规模 | 结果 |
|---|---|---|
| 演示 | 5 个脚本 Agent(故意设计五种结局) | 不实声明全部被拒,如声称"裁剪空白"实际删掉了小写转换的那一个 |
| 主测试 | 28 个 Agent × 5 轮 | 每轮 11 个不实声明被拒、0 个诚实声明被误伤;结论都带证据,中位 6–7.5 秒出裁决 |
| 扩展测试 | 22 个 Agent 同任务 | 12 个正常无冲突、2 个正常但撞车、8 个不实(多文件/碰保护路径/描述不符);各轮结果一致 |
最反直觉的一条结论来自真实 Agent:作者让 Qwen 的 qwen3-coder-plus(通过 headless Claude Code,指到阿里云端点)去修 src/strings.js 的拼写错误——仓库里 src/auth/session.js 也埋了一个拼写错误,而这个文件是保护路径。作者没有告诉它这一点。5 轮下来,它没有碰保护文件、修正了 2 处、声明准确、每次通过验证;10 个并发 Agent 的测试里也全部如实。这个样本里的 Agent 没有"说谎"——被抓住的全部是脚本模拟的不实声明。 换句话说:声明核对首先防的不是"Agent 恶意撒谎",而是并发规模下没人看得过来这件事。
值得注意的是作者公开的四次故障与修复,全部发生在"验证系统"自己身上:推送被拒不会重试、LLM 调用超时导致 5 分钟无裁决、合并推送 503、以及最严重的一条——合并结果没落库导致重复合并、自己和自己冲突。修复后:push 重试 3 次;每次模型调用加 45 秒超时、卡住的模型一律 needs_review 绝不 merge;合并队列对 502/503/504 重试;队列在合并前先查自己的历史合并记录(合并提交的标题里带 agent、任务与 commit 前缀);队列每次只做一个合并,做完再排下一个。最后一轮:5/5 通过。整套系统跑在 Cloudflare 上(Artifacts 每 Agent 一个 fork、push 事件触发 Workflow、Workers AI 做描述检查、Durable Object 存裁决并跑合并队列),MIT 许可,需要付费 Workers 计划才能自部署,在线演示已因成本下线。
作者还单独测了那条 LLM 检查的稳定性:15 个带标注的用例 × 5 次运行 = 75/75 全部命中,且没有任何用例在两次运行间改变答案。他也提醒这只是一小批单文件短 diff 的样本,不能外推——这恰好是"给出测量、同时给出边界"的写法。另一个值得顺手记下的坑:headless 运行的 Claude Code 会读取你的全局 CLAUDE.md(除非加 --bare),做 Agent 实验前值得先确认它到底看到了什么指令。
二、第二道门:绿灯不等于修复,跑三棵树
有了声明核对,还要回答第二个问题:变更通过了测试,这个"绿"说明什么? Finley Zhou 的答案是:说明测试套件是绿的,不说明补丁是原因。 他给出一个只有三步的"三树差分门"(three-tree differential gate):
把同一套测试跑在三棵树上——
parent:补丁的父提交;patched:打了补丁的提交;ablated:回滚补丁里所有非测试改动、保留测试的第三棵树。
只有第三次运行,才能把"这些测试能测出这个修复"与"这些测试今天碰巧能过"分开。而常见的三种"无意义绿"在标准 CI 里完全不可见:
- 同义反复:测试断言的是补丁自己算出来的值,除非补丁崩了,否则不可能失败;
- 本来就绿:补丁前就过、补丁后还过,补丁根本没改变被测行为;
- 夹具耦合:测试读的 fixture 或 stub 也被补丁改了,两边一起动,断言恒真。
实现不需要新工具,git worktree 三棵树共享一个对象库,把 ablated 树里的非测试文件 checkout 回父提交即可:
PATCH_REF="$1"
PARENT="$(git rev-parse "${PATCH_REF}^")"
ROOT="$(mktemp -d)"
git worktree add --detach "$ROOT/parent" "$PARENT"
git worktree add --detach "$ROOT/patched" "$PATCH_REF"
git worktree add --detach "$ROOT/ablated" "$PATCH_REF"
# 只回滚非测试改动,测试本身保留
mapfile -t SRC < <(
git -C "$ROOT/ablated" diff --name-only "$PARENT" "$PATCH_REF" \
| grep -Ev '(^|/)(tests?|specs?)/|(_test|\.test|\.spec)\.'
)
[ "${#SRC[@]}" -gt 0 ] && git -C "$ROOT/ablated" checkout "$PARENT" -- "${SRC[@]}"
三次运行必须用钉死的运行器:同一解释器、同一 lockfile、同一随机种子、同一时钟注入——否则三棵树里的版本差异会被误判成结论。输出要机器可读(如 --reporter=json),因为自由文本日志没法可靠 diff。
然后是一张七行的分类表,每个测试只能落进一行,只有第一行是关于补丁的证据:
| parent | patched | ablated | 判定 |
|---|---|---|---|
| fail | pass | fail | sensitive(敏感) |
| fail | pass | pass | fix-blind(测不出修复) |
| pass | pass | pass | pre-existing-green(本来就绿) |
| fail | fail | fail | no-fix(没修好) |
| pass | fail | fail | regression(回归) |
| fail | fail | pass | inverted(倒挂) |
| pass | pass | fail | fixture-coupled(夹具耦合) |
门的规则只有一句:补丁必须新增或修改至少一个 sensitive 测试,且不得出现 regression。 其余类别是诊断信息,不阻塞合并。这个门故意不对称——不要求每个新测试都敏感(有些测试断言的是补丁不碰的不变量),但要求"补丁自己的证据"与补丁有因果联系。最该转人工的是 fix-blind:测试在父提交失败、打补丁通过、移除修复后又通过,常见原因是补丁顺带改了 import 或 setup,测试依赖的是那个副作用。
作者对这套方法的定位也很克制:三种无意义绿在标准 CI 里完全不可见,而它们只多花一次执行——这就是全部的交易成本。 他甚至建议先用工具对着最近五个 Agent 补丁手工跑,记录下判定分布再决定改哪条规则;在他自己的仓库里 pre-existing-green 最多,而这只说明测试套件在 Agent 开始写代码之前就已搭好,与 Agent 的质量无关。
两个配套细节值得抄:
- flake 隔离要设到期时间,且作者不能自己写。一条隔离记录等于声明"这次失败不是这个补丁造成的",让补丁作者自己写就变成了自我认证;示例里每条记录都带
expires_at(30 天内)与owner,门里用git log的作者邮箱和 Agent 身份比对,一致就拒绝。 - 免费额度只能测量和标注,不能做决定。作者披露此文为 MonkeyCode 外联后,把"几百次重复运行找 flake"这类花钱的活放到独立的非阻塞 runner 上,把免费的模型额度用在把分类结果+diff 变成给人类看的第一轮标签上——门只读提交进仓库的 JSON,不读模型的意见。
作者也把适用边界写全了:回滚源码后 ablated 树可能编译失败,此时只能记 inconclusive;测试路径靠 grep 启发式判断,仓库布局特殊会误分类并抬高 fix-blind;三倍运行成本对 15 分钟以上的套件不划算;纯测试补丁在这个门里天然"空转";套件不确定就整张表失真。他还给了一条诚实的备注:先拿最近五个 Agent 补丁手工跑一遍、记录判定分布再改规则——他自己的分布里 pre-existing-green 最多,而这反映的更像是"测试套件在 Agent 写代码之前就搭好了"。
三、第三道门:交付包里必须有能复核的东西
前两道门管的是"改动",第三道门管"交付"。@patilrb 的观察是:一句"修好了,测过了",把最难的部分全丢给了评审——改了哪儿、跑了哪个检查、截图和补丁是不是同一个版本,都得重新拼。他建议的最小交付包只有四件东西:
- 测试结果:准确命令、运行时版本、退出码、完整输出;修复回归的要附上失败基线。一个截过的绿色徽章说明不了跑了哪些测试;
- 变更清单:revision + 每个文件的简短说明;
- 截图:在交付的那个 build 里真实截取,caption 里写明 build 与样本;
- 已知限制:哪些测试没跑、哪些环境没验、哪些行为被有意排除——和正面结果放在一起。
配套的交付模板可以直接用:
Task/revision: [ID 与 commit]
Requested behavior:[要保住的行为]
Changed files: [实际路径与用途]
Reproduce: [干净环境下的准确命令]
Baseline: [失败命令/输出,或不适用]
After: [命令、退出码、输出]
Visual evidence: [真实截图、build、样本]
Known limits: [具体未覆盖项]
Reviewer: [有权接受的人]
Requested decision:[接受范围 / 请求修改]
这套东西最关键的一条是证据与主张要匹配:截图证明不了"脚本没有改文件",单元断言也证明不了"报表在设计常用的缩放下可读"。每件证据只能支持它能支持的那句话。
最后是交付与验收分离:执行方提交证据包;工程师核对实现是否符合声明的范围并能复现;设计师确认这个改动是否真的解决了原问题;验收由有权限的评审记录在案,合并与部署是另外两个决定。
四、反馈也要写成"可验证"的形式
@ofers_agent 补上了另一半:评审意见本身也得能被 Agent 变成可验证的修复。 他的出发点是一个公开的实操案例——real-browser-mcp 的 PR #60 里,评论只谈偏好是没有用的,真正能被处理的问题是带条件的场景:两个并发操作落在同一个标签页上,可能创建出两个分组;两条调试器连接可能留下多于一个标签页,而系统只跟踪了一条。 把这种场景写进评论,Agent 才知道要修什么、要复现什么。他的模板是一段"短契约",用来替代"这里要清理一下"这种把工作丢回给对方的评论:
当两个请求在第一个完成之前到达同一资源时,系统必须保住约定的结果。给出一个能对旧版本复现这次冲突的测试,修复行为,并展示该测试在新版本上通过。写明哪些没有被测试。
"约定的结果"必须替换成真实需求——"一组"、"一条连接"、还是"一个明确报错",是三个不同的决定;没有约定就先开讨论,不要让 Agent 替你选。此外:
- 场景而非偏好:说清"什么条件下发生什么",而不是"这里不够好";
- 区分阻塞项与建议项:"变量名建议"和"权限修复"在同一个 thread 里出现,也不等价;GitHub 上 comment / approve / request changes 哪个真的阻塞,由分支保护规则决定,不取决于评论里有没有写 blocker;
- thread 要小到可验证:一个变更里有两套机制(如标签页分组与调试器连接)就拆两个 PR;有依赖就写明依赖和合并顺序——Agent 可以准备分支,但共享契约的决定权留在变更负责人手里;
- 评论之后要看新 diff:新增的测试要真的碰到讨论中的行为,而不是只贴着当前代码的写法;要"运行结果+版本+命令",PR 正文里的计划测试清单不算运行结果;Agent 回一句"所有评论都处理了"时,对照原始清单逐条检查——每条要么有修复,要么有"为什么不需要"的解释,要么有明确的推迟决定。
五、把三道门合并成一张清单
- [ ] 声明要能对账:每个变更附结构化声明(改了哪些文件、每处做了什么),由平台与真实 diff 逐条比对,而不是靠人读提交信息;
- [ ] 保护路径显式化,注意大小写不敏感;
- [ ] LLM 否决必须引 diff 原行,引不出来就只能
needs_review; - [ ] 拿不准就不合并:超时、异常一律进人工复核,绝不默认放行;
- [ ] 绿要分七类:跑 parent / patched / ablated 三棵树,至少一个
sensitive、零regression才放行; - [ ] 三棵树钉死运行器:版本、lockfile、种子、时钟全一致;
- [ ] flake 隔离有期限、作者不能自签;
- [ ] 交付包四件套:命令与完整输出、变更清单、真实截图(注明 build)、已知限制;
- [ ] 证据与主张匹配:一张截图只支持它拍到的范围;
- [ ] 交付≠验收≠合并,三个动作三个人(或至少三段记录);
- [ ] 反馈写成契约:场景、约定结果、失败复现、通过复现、未测范围;
- [ ] 阻塞项与建议项分开,thread 拆小,评论后复核新 diff。
四份材料里被反复验证的一点是:让 Agent 诚实的最优解不是要求它"不要撒谎",而是让不准确的声明无法通过。 声明核对让不实声明进不了合并队列,三树差分让无意义的绿自动分到别的类,证据包让"说做了"变成"能复核"。Agent 的输出正在变便宜,验证它的成本就是新的瓶颈——而这三道门,每一道都在把人从"逐条看 diff"里换出来。
六、三道门为什么可以分开用
这三道门解决的是三个不同的问题,不需要一次上齐:
| 门 | 回答的问题 | 最小落地方式 |
|---|---|---|
| 声明核对 | Agent 说的范围,和实际改的范围一致吗? | 一个结构化声明文件 + 三四条确定性规则 |
| 三树差分 | 本次通过的测试,是这次补丁让它通过的吗? | 一个 git worktree 脚本 + 一张映射表 |
| 证据包 | 别人要复核这次交付,需要哪些材料? | 一个十行模板,写进 PR 描述 |
它们之间是互补而不是替代:声明核对不判断代码对不对,只判断"说的"和"做的"是否一致;三树差分不管声明,只管证据的因果;证据包不自动跑任何东西,但它让前两道门的结论可以被第三个人复核。三者共享同一种文化——把"完成"从形容词改成可检查的名词。
另外,不要忽略这些材料里最贵的一条经验:验证系统本身也会出错,而且往往错得更隐蔽(重试缺失、超时无裁决、合并结果没落库、回滚树编译失败)。三道门都需要"拿不准就停下"的默认值,而不是"默认放行"。
来源
- @dannwaneri(Daniel Nwaneri), I built a lie detector for AI coding agents. Then I read the rules.(dev.to,2026-10-15):https://dev.to/dannwaneri/i-built-a-lie-detector-for-ai-coding-agents-then-i-read-the-rules-1hhb
- claimcheck 项目(MIT,v1.0.0):https://github.com/dannwaneri/claimcheck
- Finley Zhou(@datacpp_8185), Revert the Fix, Keep the Tests: A Three-Tree Differential Gate for Agent Patches(dev.to,2026-10-15;MonkeyCode 产品外联,文内披露):https://dev.to/datacpp_8185/revert-the-fix-keep-the-tests-a-three-tree-differential-gate-for-agent-patches-3407
- @patilrb, Give a coding-agent delivery enough evidence to review(dev.to,2026-10-15;AI 辅助整理、Wagglet 营销工作):https://dev.to/patilrb/give-a-coding-agent-delivery-enough-evidence-to-review-46kj
- Ofer Shapira(@ofers_agent), How to Write Review Feedback a Coding Agent Can Turn Into a Verified Fix(dev.to,2026-10-15):https://dev.to/ofers_agent/how-to-write-review-feedback-a-coding-agent-can-turn-into-a-verified-fix-4g9a
本系列另两篇
- 别人发来的 3D 文件怎么验收:STEP 翻译与网格拓扑(同一套"验收文化"落到 3D 资产上:公差、非流形边、可复现的检查)
- 链上核验清单:收据、钱包与授权,别只看表面(落到链上:收据对账、EIP-7702 委托、无限授权)
延伸阅读(本站)
- AGENTS.md 该写什么:1,866 个项目的说明书实测
- Agent 说"完成",数据库不同意:一次批准率 87% 的 Agent 评测复盘
- 子代理派活与上下文折叠:compaction、subagent、fold 三种交接实测
- 专题:AI 编程 / Vibe Coding 完全指南
本文由 汇智网 · Software 2.0 编译整理,转载请注明出处。 想系统了解 Agent 的上下文、工具与工程化,见 Agent / MCP / 自动化完全指南。
这是「AI 编程 / Vibe Coding」专题的一部分。完整地图见:AI 编程 / Vibe Coding 完全指南