
最近一段时间很多团队都在为“AI 编程”带来的效率提升兴奋。确实一个功能模块交给 AI 编程助手从生成代码到写出配套单测可能只需要几分钟。但随之而来一个新问题开始频繁出现在代码评审现场单测全绿代码却让人不敢合并。这不是段子。一位同事用 AI 写了一个订单价格计算模块单测 18 个用例全部通过看起来功能完全正常。可在评审时只翻了几分钟就发现函数直接修改了调用方传入的字典把原始订单数据悄悄改掉了折扣规则被硬编码在业务函数里测试一换数据就“绿”得毫无说服力。更严重的是一段数据库写入逻辑没有做事务处理一旦第二笔写入失败第一笔数据已经落库造成不可恢复的脏数据。最终得出的结论是这个模块必须推倒重来。AI 编程最大的危险不是它写不出代码而是它会快速产出一套“测试全绿、核心却烂掉”的代码让整个团队误以为项目正在健康推进。测试通过这件事本来应该带来安全感但在 AI 生成的代码里它反而经常变成危险的“信任状”。这篇文章不讨论模型选型也不聊哪个编程助手更强而是聚焦一个很多团队正在踩却还没意识到的陷阱当 AI 生成代码成为常态测试全绿与代码健康之间的鸿沟会被急剧放大。读完你会理解为什么功能测试全绿不能代表代码质量你会学会识别 AI 代码中常见的隐蔽问题你也会拿到一套包括测试设计、静态检查、代码审查、提示词约束在内的多层防线方案。1. 这篇文章真正要解决的问题传统的开发流程里测试全绿之所以有相当的说服力是因为代码是程序员一步一步写出来的测试通过至少说明“实现与预期行为一致”。但在 AI 编程的流程里这个推理链断了。AI 生成代码时模型并不理解你的业务上下文它是根据训练数据中的统计规律在预测最可能被接受的代码。它最擅长做的一件事情就是“让测试通过”。如果测试只覆盖了正常路径它就会只写出满足正常路径的实现如果测试断言没有检查副作用它就不会考虑副作用如果测试用例没有准备非法输入它默认就不处理异常。这意味着测试全绿可能只说明一件事AI 生成的代码恰好满足了测试作者的显式断言。代码是否可维护、是否安全、是否优雅、是否考虑了边界条件这些都不会在测试结果里体现。这篇文章要解决三个层面的问题认知层面理解“测试通过”和“代码健康”之间的本质区别。操作层面学会设计能逼出真实问题的测试以及如何通过代码审查和静态检查拦截“测试全绿但实现糟糕”的代码。工程层面建立一套适合 AI 协作时代的代码质量防线把 AI 的产出从“能跑”推到“能上线”。2. 为什么 AI 生成代码能“骗过”测试2.1 测试的本质是契约不是质量评估很多人把单元测试理解成“验证我的代码是否正确”。严格来说这个理解是有偏差的。单元测试更准确的定位是“契约验证”它验证的是调用方与被调用方之间约定的行为。比如有个函数add(a, b)测试里写了assert add(1, 2) 3。这个测试证明的只是“当参数是 1 和 2 时返回值是 3”它没有证明参数为负数时行为是否正确参数类型错误时是否抛出合理异常函数内部是否有不必要的高复杂度函数是否修改了传入对象函数是否依赖了全局状态。如果 AI 只看到那个assert它就能用最低成本让测试变绿。这本质上是一种“应试式编程”就像学生根据历年真题押题押中了就往答题卡上填答案但并没有真正掌握这门课。2.2 测试能发现什么不能发现什么这里需要把两类质量概念分开维度测试能覆盖测试难以覆盖功能正确性显式断言覆盖的正常值、边界值未编写的断言路径代码结构几乎无法从单测中发现依赖评审、静态分析、设计评审副作用仅在断言检查时发现调用方状态被意外修改时较隐蔽安全性极少覆盖注入、越权、明文存储依赖安全测试和人工审查性能简单性能测试可发现明显问题O(n^2) 复杂度在数据量小时不暴露可维护性完全无法从测试结果判断依赖代码评审和重构经验这个表格值得在团队里贴出来。它解释了为什么一个有经验的工程师在评审 AI 代码时会反复追问“这个函数有没有副作用”“这个常量为什么写在这里”“这个异常为什么不处理”。这些追问恰恰是测试结果无法告诉我们的。2.3 AI 编程中最常见的三个幻觉在接触大量使用 AI 编程的团队之后我发现有几个幻觉非常普遍而且危害不小。第一个幻觉测试覆盖到的代码一定是正确的。实际上测试覆盖率和代码正确性之间没有必然关系。一个测试用例只是给某一小段行为拍了一张快照覆盖率再高也只是一堆快照的集合。AI 可以写出一个分支覆盖率达到 90% 的垃圾实现每个分支都能通过但整体设计依旧是一团乱麻。第二个幻觉测试通过说明语义正确。这是 AI 编程里最隐蔽的问题。AI 产出的函数可能命名很规范、返回类型也对、测试也通过但实现的业务语义完全错了。比如把“订单总额”理解成“商品单价×数量”把“折扣后价格”理解成“折扣金额”测试都基于错误的语义来写测试反而锁定了错误行为。第三个幻觉重构和完善可以后面再做。很多开发者觉得先让 AI 把代码跑通测试全绿以后再重构。但现实是一旦测试全绿并合并进主干后续迭代会在这个错误地基上继续盖楼。AI 编程最大的隐性成本是把技术债的积累速度从“人写”提升到了“机器批量生产”。3. 一个订单模块案例测试全绿代码翻车为了更直观地说明问题我构造一个典型的 AI 编程场景。这个例子综合了很多团队评审 AI 代码时常见的问题。3.1 需求描述需求很简单给订单系统增加一个折扣功能。规则如下订单总额大于 1000 元打 8 折订单总额大于 500 元打 85 折订单总额大于 100 元打 9 折其他情况原价。同时系统需要一个函数apply_discount(order)接收订单字典返回折扣后的金额并让调用方能够拿到最终金额。3.2 单元测试看起来无懈可击测试代码先被写出来覆盖了四条规则并且额外加上了一个空订单的异常场景。# 文件路径tests/test_discount.py import unittest from discount import apply_discount class TestDiscount(unittest.TestCase): def test_order_above_1000(self): order {id: 1, total: 2000} self.assertEqual(apply_discount(order), 1600) def test_order_between_500_and_1000(self): order {id: 2, total: 800} self.assertEqual(apply_discount(order), 680) def test_order_between_100_and_500(self): order {id: 3, total: 300} self.assertEqual(apply_discount(order), 270) def test_order_below_100(self): order {id: 4, total: 50} self.assertEqual(apply_discount(order), 50) if __name__ __main__: unittest.main()这组用例覆盖了四条业务规则。运行之后四个测试全部通过。从结果看这是个“质量不错”的模块。3.3 AI 生成的实现测试全绿的“陷阱代码”以下是 AI 生成的实现# 文件路径discount.py def apply_discount(order): 计算订单折扣后的金额 if order[total] 1000: order[total] order[total] * 0.8 elif order[total] 500: order[total] order[total] * 0.85 elif order[total] 100: order[total] order[total] * 0.9 return order[total]运行测试输出结果.... ---------------------------------------------------------------------- Ran 4 tests in 0.002s OK测试全绿。但这份代码只要进入生产环境就是隐患。3.4 问题逐条分析问题具体表现为什么测试没发现实际危害副作用污染调用方直接修改传入的order字典测试从没检查调用后原订单数据是否变化调用方后续读取订单数据时得到的是被修改过的总价业务表现诡异折扣规则写死在业务函数折扣率是字面量测试只验证结果不验证实现结构后续调整折扣策略必须改代码无法配置化边界语义不清晰 1000与 1000的区别测试没有覆盖恰好等于 1000 的边界业务规则在不同口径下表现不一致缺少类型注解和返回值说明入参是dict返回float但没有标注测试不会检查类型标注大型项目中可读性和 IDE 提示能力下降最讽刺的是测试全绿给这个模块判了“健康”但真实项目里这类代码往往在集成阶段暴露问题而且问题定位成本很高调用方先要怀疑自己的逻辑查日志最后才发现是折扣模块把入参改了。这个排查过程在 AI 编程时代被成倍地放大。4. AI 代码“全绿陷阱”的四个典型表现从上面的案例延伸开我发现 AI 生成的代码在“测试全绿”的表象下经常表现出四类典型问题。这几类问题几乎贯穿了 AI 辅助开发的各个环节。4.1 应试式满足断言AI 会非常“贴心”地只实现测试需要的逻辑。如果测试只检查返回结果它就只用最直接的代码让结果正确如果测试没有检查输入校验它就不写校验如果测试没有检查异常路径它就不写异常处理。这种代码的特征是实现路径极其朴素甚至可以用“简陋”来形容。它会跳过防御性处理、跳过参数校验、跳过边界条件。因为对 AI 来说每一个多余的if都有可能引入 bug不如老老实实只满足断言这样测试全绿的几率最高。4.2 副作用入侵调用方前面订单案例已经展示了这类问题的典型形态。AI 生成代码时经常倾向于“就地修改”而不是“返回新值”因为就地修改的代码通常看起来更短也更容易通过测试。但它对调用方的影响是测试断言覆盖不到的。这类问题在 Python 的dict、list、对象传参以及 Java 的Map、List、对象引用中尤其隐蔽。如果测试只校验返回值不校验传入对象是否被修改那么副作用会一直潜伏到模块合并之后直到某个下游模块被坑了才发现。4.3 空洞代码制造绿灯另一种“全绿陷阱”是测试确实覆盖了某个行为但实现并没有真正完成该行为。一个典型的例子是“注册用户”功能测试用 mock 把数据库操作全部挡在断言之外结果 AI 生成的实现里连参数化查询都没有直接用 SQL 拼接。这类代码的特征是单测跑得很欢但拿到真实环境就崩。原因是测试与实现之间的耦合停留在“表面行为”没有触及“真实行为”。所谓“真实行为”应该是用户数据持久化、异常情况回滚、密码安全存储、重复注册拦截、参数合法性校验。这些行为没有进入测试断言AI 自然没有动力去实现。比如下面的 AI 生成代码单测能过但生产环境风险极高# 文件路径user_service.py import sqlite3 def register_user(username, password, email): conn sqlite3.connect(app.db) cur conn.cursor() # 危险SQL 拼接未做参数化密码也是明文存储 cur.execute( fINSERT INTO users (username, password, email) fVALUES ({username}, {password}, {email}) ) conn.commit() conn.close() return True如果测试执行时用的是 mock 数据库那么单测结果全绿但代码注入风险、密码明文风险、连接未关闭风险全都不会暴露。解决这类问题的核心思路不是让 AI 写更多代码而是让测试真正打到“行为契约”而不是“表面断言”。4.4 复杂度被测试掩盖AI 还有一个特点它很擅长写出“能跑但难维护”的嵌套结构。比如一个函数里塞了五层 if-else或者一个方法既做数据校验又做业务计算还做持久化。这样的代码单测全绿但后续维护的人看到它时会产生极大的心理负担。这种复杂度在测试阶段是隐藏的因为单元测试只关心输入输出不关心圈复杂度、函数长度、职责单一与否。只有把代码放进 Code Review放进静态分析工具复杂度才会暴露出来。5. 建立“防烂代码”的多层防线既然不能指望 AI 自己写出“测试全绿且设计优秀”的代码团队的工程防线就显得至关重要。这里给出四层防线每一层解决一类问题。5.1 第一层把测试定义得更接近“契约”既然 AI 是“应试型选手”那我们就该把考卷出得更有区分度。在设计测试时至少要做到以下几点检查副作用测试返回结果之后需要额外断言入参对象没有被意外修改。覆盖边界值所有和边界的临界值都要覆盖。覆盖异常路径非法输入、空值、超长字符串、依赖服务失败等场景都要有测试。检查返回值语义不仅要断言返回值还要断言返回对象的属性和类型。下面是为订单模块补充的“契约测试”# 文件路径tests/test_discount_contract.py import unittest from discount import apply_discount class TestDiscountContract(unittest.TestCase): def test_it_should_not_modify_input_order(self): order {id: 1, total: 2000} apply_discount(order) # 关键断言输入数据不应被修改 self.assertEqual(order[total], 2000) def test_order_exactly_1000_should_discount(self): order {id: 2, total: 1000} self.assertEqual(apply_discount(order), 800) def test_order_exactly_500_should_discount(self): order {id: 3, total: 500} self.assertEqual(apply_discount(order), 425) def test_order_exactly_100_should_discount(self): order {id: 4, total: 100} self.assertEqual(apply_discount(order), 90) def test_invalid_order_should_raise(self): with self.assertRaises(KeyError): apply_discount({id: 5}) # 缺失 total 字段当测试里加入了“不能修改入参”的断言时原先的 AI 实现立刻会失败。这就是“契约测试”的价值它逼着实现者重新设计方案而不是用副作用来走后门。5.2 第二层静态分析和质量门禁静态分析工具可以发现那些测试看不到的问题。比如 Python 的ruff、Java 的Checkstyle/PMD、JavaScript 的ESLint。这些工具不需要运行代码就能发现未使用变量、过于复杂的函数、不安全的 SQL 拼接线索等。一个干净的pyproject.toml配置示例# 文件路径pyproject.toml [tool.ruff] line-length 100 target-version py311 [tool.ruff.lint] select [E, F, W, B, C4, SIM] ignore [] [tool.ruff.lint.per-file-ignores] tests/* [S101]配合 CI 使用# .github/workflows/quality.yml 中的关键步骤 ruff check src tests ruff format --check . pytest --covsrc --cov-fail-under80 tests/这里把覆盖率门槛设在 80%只是一个示例。更重要的是把静态检查结果作为合并的前置条件如果代码没有通过质量门禁就不能合并到主干。这在 AI 编程时代尤其有意义因为 AI 产生的潜在问题数量已经超出了人工评审能逐一覆盖的极限。5.3 第三层进 Git 前的人工自检清单在代码合并前每个开发者都该形成一份快速自检清单。我自己在实际评审中经常用以下几个问题来问 AI 产出代码这段代码有没有修改传入参数或全局状态异常分支是否有明确处理策略有没有把不该写死在业务函数里的规则硬编码如果切换到真实数据库 / 真实网络这段代码还能跑通吗命名是否准确表达业务语义有没有过度设计这些问题表面上很基础但如果是人工一行行写出代码大多数时候不会犯前面那种“直接改入参”的错误。可当代码由 AI 快速产出后程序员往往只关注“功能是否实现”自检意识会被极大地削弱。5.4 第四层用提示词约束 AI 的生成行为最后一种防线是从源头控制不让 AI 自由发挥而是用提示词把工程约束写进去。这一点在下一章节展开。6. 用高质量提示词让 AI 交出更靠谱的代码提示词质量基本决定了 AI 生成代码的天花板。很多人觉得提示词就是“用大白话描述需求”这没错但要写出工程上可用的代码提示词需要包含约束和上下文。6.1 一个低质量的提示词看这个提示词写一个计算订单折扣的函数总价大于1000打8折大于500打85折大于100打9折。这个提示词缺乏几乎所有的工程上下文。AI 大概率会生成出前面那种“直接修改入参”的代码。它没有被告知不能修改入参没有被告知折扣规则要可配置没有被告知需要类型注解也没有被告知要覆盖边界测试。6.2 一个高质量的提示词模板一个高质量的提示词至少要包含“角色、约束、输入输出约定、测试要求、边界条件”五个部分。你是熟悉 Python 的资深后端工程师请帮我实现订单折扣模块。 需求 1. 订单总金额 total 1000 时折扣率 0.8 2. total 500 时折扣率 0.85 3. total 100 时折扣率 0.9 4. 其他情况折扣率 1.0。 约束 1. 不允许修改调用方传入的字典或对象计算结果请用新的数据结构返回 2. 折扣规则抽成独立函数方便以后配置化 3. 所有函数必须有类型注解和 docstring 4. 需要包含返回结构的设计说明。 输出要求 1. 先给出函数签名和设计说明再给出实现 2. 写出单元测试覆盖 100、500、1000 的边界情况 3. 测试中必须断言输入对象未被修改。 请按以上要求输出完整代码。在约束中加入“不允许修改入参”“覆盖边界测试”“先设计说明再给实现”这些条件后AI 生成代码的方式会完全不同。它不再只是一个计算函数而是一个有返回结构、有独立规则函数、有边界测试的模块。6.3 分步生成比一步到位更可靠还有一个值得养成的习惯分步让 AI 工作而不是一次性交付全部功能。合理的节奏是先让 AI 给出模块设计和函数签名。审查设计确认没有副作用和职责混乱。再让 AI 按设计生成实现。最后让 AI 生成测试并执行测试。这种“分步生成 中间审查”的方式比直接让 AI 输出完整项目更可控。因为每一步的产物都较小人类审查的负担也较低问题可以在早期被发现。7. 常见问题与排查方法在 AI 编程的实际使用中团队碰到的问题往往有规律可循。下面整理一份高频问题排查表既可以用于日常自查也可以作为新成员上手的快速参考。问题现象可能原因排查方式解决方案测试全绿但生产环境数据异常副作用修改了入参或全局状态在测试中增加“调用后检查入参是否变化”的断言重构为返回新对象禁止修改入参单测通过但接口返回 500真实环境未 mock 的依赖报错查看异常日志检查数据库和网络连接增加统一的异常处理补充失败路径测试SQL 注入风险AI 生成时使用字符串拼接 SQL静态分析扫描execute调用改为参数化查询并把检查接入 CI 门禁代码圈复杂度高难维护一个函数承担了太多职责使用静态分析检查圈复杂度按职责拆分函数并使用提示词要求“单一职责”测试只覆盖正常路径测试用例只写了 happy path补充边界值、空值、异常输入用例将边界用例纳入团队测试规范折扣规则调整需要改代码业务规则被硬编码Code Review 关注是否有魔法数字规则配置化或独立函数化测试全部通过但合并后功能相互影响模块间隐式共享状态检查是否有全局变量或共享可变对象使用依赖注入避免隐式全局状态这张表并不完整但它提供了一个思路遇到 AI 编程相关问题先从“测试是否覆盖了真实契约”入手而不是急着修业务逻辑。8. 工程实践建议8.1 团队协作三原则原则一测试全绿只是起点不是终点。团队要把“测试全绿”当成一次性质量检查通过而不是最终结论。合并一个 PR 之前除了测试至少还要有静态检查、代码评审、自测记录这几个步骤。原则二AI 生成代码比手写代码更需要评审。有些团队会觉得AI 生成的代码反正通过了测试评审可以放松一点。这种想法非常危险。正因为 AI 不了解业务上下文它的代码在隐含假设上出错的概率反而更高。人类评审在 AI 编程时代的价值并不仅仅是“看格式”而是验证“这段代码是否真的在实现业务目标”。原则三把质量约束写进流程而不是靠个人自觉。如果团队依赖每个人自觉检查副作用那注定会漏。更好的方式是把 check 项做成自动化工具比如 PR 模板里强制要求填写“副作用自检”“依赖变更说明”“测试边界覆盖”等字段。8.2 为 AI 代码建立专门的评审环节代码评审已经够累了为什么还要给 AI 代码单独设一个环节因为普通代码评审是“读代码找 bug”而 AI 代码评审首先要做的是“判断这段代码是否真的是这个项目的正确解法”。两者侧重点不同放在一起容易混乱。比较实用的做法是让 AI 生成的代码先经过一次“设计评审”确认模块划分、数据流、接口设计合理然后进入常规代码评审检查语法、风格、测试、边界如果条件允许在合并前跑一次静态分析和安全扫描。8.3 持续收集“反模式”并沉淀进团队知识库每个团队都会碰到自己的 AI 编程反模式。比如“AI 生成了一个函数但修改了全局配置”“AI 把测试写成了一堆空断言”等等。这些反模式第一次遇到时可能花几个小时才能定位。建议团队建立一个“AI 代码反模式清单”每次评审发现问题就追加一条并配上简短的示例和解决方案。长期来看这个知识库的价值会超过任何单一工具。因为它是团队专属的沉淀了对业务和工程环境的理解AI 模型不一定知道。9. 总结与下一步AI 编程给开发效率带来的提升是真实的但“测试全绿、代码烂掉”的风险也是真实的。问题的根源不是 AI 不够聪明而是测试的本质决定了它无法证明代码健康。当生成代码的主体从“人”变成“模型”之后这个缝隙被急剧放大团队必须用更严格的测试、更清晰的提示词、更可靠的质量门禁来重新缝合它。对开发者个人来说下次让 AI 写完代码并看到测试全绿之后不妨再多问自己几个问题这段代码有没有修改调用方的状态边界条件覆盖了吗异常路径处理了吗如果答案里有“不确定”那么恭喜你你已经比只会看测试结果的人多走了一步。对团队来说把“测试全绿”从终点改回起点是 AI 编程时代质量建设的第一件事。如果下周只做一件事可以挑一个 AI 生成的模块用本文第 5 章的四层防线做一次质量评审。把“测试全绿但代码烂”的问题在合并前拉出来比任何制度宣贯都管用。