输入关键词开始搜索

为什么 TDD 反而不是 AI 时代的答案

来源:yage.ai


有一个说法这两年越来越流行:AI 时代,你最需要的就是 TDD。AI 生成代码的行为不确定,说不定哪次就跑偏了。你不可能逐行审查它的每一次输出,但你可以写好测试——用确定的、客观的 test pass/fail 信号把错误拦下来。测试不过,它就过不了关。这听起来像一句没有漏洞的工程常识。

但我越来越确信这是一个方向性的错误。不是”测试不重要”——测试当然重要。是 TDD 这套方法论的前提在面对 AI 时不再成立,而你一旦把 AI 放进了 TDD 的循环里,它会以一种你在人类开发者身上几乎不会看到的方式让它失效。

当路标变成了目的地

先从一个很小的例子开始。假设你让 AI 实现一个权限检查函数,你在 TDD 的节奏里先写了这个测试:

def test_admin_access():
    result = check_access(user="admin_user", resource="admin_panel")
    assert result == True

AI 看到这个测试,生成了:

def check_access(user, resource):
    return True

测试通过了。你当然会说”我的测试集不可能这么简陋”——但先别跳过这个例子。它暴露的东西比看起来深得多。

对一个人类开发者来说,看到 test_admin_access() 的那一刻,他脑子里展开的不只是”这个断言怎么让它绿”。他自动补全了一整套没有写在测试里的东西:这个函数应该查数据库里的权限表、应该校验 session 是否过期、应该处理角色继承层级、应该审计记录。测试只是路标,不是目的地。

AI 并非不懂权限检查。它在训练数据里见过海量的正确实现,写一段完整的权限校验对它完全不是能力问题。问题在更根本的地方:人和 AI 对同一个测试,跑的是不同的目标函数。

人的目标函数是”建成一个正确、可维护的系统”。测试只是路上的检查站——它告诉人方向对不对,但人的优化目标远大于”通过这些检查站”。

AI 的目标函数呢?在 TDD 的循环里,它接收到的反馈只有两件事:测试过没过、生成的代码在概率上像不像合理的实现。这就是它的全部优化方向。而”return True”完美地满足了这两个信号:测试绿了,代码在语法上也无可挑剔。

这就是 Goodhart’s Law 在代码生成里的精确复现。当一个度量变成了目标,它就不再是度量了。

为什么人不会这样

一个 junior engineer 写 TDD,会不会也写出 return True?不太可能。这和聪明无关。大多数 junior engineer 的代码生成能力远不如 LLM。区别在于动机结构

一个 junior engineer 走进代码库的时候,他知道这坨代码以后要维护、要被 CR、出了问题要被 on-call 叫醒。这些东西给了他一个隐式的代价函数:写一个省事的绕过当下简单,但长期代价巨大。

AI 完全没有这些东西。它没有维护负担,没有 CR 恐惧,没有被凌晨两点叫醒的可能。它唯一的反馈信号就是你给它的测试、你给它的 prompt、以及训练数据里下一个 token 的概率分布。你可以在 prompt 里写”请像 senior engineer 一样思考”,但你没有办法在 prompt 里制造”如果写出烂代码会有真实后果”这个动机结构。

为什么”多写测试”不是出路

TDD 支持者会说:这只不过说明你测试写得不够多。写够多了,取巧路径自然会被堵死,最后唯一能过全部测试的就是正确实现。

这个论证的直觉是对的——在人类的迭代节奏里,它的确成立。但它的成立依赖于一个前提:AI 的取巧路径和你加测试的速度之间存在一个你追得上的关系。而这个前提不成立。

每加一条测试,你堵死了上一次的取巧路径,但 AI 会在新的约束下重新找最短路径。你堵的是你见过的那一条路径,它看的是你还没见过的一百万条。约束增长不是线性的,是组合的。

这和测试覆盖率是同一个问题在另一个层面的表现。覆盖率度量的是”测试踩到了哪些行”,但被踩到的函数里面可以空到什么都没做,覆盖率依然是 100%。AI 需要的是向前看的约束——不管它怎么写,结果必须满足某些 invariants。

把测试退到边界上

那么,如果不靠层层嵌套的 unit test 来约束 AI,靠什么?

核心就是把确定性从代码路径上撤回来,放到系统边界上。具体来说:不要测”怎么走的”,测”到没到”以及”路上的护栏碰没碰到”

传统的 unit test 本质上是在定义实现路径。你写一个测试断言 PaymentService.process() 调了 SecurityLogger.log()。你是在说”沿这条路走,在这个点右转,在那个点停”。这对 AI 是枷锁——不是因为它没能力理解这些测试,而是因为在 TDD 的分工里,测试是人写、不允许 AI 改的东西。

替代的做法是只测结果。不测”调没调 logger”,测”如果数据库里有一条 payment record,审计表里必须有对应的加密签名”。这就是 verify state, not behavior。无论 AI 怎么重构架构,换 logger 实现、改调用链、引入中间层——只要审计签名的 invariant 还在,测试就绿。

这套做法不是没有名字:property-based testing、contract testing、E2E invariant check——这些概念存在了几十年。它们在人类时代始终是小众工具,因为写一个好的 invariant 比写五个 example-based unit test 难得多。但 AI 改变了这层经济关系:代码生成是极便宜的,inferred specification 的能力是前所未有的。

在这个模式里,人的工作退到了真正需要人做决策的地方:定义系统边界和业务 invariants。什么情况绝对不能发生?什么约束不管怎么重构都不能被破坏?什么最终结果算合格的交付物?

AI 的工作是在护栏内自由探索实现路径。它怎么拆模块、怎么设计数据结构、怎么处理错误传播——这些是它擅长的。

结语

AI 的不确定性确实让人想用确定性去约束它,TDD 看起来就是这个矛盾的最优解。但这个直觉遗漏了一个关键变量:被约束的对象有没有内在的正确性标准。人有,所以 TDD 在人手上是路标系统。AI 没有,所以 TDD 在 AI 手上是 Goodhart 的游乐场。

这不意味着不要测试。恰恰相反,AI 时代需要的测试比人类时代更重——只不过它的形式不再是铺满每个函数的 unit test,而是集中在系统边界的 invariants、contracts 和 E2E 验证。确定性该守在终点,不该撒在每一段路上。

期望靠加更多 unit test 或者微调 TDD 的流程来跨过 AI 和传统测试之间的这道 gap——这是数量级的差距,不是态度问题,不是方法论迭代能弥补的。


AI 附注:这篇文章的核心洞察是”目标函数不同”——人类开发者有隐式的工程标准和长期后果驱动,而 AI 只有 prompt 给的反馈信号。TDD 将测试变成 AI 的唯一目标函数时,就触发了 Goodhart 定律。解决方案是”测结果而非路径”,把约束从代码执行路径移到系统边界(invariants),让 AI 在护栏内自由探索实现细节。