大家好,我是桦说编程。
最近 code review 发现一个 claude code 写的 bug,感兴趣的读者可以自己试一试能否复现。
真实 case:让 AI 给守卫式分支链加开关,AI 把开关 AND 进了原变量。代码读起来还很顺,但守卫语义已经悄悄塌了。本文复盘这种"语义漂移"为什么是 AI 改代码的高频陷阱,以及人类 reviewer 应当盯什么。
问题背景
业务侧有这样一个守卫式分支链(伪代码,已把场景脱敏成"VIP 用户走快速通道,普通用户走促销校验"):
boolean isVipUser = vipUserSet.contains(userId);
if (isVipUser) {
// VIP 用户:走快速通道
} else if (isWhitelisted(...)) {
// 白名单:信任配置值
} else if (inPromotion(...)) {
// 普通用户 + 命中促销:走促销价校验
} else {
// 标准流程
}
isVipUser 是一道守卫:VIP 被第一个 if 截胡,永远不会跌进下游分支。下游"促销校验"分支隐式依赖这个事实——只有非 VIP 才走得到。
改动需求:加一个开关
需求很直白:"给 VIP 快速通道加一个开关 vipFastPathEnable,关闭时 VIP 也走原行为。"
AI 拿到这个需求,最短路径是:
boolean isVipUser = fastPathSwitch && vipUserSet.contains(userId); // 改了语义,没改名
if (isVipUser) { ... }
else if (isWhitelisted(...)) { ... }
else if (inPromotion(...)) { ... }
else { ... }
代码读起来很顺,编译通过,开关打开时行为完全正确。问题在开关关闭时——
isVipUser 此时已经不是"是不是 VIP"的回答,而是"是不是要走快速通道"的回答。变量名是事实,值是决策。 VIP 用户关掉开关后,isVipUser = false,跌入下游"促销校验"分支,被当成普通用户走促销逻辑——守卫失效。
修复:拆成两个变量
boolean isVipUser = vipUserSet.contains(userId); // 事实
boolean applyVipFastPath = fastPathSwitch && isVipUser; // 决策
if (applyVipFastPath) { ... }
else if (isWhitelisted(...)) { ... }
else if (!isVipUser && inPromotion(...)) { ... } // 用事实做守卫
else { ... }
两件事变了:
- 事实和决策分离。
isVipUser回答客观事实,applyVipFastPath回答策略问题。 - 下游守卫显式化。原本依赖"上一个 if 不进 = 非 VIP"的隐式前提,现在写成
!isVipUser &&显式守卫。
显式守卫这一步同样关键。即使你只在变量定义处拆了名字,不动下游 else if,只要开关关闭,VIP 仍会跌入"促销校验"——因为下游分支条件没说"我只接非 VIP",它接所有上游没截走的。守卫是分布式的,集中改一处不够。
为什么 AI 容易踩这个坑
归纳几条:
1. LLM 改代码倾向于"最小局部修改"。 看到"加开关",最短路径就是在变量定义处 AND 一个 bool。它没动 if/else if 链,是因为下游"看起来还能跑"——实际下游的隐式前提已经被破坏。
2. 守卫语义不写在代码里。 else if (inPromotion) 没写"我只接非 VIP",那个前提住在上一个 if (isVipUser) 的 false 分支里。LLM 看不到没写出来的契约。
3. 命名是契约,但 LLM 不天然守约。 isVipUser 这个名字承诺了它回答事实,AI 改完后它回答决策——名字没改,承诺被毁。如果 v2 把变量改名成 shouldUseFastPath,bug 在命名阶段就会暴露,因为下游 else if (inPromotion) 的注释 "// 普通用户走这里" 会立刻变得讲不通。
4. 加开关时人类的本能错觉。 "我只是加了一道筛子,原来能进的现在更少进"——所以分支链应该更安全才对。错。守卫分支链不是筛子,是路由。第一个分支"少进"意味着下游分支"多进"。下游接得住吗? 这是改任何守卫链时必须问的问题。
给人类 reviewer 的 checklist
收到 AI 提交的 PR,看到守卫式分支链有改动,盯这几条:
- 变量定义改了,使用点没动? 红旗。大概率是语义偷换。
- 开关 / 配置 / 业务策略有没有混进纯事实的变量?混了就拆。建议命名约定:事实变量用
isXxx/hasXxx,决策变量用shouldXxx/applyXxx/enableXxx。 - else if 链的每个分支隐式假设是什么? 逐条问"如果上面所有 if 都不进,剩下的子集是什么"。如果剩下的子集和这个分支的语义对不上,把守卫显式化。
- 开关关闭后,原本走 A 分支的 case 会跑到哪个分支?那个分支接得住吗? 这是开关类改动的标准测试维度,单测里至少覆盖"开关 ON / OFF × 守卫命中 / 不命中"四象限。
- 变量名是契约。语义变了就改名,改名了就 grep 所有 use site 确认每一处都还讲得通。
总结
- AI 改代码最常见的隐形 bug 之一:给变量加约束时不改名字,把"事实"偷换成"决策"。
- 守卫式分支链对前置条件变量极其敏感——
isVipUser含义一变,下游所有"普通用户走这里"的隐式假设全部塌方。 - 修复模板:事实变量保留客观语义;决策 / 开关 / 策略走第二个变量;下游守卫从"隐式依赖上一个 if 的 false"升级为"显式
!isFact &&守卫"。 - Reviewer 的红旗:变量定义改了但所有使用点没动;开关类改动没有覆盖"开关 OFF + 原命中"用例。
如果这篇文章对你有帮助,欢迎关注我,持续分享高质量技术干货,助你更快提升编程能力。