上周我写了AI 代码“几乎正确”的 7 种方式。在结尾我向你们征集第 8 种方式,并承诺在这里分享最好的回答,并注明出处。

我只征集一条。你们给了我七条:五条全新的,还有两条让我清单里的检查更犀利。Mudassir 还分享了他团队的一条简单规则,放在接近结尾处。说实话,你们的几条回复教给我的东西比我自己的文章还多。感谢每一位花时间写下来的人,其中有些显然来自生产环境里糟糕的一天。

开始前先坦白一件事。那篇文章的封面来自一个图像生成器,第一次尝试时它把我标题里的一个词弄丢了。图片看起来很棒。句子却是错的。我在保存前一刻才发现。考虑到那篇文章的主题,这有点尴尬。

8. 形状对,值不对

Maxwell 的评论是最让我担心的。没有异常,没有可捕捉的东西,字段被填上了。只是它不是源数据所说的内容。用他的话说,“解析器可以在整个版面上把 annual 读成 monthly,而且一次都不会失败。”

30 秒检查(他的):随机挑 5 行,手动与源数据比对。随机很重要。如果你只检查那些看起来已经不对劲的行,你什么也抓不到。

我会补充两点:也检查这些行里的标签和单位,并且如果源数据有一个可信的合计(列求和、行数),就比对它。合计吻合不能证明每个字段都对,但错误的换算通常最先在那里暴露出来。

9. 你只要一行。它整理了三个文件。

Aliza Ali称之为静默范围蔓延。你要求修一行,而 diff 还重命名了一个变量、“清理”了一个 import,并动到了你从未提及的文件。单独看,这些改动都不值得担心。合在一起,它们改变的东西远比你要求的多。

她的后续回复提出了一个我没想到的点:自动接受让这种情况更容易发生,但逐步审查也不能完全救你,因为每一处编辑单独看都合理。最终的 diff 是唯一能一次性展示全部足迹的视图。

30 秒检查:运行git diff --stat在阅读任何一行之前(或git diff --stat main...HEAD如果智能体已经提交了的话)。文件更多并不一定是坏事,因为真正的修复可能同时需要它的测试和一个类型。但在审查其他任何内容之前,先问清楚每一个你没预料到的文件。

10. 会漂移的状态

这个问题更难发现,因为代码读起来很干净。Khali Sollis描述了一个自定义 hook 或异步例程,它能通过首次渲染测试,却悄悄漏掉了一个响应式依赖,或者闭包捕获了过期的状态。它“在快速手动检查时运行良好,但一旦真实用户开始按顺序与 UI 交互,就会立刻失去同步。”

30 秒检查:不要在全新加载后只测试一次。连续多次更改状态,并触发任何稍后运行的东西(定时器、异步回调)。还要留意新添加的eslint-disable用于react-hooks/exhaustive-deps在 diff 中。这条规则能抓住其中许多问题,而让它闭嘴是让警告消失的最快方式。

11. 本可以抓住它的测试被 mock 掉了

mp3sss在 Electron 工作中看到了这一点。一个 AI 编写的 IPC 处理程序看起来正确,却从不校验渲染进程发送的内容,而本应抓住这一点的集成测试被 mock 了。自动更新和崩溃上报也是同样的情况:正常路径很容易生成,而失败路径(网络中断、下载不完整、过期的崩溃转储)恰恰是被跳过的部分。

Mudassir Khan从另一个角度撞上了同一堵墙:一个catch返回空数组的函数能通过所有测试,因为测试也是同一个智能体写的。

30 秒检查:对每一处 AI 改动,故意强制制造一次失败。切断网络、发送错误载荷、破坏依赖,确保你得到的是错误,而不是悄无声息的空结果。正如 mp3sss 所说,“为每一处 AI 改动配上一个强制失败路径测试,比正常路径测试帮我们省下的更多。”

12. 被延长了超时时间的 flaky 测试

Max Kaya把这一条加进了 #2 的测试检查中。除了toBe悄悄变成toBeTruthy之外,还要留意“为了修复 flakiness”而延长的超时时间。用他的话说,这“通常意味着测试现在靠构造就能通过。”

有时旧的超时时间对 CI 来说确实太紧了。但很多时候,慢调用或竞态仍然存在,你只是不再听到它了。

30 秒检查:在 diff 中搜索更大的超时数字和新增的重试(retries,jest.retryTimes,--retries)。每一个都应该指向某人真正理解的时序问题。

