AI 写的代码,Review 要怎么改?我们团队的 15 条 PR 检查清单

26 阅读10分钟

AI 辅助编码进入团队三个月后,很多 Tech Lead 会发现一个反直觉现象:代码产出变快了,Review 却没有变轻。以前一个 PR 的主要风险是“人有没有想清楚”,现在多了一类风险:代码看起来完整、命名像样、测试也能跑过,但它可能是在错误上下文里拼出了一套貌似合理的实现。

传统 Review 通常依赖经验直觉:扫一遍 diff、看关键分支、确认命名和边界。可 AI 生成代码的危险点恰好在于“表面质量很高”。它会补齐样板代码,会模仿项目风格,也会给出自信的注释;但它不一定理解业务不变量、历史兼容约束、线上数据形态和团队约定。因此 Review 不能只问“这段代码写得好不好”,而要问“这段代码是不是在正确问题上、以正确约束运行”。

下面这份清单不是让 Reviewer 做更多无差别检查,而是把注意力集中到 AI 最容易出错的区域。建议团队把它放进 PR 模板里:作者先自查,Reviewer 再按风险挑重点看。

1. 需求边界是否被 AI 擅自扩大

检查什么:PR 是否只解决 issue 中定义的问题,有没有顺手重构、顺手改默认行为、顺手支持“看起来合理”的新场景。

为什么 AI 容易错:LLM 倾向于补全“完整方案”,而不是严格执行最小变更。它会主动添加参数、分支和兼容逻辑,让 diff 看起来更周全,但实际扩大了发布风险。

❌ 坏例子:修复分页默认值,却顺手改变排序字段。

// 原需求:pageSize 为空时默认 20
const pageSize = query.pageSize ?? 20;
const orderBy = query.orderBy ?? "created_at"; // AI 顺手新增,改变旧接口默认排序

✅ 好例子:只处理明确需求,新增行为单独开 PR。

const pageSize = query.pageSize ?? 20;
// 排序规则保持原逻辑,不在本 PR 修改

2. 业务不变量是否被显式表达

检查什么:账户余额不能为负、订单状态只能单向流转、权限不能越级等规则,是否在代码或测试中明确体现。

为什么 AI 容易错:AI 能推断常见业务流程,但不知道你们系统的“不允许”。它尤其容易写出“技术上可运行、业务上非法”的路径。

❌ 坏例子:只判断状态存在。

if (order.getStatus() != null) {
    order.setStatus(request.getStatus());
}

✅ 好例子:编码状态流转约束。

if (!OrderStatus.canTransit(order.getStatus(), request.getStatus())) {
    throw new BizException("非法订单状态流转");
}
order.setStatus(request.getStatus());

3. 是否复用了项目现有抽象,而不是新造一套

检查什么:日志、鉴权、异常、事务、RPC Client、配置读取等横切能力,是否沿用项目已有封装。

为什么 AI 容易错:模型从通用语料里学到大量“标准写法”,但不知道当前仓库已有工具类。它会重复造轮子,形成长期维护分叉。

❌ 坏例子:绕过统一异常体系。

if err != nil {
    return nil, fmt.Errorf("call user service failed: %v", err)
}

✅ 好例子:使用团队统一错误码和包装方式。

if err != nil {
    return nil, errors.WrapCode(errcode.UserServiceUnavailable, err)
}

4. 错误处理是否只是“吞掉异常”

检查什么:catch 后是否有告警、降级、错误码映射、重试边界;不能只 log 后继续执行。

为什么 AI 容易错:AI 很喜欢生成 try/catch,让代码看起来“健壮”。但它经常不知道异常对业务流程意味着什么,于是把失败变成静默脏数据。

❌ 坏例子:记录日志后返回空结果。

try:
    profile = client.get_profile(user_id)
except Exception as e:
    logger.warning("get profile failed", exc_info=e)
    return {}

✅ 好例子:区分可降级和不可降级失败。

try:
    profile = client.get_profile(user_id)
except TimeoutError as e:
    metrics.incr("profile.timeout")
    return Profile.anonymous(user_id)
except AuthError as e:
    raise PermissionDenied("profile access denied") from e

5. 空值和默认值是否改变了语义

检查什么:null、空字符串、空数组、0、false 是否被混用;默认值是否和旧逻辑一致。

