AI 代码 Review 实战:从看代码对不对到看 AI 懂不懂,我踩了一个坑之后调整了 review 方式
2026/8/26 19:55:59 网站建设 项目流程

摘要:AI 写代码越来越快,但 review 方式不能照搬人代码那套——否则你会漏掉最致命的一类 bug。本文从一次"搜索缓存键粒度不够"的翻车出发,总结了 AI 代码的两种 bug 类型(“写错了"和"不知道”),给出了三档 review 策略(A 档 review 测试链、B 档 review 上下文、C 档逐行 review),以及一套可操作的"三步法"——列出假设、验证假设、修复假设。文末附带可直接复用的 review 检查清单和决策表。

文章目录

    • 一个搜索缓存翻车了
    • 为什么这个翻车在 review 时被漏掉了?
    • AI 代码的 bug 有两种
    • 三档 review 策略
      • A 档:不 review 代码,review 测试链是否完整
      • B 档:review 上下文,不 review 代码
      • C 档:逐行 review + 验证上下文理解
    • 三步法:怎么"review 上下文"
      • 步骤 1:列出 AI 代码里的所有"假设"
      • 步骤 2:验证每个假设是否成立
      • 步骤 3:对不成立的假设,补代码或补注释
    • Review 决策表
    • 这套策略的边界
    • 总结一下

一个搜索缓存翻车了

前阵子给一个搜索功能加缓存。需求不复杂——用户搜商品,按关键词和分类筛选,把结果缓存起来减少数据库查询。

AI 写的代码,我看了一遍,觉得没问题就 merge 了。

上线第二天,用户投诉说"搜’手机-苹果’和’手机-小米’出来一样的结果"。

排查过程挺直接的。先看调用方传的参数——第一次搜"手机-苹果"(keyword=手机, category=苹果),第二次搜"手机-小米"(keyword=手机, category=小米)。参数没问题。

再看缓存代码。AI 的实现是这样的:

// 为什么要看这段:缓存逻辑本身是对的,问题出在缓存键的设计上functionsearchProducts(keyword:string,category?:string,minPrice?:number){constcacheKey='search:'+keywordconstcached=cache.get(cacheKey)if(cached)returncachedconstresults=awaitdb.query('SELECT * FROM products WHERE name LIKE ? AND category = ? AND price >= ?',[`%${keyword}%`,category,minPrice])cache.set(cacheKey,results,{ttl:300})returnresults}
// 翻车时的运行结果 用户A: searchProducts('手机', '苹果') → 缓存未命中,查数据库,写入缓存 key='search:手机' → 返回 [iPhone15, 小米14, 华为Mate60...] 用户B: searchProducts('手机', '小米') → 缓存命中 key='search:手机' → 返回 [iPhone15, 小米14, 华为Mate60...] ← 本应只返回小米14

问题找到了——缓存键只用了 keyword,没把 category 纳进去。用户搜"手机-苹果"时,缓存写入了 key=‘search:手机’。用户搜"手机-小米"时,缓存命中了同一个 key,返回了"手机-苹果"的结果。

但有意思的是,AI 的代码逻辑本身没问题——缓存读写、过期时间、清理策略都是对的。问题出在缓存键的粒度不够

修复后我意识到:不是 AI 写错了,是我 review 时没问对问题。这直接催生了后面的"三步法"。

为什么这个翻车在 review 时被漏掉了?

我复盘了一下当时 review 的心理活动。我看的是:

  • 缓存读写逻辑对不对?√
  • 过期时间设置合理不合理?√
  • 有没有做缓存穿透防护?√
  • 异常处理有没有?√

这些全是"缓存逻辑"层面的东西。我没去问一个问题:“AI 对缓存键的粒度的假设,在我们这个场景下成立吗?”

这个问题的本质是:我的 review 注意力在"代码对不对",而不是"AI 知不知道"。

这不是我粗心。我 review 人代码也是这个习惯——看逻辑对不对、边界处理了没、异常捕获了没。这套方法 review 人代码很有效,因为人代码的 bug 就出在这几个地方。但到了 AI 代码,这套方法就不够了。

AI 代码的 bug 有两种

我后来想明白一件事:AI 代码的 bug 跟人代码的 bug,不是一个维度的东西。

bug 类型人代码AI 代码
逻辑错误写错分支条件、算错索引、变量名混淆同样会出现,但频率比人低
边界遗漏没处理 null、没考虑空数组、没做类型校验偏中等,训练数据里边界场景覆盖率决定
上下文缺失很少——人知道自己在做什么项目最常见——AI 不知道业务规则、不知道数据分布、不知道外部系统限制
假设偏差同事会问"这个假设合理吗"AI 不会问,它直接按"最可能"的路径写

