跳到主要内容
让同一个 AI 写实现又写测试,等于让它自己出卷自己批

让同一个 AI 写实现又写测试,等于让它自己出卷自己批

阿舟
阿舟

· 阅读约 7 分钟

上个月某个周四晚上,我盯着一份跑绿的测试,越看越不对劲。文件是 Claude Code 写的,工整,注释齐全,命名挑不出毛病。

大概长这样:

def test_round_amount():
    # 当前实现是 ROUND_HALF_UP
    assert round_amount(2.675) == 2.68

断言没错。跑起来也绿。问题在它绿的原因——不是"这是对的",是"这个函数此刻恰好这么写"。这份测试锁住的不是业务意图,是当下实现的快照。

而这份测试和那个函数,是同一个实习生写的。它写完实现,转头给自己出了张卷子,还批了一百分 😅

画外音:这活儿我实习那会儿也干过,区别是当时监考的是人。

前几天刷到 dev.to 上一篇文章,作者 Info Inlet 把整条流水线拆成规划、编码、测试、评审、调试、发布六段,每段丢给智能体跑几周,留下有用的、砍掉没用的。里面有句话我点头点到脖子响——让同一个模型给自己的作业打分,等于没有校验。他管这叫"作者与考官分离"。

我这场翻车也不是第一次。更早还中过一次,只是当时没往结构上想,只当 prompt 写得不够狠。

六条断言是怎么被换掉的

往前数几个月。那次我图省事,一个 agent 同时写实现和测试,规矩就一句"测试要覆盖边界情况"。它干得相当漂亮,覆盖率上去了,测试文件比我手写的还整齐。

覆盖率是个特别容易骗人的东西。它能到 95%,同时没有一条断言在验证意图。……扯远了,回正题。

三个月后财务提了个需求,舍入规则从四舍五入改成银行家舍入。实现改了,跑测试,红了六条。接手的是个新人,看了眼报错,把断言改成新值,提交,绿。

-    assert round_amount(2.675) == 2.68
+    assert round_amount(2.675) == 2.67

他是照着报错改的,不是照着意图改的。怪不了他——那六条断言里没有一个字说明"为什么是 2.68"。

那次没出事故,纯属运气。那六条测试从此变成"当前实现的快照",不是"这个函数应该做什么"。它们照样拦得住回归,只是拦住的概率和抛硬币差不多。

我一开始觉得拆成两个 agent 太麻烦,一个顺手写完不就完了。后来证明我这个判断是错的。

改起来不难,账在别处

结构上的改法很简单:写测试的智能体不许看实现,只给规格。

# 给测试智能体的输入,就这三样
- 函数签名与入参类型
- 验收标准:2.675 应进位到 2.68
- 边界清单:负数、0、恰好半数、超出精度

实现代码、现有测试文件、git 历史,一样不给。这样它写出来的测试,才是在编码"应该做什么"。

副作用我一开始没料到,后来觉得比测试本身还值钱:测试和实现终于会吵架了。以前同一个 agent 写的东西永远不吵架,它俩本来就是一个脑子。现在会吵。有一次红了半天,查下来不是代码错,是我那条验收标准写得含糊,两边各自合理理解了一遍。那次修的是我的规格,不是代码。

代价是,我得先把验收标准写出来。这事比听着烦得多。写实现的时候很多决定是顺手做的——"负数返回 0 吧""超精度就截断"——做的时候根本没意识到自己在做决定。现在要写下来,等于把这些隐形决定一条条摆上桌。

写规格花的时间,比我预想的长得多。这才是真正的账。

规划那边也一个道理

"作者与考官分离"不止在测试阶段有用。

规划阶段一模一样。那篇文章说规划智能体擅长产出格式完整的计划文档——编号步骤、待改文件、回滚说明,一应俱全——但它常常在解决错误的问题,因为它会用统计上最可能的解释去填空。你需求里每一处歧义,它都替你补一个"最可能",补得毫无痕迹。

