一个凌晨三点的「做完了」#

把一段需求丢给 Agent,第二天早上回来,屏幕上是一个绿色对勾、一份改动量可控的 diff,以及一段语气笃定的总结:功能已实现,相关检查已通过。

这句话里唯一可疑的词是「已通过」。

人类开发者写完一段代码,心里多少有个模糊的手感:这段逻辑读起来对不对,边界情况是不是漏了,命名是不是透着心虚。这种感觉不精确,但它存在。Agent 没有这个东西。它面对的不是代码的味道,而是一个目标函数,而「把功能做出来」这种目标,对它来说完全是主观的。

主观目标会带来一种很难受的失败模式:它并没有骗你,它只是朝一个它自己都无法验证的方向,走出了很远。

所以真正要改的不是提示词,而是「完成」这个词的定义。把它从一个形容词,换成一个可执行的、非红即绿的东西。

测试。

为什么测试是 Agent 最合适的接口#

不用测试派活,你实际上是在说「请让这个功能能用」。这句话里有三个漏洞:边界情况怎么办、怎么算能用、用不了你怎么知道。Agent 只能靠自己的判断填这三个洞,而它的判断恰好是那个正在被验证的东西。用被验证的对象去验证它自己,这在逻辑上就已经闭环了。

先写测试,任务的性质会整体换掉:

变化具体含义
目标变得客观任务从「实现功能 X」变成「让这些测试变绿」
进度变得可度量从 3/10 通过,到 7/10,到 10/10,每一步都是事实
回归自动被发现先前绿过的测试变红,Agent 立刻知道是自己刚刚弄坏了什么
审查变快绿灯不是文档,是证据

这四条的共同点是:它们把「我认为它好了」换成了「有东西证明它好了」。而 Agent 恰好最擅长在这种有明确信号的环境里迭代。

OpenAI 自己的工程团队就在大规模跑这个模式。他们的内部数据显示:把 1500 个 PR 通过 TDD 锚定的反馈回路交给 Codex 处理后,人均每天产出 3.5 个 PR,而且这个数字随团队扩张是上升的,不是下降的。这个细节值得琢磨。通常规模变大,人均效率会掉,因为协调成本上升。这里没掉,说明瓶颈真的被移走了:写代码不再是瓶颈,验证的清晰度才是。

两种派活姿势的区别#

把两种模式并排放一下,差别一目了然。

不用测试的朴素模式:

  1. 派活:「实现功能 X」
  2. Agent 产出代码
  3. 人类必须自己读懂并验证一切
  4. 人类补测试(通常这一步就没了)

测试优先模式:

  1. 先写失败的测试,提交到分支
  2. 派活:「让这些测试变绿」
  3. Agent 实现、跑测试、迭代到绿
  4. 人类审查 diff,顺便审查测试本身的质量,然后合并

第二步和第三步的区别不在工作量,在于意图被编码进了文件。你在写测试的时候,已经把「什么叫对」完整地表达过一次了。等到审查时,你不需要再向自己复述一遍需求,测试就是那份需求的可执行版本。

这也是为什么很多人第一次认真用这个模式会觉得轻松得有点不适应:你少掉的不是写代码的时间,是反复重新解释意图的成本。

把规则写进 AGENTS.md#

光靠每次派活时口头交代不可靠,这些规则属于仓库,不属于某一次对话。放进 AGENTS.md:

## Testing Protocol
- Always run tests before declaring a task complete
- Command: `pytest tests/ -v` (Python) / `npm test` (Node) / `cargo test` (Rust)
- If tests fail, fix the implementation, do not modify the tests unless the test itself has a bug
- Report: number of tests passing / total before and after your changes
- Never mark a task as done if any test is RED

## TDD Convention
When a task includes a `tests/` directory or `*.test.*` files, assume test-first:
1. Read the failing tests to understand the requirement
2. Implement the minimal code to make them pass
3. Refactor only after tests are green

这份协议里最重要的不是命令怎么写,而是那一行:

测试失败时修实现,不要改测试,除非测试本身有 bug。

