AI 辅助重构手册:先保护行为,再移动结构

0阅读10分钟

一次让我警觉的「完美重构」

上个月,我让 AI 帮忙重构一个 400 行的订单计算模块。目标是把三个重复的价格计算分支提取成独立函数,顺便把散落在文件底部的工具函数移到 utils/ 目录。

AI 交出的 diff 漂亮极了——命名统一、结构清晰、圈复杂度从 12 降到 4。我差点直接提交。

但跑测试时发现:它在消除重复的过程中,把一处「满 3 件打 85 折、满 5 件打 75 折,两种折扣不叠加」的业务规则,「优化」成了「取最优折扣」。逻辑上看完全合理,甚至像是更好的实现。但这是错的——产品就是这么要求的,客户投诉过两次才定下来的规则。

这个经历让我整理出了本文的完整方法。AI 擅长机械移动代码、消除重复和整理结构,但它也容易「顺手」改变行为。重构的第一优先级不是让代码更好看,而是让代码在变好看的同时,行为一点都没变。

为什么 AI 重构容易「好心办坏事」

LLM 的工作原理决定了它倾向于生成「看起来更好」的代码。当它发现一段不完美的逻辑时,修复冲动几乎是内建的。arXiv 上一项覆盖 C、C++、C#、Python、Java 五种语言的实证研究表明,LLM 在 few-shot 设置下可以将代码行数从平均 20 行压缩到 7-10 行,Python 的圈复杂度从 5.00 降到 2.28——效率提升显著,但结构偏离也意味着语义漂移的风险1

更麻烦的是,AI 没有项目的「制度记忆」。GitClear 2025 年的代码质量报告揭示了一个反直觉的趋势:AI 辅助编码环境下,与重构相关的代码行比例从 2021 年的 25% 降到 2024 年的不足 10%,而代码克隆量激增了 4 倍2。AI 写新代码很勤快,但对存量代码的结构改善反而变少了——因为在缺乏上下文约束时,AI 更倾向于生成新代码而非理解已有设计意图。

Hacker News 上 2025 年的讨论也印证了这一点:65% 使用 AI 做重构的开发者报告 AI「遗漏了关键上下文」3。这些上下文通常不是代码本身,而是代码背后的原因——为什么这里用了一个看似笨拙的 switch-case?因为产品经理要求按地区走不同逻辑分支,未来可能还要加。

理解了这些局限,才能建立正确的协作模式。我的原则是:AI 做体力活,我做判断;AI 提方案,我做验证。

安全网:不是可选项,是第一步

在动手之前,先确认行为保护手段到位。我把它分成四个层次:

层次手段适用场景覆盖强度
L0已有单测 + 集成测试核心业务模块,已有较好测试覆盖★★★★★
L1特征测试(Property-based Testing)输入输出有数学或逻辑关系的纯函数★★★★☆
L2截图 / E2E 关键路径验收UI 组件重构、页面级调整★★★☆☆
L3类型检查 + lint + build无测试的遗留代码,作为最低安全网★★☆☆☆

对于没有测试的遗留模块,我的做法是先让 AI 帮忙补一组「行为快照」测试——不是完美的单测,而是把当前输入输出记录下来作为基线。Agmis 团队的实践也印证了这一点:「先用 AI 生成测试,再开始重构」,目的是捕获当前行为和边界情况4

// ❌ 重构前没有测试,直接动刀
function calculateDiscount(quantity: number, memberLevel: string) {
  if (quantity >= 5 && memberLevel === 'gold') return 0.75
  if (quantity >= 3) return 0.85
  if (memberLevel === 'gold') return 0.95
  return 1.0
}
 
// ✅ 先补行为快照,再重构
describe('calculateDiscount 行为快照', () => {
  it('gold 会员满 5 件享 75 折(不与其他折扣叠加)', () => {
    expect(calculateDiscount(5, 'gold')).toBe(0.75)
  })
  it('普通用户满 3 件享 85 折', () => {
    expect(calculateDiscount(3, 'normal')).toBe(0.85)
  })
  it('gold 会员不满 3 件享 95 折', () => {
    expect(calculateDiscount(1, 'gold')).toBe(0.95)
  })
  it('普通用户不满 3 件原价', () => {
    expect(calculateDiscount(1, 'normal')).toBe(1.0)
  })
})

这组测试写完后,我就可以放心让 AI 去提取函数、移动代码——因为每次改动后跑一遍测试,行为有没有变,测试说了算。

AI 辅助重构的完整流程

下面这张图是我在实际项目中反复使用的流程。核心思路是:每一步都可控、可验证、可回退。

