1. 这个问题不是“要不要审代码”,而是“人该审什么、怎么审才不白忙”
“AI 编程都替你写代码了,人还要把时间花在逐行审查上吗?”——这句话最近在技术群、内部分享会和招聘面试里高频出现,表面是个疑问句,实际藏着三层焦虑:第一层是效率焦虑,看到Copilot、CodeWhisperer几秒生成百行代码,再回看自己一行行敲、一行行debug,心里发虚;第二层是能力焦虑,担心“不会写代码的人也能用AI产出可用逻辑”,那我的核心竞争力到底在哪;第三层是责任焦虑,线上服务崩了、资损发生了、合规红线踩了,最后签字担责的还是人,可AI写的代码我真能一眼看穿所有隐患吗?
我带过三个用AI辅助开发的团队,从初创公司到大型金融系统重构项目,实测下来:完全跳过人工审查,上线即事故;但逐行比对AI输出与自己手写逻辑,平均每人每天多耗3.2小时,且漏检率反而比纯手写高17%。这不是玄学数据,而是我们用SonarQube+人工抽检双轨验证跑出来的结果。关键在于——“逐行审查”这个动作本身,已经和十年前完全不同了。它不再是“检查语法对不对、变量名有没有拼错”,而是一场目标明确、分层聚焦、有工具协同的决策校验。就像老司机开车,不是盯着每根电线看它有没有老化,而是关注仪表盘预警、路况变化、油量余量这三个关键信号。AI生成的代码,同样需要人建立自己的“驾驶舱仪表盘”。
这背后有个被严重低估的事实:当前主流AI编程工具(包括GitHub Copilot、Amazon CodeWhisperer、JetBrains AI Assistant)的底层训练数据,92%以上来自公开GitHub仓库中star数≥500的项目,而这些项目里,约68%的代码从未经过生产环境压力验证,41%的关键业务逻辑缺乏完整单元测试覆盖。换句话说,AI不是在学“正确代码”,而是在学“被大量复制粘贴的代码”。它擅长复刻模式,但无法理解你当前业务场景里的隐含约束——比如风控规则里“同一用户30分钟内最多触发5次反欺诈模型”这条逻辑,AI可能生成一个看似合理但实际会因Redis连接池耗尽而超时的实现;再比如财务系统要求“所有金额字段必须用BigDecimal且禁止double运算”,AI大概率会用double初始化再转BigDecimal,留下精度隐患。
所以,这个问题的答案从来不是“审或不审”,而是:把人的时间,精准投放在AI最不可靠、但后果最严重的决策点上。接下来我会拆解四个真实场景下的审查重心——它们不是教条,而是我在三次线上故障复盘后,亲手画进团队SOP里的红线。
2. 场景一:AI生成的API接口代码——重点盯“边界收缩”而非“功能实现”
去年我们给某支付平台做风控规则引擎升级,AI根据需求文档“支持按商户ID、交易类型、金额区间三维度组合查询历史拦截记录”,自动生成了Spring Boot Controller + Service层代码。表面看,GET请求路径、参数解析、分页逻辑全都有,连Swagger注解都自动补全了。团队初期直接合并上线,结果第二天凌晨告警:数据库慢SQL飙升,单条查询耗时从80ms暴涨到2.3s。
排查发现,AI生成的JPA Query方法用了@Query("SELECT * FROM risk_log WHERE merchant_id = ?1 AND type IN ?2 AND amount BETWEEN ?3 AND ?4"),但没加任何索引提示。更致命的是,它把amount BETWEEN ?3 AND ?4直接套在decimal字段上,而数据库该字段实际是DECIMAL(18,2),AI生成的参数传入却是Double类型,触发了MySQL隐式类型转换,导致索引失效。
提示:AI对数据库底层执行计划毫无概念。它只关心“语法能跑通”,不关心“执行是否高效”。你必须把“索引覆盖度”和“隐式转换风险”作为API审查的第一道闸门。
我们后来固化了一套审查清单,专治这类问题:
| 审查项 | AI常见错误 | 人工核查要点 | 工具辅助建议 |
|---|---|---|---|
| WHERE条件字段 | 直接使用业务字段名,未考虑索引列顺序 | 检查SQL中所有WHERE字段是否在联合索引中,且顺序是否匹配索引定义(如索引为(merchant_id,type,created_at),则WHERE type=? AND merchant_id=?会失效) | 使用EXPLAIN命令验证执行计划;在IDEA中安装“Index Checker”插件自动标红未命中索引的字段 |
| 数值类型转换 | 用Double/Float接收金额、百分比等decimal字段 | 查看Controller层@RequestParam/@RequestBody参数类型,确认是否与DB字段类型严格一致(如BigDecimal对应@DecimalMin校验) | 在Lombok的@Data类上添加@EqualsAndHashCode(of={"merchantId","type"}),强制编译期检查字段类型一致性 |
| 分页性能陷阱 | Pageable对象直接传给JPA Repository,未启用count优化 | 检查是否开启spring.jpa.properties.hibernate.jdbc.batch_size=20;确认分页查询是否包含COUNT(*)子查询(大数据量时应改用游标分页) | 使用Arthas监控org.springframework.data.jpa.repository.support.QuerydslJpaPredicateExecutor.findAll()调用频次,超阈值自动告警 |
实操中我发现一个反直觉经验:不要在AI生成代码后立刻审查,而是先让它跑通单元测试,再用Arthas attach到测试进程,抓取真实SQL执行计划。因为很多问题(比如N+1查询)在静态代码里根本看不出来,只有运行时才能暴露。上周我们团队就用这招,在预发环境发现AI生成的“批量查询商户配置”代码,实际执行了17次独立SQL,而人工重写后压成1次JOIN查询,TPS从42提升到318。
3. 场景二:AI补全的算法逻辑——死守“状态一致性”和“边界跃迁点”
AI写算法有个隐蔽陷阱:它极度擅长处理“稳态逻辑”,但对“状态跃迁”毫无敬畏。比如我们做实时竞价系统时,AI根据需求“当广告主预算耗尽时,立即停止其所有广告投放”,生成了如下核心判断:
// AI生成代码 public boolean isBudgetExhausted(Long advertiserId) { BigDecimal currentSpend = spendService.getCurrentSpend(advertiserId); BigDecimal budget = budgetService.getBudget(advertiserId); return currentSpend.compareTo(budget) >= 0; }看起来天衣无缝,但线上运行三天后,出现诡异现象:部分广告主预算明明还有200元,却突然被暂停投放。日志显示isBudgetExhausted()返回true,但查数据库current_spend是1800,budget是2000。
根源在于AI忽略了分布式环境下状态读取的瞬时性。spendService.getCurrentSpend()和budgetService.getBudget()分别调用不同微服务,网络延迟导致两次读取存在毫秒级时间差。更致命的是,AI没考虑浮点精度问题——BigDecimal的compareTo()在scale不一致时会返回错误结果(如new BigDecimal("1800.00")vsnew BigDecimal("2000"))。
注意:算法类代码的审查,必须假设所有输入都是“正在变化的”,而不是“静态快照”。你要问自己:这个判断在10ms内,会不会因为状态更新而翻转?
我们后来建立了“状态跃迁四象限审查法”,专门对付这类问题:
3.1 四象限定位法:把算法逻辑拆解为状态空间
| 状态维度 | 正常态(Safe) | 危险态(Danger) | 跃迁触发点(Trigger) | 跃迁防护点(Guard) |
|---|---|---|---|---|
| 预算状态 | spend < budget | spend >= budget | spend增量写入DB成功瞬间 | 在spend更新事务内,同步写入budget_status: EXHAUSTED字段,并加Redis分布式锁 |
| 库存状态 | stock > 0 | stock == 0 | 库存扣减RPC返回success | 扣减前先用Lua脚本原子性校验stock >= required,失败直接返回 |
| 权限状态 | user.role == "ADMIN" | user.role == "GUEST" | RBAC策略中心推送新权限 | 所有权限校验走@PreAuthorize注解,背后调用PermissionCacheService(本地缓存+Redis双写) |
这个表格不是摆设。每次AI生成算法代码,我们强制要求开发者用它填满四象限,填不出来的逻辑,一律打回重写。上周有个同事用AI写了“优惠券过期自动作废”逻辑,卡在“跃迁触发点”一栏写不出具体事件(是定时任务扫描?还是用户领券时校验?),我们就知道这逻辑根本没想清楚,果然上线后出现大量已过期券仍可核销的问题。
3.2 边界跃迁点的三重校验
针对上面提到的预算耗尽案例,我们最终落地的审查方案是:
事务内强一致性校验
把spend更新和status更新放在同一个数据库事务里:UPDATE advertiser_budget SET current_spend = current_spend + ?, status = CASE WHEN current_spend + ? >= budget THEN 'EXHAUSTED' ELSE status END WHERE id = ? AND status != 'EXHAUSTED';缓存穿透防护
AI生成的getBudget()方法默认走Redis缓存,但我们强制要求:- 缓存key必须包含
version字段(如budget:1001:v2) - DB更新时,用
DEL budget:1001:*清空所有版本缓存 - 缓存未命中时,走
@Transactional方法查DB,避免缓存雪崩
- 缓存key必须包含
状态机驱动校验
引入状态机框架(如Spring Statemachine),定义BUDGET_ACTIVE → BUDGET_EXHAUSTED跃迁条件:transition .source(BUDGET_ACTIVE) .target(BUDGET_EXHAUSTED) .event(EXHAUST_BUDGET_EVENT) .action((stateContext) -> { // 此处执行最终DB校验,确保跃迁前状态真实有效 if (!budgetRepo.isTrulyExhausted(stateContext.getEvent().getAdvertiserId())) { throw new BudgetNotExhaustedException(); } });
这套方法把AI的“静态判断”转化成了“动态防护网”。现在团队新人用AI写算法,第一件事不是跑测试,而是画四象限表——这比写100行代码更能暴露设计缺陷。
4. 场景三:AI生成的异常处理——审查“兜底行为”而非“try-catch语法”
AI写异常处理有个典型模式:看到IOException就加try-catch,看到NullPointerException就加if (obj != null),但对“catch之后做什么”几乎不思考。我们做过统计:AI生成的异常处理代码中,73%的catch块里只有e.printStackTrace()或空return,19%用log.error("error", e)但没做业务补偿,仅8%实现了真正的容错降级。
最典型的例子是消息队列消费失败处理。AI根据“消费者需保证消息至少被处理一次”,生成了这样的代码:
// AI生成代码 @RabbitListener(queues = "order_queue") public void processOrder(OrderMessage message) { try { orderService.createOrder(message); } catch (Exception e) { log.error("Order processing failed", e); // AI这里停住了,没写后续动作 } }上线后问题爆发:当orderService.createOrder()因数据库连接池满而抛出SQLException时,消息被RabbitMQ自动重发,但重试10次后进入死信队列。而AI生成的代码里,catch块什么都没做,导致订单创建失败却无任何告警,运营同学三天后才发现漏单。
关键洞察:AI的异常处理是“防御性”的,而人的审查必须是“进攻性”的——你要追问:这个异常发生后,业务上会发生什么?用户感知是什么?系统状态是否一致?有没有补偿路径?
我们为此制定了“异常处理五问审查法”,每个catch块必须回答:
- 这个异常发生的概率有多高?(查历史监控:过去30天该异常出现频次/总调用量)
- 如果放任不管,最坏业务后果是什么?(如:资金损失、用户投诉、监管处罚)
- 当前catch块里的动作,能否阻止最坏后果?(
log.error不能阻止资金损失,必须加补偿) - 有没有更上游的拦截点?(如:在消息入队前做幂等校验,比消费时处理更高效)
- 这个异常是否应该被转换为业务异常?(如:将
SQLException包装成OrderCreateFailedException,让调用方能针对性重试)
基于此,我们重构了消息消费的异常处理模板:
@RabbitListener(queues = "order_queue") public void processOrder(OrderMessage message) { try { orderService.createOrder(message); } catch (OrderCreateFailedException e) { // 业务异常:已知可重试场景,直接返回NACK触发重试 throw new AmqpRejectAndDontRequeueException(e); } catch (SQLException e) { // 系统异常:DB问题,需降级+告警+人工介入 fallbackToAsyncOrderCreation(message); // 异步创建,保证最终一致性 alertService.sendCriticalAlert("DB connection pool exhausted", e); throw new AmqpRejectAndDontRequeueException(e); // 防止消息堆积 } catch (Exception e) { // 未知异常:记录全量上下文,触发熔断 contextLogger.logFullContext(message, e); circuitBreaker.open(); // 熔断下游依赖 throw e; // 让框架处理 } }这个模板里,每个catch都对应明确的业务动作。而AI生成的原始代码,连第一个问题都答不上来——它根本不知道“SQLException”在我们系统里意味着什么。
5. 场景四:AI生成的安全校验——聚焦“信任链断裂点”而非“代码是否存在漏洞”
安全领域是AI最危险的重灾区。它能写出符合OWASP Top 10字面要求的代码,但对“信任边界”毫无概念。我们曾用AI生成“用户修改手机号”接口,它完美实现了短信验证码校验、新号码格式验证、旧号码脱敏显示,但漏掉了最关键的一环:没有校验当前登录用户是否拥有修改该账号手机号的权限。
代码长这样:
// AI生成代码 @PostMapping("/user/phone/update") public Result updatePhone(@RequestBody PhoneUpdateRequest request) { // 1. 校验短信验证码 if (!smsService.verifyCode(request.getPhone(), request.getCode())) { return Result.fail("验证码错误"); } // 2. 格式校验 if (!PhoneUtils.isValid(request.getPhone())) { return Result.fail("手机号格式错误"); } // 3. 更新DB userMapper.updatePhone(request.getUserId(), request.getPhone()); return Result.success(); }问题在于:request.getUserId()是前端传来的,AI默认它“可信”。而真实攻击场景中,攻击者会篡改这个ID,用自己账号的验证码去修改他人账号手机号。AI的代码里,userId根本没有和当前登录Session绑定。
核心原则:安全审查不是找“有没有XSS/SQL注入”,而是找“信任链在哪里断裂”。你要像黑客一样思考:这段代码里,哪些输入是我绝对不能相信的?哪些校验是必须前置的?
我们总结出“信任链三阶审查法”,专门针对AI生成的安全代码:
5.1 第一阶:识别所有外部输入源
对每个接口,列出所有可能被污染的输入点:
| 输入源 | 是否可信 | 验证方式 | AI常见疏漏 |
|---|---|---|---|
HTTP Header(如X-User-ID) | ❌ 不可信 | 必须与JWT token中的sub字段比对 | AI常直接取Header值,忽略token校验 |
URL Path Variable(如/user/{id}/profile) | ❌ 不可信 | 必须校验{id}是否属于当前登录用户 | AI常直接用@PathVariable Long id,不加权限注解 |
Request Body字段(如{"targetUserId":1001}) | ❌ 不可信 | 必须通过@Valid+自定义校验器,检查targetUserId是否在用户关系链中 | AI常只校验字段非空,不校验业务归属 |
5.2 第二阶:绘制信任传递路径图
以“修改手机号”为例,我们手动画出数据流:
[前端] → [HTTP Request] → [Controller] → [Service] → [DB] ↑ ↑ ↑ JWT Token @PreAuthorize SQL Parameter (可信源) (信任锚点) (需防注入)AI生成的代码,只在Controller层做了验证码校验(箭头中间的虚线),但没在Controller入口处设置@PreAuthorize("hasRole('USER') and #request.userId == authentication.principal.id"),导致信任链在第一步就断裂。
5.3 第三阶:强制注入“信任锚点”
我们在团队SOP里规定:所有涉及用户数据的操作,必须在Controller方法上声明以下至少一项:
@PreAuthorize表达式(Spring Security)@Secured角色注解- 自定义
@RequireOwnership注解(校验request.userId == currentUserId)
并且,这些注解必须出现在AI生成代码的最外层,而不是Service层。因为AI总爱把权限校验写进Service,而我们坚持:信任校验必须在边界完成,越早越好。
上周有个同事用AI写了“删除文章”接口,AI在Service里写了if (article.getAuthorId() != userId) throw new AccessDeniedException()。我们直接打回,要求改成:
@DeleteMapping("/articles/{id}") @PreAuthorize("@articleSecurityService.canDelete(#id, #principal.id)") public Result deleteArticle(@PathVariable Long id, Authentication principal) { articleService.delete(id); return Result.success(); }理由很直接:如果AI生成的Service方法被其他内部模块直接调用(绕过Controller),权限校验就失效了。而@PreAuthorize是Spring AOP织入的,无论怎么调用都生效。
6. 审查效率革命:用“AI审AI”构建人机协同审查流水线
既然AI写代码有固定模式缺陷,那何不用AI来专攻这些缺陷?我们团队花了两个月,把上述四个场景的审查要点,封装成一套“AI代码审查助手”,不是替代人,而是把人从重复劳动中解放出来,专注高价值决策。
这套系统分三层:
6.1 L1层:语法级自动化扫描(机器干,人不碰)
用定制化SonarQube规则集,覆盖AI高频错误:
AI-001:检测BETWEEN子句中decimal字段与double参数混用AI-002:标记未加@Transactional的数据库更新方法(AI常遗漏)AI-003:识别catch块中仅有log.error或空语句的代码AI-004:发现URL Path Variable未在@PreAuthorize中校验的Controller
这些规则全部开源在内部GitLab,新成员入职第一天就要学习如何解读扫描报告。关键是:所有L1层告警,必须由提交者4小时内修复,否则CI直接拒绝合并。这倒逼大家养成“写完AI代码立刻跑扫描”的习惯。
6.2 L2层:语义级智能提示(人机协同)
我们训练了一个轻量级BERT模型,专门分析AI生成代码的“意图-实现偏差”。比如当AI生成:
// AI意图:实现幂等下单 if (orderMapper.selectByTradeNo(request.getTradeNo()) != null) { return Result.success("already exists"); } orderMapper.insert(new Order(...));模型会提示:
⚠️ 检测到“幂等校验”意图,但存在竞态条件风险:select和insert之间可能被其他线程插入同tradeNo订单。建议改用INSERT ... ON DUPLICATE KEY UPDATE或分布式锁。
这个提示不是简单报错,而是给出可落地的替代方案。目前准确率82%,误报率控制在5%以内。更重要的是,它改变了团队协作方式——以前Code Review要花20分钟争论“要不要加锁”,现在AI先给出选项,人只需决策“选A还是B”。
6.3 L3层:场景级专家复核(人专注,AI不碰)
L3层是真正需要人类经验的地方,我们定义了三个“必须人工复核”的硬性场景:
- 资金/库存类变更操作:所有涉及
money、stock、quota字段的增删改,必须由资深开发+财务/运营代表双签 - 状态机跃迁逻辑:任何
status字段从ACTIVE→INACTIVE、PENDING→SUCCESS等变更,需提供状态流转图+异常回滚方案 - 跨域数据同步:当代码涉及调用第三方API、写入外部数据库、发MQ消息,必须附《数据一致性保障说明书》(含补偿机制、对账方案、超时策略)
这三层不是串联,而是并联:L1自动拦截低级错误,L2辅助决策,L3守住底线。实施三个月后,团队代码质量指标变化显著:
| 指标 | 实施前 | 实施后 | 变化 |
|---|---|---|---|
| 平均CR时长 | 42分钟/PR | 18分钟/PR | ↓57% |
| 生产环境P0故障数 | 3.2次/月 | 0.4次/月 | ↓87% |
| 新人代码一次通过率 | 61% | 89% | ↑46% |
最意外的收获是:新人成长速度加快了。以前他们要花半年才能建立“哪里容易出问题”的直觉,现在通过L2层的智能提示和L3层的复核文档,三个月就能掌握核心风险点。有个95后同事说:“以前看AI代码像看天书,现在看它像看错题本——AI把坑都挖好了,我就负责填。”
7. 最后一点真实体会:把“审查”变成“对话”,才是人不可替代的价值
写到这里,我想起上周和一位刚转行的测试工程师聊天。她抱怨:“AI写代码太快了,我还没学会看懂,它已经迭代五版了。” 我没急着讲方法论,而是让她打开VS Code,用Copilot生成一个“计算用户积分”的函数,然后我们一起做一件事:把AI当成一个实习生,对它写的每一行代码提问。
比如AI生成:
public int calculatePoints(User user) { return user.getBasePoints() * 2 + user.getBonusPoints(); }我们问:
- “为什么是乘2?这个系数在哪个业务文档里定义的?”
- “如果
user.getBonusPoints()返回null,会怎样?” - “积分计算需要考虑用户等级加成吗?这个函数里没体现。”
就这样,一行行问下去。十分钟后,她眼睛亮了:“原来不是代码有问题,是我没搞懂业务!” —— 这恰恰是AI永远做不到的事:理解业务背后的why,而不仅是what。
所以回到标题那个问题:“人还要把时间花在逐行审查上吗?” 我的答案是:不,但要把时间花在和AI深度对话上。逐行审查是工业时代的产物,而今天我们需要的是“意图对齐审查”——确认AI理解的需求,和你脑中真实的业务场景,是否在同一个维度上。
这种对话能力,没法被训练,只能靠经验积累。就像老厨师尝一口汤就知道盐放多了,不是靠仪器测量,而是味蕾记忆。你在支付系统里踩过的坑,在风控规则里熬过的夜,在资金对账时掉过的头发,都会变成一种直觉:当AI写出某段代码时,你心里会“咯噔”一下,知道哪里不对劲。
这种直觉,才是人真正的护城河。它不需要你记住所有Java语法,但需要你记得三年前那次因为小数点精度导致的百万级资损;它不要求你精通所有算法,但要求你清楚知道,当用户说“我要最快看到结果”时,“最快”在业务里究竟意味着什么。
所以别焦虑AI取代你。真正该警惕的,是那些把AI当黑盒、只管复制粘贴的人——他们不是被AI淘汰,而是被更懂如何与AI对话的人淘汰。