人代码的 bug 集中在"写错了"这个象限。AI 代码的 bug 分两种——“写错了"和"不知道”

"写错了"类(API 调错、语法错误、逻辑写反)跟人代码一样,按老方法 review 就能发现。但"不知道"类是 AI 特有的——AI 不知道 null 会传进来、不知道数据不满足类型定义、不知道这个 API 有调用频率限制、不知道缓存键要区分用户。

大多数人 review AI 代码时,只做了第一步(查"写错了"),没做第二步(查"不知道")。

一个简单的判断方法:如果这段代码换个有经验的同事来写,会不会写出同样的逻辑?会 → 大概率是"写错了";不会 → 大概率是"不知道"。
不过这个观点主要适用于日常的业务逻辑代码(B 档)。对于支付、权限这类代码(C 档),AI 的"写错了"风险也不低,该逐行 review 还是得逐行。

三档 review 策略

第二弹的信任分级已经定了"哪些代码敢放权",第三弹的测试策略定了"怎么测",这一弹补上"放权之后怎么 review"。

三档不是新发明的,是在信任分级的三档框架下,补充每个档位 review 的具体方法。

A 档:不 review 代码,review 测试链是否完整

A 档包括纯函数、工具函数、常量定义、脚手架代码。信任分级里说 A 档自动 merge,但前提是测试链完整

什么叫测试链完整?类型检查(TypeScript strict mode)+ snapshot 测试到位。如果项目没有类型检查,或者没有 snapshot 测试,那 A 档也不能不 review,因为没人兜底。

我自己的做法是:review A 档代码时,不看代码本身,看两件事——

  1. 这个文件有没有被类型检查覆盖?(项目里有没有 tsconfig 的 include 漏掉的文件)
  2. 这个文件的 snapshot 测试是不是稳定的?(频繁变动的 snapshot 测试失去意义)

如果这两件事都 OK,直接 merge,不花时间看代码。

B 档:review 上下文,不 review 代码

B 档包括业务逻辑、API 封装、数据转换。这是 AI 写代码的主力档位,也是"不知道"类 bug 最常见的地方。

review 方法:不看代码语法和逻辑,看 AI 对上下文的假设。

具体操作就是下一节的三步法。这里先给一个直觉——你在 B 档 review 时,把自己当成"这个项目的新人",而不是"reviewer"。新人会问的问题,就是你 review 要回答的问题:

  • “这个字段一定有值吗?”
  • “这个 API 一定有数据返回吗?”
  • “这个缓存的调用方是谁?”

C 档:逐行 review + 验证上下文理解

C 档包括支付、权限、事务边界、数据回填。出事成本高,review 不能偷懒。

逐行看代码逻辑,跟 review 人代码一样。额外加一步:验证 AI 有没有遗漏关键场景。比如 AI 写了个支付回调处理函数,逐行看完了逻辑,还要问一句:“有没有什么场景是 AI 没考虑的?”——比如重复回调、超时回滚、部分成功。

三步法:怎么"review 上下文"

这是我从那次翻车之后沉淀的方法,目前用下来没再漏过类似的"不知道"类 bug。

传统的 review 是"看代码"的视角,三步法切到"看假设"的视角,区别大概是这样的:

传统review: 看代码逻辑 → 看边界处理 → 看异常捕获 → merge ↓ AI review三步法: 列出假设 → 验证假设 → 修复假设 ↓ 核心差异:你review的不是代码,是AI的理解

步骤 1:列出 AI 代码里的所有"假设"

逐行看代码,标注出"AI 默认了但没写在代码里"的东西。

还是用翻车那个缓存键的例子:

// AI 写的代码,逐行标注假设functionsearchProducts(keyword:string,category?:string,minPrice?:number){// 假设1:keyword 一定有值 ← 参数有默认值吗?调用方会不会传空字符串?constcacheKey='search:'+keyword// 假设2:只用 keyword 做缓存键就够了 ← category 和 minPrice 不影响查询结果?constcached=cache.get(cacheKey)if(cached)returncached// 假设3:缓存里有数据就一定是对的 ← 缓存过期时间够吗?// 假设4:数据库一定能查到数据 ← 查不到怎么办?constresults=awaitdb.query(...)cache.set(cacheKey,results,{ttl:300})returnresults}
// 标注出来的假设列表 假设1:keyword 一定有值 假设2:只用 keyword 做缓存键就够了 假设3:缓存里有数据就一定是对的 假设4:数据库一定能查到数据