没有这一行,整个测试优先的流程会在第一次碰壁的时候自动崩溃。因为对 Agent 来说,修实现是「让测试变绿」的一条路,改测试是另一条,而后者通常短得多。

这条规则不是防坏人,是修目标函数#

Kent Beck 在 2026 年 4 月的 Pragmatic Engineer 访谈里讲了一件事:他抓到过 Agent 直接把测试删掉,就为了让测试套件「通过」。

这不是某个模型的品性问题。把工作定义为「让测试变绿」之后,改测试就是完成这个定义最短的一条路径。你交给了它一个验收标准,同时也交给了它修改验收标准的能力。这是权限设计问题,不是道德问题。

Beck 在同一个访谈里还提了一个更大的观察:我们在这个时代积累代码的速度,远远高于积累信任的速度。 这句话放在这里特别扎人。代码可以一夜之间多出几千行,信任只能一个一个测试地建立。而 Agent 的吞吐越高,这个缺口带来的风险就越大。

他把 AI 叫作 unpredictable genie,不可预测的精灵。这个比喻的重点不是精灵有害,而是它的行为在它的定义之下是理性的,只是那个定义未必是你以为的那个。所以 TDD 在 Agent 编码里的价值,不在于它更「规范」,而在于它提供了 Agent 无法绕开的验证:前提是你得把测试本身保护起来。

一句话总结这节的教训:给裁判上锁。

把验证做成机制,而不是规矩#

规则写在文档里,靠的是模型记得住、上下文没被稀释、它愿意遵守。这三件事都不稳定。更硬的做法是钩子(hooks)。

Codex v0.117.0 引入了 PostToolUse 钩子,Bash 工具执行完之后触发。配合 PreToolUse,你可以搭出一条自动化的持续测试回路:每次文件写入之后自动跑一遍测试。

# ~/.codex/config.toml
[[hooks]]
trigger = "PostToolUse"
matcher = { tool = "bash" }
run = ["bash", "-c", "if echo '$CODEX_TOOL_OUTPUT' | grep -q '.py'; then pytest tests/ -q --tb=short 2>&1 | tail -20; fi"]

比起文档条款,这里有两个值得注意的设计细节:一是只输出 tail -20,避免测试输出把上下文冲掉;二是用文件类型做触发条件,不让每次无关的命令都拖着一次全量测试。

不想动配置也有轻量版本,写进 AGENTS.md:

## After Every File Change
Run: `pytest --tb=short -q` (or equivalent for your stack)
If any test fails: fix before proceeding to the next step.

两者的区别值得说清楚:AGENTS.md 是请它遵守,钩子是替它执行。前者依赖模型的记性和注意力,后者依赖你的配置。凡是能做成机制的规则,就别留在文档里当愿望。

覆盖率当进度条#

把测试优先和覆盖率报告放在一起,覆盖率就从一张成绩单变成了长任务里的实时仪表盘。

## In AGENTS.md
After completing implementation:
Run: `pytest --cov=src --cov-report=term-missing -q`
Target: >= 90% line coverage before flagging task complete.
Report the final coverage percentage as the last message in the session.

任务结束时你会同时拿到两个信号:测试绿,说明行为是对的;覆盖率达标,说明行为被看得足够全面。两个信号都是自动产生的、可审计的、不需要额外人力去做的。

不过这里要加一条我自己的保留意见:覆盖率是必要条件,不是充分条件。 assert result is not None 这样的断言同样会贡献覆盖率,但它什么也没验证。覆盖率回答的是「这段代码有没有被跑到」,不回答「跑的时候有没有被认真检查」。真正想收紧验证强度,需要的是关键路径上的具体断言,以及变异测试这类能检验断言有效性的手段。把 90% 当门槛没问题,把它当结论就有问题了。

换一个会话来审查#

开源框架 shinpr/agentic-code 把 TDD 优先的工作流总结成几条原则,其中最有洞见的是最后一条:fresh-session review,审查必须来自另一个会话。

