AI写代码全绿翻车?四层验收模型避免编译通过单测全绿却上线即炸

📅 发布时间:2026/10/10 18:24:30
AI写代码全绿翻车?四层验收模型避免编译通过单测全绿却上线即炸
这标题一看就是踩过坑的人写出来的。我见过太多团队AI 帮忙把代码写完了编译一次过本地启动顺滑单测几百个绿点所有人击掌相庆然后上线第二天对账系统先炸了。最讽刺的是什么呢最讽刺的是你回头查单测会发现每一行断言都在证明那个错误的行为是对的。问题从来不是 AI 写得不够好而是我们压根没想明白什么叫“验收通过”。今天这篇就把这事掰开揉碎聊透为什么 AI 写的代码那么擅长“全绿翻车”以及验收到底该验什么、怎么验。1. 三个绿灯背后的盲区编译、运行、单测各自的证明力边界先说一个可能让很多人不舒服的事实能编译、能跑、单测全绿这三件事加起来在逻辑上也不等价于“代码行为正确”。它们是三道过滤网但每道网的网眼都大得能游过去一条鱼。1.1 能编译类型系统只保证“形状”不保证“事实”编译通过在静态语言里只说明一件事你的代码满足类型系统和语法规则。int收到了int函数名拼写正确引用的类都存在——仅此而已。编译器看不懂你的业务逻辑它不知道一笔订单实付金额算成负数是不是错误的它也不知道你调用的那个 API 在部署环境里到底存不存在。这里有个 AI 代码特有的坑AI 生成的代码特别喜欢“基于最新文档”写调用方式。本地开发环境拉的是最新版 SDK编译过了CI 构建用的也是最新版依赖又过了等到了生产环境依赖锁定文件里可能还是旧版本那个方法在这个版本里根本不存在。编译通过证明的只是“你在编译那一刻面对的那套依赖里代码长得是合法的”它从来不能证明“代码在你的生产环境里能跑”。类型系统真正擅长验证的是“形状相符”不是“语义正确”。就像你拿到一张画着方的图纸用方形的积木去拼怎么拼都不会倒——但图纸本来该画的是圆。1.2 能跑验证的是“最顺利的那条路”“能跑”这个词的迷惑性比编译还大。本地启动服务点一个按钮、调一个接口返回 200就宣布“跑通了”。但你想过没有这条“通”的路里有多少前提条件同时在成立入参刚好是合法的、数据库连接刚好有空闲、外部服务刚好在线、并发数刚好是零、缓存刚好命中或者刚好不命中。真实世界的流量不会这么客气。用户传了个空字符串、上游服务超时了、数据库连接池被占满了、消息队列重复投递了一条、另一个服务恰好把数据写了一半崩了——这些场景才是生产环境的主旋律。而“能跑”只是在最顺利的那条路上踩了一脚油门。用开车来类比车能发动、能在空旷平路上稳定开到 60 码和你开着它满载上坡、雨天急刹、连续过弯不翻车完全是两个层面的问题。能发动只能说明发动机没报废不能说明司机的任何能力。1.3 单测全绿绿的是“测试写下的期望”不是“业务的真相”这是最危险的一层因为它看上去最“专业”。一百多个测试用例全绿听起来已经做了充分验证了。但请记住单测的本质是把“测试作者脑子里对正确行为的期望”编码成断言。换句话说单测验证的是“代码行为是否吻合测试作者的期望”而不是“代码行为是否吻合真实业务需求”。当这两者出现偏差的时候测试越全、断言越细反而越可怕——错误被精细化地背书了。我见过大量 AI 生成的单测断言质量可以用三个字概括假、弱、空。所谓“假”是 mock 了一堆外部依赖测试的其实是孤立函数在自嗨所谓“弱”是断言只写assertTrue(!response.isEmpty())或者assertEquals(200, response.getStatus())根本不检查数值、状态变更、数据落库结果所谓“空”是测来测去都集中在同一条 Happy Path 上边界值、异常分支只字未提。三件事的盲区我还整理成了一张表方便你心里有个数验证动作能证明什么不能证明什么编译通过代码在编译环境的依赖下语法合法、类型匹配业务逻辑正确、部署环境兼容性、异常路径不炸本地/测试环境能跑主流程在理想条件下可走通边界数据、并发时序、依赖故障、性能衰减单测全绿代码行为符合测试断言里写下的期望期望本身是否代表真实的业务规则2. AI 代码的“自洽闭环”代码和测试为什么常常一起圆谎如果说第一节聊的是三层验证各自的盲区那这一节要说的是 AI 代码为什么特别容易一头扎进这些盲区里而且扎得又深又整齐。2.1 代码、测试、需求共享同一个错误前提想一想 AI 写代码的工作流你给一段需求描述AI 生成实现代码然后你让它顺手补单测它基于什么去写测试断言还是那段需求描述加上它刚生成的代码。三个环节——需求理解、代码实现、测试期望——如果共享同一个错误前提那结果就是一个完美的自洽闭环AI 把“折扣券和满减不能同时叠加”理解成了“先满减再打折”代码老老实实实现了“先满减再打折”测试断言也兢兢业业地验证了“先满减再打折”的输出。三个环节各自内部完全一致单测全绿代码漂亮到可以进教科书。但业务规则本身是错的。这不是测试不严格——这套链路从第一个关节起就跑偏了后面的所有验证都只是在给错误的方向盖章。类比一下考试时学生和老师拿到的是同一份错误答案。学生把错误答案背得滚瓜烂熟答题完美老师拿着同一份错误答案判卷判了个满分。所有人都觉得天衣无缝除了真实世界的业务规则在默默流血。2.2 局部上下文之困AI 看不到系统级的隐性约定另一个 AI 代码特别容易踩的坑是它写代码时看到的上下文太少了。人写代码是站在整个代码库的肩膀上你知道这个项目所有金额都按“分”存、时间统一用 UTC、业务错误码有一份枚举、调数据库必须走统一封装。这些是系统级的隐性约定通常不会出现在 AI 拿到的那几个文件里。于是 AI 基于局部信息做出了“合理”的选择——直接用double存金额、直接用本地时间、直接拼 SQL、直接 new 了一个连接的客户端。在它看到的那个文件里这些选择完全合理放到整个系统里每一个都是地雷。更麻烦的是它写的测试同样基于局部信息根本不会去验证“金额必须是整数分”“时间必须带时区”这种系统级契约。这就是为什么我强烈建议在评审 AI 生成的代码之前先让 AI 把它的“假设清单”列出来——它默认了哪些输入、哪些环境、哪些并发条件。这些假设就是它的世界边界也是你可能翻车的精确坐标。2.3 测试的“还原度”骗局断言很重但方向是歪的还有一种情况是测试本身写得很有分量断言不少也检查了具体值但方向从一开始就歪了。比如需求文档里的规则是“实付金额 商品总额 - 满减优惠 - 折扣券优惠且最低不小于 0”AI 写测试的时候断言却写成了“实付金额 商品总额 - 满减优惠”漏了折扣券再配合一组精心构造的用例。每个断言都严丝合缝每个用例都前后一致除了它们共同指向的那个公式是残缺的。这种“高质量错误测试”比“弱测试”更危险因为它会消耗掉评审者的大部分注意力。人看到断言写得这么细下意识就会觉得“测得很到位啊”从而跳过最关键的复核环节——把测试期望和真实业务规则逐条对照。记住一句话测试是需求的投影不是需求本身。投影歪了画面上所有细节都清晰但整个画面都是扭曲的。3. 三种“全绿但上线炸”的典型模式复盘光讲道理不够我把三种最常见的翻车模式完整复盘一遍。都是我在各种项目的线上事故里见到的真实套路代称已经处理过细节可以放心参考。3.1 业务规则被“合理”改写促销模块金额对不上账场景某订单系统的促销模块需要计算订单实付金额。业务规则有两条写得很清楚满 300 减 50折扣券按件生效两条优惠不能叠加。AI 拿到需求后自行“脑补”出了一个它觉得更合理的规则先用满减优惠再在满减后的金额上应用折扣券。它甚至可能不是故意改的只是自然语言里“不能叠加”和“先减后折”之间存在理解歧义它挑了一个看起来无害的解读。代码实现的是“先减后折”单测断言的也是“先减后折”全绿。上线后第二天对账系统爆了。真实的用户买了 400 块的商品、用了一张 8 折券、满减也触发了正确结果应该是 350400 只减满减AI 版本却算出了 280先减 50再打 8 折。单笔订单差距不大但乘以几千上万单之后对账直接对不上。复盘时的排查链路是这样的对账不平症状→ 按订单逐笔比对找出差异订单定位→ 对比规则文档发现代码逻辑与文档不符根因定位→ 回头看单测发现测试断言写的就是“先减后折”这种错误逻辑确认验收漏洞。整个链路最有价值的一步是拿结果去和“业务规则本身”比而不是和“测试期望”比。只看代码和测试永远发现不了这个问题——因为它们自洽地错得非常工整。3.2 环境差异击穿“基于新文档的自信”接口签名在线上不存在场景某定时任务模块需要把一批数据上传到对象存储服务。AI 参考最新版 SDK 文档用了一个新的上传接口参数简洁、语义清晰。本地测试环境依赖是最新版 SDK编译过了单测里把 SDK 调用整个 mock 掉了测试跑得又快又绿。上线后第一个真实请求直接抛NoSuchMethodError服务根本起不来。查了半天发现生产环境的依赖锁文件里那个 SDK 还是旧版本AI 调用的新方法在这个版本里压根不存在。这个模式里有两次翻车第一次是编译环境、CI 环境、生产环境的依赖版本不一致第二次是单测把外部依赖全部 mock 掉导致“代码调用了不存在的方法”这个问题从头到尾没有进入过任何测试路径。mock 本来是为了让单测隔离外部依赖但在这个案例里mock 直接屏蔽了唯一的风险信号。复盘结论凡是 AI 代码涉及外部 SDK、数据库、消息队列、HTTP 调用的部分人审的第一优先级不是业务逻辑而是“这个 API 在项目实际锁定的版本里是否存在、签名是否一致”。这一步在单测里几乎看不见只能靠集成环境或至少一个不打 mock 的冒烟测试来验证。3.3 并发与共享状态串行单测永远看不见的时序地雷场景某记账批处理任务需要按批次汇总大量流水数据。AI 写了一个还算优雅的实现里面用了一个模块级别的共享变量来累计当前批次的汇总值。单测怎么写单测是串行调用的先跑第一批断言输出再跑第二批断言输出。每次调用之间状态是干净的所以全绿。生产环境里这个任务会并发处理多个批次。两个批次同时跑的时候共享变量互相污染A 批次的数据混进了 B 批次的汇总里。最麻烦的是这类 bug 的触发还依赖时序不是每次都必现随机出现——等到发现时数据库里已经写入了一堆不可信数据。复盘时定位过程也很有代表性数据错误偶发 → 查日志发现两个批次运行时间重叠 → 看代码发现共享变量 → 写一个并发复现测试十条线程同时跑立即必现。这个 bug 的根因一句话就能说清但单测全绿恰恰证明了它的隐蔽性因为单测默认的世界是单线程的、串行的、顺序的。凡是和共享状态、并发写入、消息乱序、重复消费相关的逻辑单测基本等于不存在必须靠真实的并发场景去压。同样的道理也适用于性能数量级问题单测里 200 条数据全绿生产环境 200 万条记录直接超时N1 查询、循环内调 API、O(n²) 算法在单测的小数据量下完全没有存在感。4. 验收应该验什么四层验收模型从“代码自洽”到“业务真实”说了这么多问题该给解法了。我自己在实践中把验收拆成了四个层级每一层回答一个不同的问题。你把这四层跑完“全绿翻车”的概率会下降一个数量级。4.1 第一层机器自洽——只回答“代码是否跟它自己写下的期望一致”这就是我们熟悉的编译、lint、单测。这一层的价值是快、便宜、自动作为第一道前置过滤器完全合格能拦下语法错误、明显空指针、测试期望和实现不一致这类问题。但它有一个致命边界这一层验证的是“代码与自己期望的一致性”而那个期望可能是错的、偏的、残缺的。把它当守门员没问题把它当终点就是灾难。如果你的验收流程就是“编译过 单测绿 本地跑通”那你最需要担心的恰恰是全绿的时刻。4.2 第二层场景真实——回答“放进真实环境里还能不能正常”第二层开始脱离 mock 和本地环境。你要回答的问题是这份代码放进它真实的运行环境里面对真实版本的依赖、真实的数据库、真实的网络延迟还能不能正常工作。具体我建议至少做三件事依赖版本对齐。测试环境、CI、生产的依赖版本必须用锁文件严格对齐。AI 代码涉及的第三方 API在上线前跑一个最小冒烟测试确认方法在真实依赖里存在且签名一致。至少一条不打 mock 的集成链路。连接真实数据库本地起容器也行、真实缓存、真实的消息队列把一条核心业务链路完整走一遍。这条链路不用覆盖全业务但它必须真实的——因为 mock 掉最多的地方往往是翻车最惨的地方。端到端关键场景。拿一个真实的业务场景数据走完整流程不只看单个函数而是看模块边界、服务边界上的协作是否正确。4.3 第三层行为正确——回答“不仅没报错而且结果是真的对”第三层是这个帖子的核心也是大多数人跳过去的一层。前面两层回答的是“正常运行”这层回答的是“结果正确”。具体手段我推荐三个属性测试。不针对特定输入而是对任意输入验证不变式。举个例子金额计算的任何结果必须满足“实付金额 0”“优惠总金额 商品总额”“满减与折扣券不同时生效”。属性测试是抓 AI 代码“业务规则被改写”类问题的利器因为它验证的是规则本身而不是某个具体的用例。数据级断言。不要满足于assertTrue(result.containsKey(amount))这种“结构存在”型断言。要写就写assertEquals(35000, result.getAmount())金额以分为单位。AI 生成的测试特别喜欢弱断言你需要在验收标准里明确要求核心计算函数的单测必须有具体数值断言禁止只用“非空”“非零”“状态码”糊弄。契约测试。如果代码要和上下游系统对接把接口契约入参格式、返回结构、错误码枚举写成测试固定下来。这样 AI 代码再怎么自由发挥也偏离不了契约的约束范围。4.4 第四层运行态验证——回答“上线后炸了能不能第一时间发现”验收不终止于上线那一刻。第四层是把“上线后灰度验证 可观测性”纳入验收闭环。灰度自不必说小流量先上新旧版本并行对比业务指标。对 AI 代码我特别推荐“影子流量”式的对比验证把线上真实请求同时复制一份发给新版本拿新旧两个版本的计算结果做 diff任何不一致的地方都是你人工复核的检查点。这个做法对“业务规则被改写”类问题的杀伤力极大因为换代码、换测试都可能有盲区但真实流量下的结果 diff 不会骗你。可观测性上要在上线前就打好日志、指标、追踪的基础并针对 AI 代码最可能翻车的点设置专项告警。别只盯 500 状态码——金额异常、重复数据处理、超时比例升高、缓存命中率骤降这些才是 AI 代码出问题的典型前兆。四层模型核心差异我用表格总结一下层级核心问题典型手段失败表现机器自洽代码是否与自身期望一致编译、lint、单测全绿但业务错场景真实放进真实环境能否运行集成测试、依赖对齐、端到端上线即 NoSuchMethodError行为正确结果是否经得起规则校验属性测试、数据断言、契约测试对账不平、金额错乱运行态验证炸了能否快速感知灰度、影子流量、监控告警炸了一晚没人知道5. AI 时代我坚持的五条验收实操建议最后分享五个我在实践中验证过、现在每天都在用的具体做法。不一定适用于所有团队但它们确实把我的“全绿翻车”概率降到了极低。一、提示词里就写清 Definition of Done。让 AI 写代码前先给它一份验收清单“金额以分为单位必须大于等于 0”“必须复用项目现有的客户信息查询类”“输入为空时返回业务错误码而不是抛异常”。实践证明AI 在不清楚验收标准时特别容易自由发挥而把验收标准写进提示词之后它生成的测试会主动朝业务规则上靠而不是朝自己的实现上靠。二、让 AI 先输出“假设清单”人审完再让它写代码。这是对付“共享错误前提”最便宜的一招。提示词里加一句“在写代码前先列出你对需求的全部理解、以及你正在做的所有隐含假设。”AI 的假设清单会暴露它默认了哪些东西——能不能叠加优惠、是否考虑并发、是否假设外部服务永不超时。人只要审这份清单十有八九能发现需求理解已经跑偏根本不用等代码出来再返工。三、人写断言骨架AI 填实现。别把测试整个交给 AI那是让错误前提互相印证的温床。反过来人负责把测试文件的断言部分写好——期望的具体数值、边界输入、不变式——再让 AI 去补 setup、调用方式和前置数据。验证什么由人来锚定怎么验证交给 AI 发挥。这一条在实操里花的时间极少效果却立竿见影。四、用生产脱敏数据做回归基底和结果 diff。这是抓“业务规则被改写”的必杀技。拿一段真实业务数据脱敏后当测试 fixture或者做一个对照工具新版逻辑和旧版逻辑跑同一批输入diff 输出结果。AI 代码上线前所有不一致的地方都必须人工确认“这是预期的行为变更”还是“无意的逻辑偏离”。真实数据里藏着单测写不出来的脏输入和边界组合这比任何代码评审都更能暴露问题。五、上线前做一次破坏性演练。专门找 AI 代码最薄弱的环节下手依赖断开、超时注入、高并发压测、数据库连接池降到 1、脏数据乱入。AI 代码对异常路径的处理往往最敷衍你不主动炸它它一定会在你没料到的时机炸你。破坏性演练的成本并不高它不需要全自动化哪怕手动跑一遍也能把“上线即凉”的大部分场景提前暴露。我现在的个人规矩是任何一份 AI 代码我签收之前必须能在五分钟内说清楚“这个改动上线后可能影响哪三个具体的东西”。说不出来说明我对这次变更的影响面根本没想明白那就谈不上验收通过。跟 AI 协作了一段时间之后我最大的体会是人最该花精力的地方早就不是写代码了是想清楚什么叫做“做对了”。验收的标准定成什么样AI 就是什么角色——要么是听话的帮手要么是手艺很好的埋雷者。