AI 重构遗留单体应用实录:6 周从 40 万行代码到可维护架构
这篇文章记录的是一个真实的重构项目。项目规模:一个运行了 7 年的 Python Django 单体后端,约 40 万行代码,0 个测试,文档缺失,原团队成员大部分已离职。目标:在不中断生产服务的前提下,把核心模块拆分成可独立迭代的结构,并补齐关键路径的测试覆盖。
工具:Claude Code(主力)+ Cursor(辅助阅读/局部修改)。耗时:6 周。
以下是真实经历,不是广告文,有踩坑,有失败,也有意外之喜。
一、为什么这种项目 AI 特别有用(也特别危险)
遗留代码重构是 AI 编程工具”最值钱”的场景之一,原因在于:
有用的地方:
- 老代码往往逻辑正确但结构混乱,AI 很擅长”把意图说清楚”
- 大量机械性工作(统一命名、提取常量、添加类型注解)AI 可以批量处理
- 测试生成:AI 在理解现有函数后能快速产出覆盖主路径的单元测试骨架
危险的地方:
- 遗留代码充满”看起来是 Bug 其实是 Feature”的部分,AI 很容易”修掉”本不该动的逻辑
- 没有测试的代码改起来是盲飞,AI 生成的改动你必须 review 每一行
- AI 会过度自信,尤其在面对”我看不懂的代码”时,它会给你一个听起来合理但实际跑不通的方案
核心原则:AI 是加速器,不是替代品。你仍然需要真正理解代码。
二、开始前:摸底 4 天
在让 AI 动任何一行代码之前,我花了 4 天做摸底。这 4 天是整个项目最重要的投资。
2.1 生成代码地图
第一个任务交给 Claude Code:
帮我分析这个 Django 项目的整体结构。
输出:
1. 所有 app 的列表,每个 app 的大致职责(基于文件名和代码推断)
2. models.py 里所有模型及其外键关系(用 mermaid 格式输出 ER 图)
3. views.py 和 urls.py 中有多少个 API 端点,按 app 统计
4. 找出前 10 个最长的函数(按行数)
这个命令跑完,我得到了一张”地图”。发现了几个关键事实:
- 有 2 个 app(
core和billing)的代码占了整个项目的 60% billing的process_payment函数有 847 行,中间嵌套了 7 层 if/else- 有 3 个 app 的代码在 urls.py 里完全没有入口,是死代码
2.2 找关键路径
死代码可以先搁置,先搞清楚哪些代码是每天真实在用的。
借助日志分析(把 nginx access log 导出按 URL 统计),加上 Claude Code 帮我把 URL pattern 和 view 函数对应起来,我绘制出了一张”热力图”——哪些接口日调用量超过 1000 次,哪些不足 10 次。
这张图直接决定了重构优先级:先保证高频接口不动,再逐步改低频接口,最后处理死代码。
2.3 建立测试基准
在改任何代码之前,用 Claude Code 给所有高频接口生成集成测试:
为 billing/views.py 中的 process_payment 和 get_invoice_list 这两个函数
生成集成测试,使用 Django TestCase。
测试要覆盖:
- 正常流程(happy path)
- 必填参数缺失的情况
- 用户无权限的情况
测试数据用 factory_boy 的 factory 方式创建。
不需要 mock 数据库,直接用测试数据库。
生成的测试大概 70% 可以直接运行,30% 需要手动修正(主要是 factory 里不知道的特殊字段约束)。修完之后,这些测试成为整个重构的”安全网”——每次改动都跑一遍,确保核心路径没有回归。
三、第一个大坑:AI 的”善意过度修复”
第 2 周,我让 Claude Code 帮我重构 billing/utils.py 里的一个 500 行帮助函数文件。它做得很好,把 10 几个函数分组、提取、命名优化。
但其中有一个函数,它顺手帮我”修了个 Bug”。
原始代码:
def calculate_discount(price, user_level):
if user_level == 'vip':
return price * 0.8
elif user_level == 'premium':
return price * 0.85
else:
return price * 0.9 # 非会员也有折扣
AI 修改后:
def calculate_discount(price, user_level):
discount_rates = {
'vip': 0.8,
'premium': 0.85,
}
rate = discount_rates.get(user_level, 1.0) # 非会员无折扣
return price * rate
看起来更”干净”,但 else: return price * 0.9 是业务逻辑——非会员也有 9 折。AI 把它改成了 1.0(无折扣),因为它判断”这看起来像个疏漏”。
这个改动通过了我的单元测试(因为测试只覆盖了 vip 和 premium),差点上线。是代码审查时同事发现的。
教训:让 AI 重构时,明确告诉它”不要修改业务逻辑,只做结构性重构”。并且,重构后必须人工 diff 检查每一个值的变化。
四、有效的重构策略:切片而非整体替换
最危险的重构方式是”一次性替换大模块”。我们在第 1 周尝试过,Claude Code 给了一个完整的”重写版 billing app”,代码看起来很漂亮,但调试了 3 天还没能运行起来,最后放弃了。
改为”切片策略”后,效率大幅提升:
策略:Strangler Fig 模式
Strangler Fig(绞杀者无花果)是遗留系统重构的经典模式:
- 在旧代码旁边建新实现,不改旧代码
- 用 feature flag 控制流量,先让 1% 的请求走新逻辑
- 验证无误后逐步扩大,最后删除旧代码
AI 在这个模式中的角色:
任务:把 billing/views.py 的 process_payment 函数
提取成独立的 Service 类 PaymentService,
放在 billing/services/payment.py。
要求:
- 不要修改 process_payment 函数本身
- PaymentService 的接口签名要和现有函数完全一致
- 把所有内部的 db 查询提取成私有方法
- 不要修改任何业务逻辑,只做结构提取
这样的任务 Claude Code 完成得很好,因为边界清晰、可验证。
分阶段提交
每次只改一个小单元(一个类、一个模块),改完立即运行测试,通过后立即 commit,不积攒大改动。
一个 commit 覆盖的改动越小,出问题时回退的代价越低,review 也越容易。6 周下来大约有 280 个 commit,平均每个 commit 差不多改了 50–80 行代码。
五、AI 真正发挥大作用的三件事
5.1 批量添加类型注解
这是整个项目里 AI 效益最高的任务之一。
为 billing/models.py 中所有 Model 的方法添加 Python 类型注解。
对于无法确定类型的参数,使用 Any 并加注释说明原因。
不要修改任何逻辑,只加注解。
一个 1500 行的 models.py,人工加注解可能需要两天,Claude Code 约 3 分钟,90% 的质量可以接受,剩下 10% 手工修正。
加完类型注解后,mypy 能够跑起来,帮我找到了 12 个潜在的 runtime 错误(主要是 Optional 处理不当)。
5.2 文档生成
为 core/services/user_service.py 中的所有 public 方法
生成 Google 风格的 docstring。
每个 docstring 需要包含:
- 一句话功能描述
- Args(参数名、类型、含义)
- Returns(返回值类型和含义)
- Raises(可能抛出的异常)
基于代码推断上述内容,对于不确定的部分用 "TODO: verify" 标注。
这个任务生成的文档质量意外地好——因为函数名和参数名本身已经包含了很多语义信息。
5.3 迁移脚本生成
有几个重构需要数据迁移(比如把一张大表拆成两张)。数据迁移脚本的逻辑一点都不能错:
现有表 billing_invoice 有以下字段:...(贴字段列表)
需要拆分成两张表:
- billing_invoice(保留部分字段)
- billing_payment_record(新表,包含支付相关字段)
请生成一个 Django data migration,要求:
1. 先创建新表
2. 将旧表数据按规则迁移到新表
3. 保留旧表字段直到手动 cleanup(不要在此迁移中删除)
4. 迁移中遇到空值的处理:...
迁移脚本生成后,在测试数据库跑了 3 次验证才上生产。但 AI 生成的骨架节省了我大约 4–5 小时的编写时间。
六、第 4 周的危机:代码越改越乱
第 4 周遇到了一个心理低谷:改了一个模块,带出了另一个模块的问题;修了那个问题,又影响了第三个地方。感觉在拆炸弹。
问题的根源是:我们在没有整体架构规划的情况下进行局部重构。每个局部改动都是合理的,但它们加起来产生了新的混乱。
解决方案:暂停 2 天,做一次”架构复盘”。
让 Claude Code 帮我做:
分析 billing、core、accounts 三个 app 之间的相互依赖关系。
输出:
1. 哪些模块存在循环依赖?
2. 哪些模块的耦合度最高(被其他模块导入最多)?
3. 建议的依赖方向是什么(用有向图描述)?
分析结果显示 core 既依赖 billing,billing 又依赖 core,形成了循环。这个循环是之前 3 次重构带出来的,不解决它后续所有改动都会越来越乱。
花了 3 天专门处理这个循环依赖,之后的推进顺畅了很多。
七、最终结果(6 周后)
| 指标 | 重构前 | 重构后 |
|---|---|---|
| 测试覆盖率(核心路径) | 0% | 68% |
| 最长函数行数 | 847 行 | 120 行 |
| 循环依赖 | 5 处 | 0 处 |
| 平均函数圈复杂度 | 18.3 | 5.7 |
| mypy 错误数 | N/A(无注解) | 12 个(已修复) |
| 死代码量(估算) | 约 8 万行 | 已移除 6.5 万行 |
生产事故:0(重构期间未发生与重构相关的故障)。
八、给下一个做类似项目的人
做的事:
- 先摸底,再动手。不要一上来就让 AI 写代码
- 用测试建安全网,即使是 AI 生成的不完美测试,也比没有强
- 小步提交,每次只改一个清晰边界内的单元
- 明确告诉 AI”不要改业务逻辑”,并 diff 检查每次输出
不要做的事:
- 不要让 AI 一次性重写整个大模块
- 不要跳过架构复盘,局部改动的积累会产生新的混乱
- 不要把 AI 的输出当作”正确答案”,每个输出都需要你去理解和验证
- 不要在没有回滚计划的情况下上生产
九、相关阅读
- AI 重构模式总结:6 种常见重构场景的 Prompt 模板
- 用 Aider 重构历史 Python 代码的实战记录
- Claude Code 真实任务实战:我用它做了哪些事
- Cursor Background Agent 实战案例
- AI 编程工具成本控制:如何不让 Token 费用失控
大规模重构项目需要稳定的 AI API 调用,YoTradeApi 提供高可用的 Claude 和 GPT 中转服务,支持长上下文调用,是遗留代码重构项目的可靠后端。