Skip to content
Agent Planning 原理:任务分解、步骤生成与执行控制
概述
Agent Planning(智能体规划)是 LLM Agent 系统的核心模块之一。它负责将用户的高层目标转化为具体的执行步骤,并控制这些步骤的执行过程。规划模块的设计直接决定了 Agent 能否可靠地完成复杂任务。
下面从概念开始,依次介绍任务分解、步骤生成、执行控制三个核心环节,然后讨论四种典型规划模式,再给出一种可指导实现的分层架构,最后介绍工程实现与评估方法。文中示例以 TypeScript/Node.js 为主。
1. Agent Planning 的概念与边界
一个 LLM Agent 可以看作一个持续循环的系统:
目标 → 推理 → 行动 → 观察 → 推理 → 行动 → ...其中,规划指的是从目标推导出行动序列的过程。这里的行动不一定是函数调用,也可以是信息查询、计算、文件读写等任何 Agent 可以执行的操作。
规划与普通函数调用链有一个本质区别:在普通程序中,调用链是开发者预先写死的;在 Agent 中,调用链由模型在运行时动态生成,并且可能根据中间结果而改变。
js
// 普通程序:调用链固定
function handleOrder(orderId) {
const order = getOrder(orderId);
const user = getUser(order.userId);
return sendReceipt(user.email, order);
}
// Agent:调用链由模型决策
// 每次迭代,模型根据当前状态决定调用哪个工具
async function runAgent(query) {
let state = { query, messages: [] };
for (let i = 0; i < MAX_STEPS; i++) {
const decision = await model.decide(state); // 返回 action + args
if (decision.type === 'finish') return decision.answer;
const result = await execute(decision.action, decision.args);
state.messages.push(/* 将结果追加到上下文 */);
}
}上面的普通程序在编译期就确定了 handleOrder 的内部调用顺序;Agent 的 runAgent 则把函数名和参数交给模型决策,所以相同输入可能产生不同的调用序列。
规划并不是孤立存在的。一个 Agent 系统通常包含记忆(memory)、工具(tools)、执行器(executor)等组件,规划模块与它们协作。这里的边界是:
- 规划负责“决策做什么”,不负责“具体怎么做”——那是工具实现
- 规划需要读取状态和观察结果,但状态的持久化由记忆模块负责
- 规划需要选择工具,但工具的注册、鉴权、执行由独立的工具层负责
2. 核心概念:Task、Step、Observation 与 State
本节说明规划循环中四个基础数据结构。不同框架的命名可能不同,但它们承担的角色是类似的。
2.1 Task
Task(任务)是用户交给 Agent 的目标描述。它可以是一个自然语言请求,也可以是结构化的目标对象。下面的例子分别对应两种形式:
“帮我整理这周的会议记录并生成周报”
“查询北京到上海明天的高铁班次,选择最早的一班并预订”在代码中,Task 可以定义为字符串,也可以定义为更丰富的描述对象:
ts
interface Task {
id: string;
goal: string;
constraints?: string[];
}约束字段适用于需要限制时间、预算、范围等条件的场景。实际系统可能在此基础上增加元数据字段。
2.2 Step
Step(步骤)是规划的最小执行单元,表示一次具体的行动。一个 Step 可以包含以下信息:
- 动作类型:例如
web_search、read_file、python_repl - 参数:执行该动作所需的输入
- 期望输出(可选):执行后应该得到什么结果
- 状态:pending / running / succeeded / failed / skipped
ts
interface Step {
id: string;
action: string; // 工具名称
args: Record<string, unknown>; // 参数
expected: string; // 期望结果描述,用于后续校验
status: 'pending' | 'running' | 'succeeded' | 'failed' | 'skipped';
}expected 字段在计划执行时用来判断观察结果是否符合预期;status 字段由执行器在生命周期中更新。
2.3 Observation
Observation(观察)是执行一个 Step 后从环境返回的结果。它可以是工具返回的文本或结构化数据、命令行的 stdout/stderr、API 调用的响应体,或一个异常信息。
ts
interface Observation {
stepId: string;
ok: boolean;
content: string;
raw?: unknown;
}content 是序列化后送入模型的文本;raw 保留原始数据,供后续处理或审计。Observation 是 Agent 做出下一步决策的依据。如果观察结果与预期不符,Agent 需要决定:重试、调整参数、更换工具,还是放弃任务。
2.4 State
State(状态)是 Agent 在任一时刻的完整上下文。它通常包括:
- 初始任务描述
- 对话历史(用户消息、模型回复)
- 已执行步骤及其观察结果
- 当前步骤编号
- 中间变量
ts
interface AgentState {
task: string;
messages: Array<{ role: string; content: string }>;
steps: Step[];
observations: Observation[];
currentStepId: string | null;
vars: Record<string, unknown>;
}State 是规划循环中最重要的数据。它既被模型用来决定下一步动作,也被外部系统用作审计日志。
与普通程序不同的是,Agent 的 State 大部分情况下需要以文本形式序列化后送入模型,因为模型只能看到文本。这就是很多 Agent 框架会做状态压缩或摘要的原因——保持状态可被模型完整读取,是规划有效性的前提之一。
3. 任务分解:策略、粒度与提示方法
3.1 分解策略
任务分解(Task Decomposition)是让模型把一个复杂目标拆成若干可执行的子目标。下面归纳三种常用的分解策略。
自顶向下分解。从目标一层层拆分,直到每步都足够明确。适合“完成一份竞品调研报告”这类归纳型场景。
目标-行动映射。模型根据工具列表逆向推导:先看有哪些工具,再把目标映射到工具序列上。适合工具种类固定的场景。
递归分解。先拆出第一阶段,执行完再拆下一阶段,中间根据最新信息修正。这是 Plan-and-Execute 模式采用的方式,适合环境信息不断变化的场景。
3.2 粒度
子任务的粒度需要权衡:
- 粒度过粗:一步包含太多内容,模型无法生成准确的工具参数,或执行结果不可控
- 粒度过细:步骤数量多,每次模型调用都增加延迟和 token 消耗,步骤之间的关系也更难管理
- 合适的粒度:每个子任务对应一个工具调用,并且输入输出关系清晰
一个常用的判断标准是:如果一个步骤的执行结果可以由单一工具调用返回,粒度可能合适;如果一步中涉及多个工具的结果拼接、循环或条件判断,就应该继续拆分。
3.3 提示方法
任务分解的提示设计,通常需要限定输出格式。以下是一个示意性提示:
请把下面的目标拆分为子任务列表。
输出 JSON 数组,每个元素包含 title、description 和 action 三个字段。
action 只能是下面工具之一:web_search, scrape_webpage, save_file, summarize_text
不要输出任何其他文字。
目标:调研三款竞品的主打功能和价格模型需要结合工具 schema 来生成符合要求的子任务。在结构化输出可用的情况下,这一步通常不再依赖自由文本提示,而是利用结构化输出或函数调用约束模型,使返回的 JSON 可以直接进入执行器,跳过文本解析环节。
4. 步骤生成:计划表示、工具选择与参数填充
4.1 计划表示
Plan(计划)是一组有序的步骤。下面归纳三种常见的表示方式。
JSON。结构化,便于程序解析、存储和校验。
json
{
"goal": "调研三款竞品",
"steps": [
{ "id": 1, "action": "web_search", "args": { "query": "产品A 功能 价格" }, "expected": "产品A官网信息" },
{ "id": 2, "action": "web_search", "args": { "query": "产品B 功能 价格" }, "expected": "产品B官网信息" },
{ "id": 3, "action": "summarize_text", "args": { "source": "steps.1.result + steps.2.result" }, "expected": "对比摘要" }
]
}自然语言。可读性好,能表达 JSON 难以描述的复杂关系,但解析困难,需要模型后续重新理解并自行推导具体的工具调用。适合 ReAct 风格,因为 ReAct 不需要预先拆好步骤。
代码。将计划写成一段可执行的程序。这种方式极少直接由 LLM 生成,因为代码执行的安全风险很大。
4.2 工具选择
工具选择是模型在推理时从 Tool Registry 中选择一个或多个工具的过程。模型基于两个信息做出选择:
- 工具的注册元数据(名称、描述、参数 schema)
- 当前步骤的意图
如果模型本身支持 function calling,那么工具选择被表示为一组 tool definition 传给模型。如果不支持,则需要把工具描述用文本拼进系统提示词。
工具列表中工具的描述质量非常关键。模型无法“理解”工具的真实逻辑,只能看到工具描述和参数 schema。描述里应写清楚:这个工具解决什么问题、参数含义是什么、返回值是什么。
ts
const tools = [
{
type: 'function',
function: {
name: 'search_news',
description: '按关键词搜索最近七天的新闻,返回标题、来源和发布时间',
parameters: {
type: 'object',
properties: {
keyword: { type: 'string', description: '搜索关键词' },
limit: { type: 'number', description: '返回条数,默认5' }
},
required: ['keyword']
}
}
}
];这个声明同时描述了工具的行为和参数格式,模型在生成 tool call 时会参考它。工具执行时,Executor 只负责按名称查找并调用对应的 handler。
4.3 参数填充
参数填充是指模型根据步骤目标生成工具所需的参数。这一环节的错误大多来自参数描述不充分、缺少必填项约束或值域不明确。
一个常见问题是日期参数。让模型填“明天”可能填成某个具体日期,但该日期是否正确取决于当前时间。如果工具需要日期,应在步骤的期望输出中明确指定格式,或通过其他机制向模型提供当前日期。
结构化输出可以解决这类问题。xAI 等 API 支持的 Structured Outputs 允许传入 JSON Schema,要求模型返回严格匹配 schema 的内容。当模型通过工具调用生成参数时,生成的参数也会被约束为符合工具输入的格式,而不是自由文本[1]。
5. 执行控制:循环、状态追踪、容错与人工介入
5.1 执行循环
Agent 的执行过程通常是一个循环:模型产出决策 → 系统执行动作 → 将观察结果追加到上下文 → 进入下一轮。这个循环通常由 Orchestrator 控制。
ts
async function runAgentLoop(
task: string,
tools: Tool[],
callModel: (messages: Message[]) => Promise<ModelDecision>
) {
const messages: Message[] = [{ role: 'user', content: task }];
let finished = false;
while (!finished) {
const decision = await callModel(messages);
if (decision.type === 'finish') {
finished = true;
break;
}
const tool = tools.find((t) => t.function.name === decision.toolName);
if (!tool) {
// 模型选了不存在的工具,让模型知道并重试
messages.push({
role: 'tool',
tool_call_id: decision.callId,
content: 'Error: unknown tool'
});
continue;
}
const result = await tool.execute(decision.args);
messages.push({
role: 'tool',
tool_call_id: decision.callId,
content: JSON.stringify(result)
});
}
return messages.at(-1)?.content;
}这个循环会一直执行到模型返回 finish 类型的决策。为了简化,示例省略了最大循环次数限制;实际实现中必须加入该限制,否则长时间运行的任务可能产生不可控的 API 调用量。
5.2 状态追踪
在循环中需要追踪以下几项状态:
- 当前步骤:记录执行到计划的哪一步
- 已完成步骤:记录每个工具调用的结果,供后续查询
- 已用 token 和成本:防止预算超支
- 循环次数:防止死循环
最大循环次数(maxIterations)是 Agent 系统实现中应加入的限制。模型不会因为逻辑原因自然停止循环,它可能在某一观察结果上反复调用同一个工具,或不停提出新的子目标。如果不限制循环次数,一个看似简单的任务可能产生上百次 API 调用。
5.3 容错与重试
错误处理是执行控制中最容易出问题的部分。常见错误类型和处理方式:
工具不存在。模型选择了工具注册表里没有的工具,通常是描述不一致导致的。处理方式:向模型返回错误信息,让模型重新选择。
参数校验失败。模型生成的参数缺失或类型错误。处理方式:返回校验错误并附上参数 schema,模型通常会修正。
工具执行异常。工具本身抛出异常。处理方式:捕获异常,将错误文本作为 observation 返回。是否重试取决于任务性质——网络超时可以重试,数据库写入失败重试可能造成重复写入,需要结合幂等设计综合考虑。
重新规划(re-planning)。当连续多次失败,或某个中间结果证明原计划不可行时,Agent 需要放弃原计划的一部分或全部,生成新计划。Plan-and-Execute 模式把重新规划作为显式机制。
5.4 人工介入
对于高风险操作(如发送邮件、扣款、删除文件),Agent 系统应设计人工确认机制。
ts
const decision = await callModel(messages);
if (decision.toolName === 'send_email') {
const ok = await askHuman(`是否允许向 ${decision.args.to} 发送邮件?`, decision);
if (!ok) {
messages.push({
role: 'tool',
tool_call_id: decision.callId,
content: 'Rejected by human'
});
continue;
}
}人工介入不是简单的“弹窗确认”。一个好的实现会把模型的意图、参数、潜在影响展示给操作者,并允许操作者修改参数、拒绝,或直接终止整个任务。
6. 典型规划模式:ReAct、Plan-and-Execute、Reflexion、ToT
6.1 ReAct
ReAct 的核心思想是让模型交替进行 Reason(推理)和 Act(行动)。模型在每一步先输出思考,再决定调用哪个工具。观察结果直接追加到对话历史,模型据此继续推理。
ReAct 没有显式的计划。计划隐含在模型逐步的推理中,灵活性高,适合任务路径不明确的场景。缺点是每一步都可能改变方向,决策链条较长时容易反复横跳,且工具调用依赖历史上下文,容易受噪声干扰。
Thought: 用户问的是“上海明天天气”。我需要先查询明天的日期。
Action: get_today
Observation: 2025-06-20
Thought: 今天是 6月20日,明天是 6月21日。查询上海 6月21日的天气。
Action: weather(query="上海", date="2025-06-21")
Observation: 晴,28°C
Thought: 我已经获得了答案。给出回复。
Answer: 上海明天(6月21日)晴,28°C。上面的输出展示了 ReAct 循环中模型每一步产出的文本结构。实际实现中,Thought 和 Action 通常由 Function Calling 的参数结构化输出,而不是解析文本。
6.2 Plan-and-Execute
Plan-and-Execute 将生成计划与执行计划分离。模型先输出一个完整的分步计划,然后执行器逐条执行。每步执行后,如果观察结果与计划中的预期不一致,规划器可以重新规划剩余部分。
与 ReAct 相比,Plan-and-Execute 的优点在于:计划是显式结构,便于用户看到 Agent 的意图;执行过程中上下文更短(不需要把之前所有决策都塞进上下文);失败时通过重新规划来修正,而不是从头推理。
ts
interface PlanStep {
action: string;
args: Record<string, unknown>;
expectation: string;
}
async function executePlan(
initialPlan: PlanStep[],
messages: Message[]
): Promise<string> {
let plan = initialPlan;
let i = 0;
let failures = 0;
const MAX_STEP_FAILURES = 2;
while (i < plan.length) {
const step = plan[i];
const result = await executeTool(step);
if (result.ok) {
messages.push({ role: 'tool', content: summarize(result) });
i++;
failures = 0;
continue;
}
// 执行失败,请求规划器提供修正方案
const revision = await planner.revise({
plan,
fromIndex: i,
error: result.error,
history: messages
});
if (revision.retryCurrent) {
// 保留当前步骤但调整参数重试
failures++;
if (failures >= MAX_STEP_FAILURES) {
throw new Error(`step ${i} failed ${failures} times: ${step.action}`);
}
continue; // i 不变,重试当前步骤
}
if (revision.newPlanSteps) {
// 用新步骤替换从 i 向后的部分,已完成步骤保留
plan = [...plan.slice(0, i), ...revision.newPlanSteps];
failures = 0;
continue; // 仍从 i 开始执行新计划中的对应位置
}
throw new Error(`plan failed at step ${i}: ${result.error}`);
}
return buildFinalAnswer(messages);
}这里需要注意重新规划的边界:newPlanSteps 只能替换 fromIndex 向后的部分。已完成步骤的结果已经写入 messages,不应被改动。如果新计划需要重新执行某些已完成步骤,规划器应在 newPlanSteps 中显式包含这些步骤,由执行器重新执行。
6.3 Reflexion
Reflexion 不是一种独立的规划算法,而是在 ReAct 或 Plan-and-Execute 之上增加了一个“反思”环节。当任务执行失败时,Agent 不是直接重复或放弃,而是先让模型基于观察结果生成一段反思文字,总结失败原因和下次应该怎么做。反思结果被保存在记忆里,在下一次尝试中作为参考。
ts
const reflection = await model.call({
prompt: `任务失败了。这是执行历史:...
请反思失败原因,并给出一个下次执行的改进建议。`
});
memory.remember('reflection', reflection);Reflexion 的规划价值在于:它让 Agent 利用失败经验修正决策,而不是每次从零开始。但由于反思本身也是模型生成的,可能包含不准确的信息。需要设计机制来校验或约束反思的粒度,使改进建议能被后续步骤真正引用。
6.4 Tree of Thoughts
Tree of Thoughts(ToT)把规划视为一棵多分支搜索树。模型在每一步生成多个候选思路(thought),然后评估每个思路的可行性,选择有希望的分支继续。规划不再是一个线性序列,而是一个搜索过程。
ToT 的典型流程:
- 模型为当前状态生成 K 个候选思路
- 模型(或外部评估器)对每个思路打分
- 选择分数最高的分支继续扩展
- 当某个分支无解时,回溯到上一个分支点
ToT 能改善需要探索和回溯的任务,但成本很高:每一步都要生成多个候选并评估,API 调用量显著增加。在 Agent 系统中,ToT 主要用于模型的推理阶段,而不是整个工具调用循环。
7. 规划与记忆、观察、反思的协作
规划依赖上下文。这些上下文按时间或粒度可分为多个来源:
- 对话记忆:用户在本轮对话中说过什么
- 工作记忆:本次任务执行过程中产生的步骤结果(observation)
- 长期记忆:之前任务中沉淀的知识,如反思、用户偏好、失败教训
- 外部数据:数据库、知识库、文件
规划器在生成下一步行动时,需要把当前相关的记忆片段提取出来。例如,如果之前任务中发现“查天气时一定要带上城市代码,用城市名会失败”,那么下一步规划中应当先查询城市代码。
观察结果直接影响计划的合理性。当观察结果与步骤的 expected 不一致时,规划器需要判断是偏差还是完全错误。偏差可以忽略,错误则需要重新规划。
反思与规划的关系在前面 Reflexion 一节已经介绍。这里补充一点:反思不一定只在失败后才进行。在一个较长任务中,每完成一个重要阶段,可以让模型简短总结问题,并用这个总结指导后一阶段的规划。这种方式可以缓解长任务中上下文丢失的影响。
8. Planner/Executor 架构、工具注册与安全边界
8.1 分层架构
将 Agent 规划系统划分为以下几层,便于独立开发和测试:
- Orchestrator(编排器):主导循环,负责调用模型、把步骤交给执行器、收集观察结果、判定任务是否结束
- Planner(规划器):生成计划、修订计划,输入是任务和状态,输出是步骤序列
- Executor(执行器):接收一个具体步骤,执行工具调用,返回观察结果
- Tool Registry(工具注册表):保存全部可用工具的元信息和执行函数,对 Executor 暴露按名称查找的接口
+----------------+ plan/steps +-----------------+
| Orchestrator | ------------------> | Planner |
| | <------------------ | |
| | +-----------------+
| | step + args +-----------------+
| | ------------------> | Executor |
| | observation <---- | |
+----------------+ +-----------------+
| |
+--------+ Tool Registry <----------+8.2 工具注册
工具注册表是一个简单的查找表,每条记录包含工具的元数据、执行函数以及可选的鉴权信息。
ts
interface ToolRecord {
name: string;
description: string;
schema: JSONSchema;
handler: (args: any) => Promise<any>;
permissions: { roles: string[]; allowAPIKey?: boolean };
}一个工具一旦注册,它就同时拥有“对模型可见的描述”和“真实执行函数”两个身份。这里有两个风险:描述可能过度承诺(模型以为工具能做某事),执行函数也可能没有做和描述一致的校验。这两个风险都应当由工具注册表约束。
工具注册表还应该维护一个“不可用工具”列表(例如尚未准备好的工具、被下线的工具),并在模型选择这些工具时返回明确信息,而不是让模型误认为工具可用而反复尝试。
8.3 安全边界
安全边界要明确“模型可以做哪些动作”:
- 不联网的危险操作:文件删除、命令执行、权限变更,默认禁止
- 高影响操作:发送消息、下单、修改共享数据,需要人工确认
- 低影响操作:搜索、读取、计算,允许自动执行
实现时,可以在注册工具时标注所需权限级别,在 Executor 执行时对照当前会话的授权情况拦截。
ts
await assertPermission(tool.permissions, session.permissions);安全边界不是规划模块的职责,但规划模块是触发工具执行的地方。因此,安全策略的检查点放在 Executor 入口最为合理——即使模型生成了计划,也不能绕过权限检查直接执行。
9. 工程实现:结构化输出、函数调用与可观测性
9.1 结构化输出
LLM 天然输出文本。为了让脚本直接使用模型的规划结果,需要结构化输出能力。常见做法可以归纳为两类:
基于 prompt 约束。在提示词中要求输出 JSON,再用 JSON.parse 解析。这种方式实现简单,但解析可能失败——模型可能输出 Markdown 片段、尾随逗号、多余文字。解析异常时,可以让模型修正并重试。
基于 API 的结构化输出。较新的模型 API 支持约束输出。资料 [1] 中提到,xAI 的 Structured Outputs 通过 response_format 参数接受 JSON Schema,当传入的 schema 符合受支持的特性时,响应保证匹配该 schema。这使得规划器的输出可以被直接解析为计划对象。
ts
const completion = await client.chat.completions.create({
model: 'grok-json',
messages: [{ role: 'user', content: '规划任务:调研竞品 A' }],
response_format: {
type: 'json_schema',
json_schema: {
name: 'plan',
strict: true,
schema: PlanSchema
}
}
});
const plan = JSON.parse(completion.choices[0].message.content);资料 [1] 还区分了两种请求方式:response_format 参数和工具调用。在工具调用中,模型生成的工具调用参数严格符合工具输入 schema——这就是函数调用天然的结构化输出路径。
9.2 函数调用(Function Calling)
函数调用是另一种常用方式。可以把工具声明为一个 function,模型即使不执行它,也会返回一个结构化的 tool_call。在一些实现中,Planner 被实现为一次“生成多个函数调用”的请求,直接把计划定义为并行的函数调用列表。
一个实现参考流程来自资料 [2] 的官方示例:
- 构建包含工具定义的 messages
- 循环调用模型,每次检查回复中是否有 tool_call
- 如果有,执行对应工具,将结果以
role: 'tool'和tool_call_id追加回 messages - 再次调用模型,直到模型不再返回 tool_call
ts
const messages = [{ role: 'user', content: task }];
const tools = buildToolDefinitions(toolRegistry);
for (let i = 0; i < maxSteps; i++) {
const response = await model.chat({ messages, tools });
if (!response.tool_calls?.length) {
return response.content;
}
for (const call of response.tool_calls) {
const tool = toolRegistry.get(call.function.name);
const result = tool
? await tool.handler(JSON.parse(call.function.arguments))
: `Error: unknown tool '${call.function.name}'`;
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify(result)
});
}
messages.push({
role: 'assistant',
content: response.content,
tool_calls: response.tool_calls
});
}上面的循环会持续把工具结果追加回 messages,直到模型不再请求调用工具,此时 response.content 就是最终回答。注意 messages 中需要同时保留 assistant 的 tool_calls 和每个 tool 的返回结果,模型才能正确关联调用。
9.3 可观测性
Agent 的规划过程相比普通程序更难调试。需要记录的信息包括:
- 每次模型调用传入的 messages(或摘要)
- 模型返回的 decision 原始结构
- 每次工具调用的名称、参数、耗时、返回码
- 每个步骤执行前后的状态变化
- 任何重试、重新规划、人工介入的事件
日志中应保留足够的追踪信息,使开发者可以在任务失败后复现决策路径。这通常在框架中通过日志中间件实现。
ts
function withTracing(tool: ToolRecord): ToolRecord {
const original = tool.handler;
return {
...tool,
handler: async (args) => {
const start = Date.now();
try {
const result = await original(args);
logger.log('tool_ok', { tool: tool.name, args, ms: Date.now() - start });
return result;
} catch (err) {
logger.log('tool_error', { tool: tool.name, args, error: String(err) });
throw err;
}
}
};
}withTracing 对工具的执行函数做包装,在调用前后记录日志,不改变原函数的行为。将注册表中的 handler 替换为包装后的函数,即可获得统一的可观测性。
10. 规划评估的思路与常见陷阱
10.1 评估思路
规划质量不能只用“任务最终是否成功”来评估,需要分维度:
- 任务成功率:最终是否达到目标。这是对外最直观的指标,但无法定位失败环节
- 步骤有效率:执行计划中有多少步骤对最终结果有贡献,有多少是无效调用
- 工具选择准确率:每一步选择的工具是否最合适
- 参数正确率:生成的参数是否通过工具校验
- 计划修订次数:执行原计划时触发重新规划的次数,间接反映计划质量
- 成本与延迟:API 调用次数、token 消耗、端到端耗时
评估需要人工标注或定义一个自动评估脚本。常用做法是维护一个回归测试集:每类任务准备数十个样本,规定输入、期望输出、允许使用的工具集合,然后运行完整 Agent 循环,统计上述指标。
10.2 常见陷阱
只统计最终成功率。最终成功可能掩盖大量无效中间步骤;最终失败也可能是因为工具异常而非规划本身有问题。
工具调用次数不设上限。有些 Agent 看似规划很好,实际是靠多次尝试碰运气。这类方案的尾部成本很高,评估时需要统计平均调用次数和最大调用次数。
测试任务过于简单。如果测试只包含“查询某地天气”这一级别的任务,任何 ReAct 实现都能过关。真正检验规划能力的任务需要多步信息依赖、条件分支和失败恢复。
忽略模型差异。不同模型对同一规划的遵循能力不同。一个在大型模型上表现良好的规划 prompt,换到小型模型上可能无法稳定输出。
失败案例不做归因。为了发现规划缺陷,需要逐个失败案例查看模型在哪一步选错了工具、填错了参数,或低估了步骤之间的依赖。
11. 小结与演进方向
规划模块的核心工作可以概括为三件事:把目标分解成步骤、生成可执行的行动、在循环中根据观察控制执行。本文围绕这三个环节,介绍了相关的概念、模式、架构和工程实现。
几个需要记住的要点:
- ReAct 灵活但决策路径长;Plan-and-Execute 可审查、可控,但需要可靠的重新规划机制
- 规划质量受工具描述、上下文长度、模型能力三个因素共同影响,不能单独优化某一方面
- 结构化输出和函数调用是当前实现规划模块最实用的两个 API 能力
- 安全边界应该放在执行层,不能依赖模型主动规避风险
演进方向可以从三个方面看。
第一,规划的隐式化。越来越多的模型在内部完成部分规划能力,不需要显式输出“计划文本”。但隐式规划不利于调试和人工干预。
第二,测试时计算。在推理时用更多计算换取更可靠的规划,ToT 是这一类思想的代表。随着模型推理成本下降,这类思路会继续发展。
第三,规划与执行的边界将更加模糊。未来的 Agent 框架可能不再区分“先规划再执行”,而是让模型在持续执行中动态调整,人的角色从“编写计划”转向“设定约束与监督结果”。
