用 AI 辅助代码 Review:从 Prompt 设计到单元测试验证的实践记录
在日常开发中,代码 Review 经常会遇到两个问题:
- Review 时间不够:业务迭代快,很多 MR/PR 只能看主流程,边界分支容易漏;
- Review 质量不稳定:不同开发者经验不同,对异常处理、空值判断、性能风险、安全边界的敏感度也不同。
ChatGPT、Claude、Gemini、DeepSeek 这类大模型,比较适合在代码 Review 前充当“第二视角”:
它不能替代开发者做最终判断,但可以帮助我们提前发现一些明显问题,例如:
- 空指针风险;
- 异常处理不完整;
- 参数校验缺失;
- SQL 查询性能隐患;
- 重复逻辑;
- 命名不清晰;
- 测试用例遗漏;
- 接口返回语义不一致。
本文以一个后端接口代码 Review 场景为例,记录一套比较实用的 AI 辅助 Review 流程。
一、本文适合谁
这篇文章适合:
- Java / Go / Python / Node.js 后端开发者;
- 需要经常提交 MR/PR 的开发者;
- 想用 AI 辅助代码 Review,但不想直接复制 AI 结论的人;
- 技术负责人或团队 Reviewer;
- 希望比较 ChatGPT、Claude、Gemini、DeepSeek 在开发任务中差异的用户;
- 正在尝试把 AI 编程助手接入日常研发流程的团队。
本文不会讨论“让 AI 自动合并代码”这种高风险做法,而是重点讲:
如何让 AI 帮我们发现问题,再由开发者验证问题。
二、为什么代码 Review 适合引入 AI
代码 Review 本质上不是“找语法错误”,而是检查代码是否符合业务预期和工程规范。
常见 Review 关注点包括:
- 代码是否实现了需求;
- 参数校验是否完整;
- 异常处理是否合理;
- 是否存在空值、越界、并发问题;
- 是否影响性能;
- 是否破坏已有接口兼容性;
- 是否有安全风险;
- 是否补充了必要测试;
- 是否容易维护。
AI 的优势在于,它可以快速从多个角度扫一遍代码,并输出问题清单。
但 AI 的限制也很明显:
- 不知道你们项目真实业务规则;
- 可能误判框架能力;
- 可能给出“看似合理但不符合项目规范”的建议;
- 对权限、安全、资金、订单等核心业务不能直接下结论;
- 无法替代编译、单元测试、集成测试和人工 Review。
所以更合理的定位是:
AI 适合做 Review 前置检查,不适合做最终审批人。
三、本次示例场景:用户地址新增接口
假设有一个新增用户收货地址的接口,代码如下。
@RestController
@RequestMapping("/api/address")
public class AddressController {
@PostMapping("/create")
public Result<Long> create(@RequestBody CreateAddressRequest request) {
Long id = addressService.createAddress(request);
return Result.success(id);
}
}
请求对象:
public class CreateAddressRequest {
private Long userId;
private String receiverName;
private String phone;
private String province;
private String city;
private String detailAddress;
private Boolean defaultAddress;
// getter/setter 省略
}
Service 代码:
@Service
public class AddressService {
@Autowired
private AddressRepository addressRepository;
public Long createAddress(CreateAddressRequest request) {
Address address = new Address();
address.setUserId(request.getUserId());
address.setReceiverName(request.getReceiverName());
address.setPhone(request.getPhone());
address.setProvince(request.getProvince());
address.setCity(request.getCity());
address.setDetailAddress(request.getDetailAddress());
address.setDefaultAddress(request.getDefaultAddress());
addressRepository.save(address);
return address.getId();
}
}
这段代码能跑,但从 Review 角度看,问题不少。
例如:
request可能为null;userId是否应该从登录态获取,而不是前端传入;- 收件人、手机号、地址是否需要校验;
defaultAddress为true时,是否需要取消其他默认地址;- 是否需要事务;
- 是否需要限制地址数量;
- 是否需要防止越权;
- 是否需要统一错误码;
- 是否需要记录操作日志;
- 是否需要补测试用例。
这些问题,AI 可以帮助我们更快列出来。
四、ChatGPT、Claude、Gemini、DeepSeek 在代码 Review 中怎么选
不同模型在代码 Review 场景中的侧重点略有不同。
| 模型 | 更适合的 Review 任务 | 使用建议 |
|---|---|---|
| ChatGPT | 通用代码解释、Review 清单、重构建议、测试用例生成 | 适合快速检查代码质量和边界条件 |
| Claude | 长上下文代码、复杂需求文档、PRD 与代码一致性检查 | 适合输入较长业务背景和多文件代码 |
| Gemini | 文档归纳、多来源资料整理、接口说明辅助生成 | 适合结合接口文档、需求说明一起 Review |
| DeepSeek | 中文开发语境、代码逻辑分析、算法和工程实现检查 | 适合中文项目说明和代码辅助分析 |
实际使用时,不建议纠结“哪个模型一定最好”。
更合理的做法是:
- 小改动:用一个模型做快速检查;
- 涉及多个模块:用长上下文能力较好的模型;
- 关键业务改动:用两个模型交叉 Review;
- 生成测试用例:让模型单独针对测试场景输出;
- 不确定的建议:必须回到代码、需求和测试中验证。
如果只是为了比较多个模型在同一段代码上的输出差异,也可以使用一些多模型聚合工具,例如 KULAAI 这类支持 ChatGPT、Claude、Gemini、DeepSeek 等模型切换的产品。工具本身不是重点,关键是 Review 流程要可验证。
五、代码 Review Prompt 怎么写
很多人使用 AI Review 代码时,只会问:
帮我看看这段代码有没有问题。
这种问法太宽泛,模型通常会给出一些泛泛建议。
更好的方式是明确:
- 你的角色;
- 业务背景;
- 代码目标;
- Review 维度;
- 输出格式;
- 不要做什么;
- 哪些地方需要标注不确定性。
可以使用下面这个 Prompt 模板。
你是一名有经验的 Java 后端开发工程师,请帮我对下面代码做 Code Review。
业务背景:
这是一个新增用户收货地址的接口。
用户登录后可以新增地址。
一个用户可以有多个地址,但最多 20 个。
如果本次新增地址设置为默认地址,则需要取消该用户其他默认地址。
userId 理论上应来自登录态,而不是完全信任前端传参。
Review 目标:
1. 找出可能的空指针问题;
2. 找出参数校验缺失;
3. 找出权限或越权风险;
4. 找出事务一致性问题;
5. 找出接口设计不合理之处;
6. 给出可维护性建议;
7. 给出需要补充的单元测试和接口测试。
输出格式:
- 问题类型
- 问题描述
- 风险等级:高 / 中 / 低
- 为什么是问题
- 建议修改方向
- 是否需要人工确认
要求:
1. 不要直接重写完整项目;
2. 不要假设项目中存在未提供的工具类;
3. 如果某个结论依赖业务规则,请标注“需要业务确认”;
4. 代码建议尽量保持简单;
5. 最后输出测试用例清单。
待 Review 代码:
【在这里粘贴 Controller、Request、Service、Repository 相关代码】
这个 Prompt 的重点是:让 AI 按 Review 维度输出结构化结果。
六、AI 可能会发现哪些问题
针对上面的地址新增接口,AI 通常会指出以下问题。
1. 不应该完全信任前端传入的 userId
如果接口直接使用前端传入的 userId:
address.setUserId(request.getUserId());
可能出现越权风险。
例如用户 A 登录后,构造请求:
json
{
"userId": 10086,
"receiverName": "张三",
"phone": "13800000000",
"province": "浙江省",
"city": "杭州市",
"detailAddress": "某某路 1 号",
"defaultAddress": true
}
如果后端不校验登录用户身份,就可能给其他用户新增地址。
更合理的做法是:
Long currentUserId = loginUserContext.getCurrentUserId();
address.setUserId(currentUserId);
或者至少校验:
if (!Objects.equals(currentUserId, request.getUserId())) {
throw new BizException("无权限操作该用户地址");
}
在真实项目中,更建议从登录态获取用户 ID,而不是依赖客户端传参。
2. 缺少请求参数校验
原代码没有检查:
request是否为空;receiverName是否为空;phone格式是否合法;province、city、detailAddress是否为空;- 字段长度是否超限;
defaultAddress为null时如何处理。
可以增加基础校验,例如:
private void validateCreateAddressRequest(CreateAddressRequest request) {
if (request == null) {
throw new BizException("请求参数不能为空");
}
if (isBlank(request.getReceiverName())) {
throw new BizException("收件人不能为空");
}
if (isBlank(request.getPhone())) {
throw new BizException("手机号不能为空");
}
if (!request.getPhone().matches("^1\\d{10}$")) {
throw new BizException("手机号格式不正确");
}
if (isBlank(request.getProvince()) || isBlank(request.getCity())) {
throw new BizException("省市信息不能为空");
}
if (isBlank(request.getDetailAddress())) {
throw new BizException("详细地址不能为空");
}
if (request.getReceiverName().length() > 30) {
throw new BizException("收件人名称过长");
}
if (request.getDetailAddress().length() > 200) {
throw new BizException("详细地址过长");
}
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
如果项目使用 Bean Validation,也可以放到 DTO 上:
public class CreateAddressRequest {
@NotBlank(message = "收件人不能为空")
@Size(max = 30, message = "收件人名称过长")
private String receiverName;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确")
private String phone;
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
@NotBlank(message = "详细地址不能为空")
@Size(max = 200, message = "详细地址过长")
private String detailAddress;
private Boolean defaultAddress;
// getter/setter 省略
}
然后 Controller 中使用:
@PostMapping("/create")
public Result<Long> create(@Valid @RequestBody CreateAddressRequest request) {
Long id = addressService.createAddress(request);
return Result.success(id);
}
AI 生成类似建议时,需要结合项目规范选择手写校验还是注解校验。
3. 默认地址逻辑缺少事务
如果新增地址设置为默认地址,常见逻辑是:
- 取消该用户原有默认地址;
- 新增当前地址为默认地址。
这两个操作应该放在同一个事务中。
示例:
@Transactional(rollbackFor = Exception.class)
public Long createAddress(CreateAddressRequest request) {
validateCreateAddressRequest(request);
Long currentUserId = loginUserContext.getCurrentUserId();
if (Boolean.TRUE.equals(request.getDefaultAddress())) {
addressRepository.clearDefaultAddress(currentUserId);
}
Address address = new Address();
address.setUserId(currentUserId);
address.setReceiverName(request.getReceiverName().trim());
address.setPhone(request.getPhone());
address.setProvince(request.getProvince());
address.setCity(request.getCity());
address.setDetailAddress(request.getDetailAddress().trim());
address.setDefaultAddress(Boolean.TRUE.equals(request.getDefaultAddress()));
addressRepository.save(address);
return address.getId();
}
如果不加事务,可能出现:
- 旧默认地址已取消,新地址保存失败;
- 多个默认地址同时存在;
- 数据状态不一致。
不过这里还要继续考虑并发问题。
4. 并发下可能出现多个默认地址
即使加了事务,如果两个请求同时设置默认地址,也可能出现并发问题。
例如:
- 请求 A 清除默认地址;
- 请求 B 清除默认地址;
- 请求 A 新增默认地址;
- 请求 B 新增默认地址;
最终可能出现两个默认地址。
解决方式要看项目数据库和业务要求,常见选择包括:
- 数据库唯一约束;
- 悲观锁;
- 乐观锁;
- 串行化处理;
- 在用户维度加锁;
- 业务上允许短暂不一致,通过后续修正。
如果使用数据库唯一约束,可以考虑设计:
-- 示例,仅表达思路,具体语法需根据数据库调整
-- 保证同一用户最多只有一个默认地址
CREATE UNIQUE INDEX uk_user_default_address
ON user_address(user_id, default_address)
WHERE default_address = true;
注意:不同数据库对部分索引支持不同,例如 MySQL、PostgreSQL 处理方式不同,不能直接复制上述 SQL 到所有环境。
AI 可以提醒风险,但具体方案必须由开发者结合数据库类型、并发量和业务要求决定。
七、让 AI 生成 Review 清单,而不是只给修改代码
对团队来说,最好沉淀一份通用 Review 清单。以后每次提交代码前,可以先让 AI 按清单检查。
例如:
请根据下面维度对代码做 Review:
1. 功能正确性:
- 是否满足需求描述;
- 是否遗漏主流程;
- 是否遗漏异常流程。
2. 参数校验:
- 是否校验必填字段;
- 是否校验长度、格式、枚举值;
- 是否处理 null。
3. 权限与安全:
- 是否信任了客户端传入的用户身份;
- 是否存在越权访问;
- 是否暴露敏感信息。
4. 数据一致性:
- 是否需要事务;
- 是否存在并发问题;
- 是否需要唯一约束或幂等控制。
5. 可维护性:
- 命名是否清晰;
- 方法是否过长;
- 是否存在重复代码;
- 异常和错误码是否统一。
6. 性能:
- 是否有循环查询;
- 是否可能产生慢 SQL;
- 是否缺少索引。
7. 测试:
- 是否覆盖正常用例;
- 是否覆盖异常用例;
- 是否覆盖边界条件;
- 是否覆盖并发或重复请求。
这样的清单比“帮我看看代码”更稳定,也更容易在团队中复用。
八、AI 辅助生成单元测试用例
Review 完代码后,可以继续让 AI 生成测试用例。
Prompt 示例:
请根据上面的地址新增接口,生成单元测试和接口测试用例。
要求:
1. 覆盖正常新增地址;
2. 覆盖必填字段为空;
3. 覆盖手机号格式错误;
4. 覆盖 defaultAddress 为 null、false、true;
5. 覆盖用户已有默认地址时新增默认地址;
6. 覆盖地址数量超过 20;
7. 覆盖越权风险;
8. 标注哪些用例需要 Mock 登录态;
9. 标注哪些用例需要数据库事务验证。
输出格式:
用例名称 / 前置条件 / 输入 / 预期结果 / 优先级 / 备注。
可能得到类似表格:
| 用例名称 | 前置条件 | 输入 | 预期结果 | 优先级 |
|---|---|---|---|---|
| 正常新增非默认地址 | 用户已登录 | defaultAddress=false | 新增成功,返回地址 ID | P0 |
| 正常新增默认地址 | 用户已登录,已有非默认地址 | defaultAddress=true | 新增成功,该地址为默认地址 | P0 |
| 收件人为空 | 用户已登录 | receiverName="" | 返回参数错误 | P0 |
| 手机号格式错误 | 用户已登录 | phone="123" | 返回手机号格式错误 | P0 |
| defaultAddress 为 null | 用户已登录 | defaultAddress=null | 按 false 处理或按业务约定处理 | P1 |
| 用户已有默认地址 | 用户已登录,已有默认地址 | 新增 defaultAddress=true | 旧默认地址取消,新地址成为默认 | P0 |
| 地址数量超过限制 | 用户已有 20 个地址 | 新增地址 | 返回地址数量超限 | P1 |
| 越权传入其他 userId | 用户 A 登录 | 请求中传入用户 B 的 userId | 不允许操作用户 B 地址 | P0 |
注意:AI 生成的是测试用例初稿,最终还要根据真实需求补充。
九、示例单元测试草稿
下面是一个 JUnit 5 + Mockito 风格的测试草稿,仅用于说明思路。
@ExtendWith(MockitoExtension.class)
class AddressServiceTest {
@Mock
private AddressRepository addressRepository;
@Mock
private LoginUserContext loginUserContext;
@InjectMocks
private AddressService addressService;
@Test
void shouldCreateAddressSuccessfullyWhenRequestValid() {
CreateAddressRequest request = new CreateAddressRequest();
request.setReceiverName("张三");
request.setPhone("13800000000");
request.setProvince("浙江省");
request.setCity("杭州市");
request.setDetailAddress("某某路 1 号");
request.setDefaultAddress(false);
when(loginUserContext.getCurrentUserId()).thenReturn(1001L);
Address savedAddress = new Address();
savedAddress.setId(1L);
when(addressRepository.save(any(Address.class))).thenAnswer(invocation -> {
Address address = invocation.getArgument(0);
address.setId(1L);
return address;
});
Long id = addressService.createAddress(request);
assertEquals(1L, id);
verify(addressRepository, never()).clearDefaultAddress(anyLong());
verify(addressRepository, times(1)).save(any(Address.class));
}
@Test
void shouldClearOldDefaultAddressWhenCreateDefaultAddress() {
CreateAddressRequest request = new CreateAddressRequest();
request.setReceiverName("张三");
request.setPhone("13800000000");
request.setProvince("浙江省");
request.setCity("杭州市");
request.setDetailAddress("某某路 1 号");
request.setDefaultAddress(true);
when(loginUserContext.getCurrentUserId()).thenReturn(1001L);
when(addressRepository.save(any(Address.class))).thenAnswer(invocation -> {
Address address = invocation.getArgument(0);
address.setId(2L);
return address;
});
Long id = addressService.createAddress(request);
assertEquals(2L, id);
verify(addressRepository, times(1)).clearDefaultAddress(1001L);
verify(addressRepository, times(1)).save(any(Address.class));
}
@Test
void shouldThrowExceptionWhenPhoneInvalid() {
CreateAddressRequest request = new CreateAddressRequest();
request.setReceiverName("张三");
request.setPhone("123");
request.setProvince("浙江省");
request.setCity("杭州市");
request.setDetailAddress("某某路 1 号");
request.setDefaultAddress(false);
assertThrows(BizException.class, () -> addressService.createAddress(request));
verify(addressRepository, never()).save(any(Address.class));
}
}
这类测试代码可以让 AI 生成初稿,但需要人工检查:
- Mock 对象是否符合项目真实结构;
- 异常类型是否正确;
- Repository 方法名是否真实存在;
- 测试是否覆盖关键业务规则;
- 断言是否足够明确。
十、AI 输出结果如何验证
AI Review 的结果不能直接当作结论。建议按下面步骤验证。
1. 先区分问题类型
AI 输出的问题可以分成三类:
| 类型 | 处理方式 |
|---|---|
| 明确代码问题 | 直接结合代码验证,如空指针、缺少参数校验 |
| 业务规则问题 | 找产品、测试或技术负责人确认 |
| 架构建议问题 | 结合系统复杂度和维护成本评估 |
例如:
- “request 可能为空”属于明确代码问题;
- “defaultAddress 为 null 应该按 false 处理”属于业务规则问题;
- “建议引入领域模型和策略模式”可能属于过度设计,需要谨慎。
2. 用编译和测试验证代码建议
AI 给出的代码建议必须经过:
- 编译检查;
- 单元测试;
- 接口测试;
- 静态代码扫描;
- 代码规范检查;
- 人工 Review。
尤其注意:
- AI 可能调用不存在的方法;
- AI 可能使用项目中没有的工具类;
- AI 可能忽略事务传播行为;
- AI 可能生成不符合团队规范的异常处理。
3. 对安全问题单独复核
涉及以下内容时,不要只看 AI 结论:
- 登录态;
- 权限校验;
- 越权访问;
- 数据脱敏;
- 资金交易;
- 订单状态流转;
- 审计日志;
- 用户隐私;
- 文件上传;
- 外部回调。
这些场景必须由有经验的开发者或安全人员复核。
十一、多模型交叉 Review 的使用方式
对重要代码,可以让多个模型分别 Review 同一段代码。
比较实用的方式是:
- 用模型 A 输出问题清单;
- 用模型 B 检查模型 A 的结论是否有遗漏或误判;
- 让模型 C 根据最终问题清单生成测试用例;
- 由开发者做最终判断。
例如第二轮可以这样问:
下面是另一个模型对代码 Review 的结果。
请你不要直接接受这些结论,而是逐条判断:
1. 该问题是否成立;
2. 是否存在误判;
3. 是否还有遗漏;
4. 哪些问题需要结合业务确认;
5. 哪些问题必须通过测试验证。
Review 结果:
【粘贴第一轮输出】
这样可以减少单一模型的偏差,但仍然不能替代人工判断。
十二、团队如何沉淀 AI Review 流程
如果团队希望长期使用 AI 辅助代码 Review,可以从轻量方式开始。
1. 建立通用 Prompt 模板
按语言和场景维护几类模板:
- Java 后端接口 Review;
- SQL Review;
- 前端组件 Review;
- 单元测试 Review;
- 接口文档 Review;
- 异常处理 Review;
- 安全风险 Review。
2. 在提交 PR 前自查
可以要求开发者在提交 PR 前完成:
- AI Review 一轮;
- 修复明确问题;
- 标注未采纳建议原因;
- 补充测试用例;
- 在 PR 描述中说明风险点。
PR 描述可以参考:
本次变更:
- 新增用户地址创建接口;
- 支持设置默认地址;
- 增加参数校验;
- 增加地址数量限制。
已自查:
- 已处理 request 为空问题;
- userId 改为从登录态获取;
- defaultAddress=true 时增加事务处理;
- 已补充手机号格式错误、默认地址切换等测试。
待确认:
- defaultAddress=null 时是否按 false 处理;
- 地址数量上限是否固定为 20;
- 详细地址最大长度是否为 200。
3. 明确哪些场景不能只依赖 AI
例如:
- 核心交易链路;
- 权限系统;
- 数据库迁移;
- 大规模重构;
- 性能优化;
- 安全加固;
- 用户隐私处理。
这些场景可以使用 AI 辅助整理思路,但必须走团队正式评审流程。
十三、注意事项:不要把敏感信息直接发给 AI
使用 AI 做代码 Review 时,要特别注意输入内容。
不建议直接输入:
- 公司未公开完整源码;
- 生产数据库连接;
- API Key;
- Token;
- 用户手机号、身份证、邮箱;
- 内部服务域名;
- 访问控制策略;
- 生产环境完整日志;
- 商业敏感规则。
如果确实需要分析代码或日志,建议:
- 只截取最小必要代码片段;
- 替换真实域名和账号;
- 删除密钥和 Token;
- 对用户数据脱敏;
- 避免上传完整项目;
- 遵守公司内部安全规范。
十四、常见问题
1. AI 能不能替代人工代码 Review?
不能。AI 适合发现明显问题和补充检查视角,但最终 Review 仍然需要开发者结合业务、架构、测试和上线风险判断。
2. AI Review 适合检查哪些问题?
比较适合检查:
- 空值风险;
- 参数校验;
- 异常处理;
- 重复代码;
- 简单性能问题;
- 测试遗漏;
- 命名和可读性问题。
对于复杂架构、安全策略、业务状态机,需要人工复核。
3. 代码是不是贴得越多越好?
不是。更好的方式是提供最小相关上下文,包括:
- 需求背景;
- 修改目标;
- 相关代码;
- 调用关系;
- 已知约束;
- 期望输出格式。
过多无关代码会干扰模型判断,也可能带来安全风险。
4. 多模型 Review 有必要吗?
普通代码改动不一定需要。
但涉及权限、订单、支付、库存、并发、数据一致性等场景时,多模型交叉 Review 可以帮助发现遗漏,但仍要以测试和人工判断为准。
5. AI 生成的单元测试能直接用吗?
通常不能直接用。AI 生成的测试代码可能调用不存在的方法,或者断言不符合真实业务。更适合当作测试思路初稿。
十五、总结
AI 辅助代码 Review 的价值,不在于替开发者“拍板”,而在于提前暴露问题。
一套比较稳妥的流程是:
- 提供需求背景和最小相关代码;
- 用明确 Prompt 约束 Review 维度;
- 让 AI 输出结构化问题清单;
- 区分明确问题、业务问题和架构建议;
- 对代码建议进行编译、测试和人工验证;
- 让 AI 辅助生成测试用例;
- 把 Prompt 和 Review 清单沉淀为团队规范。
对开发者来说,AI 最适合做“辅助 Reviewer”。
它可以提高 Review 覆盖面,但不能替代工程责任。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)