让 Claude Code 或 Cursor”重构一下这个 handler 让它更干净”,diff 看起来漂亮——50 行变 28 行——你 merge 了。两天后客服群里出现”老用户登录跳错页”或”Safari 上传文件失败”——AI 把它判定为”看起来没用”的某个分支删掉了,但那个分支正是某个客户场景的兜底逻辑。
最快的修法:用 git bisect 定位到那个 AI commit,然后用 git restore --source=<good-commit> -p <file> 只把被删的 hunk 加回来(不必整体 revert 整个重构)。再补一条带名字的回归测试,让这个分支以后不会被悄悄删掉。本文余下部分讲的是:在决定保留什么之前,怎么先确认 prod 里真正在跑的是哪些分支。
这不是小概率事件。Agent 编辑器对”整理代码”非常激进,2026 年初有不少广为报道的案例(包括 Cursor 3 的一个静默 revert/删除 bug),逻辑就这么从 diff 里消失而用户毫无察觉。把任何一次大型 agent 重构都当成”未经验证”来对待,直到你确认过线上真实代码路径为止。
常见原因
按命中率排序。
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 是最快办法。它走二分查找,所以即便是 1000 个 commit 的区间,也大约 10 次 checkout 就能收敛。
git bisect start
git bisect bad HEAD # 当前坏
git bisect good v1.2.3 # 上个已知好的 tag
# 自动模式:跑一个命令,退出码 0 = good,非 0 = bad
git bisect run npm test -- failing-spec.test.ts
git bisect reset # 结束后回到 HEAD
git bisect run 的约定是基于退出码的:脚本对”好”的 commit 退出 0,对”坏”的退出 1-124(或 126/127),退出 125 表示这个 commit 无法测试、让 bisect 跳过(比如根本编译不过)。如果没有任何测试能复现这个 bug(被删的分支没测试覆盖——这正是本文最常见的情况),就改成手工 bisect:每轮亲手验证线上行为,再敲 git bisect good 或 git bisect bad。
几轮就能定位到引入问题的那个 AI commit。完整状态机见 git-bisect 官方文档。
Step 4:对比 diff,把被删的逻辑加回来
git show <bad-commit> -- src/checkout/handler.ts
逐段看被删的代码,按下面表格决定:
| 被删代码 | 处理 |
|---|---|
| 真的是 dead code | 留着 deleted,写 commit 解释为什么确定无用 |
| 还在用但写法过时 | 现代化重写,但保留行为 |
| 处理某个边界 case | 把该 hunk 原样加回 |
| Feature flag 分支 | 查 flag 状态;没到 100% rollout 就加回 |
整文件回退,或只挑你要的 hunk:
# 整个文件回到 good 版本
git restore --source=<good-commit> src/checkout/handler.ts
# 逐 hunk 交互(y 保留某段被删的代码,n 跳过)
git restore --source=<good-commit> -p src/checkout/handler.ts
git restore --source=<commit> -p <file> 是当前推荐的、语义更正确的写法(Git 2.23+);旧版 git checkout -p <good-commit> -- <file> 在老 Git 上效果完全一样。在交互提示里按 s 可以把一个大 hunk 拆成更小的几块,这样你就能只加回被删的那个分支,而保留 AI 那些合理的清理。patch 模式的按键见 git-restore 文档。
Step 5:补上测试再 commit
被 AI 删了一次的逻辑,下次还会被删。立刻补上测试:
// 例:legacy admin 必须能登录
test("legacy admin role still grants access (regression: removed in refactor 2026-05-22)", () => {
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)“。
怎么确认真的修好了
别只看”测试全绿”——这个 bug 之所以存在,就是因为那个分支没有测试。三条都要确认:
- 新加的回归测试在坏 commit 上会失败、在修复后会通过。 快速验证:
git stash暂存你的修复,跑新测试(应该失败),git stash pop还原,再跑(应该通过)。如果两种情况都通过,说明这条测试根本没碰到你加回的那个分支。 - 真实用户路径端到端能跑通。 手动复现最初那个工单——用 legacy-admin 角色登录、用 Safari 上传、访问旧的字符串 ID URL——而不是只跑单元测试。
- 错误 / 日志信号消失了。 如果 Step 5 的成因是日志暴涨(有意吞掉的异常被改成了 throw,或被重构函数里冒出
TypeError),部署后盯同一条查询看 15-30 分钟,确认速率回落到 baseline。
预防建议
- 只让 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 解释每一段删除的理由
常见问题
没有失败的测试,怎么定位到 AI 删代码的那个 commit?
手工 bisect。git bisect start,标 git bisect bad HEAD 和 git bisect good <上个好 tag>,然后每一轮亲手复现真实症状,敲 good 或 bad。没有脚本就用不了 git bisect run,但二分查找仍然能省掉你逐个 diff 翻看的功夫。定位到 commit 后,git show <commit> -- <file> 就能看到具体删了什么。
能只加回某一段被删的代码,而不撤销 AI 那些好的改动吗?
能。用 git restore --source=<good-commit> -p <file>,对着逐 hunk 提示回答——y 把某段被删的代码加回来,n 保留 AI 的版本,s 把同时含两者的 hunk 拆开。这样既留住合理的清理,又只复活你需要的那个分支。
测试全过,但 prod 还是挂,为什么? 被删的分支没有测试覆盖——这正是 agent 以为它是 dead code 的原因。绿测试对一条没被测过的路径毫无证明力。对照 prod 日志(Step 2)看真实流量实际走哪些代码路径,然后给那条路径补上回归测试,再去信任整个测试套件。
怎么防止 agent 下次又把同一段逻辑删掉?
三层:(1) 加一条带名字的回归测试;(2) 在分支上留一句 // IMPORTANT: handles X from incident #1234 注释——agent 在有解释的代码周围会保守得多;(3) 在 CLAUDE.md / AGENTS.md 里加规则:禁止删除带注释、引用 feature flag、或包含 legacy/fallback 关键字的分支,除非被显式要求。
是不是直接 revert 掉整个 AI commit 更省事?
只有当这次重构整体是负收益时才这么做。如果清理大部分是好的、只是误删了一个分支,整体 git revert 会把好的工作也一起扔掉。优先用 hunk 级别的 restore 加回归测试。整体 revert 适合的场景是:你正在事故中、需要立刻止血——先恢复正确性,事后再小心地把清理重做一遍。