带着AI去做代码审查,扫一个2022年落地的Java老项目,AI一口气挑出来20个坑,站在旁边的老炮工程师一条一条过,最后只签收15个。剩下5个不是AI说错了,而是它不懂"这个项目为什么长成这样"。这个场景这两年越来越常见:AI代码审查确实能把人从逐行读代码里解放出来,但真正决定审查质量的,仍然是人。
这次实战用的项目是一个典型的2022年Spring Boot工程,JDK8,Maven多模块,代码风格带着那个年代特有的味道:Controller厚、Service薄、日期用Date和LocalDateTime混着来、异常处理靠try-catch吞。写这篇东西,主要是想记录一下AI代码审查的完整流程:AI怎么用、结果怎么筛、老炮的判断逻辑是什么,给同样打算拿AI审老代码的人一份可以直接抄的作业。
1. 项目整体设计与思路拆解:AI审查老代码,到底选什么姿势
1.1 为什么直接让AI扫全量代码不靠谱
很多人一上来就把整个项目压缩包丢给AI,让它"全面审查",结果要么被上下文长度卡住,要么AI只看了一个大概就开始编。老项目动辄几万行代码,模型窗口根本装不下,AI为了回答你,会产生幻觉,把根本不存在的行号报给你,或者把A类的问题安到B类头上。我这次实际跑下来,直接投喂整个模块的效果极差,输出里充斥着"这里建议使用Stream优化""那里建议引入Optional"之类的通用话术,真正有价值的并发和资源泄漏问题反而被稀释了。
正确的用法是把它当成"结对审查的初级工程师":一次给一个模块,给足上下文,让它在一个有限范围内充分发挥。AI不需要看完全部代码才能发现问题,它更需要的是"这一段代码在什么场景下被调用、依赖了哪些对象、有没有并发访问"这些精准信息。给足这些,它给出的判断质量会有质的提升。
1.2 三种落地姿势对比:全量投喂、模块切片、静态工具加AI复核
我后来把AI代码审查的常见姿势整理成了三种,各有适用场景:
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 全量投喂 | 操作最省事,丢进去就跑 | 上下文溢出,幻觉率高,问题流于表面 | 几百行的小工具类、单文件 |
| 模块切片投喂 | 上下文可控,幻觉率低,能审出跨方法问题 | 需要提前拆代码、写上下文说明 | 老项目审查的主力方式 |
| 静态工具扫描加AI复核 | 自动扫描覆盖全,AI做解释和排序 | 工具本身噪音大,仍需人工过滤 | 成熟团队、长期维护的代码库 |
老项目最适合第二种。先把一个包或者一个核心类拿出来,连同周边调用关系告诉AI,让它在这个范围里做深入检查,再把各个模块的输出统一汇总。这样做的好处是:AI每一次的分析都是完整的,不会因为上下文被截断而漏掉关键链路。
1.3 老炮复核是过滤层,不是审核层
AI擅长的是模式识别:空指针、并发问题、资源泄漏这些有固定套路的,它一眼就能看穿,甚至比很多人快得多。但AI没有业务背景,不知道这个接口一个月调用几次,不知道这段代码明年就会被重写,也不知道团队为什么在命名上坚持某种"不标准"的风格。老炮的工作不是把20个问题重新审一遍,而是用"线上会不会出故障、现在要不要动、改了会不会引入新问题"这三个维度做过滤。
用一句话概括:AI给的是可能性,老炮给的是决策。AI负责把所有可疑的点全部挖出来,哪怕挖多了也没关系;老炮负责判断哪些真正值得进入修复计划,哪些只需要记录归档,哪些干脆划掉。两者配合,审查效率和准确率都比单靠人肉逐行读高得多。
2. 核心细节解析:AI挑出的20个坑,逐一过一遍老炮的验收逻辑
2.1 被认可的15个坑:哪类问题是AI真正抓得准的
把20个坑摊开看,会发现一个规律:老炮签收的15个,全部是"模式和套路明确、一旦触发就是真实故障或显著劣化"的问题。这种问题AI一抓一个准,因为它们大量出现在训练数据里,特征非常标准化。我按当时的审查清单整理如下:
| 编号 | 问题位置(示例) | AI给出的结论 | 风险等级 | 老炮认可理由 |
|---|---|---|---|---|
| 1 | UserService.getUserById | 返回null未判空,调用链存在NPE | 高 | 线上接口会因为一条脏数据直接500,必须提前兜底 |
| 2 | OrderServiceImpl.listByPage | 循环内逐条查数据库,典型的N+1查询 | 高 | 列表接口数据量过百以后会明显变慢,属于性能债 |
| 3 | DateUtils | SimpleDateFormat定义成static字段且被多线程共享 | 高 | 偶发性时间解析错乱,是最难排查的那类并发问题 |
| 4 | ReportGenerator.export | 在for循环里用加号拼接大字符串 | 中 | 循环量不大时不致命,但数据量上来会频繁GC |
| 5 | FileLoader.load | InputStream没有用try-with-resources | 高 | 文件句柄泄漏,开发环境看不出,压测或长期运行就暴露 |
| 6 | Goods.equals | 重写了equals但没重写hashCode | 中 | 放入HashSet或HashMap后去重失效,行为不可预期 |
| 7 | VipService.check | synchronized锁了整个方法,里面还有远程调用 | 中 | 锁粒度太大,高并发下吞吐直接掉一大截 |
| 8 | TaskExecutor | 用Executors.newFixedThreadPool创建线程池 | 中 | 无界队列,任务积压到一定程度就会OOM |
| 9 | PayService.pay | 事务方法内部用this调用,@Transactional失效 | 高 | 数据不一致是所有支付类系统的红线 |
| 10 | RemoteClient | 所有HTTP接口没有设置连接和读取超时 | 高 | 依赖方一旦变慢,线程池集体阻塞,可能引发雪崩 |
| 11 | PriceUtil.compare | BigDecimal用equals比较大小 | 中 | 1.0和1.00不相等,金额比较会给出错误结果 |
| 12 | CacheManager | 用HashMap做多线程读写的缓存 | 中 | 并发扩容可能形成链表环,属于JVM里的著名事故 |
| 13 | ExceptionAspect | 捕获异常后只打印e.getMessage() | 中 | 没有堆栈等于没打日志,线上问题只能靠猜 |
| 14 | OrderController | 几百行业务逻辑写在Controller里 | 中 | 代码腐化的起点,后续测试根本无从下笔 |
| 15 | StatusConstants | 状态值到处硬编码魔法数 | 低 | 改一个状态要全局搜索,确实容易出事 |
这15个里没有一个需要AI懂业务。它不需要知道订单是什么、VIP是什么,光是"代码出现了什么结构"就足够判断了。这也是AI代码审查最可靠的部分:结构化缺陷。这类问题用静态工具也能扫出一部分,但AI的厉害之处在于它能把"跨方法的调用关系"串起来,例如第9条事务自调用,单纯看类文件很难发现,AI把代理机制和调用链一结合,问题就浮出来了。
2.2 AI报的另外5个"坑":为什么老炮不认
再来看看被否掉的5个。这几个更微妙——从纯代码角度看,AI说得不算错,但老炮基于项目实际把它划掉了。我把当时的争议列一下:
第一,AI认为"类名和包名不符合最佳实践,需要按DDD重构"。项目是一个内部管理系统,包名用controller、service、dao、entity这套经典分层已经跑了两年,团队没人觉得它阻碍开发。此时做DDD重构,动的是全工程,风险远大于收益。AI看的是"理论上的整洁",老炮看的是"动这一刀要流多少血"。
第二,AI建议"两层if-else应该用策略模式替换"。它还贴心地给出了三个类的示范代码。但那段逻辑就两个分支,而且业务上几乎不可能再加第三种。强行上策略模式,等于用一个复杂的框架解决一个简单的问题,后面来维护的人大概率会骂人。设计模式是用来消除重复和分支爆炸的,不是用来给代码上装饰的。
第三,AI要求"所有public方法补齐Javadoc"。实际方法名和参数名已经表达得很清楚,比如getOrderAmountByUserId,看一眼就知道是什么。AI把可读性和写注释混为一谈,老炮最烦这种为注释而注释的建议。注释应该解释"为什么",而不是复述"做了什么"。
第四,AI认为"某查询没有加索引,建议加索引"。那条SQL对应的表只有几千行数据,查询频率一天几百次,加索引后的收益约等于零,还白占存储空间、增加插入开销。AI在训练数据里见过太多"慢查询加索引"的案例,但它不知道这张表的量级,所以给出的方案属于典型的过度优化。
第五,AI提出"代码里出现了Date和SimpleDateFormat,要求全部迁移到java.time"。项目是JDK8,大部分时间操作已经用了LocalDateTime,剩下的Date出现在对接第三方老接口和Excel导出的历史代码里。为了现代化去动稳定代码,业务价值为零,回归风险却真实存在。这属于"正确但不做"的典型。
这5个不是AI蠢,而是它没有成本概念、没有历史包袱概念、也没有"这个项目的下一任维护人是谁"的概念。AI只能比较"理论最优"和"当前代码",老炮比较的是"改了会怎样"和"不改会怎样"。
2.3 老炮筛选问题时的三把尺子
真要说老炮凭什么只认15个,核心是三把尺子:
- 线上故障尺子:这个问题如果不改,会不会在特定场景下直接导致线上报错、数据错乱、系统不可用?会,就进名单。NPE、事务失效、线程池无界队列,都属于这一类。
- 改动成本尺子:修复这个问题要改多少文件、会不会牵连历史逻辑、有没有现成测试保护?如果改动成本高于潜在损失,优先级就往后放,甚至直接划掉。
- 未来演进尺子:这段代码是不是马上要重写?如果半年内会迁移到新系统,现在花精力去优化它就是在浪费子弹。
AI永远不会自动评估这三把尺子,因为它缺两个关键输入:当前系统的用户规模、业务生命周期。老炮的真正价值,就是把这些外部信息补进来,把AI的输出从"建议列表"变成"决策列表"。这也解释了为什么同一份AI报告,在不同团队手里会得出完全不同的整改清单。
3. 实操过程与核心环节实现:从代码库到问题清单的完整流水线
3.1 准备工作:先把老项目"切"成AI能处理的小块
实际操作中,我不会直接把整个Maven工程喂给AI。原因是上下文装不下,装下了也容易在还没看到关键类时就开始输出。我的标准流程是:
- git checkout 出要审查的那个发布分支,确保代码状态可复现。
- 先用IDEA的Inspections和SonarQube跑一遍,记录所有非风格类告警作为基线。
- 按模块拆代码:一个service包、一个controller包、一个工具类集合,分别打包成片段。
- 对每一个要审查的核心类,把类名、职责、主要依赖、调用方关系整理成一两句话,随代码一起给AI。
这一步决定了后续整个过程的质量。你给AI的信息越结构化,它返回的问题就越集中。你只是丢一堆文件让它"看",它就会东一榔头西一棒子,最后给你一堆通用废话。别嫌准备工作烦,它花费的时间最后都会从筛选环节省回来。
3.2 审查提示词:决定AI当"流水线工人"还是"理论派顾问"
AI代码审查的效果,一半靠提示词。下面是我当时用的模板,你可以直接抄:
你是一位有10年经验的Java架构师,正在参与一次代码审查。 请只审查我提供的代码片段,忽略命名风格和缩进等纯格式问题。 请聚焦以下五类问题: 1. 正确性:空指针、逻辑错误、异常处理不当 2. 并发:线程安全、锁粒度、共享可变状态 3. 性能:N+1查询、循环内耗时操作、资源泄漏 4. 可维护性:过长方法、重复代码、明显坏味道 5. 安全:SQL注入、路径遍历、敏感信息泄漏 对每个问题,请严格按以下格式输出: - 位置:引述代码原文或方法名 - 问题:一句话说清是什么问题 - 严重级别:高/中/低 - 影响场景:什么情况下会触发 - 修复建议:给出最小改动方案 如果你不确定某个建议是否适用于当前场景,请单独标注[需人工确认]。 不要为了凑数量而提出没有把握的问题。这个模板有几个关键设计。明确要求"忽略命名风格",AI就不会把一半输出浪费在命名和缩进上;要求"不要凑数量",能压制AI为了显得勤快而硬凑的冲动;要求"引述代码原文"而不是报行号,能大幅降低编造行号的概率,因为行号模型真的记不住,代码片段它反而能准确复读;最后要求"最小改动方案",AI就不会一上来就给你重构一个全新的设计。
3.3 喂代码与采集结果:用最小改动换取最大覆盖
我的做法是一次喂一个类,最多附带一两个关联方法。比如审UserService的时候,把UserMapper接口的定义和getUserById的SQL注解一起给过去,AI就能判断"返回null后调用方判不判空"这种跨层问题。只给一个类文件,AI只能在这个类内部找事,很多真正的问题在调用链上。
一个小技巧:有对比例子的时候,效果会翻倍。我先给AI看一段没有问题的同类代码做基线,再给它看要审查的代码,让它"找出两段代码的差异"。实测下来,这个方式比直接让AI找bug误报率更低,因为AI会真的去对比差异,而不是基于模糊记忆开始自由发挥。审查老代码的时候,你甚至可以拿新写的模块当对照,让AI看看老模块缺了什么防护。
采集完每个模块的输出后,统一汇总到一张问题清单里,去掉重复项、合并同类项,就得到了最初的20个问题。这个阶段先不删任何东西,让AI把所有想法说完。宁多勿漏,过滤的事后面再说。
3.4 交叉验证与人工决断:静态工具、AI、老炮三方对质
20个问题出来之后,关键一步是和静态工具的结果做交叉比对。我当时的做法分四步:
- 把AI报的每条问题,在IDEA和SonarQube里重新定位,看工具能不能扫出来。
- 工具能扫出来且AI也报了的,属于"大概率真实",直接进入处理队列。
- 工具没扫出来但AI报了的,单独开会讨论。这类往往是AI真正提供增量价值的地方,比如循环内远程调用无超时、事务自调用失效,静态工具很难从语法层面识别。
- 工具报出来但AI没报的,通常是风格类和命名类问题,这类不需要AI,统一过滤掉。
最后老炮拿着一份汇总表逐个打勾或画叉。打勾的进修复计划,画叉的写一行理由归档。这个过程非常快,因为三把尺子已经心里有数,大多数问题几秒钟就能下判断。
当时最终形成的表格字段大概是:ID、所属模块、问题描述、AI级别、工具是否检出、老炮结论、结论理由、修复负责人、计划版本。列不列完整清单不重要,重要的是这个结构能让所有争议有据可查,而不是开会的时候凭印象吵。
4. 常见问题与排查技巧实录:AI代码审查避坑指南
4.1 五种最典型的AI误报,见过一次就会分辨了
除了这次实战,我后面又拿其他项目试了几次,发现误报类型高度可预测,基本是下面这五类:
- 理论正确但实践无益:比如"用Optional替代所有null判断""所有集合改成不可变集合"。这类建议本身没错,但放进老项目里就是给自己找麻烦,改出一堆编译错误还要擦屁股。
- 把设计决策当缺陷:老代码里出现全局变量、静态缓存,很多时候不是作者不懂,而是在当时的技术选型和并发模型下,这是成本最低的妥协。AI会把这些当成坏味道,但它看不到背后的约束。
- 把风格当规范:命名规则、注释数量、方法长度阈值,每个团队有自己的尺度。AI默认的是"主流开源项目的平均数",不等于你们团队的代码标准。
- 把单一场景当巨大风险:AI看到一个没有索引的查询,就会联想到千万级数据量。但你心里要清楚,这个表可能永远只有几百行。场景不匹配,建议就失去了意义。
- 幻觉式报错:AI混淆了类名、方法名,甚至报出了项目里根本不存在的文件。这类问题必须靠代码定位来甄别,不能直接采信。
4.2 如何快速判断一个问题"值不值得改"
给所有踩坑的人一条实用建议:不要问AI"这是不是问题",要问自己"这个问题不修,最坏会怎样"。如果最坏情况是"线上偶发500,且可以快速重启恢复",那可以下个版本修;如果最坏情况是"资金数据错了还没告警",当天就得上线修复。
为了把这个判断做得更稳,我有意用了一个很朴素的优先级公式:
优先级 = 故障发生概率 x 故障影响范围 ÷ 修复成本
概率和影响范围主要靠拍脑袋,但这个公式能让讨论聚焦在"三个数字分别打几分"上,而不是停留在"AI说很严重"这种情绪层面。AI给出问题之后,我们一般先给前两项打分,再讨论修复成本,分数高的进迭代,分数低的直接归档。这套做法在实际运转中比"按严重级别排序"更贴近老项目的真实需要,因为有些中等级别的问题修复起来非常便宜,顺手就改了,有些高等级问题却因为改动面太大,只能排期。
4.3 给AI代码审查写提示词的几条经验
这部分是我踩了几次坑之后总结出来的,每一条都对应过实际翻车现场:
- 先声明"忽略代码风格",否则AI一半输出都在说命名和缩进。
- 明确要求"不要凑数量",AI为了显得勤快会硬凑,这条能减少两成噪音。
- 给足上下文:把类和调用方一起喂进去,AI才有判断跨方法问题的基础。
- 要求最小改动方案:AI默认倾向"重构成最优设计",你要逼它给出最小改动,它才会更贴近老项目实际。
- 分批处理,别贪多:一次只审一个模块,效果远好于一次给十个文件。
- 最后让AI标注"需人工确认":它不确定的时候会主动说出来,而不是硬撑着给一个错误结论。
4.4 我的个人体会:AI是副驾,方向盘从来都在人手里
最后聊点个人感受。我见过两种极端:一种是把AI输出当成天条,逐条整改,结果项目被搞成四不像,改完还说不清收益在哪;另一种是看了几眼AI报告就扔进回收站,觉得全是形式废话。我认为正确的是第三条路:让AI做大量低成本的初筛,把那些一眼就能判断的机械问题处理掉,把剩余的高价值问题留给有经验的人做决策。
老炮的15个对AI的20个,差的5个并不是AI无能,而是它看不到业务的生命周期。AI代码审查真正的价值,不在于抓住每一个bug,而在于把那些你早就习以为常的坏味道重新摆到台面上,逼你在"改"与"不改"之间做一次认真选择。有些问题你天天看,早就免疫了,AI以一个新人的视角重新指出来,你才意识到"原来这里一直埋着雷"。
这个工作流我现在已经沉淀成了固定套路:先静态工具扫基线,再AI模块化初筛,然后老炮拿三把尺子逐条过滤,最后顺手把过滤理由写进清单里,给后面的维护者留个上下文。这套流程跑下来,AI可能还是给出20个问题,但最终落到修复计划里的每一个,都是经得起推敲的。如果你也在做类似的事情,我给的最朴素建议是:永远让AI做充分的输出,永远由人类做克制的决策。