用AI重构代码的正确姿势
SmartInspector 经历了一次涉及全部4层的重构:采集层、Agent编排层、SDK层、基础设施层。全程用 Claude Code 辅助,踩了不少坑,也总结出了一套可复用的方法论。
先别动手:重构的准备工作
很多人一说重构就直接让 AI "优化这个模块"。结果就是 AI 到处改一通,改好改坏混在一起,最后你自己都不知道哪些改动是安全的。
SmartInspector 的重构之前,我做了两件事:
第一,写改造方案。 不是口头说说,是落到文档里。4层改造方案,每层包含:当前问题、目标状态、具体修改点、接口变更、测试策略。4个文档加起来约6000字,后来成了给 Claude Code 的上下文。
第二,建 worktree。 在 refactor-merge 分支上做改造,主分支不受影响。每个模块改完单独建分支(refactor/collector-p0、refactor/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 和架构调整的决策)。
节奏的关键:
- 每层独立验证,不混合改动
- 测试先行,每个 commit 都能跑通全部测试
- 架构决策自己做,代码执行交给 AI
- 遇到 AI 连续两次搞不定的问题,立刻收回手动处理
几条务实建议
-
方案先于代码。 花 20% 的时间写方案,能省掉 50% 的返工。AI 按方案施工比自己从零推理效果好得多。
-
分支隔离。 用 worktree 或 feature branch 做重构,主分支保持可发布状态。每个模块改完单独验证,再合并。
-
一次一个模块。 这是重中之重。AI 处理单点任务的成功率和处理全局任务的成功率差了将近一倍。
-
测试是你的安全网。 有测试,AI 才能自己验证修改。没有测试,每次改动都是赌博。
-
知道 AI 的边界。 AI 擅长"给定明确输入输出实现逻辑",不擅长"理解运行时系统找到隐含问题"。前者交给它,后者自己做。
-
commit 要小。 每个 commit 只改一件事。这样即使后面发现问题,也能快速定位和回退。
下一篇预告:Token 消耗优化 —— SmartInspector 一次分析要跑几轮 LLM 调用,max_tokens=5 的意图路由能省多少 Token,TokenTracker 是怎么实现的。