我很少直接说哪篇文章必须看,但 dev.to 上一篇署名 Don Johnson 的文章《Shipping Assumptions》,里面有一句把我这大半年堵着的话给捅了出来:AI 辅助开发让团队生成代码的速度,超过了他们理解系统的速度——真正危险的地方不在单个文件,在边界上。这句话放在几年前就是句提醒,放在今天人人喊着“三分钟生成一个项目”的气氛里,是警告。
把需求描述丢给 Copilot,两分钟后满屏代码。跑通了,单测绿了,PR 一交,CI 过了,第二天上线。没人问一句:这系统给谁用?哪些状态绝对不能出现?哪些边界归谁?代码先于理解出现了。我见过太多这种场面,多到我现在看见“生成成功”四个字都胃不舒服。然后代码就成了设计。病根就摆在这。
代码能过 lint,能过浅层测试,照样能藏着全局风险。浅层测试验的是“这行代码做了它说要做的事”,验不了“这个系统在并发重试、部分失败、权限迁移、状态切换下还成立不成立”。这类错从来不出在某个函数体里,出在两个组件之间的空档里——一个边界上的假设被改了,两边的代码都还在各自世界里合理。生成器最擅长的就是生成这种:每个组件看起来都对,合起来是一堆互相打架的隐性承诺。
一个订单系统里,账户余额不能在没有对应交易记录的情况下变化。这条不变量没人写下来。工程师用 AI 生成了扣款逻辑,生成了余额表,生成了交易流水表。扣款先改余额,再写流水。跑起来没问题,退款也行。直到某天接口超时,重试,同一笔退款双写了两次。丢钱的那种错,慢吞吞浮出来,回滚都来不及。问题根本不在扣款那段代码,问题在“余额变化必须伴随唯一交易流水”这个承诺没被任何人说出口。AI 先把实现写出来了,于是团队默认它已经想过了。
所以这波 AI 编程最大的坑不是代码写得差,是它把“设计”这个动作悄悄跳过了。过去你写代码慢,慢在必须先在脑子里把系统揉一遍:数据结构、边界、不变量、失败路径。现在生成太快,快到你以为可以不用揉,直接看“感不感觉对”。可一个看起来对的代码,经常只是一个高置信度的错误。
补代码审查没多大用。审查发生在代码出现之后。一个没经过显式设计讨论的系统,你就算派三个人去看代码,也只是在看一个从未被挑战过的默认设计。审查者大多不会自己重新推导一遍系统不变量,他们会被“看起来合理”推着走。真正缺的那层不是更多 review,是一个人写出的承诺和机器生成输出之间的共享模型。这个模型不会自己长出来,得靠硬功夫把它钉出来。
第一层,画边界。C4 那张图,上下文图、容器图、组件图——很多人一听就烦,觉得是给架构师开会用的。但 C4 的用处从来不是看图,是让边界可讨论。上下文图上谁用这个系统、外部依赖是什么;容器图里哪个服务读写哪些数据、边界归谁。AI 可以帮你草稿一张图,但最后一笔必须你自己画,因为边界权属这件事,模型不知道哪条线是“出了事要赔钱”的那条。
第二层,写不变量。TLA+ 更直接,它逼你把“什么必须永远成立”写成一个精确的模型。它验证的不是你的代码库,是你的脑子。你说出“任何两个成功的写操作不能悄悄覆盖彼此”,它帮你找反例。写 TLA+ 的过程,就是把你以为懂并发变成你确认不懂并发的过程。我以前试过给一笔订单的并发状态写几行 TLA+,TLC 直接给出一条反例:两个离线设备各改了一次同一张照片的说明,重传顺序不同,一个字留在了错误的时间线上。代码是对的,模型也没写错,是实现里没处理这个顺序。不写模型,你永远不会在测试里想到“重传顺序不同”这种场景。
第三层,确定性模拟。它跑的是真实实现代码,把时间、随机、调度、网络、存储、故障全塞进模拟器当输入,失败靠种子复现。TigerBeetle 的 VOPR 就这么干。他们文档里有个关键区分:model checking 暴露算法错误,simulation 暴露具体实现及其底层假设。这两者管的是两种不同的错。只写 TLA+ 就觉得自己安全了?不行,因为 TLA+ 验证的是模型,不是你这堆真代码。只做模拟不写 TLA+ 呢?也不行,因为模拟可能只是把你生成出来的高置信错误在各种种子下重复了一遍。
这三层合起来就一句老话:先确认你想要什么系统,再确认系统在这个模型下守不守得住承诺,最后拿真实代码在受控世界里把它打翻。顺序不能反。
反了是什么样?评论区里有个人讲了个事。AI 生成了一段合并图数据里实体身份的代码,看起来完全没问题,本地跑得好好的,单测也绿。三个月后才被发现。它破坏的是跨实体身份一致性,不是单点正确性。AI 没想过的边界,系统就没有边界。你后面补测试、补审查、补监控,都是在给一个根本没被设计过的默认结果打补丁。
我不是说每个系统都得上 TLA+。我见过有人给家里老人存照片的小应用上形式化验证,旁边的人问“你疯了吗,这么个小东西要上这个?”这是完全没搞懂严重性怎么算。严重性不是基础设施规模决定的,是失败后果对信任者造成的伤害决定的。那个小相册的承诺是:原始照片不能丢、私密照片不能意外暴露、说明和时间不能乱、两个家人不能把同一个修改覆盖掉。这些承诺破了,对一个普通人的伤害大不大?很大。她不需要形式化整个系统,但她需要把上面五条写下来,然后拿一个小模型去验最危险的那条:并发编辑和重试上传。上下文图也可以很简单——使用者、应用、身份提供者、本地存储、云端存储、分享边界。画出这张图,她才知道“分享”和“私有”是两条不同的边界。AI 才能在这个边界里生成代码而不越界。
现在这一行最大的问题,是我们一边用生成器加速生产代码,一边把“写承诺”这件事从流程里挤掉了。为什么?因为写承诺很慢,费脑。你让 AI 写实现很快,让人写“原图无论如何不能丢”很慢。想写清楚,他得先想明白:这系统为谁服务,谁会被伤害,哪个状态变化最不可逆,哪些边界一跨就有损失。这些都比生成十页代码重要。可它们都是隐性的、不被追踪的、不进 PR 的东西。没人提,于是就不存在。
有个做支付集成的团队被质量卡住——测试写了不少,代码生成也在用,就是不停出小事故。后来他们把问题收敛到五个:什么必须守恒?什么必须唯一?什么只能单向移动?什么不能重复执行?谁有权造成这个变化?五个人,每人写五条,重复的去掉,剩下十一二条硬约束。从那天起,他们给 AI 的提示词里先塞这些不变量,再谈生成。效果立刻不一样。不是模型变聪明了,是模型终于知道哪条线不能碰。后来他们把这些不变量做成边界上的可执行检查,放进 CI,加进生成代码的守卫,小事故少了一大半。
这话我得说得更重一点:不是 AI 让开发变危险了,是 AI 让一直存在的坏习惯变贵了。以前的软件也有隐性假设,但以前写代码慢,假设在打字过程中被迫慢慢浮出来;写错一次代价也高,人多少会停下来想想。现在不用了。更快的实现、更厚的框架抽象、更廉价的使用体验,把人包进了一个“不用看底下”的茧里。原来靠人脑撑着的设计过程,现在可以直接绕过去。绕过去一次没出事,你就以为不需要了。等出事时,已经不是改一行代码能救的。
那些真正靠谱的团队不是拒绝生成代码。他们是在生成之前把设计先写下来。写上下文图,不为文档美观,为把边界所有权定死;写几条不变量,不为做形式化,为给生成器划禁区;做确定性模拟,不为炫技,为在真实实现里找那些高置信度的错。这个顺序才是 AI 时代该有的工程纪律。不新鲜,都是计算机科学里早就有的东西,只是被便捷埋得太深,现在必须挖回来。
我们这一行现在有个很阴险的趋势,把“生成出来的东西能跑”当成“产品没问题”,把“我让 AI 写了”当成“我做了设计”。这叫什么?本末倒置。AI 生成代码这件事本身没错,效率提升也是真的。但如果不先让人类把系统承诺写下来,那个效率就是制造烂账的效率——说白了是在沙子上盖楼,生成得越快楼盖得越高,塌起来越响。代码生成得越快,隐性边界假设越多,爆出来越晚,排查代价越大。欠的债迟早要还。早还,是写五条不变量;晚还,是一个深夜的生产事故。
我的态度很明确:先把承诺写下来,再让模型生成实现。不变量不能等代码出来之后再补认,因为后补的人只会从已经生成的代码里复制假设,再把它包装成规则。要做就做完整的一条线:先画边界,再写不变量,再生成实现,再用确定性手段攻击它,最后把攻击结果喂回模型。少一步,这个系统就不值得上生产。
具体说怎么开始。我用过的最轻量版本是这样的:一张上下文图,一张容器图,五条硬不变量,对最危险的那次状态迁移写一个小模型,再加一个带种子的模拟测试。不是为了形式化,是为了让每个重要假设有一个地址。写错了可以改,但没写就是没有。
别把“普通 CRUD 不值得这么搞”当逃避的借口。一个 CRUD 应用也会有两个用户同时编辑一条记录,也会有重试导致双写,也会有权限没检查到的暴露。你不管它,它就在那等着。AI 不会替你管这些。它只会生成一个看起来把这四件事都做了的系统。到底有没有做对,只有你写下的那几条承诺知道。
我先自己往回拉一句:别把上面那套理解成每个项目都要上 TLA+。我不是这个意思。我是要每个项目都有人负责把关键承诺写出来。写法可以轻,责任不能省。你要是连五条不变量都写不出来,那这个系统到底要守什么,你自己也不清楚。这不是保守,这是这一行里唯一还没过时的工程素养。基础功没有捷径。
那就是个更大的问题了。