用AI重构代码的正确姿势

7 阅读1分钟

用AI重构代码的正确姿势

SmartInspector 经历了一次涉及全部4层的重构:采集层、Agent编排层、SDK层、基础设施层。全程用 Claude Code 辅助,踩了不少坑,也总结出了一套可复用的方法论。

项目地址:github.com/mufans/AppS…

先别动手:重构的准备工作

很多人一说重构就直接让 AI "优化这个模块"。结果就是 AI 到处改一通,改好改坏混在一起,最后你自己都不知道哪些改动是安全的。

SmartInspector 的重构之前,我做了两件事:

第一,写改造方案。 不是口头说说,是落到文档里。4层改造方案,每层包含:当前问题、目标状态、具体修改点、接口变更、测试策略。4个文档加起来约6000字,后来成了给 Claude Code 的上下文。

第二,建 worktree。refactor-merge 分支上做改造,主分支不受影响。每个模块改完单独建分支(refactor/collector-p0refactor/sdk-p0 等),测试通过后合并到 refactor-merge,最后一次性合入主分支。

这两步花了大约一天时间,但省掉了后面无数的返工。

核心原则:一次只改一个模块

这是最重要的原则。AI 在处理单点任务时成功率超过 90%,但如果你让它"全面重构",成功率会降到 50% 以下。

为什么?因为 AI 会倾向于在局部做改动,而不是从全局视角重新设计。它会修改函数 A 的签名,但忘了更新调用方 B;它会改了数据结构,但忘了同步修改序列化逻辑。

SmartInspector 的 4 层改造,我就是严格按优先级一层一层来的:

P0 → 采集层(修复数据覆盖Bug)
P0 → SDK层(Release变体no-op化)
P1 → Agent编排层(意图路由、Token优化)
P2 → 基础设施层(REPL健壮性)

每改完一层,跑完全部测试,确认没问题再进下一层。89 个 commit 里的重构相关 commit 大约 20 个,分布在这 4 个模块里,每层 5 个左右。

让 AI 自己跑测试、自己修 Bug

这个技巧来自 1.2 文章提到的"原则 3",在重构场景下尤其重要。

我给 Claude Code 的每个任务描述末尾都加了一句话:

修改完成后运行 python3 -m pytest tests/ -v,
如果有测试失败,修复后重新运行,直到全部通过再 commit

效果很明显:AI 自己改代码 → 跑测试 → 发现失败 → 分析原因 → 修复 → 再测 → 通过 → commit。整个循环不需要我介入。

一个真实的例子:SDK 层的 registered 集合泄漏修复。CC 第一次改完跑了测试,发现 test_tracelib 失败了,因为它没有考虑到 release 变体下的初始化顺序。它自己分析失败原因,调整了初始化逻辑,再跑测试,通过了,然后 commit。全程我什么都没做。

但有一个前提:测试覆盖率要够。 SmartInspector 有 24 个测试用例,覆盖了主要模块的输入输出。如果没有测试,AI 改了什么你都不确定,这时候只能人肉 review。

踩坑实录:AI 改了 3 次都没修对的 Bug

这是整个重构过程中最典型的反面案例。

问题:SmartInspector 的 WebSocket 采集层会实时推送 block events(方法耗时数据),collector 收到后存入 self.events。但同时,collector 也会从 Perfetto trace 的 SQL 查询结果中获取 events。两份数据需要合并。

Bug 在 collector.py 第 79 行:

# Bug:直接赋值覆盖了 WS 传来的 events
self.events = query_result['events']  # ← 应该是合并

这行代码把 WebSocket 推送的实时数据直接覆盖了。

AI 的 3 次尝试

第 1 次:我描述了"events 被覆盖"的问题,CC 把赋值改成了 extend。但它没考虑到 WS 推送的 events 可能和 SQL 查询的 events 有重复(同一个方法调用在两边都有记录),导致数据重复。

第 2 次:我补充了"需要去重"的需求,CC 加了基于 id 的去重逻辑。但它用了 list 的 O(n²) 去重方式,没考虑性能。

第 3 次:我直接给了去重方案"用 dict 按 id 去重",CC 改成了 dict 去重。但测试还是失败——因为它不理解 WS 推送和 SQL 查询的时序关系:WS 是异步推送的,可能在 SQL 查询之后才到,所以合并的时机不对。

