1. 先搞清楚“AI 写的代码”到底处在什么位置
“AI 生成的代码敢直接上生产吗?”这个问题,我在团队里被问过不下十次。每次我的回答都一样:敢不敢上生产,跟代码是谁写的没关系,跟代码经过了什么检查有关系。人写的代码照样能把生产库删了,AI 写的代码也有跑得稳稳当当的。真正决定能不能上生产的,是你上线前那几道关卡有没有认真做。
先把一个误区掰开:很多人把“AI 生成代码”当成一个整体来讨论,其实它至少分三种情况,风险等级完全不同。
第一种是辅助补全型,比如你在 IDE 里敲了个方法名,AI 帮你把剩下的循环补完。这种代码你基本是逐行看过的,风险最低,本质上还是你在写,AI 只是个打字加速器。
第二种是整函数/整模块生成型,你给一段注释或者一个需求描述,AI 吐出来几十上百行。这种就需要你完整 review,因为它可能引入你根本没想过的依赖、边界处理或者性能问题。
第三种是Agent 自主执行型,AI 自己读代码库、自己改文件、自己跑测试。这种风险最高,因为它改动的范围可能超出你的预期,而且中间步骤你不一定都看到了。
我这次要聊的,是我自己项目里真实走的一遍流程:一个用 AI 辅助生成的 Java 服务模块,从“AI 吐出来”到“上线生产”,中间我做了四道检查。这四道检查不是什么高深的东西,但每一道都拦下过真实的问题。下面我把每一道拆开讲,包括为什么这么设计、具体怎么操作、以及我在实操中踩过的坑。
提示:本文说的“上生产”,指的是部署到真实用户会访问的环境。测试环境、预发环境不在此列,那些环境本来就该随便折腾。
2. 第一道检查:编译与静态分析,别让低级错误浪费你的时间
2.1 为什么第一道是编译而不是 review
很多人拿到 AI 生成的代码,第一反应是打开逐行读。我建议你先别急,先让它过一遍编译和静态分析。原因很简单:AI 生成的代码里有相当一部分问题是机械性的,比如引用了不存在的包、方法签名对不上、变量名拼错、泛型写错。这些问题你肉眼 review 也能发现,但效率极低,而且容易看漏。
让工具先跑一遍,把机械性问题全部清掉,你再去 review 剩下的逻辑,注意力才能集中在真正需要人判断的地方。这是我一直坚持的顺序:机器能查的交给机器,人只做人才能判断的事。
具体操作上,Java 项目我一般跑这几步:
# 1. 先编译,看有没有语法和类型错误 mvn clean compile # 2. 跑静态分析,我常用 SpotBugs + Checkstyle mvn spotbugs:check mvn checkstyle:check # 3. 如果有 SonarQube,直接跑扫描 mvn sonar:sonar2.2 静态分析到底在查什么
静态分析工具查的东西,大致分几类,我按对 AI 代码的命中率排个序:
| 问题类型 | 典型表现 | AI 代码命中率 |
|---|---|---|
| 空指针风险 | 调用了可能为 null 的返回值 | 高 |
| 资源未关闭 | 流、连接没放进 try-with-resources | 高 |
| 异常吞掉 | catch 块里什么都不做 | 中 |
| 并发问题 | 共享变量没加同步 | 中 |
| 硬编码 | 密钥、URL 直接写死 | 中 |
| 死代码 | 永远走不到的分支 | 低 |
我实测下来,空指针和资源未关闭是 AI 生成代码的两大高发区。原因也不难理解:AI 是根据上下文概率生成代码的,它倾向于写出“看起来合理”的调用链,但不一定知道你这个方法的返回值在什么情况下会是 null,也不一定记得帮你补上 close。
2.3 一个真实的拦截案例
有一次 AI 帮我生成了一个读取配置文件的方法,大概是这样的:
public String readConfig(String path) { BufferedReader reader = new BufferedReader(new FileReader(path)); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } return sb.toString(); }逻辑上没问题,编译也过。但静态分析直接报了资源未关闭。这个 reader 如果不在 finally 里关掉,在高频调用场景下会耗尽文件句柄。AI 没写 close,因为它生成的是一段“功能正确”的代码,而不是“生产可用”的代码。这就是为什么第一道检查不能省。
注意:静态分析会有误报,不要看到报错就无脑改。我的做法是先看规则名,再判断这个告警在当前上下文里是否成立。比如某些空指针告警,如果你能证明这个值不可能为 null,加个注解抑制掉就行,不用硬改。
3. 第二道检查:逻辑正确性,AI 最擅长“看起来对”
3.1 编译过了不代表逻辑对
第一道检查能拦下机械性错误,但拦不住逻辑错误。而逻辑错误恰恰是 AI 生成代码最危险的地方,因为它看起来太对了。AI 生成的代码通常结构清晰、命名规范、注释齐全,读起来很舒服,这种“表面质量”很容易让人放松警惕。
我踩过的一个坑:AI 帮我写了一个分页查询的方法,参数是 pageNum 和 pageSize,它内部算 offset 的时候写的是pageNum * pageSize。看起来没问题对吧?但我们的接口约定 pageNum 从 1 开始,所以正确的 offset 应该是(pageNum - 1) * pageSize。这个 bug 编译能过、静态分析查不出、单元测试如果只测第一页也发现不了。它是上线后用户翻到第二页发现数据重复才暴露的。
3.2 我 review AI 代码时重点看什么
经过多次踩坑,我总结了一个 review AI 代码的检查清单,按优先级排:
- 边界条件:空集合、null 输入、零值、负数、最大值,这些 AI 经常处理不全。
- 循环边界:是
<还是<=,是length还是length - 1,差一错误高发。 - 状态变更:方法有没有副作用,改了哪些共享状态,并发下安不安全。
- 异常路径:出错时是抛异常还是返回默认值,调用方能不能正确处理。
- 业务语义:这个逻辑符不符合我们的业务规则,AI 不懂你的业务。
其中第 5 点是最需要人的。AI 可以写出语法完美、逻辑自洽的代码,但它不知道“这个用户状态在业务上不允许被删除”这种规则。这类问题只能靠熟悉业务的人来把关。
3.3 用单元测试把逻辑钉死
review 完之后,我会针对 AI 生成的代码补单元测试。这里有个技巧:不要只测正常路径,重点测边界和异常路径。正常路径 AI 自己大概率是对的,出问题的地方往往是它没想到的那些情况。
@Test public void testPagination_FirstPage() { // 正常路径,AI 一般能过 List<Item> result = service.query(1, 10); assertEquals(10, result.size()); } @Test public void testPagination_SecondPage() { // 边界路径,这里曾经抓出过 offset 计算的 bug List<Item> result = service.query(2, 10); assertEquals(10, result.size()); // 验证第二页数据和第一页不重复 } @Test public void testPagination_EmptyResult() { // 空结果,AI 经常在这里 NPE List<Item> result = service.query(999, 10); assertTrue(result.isEmpty()); }我一般要求 AI 生成的代码,单元测试覆盖率至少覆盖到所有分支。不是追求那个百分比数字,而是通过写测试的过程,逼自己把每个分支都想一遍。
4. 第三道检查:安全与依赖,AI 不知道你的安全底线
4.1 AI 生成代码的安全盲区
安全这块是 AI 生成代码的重灾区,因为 AI 的训练目标是“生成能跑的代码”,不是“生成安全的代码”。它不知道你的安全规范,也不知道当前最新的漏洞库。我遇到过几类典型问题:
第一类是硬编码敏感信息。AI 有时候会把示例里的密钥、token 直接写进代码,因为它觉得这样“完整”。这个必须拦,生产代码里绝对不能出现任何明文密钥。
第二类是引入了有漏洞的依赖版本。AI 生成 pom.xml 或者 build.gradle 的时候,用的可能是它训练数据里的旧版本,那个版本可能已经有已知漏洞了。
第三类是输入校验缺失。AI 生成的接口方法,经常直接拿参数就用,不做长度校验、格式校验、范围校验。SQL 注入、路径穿越这类问题往往就从这里来。
4.2 依赖检查怎么做
依赖检查我用两个工具配合:OWASP Dependency-Check 查已知漏洞,mvn dependency:tree 看依赖树有没有冲突。
# 查依赖漏洞 mvn org.owasp:dependency-check-maven:check # 看依赖树,排查版本冲突 mvn dependency:tree -Dverbose依赖冲突这个问题在 AI 生成的代码里特别隐蔽。因为 AI 可能给你引入了一个新库,而这个库又传递依赖了某个你已经在用的库的不同版本,导致运行时行为和你预期的不一样。这种问题编译期发现不了,跑起来才炸。
我一般的做法是:AI 每引入一个新依赖,我都要在依赖树里确认它没有和现有依赖打架。如果打架,用<dependencyManagement>统一版本,别让它自己选。
4.3 输入校验不能省
对于 AI 生成的对外接口,我会强制加一层校验。Java 里我常用 Bean Validation:
public class QueryRequest { @NotNull @Min(1) private Integer pageNum; @NotNull @Min(1) @Max(100) private Integer pageSize; @Size(max = 50) private String keyword; }然后在 Controller 上加@Valid。这层校验看起来啰嗦,但它能挡掉大量脏输入。AI 生成代码时不会主动加这些,你得自己补。
提示:安全检查和依赖检查最好做成 CI 流水线的一环,每次提交自动跑。靠人记着做,迟早会漏。
5. 第四道检查:生产环境适配,本地跑通不等于线上能跑
5.1 环境差异是最大的隐形杀手
前三道检查都在代码层面,第四道检查是环境层面。这是最容易被忽略、但上线后最容易出事的地方。AI 生成的代码在本地跑得好好的,一上生产就各种问题,原因基本都是环境差异。
我列一下常见的环境差异点:
| 差异点 | 本地环境 | 生产环境 | 可能引发的问题 |
|---|---|---|---|
| 数据库 | 单机、小数据量 | 集群、大数据量 | 慢查询、连接池耗尽 |
| 配置 | 硬编码或本地文件 | 配置中心 | 配置读取失败 |
| 并发 | 单线程测试 | 高并发 | 线程安全问题 |
| 网络 | 直连 | 经过网关、负载均衡 | 超时、连接重置 |
| 资源 | 充足 | 受限 | OOM、CPU 打满 |
5.2 配置外置是底线
AI 生成的代码经常把配置写死在代码里,比如数据库地址、超时时间、线程池大小。这些必须外置到配置中心或者环境变量。我一般会检查这几类配置有没有外置:
- 数据库连接信息
- 第三方服务的地址和密钥
- 超时时间、重试次数
- 线程池、连接池的大小
- 开关类的功能标志
# 正确做法:配置外置 spring: datasource: url: ${DB_URL} username: ${DB_USER} password: ${DB_PASSWORD} redis: timeout: ${REDIS_TIMEOUT:2000}冒号后面的值是默认值,这样即使配置中心没配,也有个兜底。
5.3 并发和资源问题要专门测
AI 生成的代码在单线程下跑通很容易,但生产环境是多线程的。我一般会针对 AI 生成的核心方法做一轮并发测试,看看有没有共享状态被并发修改的问题。
@Test public void testConcurrentAccess() throws Exception { int threadCount = 50; ExecutorService executor = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { executor.submit(() -> { try { service.process(sharedData); } finally { latch.countDown(); } }); } latch.await(); // 验证结果是否符合预期 }这个测试不追求覆盖率,就是专门用来暴露并发问题的。AI 生成的代码如果用了非线程安全的集合、或者有竞态条件,这个测试大概率能抓出来。
5.4 上线前的最后一道人工确认
四道检查都过了之后,我还会做一件事:把 AI 生成的代码和人工写的代码分开标记,上线后重点观察。具体做法是在日志里加上标记,或者在监控里单独建一个看板。
这样做的目的是:如果上线后真出了问题,我能快速定位是不是 AI 生成的代码引起的。这不是不信任 AI,而是给自己留一条快速回滚和排查的路。实测下来,这个习惯帮我省过好几次排查时间。
6. 四道检查之外,我踩过的那些坑
6.1 过度信任 AI 的“自信语气”
AI 生成代码的时候,注释和命名都特别自信,读起来像是经过深思熟虑的。但实际上它可能只是概率上觉得这样写合理。我踩过最坑的一次,是 AI 生成了一段加密逻辑,注释写着“使用 AES-256 加密”,实际代码里 key 的长度根本不够 256 位。这种问题你不逐行核对参数是发现不了的。
所以我的经验是:AI 生成的代码,注释可以看,但不能信。注释说是什么,你得自己验证是不是真的。
6.2 忽略了 AI 的“版本幻觉”
AI 有时候会调用一些不存在的方法,或者用了某个库在新版本里已经废弃的 API。这在编译期能发现一部分,但有些是运行时才报错的。我的做法是:AI 生成的代码里,凡是调用第三方库的地方,我都去官方文档核对一遍方法签名和版本。麻烦是麻烦,但比上线后炸了强。
6.3 忘了检查日志和监控埋点
AI 生成代码时不会主动加日志和监控埋点,它只管功能实现。但生产环境没有日志和监控,出了问题就是两眼一抹黑。我一般会在 review 的时候专门检查:关键路径有没有日志、异常有没有记录、核心指标有没有埋点。这块 AI 帮不上忙,得自己补。
6.4 测试数据和生产数据不是一回事
AI 生成的代码在测试数据上跑得好,不代表在生产数据上跑得好。生产数据可能有脏数据、有极端值、有历史遗留的特殊格式。我一般会从生产环境脱敏拉一批真实数据,在预发环境跑一遍,看看有没有意外情况。这一步能抓出不少边界问题。
7. 我的最终结论:不是敢不敢,是流程到不到位
回到最开始的问题:AI 生成的代码敢直接上生产吗?
我的答案是:经过完整检查流程的 AI 代码,可以上生产;没经过检查的,不管是谁写的,都不该上。这四道检查——编译与静态分析、逻辑正确性、安全与依赖、生产环境适配——不是专门为 AI 代码设计的,它们本来就是任何代码上生产前都该做的。只不过 AI 代码在某些环节的出错率更高,所以这些检查更不能省。
我自己的实践是,AI 生成的代码大概能帮我省掉 60% 到 70% 的敲键盘时间,但 review 和检查的时间省不掉,甚至因为要额外核对一些东西,某些环节比纯手写还慢一点。但整体算下来还是划算的,因为 AI 帮我跳过了那些机械性的、不需要思考的部分,让我能把精力集中在真正需要判断的地方。
最后分享一个我一直在用的小习惯:每次 AI 生成的代码上线后,我都会记录一下它有没有出问题、出了什么问题。攒了一段时间之后,你会发现 AI 在某些类型的代码上特别容易出错,在另一些类型上则很稳。有了这个数据,你就能更精准地决定哪些代码可以让 AI 多写、哪些必须自己来。这比笼统地说“AI 代码行不行”有用得多。