流程图画布 · 115%
Mermaid 流程图加载中...

关键原则是「每次只做一件事」。Augment Code 的最佳实践建议每个 PR 只做一种修改,比如提取类或重命名变量,并将 diff 控制在 200 行以内5。这个约束看起来保守,但实际效果是:review 变得容易,回退成本低,出问题时定位快。

案例一:提取组件——一个页面拆成三块

一个 300 行的仪表盘页面,包含统计卡片、图表和最近活动列表。目标是拆成三个独立组件。

重构前:

// Dashboard.tsx — 300 行,所有逻辑混在一起
export default function Dashboard() {
  const [stats, setStats] = useState<Stats | null>(null)
  const [chartData, setChartData] = useState<ChartData[]>([])
  const [activities, setActivities] = useState<Activity[]>([])
  const [loading, setLoading] = useState(true)
 
  useEffect(() => {
    Promise.all([fetchStats(), fetchChartData(), fetchActivities()])
      .then(([s, c, a]) => { setStats(s); setChartData(c); setActivities(a) })
      .finally(() => setLoading(false))
  }, [])
 
  if (loading) return <Spinner />
 
  return (
    <div className="grid gap-6">
      {/* 统计卡片 */}
      <div className="grid grid-cols-3 gap-4">
        <Card title="总用户" value={stats?.totalUsers} />
        <Card title="活跃用户" value={stats?.activeUsers} />
        <Card title="收入" value={stats?.revenue} />
      </div>
      {/* 图表 */}
      <Chart data={chartData} />
      {/* 最近活动 */}
      <ActivityList items={activities} />
    </div>
  )
}

重构 prompt(给 AI 的指令):

将 Dashboard.tsx 拆分为三个独立组件:StatsCards、RevenueChart、RecentActivities。数据获取逻辑保留在 Dashboard 中,通过 props 传递给子组件。不要修改任何数据获取逻辑和渲染结构,只做组件拆分。

重构后:

// Dashboard.tsx — 数据获取层,只负责装配
export default function Dashboard() {
  const [stats, setStats] = useState<Stats | null>(null)
  const [chartData, setChartData] = useState<ChartData[]>([])
  const [activities, setActivities] = useState<Activity[]>([])
  const [loading, setLoading] = useState(true)
 
  useEffect(() => {
    Promise.all([fetchStats(), fetchChartData(), fetchActivities()])
      .then(([s, c, a]) => { setStats(s); setChartData(c); setActivities(a) })
      .finally(() => setLoading(false))
  }, [])
 
  if (loading) return <Spinner />
 
  return (
    <div className="grid gap-6">
      <StatsCards stats={stats} />
      <RevenueChart data={chartData} />
      <RecentActivities items={activities} />
    </div>
  )
}
 
// _components/StatsCards.tsx
export function StatsCards({ stats }: { stats: Stats | null }) {
  return (
    <div className="grid grid-cols-3 gap-4">
      <Card title="总用户" value={stats?.totalUsers} />
      <Card title="活跃用户" value={stats?.activeUsers} />
      <Card title="收入" value={stats?.revenue} />
    </div>
  )
}

这个重构的关键是:数据获取逻辑一行没动,只是把 JSX 搬到了不同文件。测试只需要验证三个子组件的渲染是否正确,Dashboard 组件的集成测试不需要修改。

案例二:跨文件归拢——散落的工具函数收口

项目里有三个文件都定义了 formatDate,而且实现还不一样。一个用 Date.toLocaleDateString(),一个用 dayjs,还有一个手写的。

重构 prompt:

项目中存在三个不同的 formatDate 实现(分别在 utils/date.ts、helpers/format.ts、components/OrderCard.tsx 内部)。请将它们统一为 utils/date.ts 中的单一实现,使用 dayjs。更新所有引用点。不要修改任何日期的显示格式和输出结果。

这里有一个容易踩的坑:AI 可能「顺便」统一日期格式,比如把 YYYY/MM/DD 改成 YYYY-MM-DD。这属于行为变更,必须在 prompt 里明确禁止。

做法prompt 关键约束AI 行为
只移动代码「不要修改任何日期的显示格式和输出结果」只改 import 路径,保留原始格式
移动 + 统一格式「统一为 YYYY-MM-DD 格式,并更新相关测试」改代码 + 改测试,行为变更明确
混在一起「整理日期相关代码」不确定,可能做太多也可能做太少

重构后的目录结构:

src/
├── utils/
│   └── date.ts          ← 唯一的 formatDate 实现
├── helpers/
│   └── format.ts        ← 已删除 formatDate,其他函数保留
└── components/
    └── OrderCard.tsx     ← 移除内部 formatDate,改用 import

