云计算百科
云计算领域专业知识百科平台

Agent架构设计:Planning与动态任务规划

Agent架构设计:Planning与动态任务规划

很多 Agent 的问题,不是不会调用工具,而是不知道什么时候该停下来规划。

简单任务中,边思考边执行没有问题。但当任务涉及多个模块、多个步骤时,Agent 很容易陷入一种状态:每一步都合理,最后结果却和最初目标不一致。区别在于:执行能力和规划能力是两个独立的问题。

目录

  • 没有规划的Agent
  • 规划的本质
  • 为什么LLM不会天然规划
  • 两种范式
  • Plan不是任务列表
  • 怎么拆任务
  • 动态调整
  • 实战:核心流程
  • 小结

没有规划的Agent

在 AgentLoop 那篇文章里,我们讲过 Agent 的基本循环:收到用户输入,推理,调工具,拿到结果,再推理,直到任务完成。这个模式在简单任务上表现很好。创建一个文件、查一个信息、改一段代码,Agent 每一步都能看到当前状态,做出合理判断。

但任务一旦变复杂,这个模式就开始暴露问题。

让 Agent “把一个单体项目拆成微服务”。它没有先想清楚要拆几个服务、边界在哪、共享数据怎么处理,而是直接上来就开始动手:先创建了一个目录,然后开始搬代码。搬着搬着发现两个模块之间有循环依赖,于是停下来处理依赖关系。处理完继续搬,又发现数据库表被两个模块共用,不知道该放哪个服务里。它每一步都在做局部最优的决策,但这些局部最优拼在一起,并不等于全局最优。

如果有规划呢?提前查好景点开放时间,按地理位置排好路线,吃饭的地方也标好。同样的时间,能多去两个地方,还不累。

Agent 也是一样。没有规划的 Agent 是 reactive 的,走一步看一步;有规划的 Agent 是 proactive 的,先想清楚再动手。

规划的本质

从工程角度看,Planning 解决的是执行前的信息组织问题:提前明确目标、步骤以及每一步的预期结果。

人类做复杂任务时天然就会规划。搭一个博客系统,脑子里会先过一遍:要有哪些模块?数据库怎么设计?先做什么后做什么?这些思考发生在动手写代码之前,但 LLM 不一样,它不会天然规划。

为什么 LLM 不会天然规划

LLM 本身并不维护任务状态。它看到的是当前上下文,而不是一个结构化的任务模型。在 AgentLoop 里,每一步的决策都是根据当前 messages 预测下一个动作,执行后把结果塞回 messages,进入下一轮。

问题在于两点:

当前上下文 ≠ 完整任务状态。 messages 里堆的是历史对话和工具输出,但它不包含"这个任务的最终目标是什么"、“已经完成了哪些子目标”、"还剩什么没做"这类结构化的任务状态信息。LLM 看到的是一堆碎片,它需要从碎片中推断全局,这本身就是一个不稳定的任务。

当前最优动作 ≠ 全局最优路径。 每一步 LLM 都在做局部最优决策,但局部最优拼起来不等于全局最优。

所以复杂任务会出现这种典型的局部决策陷阱:

Step1: "创建目录 src/modules/"

Step2: "写代码搬进去"

Step3: 发现目录结构不合理,和另一个模块冲突

返工

每一步在当时看都是合理的,但从全局看,Step1 的目录结构就不对。

Planning 是额外引入一个结构化的能力层:

任务状态模型(当前在哪、目标是什么)
+
目标分解(大目标拆成小目标)
+
执行路线(先做什么、后做什么、每步产出什么)

这三层不是 LLM 天然具备的,需要我们在架构层面主动加进去。在 Agent 开始执行之前,先插入一个"规划阶段",让 LLM 把任务拆解成步骤,生成一个计划,然后按计划逐步执行。

两种范式

目前主流的 Agent 执行范式有两种:ReAct 和 Plan-and-Execute。

ReAct 就是我们之前讲的 AgentLoop 的模式。每一步都是:思考(Reason)→ 行动(Act)→ 观察(Observe)→ 再思考。Agent 永远在根据当前状态做即时反应。

ReAct 循环:

用户输入


思考:我该做什么?


行动:调用工具


观察:拿到结果


思考:下一步该做什么?


行动:调用工具

Plan-and-Execute 多了一个规划阶段。先让 LLM 生成一个完整的执行计划,然后逐步执行。执行过程中如果发现计划需要调整,再重新规划。

Plan-and-Execute 流程:

用户输入


规划:生成执行计划 [步骤1, 步骤2, 步骤3, …]


执行步骤1 → 观察结果


执行步骤2 → 观察结果


需要调整吗?→ 是 → 重新规划


执行步骤3 → …

两者的区别在决策时机:ReAct 是"边做边想",Plan-and-Execute 是"先想后做"。

维度ReActPlan-and-Execute
决策时机 每步即时决策 先规划再执行
全局视角
适用场景 简单任务、步骤少 复杂任务、步骤多且有依赖