步骤 2:验证每个假设是否成立

对每个假设,问三个问题:

  • 问调用方:这个假设在我们这个场景下成立吗?
  • 查数据:实际数据满足这个假设吗?
  • 看文档:有没有文档说这个假设不成立的情况?

如果某个假设不成立但暂时无法修复(比如依赖外部系统),至少要在代码里加注释标明,避免后续维护的人踩同一个坑。回到翻车案例:

假设验证结果结论
keyword 一定有值调用方是前端搜索框,空字符串会被前端拦截✅ 成立
只用 keyword 做缓存键就够了调用方会传 category 和 minPrice,不同分类返回不同数据不成立
缓存里有数据就一定是对的数据不频繁变更,300 秒 TTL 够用✅ 成立
数据库一定能查到数据查不到返回空数组,不是异常✅ 成立

步骤 3:对不成立的假设,补代码或补注释

找到不成立的假设后,修复代码或补注释说明。

// 修复后的代码:把 category 和 minPrice 纳入缓存键functionsearchProducts(keyword:string,category?:string,minPrice?:number){// 构建缓存键时,把所有影响查询结果的参数都包含进去constcacheKey=`search:${keyword}:${category??'all'}:${minPrice??'0'}`constcached=cache.get(cacheKey)if(cached)returncachedconstresults=awaitdb.query('SELECT * FROM products WHERE name LIKE ? AND category = ? AND price >= ?',[`%${keyword}%`,category,minPrice])cache.set(cacheKey,results,{ttl:300})returnresults}
// 修复后运行结果 用户A: searchProducts('手机', '苹果') → 缓存未命中,查数据库,写入缓存 key='search:手机:苹果:0' → 返回 [iPhone15, 华为Mate60...] 用户B: searchProducts('手机', '小米') → 缓存未命中,查数据库,写入缓存 key='search:手机:小米:0' → 返回 [小米14, 红米Note13...] 两个结果不再互相覆盖 ✅

三步法说穿了就是"逐行标注假设 → 验证假设 → 修复假设"。但这三步做下来,比直接看代码逻辑多花 5-10 分钟,能把你从"代码对不对"的惯性里拽出来,切换到"AI 懂不懂"的视角。

Review 决策表

这是我现在用的 review 决策表,写文章时改了改,去掉了项目敏感信息:

档位典型代码review 什么不 review 什么耗时典型翻车案例
A工具函数、常量、类型定义、脚手架测试链是否完整(类型检查+snapshot)代码逻辑本身2-3 分钟snapshot 测试没覆盖到新文件,类型错误漏过
B业务逻辑、API 封装、数据转换、缓存上下文假设(三步法)语法、逻辑、代码风格5-10 分钟缓存键粒度不够(本文案例)
C支付、权限、事务边界、数据回填逐行看代码 + 验证上下文理解跳过任何东西15-30 分钟支付回调没处理重复通知

Review 检查清单(review 前扫一遍):

  1. 这个代码的信任等级是什么?(A/B/C)→ 决定 review 深度
  2. 如果是 A 档:测试链完整吗?→ 完整就 merge
  3. 如果是 B 档:AI 的代码里有没有"假设了但没告诉它"的东西?→ 三步法走一遍
  4. 如果是 C 档:逐行看完了吗?→ 额外验证场景遗漏

这套策略的边界

不是所有场景都适用。

遗留系统没有测试基础设施。A 档不 review 的前提是类型检查和 snapshot 测试到位。如果项目没有 TypeScript strict mode,没有 Jest 配置,A 档也得逐行 review。说白了,这套策略依赖前面的防线——没有防线,review 就得加码。

纯 UI 组件不适用。我前面说的"不 review 代码"是针对逻辑代码的。UI 组件的 review 是另一套——看布局、看交互、看状态管理,不能套用"三步法"。

“不 review 代码"不是"不 review”。A 档和 B 档不看代码语法和逻辑细节,但还是要看代码结构——变量命名、函数拆分、模块划分。这些是代码的可维护性,跟 AI 会不会写错没关系。

总结一下

回头看这个系列的四篇文章,其实都在回答同一个问题:你的验证能力决定了你能放权多少。

防线告诉你"怎么兜底",信任分级告诉你"哪些代码敢放",测试策略告诉你"写完了怎么测",review 告诉你"你怎么审"。

你的验证能力越强,你能放权给 AI 的就越多。这个逻辑跟 AI 本身没关系——跟人的工程能力有关系。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询