AI 生成的代码有个共同特征:读起来比写起来顺。命名规范、注释齐全、结构工整,评审时一眼扫过去挑不出毛病。问题在于毛病不在字面上,在你没去跑的那条路径上。
下面这套流程解决一件事:怎么在不把每行代码都重读一遍的前提下,判断这段生成代码能不能进主干。
1. 缺陷分布:它们通常错在哪
| 缺陷类型 | 外观 | 典型表现 | 发现方式 |
|---|---|---|---|
| 边界未处理 | 主路径完美 | 空集合、null、0 条记录、超长字符串时崩 |
构造边界输入跑一次 |
| 异常被吞 | try-catch 齐全 | catch (Exception e) { log.warn(...) } 然后返回默认值 |
看 catch 块是否有重抛或状态变更 |
| 依赖不存在的方法 | 调用链看着合理 | list.stream().first()、Optional.getOrElse() 这类不存在的 API |
编译 |
| 配置项名字编造 | 配置写得像真的 | spring.datasource.hikari.max-pool-size(真实键名不同) |
启动 + 核对官方键名 |
| 日志与错误信息空泛 | 有日志 | log.error("error occurred"),无 id、无入参 |
搜日志语句里的占位符数量 |
| 版本不匹配 | 语法正确 | 用了 JDK 21 的 API 而项目是 17 | 编译或核对 pom.xml |
最值得注意是第四类:配置项名字编造。它编译通过、测试通过(测试里没加载那段配置),只在真实启动时暴露,而且报错信息往往指向别处。
一个经验判断:生成代码里"能编译但从未被执行"的行,是缺陷密度最高的地方。所以检查的重点不是读全量代码,是把没跑过的分支逼出来跑一次。
2. 五个检查点
按性价比排序,前两个能拦掉大部分问题。
| 检查点 | 具体动作 | 不做会怎样 |
|---|---|---|
| 编译与测试是否真跑过 | 要命令原文 + 输出尾部,自己再跑一遍 | 它说"已测试通过",实际没跑 |
| 外部调用的错误路径 | 逐个看超时、重试、降级、返回码分支 | 上线后第一个 500 就是它 |
| 并发与资源释放 | 看锁粒度、线程池来源、try-with-resources/finally |
泄漏或死锁,压测才显形 |
| 与既有约定一致性 | 命名、包结构、返回体、异常体系 | 能跑,但破坏仓库一致性 |
| 有没有引入新依赖 | git diff 看构建文件 |
供应链与体积成本无人评估 |
2.1 外部调用是重灾区
生成代码对"外部调用可能失败"的建模普遍偏乐观。逐条核对:
// 生成版本:超时、重试、降级全缺
public Price getPrice(String sku) {
return restTemplate.getForObject(url + sku, Price.class);
}
// 验收要求:显式超时 + 失败语义 + 可观测
public Price getPrice(String sku) {
try {
return restTemplate.exchange(url + sku, GET, entity, Price.class,
Map.of("connectTimeout", 300, "readTimeout", 800)).getBody();
} catch (RestClientException e) {
log.warn("price_fetch_failed sku={} err={}", sku, e.getClass().getSimpleName());
return Price.fallback(sku); // 降级策略必须写清楚来源
}
}
RestTemplate 默认没有读超时,这条在多数项目里等于"永远等下去"。这类"默认值陷阱"是生成代码的固定弱点。人工核对时可以直接搜:
# 列出所有外部调用点,再逐个确认超时是否显式设置
$ rg -n "RestTemplate|WebClient|HttpClient" --type java -l
$ rg -n "setConnectTimeout|connectTimeout|Duration.ofMillis" --type java
两个结果集的差集,就是没设超时的地方。
3. 为什么"看起来合理"是最危险的通过条件
人评审代码时依赖两个信号:语法熟悉度和风格一致性。生成代码在这两项上得分极高,于是评审者的警觉被提前消耗掉了。
| 信号 | 生成代码表现 | 与真实质量的关联 |
|---|---|---|
| 命名规范 | 通常很好 | 弱 |
| 注释密度 | 偏高,甚至高于人手代码 | 弱,可能反向 |
| 结构对称 | 很好(模板化产物) | 弱 |
| 错误分支完整 | 经常缺失 | 强 |
| 是否被实际执行过 | 常常没有 | 最强 |
结论很直接:风格类信号不能作为通过依据,执行证据才能。
危险的不是"它写错了",是"它写得让你不想查了"。一段 200 行、格式完美、注释齐全的代码,比一段 30 行粗糙代码更容易被放行。
4. 验证策略的四个层次
从便宜到贵,能上一层就不必上下一层:
- 跑测试:最快,但只覆盖已有断言的路径。生成代码常顺手改测试来迁就实现,所以先看测试文件有没有被改动。
- 跑真实入口:用真实请求/真实 CLI 参数走一遍,验证装配、序列化、配置加载。这一步最容易被跳过,也最能抓到问题。
- 看日志:确认日志里出现了预期的关键字段(id、耗时、结果码),而不是只有"进入方法"。
- 读代码:只在前三步有疑点时做,重点读错误分支和资源释放。
# 1. 测试:先确认测试文件本身没被改
$ git diff --stat HEAD -- '*Test.java'
# 2. 真实入口
$ curl -s -X POST localhost:8080/api/v1/orders/1/cancel -d '{}' | jq
# 3. 日志
$ grep -E "order_cancel|40901" logs/app.log | tail -20
5. 三种不可信信号
看到以下任一情况,把这段产出降级为"未验证草案":
| 信号 | 具体样子 | 正确处理 |
|---|---|---|
| 声称跑过但没给命令与输出 | "已测试通过"、"本地验证无误" | 要命令原文与输出尾部,自己复跑 |
| 引用不存在的 API | 方法名合理但库里没有 | 编译;编译过了也要搜官方文档确认语义 |
| 注释比代码更详细地描述"应该" | 注释写"这里应做幂等校验",代码没做 | 注释即承诺,逐条对照实现 |
第三类最隐蔽。它不是谎言,是计划与实现的错位:模型在注释里写下了正确设计,然后实现了简化版。
6. 让它自证
把"我相信你"变成"你给出证据"。要求固定成模板,每次都用:
完成后请给出:
1. 你执行过的命令原文,以及每条命令输出的最后 5 行
2. 你没有验证的假设清单(例如"假设 status 字段只有 4 个取值")
3. 你改动的文件列表,以及每个文件改了什么(一句话)
4. 你认为最可能出错的一处,以及怎么复现
不要写"应该可以"、"理论上没问题"。没跑过就写没跑过。
第 2 项价值最高。模型列出的"未验证假设"往往正好是你需要人工确认的业务前提,比它写的代码更有信息量。
6.1 小步提交与回滚
# 每个可验证单元一次提交,提交信息写清验证方式
$ git add -p
$ git commit -m "feat(order): 新增取消订单状态校验
验证:./mvnw -q test -Dtest=OrderCancelTest(3 passed)
未验证:并发取消同一订单的行为"
# 出错时回退到上一个已验证点,而不是继续叠补丁
$ git reset --hard HEAD~1
一次生成 800 行再评审,出问题时无法定位是哪一段引入的;一次 80 行、每步可跑,回退成本接近零。
7. 对生成代码的差异化要求
同样的功能,对 AI 产出的部分可以提高门槛,因为它补测试和补日志的边际成本几乎为零。
| 维度 | 人手代码常见标准 | 对生成代码的要求 |
|---|---|---|
| 测试 | 覆盖主要路径 | 覆盖关键路径 + 至少一条错误路径 + 一条边界 |
| 日志 | 关键节点有日志 | 每条错误日志必须带可定位的标识(id、参数、结果码) |
| 注释 | 解释为什么 | 允许少写,但凡写"应该"的必须与实现一致 |
| 依赖 | 按需引入 | 新增依赖需说明为何不能用现有能力 |
| 配置 | 允许隐式默认 | 超时、池大小、重试次数必须显式写出 |
8. 排错:验收阶段卡住的情况
| 现象 | 原因 | 处理 |
|---|---|---|
| 编译报符号找不到 | 用了不存在的 API 或版本不符 | 核对构建文件里的依赖版本,让它按真实签名重写 |
| 测试通过但启动失败 | 测试未加载真实配置,键名编造 | 用生产 profile 启动一次;核对官方配置键名 |
| 本地通、容器里挂 | 写死 localhost、端口或路径 |
改成配置注入,检查 docker-compose.yml 的网络别名 |
| 行为与旧逻辑不一致 | 它顺手"优化"了原有分支 | 对比改动前的行为契约,把无关重构拆出去 |
| 日志里没有预期字段 | 只打了"进入方法",没打结果 | 要求在结果分支补标识字段 |
| 压测时线程数暴涨 | 自建线程池,未复用容器管理的池 | 改为注入既有 ExecutorService |
最后一条的根因不在代码质量,在上下文缺失:它不知道项目里已有共享线程池。这类问题回到约定文件里补一条,比逐次纠正更省事。
9. 边界:验收能覆盖到哪里
| 场景 | 验收能做到 | 做不到 |
|---|---|---|
| 纯函数式逻辑 | 单测 + 边界用例基本可判 | 业务语义是否符合预期 |
| CRUD 接口 | 真实请求可验证 | 长事务下的并发行为 |
| 性能相关改动 | 只能确认"没明显变慢" | 需要压测与 profiler 才有结论 |
| 并发/分布式 | 代码可读、单测可过 | 竞态与顺序问题需专门构造 |
| 数据迁移 | 可验证样本 | 全量数据的例外形态 |
一句话:验收能证明"这段代码在验证过的输入下是对的",证明不了"它在所有输入下是对的"。这个差距只能靠测试覆盖率和灰度观察来缩小,不能靠评审。
10. 自测
- 生成代码里缺陷密度最高的,通常是哪一类行?
- 五个检查点里,哪两个能拦掉大部分问题?为什么排在前面?
- 为什么"命名规范、注释齐全"不能作为通过依据?
- 验证四层次中,哪一层最容易被跳过、又最能抓到装配类问题?
- 模型说"已测试通过"时,你要它补哪两样东西才可信?
- 一次生成 800 行与分四次各 200 行,回退成本的差别在哪里?