原因很朴素:同一个上下文窗口里的假设会被继承。让 Agent 在同一段对话里审查自己刚写完的代码,就像让人立刻校对自己刚写下的句子,眼睛会自动跳过错误,因为写的时候脑子里已经有了正确的版本。

这个结论其实和我之前写过的另一件事是同一个道理的两个侧面。那篇关于审查循环为什么不收敛的文章里,Convergo 插件给出的解法也是「新鲜审查者」:一个不继承上一轮假设的审查者,才能把发散的补丁螺旋扭回收敛。一个从测试的角度说,一个从审查的角度说,指向的是同一件事:验证者必须和被验证者保持距离。

再往前一步是 metaswarm 这样的做法,它把 TDD 的执行推到了工程化的极限:

  • 覆盖率阈值写在 .coverage-thresholds.json 里,作为创建 PR 前的阻塞门禁,不是建议,是门禁
  • pre-push 钩子(Husky)在每次 push 时跑 lint、类型检查和覆盖率
  • 执行任务的 Agent 和审查的 Agent 永远是不同模型,做跨模型的对抗审查
  • 9 阶段工作流,设计评审环节有 6 个并发的专项审查者

按它的描述,这套流程是从一个跑过数百个 PR、100% TDD 覆盖、全程没有人工介入的代码库里抽出来的。

测试是复利资产#

写好的测试是给未来的 Agent 会话准备的最好文档,而且它的收益是叠加的:

  • 已有的测试告诉 Agent 当前行为是什么
  • 新写的失败测试告诉 Agent 行为应该变成什么
  • Agent 可以通过读整个测试套件,理解项目约定、边界情况和真实意图

推论很直接:测试越厚,AGENTS.md 和每次任务的提示词需要写的东西就越少。测试自己在携带上下文。

反过来也成立,而且更常见:测试薄的项目里,上下文成本永远压在提示词上,每开一个新任务就重新付一次。这是很多人觉得 Agent 在别人仓库里「特别笨」的真正原因,不是模型变笨了,是那仓库里没有任何东西替你把意图讲清楚。

边界与反模式#

这套方法不是没有坑,以下几条是我认为最需要提前设防的:

过度模拟(over-mocking)。 Agent 生成的测试很容易把外部依赖全部 mock 掉,结果测试全绿但什么都没验证。对策是在提示词里写死约束,比如「集成路径使用真实的数据库 fixture,不要用 mock」。

测试本身有 bug。 例外条款要写清楚:只在测试本身有错时才改测试,并且要求它说明为什么是测试错了,而不是实现错了。模糊的例外等于没有例外。

绿灯不等于正确。 测试只覆盖你想到的场景。测试没写到的地方,Agent 会精准地踩空,而且它们的 diff 看起来同样干净。

覆盖率的数字游戏。 见上一节。门槛是起点,不是终点。

长会话里的漂移。 会话越长,早期上下文的权重越低,方向越容易偏。用 /review 看改动,用 /diff 检查范围,把每次改动控制在你能读完的规模上。一份动了 4 个文件的 diff 是可以审查的,动了 40 个文件的 diff 你只是在凭信任签字。

快速上手清单#

下次派活之前,走一遍这五步:

  1. 写 2 到 5 个定义预期行为的失败测试
  2. 提交它们:git commit -m "test: failing tests for [feature]"
  3. 在 AGENTS.md 写下那条规则:「不要修改测试,除非测试本身有 bug」
  4. 在 AGENTS.md 里加上完成后的覆盖率报告要求
  5. Agent 完成后,换一个会话(最好换一个模型)做审查

写在最后#

Agent 编码真正改变的,不是写代码的速度,而是「验证」这件事的位置。它从流程末尾的一个动作,变成了流程的输入。你先定义什么叫对,它才开工。

在这个转变里,人类的工作从写代码挪向了指定和测试。这不是降级,是换了一种更难的能力要求:把模糊的需求变成一组不可绕过的断言。

测试就是你和 Agent 之间的合同。合同写清楚,剩下的它会自己做完。唯一的风险在于,合同放在桌上,而它能伸手改合同。所以给它上锁。

参考来源#