我让 AI 加了一个开关,结果代码走了原本不该走的分支

大家好,我是桦说编程。

最近 code review 发现一个 claude code 写的 bug,感兴趣的读者可以自己试一试能否复现。

真实 case:让 AI 给守卫式分支链加开关,AI 把开关 AND 进了原变量。代码读起来还很顺,但守卫语义已经悄悄塌了。本文复盘这种"语义漂移"为什么是 AI 改代码的高频陷阱,以及人类 reviewer 应当盯什么。

问题背景

业务侧有这样一个守卫式分支链(伪代码,已把场景脱敏成"VIP 用户走快速通道,普通用户走促销校验"):

arduino 复制代码
boolean isVipUser = vipUserSet.contains(userId);
if (isVipUser) {
    // VIP 用户:走快速通道
} else if (isWhitelisted(...)) {
    // 白名单:信任配置值
} else if (inPromotion(...)) {
    // 普通用户 + 命中促销:走促销价校验
} else {
    // 标准流程
}

isVipUser 是一道守卫 :VIP 被第一个 if 截胡,永远不会跌进下游分支。下游"促销校验"分支隐式依赖这个事实------只有非 VIP 才走得到。

改动需求:加一个开关

需求很直白:"给 VIP 快速通道加一个开关 vipFastPathEnable,关闭时 VIP 也走原行为。"

AI 拿到这个需求,最短路径是:

arduino 复制代码
boolean isVipUser = fastPathSwitch && vipUserSet.contains(userId);   // 改了语义,没改名
if (isVipUser) { ... }
else if (isWhitelisted(...)) { ... }
else if (inPromotion(...)) { ... }
else { ... }

代码读起来很顺,编译通过,开关打开时行为完全正确。问题在开关关闭时------

isVipUser 此时已经不是"是不是 VIP"的回答,而是"是不是要走快速通道"的回答。变量名是事实,值是决策。 VIP 用户关掉开关后,isVipUser = false,跌入下游"促销校验"分支,被当成普通用户走促销逻辑------守卫失效。

修复:拆成两个变量

arduino 复制代码
boolean isVipUser = vipUserSet.contains(userId);                   // 事实
boolean applyVipFastPath = fastPathSwitch && isVipUser;            // 决策
if (applyVipFastPath) { ... }
else if (isWhitelisted(...)) { ... }
else if (!isVipUser && inPromotion(...)) { ... }                   // 用事实做守卫
else { ... }

两件事变了:

  1. 事实和决策分离isVipUser 回答客观事实,applyVipFastPath 回答策略问题。
  2. 下游守卫显式化 。原本依赖"上一个 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,看到守卫式分支链有改动,盯这几条:

  1. 变量定义改了,使用点没动? 红旗。大概率是语义偷换。
  2. 开关 / 配置 / 业务策略 有没有混进纯事实 的变量?混了就拆。建议命名约定:事实变量用 isXxx / hasXxx,决策变量用 shouldXxx / applyXxx / enableXxx
  3. else if 链的每个分支隐式假设是什么? 逐条问"如果上面所有 if 都不进,剩下的子集是什么"。如果剩下的子集和这个分支的语义对不上,把守卫显式化。
  4. 开关关闭后,原本走 A 分支的 case 会跑到哪个分支?那个分支接得住吗? 这是开关类改动的标准测试维度,单测里至少覆盖"开关 ON / OFF × 守卫命中 / 不命中"四象限。
  5. 变量名是契约。语义变了就改名,改名了就 grep 所有 use site 确认每一处都还讲得通。

总结

  • AI 改代码最常见的隐形 bug 之一:给变量加约束时不改名字,把"事实"偷换成"决策"
  • 守卫式分支链对前置条件变量极其敏感------isVipUser 含义一变,下游所有"普通用户走这里"的隐式假设全部塌方。
  • 修复模板:事实变量保留客观语义;决策 / 开关 / 策略走第二个变量;下游守卫从"隐式依赖上一个 if 的 false"升级为"显式 !isFact && 守卫"。
  • Reviewer 的红旗:变量定义改了但所有使用点没动;开关类改动没有覆盖"开关 OFF + 原命中"用例。

如果这篇文章对你有帮助,欢迎关注我,持续分享高质量技术干货,助你更快提升编程能力。

相关推荐
fly spider1 小时前
AI 到底是怎么访问网页的?从爬虫、Browser Agent 到 Computer Use
人工智能·爬虫
Lee川2 小时前
RAG 实战:从一篇掘金文章出发,拆解检索增强生成的全链路
前端·人工智能·后端
码农小旋风2 小时前
Codex小白入门使用教程
人工智能·chatgpt·claude
Lee川2 小时前
MCP 高德地图实战:当 AI 学会使用工具,一个协议如何重塑大模型的行动边界
前端·人工智能·后端
凌杰2 小时前
AI 学习笔记:Agent 的应用演示
人工智能
程序员cxuan2 小时前
Codex 把我家烂网给优化后,我 TM 直接原地起飞了。
人工智能·后端·程序员
IT_陈寒2 小时前
Redis批量删除踩了坑,原来DEL命令不是万能的
前端·人工智能·后端
xinhuanjieyi2 小时前
gpt-sovits测试语音克隆
人工智能·gpt
星辰AI2 小时前
Transformers 架构核心原理:从注意力机制到 GPT
人工智能·ai·语言模型