13. 一个真实的包,但用得实在太多了

Artur Pokładek称这是 #3 的表亲。这个包确实存在而且很流行,只是它为了本该十五行代码就能搞定的事情拉进了一堆依赖。他没细看就接受了一个智能体提交的版本。

30 秒检查(他的做法): git diff --stat检查 lockfile。如果一个小功能让它增加了几百行,就要问为什么。大的 lockfile diff 也可能有平淡无奇的原因,所以把它当作需要查看的理由,而不是证据。

我问他那他会怎么做,他的回答比我的任何规则都好。小的辅助函数,他自己写并测试。日期、解析和加密仍然交给知名库,因为在那里他自己写的十五行代码反而会是 bug。那个长得像的小包是他最不信任的选项。

14. 它通过了一切,却从未发布

Guillermo Baigorria在他评论的前一天就遇到了这种情况。AI 用一个新补丁文件夹修复了一个 bug,所有测试在他的机器上都通过了。但部署脚本打包的是一个固定的文件夹列表,新的那个不在其中。没有测试能发现这一点,因为测试运行在文件存在的地方。

Paul Lovelace称这是测试运行的位置与实际发布内容之间的鸿沟,并说这是他最先会借鉴的检查。

顺便说一句,这个列表是故意硬编码的。固定列表正是防止杂散的.env被发布出去的东西。所以 Guillermo 的团队检查构建出的包而不是脚本,标记新文件,然后由人来决定。

30 秒检查(他的做法):列出这次改动新增的文件,然后在你要发布的内容里逐个查找。

git diff --name-status main | grep '^A'

将其与你的 Dockerfile 对比COPY行或files列表在你的package.json中。

一条值得照搬的规则

Mudassir 还发来了对上次 #2 的辩护。他的观点是:当测试文件在同一个 diff 中被改动时,一次绿色的运行能告诉你的信息很少。它表明新测试通过了,而不是旧行为仍在被检查。所以他们团队的规则是,任何触及测试文件的 diff 都需要在 PR 中给出一行理由,而他们的审查机器人会让被跳过或被放宽到toBeDefined的测试失败。

我喜欢的是它有多便宜。改动测试的人只需说明原因。而如果没人能解释某个测试为什么变了,那就是一个停下来仔细看看的好理由。

又轮到你了

如果非要挑最糟的,那就是第 8 条。一个从未发布的修复通常会在有人再次碰到这个 bug 时被发现。而报告里一个错误的数字可以悄无声息地存活好几个月。

这些里面你实际遇到过哪些?有没有人在 CI 里自动捕获其中任何一个,而不需要有人去读每一处 diff?如果评论像上次那样涨起来,也许这里面还有第三篇。


小提示:Mudassir 的规则让我想起了我的免费 Claude Code 插件,proof-of-done。它会标记那些让检查通过却没有真正修复的测试改动(跳过的测试、更弱的断言、被删除的测试、新增的eslint-disable或@ts-ignore),并且在它的报告解释清楚这些之前,不允许智能体说"完成"。自上一篇文章以来,它还会单独检查 monorepo 中的每个包,这一点来自 Mudassir 早先的一条评论,并且会指出你的测试从未运行过的改动行。观看 30 秒演示。

我构建的一切(全部免费且开源)

which-ai-to-use:为每个任务挑选最佳的 AI 和模型,并在你触及用量上限时把你的工作交给另一个 AI

proof-of-done:一个 Claude Code 插件,在测试通过之前不允许 AI 说"完成"

  • 安装:/plugin marketplace add WST2002/claude-code-skills然后/plugin install proof-of-done@claude-code-skills
  • GitHub·登上 HOL 博客精选

security-audit:一个 Claude Code 技能,在攻击者之前发现安全漏洞

  • 安装:npx skills add WST2002/claude-code-skills --skill security-audit
  • GitHub·基准测试

我所有的 Claude Code 技能:github.com/WST2002/claude-code-skills

在 YouTube 上观看演示:@wst2002rk·proof-of-done 短片·which-ai-to-use 短片·security-audit 短片

阅读更多:AI 代码"几乎正确"的 7 种方式·在任务中途切换 AI 而不丢失上下文·你的 AI 说"已修复"但从未运行测试·我的 AI 在攻击者之前发现安全漏洞

找到我:GitHub·YouTube·LinkedIn·作品集·Hashnode·Peerlist·npm·LeetCode·电子邮件