摘要: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 档代码时,不看代码本身,看两件事——
- 这个文件有没有被类型检查覆盖?(项目里有没有 tsconfig 的 include 漏掉的文件)
- 这个文件的 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 前扫一遍):
- 这个代码的信任等级是什么?(A/B/C)→ 决定 review 深度
- 如果是 A 档:测试链完整吗?→ 完整就 merge
- 如果是 B 档:AI 的代码里有没有"假设了但没告诉它"的东西?→ 三步法走一遍
- 如果是 C 档:逐行看完了吗?→ 额外验证场景遗漏
这套策略的边界
不是所有场景都适用。
遗留系统没有测试基础设施。A 档不 review 的前提是类型检查和 snapshot 测试到位。如果项目没有 TypeScript strict mode,没有 Jest 配置,A 档也得逐行 review。说白了,这套策略依赖前面的防线——没有防线,review 就得加码。
纯 UI 组件不适用。我前面说的"不 review 代码"是针对逻辑代码的。UI 组件的 review 是另一套——看布局、看交互、看状态管理,不能套用"三步法"。
“不 review 代码"不是"不 review”。A 档和 B 档不看代码语法和逻辑细节,但还是要看代码结构——变量命名、函数拆分、模块划分。这些是代码的可维护性,跟 AI 会不会写错没关系。
总结一下
回头看这个系列的四篇文章,其实都在回答同一个问题:你的验证能力决定了你能放权多少。
防线告诉你"怎么兜底",信任分级告诉你"哪些代码敢放",测试策略告诉你"写完了怎么测",review 告诉你"你怎么审"。
你的验证能力越强,你能放权给 AI 的就越多。这个逻辑跟 AI 本身没关系——跟人的工程能力有关系。