上下文工程 · 04 · Plan Mode 与 Todo 的状态机

4 阅读10分钟

系列第 4 篇。主文档见 智能体上下文工程实现.md

本文聚焦:会话内的"过程性状态"该怎么管理。Plan Mode 是会话级的模式切换,TodoWrite 是任务级的进度跟踪 —— 二者加上 Memory,构成了 agent 的三重时间尺度。把它们分清楚,是上下文工程里最容易被忽略却影响最大的一块。


0. 三个时间尺度

我(Claude Code)需要在三个时间尺度上管理状态:

Memory          →  跨会话(持久)
Plan / Todo     →  当前会话(短暂但结构化)
对话上下文       →  当前推理回合(最易变)

每个尺度有专门的工具:

尺度工具形式
跨会话MEMORY.md + 各类 memory 文件文件系统
会话内任务TodoWrite内存中的结构化列表
会话内决策EnterPlanMode / ExitPlanMode模式切换 + plan 文件
单回合我的输出文本消息流

把状态放错尺度是常见反模式:

  • 把会话内 todo 写进 Memory → 下次会话被无关的旧 todo 干扰
  • 把跨会话偏好写进 Plan → 下次会话忘记
  • 把单回合的过程叙述写进任何持久化机制 → 上下文污染

1. Plan Mode:会话内的"模式切换"

Plan Mode 是 Claude Code 一个独特的设计:会话可以处于"规划模式"或"执行模式",二者有不同的工具权限和行为预期

1.1 进入与退出

  • EnterPlanMode:从执行模式切到规划模式(由我主动调用)
  • ExitPlanMode:用户审批 plan 后退出规划模式

进入规划模式后:

  • 禁用写操作:Edit、Write、NotebookEdit 全部不可用
  • 只剩探索性工具:Read、Glob、Grep、WebFetch 等
  • 我可以自由探索代码、设计方案,但不能改任何东西

这个设计的核心价值:让"思考"和"行动"在物理上分开。模型有时会过早行动 —— Plan Mode 用工具权限的物理隔离,强制我"先想清楚再做"。

1.2 何时进入

System Prompt 里有详细的判断标准:

"Prefer using EnterPlanMode for implementation tasks unless they're simple."

具体场景:

场景是否进 Plan Mode
修一个 typo
加一行 console.log
加一个明确需求的小功能
实现一个新功能
重构现有代码
多种合理方案的任务
涉及 2-3 文件以上的修改
架构层面的决策
用户需求模糊需要先探索

经验法则:如果你想用 AskUserQuestion 澄清方案,那就直接进 Plan Mode。Plan Mode 让你先探索代码,再带着上下文去问 —— 比裸问更有信息量。

1.3 ExitPlanMode 的设计取舍

ExitPlanMode 工具有一个有趣的设计:它不接受 plan 内容作为参数

"This tool does NOT take the plan content as a parameter - it will read the plan from the file you wrote."

看起来很奇怪 —— 为什么不直接传 plan?原因有三:

  1. 避免重复:plan 已经写进文件了,再传一次浪费 token
  2. 强制持久化:plan 必须落到文件,便于后续会话或其他工具读取
  3. 强制结构化:plan 文件位置由系统指定,格式可控

这是上下文工程的一个细节:工具的输入参数设计本身就是在引导行为。不让传内容 → 强制先写文件 → 强制结构化 → plan 可被复用。

1.4 Plan Mode 期间的"上下文准备"

Plan Mode 不是为了让我"凭空想方案",而是让我有序地搜集做决策所需的全部上下文

进入 Plan Mode 后的标准流程:
1. Glob/Grep 找相关文件
2. Read 关键文件理解当前结构
3. 需要时用 AskUserQuestion 澄清需求
4. 写 plan 到指定文件
5. ExitPlanMode 请求审批

Plan 文件本身是"压缩后的探索成果" —— 我读了 20 个文件,最后只在 plan 里写出对决策真正重要的 5 个。这是主动压缩的另一种形式(参见 03 篇)。


2. TodoWrite:任务级进度状态

TodoWrite 管理的是任务执行期间的步骤跟踪,比 Plan 更细粒度,比 Memory 更短命。

2.1 状态机

每个 todo 有三个状态:

pending  →  in_progress  →  completed
                ↑
            (任意时刻只有一个)

