复杂报错的拆解范式:Plan、Agent、TodoWrite 怎么配合

3098 字
15 分钟
复杂报错的拆解范式:Plan、Agent、TodoWrite 怎么配合

大多数介绍 Claude Code 的文章会告诉你”怎么用 Plan 模式拟计划”、“怎么用 Agent 并行查东西”、“怎么用 TodoWrite 追踪进度”。单独讲每一个的时候,这些工具看起来都很朴素,朴素到让人怀疑”这东西有什么好说的”。

但真正的价值不在于它们各自是什么,而在于当你在处理一个复杂 bug 时,这三个工具合起来能把”难收敛的问题”变成”可收敛的问题”。这篇文章就讲这件事。

简单 bug 和复杂 bug 是两种不同的问题#

先区分一下这两类,否则下面的讨论会失去对比。

简单 bug 的特征是:有一个单一触发点,修掉那个点就好了。比如你在某个函数里写了 x.lengthx 是 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把”还没做完”可视化,防止半途收兵

标准的流程是这样的:

  1. 看到报错,进入 Plan 模式。写下”真正的形状假设”——不是修法,是问题形状
  2. 假设需要验证时,从 Plan 里延伸出几个并行 Agent 任务。让 Explore subagent 去挖证据,主会话不动
  3. Agent 结果回来,更新 Plan——确认假设、调整假设或推翻假设
  4. 假设确认后,把修复动作落成 TodoWrite 清单,每一条足够具体
  5. 清单执行过程中如果发现新的候选点(Agent 没挖到的),就追加到 todo 里,不要”顺手一改过了再说”
  6. 所有 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 的差距还要大。

Profile Image of the Author
xt53
Hi
分类
标签
站点统计
文章
10
分类
2
标签
19
总字数
22,614
运行时长
0
最后活动
0 天前

目录