验证方式:让 AI 写一个对比脚本,把所有调用点的输入输出在重构前后各跑一次,确保结果一致。

案例三:收敛重复——消除 copy-paste 但不改变分支逻辑

一个权限检查模块里有五段几乎一样的代码,都是「检查用户角色 → 检查资源归属 → 返回权限结果」,但每一段的条件略有不同。

重构前:

function canEditPost(user: User, post: Post): boolean {
  if (user.role === 'admin') return true
  if (user.role === 'author' && post.authorId === user.id) return true
  if (user.role === 'editor' && post.status === 'draft') return true
  return false
}
 
function canDeletePost(user: User, post: Post): boolean {
  if (user.role === 'admin') return true
  if (user.role === 'author' && post.authorId === user.id && post.status === 'draft') return true
  return false
}
 
function canPublishPost(user: User, post: Post): boolean {
  if (user.role === 'admin') return true
  if (user.role === 'editor') return true
  if (user.role === 'author' && post.authorId === user.id && post.status === 'draft') return true
  return false
}

重构 prompt:

将 canEditPost、canDeletePost、canPublishPost 中的重复权限检查模式提取为通用函数。注意:每个函数的条件分支和返回值逻辑必须完全保持不变,特别是 canDeletePost 中 author 只能删除 draft 状态文章的约束。补测试覆盖所有分支。

重构后:

type PermissionRule = (user: User, post: Post) => boolean
 
function checkPermissions(user: User, post: Post, rules: PermissionRule[]): boolean {
  return rules.some(rule => rule(user, post))
}
 
function canEditPost(user: User, post: Post): boolean {
  return checkPermissions(user, post, [
    (u) => u.role === 'admin',
    (u, p) => u.role === 'author' && p.authorId === u.id,
    (u, p) => u.role === 'editor' && p.status === 'draft',
  ])
}
 
function canDeletePost(user: User, post: Post): boolean {
  return checkPermissions(user, post, [
    (u) => u.role === 'admin',
    (u, p) => u.role === 'author' && p.authorId === u.id && p.status === 'draft',
  ])
}
 
function canPublishPost(user: User, post: Post): boolean {
  return checkPermissions(user, post, [
    (u) => u.role === 'admin',
    (u) => u.role === 'editor',
    (u, p) => u.role === 'author' && p.authorId === u.id && p.status === 'draft',
  ])
}

这个重构的核心风险在于:AI 可能在「消除重复」的过程中合并条件分支,把 canDeletePostpost.status === 'draft' 这个限制条件丢掉。行为快照测试可以捕获这种问题。

AI 重构的常见翻车模式

在多次实践后,我整理出了 AI 重构时最常出现的问题模式:

翻车模式表现防御手段
行为静默修改代码看起来更「合理」,但改变了业务规则行为快照测试 + diff 逐行审查
过度重构只要求提取函数,结果还改了命名、调了结构prompt 明确约束范围 + diff 自检
上下文丢失改了 A 文件,没更新引用 A 的 B、C 文件重构后用 typecheck + build 全局验证
幻觉 API调用不存在的方法或已废弃的库本地跑通验证命令,不盲信 AI 输出
格式噪音diff 里混了大量格式化变更重构前统一 prettier/eslint 配置,或让 AI 不改格式
指令过载一条 prompt 塞了 5 种重构,质量断崖下降每条 prompt 只做一种重构

Alex 团队的经验总结得很直接:「复杂指令会导致质量下降,指令被部分遵循或直接被忽略」4。我现在的习惯是每次只给 AI 一个动词——提取、移动、重命名、内联——绝不混用。

重构类型与 AI 适用度

不是所有重构都适合交给 AI。下面是我的判断矩阵:

重构类型AI 适用度风险等级推荐做法
重命名变量/函数★★★★★直接用 IDE 重构工具,不需要 AI
提取函数/组件★★★★☆中低AI 做提取,人审查接口设计
移动文件/调整目录结构★★★★☆AI 做移动 + 更新引用,人确认结构
收敛重复代码★★★☆☆AI 提方案,人逐分支验证行为
设计模式引入★★☆☆☆AI 辅助分析,人做设计决策
跨模块架构调整★☆☆☆☆极高不推荐 AI 独立执行,人主导 + AI 辅助

规律很明显:越接近「机械移动」的重构,AI 越可靠;越接近「设计决策」的重构,AI 越容易犯错。DX 的企业级重构指南也建议:「针对高价值的遗留模块做定向重构,而不是大规模全面改造,前者的 ROI 是后者的 4 倍」6

