在 AI 编程成为标配的今天我们面临一个尴尬的现实AI 写的代码上线后bug 数量不降反增。经过 30 天实验和 11 个线上 bug 的深度复盘我总结出一套五步审查法——不是教你如何逐行检查代码而是告诉你应该关注哪些 AI 永远无法理解的业务上下文。这套方法让我将 AI 生成代码的线上问题降低了 90%更重要的是它把审查经验变成了可复用的团队规范。摘要本文分享一套从 11 个线上 bug 中提炼出的 AI 代码五步审查法帮你将 AI 生成代码的线上问题降低 90%。核心要点第一关需求审查- 80% 的问题源于需求描述不完整必须明确并发模型、上游输入和失败语义第二关并发模式识别- 寻找“读-算-写”模式这是并发问题的根源需要针对性保护第三关事务边界检查- 确保远程调用不在事务内避免缓存一致性和消息投递问题第四关空指针防御- 重点关注外部来源字段而非盲目添加 null 检查第五关安全检查清单- SQL 注入、SSRF、脱敏、越权等安全漏洞的固定检查点这套方法不是增加审查时间而是将经验编码为规则让 AI 下次不再犯同样的错误。做完那个 30 天的 AI 实验之后我没有停下来。数据还在脑子里热乎着——编码时间降了 65%线上 bug 从 3 个涨到了 11 个——我最想做的一件事不是写总结是把这件事往深里再推一步。那篇东西聊的是“意识到要审”这件事有多重要。但那只是第一步。更扎心的问题是我知道该审但我到底审什么后面一个月我把之前踩的 11 个 bug 翻来覆去地拆每拆一个就逼自己问一句如果让我重新审这段代码我能不能在 3 秒内看出它有问题如果不能缺了什么两个月下来我整理出了一套专门针对 AI 生成代码的审查方法——不是那些网上随便搜的 Code Review 原则是具体到每一个步骤、每一类陷阱的东西。先说个结论可能有点反直觉AI 写的代码你审的时间至少应该是它写的时间的两倍。而且这不是效率低这是把钱花在了最该花的地方。AI 写一个接口 30 秒你花 1 分钟审——这是亏的。因为你在看它能写对什么不是找它可能写错了什么。AI 写一个接口 30 秒你花 5 分钟审——这是赚的。因为这 5 分钟里你在做 AI 永远做不了的事判断这段代码在你的系统里什么条件下会炸。不绕了说具体的。第一关不是看代码是先看需求一个你可能没意识到的真相AI 生成的代码80% 的问题不在代码本身在你给它的需求描述里。你试过没有你跟 AI 说“写一个积分扣减接口”然后你审它写的代码。方法名对、参数对、业务逻辑对——你审完了觉得没问题。上线炸了。问题在哪在你的需求描述里缺了最关键的信息。你脑子里默认知道但没写出来的东西这个接口的并发量是多少你不知道那这个需求就不完整。积分扣减失败了要不要补偿你不知道那设计就没完。流水记录失败能不能影响主流程你不确定那这个接口无法实现。AI 不是故意忽略这些。它是真的不知道。所以我现在的规矩是给 AI 需求之前先花 10 分钟做一个“需求评审”。不是那种正规的、写文档的评审。就三个问题写在 prompt 的第一段第一并发模型。这个接口同一时刻可能有多少个请求同时访问如果两个请求同时操作同一个数据你希望它们怎么处理排队乐观锁直接报错让客户端重试举个例子。同样是积分扣减如果你面对的是 C 端用户自己点“兑换”一个用户一秒钟点一万次——那你第一反应应该是做前端防重加幂等键而不是想数据库加行锁。因为行锁会把整个扣减链路拴死。但如果是 B 端运营批量发放积分一秒可能只有几十个请求——行锁就是最优解。AI 不知道你在做 C 端还是 B 端但你知道。审代码之前先审你自己的需求描述有没有把这个写进去。第二上游输入。每个字段的来源是谁可能为 null 吗有没有可能传空字符串有没有非法值这个不用想得很复杂就一句话“上游是前端表单用户可以不填”或者“上游是 MQ 消息消息体里的 userId 一定不为空”。你给 AI 的需求里加上这句话它生成的代码质量能提高一个数量级。不加——它默认所有字段都可能为 null然后给你塞几十个 null 判断看着烦改起来更烦。第三失败语义。这个操作失败了要不要回滚回滚到哪个边界流水记录算不算回滚范围这些不是代码问题是业务设计问题。设计不在 AI 手里在你手里。一个检验标准你给你的需求描述加完这三条之后能不能让你同事不看任何其他上下文理解这个接口要做什么如果不能AI 也不能。如果能AI 写的代码至少在方向上是不会错的。第二关找“读-算-写”这三个字需求审完了开始看代码。AI 生成的代码有一个共同特征——你不需要逐行读。你需要找模式。我审了两个月 AI 的代码之后发现所有线上炸出来的 bug 都遵循一个模式。三个字“读-算-写”。就是代码里出现这样一个片段先从某个地方读一个值数据库、Redis、内存然后对这个值做一个计算加减乘除、状态变更、条件判断然后再把这个新值写回去就说积分扣减那个 bug读积分、减积分、写积分。缓存一致性那个也一样读缓存、改数据、删缓存。这三个字出现在代码里的任何地方——不管前端后端不管 Java Python——都意味着这里对并发是敏感的。因为它们之间没有任何锁保护任何人在任何时间点插进来读到的都是旧值。所以我的审查节奏现在是这样的AI 代码生成完第一遍我根本不看逻辑。我就扫代码找“读-算-写”模式。找到了下一步才看逻辑。找不到——那这个代码可能简单到不需要并发考虑也可能是 AI 写得不够好。找到之后怎么办不是简单加个锁就完了。你要推演。拿一支笔推演两个并发请求同时打到这段代码时数据会怎么流转。推出来了你才知道该怎么保护。推不出来——这段代码你不敢上线。这里有一个所有“读-算-写”场景都绕不开的选择题。你的并发冲突多不多冲突少比如不同用户操作不同数据——乐观锁就够了。成本低只在真正冲突的时候重试。冲突多比如秒杀扣库存所有请求打同一行——乐观锁会引发大量重试浪费资源。行锁或 Redis 原子操作是更优解。冲突量你不确定先上用乐观锁加监控。三天数据出来重试率超过 5% 就换方案。AI 不会问你这个。它给你写的默认方案是它在训练数据里见得最多的方案——通常是乐观锁或者干脆没有锁。但你的业务需要什么方案只有你知道。你的职责不是审查它有没有写锁是判断它写的锁是不是“对”的。这层区别是 P6 和 P7 的区别。第三关跟着事务边界走一圈读过《阿里巴巴 Java 开发手册》的人应该都知道一条铁律事务方法里面不要做远程调用。AI 也知道。但它知道归知道写的时候大概率还是会犯规。因为“远程调用”的定义比你想象的宽得多。不只是 RPC。你发一条 MQ 消息——这是远程调用。你调了 Redis——这也是远程调用。你发了一封邮件、调了第三方短信接口、甚至写了一个文件到 NAS 上——这些都算。为什么不能在事务里做这些事务的本质是保证一组数据库操作要么全成功、要么全回滚。但 MQ 发出去的消息没法回滚Redis 里删掉的 key 没法撤销短信发出去了就是发出去了。AI 生成的代码最喜欢犯的错就是——把“数据库操作”和“非数据库操作”搅在一个 Transactional 里。上一篇文章里那个缓存一致性的 bug 就是这么出来的更新数据库和删除 Redis 在同一个事务里事务还没提交缓存已经被删了这个窗口谁碰谁倒霉。所以我审代码的第三关很简单找到 Transactional把它包住的每一行代码问一遍——这一行如果事务回滚了它能不能跟着回滚不能那就提到事务外面去。有一个很实用的套路在事务方法内部只做数据库读写。所有外部依赖——MQ、Redis、HTTP——全部放到事务后面。不是事务“里面”的后面是事务“外面”的后面。也就是说等 transactionTemplate.execute() 返回了确认事务提交成功了再去发消息、清缓存。你可能会问那如果消息发送失败了怎么办这是好问题。答案是——不要在同一段代码里同时处理“数据库提交”和“消息投递”两个生命周期。把消息发到一个本地事件表里然后用一个后台任务去扫表投递。这样数据库提交和消息投递就是两个独立的系统了。一个出问题不影响另一个出了问题也能追溯。这个思路在《凤凰架构》里有更系统的阐述感兴趣的可以去读。我这里只讲一个最朴素的版本——别让 AI 把两个不同生命周期的操作写在同一层事务里。这事 AI 永远不会替你做因为它根本不知道你的部署环境里哪些服务是“远程”的。第四关空指针——不是判不判的问题AI 在判 null 这件事上有一个很诡异的行为特征。它一旦被要求做防御编程就会给你塞一地的 if (obj ! null)每一个层级每一个字段事无巨细。你读着烦但不是 bug。真正危险的反而是它没判 null 的地方——那些你和 AI 都觉得“理论上不可能为 null”的地方。我踩过的两个空指针 bug 都是这个类型。一个是从 MQ 消息里取 userId。消息是大数据平台发的离线计算结果99.99% 的情况下 userId 都在。但平台偶尔升级的时候会发测试消息测试消息里没有 userId。线上炸了。AI 没判 null因为它不知道上游“偶尔”会发测试消息。另一个更隐蔽。一个 Service 方法里调了另一个 Service返回了一个 List。AI 直接 .stream() 了。但是上游的 Service 在某些异常分支返回的不是空 List是 null。AI 不知道上游有异常分支——它只知道正常路径上返回的是 List。AI 的盲区不是“它忘了判 null”。它的盲区是“它不知道哪里可能为 null”。因为它不知道你的系统里哪些字段是从 MQ 来的、哪些是从前端表单来的、哪些是从别人接口里透传过来的。它只能基于类型签名猜测——猜测必然出错。所以我审代码的时候有一条专门给 null 的规矩不看 AI 判没判 null只看所有从外部来源进来的字段——MQ、HTTP 请求、RPC 响应、数据库查询结果里的关联字段——有没有判。这里面有一个很容易被遗漏的RequestParam 和 RequestBody 里的非必填字段。AI 通常会判必填字段但不会判“用户可以不填、后端没做默认值”的字段。顺便说一句如果你用的是 Java 17 以上版本优先考虑 Optional 而不是 if (obj ! null)。不是说 Optional 更优雅——是因为 Optional 能让你在接口层面就把“这个字段可能为空”的信息编码进去。AI 读到 Optional 的时候它自动生成的代码也会正确处理空值不需要你每次审都提醒一遍。第五关安全——最容易漏的一条这一关放最后说因为大部分人在审 AI 代码的时候根本不会想起它。AI 生成的代码在安全意识上有一个非常特定的问题它会照搬“最流行的写法”。最流行的写法在安全性上通常是及格线但在一些特定场景下不够。我列几个你审代码的时候必须有意识去找的SQL 注入。AI 用 MyBatis 的时候几乎总是用 #{} 参数化这点它比很多人写得好。但它偶尔会在动态排序字段order by #{sortField}和 like 查询里翻车。审的时候搜一下 $ 符号看看每一个 $ 后面的变量是不是前端直接传过来的。只要有一个是——改掉。SSRF。AI 生成的文件下载、URL 请求、内网转发类的代码几乎百分之百会忽略 SSRF 校验。你要做的很简单看到 HttpClient、RestTemplate、OkHttp 这些关键字的时候问一句——这个 URL 是用户传过来的吗如果是做了域名白名单或内网 IP 过滤吗脱敏。AI 生成的日志里经常直接打印整个对象。如果这个对象里包含手机号、身份证、密码、Token——那就等于把这些东西写到了日志文件里而日志文件通常没有严格访问控制。审代码的时候看到 log.info(“user: {}”, user) 这种写法停一下确认 user 对象没有敏感字段。更好的做法是覆盖 toString() 或者用脱敏注解。越权。AI 生成的权限相关代码通常只校验“用户登录了吗”不校验“这个资源是不是这个用户的”。比如你让 AI 写一个“查询用户订单”接口它大概率会在 Controller 上加一个认证拦截但在 Service 层不做数据级权限校验——它不知道这个订单是不是当前用户的它只知道当前用户已经登录了。安全类问题有个特点——AI 默认会生成“能用但不安全”的版本。因为“安全”需要上下文——哪些数据是敏感的、哪些 URL 是不可信的、哪些资源应该是数据隔离的——而上下文在 AI 那里是不存在的。审安全这件事没有任何捷径。它就是需要你一个一个手动去查。好在这类问题通常有固定的检查点——SQL 注入搜 $、SSRF 搜 HttpClient、脱敏搜 log.info 和 toString、越权搜 userId 的来源判断。把这些关键字做成一个 check list审的时候当安检一样过一遍比凭直觉可靠得多。五关过完合起来是什么你可能觉得这五关有点多。审一个 AI 写的接口要审这么久那用 AI 还有什么意义意义在于——你审完以后可以把这个经验硬化成规则。怎么写规则很简单把你审出来的每一个问题点翻译成一条 prompt 命令写进 CLAUDE.md 或者 Codex 的 skills 配置里。比如缓存一致性那条你审了一次、踩了一次坑下次就可以在项目配置里写、、、、、凡是涉及缓存更新的操作删除缓存的代码必须放在事务提交之后不能在 Transactional 方法体内直接调 Redis。至少先用 TransactionSynchronization复杂的场景用延迟双删或 binlog 订阅。、、、、、积分类似、、、、、余额、库存、计数类字段的更新不允许使用先查后改的 SELECT UPDATE 模式。必须使用行锁SELECT FOR UPDATE或乐观锁version 字段。、、、、、你每踩一个坑就往这个配置里加一条。用不了三个月这个配置就是你跟 AI 协作的“团队规范”——你不需要每次都口头提醒 AI 注意缓存一致性了因为你已经在 prompt 里写死了规则。AI 照你的规范写代码你只需要确认它有没有遵守规范而不需要每次从头想象可能的 bug。这才是 AI 编程的正确打开方式——不是“我每次审得越来越细”而是“我把审出来的经验编码成规则让 AI 下次别犯同样的错”。一个经验丰富的老程序员和一个刚入行的新人的区别就在于老程序员脑子里装了一套规则而新人还在靠自己临场反应。用 AI 也是一样——你能不能把规则硬化出来决定了你是 P7 还是那个永远带 P6 的人。你可能看完这五关想问你花这么多时间审 AI 的代码为什么不让 AI 审你的代码因为这不是二选一。我审 AI 写的AI 审我写的——这两个动作恰好是互补的。AI 审你不会的格式、异常分支、边界遗漏你审 AI 不会的并发安全、缓存一致性、业务上下文。两个加在一起才是一个完整的过程。工具没有替代你的判断力。它替代的是你重复劳动的时间。省出来的时间不是用来摸鱼的是用来把这些高级判断做得更扎实的。如果这篇文章让你觉得审 AI 代码这件事值得认真对待不妨把它存下来下次审 AI 的 PR 之前打开对着过一遍。不管是哪一关只要多盯一眼就少一个线上 bug。下一篇文章已经在写了聊 MCP——怎么让 AI 不止帮你写代码还帮你查日志、调接口、发告警。不发教程只