为什么 AI 容易错:模型倾向使用 ??||Optional.orElse 这类简洁写法,但业务里“未传”和“传空”经常代表不同含义。

❌ 坏例子:把 0 当作未传。

const retryTimes = input.retryTimes || 3;

✅ 好例子:只对 null/undefined 使用默认值。

const retryTimes = input.retryTimes ?? 3;

6. 权限检查是否放在真实执行路径上

检查什么:新增接口、批量操作、导出、异步任务是否都经过鉴权;不能只在 Controller 层做一次象征性校验。

为什么 AI 容易错:AI 常按“入口校验”模板写代码,但会忽略内部复用方法可能被其他路径调用。

❌ 坏例子:只有 HTTP 入口检查权限。

@PostMapping("/users/export")
public File export() {
    auth.check("user:export");
    return userExportService.exportAll();
}

✅ 好例子:核心服务层也带权限上下文。

public File exportAll(UserContext ctx) {
    permission.require(ctx, "user:export");
    return doExport();
}

7. 数据库查询是否引入隐藏的 N+1

检查什么:循环中查库、循环中调 RPC、ORM 懒加载字段、批量接口是否真的批量。

为什么 AI 容易错:AI 更容易根据“单条数据”的样例补代码,不会主动推断线上列表页可能有几百条记录。

❌ 坏例子:循环查用户。

orders.map { order ->
    val user = userRepo.findById(order.userId)
    OrderVO(order, user.name)
}

✅ 好例子:先批量加载再组装。

val users = userRepo.findByIds(orders.map { it.userId }).associateBy { it.id }
orders.map { order -> OrderVO(order, users[order.userId]?.name) }

8. 并发和幂等是否只覆盖了“单线程快乐路径”

检查什么:重复提交、消息重放、定时任务并发、回调多次到达时是否安全。

为什么 AI 容易错:模型生成的流程通常是线性的:先查、再改、再保存。它不会天然考虑两个请求同时进来。

❌ 坏例子:先查后插,没有唯一约束兜底。

SELECT id FROM coupon_usage WHERE user_id = ? AND coupon_id = ?;
-- not exists then insert

✅ 好例子:数据库唯一键 + 冲突处理。

CREATE UNIQUE INDEX uk_coupon_user ON coupon_usage(coupon_id, user_id);
INSERT INTO coupon_usage(coupon_id, user_id) VALUES (?, ?)
ON CONFLICT DO NOTHING;

9. 时间、时区和单位是否明确

检查什么:秒/毫秒、UTC/本地时区、自然日/24 小时、过期时间是否统一。

为什么 AI 容易错:语料里的时间写法很多,AI 很容易混用 Date.now()、Unix 秒和数据库 timestamp。

❌ 坏例子:把毫秒传给要求秒的接口。

cache.expire(key, Date.now() + 3600 * 1000);

✅ 好例子:变量名和 API 都标明单位。

const ttlSeconds = 3600;
cache.expire(key, ttlSeconds);

10. 测试是否只验证了 AI 自己写出的实现

检查什么:测试是否覆盖旧 bug、边界条件、失败路径;有没有只测 happy path。

为什么 AI 容易错:AI 会根据实现反推测试,导致“实现错,测试也跟着错”。这种测试提高覆盖率,却不提高信心。

❌ 坏例子:只验证正常输入。

test("create user", () => {
  expect(createUser({ name: "Tom" }).name).toBe("Tom");
});

✅ 好例子:测试需求约束和历史 bug。

test("reject duplicated email ignoring case", () => {
  createUser({ email: "A@EXAMPLE.com" });
  expect(() => createUser({ email: "a@example.com" })).toThrow("EMAIL_EXISTS");
});

11. 日志是否会泄露敏感信息

检查什么:token、手机号、邮箱、身份证、Cookie、请求体是否被完整打印。

为什么 AI 容易错:AI 常把“方便调试”放在第一位,生成 JSON.stringify(request) 这类全量日志。

❌ 坏例子:打印完整请求。

logger.info("payment request", { body: req.body });

✅ 好例子:只打印必要字段并脱敏。

logger.info("payment request", {
  orderId: req.body.orderId,
  amount: req.body.amount,
  phone: maskPhone(req.body.phone)
});