Diff 自检:最后的质量关

重构完成后,我会花 5 分钟逐行看 diff。重点检查这四项:

  1. 有没有行为变更 —— 条件分支、返回值、副作用有没有被「顺手」修改
  2. 有没有无关变更 —— 格式化、注释修改、import 排序是否混了进来
  3. 引用有没有断 —— 移动文件后,所有 import 路径是否更新,是否还有死链
  4. 测试有没有跟着改 —— 如果重构改了测试本身,要问自己:是测试适配新结构,还是测试被偷偷改了断言?

第三项尤其容易忽略。Reddit 上有经验的开发者反馈:LLM 在本地改动上表现很好,但规模一上去就容易遗漏跨文件的引用关系7。我现在的做法是重构完成后必跑一遍 pnpm typecheck && pnpm build,让编译器帮忙兜底。

重构前检查清单

每次开始 AI 辅助重构之前,我过一遍这个清单:

  • 重构目标是否明确?用一句话说清楚要做什么
  • 是否只包含一种重构动作(提取 / 移动 / 重命名 / 内联)?
  • 行为保护手段是否到位?测试、截图、类型检查至少有一种
  • 已有测试是否全部跑通并作为基线?
  • prompt 是否包含明确的「不要做什么」约束?
  • 是否已识别出涉及该模块的所有引用文件?
  • 是否准备好验证命令(test / typecheck / build)?
  • 重构分支是否与功能开发分开?
  • 是否理解为什么现有代码是这样写的?(如果不确定,先问团队)
  • diff 超过 200 行时,是否拆分成多个独立提交?

最后一条值得特别强调。当你发现 AI 给出的 diff 已经超过 200 行,说明这次重构的范围太大了。把它拆成多个小步骤,每一步独立验证、独立提交。保守不是胆小,是对生产环境的尊重。

验证命令速查

不同重构类型对应的最低验证手段:

重构类型最低验证推荐验证理想验证
重命名typechecktypecheck + testtest + E2E
提取函数/组件testtest + buildtest + 视觉回归
移动文件typecheck + buildtypecheck + test + buildtest + 全量回归
收敛重复testtest + linttest + 覆盖率对比
接口调整typecheck + testtypecheck + test + build契约测试

给 AI 下重构指令的三个原则

经过反复试错,我总结出给 AI 写重构 prompt 的三个原则:

原则一:一个动词一条命。 每次 prompt 里只出现一个核心动作——提取、移动、重命名、内联。如果发现自己写了「提取函数并优化命名,顺便更新注释」,立刻拆成三条。

原则二:显式声明「不要做什么」。 只说「做什么」是不够的,AI 会根据自己的理解加戏。必须加上约束,比如「不要修改任何条件分支」「不要改变日期格式」「不要动 import 顺序」。这些约束比目标描述更重要。

原则三:附带验证预期。 告诉 AI 重构后应该跑什么命令验证,比如「完成后运行 pnpm test -- --filter=order 确认所有测试通过」。这让 AI 在生成代码时会考虑可验证性,而不只是语法正确性。

写在最后

AI 改变了重构的经济学。以前那些「不值得做」的重构——比如整理一个 500 行文件的目录结构、把五段相似的权限检查逻辑收敛成一个通用函数——现在可以在几分钟内完成。Salesforce 用 AI 驱动重构把一个预估 2 年的遗留系统迁移压缩到了 4 个月4

但速度快不等于可以跳过安全检查。AI 辅助重构的核心矛盾在于:它太擅长让代码「看起来更好」,以至于容易让我们忘记问「行为有没有变」。我的做法是把 AI 当作一个手艺很好但不了解项目历史的实习生——它能做出漂亮的代码,但需要你告诉它什么能改、什么不能动。

先保护行为,再移动结构。这句话值得贴在显示器旁边。


参考资料

Footnotes

  1. Code Refactoring with LLM: A Comprehensive Evaluation With Few-Shot Prompting. arXiv, 2025. 链接

  2. GitClear. AI Copilot Code Quality: 2025 Data Suggests 4x Growth in Code Churn. 链接

  3. Hacker News. 2025 State of AI Code Quality. 链接

  4. Agmis. What We've Learned Refactoring Legacy Code with AI. 链接 2 3

  5. Augment Code. AI Code Refactoring: Tools, Tactics & Best Practices. 链接

  6. DX. AI Code Refactoring: Strategic Approaches to Enterprise Software. 链接

  7. Reddit r/ExperiencedDevs. Large scale refactoring with LLM, any experience? 链接

评论 0

0 / 1000