03 · Plan-and-Execute:先谋后动
前置:01 ReAct 的 4.6 节(
messages如何增长)。本篇所有问题都源于那里。
一、问题:任务一长,Agent 就自己收工了
给 ReAct Agent 派活:「把项目里所有旧版日志库调用改成新版,改完跑测试。」
第 1 步 搜索 → 找到 12 处
第 2-8 步 逐个改 → 改完 6 个
第 9 步 第 7 个文件还依赖了旧版专有方法
第 10 步 查新版替代
第 11 步 修好这个文件
第 12 步 「已完成日志库迁移。」 ← 还剩 5 个文件,测试一次没跑
成因不是模型笨,是上下文结构。 每轮调模型都要重发完整历史,此时历史是:
| 位置 | 内容 |
|---|---|
| 最前面 | 「12 处 + 跑测试」这个全局目标 |
| 中段 | 11 轮工具调用,上万 token 的文件内容 |
| 末尾 | 刚修好第 7 个文件 |
模型注意力在长上下文中段最弱(lost in the middle),最清楚的是末尾。刚修完一个文件、感觉完整,就收工了。
ReAct 的结构缺陷:只有「下一步」,没有「全局」。
两个朴素解法都不够用:
- 在 system prompt 里强调目标 —— 它是静 态的,记不住「已经改了几个」
- 让模型先列个计划 —— 方向对,但计划列完放哪?打印出来模型下轮看不见;塞进 system prompt 就改不了,而第 9 步恰好证明计划一定要改
所以这个范式真正要解决的是两件事:① 全局目标怎么始终停在模型看得见的地方;② 计划中途怎么改。
二、两条路线
| ① 静态计划 | ② 动态 todo | |
|---|---|---|
| 计划何时定 | 开头一次 | 随时改 |
| 存在哪 | 一个 Python 变量 | Agent 状态 + 上下文 |
| 谁维护 | 你的编排代码 | 模型自己,通过工具调用 |
| 代表 | Plan-and-Solve(arXiv 2305.04091) | Claude Code TodoWrite、Manus todo.md、LangChain write_todos |
| 适合 | 步骤能提前想清的推理题 | 边做边发现的工程任务 |
生产环境跑的基本都是② —— 真实任务的计划一定会变。但①值得先看,因为它把「执行器需要哪些信息」暴露得最清楚。
三、静态计划:执行器需要四样东西

图 3-1 静态计划的两阶段
图片来源:Hello-Agents 第四章(CC BY-NC-SA 4.0)
规划器没什么可讲的 —— 一次普通调用,要它输出步骤列表。重点在执行器:执行第 3 步时要告诉模型什么?
EXECUTOR_PROMPT = """严格按计划解决"当前步骤",只输出该步答案。
# 原始问题: {question}
# 完整计划: {plan}
# 历史步骤与结果: {history}
# 当前步骤: {current_step}
"""
四个占位符,各对应一种翻车:
| 占位符 | 不给会怎样 |
|---|---|
question | 忘了最终目标,把第 3 步当孤立小任务做 |
plan | 不知道当前步的位置,越界去干第 4 步 |
history | 拿不到前面算出的中间值,只能瞎猜 |
current_step | 试图一次把整个问题解完 |
两个死穴,正是路线②要解决的:
history全量累加 —— 每步都要带上之前所有步骤,步骤数翻倍则 token 翻四倍- 计划改不了 —— 第 2 步发现原计划有误,只能硬走或整个重来
针对死穴 1 有两个变体:ReWOO 在规划期就用
#E1、#E2写清步骤间的变量依赖,执行时不带历史;LLM Compiler 把计划编译成 DAG 并行跑。两者都假设「计划不会变」,所以生产上少见。
四、动态 todo:把计划做成一个工具
不在 ReAct 外面套规划阶段,而是给 ReAct 加一个「写待办清单」的工具。改计划本身就是一次工具调用,于是计划随时可改、且以工具结果的形式进入消息历史。
4.1 数据结构:三个状态,故意缺一个
class Todo(TypedDict):
content: str
status: Literal["pending", "in_progress", "completed"]
没有优先级、ID、依赖、子任务,也没有 failed。
缺 failed 是刻意的。 有这个状态,模型遇到搞不定的任务就会标记 failed 然后跳过;而你要的是它去解决卡住的原因。官方规定:
遇到错误、阻塞或无法完成时保持
in_progress;被阻塞时新建一个任务描述需要解决什么。
用状态机里缺一个状态来约束模型行为。
4.2 34% 的文件是提示词
实测 langchain/agents/middleware/todo.py:
| 字符数 | |
|---|---|
| 整个文件 | 15,525 |
| 工具描述 | 3,881 |
| 系统提示词 | 1,378 |
| 提示词占比 | 34% |
Python 逻辑不到 60 行,工具本体只有 10 行(todos 全量覆盖,不是增量更新)。
这是本篇的认知转折点:01、02 的行为由代码结构决定,Plan-and-Execute 的行为几乎全由提示词决定。删掉 write_todos 的描述,工具还在,模型不会用了。
4.3 提示词里真正改变行为的四条
① 反复劝你别用
如果用户的请求很简单、不到 3 步,最好不要用,直接做。
开头和结尾各说一遍。写清单要烧 token 和延迟,小任务上是纯负收益。
② 立刻标记,禁止批量标
批量标记会让上下文里长时间存在一份过期清单,模型看到「都还是 pending」可能重做已完成的事。
③ 永远至少有一个 in_progress
给模型一个「当前焦点」锚点。全是 pending 时模型不知道自己在哪。
④ 标记完成 ≠ 交付答案
write_todos追踪工作,它不交付答案。用户要的东西必须出现在最后一次write_todos之后的消息里。
对应一个真实翻车场景:
第 9 轮 调 write_todos,全部标记 completed
第 10 轮 模型觉得没事干了,不调工具 → 循环退出 → 用户屏幕上什么都没有
因为 01 讲过,退出条件是「这轮没有工具调用」,而它最后一个动作恰好是调工具。