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

发布时间:2026/7/27 23:46:53
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 vitelatest . -- --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 把任务拆成几个大步骤再把每个大步骤继续拆直到每一步都是一个工具调用能完成的粒度。defplan_task(task:str,tools:list,depth:int0,max_depth:int3)-list:递归拆解任务ifdepthmax_depth:return[{task:task,subtasks:[]}]# 让 LLM 拆解任务plan_promptf把以下任务拆成 2-5 个子任务每个子任务用一句话描述。 任务:{task}输出 JSON: {{subtasks: [子任务1, 子任务2, ...]}}resultjson.loads(llm(plan_prompt))subtasks[]forstinresult[subtasks]:# 根据工具能力判断是否需要继续拆ifis_atomic(st,tools):subtasks.append({task:st,subtasks:[]})else:subtasks.append({task:st,subtasks:plan_task(st,tools,depth1,max_depth)})returnsubtasksdefis_atomic(task:str,tools:list)-bool:判断任务是否可以用单个工具调用完成promptf判断以下任务是否可以用单个工具调用完成。 任务:{task}可用工具及参数:{describe_tools(tools)}判断标准任务的输入能否完全映射到一个工具的参数输出是否就是该工具的返回值。 回答 yes 或 no。returnyesinllm(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 步的实际结果和预期不符defshould_replan(step:dict,result:str,history:list)-bool:判断是否触发 Replan# 条件1执行失败iferrorinresult.lower()orfailedinresult.lower():returnTrue# 条件2结果和预期不符ifstep.get(expected)andstep[expected]notinresult:returnTrue# 条件3连续 N 步偏离recent_failuressum(1forhinhistory[-3:]ifh.get(deviated))ifrecent_failures2:returnTruereturnFalse举个实际场景。Agent 的计划是创建数据库表 → 写 Model 类 → 写 API 接口。执行第一步时发现数据库里已经有一个同名的表结构还不一样。触发条件命中Agent 暂停执行把表已存在这个信息反馈给 PlannerPlanner 生成新计划先对比已有表结构和需求的差异再决定是修改表还是修改 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 ConstraintsExecutor执行单个步骤调用对应的工具返回结果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 成本五步以内的任务加规划层反而更慢规划的价值在复杂任务中才真正体现。