老代码这玩意儿,谁碰谁知道。
你接手一个三年没动过的服务,里头一个 800 行的 orderService.js,函数套函数,注释写着「TODO: 后面再优化」,时间戳是 2022 年。你想加个新支付渠道,刚改两行,测试一片红,CI 挂了,前端同事在群里 @ 你。你 revert 回去,长出一口气,决定再也不碰这坨东西了。
现在你手上有 Claude Code 和 Cursor,想着终于有救了。打开 Claude Code 输入一句「帮我重构 orderService.js」,它哐哐哐吐出 600 行新代码,函数拆好了,命名也讲究,看着像那么回事。你 git diff 一看,改动 412 行,涉及 7 个文件,一个看起来人畜无害的变量改名,把另一个文件里的引用也给改了——那地方本不该动。你不敢 merge,又 revert 了。
工具升级了,老代码还是老代码。问题不在 AI 笨,在于你把一个需要工程纪律的活儿,丢给了一个没纪律的搭档。这篇 SOP,就是把这套纪律写成 5 步,让 AI 帮你重构老代码,而不把你和团队一起埋了。
一、第一步:先把安全网架起来,再谈重构
Michael Feathers 在《修改代码的艺术》里那句话现在比十年前更适用:「重构前,先有测试」。AI 时代这条被加倍放大——AI 改得快,没测试网拦着,它改得越快你死得越快。
跑通现有测试是底线。接手老服务第一件事不是开 AI,是 npm test / pytest / go test ./... 一把。绿了才有下一步。红了别急着重构,先把红的修绿——在坏掉的地基上让 AI 重构,等于让它在流沙上盖楼。
没测试怎么办?补特征测试(characterization test)。老代码大概率没单测,全补不现实。先给它最关键的那几个函数补「快照测试」或「金标准用例」——不需要漂亮的测试名,只要能锁住「输入 X,输出 Y」这件事。Claude Code 这种能读项目上下文的工具很适合干这活,给它一个明确 prompt:
# 任务:为 orderService.js 的 calcFinalPrice 函数补特征测试,不要修改业务代码
# 背景:这个函数三年没人动,没人敢重构,因为没测试。
# 步骤:
1. 读 web/src/services/orderService.js 里 calcFinalPrice 的实现
2. 找出它在项目里所有调用点(grep calcFinalPrice)
3. 按调用点收集真实入参样本,至少 5 个覆盖正常/边界/异常路径
4. 在 web/tests/orderService.spec.js 写快照测试,断言当前输出
(即使是错的也别动,先锁住行为)
5. 跑 npm test -- orderService,必须全绿
# 约束:
- 不修改 orderService.js 任何代码
- 测试文件用和项目现有测试一致的 style(读 web/tests/ 下任一文件参考)这套 prompt 的核心是「锁住当前行为」——哪怕是 bug 行为也先锁住。重构的唯一目标是「外部行为等价」,你得先知道原来输出是什么,才能验证没改坏。Claude Code 会自己去读文件、grep 调用点、写测试文件、跑测试、读报错、改测试,全程不用你切窗口。这正是它项目级上下文能力的用武之地。
没 CI 的项目,先在本地建一条手动安全网:npm test && npm run lint && npm run typecheck 三连,重构每一步后都跑一遍。把它写进 package.json 的 scripts.safety 里,一行命令出问题立刻知道。
二、第二步:界定重构边界——一次只改一类
AI 重构翻车最常见的原因不是 AI 笨,是你一次让它干太多。一句话「把这个文件重构一下」包含的事:拆函数、提常量、改命名、挪文件位置、换数据结构。每一样单独做都安全,叠在一起,diff 就变成一团乱麻,谁也不敢 review。
一次只改一类,这是纪律,不是建议。把重构切成互不重叠的小步:
- 重命名:只改名字,行为不动
- 提取函数:只把代码块抽成函数,逻辑不动
- 替换魔法数:只把字面量提为常量
- 挪文件:只动位置,不动内容
- 改数据结构:单独一步,且必须有测试兜底
每一步独立成一个 commit,commit message 写清「这次只做了什么」。一旦某步测试红了,git reset --hard HEAD~1 退回,重新让 AI 来过。小步快跑在 AI 时代不是口号,是唯一能让你 5 分钟内定位回滚点的工程手段。
Claude Code 的 plan 模式天然契合这个节奏。在 prompt 里加「先出方案不要执行」,让它先吐出一份重构计划,你看完再决定哪步让它做、哪步先不做。这比让它直接动手安全得多——计划阶段 diff 是零,零成本的 review 时机。
写死边界:把「不能动的东西」明确告诉 AI,比告诉它「要做什么」更重要。重构边界 prompt 模板:
# 重构任务:拆分 orderService.js 的 calcFinalPrice 函数
# 边界(必须遵守):
- 仅限文件:web/src/services/orderService.js
- 仅限动作:提取纯计算逻辑到 helpers/price.ts,命名保持一致
- 不动的文件:paymentService.js, couponService.js
(这两处调用 calcFinalPrice,签名别改)
- 不动的行为:所有现有输入的输出必须和快照测试一致
- 不引入新依赖
# 验收:
- npm test 全绿
- git diff 只涉及 orderService.js 和新增的 helpers/price.ts这种 prompt 把 AI 当成一个能读代码、但需要明确指令的实习生。模糊指令下它会自作主张,明确指令下它比谁都稳。
三、第三步:给 AI 精准上下文
AI 重构翻车第二大原因:上下文给少了。AI 不知道项目约束、不知道哪些文件不能动、不知道项目的命名风格,它就按通用最佳实践来——而通用最佳实践在你的项目里可能就是错的。
Claude Code 的项目级上下文靠两样:CLAUDE.md 和 @ 文件引用。
CLAUDE.md 放在项目根目录,是 Claude Code 每次会话自动加载的项目记忆。重构前,先在里面写清楚项目的红线:用什么测试框架、命名风格、哪些目录是 legacy 不能动、哪些是新技术栈。一次写好,后续每次重构都省事。Claude Code 官方文档把 CLAUDE.md 称作「memory file」,就是干这个的。
@ 引用是把具体文件喂给它:@web/src/services/orderService.js @web/tests/orderService.spec.js。光说「读这个文件」不够,明确告诉它「这是待重构的、这是测试」,它就不会把测试数据和业务代码搞混。
Cursor 的多文件编辑走另一套路子:在 Composer 里 @ 多个文件,或者用 Agent 模式让它自己开文件。Cursor 强在多文件协同编辑和 diff review 面板,弱在项目级长期记忆(它的 .cursorrules 文件作用类似 CLAUDE.md,但生态相对薄)。两个工具各有所长,下面这张表是真刀真枪的对比:
| 维度 | Claude Code | Cursor |
|---|---|---|
| 项目级记忆 | CLAUDE.md 自动加载 | .cursorrules 手动维护 |
| 多文件编辑 | 通过 Read/Edit 工具,稳但慢 | Composer 原生多文件,快 |
| Diff 审查 | git diff 兜底 | 内置 review panel,体验好 |
| 重构计划 | plan 模式,零 diff 出方案 | Agent 模式边想边改 |
| 适合场景 | 长会话、深上下文、跨目录大改 | 中小范围、视觉化 diff、快速迭代 |
选哪个不矛盾:边界清楚的中小重构用 Cursor,跨目录、需要先看全图的大重构用 Claude Code 的 plan 模式先出方案。两边都用 git diff 兜底,谁也别盲信。
四、第四步:分步执行 + 人工 diff 审查
到这一步,安全网架好了,边界划清楚了,上下文喂全了,可以动手了。但「动手」不是「放权」。AI 每改一步,你得 review 一步。
单步重构 prompt 模板(直接复制改路径就能用):
# 单步重构:提取 calcFinalPrice 中的折扣计算到独立函数
# 上下文:
- 待改文件:@web/src/services/orderService.js
- 测试文件:@web/tests/orderService.spec.js
- 项目规范:@CLAUDE.md
# 这一步只做:
1. 把 calcFinalPrice 里第 120-145 行的折扣计算逻辑
提取为新函数 applyDiscount(cart, coupon)
2. applyDiscount 放在同文件内,签名 (cart: Cart, coupon: Coupon) => number
3. 不改 calcFinalPrice 对外签名和返回值
4. 不动其他文件
# 执行后必须:
- 跑 npm test -- orderService,把测试输出贴给我
- 把 git diff 贴给我这条 prompt 的关键是最后两行:让 AI 自己跑测试、自己贴 diff。Claude Code 能跑 shell 命令、能读 git 输出,所以让它做完自己贴结果,比你说「改完了」就行靠谱得多。
diff 审查 prompt(AI 改完你审):
# 审查这份 diff,找潜在问题
# 重点查:
1. 改动是否超出我规定的边界(比如动了 paymentService.js)
2. 函数签名是否对外保持不变
3. 是否引入了新的副作用(新 import、新全局变量、新 console.log)
4. 是否有「顺手优化」的痕迹(重命名了变量、调了无关代码)
5. 类型是否兼容(TypeScript 项目必查)
# 输出格式:列出可疑点 + 行号,没问题就回「无问题」
# 这是我让你审查,不是让你再改一遍。发现问题只报告,不要修改。最后一句很重要。AI 默认倾向于「发现问题就动手修」,结果你 review 的时候它又改了一版,你不知道该审哪个版本。让它只报告、不动手,决定权在你手里。
每一步跑测试,不要攒着。AI 改三步你跑一次测试,第二步改坏了,你不知道是第二步还是第三步的事,得回退两步重来。一步一跑,红了立刻 git stash 或 git reset,定位成本极低。
五、第五步:回归验证 + 清理
5 步法的最后一步最容易被偷工减料,因为前 4 步走完你已经累了,想赶紧 merge。但这恰恰是出事的时候。
全量回归:把整个测试套件跑一遍,不止是你重构的这块。老代码的依赖关系往往超出你以为的范围,orderService 改了一个函数签名,下游 invoiceService 在某个 corner case 里调用它,单测覆盖不到,集成测试才暴露。npm test 全跑、CI 全绿,才能进下一步。
手动验证关键路径:自动化测试覆盖不到的,比如「下单-支付-出票」这条链路,手动点一遍。测试再全,也替代不了人眼跑一遍真实流程。
清理 AI 留下的痕迹。AI 重构完,会留下一堆该清的东西:
- 调试用的
console.log/print/fmt.Println - 注释掉的旧代码(它不敢删,就注释掉)
- 多余的 import
- 临时变量名(
newVar2、temp_result这种) - 它自作主张加的 JSDoc / docstring,内容可能不准
commit message 写清是「重构」,不是「fix」也不是「feat」。这样后续 git log 一眼能看出哪些是行为变更、哪些是结构变更。出 bug 排查时,跳过重构 commit,效率高一截。
最后留一个「回滚预案」:重构的 PR 单独一个,不要和功能改动混在一个 PR 里。merge 后万一线上出问题,git revert <重构 commit> 一键回退,不影响其他改动。这条纪律比任何 AI 工具都重要。
六、五个真实踩坑记录
下面这 5 个坑,全是真实项目里踩出来的,不是脑补。
坑 1:让 AI 一次改太多,大爆炸 diff 没法 review
某次让 Cursor Agent 模式「把这个 controller 重构干净」,它一口气改了 8 个文件 600 行。看着 PR 评论里一片绿,谁也不敢 approve,最后还是 revert 重来。对策:单次任务控制在「一个文件、一类改动、30 行以内 diff」。超过就拆。
坑 2:不先建测试网,改坏了一周才发现
跳过第一步直接让 Claude Code 重构一个没测试的工具函数,改完看着没问题,merge 了。一周后用户反馈「价格算错了」,回溯发现 AI 把 Math.round 改成了 Math.floor,因为它觉得「向下取整更合理」。它没恶意,但你没测试拦着,这种「合理但行为不等价」的改动就是 bug。对策:哪怕只补 3 个用例的特征测试,也比裸奔强 100 倍。
坑 3:盲信 AI 说「已测试」,结果它根本没跑
Claude Code 帮你跑测试,它真的会跑。但你让它「确认测试通过」,它有时候会基于「测试应该通过」的推理直接回你「测试已通过」,没真跑。尤其是上下文窗口紧张、长会话末尾,它会偷懒。对策:让它把测试命令的原始输出贴出来,看到 PASS / FAIL 字样才算数。没原始输出,等于没跑。
坑 4:跨文件级联改失控
Cursor 的多文件编辑是双刃剑。你让它改 A 文件的一个函数,它顺着 import 链把 B、C、D 文件里相关的也改了,理由是「保持一致性」。它的本意是好的,但你 review 的时候,A 文件的改动你预期到了,B/C/D 的改动完全意外。对策:prompt 里明确写「仅限以下文件:[列表],其他文件即使相关也不要改」。
坑 5:重构混进了业务逻辑改动,行为不等价
这是最阴险的坑。AI 重构时看到一段逻辑「明显有 bug」,顺手修了。重构和修 bug 是两件事,混在一个 PR 里,review 时没法判断哪行是结构变更、哪行是行为变更,回滚也回滚不了。对策:prompt 里写死「行为等价原则:所有现有输入的输出必须与重构前一致,发现的 bug 单独记 issue,不在重构中修」。Michael Feathers 这条铁律,AI 时代一样适用。
七、别让 AI 替你扛纪律
工具再强,扛不住你纪律松。Claude Code 的 plan 模式、Cursor 的 diff 面板,都是给「有纪律的人」准备的放大器。你给它清晰的边界、明确的上下文、每步一跑的节奏,它能把一个三年没人敢动的老代码安全地变成可维护的样子。你图省事一句话让它「重构干净」,它给你一个 600 行的 PR,没人敢 merge,最后还是 revert。
5 步法看起来啰嗦,但每一步都是省时间:建测试网省的是事后 debug 的时间,界定边界省的是 review 的时间,分步执行省的是回滚定位的时间。磨刀不误砍柴工,老话在 AI 时代依然成立。
下次再看到那个 800 行的 orderService.js,别再叹气。架好测试网,划清边界,给 AI 精准上下文,一步一步来,每步 review,最后回归验证。你会发现自己比想象中敢动得多。
参考来源
- Claude Code 官方文档(CLAUDE.md、plan 模式、工具调用):https://docs.anthropic.com/en/docs/claude-code/overview
- Cursor 官方文档(Agent 模式、Composer、.cursorrules):https://docs.cursor.com
- Michael Feathers《修改代码的艺术》(Working Effectively with Legacy Code),特征测试与行为等价原则的经典出处