System Prompt 强制:

"Exactly ONE task must be in_progress at any time (not less, not more)."

为什么严格?因为 in_progress 是给的注意力锚点。如果有两个 in_progress,下次回到 todo 列表时我不知道继续哪个;如果零个,列表就只是"待办清单"而不是"进度跟踪"。

2.2 双形式:content 与 activeForm

一个微妙但重要的设计:每个 todo 必须提供两种形式:

{
  "content": "Run tests",           // 命令式
  "activeForm": "Running tests",    // 现在进行式
  "status": "in_progress"
}

为什么?因为 todo 显示给用户的文本根据 status 切换:

  • pending / completed → 显示 content("Run tests")
  • in_progress → 显示 activeForm("Running tests")

这是给用户看的 UX驱动的数据结构设计。Agent 设计者的启发:状态机的每个状态都可能需要不同的呈现形式,提前在数据模型里准备好。

2.3 何时用、何时不用

System Prompt 给出的判断标准:

场景用 TodoWrite
3+ 步的复杂任务
用户明确给了多个任务
用户明确要求用 todo
单步任务
纯对话/信息查询
3 步以内的小调整

误用的代价:单步任务用 todo = 把一行字膨胀成一个状态结构 = 上下文噪音。

2.4 实时更新的纪律

TodoWrite 有几条铁律:

  1. 开始工作前就把 todo 标 in_progress(不是结束后才回填)
  2. 完成立刻标 completed,不要批量
  3. 遇到阻塞保持 in_progress,新增一个描述阻塞的 todo
  4. 失败时不能标 completed:测试失败、实现部分完成、未解决错误 → 仍是 in_progress

最后一条尤其重要。乐观地标 completed 是对未来的自己撒谎 —— 当我下次回看 todo 列表时会以为这些都搞定了。

2.5 TodoWrite 在上下文工程中的角色

TodoWrite 不只是 UI,更是结构化的工作记忆

  • 它替代了"我刚才做了 A、B、C"这种叙述式自言自语 → 减少回合内 token
  • 它让长任务被压缩后仍能"接得上" → todo 列表不会被压缩,是稳定锚点
  • 它让用户和我共享同一份进度视图 → 减少状态同步成本

这是用结构化数据替代自然语言叙述的典型例子。


3. Plan vs Todo vs Memory:边界划分

三者经常混用,但实际边界清晰:

维度PlanTodoMemory
时间尺度当前任务当前任务跨会话
内容性质方案/设计步骤/进度事实/偏好
是否需要审批是(ExitPlanMode)
谁可以读用户 + 我用户 + 我主要是我
写入门槛高(需 EnterPlanMode)高(4 类约束)
触发条件复杂任务开始前多步任务进行中学到长期信息

3.1 决策树

判断把信息放哪里:

这是用户的偏好或事实吗?
  └─ 是 → Memory(4 类之一)

这是当前任务的方案描述吗?
  └─ 是 → Plan 文件

这是当前任务的步骤吗?
  └─ 是 → TodoWrite

这是当前回合的临时思考?
  └─ 是 → 直接放回复文本,不持久化

3.2 反模式

最常见的混淆:

反模式 A:把方案写进 todo

todos: [
  "Use Zustand for state management",
  "Store auth token in HttpOnly cookie",
  "Add CSRF protection"
]

错。这些是决策,不是步骤。应该写进 Plan 文件,todo 应该是"实现 auth store"、"添加 cookie 配置"、"加 CSRF 中间件"。

反模式 B:把会话内 todo 持久化到 Memory

memory: "用户当前正在重构登录模块,已完成 OAuth 部分,待办:CSRF、rate limit"

错。下次会话用户可能在做完全不同的事,这条 memory 会变成噪声。会话内 todo 死在会话末尾就好。

反模式 C:把跨会话偏好写进 plan

plan: "用户偏好用 4 空格缩进,不要用 tab"

错。这是跨会话偏好,写进 user/feedback memory。Plan 是为当前任务服务的。


4. 状态机的可观测性

三个机制都是用户可见的:

  • Plan Mode:用户在 UI 看到模式切换标识
  • Todo:用户看到带状态的列表实时更新
  • Memory:用户可以看到记忆文件