但实际生产环境的 Agent 很少纯粹用其中一种。更多是混合模式:

在这里插入图片描述

Plan 提供方向,ReAct 提供执行灵活性。 目前很多代码 Agent 都采用类似思路:先生成执行方向,再进入工具调用循环,并在必要时重新调整计划。和之前讲的 Agent 错误恢复也是同一个思路——执行过程中出问题了,暂停、调整、继续。

对于大多数简单任务,ReAct 足够了。但当任务步骤超过五六步、且步骤之间有依赖关系时,Planning 的全局视角就能避免很多"局部最优、全局混乱"的问题。Agent 不需要每步都重新想"我该做什么",它只需要看一眼计划,执行当前步骤,然后继续。

Plan不是任务列表

会有初学者理解的 Plan 是这样的:

1. 创建文件
2. 安装依赖
3. 写代码
4. 跑测试

这只是一个 Task List,不是 Plan。Task List 只告诉你"做什么",不告诉你"为什么做"、“做到什么程度”、“怎么验证做对了”。

一个完整的 Plan 应该包含五个要素:

要素含义示例
Goal 最终目标 搭建一个可运行的 React 项目
State 当前状态和目标状态 当前:空目录 → 目标:npm run build 通过
Constraints 约束条件 React 18、TypeScript、feature 目录结构
Steps 执行步骤 初始化 → 配置 → 实现 → 验证
Validation 验证条件 npm run build 无报错、ESLint 无警告

为什么这个区分重要?因为在 Agent 执行过程中,最容易出问题的不是"下一步调哪个工具",而是任务目标有没有漂移。

举个例子。Agent 的目标是"搭建一个 React 项目,用 feature 模块划分目录"。执行到第三步时,它发现 Vite 默认模板用的是 pages 结构,于是顺手就按 pages 结构搭了。每一步的代码都没问题,但最终结果偏离了原始需求。

这就是只维护 Task List 不维护 Plan State 的后果。Agent 知道自己在执行第几步,但它不知道"原始目标是什么"、“当前进度和目标之间的差距有多大”。没有 Goal 和 Constraints 做锚点,执行过程中很容易被中间结果带偏。

所以 Planner 输出的不应该只是步骤列表,还应该包含目标描述和约束条件,执行过程中持续对照,防止漂移。

怎么拆任务

规划的核心环节是任务拆解。把一个大任务拆成可执行的步骤列表,这是 Planner 的职责。

最直接的方式:让 LLM 根据用户输入,输出一个步骤列表。

给 LLM 的 prompt 模板:

你是一个任务规划器。根据用户的请求,生成一个执行计划。
每个步骤必须是可独立执行的原子操作。
输出 JSON 格式。

用户请求: {user_request}

可用工具: {tool_descriptions}

输出格式:
{
"steps": [
{"id": 1, "description": "步骤描述", "tool": "工具名", "params": {…}},

]
}

LLM 返回的计划大概长这样:

{
"steps": [
{"id": 1, "description": "初始化 TypeScript 项目", "tool": "bash", "params": {"command": "npm create vite@latest . — –template react-ts"}},
{"id": 2, "description": "安装 ESLint 和 Prettier", "tool": "bash", "params": {"command": "npm install -D eslint prettier eslint-config-prettier"}},
{"id": 3, "description": "写入 ESLint 配置", "tool": "write_file", "params": {"path": ".eslintrc.js", "content": "…"}},
{"id": 4, "description": "写入 Prettier 配置", "tool": "write_file", "params": {"path": ".prettierrc", "content": "…"}},
{"id": 5, "description": "按 feature 创建目录结构", "tool": "bash", "params": {"command": "mkdir -p src/features/auth src/features/dashboard …"}}
]
}

这种方式简单直接,适合步骤不多的任务。但有个问题:如果任务本身很复杂,一步"搭建后端"太粗了,需要进一步拆解。

这时候可以递归拆解。先让 LLM 把任务拆成几个大步骤,再把每个大步骤继续拆,直到每一步都是"一个工具调用能完成"的粒度。

def plan_task(task: str, tools: list, depth: int = 0, max_depth: int = 3) > list:
"""递归拆解任务"""
if depth >= max_depth:
return [{"task": task, "subtasks": []}]

# 让 LLM 拆解任务
plan_prompt = f"""把以下任务拆成 2-5 个子任务,每个子任务用一句话描述。
任务:
{task}
输出 JSON: {{"subtasks": ["子任务1", "子任务2", …]}}"""

result = json.loads(llm(plan_prompt))
subtasks = []
for st in result["subtasks"]:
# 根据工具能力判断是否需要继续拆
if is_atomic(st, tools):
subtasks.append({"task": st, "subtasks": []})
else:
subtasks.append({
"task": st,
"subtasks": plan_task(st, tools, depth + 1, max_depth)
})
return subtasks