12. 配置和常量是否硬编码

检查什么:超时时间、重试次数、URL、开关、阈值是否写死在代码里。

为什么 AI 容易错:为了让示例自洽,AI 常直接写一个“合理数字”。但生产系统里这些数字通常需要按环境调整。

❌ 坏例子:代码里写死外部地址。

private static final String API = "https://api.partner.com/v1";

✅ 好例子:走配置并给出默认值说明。

@Value("${partner.api.base-url}")
private String partnerApiBaseUrl;

13. 注释是否在解释错误的事实

检查什么:注释、README、接口文档是否和代码一致;尤其关注 AI 生成的“自信解释”。

为什么 AI 容易错:LLM 很擅长生成听起来合理的注释,但注释可能描述的是它想象中的系统,而不是实际系统。

❌ 坏例子:注释承诺了代码没做到的行为。

# retry 3 times with exponential backoff
return client.call(payload)

✅ 好例子:要么实现,要么删掉虚假注释。

return retry(max_attempts=3, backoff="exponential")(client.call)(payload)

14. 依赖升级和新增包是否必要

检查什么:是否为几行工具函数引入新依赖;版本是否和项目兼容;许可证是否可接受。

为什么 AI 容易错:模型会推荐流行包,但不知道你们的依赖治理、镜像源、漏洞扫描和许可证限制。

❌ 坏例子:为了格式化日期新增大型库。

{
  "dependencies": {
    "moment": "^2.30.0"
  }
}

✅ 好例子:优先使用项目已有工具或标准库。

const date = new Intl.DateTimeFormat("zh-CN", { dateStyle: "short" }).format(value);

15. 回滚路径和灰度开关是否清晰

检查什么:变更是否可关闭、可回滚;数据结构变更是否兼容旧代码;发布失败时如何恢复。

为什么 AI 容易错:AI 关注“如何实现功能”,很少主动考虑发布过程。它会把新逻辑直接接入主路径,让小 PR 变成不可逆变更。

❌ 坏例子:新算法直接替换旧算法。

score := newRanker.Score(item)

✅ 好例子:用配置开关控制切换,并保留观测指标。

if config.EnableNewRanker {
    metrics.Incr("ranker.new.used")
    score = newRanker.Score(item)
} else {
    score = oldRanker.Score(item)
}

可直接复制的 PR 描述模板

## 变更摘要
- 本 PR 解决的问题:
- 本 PR 不解决的问题:

## 是否含 AI 生成代码
- [ ] 否,全部手写
- [ ] 是,局部使用 AI 辅助
- [ ] 是,核心逻辑由 AI 生成后人工修改

使用的 AI 工具 / 模型:

## AI 生成代码自查
- [ ] 我确认 PR 没有扩大需求边界
- [ ] 我确认核心业务不变量已在代码或测试中体现
- [ ] 我确认没有绕过项目现有鉴权、异常、日志、配置封装
- [ ] 我确认错误处理没有吞异常或静默返回错误默认值
- [ ] 我确认空值、默认值、时间单位、时区语义清晰
- [ ] 我确认没有新增不必要依赖或硬编码环境配置

## 传播风险自评
这次变更影响范围:
- [ ] 单个内部函数
- [ ] 单个接口 / 页面
- [ ] 多个服务调用链
- [ ] 数据库结构 / 消息格式 / 公共 SDK

如果出错,可能影响:
- 用户范围:
- 数据范围:
- 是否可快速回滚:是 / 否
- 回滚方式:

## Reviewer 重点关注点
请重点看:
1. 
2. 
3. 

## 测试说明
- 单元测试:
- 集成测试:
- 手工验证:
- 未覆盖但已知风险:

## 发布与观测
- 是否需要灰度开关:是 / 否
- 关键监控指标:
- 预期日志 / 告警:

总结

AI Coding 之后,Review 的核心不再是“帮作者找语法问题”,而是验证代码有没有遵守业务约束、工程约定和发布边界。真正有效的做法,是让作者先用清单暴露风险,再让 Reviewer 把精力放在最可能被 AI 忽略的地方。清单不需要一次做到完美,但一定要沉淀到 PR 模板和团队共识里。否则 AI 提升的是提交速度,透支的却是 Review 质量。