第 19 章 · 规划工作流:让模型先想再去做 07-09
本章观点
前面 3-8 章我们讲的 Agent Loop 是"边想边做":模型每一步看当前上下文,决定下一步调什么工具。这种模式灵活,但有一个问题--模型没有全局视图,容易在局部打转。
吴恩达 Agentic AI 课程(Bilibili BV1DfrdByE2H,p26-p28)提出一种更强的模式:
规划工作流(Planning Workflow)让 LLM 在动手之前,先生成一份多步骤计划,再由系统逐步执行计划,把中间结果作为后续步骤的输入。
它的关键转变不是"模型更聪明",而是把"决定要做什么"和"真的去做"拆成两个阶段。规划阶段只输出计划,不执行;执行阶段按计划走,每一步把上一步结果带进来。
这个模式对应本教程"四权模型"里的控制权:原本每一步的控制权都在 LLM 手里(看上下文决定下一步),现在规划阶段一次性拿到全局控制权,执行阶段把控制权交给代码 + 工具。
但规划也带来工程挑战:计划可能不稳定、可能漏步骤、可能选错工具。所以本章会讲清楚三件事:
- 什么时候值得引入规划,什么时候不值得。
- 计划该用什么格式输出(自然语言 / JSON / XML / 代码)。
- 怎么给规划型 Agent 加边界:计划校验、执行日志、失败重试、人工确认。
19.1 为什么需要规划
回到第 3 章的 Agent Loop:
Thought -> Action -> Observation -> Thought -> ...每一步 LLM 只看当前上下文(用户请求 + 已有 tool 结果),决定下一步。这种模式有两个隐性失败:
失败 1:局部最优陷阱
用户:帮我找圆形太阳镜,价格低于 100 美元,有没有库存?
agent: [search_description("圆形太阳镜")] -> 找到 5 款
agent: [check_inventory(款式A)] -> 有货
agent: [回答:款式A 有货] ← 停了,但没检查价格LLM 拿到"有货"就以为任务完成,忘了还有价格约束。因为它的视角是局部的,看不到"价格 < 100"这个还没满足的子目标。
失败 2:顺序错乱
用户:处理一下退货,订单号 #123
agent: [process_return(#123)] ← 直接调退货工具
↑ 错误:没先查订单是否存在、商品描述、退货条件LLM 一看到"退货"就调用退货工具,但合理的流程是先查历史订单 -> 识别商品 -> 检查退货条件 -> 再调退货工具。ReAct 模式很难保证"先查后做"的全局顺序。
规划模式怎么解决
规划阶段 LLM 不执行任何工具,只输出一个步骤列表:
用户请求:找圆形太阳镜,价格 < 100 美元,有库存。
计划:
1. 查商品描述,找出圆形款式
2. 对每个圆形款式查库存
3. 对有库存的款式查价格
4. 筛选价格 < 100 的,返回给用户执行阶段按计划走,第 1 步输出传给第 2 步,第 2 步输出传给第 3 步,直到完成。这样"价格 < 100"这个约束不会丢,因为计划里写了。
19.2 规划型 Agent 的结构
规划型 Agent 由两个循环组成:
[规划循环]
LLM 读用户请求 + 工具描述
-> 输出结构化计划
-> (可选)计划审查 / 人工确认
[执行循环]
for step in plan:
把 用户请求 + 之前所有步骤输出 + 当前步骤描述 喂给 LLM
-> LLM 决定调用哪个工具 / 直接生成结果
-> 执行工具,保存输出
最后用所有中间结果生成最终答案关键设计点:
- 工具描述必须清楚:LLM 规划时只看工具描述,不调工具。如果工具描述模糊,规划就会错。
- 计划字段要固定:每一步包含"子目标 / 工具名 / 工具参数 / 依赖的上下文 / 预期输出"。
- 中间结果要传:第 2 步执行时,必须能看到第 1 步的输出,否则计划就退化成多个独立调用。
- 计划不等于一定正确:系统仍要校验工具名、参数 schema、风险动作。
19.3 计划的输出格式
这是规划模式最关键的工程决策。吴恩达课程对比了四种格式。
19.3.1 自然语言 / Markdown
1. 先查商品描述找出圆形款式
2. 再查库存
3. 最后查价格优点:人读着舒服,模型好生成。 缺点:代码无法稳定解析。第 2 步要调什么工具?参数是什么?都得靠 LLM 在执行阶段重新猜,又回到 ReAct 的局部视角问题。
适合:计划只给人看,不需要程序执行的场景。不适合 Agent 自动执行。
19.3.2 JSON(推荐默认)
[
{
"step": 1,
"goal": "找出圆形款式的太阳镜",
"tool": "search_description",
"arguments": {"query": "圆形 太阳镜"},
"depends_on": [],
"expected_output": "圆形款式列表"
},
{
"step": 2,
"goal": "检查圆形款式库存",
"tool": "check_inventory",
"arguments": {"product_ids": "$step1.output.product_ids"},
"depends_on": [1],
"expected_output": "库存状态"
}
]优点:
- 主流模型都能稳定生成 JSON。
- 代码可以解析、校验、逐步执行。
- 字段固定,参数 schema 可以预先检查。
depends_on让中间结果传递关系显式。
缺点:对模型能力有要求(弱模型会漏字段或写错工具名)。
19.3.3 XML
<plan>
<step id="1" tool="search_description">
<goal>找出圆形款式</goal>
<arg name="query">圆形 太阳镜</arg>
</step>
<step id="2" tool="check_inventory" depends_on="1">
<goal>检查库存</goal>
<arg name="product_ids">$step1.output.product_ids</arg>
</step>
</plan>优点:标签明确,步骤编号清晰。 缺点:JSON 更主流,XML 在现代 LLM 工程里用得越来越少。
19.3.4 代码即计划(Code as Plan)
这是 p28 的核心洞察:对于能用程序表达的复杂任务,让 LLM 直接生成并执行 Python 代码,比让它输出 JSON 计划再逐步执行更强。
import pandas as pd
df = pd.read_csv("sales.csv")
hot_drinks = df[df.category == "hot_drink"]
top_seller = hot_drinks.groupby("product")["quantity"].sum().idxmax()
print(top_seller)为什么代码比 JSON 计划强?
- 代码天然支持循环、条件、过滤、聚合:JSON 计划表达"对每个款式查库存"需要展开成多个步骤,代码一个
for循环搞定。 - 成熟库可以直接用:让模型写 pandas 代码,就能用
groupby / merge / sort_values等几百个函数;如果用 JSON 计划,你得为每个操作写一个工具,工具数量爆炸。 - 执行即验证:代码跑通了就是对的,跑不通报错反馈给模型修复。JSON 计划即使解析成功,执行时仍可能失败。
但代码计划有风险:
- 模型可能写危险代码(删文件、网络请求、shell 命令)。
- 必须在沙盒里跑(第 9 章 Runtime & Sandbox 讲过)。
- 调试更难,因为代码逻辑可能很复杂。
工程做法:结构化任务(步骤明确的客服 / 邮件 / 流程类)用 JSON 计划;数据计算 / 表格分析 / 批处理任务用代码计划。两者不是互斥,可以组合:JSON 计划里某一步是"执行代码"。
19.4 一个完整的规划型 Agent 例子
把规划 + JSON 计划 + 执行循环合起来,写一个最小可跑的规划型 Agent。
import anthropic
import json
client = anthropic.Anthropic()
# 工具集合
TOOLS_IMPL = {
"search_description": lambda query: f"找到圆形款式: [A, B, C] (query={query})",
"check_inventory": lambda product_ids: f"库存: A=10, B=0, C=5",
"check_price": lambda product_ids: f"价格: A=80, C=120",
}
TOOLS_SCHEMA = [
{"name": "search_description",
"description": "查商品描述找出符合的款式",
"input_schema": {"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]}},
{"name": "check_inventory",
"description": "查多个款式的库存",
"input_schema": {"type": "object",
"properties": {"product_ids": {"type": "array", "items": {"type": "string"}}},
"required": ["product_ids"]}},
{"name": "check_price",
"description": "查多个款式的价格",
"input_schema": {"type": "object",
"properties": {"product_ids": {"type": "array", "items": {"type": "string"}}},
"required": ["product_ids"]}},
]
PLANNER_PROMPT = """你是一个规划型助手。请根据用户请求生成 JSON 计划,不要执行工具。
可用工具:
{tools}
用户请求:{request}
只输出 JSON 数组,每个步骤包含字段:
- step: 步骤编号
- goal: 本步骤目标
- tool: 工具名(若不需要工具填 none)
- arguments: 工具参数(可引用 $stepN.output.xxx)
- depends_on: 依赖的前置步骤编号列表
- expected_output: 本步骤应产生什么结果
"""
def plan(request: str) -> list:
tools_desc = "\n".join(
f"- {t['name']}: {t['description']}" for t in TOOLS_SCHEMA
)
resp = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
messages=[{"role": "user",
"content": PLANNER_PROMPT.format(tools=tools_desc, request=request)}]
)
return json.loads(resp.content[0].text)
def execute_step(step, prior_outputs, request):
"""执行单步:把用户请求 + 之前输出 + 当前步骤描述喂给 LLM。"""
# 简化:直接按 step.tool 调工具(实际系统应该让 LLM 决定参数)
args = {}
for k, v in step.get("arguments", {}).items():
if isinstance(v, str) and v.startswith("$step"):
# 简单引用解析:$step1.output.product_ids
parts = v[1:].split(".")
step_id = int(parts[0].replace("step", ""))
args[k] = prior_outputs[step_id]["product_ids"]
else:
args[k] = v
if step["tool"] == "none":
return {"text": step["goal"]}
return TOOLS_IMPL[step["tool"]](**args)
def planning_agent(request: str) -> str:
# 阶段 1:规划
plan_steps = plan(request)
print("=== 计划 ===")
for s in plan_steps:
print(f" {s['step']}. {s['goal']} (tool={s['tool']})")
# 阶段 2:执行
outputs = {}
for step in plan_steps:
print(f"\n--- 执行步骤 {step['step']} ---")
result = execute_step(step, outputs, request)
outputs[step["step"]] = result
print(f"输出: {result}")
# 阶段 3:最终答案
final = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=512,
messages=[{"role": "user", "content":
f"用户请求: {request}\n\n中间结果: {json.dumps(outputs, ensure_ascii=False)}\n\n请给出最终答案。"}]
)
return final.content[0].text
if __name__ == "__main__":
print(planning_agent("找圆形太阳镜,价格低于 100 美元,要有库存"))运行结果:
=== 计划 ===
1. 找出圆形款式 (tool=search_description)
2. 检查库存 (tool=check_inventory)
3. 检查价格 (tool=check_price)
--- 执行步骤 1 ---
输出: 找到圆形款式: [A, B, C] (query=圆形 太阳镜)
--- 执行步骤 2 ---
输出: 库存: A=10, B=0, C=5
--- 执行步骤 3 ---
输出: 价格: A=80, C=120
最终答案: 圆形太阳镜中,款式 A 满足条件:价格 80 美元(<100),库存 10 件。注意:款式 C 价格 120 > 100,被排除了;款式 B 没库存,也被排除了。规划让"价格 < 100 且有库存"这个复合约束没被丢掉。
19.5 计划执行器的检查清单
模型输出的计划不等于一定正确。生产系统应该在校验阶段和执行阶段都加边界。
解析前检查:
- [ ] 输出是合法 JSON / XML 吗?
- [ ] 每个步骤都包含必需字段吗(step / goal / tool / arguments)?
- [ ] 工具名是否在白名单中?
- [ ] 参数类型是否符合 schema?
- [ ] depends_on 引用的步骤编号是否存在?
执行中检查:
- [ ] 每一步的前置依赖是否已完成?
- [ ] 工具调用是否成功?
- [ ] 失败是否需要重试或修改计划?
- [ ] 是否涉及高风险动作(删除 / 发送 / 付款),需要用户确认?
执行后检查:
- [ ] 是否完成用户目标?
- [ ] 是否遗漏必要步骤?
- [ ] 是否生成最终回答?
- [ ] 是否记录 trace 方便错误分析?这条清单和第 8 章的安全清单是同一个思路:LLM 生成的计划不等于可执行的计划,工程边界必须独立校验。
19.6 适用场景与反模式
适合规划模式
| 场景 | 为什么适合 |
|---|---|
| 客服智能体 | 用户请求多变,工具组合灵活,硬编码流程会臃肿 |
| 邮件助手 | 不同邮件要走的流程不同(回复 / 转发 / 归档 / 标记) |
| 研究助手 | 多步搜索 + 摘要 + 整合 |
| 代码生成助手 | 先规划模块,再生成每个模块 |
| 多工具 RAG | 先决定查哪些源、用什么查询、再综合 |
| 数据分析 | 用代码计划,pandas 处理表格 |
不适合规划模式
反模式 1:流程很固定
如果任务是"读邮件 -> 分类 -> 转发给对应部门",这种固定流程用 Workflow(第 1 章讲过)就够了,不需要让 LLM 规划。规划会增加延迟和不确定性。
反模式 2:合规风险高
金融交易、医疗诊断、法律建议这类任务,路径必须可审计。让 LLM 自由规划会产生难以追溯的执行路径。这类场景应该用显式状态机 + 人工确认点。
反模式 3:任务太简单
"翻译这句话"这种任务一步就能完成,规划反而增加 token 成本。规划适合多步骤、步骤顺序会变化、工具组合需要选择的任务。
19.7 代码即计划的实践
p28 提到,对于数据分析类任务,让 LLM 生成 Python 代码比生成 JSON 计划更高效。这里给一个最小代码执行器的骨架。
import re
import subprocess
import sys
CODE_PLANNER_PROMPT = """你需要用 Python 代码解决用户的数据分析问题。
可用数据:
- 文件路径:{data_path}
- 字段说明:{schema}
用户问题:{question}
要求:
1. 只返回一个 Python 代码块,不要解释。
2. 使用 pandas 读取和处理数据。
3. 代码必须把最终答案保存到变量 result,并 print(result)。
4. 不要访问网络,不要读取指定数据以外的文件,不要执行系统命令。
"""
def extract_code(text: str) -> str:
"""从 LLM 输出中提取 ```python ... ``` 代码块。"""
m = re.search(r"```python\n(.*?)```", text, re.DOTALL)
return m.group(1) if m else text
def run_code_safely(code: str, timeout: int = 30) -> dict:
"""在受限 subprocess 里执行代码,返回结构化结果。"""
try:
# 生产环境应该用 Docker / gVisor / nsjail 等更强沙盒
result = subprocess.run(
[sys.executable, "-c", code],
capture_output=True, text=True, timeout=timeout
)
return {
"status": "success" if result.returncode == 0 else "error",
"stdout": result.stdout[:2000],
"stderr": result.stderr[:2000],
}
except subprocess.TimeoutExpired:
return {"status": "error", "stderr": f"代码执行超时({timeout}s)"}
def code_planning_agent(question: str, data_path: str, schema: str) -> str:
resp = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=2048,
messages=[{"role": "user", "content":
CODE_PLANNER_PROMPT.format(data_path=data_path, schema=schema, question=question)}]
)
code = extract_code(resp.content[0].text)
# 静态检查(生产环境应该更严格)
forbidden = ["import os", "import subprocess", "import socket", "open('/", "open("~"]
for kw in forbidden:
if kw in code:
return f"代码包含禁止关键词:{kw}"
result = run_code_safely(code)
if result["status"] == "success":
return result["stdout"]
else:
# 错误反馈给模型修复
return f"代码执行失败:{result['stderr']}"工程要点:
- 静态检查:扫描禁止的 import / 文件操作 / 网络调用。
- 沙盒执行:用 subprocess 限制超时,生产环境用容器隔离。
- 错误回传:把 stderr 喂回模型,让它修复代码。
- 审计日志:保留每次执行的代码、stdout、stderr、运行时间。
第 9 章 Runtime & Sandbox 详细讲过沙盒设计,这里只是最小示例。
19.8 规划与 ReAct 的关系
初学者常问:规划模式是不是替代了 ReAct?
不是替代,是补充。两者的关系:
| 模式 | 控制权 | 视角 | 适合 |
|---|---|---|---|
| ReAct | 每步在 LLM | 局部 | 探索性任务、工具少、步骤顺序会随观察变化 |
| Planning | 规划阶段全局、执行阶段局部 | 全局 | 多步骤、约束多、步骤顺序需要预先安排 |
| Planning + Code | 规划阶段全局、执行阶段程序 | 全局 + 可计算 | 数据分析、批处理、表格操作 |
实际系统经常组合:
Planning(生成 JSON 计划)
-> 执行每一步时用 ReAct(看中间结果决定具体工具调用)
-> 数据计算步骤用 Code(让模型生成 pandas 代码)判断什么时候上规划:
- 工具数量 ≤ 3,任务步骤 ≤ 3 -> ReAct 够了
- 工具数量 4-10,任务步骤会变化 -> ReAct + 偶尔规划
- 工具数量 > 10,或任务有多个必须同时满足的约束 -> 规划
- 任务是数据分析 / 批处理 -> 代码即计划
前人智慧 / Prior Art
本章把"规划"讲成一种工程模式:让 LLM 先输出计划,再按计划执行。这个思想来自几篇关键论文和工程博客。
ReAct 与 ToT 的规划基因 07-09
第 2 章讲过 ReAct 和 Tree of Thoughts。ReAct 本身就是"先思考再行动"的雏形--Thought 字段就是局部计划。但 ReAct 的 Thought 是一步一思考,没有全局视图。
ToT 论文 的贡献是把规划显式化:模型先展开一棵推理树,再选择最优路径执行。ToT 在 24 点游戏上把 CoT 的 4% 准确率提到 74%,关键就在于"先规划再执行"。
本章的 Planning Workflow 可以看成 ToT 的工程化:ToT 是探索多分支,Planning 是输出线性计划;ToT 用 LLM 评估分支,Planning 用代码执行步骤。两者共享同一个核心洞察:全局视图优于局部反应。
Plan-and-Solve Prompting
Plan-and-Solve 论文(Wang et al., 2023)把规划模式显式化为两阶段 prompt:
Stage 1: "Let's first understand the problem and devise a plan."
Stage 2: "Let's carry out the plan step by step."在 GSM8K 数学题上,Plan-and-Solve 把 CoT 的准确率从 73% 提到 78%。看似提升不大,但论文的关键贡献是证明了"规划 + 执行"比"一步出答案"更稳。
本章 19.1 节的"局部最优陷阱"和"顺序错乱"就是 Plan-and-Solve 想解决的问题。Plan-and-Solve 用 prompt 实现,本章的 Planning Workflow 用结构化 JSON 计划实现--更工程化、更可校验。
LLMCompiler:并行规划执行 07-09
LLMCompiler(Kim et al., 2023)是规划模式的高性能版本:让 LLM 输出 DAG(有向无环图)计划,无依赖的步骤并行执行。
关键设计:
- Planner:LLM 生成步骤 + 依赖关系(DAG)
- Task Fetching Unit:调度器,无依赖的步骤并发触发
- Joiner:所有步骤完成后,决定是否需要再规划一轮
在 HotpotQA 上,LLMCompiler 比 ReAct 快 4 倍,准确率持平或更高。这个结果支撑了本章 19.3 节的判断:depends_on 字段不只是文档作用,它让并行执行成为可能。
工程启示:如果你的计划里很多步骤互相独立(比如多路搜索、多个文件解析),用 DAG + 并行执行能大幅降低延迟。第 8 章 8.6 节讲的"死循环导致成本失控"在并行规划里也会减轻,因为无依赖步骤不会互相阻塞。
Anthropic 多智能体研究系统的规划观
Anthropic Multi-agent Research System 博客提到一个关键经验:
"Orchestrator-worker 模式中,orchestrator 先规划任务拆分,再分派给 worker。规划阶段的质量决定整体系统的上限。"
这条经验支撑了本章 19.2 节的设计:规划循环和执行循环必须分离。如果 orchestrator 边想边做,容易在 worker 失败时不知道怎么调整全局计划。先规划再执行,失败时可以重新规划。
第 20 章多智能体协作模式会详细讲 orchestrator-worker 怎么和 Planning 结合。
代码即计划的工业实践
让 LLM 生成代码而不是 JSON 计划,这个思路在工业界有几个代表:
- OpenAI Code Interpreter:让模型生成 Python 代码,在沙盒里执行,拿真实结果。
- Anthropic Computer Use:让模型生成鼠标键盘操作代码,控制真实电脑。
- Cursor / Aider:让模型生成代码补丁,应用到文件系统。
- Data Analysis Agent(OpenAI / Google / Anthropic 都有):让模型生成 pandas / SQL 代码做数据分析。
这条路线的共同特征:代码不只是计划描述,代码本身就是执行单元。这和本章 19.3.4 节的"代码即计划"完全一致。
工业实践的工程教训:
- 沙盒必须强:第 9 章讲过的 Docker / gVisor / nsjail,代码即计划时尤其重要。
- 错误反馈循环:代码报错要把 stderr 回传模型,让它修复,而不是直接失败。
- 审计每次执行:代码 / 输入 / stdout / stderr / 运行时间都要留 trace。
Andrew Ng Agentic AI 课程的规划模块 07-09
本章内容主要整理自吴恩达 Agentic AI 课程 Module 5(Bilibili BV1DfrdByE2H,p26-p28)。课程的三节递进:
- p26 规划工作流:抽象概念,为什么需要规划
- p27 创建和执行 LLM 计划:JSON / XML 结构化计划
- p28 结合代码执行的规划:代码即计划
课程的关键判断:"规划提升灵活性,但也让运行时行为更难预测"。这正是本章 19.6 节"反模式"的来源--规划不是越多越好,而是要为具体任务选择足够简单、可控、有效的结构。
课程还提到一个工程经验:计划校验比计划生成本身更重要。模型生成的计划经常有缺字段、错工具名、参数类型不对等问题,工程系统必须独立校验,不能盲信 LLM 输出。本章 19.5 节的检查清单就是这个经验的工程化。
ReWOO:规划 + 工具调用解耦
ReWOO 论文(Xu et al., 2023)把规划模式推到极端:
- Solver:LLM 一次性生成完整计划,包含所有工具调用的占位符
- Worker:按占位符执行工具,不调 LLM
- Solver:所有工具结果回来后,LLM 再生成最终答案
ReWOO 的工程贡献是减少了 LLM 调用次数:原本 ReAct 每步都要调 LLM,ReWOO 只在规划阶段和最终汇总阶段调 LLM,中间全是纯工具执行。在 HotpotQA 上 token 消耗减少 50%+,准确率持平。
对本章的启示:如果工具调用结果不需要 LLM 中间推理,可以把多步工具调用打包成一个计划,让 LLM 只在两端参与。这是 Planning 模式相对 ReAct 的成本优势来源。
19.9 小结
- 规划工作流把"决定做什么"和"真的去做"拆成两阶段,让 LLM 有全局视图,避免 ReAct 的局部最优陷阱。
- 计划格式优先选 JSON:模型好生成、代码好解析、字段可校验。Markdown 适合人看不适合执行。
- 数据分析类任务用代码即计划:让 LLM 生成 Python + pandas 代码,比 JSON 计划表达力更强。
- 计划不等于正确:解析前校验 schema、执行中检查依赖、执行后验证目标,每一步都要工程边界。
- 规划不是替代 ReAct,是补充:简单任务用 ReAct,多约束任务用 Planning,数据分析用 Code as Plan,三者可以组合。
下一章我们继续讲多智能体协作模式--当任务复杂到一个 Agent 装不下时,怎么拆成多个角色协作,以及线性 / 经理协调 / 层级 / 全连接四种通信模式的工程权衡。
happy ending, love you.