让 Claude Code 或 Cursor”重构一下这个 handler 让它更干净”,diff 看起来漂亮——50 行变 28 行——你 merge 了。两天后客服群里出现”老用户登录跳错页”或”safari 上传文件失败”——AI 把它判定为”看起来没用”的某个分支删掉了,但那个分支正是某个客户场景的兜底逻辑。
本文给你一条”用 bisect 精确定位删了什么、对照 prod 日志找出真正在跑的分支、把被删的逻辑加回来并补上测试防回归”的修复路径。
常见原因
按命中率排序。
1. AI 认为”没测试”就是”没用”
最常见的失败模式。Agent 看到一段 if (user.legacyRole === "admin") 没有覆盖测试,就判定为 dead code 删掉。但 legacy admin 在 prod 里有 2% 流量,删了立刻有人 401。
- if (user.legacyRole === "admin" || user.role === "admin") {
- return grantAdminAccess(user);
- }
+ if (user.role === "admin") {
+ return grantAdminAccess(user);
+ }
如何判断:被删的分支条件涉及 legacy、old、v1、fallback、deprecated 这些词,或者引用了不再活跃但仍存在的字段。
2. 边界 case 没写在 prompt 里
你说”简化这个 parser”,没说”必须保留对空字符串、Unicode 零宽字符、CRLF 的处理”。Agent 拿掉所有它觉得”防御性过头”的判空、字符规范化代码。
- if (!input || input.trim() === "") return defaultValue;
- input = input.normalize("NFC").replace(//g, "");
return parse(input);
如何判断:Prod 报 TypeError: Cannot read properties of undefined 或乱码错误,且错误堆栈指向被重构过的函数。
3. 重构范围太模糊
“清理一下这个文件”或”让代码更现代”这种 prompt 让 agent 自由发挥。它顺手把”看起来是临时 hack”的 retry / timeout / circuit breaker 一起删了——因为没注释解释为什么需要。
如何判断:被删代码周围没有解释性注释,且 git blame 显示该代码是某次 incident postmortem 后加的。
4. AI 把 feature flag 分支当成已废弃
- if (isFeatureEnabled("new-checkout", user)) {
- return newCheckoutFlow(user);
- }
- return oldCheckoutFlow(user);
+ return newCheckoutFlow(user);
Agent 假设 flag 已经 100% rollout 就把另一边删了;但其实 flag 还在 50%,删完一半用户立刻进了未充分测试的新路径。
如何判断:被删代码包含 isFeatureEnabled / getFlag / LaunchDarkly 类的开关判断。
5. AI 简化 error handling,把”沉默吞掉”也删了
某些 try { ... } catch (e) { /* intentionally swallowed */ } 是真的有意吞掉(比如 analytics 上报失败不能阻塞 checkout)。AI 看到空 catch 就改成 throw 或 logger.error,结果支付流程被 analytics 报错挂掉。
如何判断:日志里突然冒出大量之前没有的 ERROR 级别条目,且业务功能反而开始挂。
6. AI 删了”看起来重复”的方法但其实签名不同
Agent 把 getUserById(id: string) 和 getUserById(id: number) 重载合并成一个,删掉了字符串 ID 的兜底解析。但旧 URL 还带字符串 ID,直接 404。
如何判断:TypeScript 报 overload 错或 runtime 报 id is undefined。
最短修复路径
Step 1:跑完整测试套件,先看挂了哪些
npm test -- --reporter=verbose 2>&1 | tee /tmp/test.log
但不要只信测试——如果是”没测试覆盖的分支被删”,测试反而全绿。同时跑:
# E2E / 集成测试
npm run test:e2e
# 手工 QA 关键场景(按业务列清单跑一遍)
Step 2:从 prod 日志反查真正在跑哪些分支
打开过去 7 天的 prod 日志,搜被改文件相关的入口:
# 例如:找最近 7 天调用 checkout handler 的 user agent / role 分布
grep "POST /api/checkout" /var/log/app.log | awk '{print $7}' | sort | uniq -c
输出会告诉你”实际在跑的代码路径分布”——比如发现 8% 流量 user-agent 是 IE/Safari,但 AI 重构后把那个分支删了。
Step 3:用 git bisect 定位回归 commit
如果回归是在多个 commit 之内引入的,bisect 是最快办法:
git bisect start
git bisect bad HEAD # 当前坏
git bisect good v1.2.3 # 上个已知好的 tag
git bisect run npm test -- failing-spec
# 或者手工:每轮 git bisect good/bad
bisect 自动二分,几轮就定位到引入问题的那个 AI commit。
Step 4:对比 diff,把被删的逻辑加回来
git show <bad-commit> -- src/checkout/handler.ts
逐段看被删的代码,按下面表格决定:
| 被删代码 | 处理 |
|---|---|
| 真的是 dead code | 留着 deleted,写 commit 解释为什么确定无用 |
| 还在用但写法过时 | 现代化重写,但保留行为 |
| 处理某个边界 case | 直接 revert 该 hunk |
| Feature flag 分支 | 查 flag 状态;没 100% rollout 就 revert |
revert 单个 hunk:
git checkout <good-commit> -- src/checkout/handler.ts
# 或精确到 hunk
git checkout -p <good-commit> -- src/checkout/handler.ts
Step 5:补上测试再 commit
被 AI 删了一次的逻辑,下次还会被删。立刻补上测试:
// 例:legacy admin 必须能登录
test("legacy admin role still grants access", () => {
const user = { id: 1, legacyRole: "admin" };
expect(authorize(user)).toBe(true);
});
测试名要说清”为什么”——不只是 “test legacy admin”,而是 “legacy admin role still grants access (regression: removed in refactor 2026-05-22)“。
预防建议
- 只让 AI 重构有测试覆盖的代码——没测试就先补测试再重构
- Prompt 显式列出”必须保留的行为”:
"保留对空字符串 / IE11 / legacy admin 的处理" - 重要的边界处理加
// IMPORTANT: handles X case from incident #1234,agent 看到注释会更谨慎 - 在 CLAUDE.md / AGENTS.md 写:“禁止删除有注释、引用 feature flag、或包含 ‘legacy’ / ‘fallback’ 关键字的代码分支,除非显式被要求”
- 大重构强制走 PR,diff 里超过 30 行删除就 require 至少一个人 review
- 接入 AI 提交前审查工作流,让 agent 先自己 diff 解释每一段删除的理由