复杂报错的拆解范式:Plan、Agent、TodoWrite 怎么配合
大多数介绍 Claude Code 的文章会告诉你”怎么用 Plan 模式拟计划”、“怎么用 Agent 并行查东西”、“怎么用 TodoWrite 追踪进度”。单独讲每一个的时候,这些工具看起来都很朴素,朴素到让人怀疑”这东西有什么好说的”。
但真正的价值不在于它们各自是什么,而在于当你在处理一个复杂 bug 时,这三个工具合起来能把”难收敛的问题”变成”可收敛的问题”。这篇文章就讲这件事。
简单 bug 和复杂 bug 是两种不同的问题
先区分一下这两类,否则下面的讨论会失去对比。
简单 bug 的特征是:有一个单一触发点,修掉那个点就好了。比如你在某个函数里写了 x.length 但 x 是 undefined,加一个判空就解决。这类 bug 最适合的工作方式是:看到报错 → 定位到行 → 改 → 跑一下 → 关工单。整个过程不需要任何”范式”。
复杂 bug 的特征恰好相反:要么有多个触发点,要么触发点不在报错行附近,要么当前看到的”修法”其实只是一个补丁,下次还会从另一个地方炸出来。它的核心困难不是”不会修”,而是**“不知道自己到底该修多少个地方”**。
举一个我最近遇到的真实例子。某天扩展的 webview 报错:
Error rendering content: Cannot read properties of undefined (reading 'trim')第一眼看去这是最典型的”简单 bug”——一定是某个地方在 undefined.trim()。你搜 .trim(),定位到一个 mH1 函数里的 $.text.trim() 没做判空,加个 ?? "",提交。
这就是补丁式修复的陷阱。你以为你修完了,过两天同样的报错又从另一个地方冒出来——这次不是 $.text,是 command.title。你又修一个点,过两天再冒一个 toolResult.text。这时候你才会意识到:这类问题不是”某个地方漏了判空”,是某种数据 shape 在多个消费点都没有被规范化。
复杂 bug 的共同敌人有一个名字:补丁式修复。它会让你陷入一个”修了,但还在”的循环,每次都觉得”这次应该真的修完了”。
让我换个角度讲:复杂 bug 的核心任务不是”修一个 bug”,是”证明这类 bug 不会再出现”。这件事靠单点修复是做不到的。
Plan 的作用:强迫你先形成假设再动手
Claude Code 的 Plan 模式最表层的作用是”写个计划给用户看”。但它真正的价值在更深的地方:它强制你在动手之前先形成一个完整的假设。
什么叫”完整的假设”?不是”我要修这个 trim 的 bug”,而是:
- 这个错误的真正形状是什么?单点还是多点?
- 如果是多点,所有候选触发点在哪里?
- 如果修掉一个还会复发,根本的数据 shape 问题在哪里?
- 真正的修复应该在数据入口归一化还是每个消费点做判空?
只有先回答了这些问题,你才有权利去动键盘。Plan 模式逼你把这些问题写出来,写出来的东西你自己会审视一遍——这个动作就已经把 80% 的”下意识补丁”堵掉了。
在 undefined.trim() 这个案例里,Plan 阶段要先形成一个基本判断:“看起来像单点,但要当多点假设来验证”。这一行写下来,后面的动作就完全不一样了。如果不写这行,你会直接去修 mH1 然后收工。
Plan 模式不是给用户看的花架子。它是一个前置的认知约束——没形成假设就不让你动手。
Agent 的作用:并行挖证据,不让假设停在纸面上
假设已经形成了:“可能是多点”。下一个问题是:怎么高效地验证?
手动地在代码库里一次次地 grep、一次次地读可疑文件、一次次地跟踪调用链,这是可行的但效率很差。更重要的是,手动操作的时候你会倾向于”找到一个就停下来高兴一下”——这正是补丁式修复的认知前奏。
Agent 工具(尤其是 Explore subagent)的真正价值在这里出现:它可以让你同时问几个完全独立的问题,并且互不干扰。
在那个 undefined.trim() 案例里,我让三个 Explore agent 并行跑:
- Agent 1:搜索所有
.trim()调用,筛出接收 undefined 可能的入口 - Agent 2:扫描消息构造链路,看哪些地方会生成没有 text 字段的 content block
- Agent 3:读取 content block 的消费端(渲染、序列化、判空逻辑),看哪些地方对 shape 做了假设
这三个问题如果串行做,我会不知不觉地把结论收束到”最容易发现的那一个”。并行做的时候,三份结果回来同时摆在我面前,我看到的是一张”所有候选点地图”,而不是”最先找到的那一个点”。
这里有一个微妙但很重要的认知效应:并行获取证据能对抗”收敛过早”的倾向。人在串行搜索的时候天然会在”第一个看起来合理的解释”上停下来(确认偏见);同时拿到多个视角的结果会逼你做真正的比较。
Agent 的另一个作用是保护主上下文。让主会话去 grep、读文件、追调用链,会让上下文被大量中间结果污染,真正做判断的时候反而信号稀薄。让 Explore agent 去做脏活,只把总结带回主会话,主会话留的是清晰的判断空间。
TodoWrite 的作用:把”根除”从模糊目标变成可收敛清单
三个 agent 跑完,你手里现在有一堆候选点——可能 4 个、可能 8 个。这时候最大的风险是**“我已经看见它们了”变成”我已经处理它们了”的幻觉**。
人脑很擅长在”知道有这些事”和”做完这些事”之间打马虎眼,尤其在你已经修了其中 2 个、开始感觉”应该差不多了”的时候。TodoWrite 在这一步的作用是反向地逼着你承认”还没做完”。
把 Agent 返回的每一个候选点变成一条明确的 todo:
- [ ] 归一化 content block shape 在构造入口(CY 构造函数)- [ ] setToolResult 写入前强制 normalize text block- [ ] mH1 函数改为基于安全字符串判断- [ ] m30 函数改为安全读取- [ ] command.title 处加 ?? ""- [ ] 全链路回测:验证这 5 个点覆盖了报错 trace 里的所有调用路径每一条都写得非常具体。然后你按顺序做,每做完一条划掉一条。看起来像”给学生写作业清单”,但它真正的作用是把”根除”这个模糊目标拆成一组明确的、可勾选的动作。
没有 TodoWrite 的做法是什么?你记在脑子里:“我记得还有一个 toolResult 要改,还有什么来着……反正都差不多了,先跑一下试试”。这就是补丁式修复最后一步的经典姿势。
TodoWrite 还有一个副作用:它让**“还剩几项”这件事可见**。当你划到倒数第二项时,你会很清楚地知道”再忍一下就真的根除了,现在不能停”。这种可视化的压力比任何”专注力”都管用。
三个工具的配合顺序
单独看每一个工具都不神奇。它们的价值在连起来——因为这三个工具各自对应了复杂 bug 拆解的一个必要环节:
| 环节 | 工具 | 对应的认知动作 |
|---|---|---|
| 形成假设 | Plan | 先想清楚”这是单点还是多点” |
| 验证假设 | Agent | 并行挖证据,防止过早收敛 |
| 执行收敛 | TodoWrite | 把”还没做完”可视化,防止半途收兵 |
标准的流程是这样的:
- 看到报错,进入 Plan 模式。写下”真正的形状假设”——不是修法,是问题形状
- 假设需要验证时,从 Plan 里延伸出几个并行 Agent 任务。让 Explore subagent 去挖证据,主会话不动
- Agent 结果回来,更新 Plan——确认假设、调整假设或推翻假设
- 假设确认后,把修复动作落成 TodoWrite 清单,每一条足够具体
- 清单执行过程中如果发现新的候选点(Agent 没挖到的),就追加到 todo 里,不要”顺手一改过了再说”
- 所有 todo 划完后,做一次整体回测——这一条也应该是 todo 的最后一项
这套流程里最容易被偷工减料的是第 5 步。做到一半发现有新点时,人的本能是”顺手改了继续”,但这会打破你之前建立的”可收敛清单”——一旦有”顺手改”的漏网动作,你就回到了”记在脑子里”的状态,TodoWrite 的作用也就没了。坚持”新点必须上 todo 再处理”,这条纪律是整个范式能不能奏效的门槛。
什么时候不要用这套
这套范式不是万能的,也不应该万能。
简单 bug 不要用。如果一个报错一眼就能看到修法,不要强行走 Plan / Agent / TodoWrite——三个工具的开销加起来远大于修一个判空的成本。坚持用范式会变成一种”表演式工程”,看起来很专业,其实在浪费时间。
探索性工作不要用。如果你还不知道自己想做什么(“我想试试能不能用 X 方案”),这套范式会过早地把你锁进一个假设。探索期适合的是”快速试、多失败、低投入”,和”形成假设再行动”的哲学是矛盾的。
已知是单点补丁的时候不要装作根除。有时候你就是要打一个补丁,因为完整修复的成本太大或者时间不够。这种情况下坦诚地打补丁、留 FIXME、记一笔欠债,比假装自己在”根除”诚实得多。这套范式是给”我有能力也有意愿根除这类 bug”的场景用的,不是给”我在打补丁但想让它看起来更严肃”的场景用的。
结尾:工具的意义不是更快,是让问题可收敛
回到最开始的那个观察——这三个工具各自看都很朴素。但它们放在一起解决的是一个非常具体的问题:
复杂 bug 真正难的从来不是”怎么修”,是”怎么知道自己已经修完了”。
Plan 让你先想清楚要修哪一类问题。Agent 让你并行地收集候选点,对抗过早收敛的本能。TodoWrite 让”还没做完”这件事变得可见,逼你走到真正的终点。
这三件事合起来做的,是把一个原本”难收敛”的问题变成一个可收敛的问题。它不会让你修得更快——大多数时候反而更慢。它会让你修得更彻底,让同一个 bug 在第三次出现的概率显著下降。
而”第三次不再出现”这件事的价值,通常远大于”这一次修得更快”。
如果你用 Claude Code 时还停留在”让它一次性解决一个 bug”的用法上,下一次遇到一个有点硬的问题时,试一下这个顺序:先 Plan 形成假设、再 Agent 并行验证、最后 TodoWrite 收敛动作。你会发现自己从”碰运气修 bug”的模式,换到了”系统性根除”的模式——而这两种模式之间的差距,比用不用 AI 的差距还要大。