def is_atomic(task: str, tools: list) > bool:
"""判断任务是否可以用单个工具调用完成"""
prompt = f"""判断以下任务是否可以用单个工具调用完成。
任务:
{task}
可用工具及参数:
{describe_tools(tools)}
判断标准:任务的输入能否完全映射到一个工具的参数,输出是否就是该工具的返回值。
回答 yes 或 no。"""

return "yes" in llm(prompt).lower()

递归拆解出来的是一棵任务树,而不是扁平的列表。执行时按深度优先遍历,先处理叶子节点,再向上汇总。

判断"任务是否足够细粒度"不应该完全依赖 LLM。LLM 说"一步就能完成",实际上可能需要三步。更好的做法是结合工具能力来判断——如果一个任务的输入能完全映射到某个工具的参数,输出就是该工具的返回值,那它就是原子的。比如"创建 User.java 文件并添加字段",可以拆成 write_file 一个调用,是原子的;但"搭建用户管理模块"涉及建表、写 Model、写 Service、写 Controller,不是原子的。

不过在实际工程中,递归拆解的 token 消耗比较大,大多数场景下让 LLM 直接输出扁平的步骤列表就够了。只有当任务确实需要多层分解时,才值得用递归方式。

动态调整

计划不是一成不变的。Agent 在执行过程中会遇到各种意外:某个步骤执行失败了,执行结果和预期不一样,或者执行过程中发现了新的信息需要调整后续计划。

所以 Plan-and-Execute 需要一个 Replan 机制。

但实际系统不会每一步都 Replan。一个 100 步的任务,每步都调一次 Replanner,等于多了一倍的 LLM 调用,成本扛不住。生产环境通常用触发式 Replan:

执行步骤


是否满足触发条件?

┌──┴──┐
否 是
│ │
▼ ▼
继续 Replan
执行 生成新计划

常见的触发条件有三种:

触发条件示例
执行失败 write_file 权限不足,bash 命令报错
关键状态变化 发现数据库里已有同名表,结构不同
连续偏离计划 连续 N 步的实际结果和预期不符

def should_replan(step: dict, result: str, history: list) > bool:
"""判断是否触发 Replan"""
# 条件1:执行失败
if "error" in result.lower() or "failed" in result.lower():
return True
# 条件2:结果和预期不符
if step.get("expected") and step["expected"] not in result:
return True
# 条件3:连续 N 步偏离
recent_failures = sum(1 for h in history[3:] if h.get("deviated"))
if recent_failures >= 2:
return True
return False

举个实际场景。Agent 的计划是"创建数据库表 → 写 Model 类 → 写 API 接口"。执行第一步时发现数据库里已经有一个同名的表,结构还不一样。触发条件命中,Agent 暂停执行,把"表已存在"这个信息反馈给 Planner,Planner 生成新计划:先对比已有表结构和需求的差异,再决定是修改表还是修改 Model 类。

如果第一步顺利,第二步也顺利,那就正常往下走,不触发 Replan。只有出问题了才暂停调整。

这就是动态规划和静态规划的区别。静态规划是一次性的,生成完就不管了;动态规划是持续的,按需触发调整。实际项目中几乎都是动态规划,因为现实世界太复杂,不可能一次就把所有情况都考虑到。

实战:核心流程

把上面的概念串起来,Plan-and-Execute 的核心流程用伪代码表示:

# 规划阶段
plan = planner.generate(user_request)

# 执行阶段
while plan.has_next():
step = plan.next_step()
result = executor.run(step)

# 触发式 Replan:执行失败或结果不符预期时才调整
if need_replan(step, result):
plan = planner.replan(plan.state, result)

plan.mark_done(step, result)

三个组件的职责:

  • Planner:接收用户请求,结合工具定义,生成结构化的执行计划(Goal + Steps + Constraints)
  • Executor:执行单个步骤,调用对应的工具,返回结果
  • Replanner:在触发条件命中时,根据当前状态和执行结果调整剩余计划

整个流程走一遍:

用户: "创建一个 Python 项目,包含 main.py 和 requirements.txt"

计划:
步骤1: write_file → 创建 main.py
步骤2: write_file → 创建 requirements.txt

执行: 创建 main.py → done
执行: 创建 requirements.txt → done

全部完成

如果中间出了问题,比如写文件权限不足,Replanner 会收到失败信息,调整计划。实际项目中,Executor 的工具调用应该通过沙箱执行(参考 Agent 沙箱那篇),而不是直接暴露 shell。

小结

Planning 并不是所有 Agent 都必须增加的一层。对于简单任务,ReAct 已经足够;但当任务包含多个依赖步骤时,引入规划阶段可以降低执行过程中的方向偏移和返工。实际工程中的做法通常是混合模式:Plan 给方向,ReAct 做执行,Replan 兜底。生成计划和 Replan 都有 token 成本,五步以内的任务加规划层反而更慢,规划的价值在复杂任务中才真正体现。

赞(0)
未经允许不得转载:网硕互联帮助中心 » Agent架构设计:Planning与动态任务规划
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!