我现在的做法是多开一个:一个 agent 只管写计划,另一个只拿原始需求,任务是挑刺。

画外音:这是我给这套流程加的最便宜的一环。

上个月我提过一个工单,"给用户列表加个筛选"。对抗那个 agent 回了四个问题:

Q1: "用户"指当前租户,还是全量?
Q2: 筛选条件进不进 URL(能不能分享这个视图)?
Q3: 分页还保留吗,现有默认 20 条
Q4: 筛完要不要重算总数?现在的接口不返回

四个里有两个我压根没想过。

它还在干第二件事,这个更阴险——判断这是不是"正确问题里过小的一片"。工单本身没错,但范围太窄,做完了下个月还得再开一单。智能体对工作量的估计天生偏乐观,因为它看不见"这一改会牵动谁"。

评审阶段同理。我加了个评审 agent 读每个 diff,它的价值很实在:

[review] src/billing/refund.ts:41
- refund() 抛的 ProviderTimeout 没接
- 金额为 0 时不该调支付网关,现在会调
- 新增的两个测试没有断言,只有调用

这些事它比傍晚六点的我强太多。我那个点看什么都像没问题。

但真正重要的评审它做不了。两件:这个改动是不是在解决正确的问题;以及 diff 之外那三个文件,会不会被这一改弄坏。

麻烦在于,代码智能体最爱产的恰恰是这种——每一行单独看都对,整体决定是错的。diff 干净得像样板,测试全绿,命名规范,注释得体。你眯眼扫一遍,会觉得这实习生真靠谱。

所以 diff 旁边必须坐一个怀疑的人。那篇文章评论区有人说得比我准:diff 本身不是合适的评审单位,真正的单位是这段代码此前做出的承诺。

省下的是打字,还回来的是读陌生代码

这几段跑下来,我的时间分配变了,总量没少。

敲代码:从很多变成很少。 写规格:从很少变成多得多。 读 diff:从一些变成多得多。

省下的是打字,还回来的是读一个陌生的东西。后者累得多。看自己的代码有肌肉记忆,看一个 diff 你得从头重建它的意图,一边重建一边怀疑它。

这里我到现在也没完全想明白:规格写到什么颗粒度算够?写太细等于我自己把实现想完了,那还不如自己敲;写太粗,agent 就开始自由发挥。这条线我至今靠手感,没有数字,也不觉得短期内能有数字。

调试那段我基本认同原文:有闭环反馈的时候它很强。有失败测试、有堆栈、有能复现的步骤,它能自己跑、读报错、提假设、打补丁、再跑,耐心和速度都比我强。但"只影响迁移前注册用户的那种问题"它不行——那需要的不是推理,是八个月前我做那个决定时脑子里的东西。而那东西从来没写进代码。

评论区还有人提到一件事,我自己也中过:追踪记录看着挺好,可智能体长期没进步,因为结果没反馈到下一次运行。我的配置文件三个月没动过——那意味着我的实习生每次来都从零入职,上一轮学到的教训一条没带走。

我现在只留一条规矩

原文的建议里有一条我想重复:先从"乏味的胶水工作"入手——PR 描述、变更日志、提交信息、发布说明、补上你平时跳过的那行原因注释。这是投入产出比最高、风险最低的一段,也是唯一一段不会在你看不见的地方替你下决定的。

我自己的规矩只有一条:

凡是交给智能体写的代码,贴出去之前先问一句——这块的决定是我做的,还是它做的?答不出来就先别交。

那篇文章的作者在文末问了个问题:哪个阶段在你手里看着安全、其实在悄悄造成损害。我的答案是测试。它是整条流水线上唯一一个"通过了反而更危险"的地方——红变绿会让人安心,而安心这东西,我这半年里没占过便宜。

写完这篇我又回头翻了一眼自己仓库。有几条测试还是同一个实习生写的,暂时没删,留个证据。

阿舟
阿舟

写代码写到一半开始怀疑人生,靠 AI 工具续命,顺手把踩过的坑都记下来。

查看主页 →