机器写的代码,要过哪些关
2026-08-18 · 4 分钟阅读
我现在上线的代码,大部分是 coding agent 写的。这句话仍然会让一些工程师皱眉,我理解。 接下来的问题永远是同一个:你怎么知道它是对的?想听技巧的话,我的回答会让人失望; 想要一套系统的话,我可以说得很具体。我不再只依赖评审,而是把标准做成了一道道关卡。
我说的“关卡”,是跑在合并路径上、不会因为你忙、你累、你自信就被跳过的检查。人工评审还在。 但评审是一个人在下午五点、读完当天第四十个 diff 之后做出的判断;关卡对第一个 diff 和第四百个 diff 用同一个标准。当代码到来的速度超过任何人能仔细阅读的速度 —— agent 干的就是这件事 —— 标准就必须住在机器里,而不是住在意志力里。
不是这条路上的所有东西都叫关卡,这个区分值得较真。关卡负责拦截:在合并之前,失败即封闭。 审计负责观察:在事后运行,专门抓绕过关卡的东西。还有一些决定就该留在人手里 —— 发布、资金操作、一切不可逆的动作。把这三者混为一谈的团队,最后会把告警当关卡用, 再把真正拦住自己的关卡悄悄关掉。
值得跑的关卡都来自事故。下面的例子是有代表性的故障类别,刻意在我经手过的多个生产系统之间做了模糊化。 一个计费 bug,变成一条幂等检查:只要存在某条代码路径能让扣费执行两次,构建就失败。 环境之间悄悄发生的配置漂移,变成一项漂移检查:声明的状态和实际在跑的状态逐项比对。 绕过流程进入主线的变更,变成一套独立的事后审计:盯着合并路径,对已知的绕行模式告警。 套路始终是同一个:写复盘,修根因,再把修复变成检查,让同一类故障没法悄悄回来。
把其中一条链路从头到尾展开(已匿名化)。契约:一次付费动作,恰好扣费一次 —— 无论客户端重试、网络重发,还是后台任务重放。场景清单从契约来,不从代码来:同一个请求发两次; 两个请求并发竞争;扣费成功但交付之前失败;隔了一天的重放。生产环境里的落点, 是每一次资金变更都必须携带的请求级幂等键。关卡断言的是可观察结果:同一个键,账本里至多一笔扣费; 任何代码路径能违反它,构建就失败。关卡红了,合并就停 —— 没有人能用一句“重试大概率不会发生”把它说过去。
审计那一侧同样要有下文。当主线审计发现一个绕过流程进来的提交,处置是预先定义好的: 补做评审;它走的那条路,要么封死,要么正式化;然后更新审计自己的不变量。 没有人处理的告警,只是装饰。
让我意外的是,这件事反过来改变了我和 agent 的协作方式。关卡够硬,前期就可以放得开。 我让它多提案、多尝试、多重构,因为最后一道防线不是我,是合并路径。焦虑从我的脑子里搬进了 CI 配置 —— 在那里,它可以被评审、被版本化、被持续改进。
有一种失败模式值得点名:看着实现写出来的测试。agent 特别擅长给它刚写的代码配上必然通过的断言 —— 这等于同一个 bug 写了两遍,还被标成了覆盖率。测试要成为关卡,断言的预期值必须来自代码之外: 契约、边界条件、空值、恶意输入、可观察的副作用。我在看 diff 之前先列场景清单; 一条只是和实现互相印证的断言,不过是把实现复述了一遍。
这一切并不会让机器写的代码天生安全。它做到的,是把“安全”从读代码那个人的属性, 变成代码必经之路的属性。这个区别重要,因为路径可以扩展,注意力不能。