AI 代码审查漏报不能靠“再审一遍”解决。更可靠的做法是按数据与权限、状态与并发、异常与资源、测试与回滚四类风险复核;每类都用一个失败输入或并发场景验证,而不是只读 AI 给出的评论。

本文用“优惠券核销”改动做示例:代码表面只有十几行,AI 可能发现空值,却漏掉越权、重复核销和吞异常。你会得到一份可以贴进 Pull Request 的 AI 代码审查清单。它适合普通业务仓库,不替代安全团队对高风险系统的专业审计。
一、先接受一个事实:AI 审查不是完备证明
GitHub 在 Copilot代码审查说明 中明确提醒:工具不能保证发现所有问题,可能犯错,反馈必须由人验证。文档还列出某些不会被审查的文件类型,例如依赖管理文件、日志与 SVG。看见“无问题”并不代表整个变更面已经覆盖。
AI 代码审查的优势是快速扫描常见模式、解释 diff、建议测试;弱点是看不见未提供的业务规则,也可能缺少运行时状态、权限模型和外部系统语义。你需要把它定位为第一遍筛查,而不是合并许可。
| AI 审查能做什么 | AI 审查不能做什么 |
|---|---|
| 扫描常见语法与模式问题 | 保证发现所有缺陷 |
| 解释 diff、生成测试建议 | 替代业务规则确认 |
| 提示空值、类型、风格问题 | 验证运行时并发状态 |
| 快速覆盖大量文件 | 理解完整权限模型与外部系统语义 |
| 给出修改方向 | 替代安全审计与发布回滚设计 |
如果团队刚开始引入这类工具,可以先用 AI编程技能教程 统一提示与验证方式,再把高风险项固化为仓库规则。
二、四类风险复核总览
| 风险类别 | 核心问题 | 验证方式 |
|---|---|---|
| 数据与权限 | 调用者能否读取和修改这条记录? | 用他人 ID、不存在 ID、过期状态测试 |
| 状态与并发 | 两个请求同时执行,最终状态是什么? | 并发发起两次相同请求,只允许一条成功 |
| 异常与资源 | 数据库抛错时,日志、连接和返回码怎样? | 模拟超时、唯一约束、连接失败 |
| 测试与回滚 | 哪些测试阻止回归,发布失败怎样恢复? | 检查测试集、迁移脚本、回滚开关 |
一句话:AI 标出可疑点,人用失败输入和并发场景验证四类风险。
三、案例:看似正常的核销函数藏着哪些问题
假设本次改动新增下面的函数:
export async function redeemCoupon(db, couponId, userId) {
// 按主键查询优惠券,但没有校验归属
const coupon = await db.coupons.findById(couponId);
// 只判断是否存在
if (!coupon) return { ok: false, reason: 'not_found' };
// 只判断是否已使用,但没有原子性保证
if (coupon.used) return { ok: false, reason: 'used' };
try {
// 直接更新为已使用,条件只有 couponId
await db.coupons.update(couponId, { used: true, usedBy: userId });
return { ok: true };
} catch {
// 吞掉所有异常,没有日志、没有错误类型
return { ok: false, reason: 'failed' };
}
}
单次运行时它很像正确代码:查记录、判状态、更新、返回结果。但它存在四个问题:
- 越权风险:没有验证优惠券属于谁;
- 并发风险:两个请求可同时读到
used=false; - 异常风险:异常被压成统一字符串,日志和告警拿不到原因;
- 测试风险:测试若只覆盖成功路径,就不会暴露这些问题。
复核前先收集最小业务约束:优惠券归属规则、是否允许管理员代操作、数据库是否支持条件更新、失败是否可重试、核销成功后有哪些下游动作。AI 没拿到这些事实时,不应期待它猜出正确答案。
四、第一类:数据与权限风险
数据与权限复核要问“调用者能否读取和修改这条记录”。findById(couponId) 只按主键查询,没有把 userId 放进条件。攻击者只要猜到别人的券号,就可能代为核销。
更稳的查询要把租户、所有者或可见范围放进数据库条件,而不是查出后再靠 UI 隐藏。
// 把 ownerId 和 status 放进查询条件,避免越权
const coupon = await db.coupons.findOne({
id: couponId,
ownerId: userId, // 必须是当前用户拥有的券
status: 'active' // 只允许有效状态的券
});
还要测试不存在、属于别人、已过期、已禁用四种状态。权限测试应从不可信输入开始,不要只用管理员账号。
| 检查项 | 失败输入 | 预期结果 |
|---|---|---|
| 券不存在 | 随机 couponId | 返回 not_found |
| 券属于别人 | 他人 couponId | 返回无权限或 not_found |
| 券已过期 | status=expired | 拒绝核销 |
| 券已禁用 | status=disabled | 拒绝核销 |
| 管理员代操作 | 管理员 userId | 按业务规则单独授权 |
这只是示意接口,真实字段要以仓库模型为准。进行 AI 代码安全复核时,可把权限规则写进仓库级技能或指令;Codex插件与技能说明 能帮助理解规则如何被复用,但规则内容仍需团队给出。
五、第二类:状态与并发风险
“先读 used,再写 used=true”存在检查与写入分离。两个请求可能同时通过判断,随后都返回成功。正确方向是把条件和更新放进一个原子操作,让数据库只允许一方把 active 改为 used。
-- 原子更新:只有满足 id、owner_id、status 条件时才更新
UPDATE coupons
SET status = 'used',
used_by = :user_id,
used_at = CURRENT_TIMESTAMP
WHERE id = :coupon_id
AND owner_id = :user_id -- 防止越权
AND status = 'active'; -- 防止重复核销
应用层检查受影响行数:
- 等于 1:核销成功;
- 等于 0:重新查询并区分不存在、无权限或已核销。
并发测试至少同时发起两次相同请求,预期只能一条成功。
| 场景 | 旧写法结果 | 原子更新结果 |
|---|---|---|
| 两个请求同时核销 | 都可能成功 | 只有一条成功 |
| 券已核销 | 第二次返回 used | 受影响行数为 0 |
| 券属于别人 | 可能越权成功 | 条件不匹配,更新失败 |
| 数据库超时 | 统一返回 failed | 可区分超时并记录日志 |
若下游还要发积分或消息,需要事务、幂等键或 outbox,而不是把两个动作简单串起来。
状态复核还要画出允许的迁移,例如 active → used、active → expired。任何能从 used 回到 active 的入口都要单独授权和留审计记录。
六、第三类:异常与资源风险
空 catch 会丢掉数据库错误、超时和唯一约束信息。对外可以返回稳定错误码,对内必须保留异常类型、请求标识和关键业务键;日志不能写完整令牌或隐私数据。
try {
await db.coupons.update(couponId, { used: true, usedBy: userId });
return { ok: true };
} catch (error) {
// 对外返回稳定错误码,对内记录异常类型和请求标识
logger.error('coupon_redeem_failed', {
couponId,
userId,
errorType: error.name,
requestId
});
return { ok: false, reason: 'internal_error' };
}
连接、文件和锁要确认在异常路径释放,重试也要限制次数并只针对可恢复错误。
| 检查项 | 常见问题 | 修复方向 |
|---|---|---|
| 异常类型 | 空 catch 吞掉所有错误 | 记录 error.name 和关键业务键 |
| 日志内容 | 写入完整令牌或隐私数据 | 脱敏,只记录业务键 |
| 连接释放 | 异常路径未释放连接 | 使用 finally 或连接池管理 |
| 重试策略 | 无限重试或重试不可恢复错误 | 限制次数,只重试超时等可恢复错误 |
| 对外错误码 | 直接暴露数据库错误 | 返回稳定业务错误码 |
七、第四类:测试与回滚风险
测试与回滚复核关注“怎样证明修复有效,以及失败时怎样撤回”。
最低测试集包括:
| 测试用例 | 验证目标 |
|---|---|
| 合法核销 | 正常路径成功 |
| 他人优惠券 | 越权被拒绝 |
| 已核销 | 重复核销被拒绝 |
| 两请求并发 | 只有一条成功 |
| 数据库超时 | 异常被记录,返回稳定错误码 |
| 迁移字段非空 | 旧数据兼容,回滚脚本可执行 |
迁移字段若新增非空约束,还要验证旧数据和回滚脚本。配置开关的默认值、灰度范围与监控指标也应在 PR 中写明。
| 回滚检查项 | 要求 |
|---|---|
| 数据库迁移 | 有回滚脚本,旧数据可兼容 |
| 配置开关 | 默认值安全,可快速关闭 |
| 灰度范围 | 明确首批用户与监控指标 |
| 监控告警 | 核销失败率、异常类型可观测 |
| 发布失败 | 有明确恢复步骤和责任人 |
八、把四类风险变成 PR 复核清单
可以把四类复核转成四条追问:
- 哪个失败输入能证明权限条件生效?
- 两个请求同时执行,最终状态是什么?
- 数据库抛错时,日志、连接和返回码分别怎样?
- 哪些测试阻止问题回归,发布失败怎样恢复?
如果 AI 只给出风格建议,而四条都没有证据,AI 代码审查漏报风险仍然很高。
| 复核项 | 需要留下的证据 |
|---|---|
| 数据与权限 | 他人 ID、不存在 ID、过期状态的测试结果 |
| 状态与并发 | 并发请求日志或测试断言 |
| 异常与资源 | 错误日志样例、返回码、连接释放确认 |
| 测试与回滚 | 测试清单、迁移回滚脚本、灰度开关 |
| PR 结论 | 代码、测试、监控、回滚闭环 |