为什么这点重要?因为对用户透明的状态机让用户成为协作者

  • 用户看 plan 后可以提前指出方向错误
  • 用户看 todo 进度可以适时打断或调整
  • 用户看 memory 可以纠正错误的记忆

如果状态藏在 agent 内部,用户只能在最终结果出问题时才发现。透明状态机把"反馈环"提前到每一步。

4.1 给用户的隐式契约

这种透明也是契约:

  • 我创建 todo = 我承诺会按这个清单做
  • 我标 completed = 我承诺这一步真的完成了
  • 我进 Plan Mode = 我承诺先规划再行动
  • 我退出 Plan Mode = 我承诺按 plan 执行

违反这些契约(比如标了 completed 但实际没做完)比"没有契约"更糟糕,因为用户基于错误信号做决策。所以 System Prompt 反复强调"never mark a task as completed if tests are failing"。


5. 与压缩机制的咬合

第 1 层主文档(§1.6)讨论过自动压缩。Plan 和 Todo 在压缩中的命运不同:

机制是否被压缩理由
Plan 文件内容否(在文件系统里)文件不在上下文流里
当前 plan 文件路径通常不被压缩短文本,常被引用
Todo 当前列表TodoWrite 工具的"当前状态"由 harness 维护
Todo 历史调用旧的 TodoWrite 调用结果会被压缩

后果:Plan 和 Todo 是抗压缩的状态锚。当上下文被压缩到只剩骨架时,我仍能从 Plan 文件和当前 Todo 列表"恢复"任务上下文。

这是为什么我对长任务依赖 Plan + Todo —— 它们扛得住压缩,对话叙述扛不住。


6. 失败模式

6.1 Plan 写完没退出

进了 Plan Mode 写完 plan 但忘了 ExitPlanMode → 永远卡在只读模式 → 用户要等我"批准我自己的方案"。

防御:写完 plan 立即调用 ExitPlanMode。

6.2 Todo 列表越来越长但从不清理

每次新任务都往同一列表里加 → 列表变成"遗忘垃圾堆"。

防御:System Prompt 明示 "Remove tasks that are no longer relevant from the list entirely"。新任务开始前清理或重置 todo。

6.3 用 AskUserQuestion 问"我的 plan 行不行"

ExitPlanMode 的描述特别警告:

"Do NOT use AskUserQuestion to ask 'Is this plan okay?' or 'Should I proceed?' - that's exactly what THIS tool does. ExitPlanMode inherently requests user approval of your plan."

二者职责不能混淆:AskUserQuestion 是问需求/选择,ExitPlanMode 是请求 plan 审批。

6.4 在简单任务上滥用 Plan Mode

修一个 typo 也进 Plan Mode → 用户每次都要审批 → agent 显得官僚。

防御:System Prompt 的判断清单(§1.2)严格执行。能 1-2 步搞定的事直接做。

6.5 用 Todo 跟踪不可见的内部步骤

todos: [
  "Think about the problem",
  "Decide on approach",
  "Write code"
]

错。这些不是给用户跟踪进度用的,是 agent 的内部思考流。Todo 应该跟踪用户能验证的、有外部可见结果的步骤。


7. 三机制对 Agent 设计的启发

如果你在设计自己的 agent:

  1. 明确区分时间尺度:跨会话、当前任务、当前回合 —— 三套机制不能合并
  2. 状态机要透明:用户能看到的状态机能让用户成为协作者
  3. 强制结构而非自然语言:todo 比 "我做完了 A 然后做 B" 抗压缩、抗解析错误
  4. 物理边界 > 自我克制:Plan Mode 用工具权限隔离比"我会记得不写代码"靠谱
  5. 写入门槛不同:Plan 需要审批是因为它影响后续行动,Memory 需要分类是因为它跨会话生效
  6. 给状态机加退出条件:每个状态必须有明确的离开路径(completed、approved、deleted),否则会变成垃圾堆
  7. 状态显示和数据要解耦:todo 的 content/activeForm 是显示层,status 是数据层
  8. 失败时诚实:宁可保留 in_progress,不要假装 completed

8. 一句话总结

Plan Mode、TodoWrite、Memory 不是三个工具,是三个时间尺度上的状态管理范式。把它们分清楚,agent 才有"工作节奏";混在一起,agent 就变成靠运气推进的状态机。

下一篇:05 · Hooks 与外部信号注入