最终方案:我自己花了 20 分钟定位,用了 event_id 作为唯一键,在 collector 的 merge_events 方法里做了幂等合并。核心逻辑不到 10 行代码。

教训:涉及运行时状态、时序、多模块交互的 Bug,AI 很难搞定。它能看到代码结构,但理解不了运行时的数据流向。这类问题自己动手更快。

4 层改造的具体落地

采集层:数据流修复

除了上面那个 Bug,采集层还做了 Perfetto trace 采集的健壮性改进:

  • trace 文件可能为空或格式损坏 → 加了格式校验和重试
  • SQL 查询可能超时 → 加了超时控制和 fallback
  • 大 trace 文件的内存占用 → 加了分页查询

这些改动边界清晰,AI 处理起来很顺手。5 个 commit,全部一次通过。

Agent 编排层:架构调整

这是改动最大的一层。核心变更是把意图路由从"关键词匹配"改成了"LLM few-shot 分类"。

之前:
  用户输入 → 关键词匹配 → 路由到对应 Agent

之后:
  用户输入 → LLM 分类(max_tokens=5)→ 路由到对应 Agent

这个变更的影响面很广:State 结构要调整、prompt 要重写、Agent 的输入输出要适配。

我的做法:先画新的数据流图,然后让 CC 按图施工。每改一个模块就跑测试,确认没有破坏现有功能。

AI 在这类任务中的价值不在于"设计",而在于"执行"。你给它明确的目标和边界,它能高效地把代码改完。

SDK 层:Android 端的 no-op 化

Android SDK 的 tracelib 模块需要在 Release 构建中完全不执行任何 trace 逻辑。之前的做法是在关键方法里加 if (BuildConfig.DEBUG) 判断,但这种方式有隐患:BuildConfig.DEBUG 的值可能在某些构建系统中不正确。

改造方案:用 release product flavor 替换整个实现为 no-op stub。

// release/java/.../TraceHook.java
public class TraceHook {
    public static void start() { /* no-op */ }
    public static void end() { /* no-op */ }
    public static void event(String name, long duration) { /* no-op */ }
}

这种方式的好处:编译期就确定了行为,不依赖运行时判断,零开销。CC 很容易理解这种模式,一次就写对了。

基础设施层:健壮性加固

  • REPL:处理了用户输入非法命令时的崩溃问题
  • WebSocket:加了启动失败的重试和超时控制
  • 配置:修了 PineConfig.debug 在 Release 构建中的泄漏

这些都是小改动,每个 1-2 个 commit,AI 独立完成。

重构的节奏控制

总结一下 SmartInspector 重构的节奏:

Day 1:  写改造方案(4层文档)
Day 2:  采集层 P0 修复(5 commits)
Day 3:  SDK层 P0(3 commits)+ 测试验证
Day 4-5: Agent编排层 P1(8 commits)
Day 6:  基础设施层 P2(4 commits)+ 全量回归测试
Day 7:  合并到主分支,更新文档

7 天,20 个 commit,4 层改造。其中 AI 独立完成的大约 15 个 commit,需要我介入的 5 个(主要是那个 trace 数据覆盖 Bug 和架构调整的决策)。

节奏的关键

  1. 每层独立验证,不混合改动
  2. 测试先行,每个 commit 都能跑通全部测试
  3. 架构决策自己做,代码执行交给 AI
  4. 遇到 AI 连续两次搞不定的问题,立刻收回手动处理

几条务实建议

  1. 方案先于代码。 花 20% 的时间写方案,能省掉 50% 的返工。AI 按方案施工比自己从零推理效果好得多。

  2. 分支隔离。 用 worktree 或 feature branch 做重构,主分支保持可发布状态。每个模块改完单独验证,再合并。

  3. 一次一个模块。 这是重中之重。AI 处理单点任务的成功率和处理全局任务的成功率差了将近一倍。

  4. 测试是你的安全网。 有测试,AI 才能自己验证修改。没有测试,每次改动都是赌博。

  5. 知道 AI 的边界。 AI 擅长"给定明确输入输出实现逻辑",不擅长"理解运行时系统找到隐含问题"。前者交给它,后者自己做。

  6. commit 要小。 每个 commit 只改一件事。这样即使后面发现问题,也能快速定位和回退。


下一篇预告:Token 消耗优化 —— SmartInspector 一次分析要跑几轮 LLM 调用,max_tokens=5 的意图路由能省多少 Token,TokenTracker 是怎么实现的。