总结
AI 审查适合做第一遍扫描,却不能替代业务知识、并发验证和回滚设计。面对短小 diff,也要分别检查数据权限、状态并发、异常资源、测试回滚,因为严重缺陷往往不在语法层。
实操时先让 AI 标出可疑点,再由人把四类风险映射到失败输入和测试命令。最终合并依据应是代码、测试、监控与回滚都能闭环,而不是评论区出现一句“看起来没问题”。
延伸学习
- 需要在本地配置 Agent 时,先查 Codex安装指南;
- 想把复核步骤接进命令行,可对照 CodeBuddy CLI参考;
- 选工具前可阅读 AI编程助手横向对比,但审查责任不因产品变化而转移。
常见问题
Q:AI 审查没有评论,可以直接合并吗?
A:不可以。没有评论可能表示没有发现,也可能表示文件未覆盖或上下文不足。至少执行项目测试,并按四类风险完成一次人工复核。
Q:四类风险每次都要全部检查吗?
A:都要过一遍,但深度随变更调整。纯文案变更可很轻;涉及权限、资金、库存、并发或数据库迁移时,每类都应留下证据。
Q:怎样减少同一种漏报反复出现?
A:把业务不变量写成自动测试,把通用审查点写进仓库指令或技能,把高风险文件设置责任人。文字提醒只能辅助,测试才会持续阻断回归。
Q:AI 建议的修复也要复核吗?
A:要。修复建议仍是代码变更,可能引入性能、兼容或权限问题。应用后重新看 diff、运行测试,并确认改动没有超出原问题范围。

免费 AI IDE



