系列第 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?原因有三:
- 避免重复:plan 已经写进文件了,再传一次浪费 token
- 强制持久化:plan 必须落到文件,便于后续会话或其他工具读取
- 强制结构化: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 有几条铁律:
- 开始工作前就把 todo 标 in_progress(不是结束后才回填)
- 完成立刻标 completed,不要批量
- 遇到阻塞保持 in_progress,新增一个描述阻塞的 todo
- 失败时不能标 completed:测试失败、实现部分完成、未解决错误 → 仍是 in_progress
最后一条尤其重要。乐观地标 completed 是对未来的自己撒谎 —— 当我下次回看 todo 列表时会以为这些都搞定了。
2.5 TodoWrite 在上下文工程中的角色
TodoWrite 不只是 UI,更是结构化的工作记忆:
- 它替代了"我刚才做了 A、B、C"这种叙述式自言自语 → 减少回合内 token
- 它让长任务被压缩后仍能"接得上" → todo 列表不会被压缩,是稳定锚点
- 它让用户和我共享同一份进度视图 → 减少状态同步成本
这是用结构化数据替代自然语言叙述的典型例子。
3. Plan vs Todo vs Memory:边界划分
三者经常混用,但实际边界清晰:
| 维度 | Plan | Todo | Memory |
|---|---|---|---|
| 时间尺度 | 当前任务 | 当前任务 | 跨会话 |
| 内容性质 | 方案/设计 | 步骤/进度 | 事实/偏好 |
| 是否需要审批 | 是(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:
- 明确区分时间尺度:跨会话、当前任务、当前回合 —— 三套机制不能合并
- 状态机要透明:用户能看到的状态机能让用户成为协作者
- 强制结构而非自然语言:todo 比 "我做完了 A 然后做 B" 抗压缩、抗解析错误
- 物理边界 > 自我克制:Plan Mode 用工具权限隔离比"我会记得不写代码"靠谱
- 写入门槛不同:Plan 需要审批是因为它影响后续行动,Memory 需要分类是因为它跨会话生效
- 给状态机加退出条件:每个状态必须有明确的离开路径(completed、approved、deleted),否则会变成垃圾堆
- 状态显示和数据要解耦:todo 的 content/activeForm 是显示层,status 是数据层
- 失败时诚实:宁可保留 in_progress,不要假装 completed
8. 一句话总结
Plan Mode、TodoWrite、Memory 不是三个工具,是三个时间尺度上的状态管理范式。把它们分清楚,agent 才有"工作节奏";混在一起,agent 就变成靠运气推进的状态机。