让 Coding Agent 重构时,边界要比目标更清楚
重构任务最容易被 AI 扩大范围
在所有交给 Coding Agent 的任务里,重构最容易失控。
原因很直接:AI 擅长发现代码中的「改进机会」——重复代码、不一致的命名、过长的函数、可以提取的公共逻辑。这些能力在写新功能时是优点,在重构时却变成风险。你让它「优化一下这个函数」,它很可能顺便整理了相邻的函数、重命名了参数、改了样式、甚至动了不在任务范围内的文件。
我见过太多开发者踩过这个坑。AI 没有「只改这里」的隐含意识——对它来说,上下文窗口里的每一行代码都是可修改的。
Martin Fowler 对重构的经典定义包含两个硬约束:行为保持不变,结构得到改善。两者缺一不可。但 AI 在执行重构时几乎总是只关注「让代码更好」,忽略了「让行为不变」——因为「不变」不会体现在 diff 里,「改善」会。
我想聊的核心观点是:给 AI 做重构时,边界比目标更重要。 目标决定 Agent 往哪走,边界决定它不会走多远。没有边界的重构,就是让 AI 自由发挥。
重构到底是什么
前面提到了「行为保持不变」和「结构得到改善」两个约束。日常开发中重构经常被滥用为「改代码」的同义词,先把定义展开。
Martin Fowler 的经典定义
Martin Fowler 在《Refactoring: Improving the Design of Existing Code》中给出了重构的标准定义:
重构(名词):对软件的内部结构所做的一种调整,使其更易于理解和修改,同时不改变其可观察行为。
——Martin Fowler, Definition of Refactoring
这个定义有三个核心支柱:
| 支柱 | 含义 | 反例 |
|---|---|---|
| 内部结构调整 | 重命名、提取函数、移动职责、拆分大类 | 不改结构只改格式 |
| 行为保持不变 | 外部可观察的行为必须完全一致 | 优化时顺便修了个 bug |
| 设计得到改善 | 代码更容易理解、更容易修改 | 改了结构但变得更复杂 |
Fowler 还特别强调,重构和添加新功能应该在两个独立的阶段进行——永远不要同时做两件事,这样你才能确信行为没有改变。
Opdyke 的行为保持变换
更早的理论基础来自 William Opdyke 在 1992 年的博士论文《Refactoring Object-Oriented Frameworks》,他提出了「行为保持变换」(behavior-preserving transformation)的概念:每个重构步骤都是一组满足前置条件的变换,只要前置条件成立,行为就自动得到保持。
人工开发时代,这些是「软约束」——靠开发者自觉。但重构主体变成 AI Agent 后,约束必须变成「硬约束」——AI 没有「自觉」这个概念。
AI 为什么容易扩大重构范围
AI 特别容易突破重构边界,根子在它的工作机制。
原因一:目标函数倾向于「尽可能多的改善」
AI Agent 收到「优化这个组件」这样的指令时,优化目标会在上下文中被最大化。能发现 5 处可以改进的地方,它就会倾向于同时改进 5 处,而不是只改你要求的那 1 处。
人类开发者在重构时有隐含的所有权意识——「这个文件是别人负责的,我不该改」。AI 没有这种意识,它把整个仓库都视为可修改范围。
原因二:上下文窗口的「全知错觉」
当 AI 读取了多个文件后,它会在上下文窗口中形成一个全局视图。这个全局视图会产生一个副作用:AI 会认为它看到的「可以改进的地方」都是它应该改进的。
一个工程师说得很直接:「Simple changes often touched 20+ files」(简单的改动经常触及 20 多个文件)。AI 把整个仓库当作可修改范围,缺乏对「哪些文件不归我管」的判断。
原因三:缺乏「风险-收益」的权衡能力
人类开发者在重构时会做一个隐性判断:这个改动的收益是否值得引入的风险?改一个稳定的核心函数,即使代码写得不好,风险也可能大于收益。AI 很难做这种权衡,它更多是从「代码质量指标」出发,而不是从「工程风险」出发。
原因四:没有「沉没成本」概念
人类开发者重构到一半发现影响范围太大,会主动收手。AI 没有这种直觉,它会一视同仁地执行所有发现的重构机会,直到任务完成。
案例一:组件拆分改坏了公开 API
场景
一个 ArticleList 组件有 320 行,负责渲染文章列表、处理分页和筛选。团队让 Agent「把组件拆小,提高可读性」。
Agent 做了什么
Agent 将 ArticleList 拆成 5 个子组件(Header、Body、Footer、Card、FilterPanel),结构本身合理。但它同时改了 Props 接口、事件回调签名(onSelect(article) → onSelect(articleId))、CSS 类名(.article-list → .article-list-root)、导出方式(default → named)、内部状态管理(useState → 自定义 hook)。结果:12 处 import 报错,3 处调用方编译失败,E2E 选择器失效,初始化时序微妙变化。
好的做法
提前定义边界:Props 接口不变、export default 不变、CSS 类名不变、onSelect 签名不变、子组件不对外暴露。Agent 的拆分就被限制在组件内部,不影响外部调用方。
坏 diff 547 行,审查者分不清哪些是必要拆分、哪些是顺手整理。好 diff 85 行,一眼看出:结构拆了,公开接口和样式完全不变。
案例二:函数重构让行为悄悄变了
场景
一个 150 行的数据转换函数 transformApiResponse,负责把后端 API 原始响应转成前端类型。团队让 Agent「重构,提高可读性」。
Agent 做了什么
Agent 拆成 4 个辅助函数,代码更好读了,但边界行为悄悄变了:null 字段从返回默认值 {} 变成返回 null;空数组从返回 [] 变成返回 undefined;嵌套对象从递归转换所有层级变成只转第一层;日期字符串从保持原样变成转成 Date 对象。
每个变化单独看都有道理,合在一起就改变了整体行为。由于没有测试保护,code review 没发现。上线后 Dashboard 图表全部显示异常,团队才定位到问题。
正确做法
重构前先为现有行为建立快照测试,覆盖所有边界情况。每次重构后跑测试,失败就知道行为变了。
教训:没有测试的重构不是重构,是赌博。 对人类如此,对 AI 更是如此。
案例三:大规模重命名让 diff 无法审查
场景
一个 2000 行的模块用了匈牙利命名法(strName、intCount),团队让 Agent「重命名变量,符合 TypeScript 规范」。
Agent 做了什么
Agent 重命名了 437 个标识符,涉及 23 个文件,产生 1200 行 diff。单个改动看都合理:
- function calculateTotalPrice(intQuantity: number, fltPrice: number): number {
- const fltTax = fltPrice * 0.1
- return intQuantity * (fltPrice + fltTax)
- }
+ function calculateTotalPrice(quantity: number, price: number): number {
+ const tax = price * 0.1
+ return quantity * (price + tax)
+ }但 437 个改动混在一起,code review 就变成了不可能完成的任务:无法判断每个重命名是否正确、无法确认是否有漏改、无法发现「顺手」改的逻辑、无法验证跨模块影响。
正确做法
分批次进行,每批控制在可审查范围内:公开 API 约 15 个标识符 → 内部辅助函数约 20 个 → 局部变量按文件逐个进行。每批次独立 PR,diff 不超过 100 行。
重构边界确认流程
让 Agent 执行重构前,先走边界确认流程。目标是:在 Agent 写第一行代码之前,把「不可改变项」锁定。
关键节点:明确重构目标(具体到「降低圈复杂度」,别说「优化一下」);列出不可改变项(API、路由、文案、样式、依赖、导出方式);审查 diff 形状(一句话说不清收益就暂停拆分)。
行为保护策略
重构的核心约束是「行为保持不变」,但「不变」需要证据。以下分级的行为保护策略:
| 保护级别 | 策略 | 适用场景 | 成本 | 可靠性 |
|---|---|---|---|---|
| L1 | 单元测试 | 纯函数、工具函数、数据转换 | 中 | 高 |
| L2 | 快照测试 | 组件输出、API 响应结构 | 低 | 中高 |
| L3 | 页面截图对比 | UI 组件、样式相关的重构 | 低 | 中 |
| L4 | 类型检查 | 接口重构、类型定义调整 | 低 | 中(只覆盖类型层面) |
| L5 | E2E 测试 | 关键路径、跨组件交互 | 高 | 高 |
| L6 | 手动验收 | 视觉效果、交互细节 | 高 | 取决于审查者 |
使用建议
有测试:先跑一遍确认基线,重构后再跑一遍。测试失败时先假设重构引入了回归。无测试:用快照测试(toMatchSnapshot)快速锁定行为。核心逻辑:先补小范围单测再重构。UI 组件:用页面截图作基线,Playwright toHaveScreenshot 或 Chromatic 对比。类型变更:跑 pnpm typecheck 确认类型层面没破坏,运行时行为仍需测试保护。
行为保护的「不可改变项」清单
重构指令中,以下项目应显式列为「不可改变」,除非重构目标明确要求修改:
不可改变项(Invariants):
1. 组件的 Props 接口
2. 函数的参数和返回值类型
3. 导出的名称和方式(default vs named)
4. 路由路径和 URL 结构
5. 数据库 schema 和 API 契约
6. 文案和用户可见的字符串
7. 视觉样式(CSS 类名、样式文件)
8. 事件回调的签名和触发时机
9. 错误处理行为和错误消息
10. 第三方依赖的调用方式
11. 环境变量的读取方式
12. 日志输出的格式和级别
13. 并发和时序相关行为
14. 缓存策略和缓存 key
好的 diff vs 坏的 diff
审查 Agent 的 diff 时,几个快速判断标准:
| 好的 diff | 坏的 diff |
|---|---|
| 可一句话解释 | 混合多种改动 |
| 方向一致,所有改动指向同一目标 | 大面积重命名 |
| 删除 > 新增 | 引入新依赖 |
| 无副作用,不动不相关文件 | 改了不相关文件 |
| 可分步验证 | 改变公开 API |
| 删除了测试 |
一句话解释不了收益,就该暂停拆分。
重构边界清单
下次让 Agent 重构前,按这个清单过一遍:
重构前
- 明确重构目标:降低哪种复杂度(圈复杂度、代码行数、认知负担、耦合度)
- 列出不可改变项:API、路由、文案、样式、依赖、导出方式
- 确认行为保护证据:有测试?可以跑?没有测试?先补快照或截图
- 评估影响范围:这个改动会影响哪些文件和模块?有没有跨模块依赖?
- 拆分重构步骤:一次重构只做一件事。大范围的重构拆成多个独立 PR
- 编写明确的重构指令:告诉 Agent 目标、边界、不可改变项
重构中
- 监控 diff 大小:如果 diff 超过 200 行,考虑是否需要拆分
- 检查 diff 方向:所有改动是否都指向同一个目标?有没有「顺手整理」?
- 检查公开 API:Props 接口、导出方式、事件回调签名是否被修改?
- 检查不相关文件:是否修改了目标文件之外的文件?
重构后
- 运行验证命令:typecheck、测试、lint
- 审查 diff 形状:能否用一句话解释收益?
- 对比行为证据:测试通过?快照一致?截图一致?
- 检查遗漏:是否有引用目标组件/函数的其他文件没有被更新?
- 记录重构决策:在 PR 描述中写明目标、不可改变项、验证方式
重构指令模板
重构任务:[具体目标,如「降低 ArticleList 组件的圈复杂度」]
目标文件:[精确到文件和函数]
不可改变项:
- Props 接口不变
- 导出方式不变
- CSS 类名不变
- 事件回调签名不变
- 不引入新依赖
- 不修改其他文件
行为保护:
- 重构前运行 [命令] 确认测试通过
- 重构后再次运行确认测试通过
验证标准:
- typecheck 通过
- 测试通过
期望 diff:
- [一句话描述,如「将 300 行组件拆为 3 个子组件,公开接口不变」]
关键是把「不可改变项」放最前面。Agent 开始工作前就知道哪些不能碰,比事后检查 diff 有效得多。
结论
AI 做重构最容易超范围——目标函数倾向于最大化改善、上下文窗口产生全知错觉、缺乏风险权衡、没有沉没成本概念。应对策略:开始工作前把边界锁死。
- 先写不可改变项:API、路由、文案、样式、依赖
- 用行为保护重构:测试、快照、截图多层验证
- 审查 diff 形状:一句话说不清收益就拆分
- 拆分重构步骤:大范围拆成多个独立 PR
下次让 Agent 重构,先回答三个问题:
- 要降低哪种复杂度?(目标)
- 哪些东西不能改?(边界)
- 怎么证明行为没变?(验证)
这三个问题能把失控的 diff 变成精准的重构。边界不是束缚,是让重构可控的前提。
参考资料
- Martin Fowler, Definition of Refactoring — 重构的标准定义
- Martin Fowler, Refactoring: Improving the Design of Existing Code — 重构领域的经典著作
- William Opdyke, Refactoring Object-Oriented Frameworks (1992) — 行为保持变换的理论基础
- CleanAim, Scope Creep: When AI Changes 47 Files Instead of One — AI 重构 scope creep 的典型案例分析
- arXiv, AI-Assisted Unit Test Writing and Test-Driven Code Refactoring (2026) — 测试驱动的安全重构方法论
- CodeScene, Making Legacy Code AI-Ready: Benchmarks on Agentic Refactoring (2026) — Agent 重构的基准测试和稳定性分析
- Medium, AI Scope Creep: Why Your Pull Requests Are Quietly Getting Bigger (2026) — 对抗 AI scope creep 的实用策略
- Qt, Refactoring and Software Architecture (2026) — 重构与架构的关系