开发者用Claude Code快速交付了PR,三月后同一段代码引发生产事故。原因是代码虽能跑,但无人真正理解其逻辑,无法安全修改。AI编程工具提升了首版交付速度,却增加了后续维护难度,作者建议跟踪「第二次修改成本」而非仅看首版效率。
三个月前,我在十一分钟内审批通过了一个 PR。Claude Code 写的 diff,代码干净,测试全绿,完全符合工单需求。上周,同一个文件引发了一次生产事故。不是因为代码写错了,而是因为没有人——包括我在内——真正理解它了。
所有人都在用「产出第一版有多快」来衡量 AI 编程工具。没有人去衡量在第四个月再动这笔代码要花多少成本。我一整年都在生产级 TypeScript 项目里跑 Claude Code,而维护账单是我之前算错的那部分。
那个速度数字是真实的。但它不是正确的数字。
我并不打算撤回之前写过的结论。那次 20 万行 JS 到 TS 的迁移确实用了六周,零新增生产 bug。之前要花三小时的重构现在确实只要二十分钟了。这些数字都站得住脚。
我没有追踪的是:三个月后,换一个人,安全地改同一笔代码要花多长时间。这个数字变大了。只是我没有在度量,所以直到它以事故的形式出现时我才注意到。
以下是我今年接触的四个团队中反复看到的模式。第一周,速度提升了三倍。每个人都很兴奋。第八周,代码库里凭空多出了三倍的代码量,其中大部分再也没人碰过,而那些确实被碰到的部分改起来比等价的手写代码更花时间。
测试覆盖率的假象是一个陷阱
上面那起事故涉及一个支付重试函数。它有 94% 的测试覆盖率。大致如下:
it('retries the payment', async () => {
const result = await retryPayment(mockPayment)
expect(result).toBeDefined()
})
it('handles failure', async () => {
const result = await retryPayment(failingPayment)
expect(retryPayment).toHaveBeenCalled()
})
每一行都执行了。什么都没有真正被验证。toBeDefined() 对任何返回值都会通过,包括一个 error 对象。第二个测试甚至没有检查结果,它检查的是函数被调用了,而这从来就不是疑问所在。
当你只说「给这个函数写测试」时,Claude Code 会不断写出这样的测试。它不是偷懒,它是在满足指令。覆盖率上去了,信心也上去了,但两者都没有意义。那个发布出去的 bug 是一个重试计数器在遇到特定超时错误时重置了而不是递增。四套测试全部通过,永远通过,因为没有一套断言了计数器的值。
修复方法不是写更多测试,而是指定行为而不是要测试。
it('increments the retry counter on timeout, does not reset it', async () => {
const payment = createPayment({ retryCount: 2 })
const result = await retryPayment(payment, { failureType: 'timeout' })
expect(result.retryCount).toBe(3)
})
在 prompt 里多写这么一句具体的描述,就是一个能捕获 bug 的测试和一个看起来能捕获 bug 的测试之间的差别。我现在在要求实现之前先写好我想要的断言。多花两分钟。如果当时这么做,能节省四个小时的生产环境调试。
代码审查变得更快的同时也更差了
这是最让我意外的部分。AI 生成的代码在风格上是一致的。格式一致、命名一致、结构一致。这种一致性让它读起来显得可信赖,而看起来可信赖的代码会被更快地审查,审查得也更不仔细。
我发现自己就在这么做。一个「看起来像 Claude 写的」的 diff,比一个乱糟糟的人工 diff 更容易被快速扫过,尽管统计上乱糟糟的人工 diff 更可能有明显的 bug,而更不可能有隐蔽的 bug。在 AI 代码审查中存活下来的 bug 不是语法错误,没人会漏掉那些。是在整洁代码外表下的逻辑错误。一个重试计数器重置、一个日期范围的 off-by-one、一个处理了联合类型错误分支的空值检查。
改变了我的审查流程的是:我不再用审查人工代码相同的方式审查 AI 生成的 diff。对于人工代码我在检查「这有没有道理」。对于 AI 生成的代码我在检查「这是否处理了工单没提到的那个 case」。问题不同,审查的轮次也不同。每个 PR 花的时间更长了,而不是更短,不管营销文案怎么说 AI 审查提速。
债务是架构层面的,不是风格层面的
糟糕的命名和不一致的格式是你在 diff 里能看到的债务。我现在真正担心的债务在 diff 里完全看不到:同一个问题在一个代码库里被解决了四种不同的方式,因为每个 PR 都是一次孤立的会话,记忆里没有其他三次有人解决过它。
我们遇到过完全相同的事——日期范围验证。四个功能里四个不同的实现,都是 Claude Code 在六周内不同会话中写的,每个内部一致、单独来看都正确。没有一个调用了同一个函数。当其中一个的 bug 被修复时,另外三个依然有 bug,因为没有人意识到一开始有四个,包括审查每个独立 PR 的人。
这是一个没有持久记忆代码库决策的工具的直接成本。一个三个月前写过日期验证的人类工程师记得自己写过,会再去找它,或者至少会有一个「我之前不是做过这个吗」的念头。Claude Code 每次会话都是从零开始,除非你给它东西读。
对我们真正有效的修复,写在了代码库本身里:
## Existing Utilities — Check Before Writing
- Date range validation: `src/shared/validateDateRange.ts`
- Retry logic with backoff: `src/shared/withRetry.ts`
- Currency formatting: `src/shared/formatCurrency.ts`
Before writing a new utility, search this list and the `shared/` directory.
CLAUDE.md 里三行字,让那个代码库里重复的工具函数从每周都能发现,变成了接下来两个月只发现了两次。便宜的修复。只是我们之前没有把 CLAUDE.md 当作活的文档来维护,在项目开始时写过一次就扔在那儿了。
我工作方式的实际改变
我比六个月前对 AI 辅助开发并没有更悲观。只是对更窄的一块更乐观了。
我不再说「写测试」,而是指定断言。现在的 prompt 里包含了精确的期望值,而不只是「增加测试覆盖率」。
我用审查未声明 case 的方式审查 AI diff,而不是已声明的 case。工单天然只描述快乐路径。审查必须去狩猎它没有描述的东西。
CLAUDE.md 现在是一份活的文档,不是一次性的设置步骤。每次我抓到一个重复实现,就往 CLAUDE.md 里加一行指向规范版本。这个文件每周都在增长,否则它就没有尽到它的职责。
我在速度旁边多追踪一个数字。不只是「这次发布有多快」,还有「下次改这个文件花了多长时间,改的人理不理解当时为什么那样构建」。第二个数字才是预测事故的那个。
AI 生成代码的维护成本不是放慢脚步的理由。是停止度量错误事情的理由。快速发布从来都不是软件的难点。六个月后理解你发布了什么才是难点,我们只是过去把那个成本支付得足够慢,慢到注意不到它。
如果你在真实团队里让 AI 生成的代码跑过了三个月这个节点,我真的很想知道重复实现的问题在你那里是否也出现了,还是说那是我们工作方式特有的。在 Twitter 或 LinkedIn 上关注我,会有更多关于 AI 与架构的内容。
Originally published on my Hashnode blog. Follow me for more AI + Architecture content.