【从0开发一个 Agent】第十一章:构建 Planner

发布时间:2026/7/24 20:30:59
【从0开发一个 Agent】第十一章:构建 Planner 在前面的章节中我们赋予了 AI 工具调用、长期记忆、RAG 知识库和工程化 Prompt 等强大能力。但当你向它提出“帮我分析上周的销售数据生成一份包含图表的周报并发送给团队”这样的复杂指令时它依然会陷入混乱。它可能会在还没有获取数据时就尝试生成图表或者在发送失败后直接放弃而不是寻找替代方案。这是因为传统的 ReAct 模式思考-行动-观察在处理长链路、多依赖的复杂任务时存在严重的“上下文稀释”和“意图漂移”问题。本章我们将引入 Planner规划器与多 Agent 协作架构让 AI 从“单步执行者”进化为“项目管理者”。1. 为什么复杂任务不能一次完成在单 Agent 模式下模型需要同时承担“拆解任务”、“执行工具”、“整合结果”三重角色。随着任务步骤增加Prompt 中充斥着大量的中间状态和工具返回结果导致模型对最初目标的注意力被严重稀释。更致命的是ReAct 模式是串行的每一步都依赖上一步的输出一旦中间某个工具调用失败或返回非预期结果整个链路就会崩溃且模型缺乏“回溯重规划”的能力。Planner 的核心价值将“规划”与“执行”解耦。Planner 负责将复杂目标拆解为有向无环图DAG明确任务依赖关系Executor 负责按图执行当执行失败时Planner 能够基于当前状态进行局部重规划而不是推倒重来。2. Planner 架构设计从 ReAct 到 REWOO我们摒弃传统的串行 ReAct采用REWOOReason without Observation架构。它通过“一次性生成完整工具链”来减少 Token 消耗并通过“规划-执行-合并”三阶段实现稳定的复杂任务处理。REWOO 三阶段解析Planner规划器接收用户指令一次性生成包含多个相互关联计划的蓝图。每个计划都分配了变量如#E1供后续任务引用。Executor执行器根据 Planner 的蓝图循环执行任务。它不依赖 LLM 的实时推理而是直接调用工具将结果存入变量。Solver合并器将所有计划和执行结果结合起来形成最终解决方案。为什么选择 REWOO 而非 ReActToken 效率ReAct 每一步都需要 LLM 调用长任务会导致 Token 爆炸REWOO 只在规划和合并阶段调用 LLM中间执行阶段零 LLM 开销。稳定性ReAct 的串行依赖容易因单步失败而崩溃REWOO 的变量引用机制和重规划能力使其具备更强的容错性。可观测性REWOO 生成的任务 DAG 是结构化的便于前端可视化展示任务进度也便于后端进行断点续传和状态追踪。3. 核心代码实现3.1 Planner生成结构化任务 DAG我们使用 Zod 定义严格的任务输出格式确保 Planner 生成的计划是可执行的。// src/lib/planner/schema.tsimport{z}fromzod;exportconstTaskSchemaz.object({id:z.string().describe(任务唯一标识),name:z.string().describe(任务名称),description:z.string().describe(任务详细描述),tool:z.string().describe(需要调用的工具名称),params:z.record(z.any()).describe(工具参数可引用上游变量如 #E1),dependencies:z.array(z.string()).describe(依赖的上游任务ID列表),});exportconstPlanSchemaz.object({title:z.string().describe(计划标题),tasks:z.array(TaskSchema).describe(任务列表),});exporttypePlanz.infertypeofPlanSchema;在src/lib/planner/index.ts中实现 Planner// src/lib/planner/index.tsimport{generateObject}fromai;import{openai}from/lib/ai/config;import{PlanSchema}from./schema;exportasyncfunctioncreatePlan(userQuery:string,availableTools:string[]):PromisePlan{const{object}awaitgenerateObject({model:openai(gpt-4o),schema:PlanSchema,system:你是一个任务规划专家。请将用户指令拆解为可执行的任务DAG。 可用工具:${availableTools.join(, )}规则: 1. 任务必须原子化每个任务只调用一个工具。 2. 明确任务依赖关系确保DAG无环。 3. 参数中引用上游结果时使用 #E{taskId} 格式。 4. 如果任务无法完成不要编造返回空任务列表。,prompt:userQuery,});returnobject;}3.2 Executor按图执行与变量管理Executor 负责解析 Planner 生成的 DAG按依赖顺序执行任务并管理变量池。// src/lib/planner/executor.tsimport{Plan,Task}from./schema;import{getTool}from/lib/ai/tools;exportclassExecutor{privatevariablePool:Mapstring,anynewMap();privatetaskResults:Mapstring,anynewMap();asyncexecute(plan:Plan):PromiseMapstring,any{// 拓扑排序确保依赖任务先执行constsortedTasksthis.topologicalSort(plan.tasks);for(consttaskofsortedTasks){try{// 解析参数中的变量引用constresolvedParamsthis.resolveParams(task.params);// 调用工具consttoolgetTool(task.tool);constresultawaittool.execute(resolvedParams);// 存入变量池和结果集this.variablePool.set(#E${task.id},result);this.taskResults.set(task.id,{status:completed,result});}catch(error){this.taskResults.set(task.id,{status:failed,error:error.message});// 失败时触发重规划见下文constreplannedawaitthis.replan(plan,task.id,error);if(replanned){returnthis.execute(replanned);}thrownewError(Task${task.id}failed and replanning failed:${error.message});}}returnthis.taskResults;}privateresolveParams(params:Recordstring,any):Recordstring,any{constresolved:Recordstring,any{};for(const[key,value]ofObject.entries(params)){if(typeofvaluestringvalue.startsWith(#E)){resolved[key]this.variablePool.get(value);}else{resolved[key]value;}}returnresolved;}privatetopologicalSort(tasks:Task[]):Task[]{// 实现 Kahn 算法进行拓扑排序// ...returntasks;}privateasyncreplan(plan:Plan,failedTaskId:string,error:any):PromisePlan|null{// 调用 Replanner基于当前状态生成新计划// ...returnnull;}}3.3 Solver整合结果与生成最终响应// src/lib/planner/solver.tsimport{generateText}fromai;import{openai}from/lib/ai/config;exportasyncfunctionsolve(plan:Plan,results:Mapstring,any):Promisestring{constresultSummaryArray.from(results.entries()).map(([id,data])任务${id}:${JSON.stringify(data)}).join(\n);const{text}awaitgenerateText({model:openai(gpt-4o),system:你是一个结果整合专家。请基于以下任务执行结果生成最终的用户响应。 如果任务失败请向用户解释原因并提供替代方案。,prompt:计划:${JSON.stringify(plan)}\n执行结果:\n${resultSummary},});returntext;}3.4 在 API Route 中集成 Planner// src/app/api/chat/route.tsimport{createPlan}from/lib/planner;import{Executor}from/lib/planner/executor;import{solve}from/lib/planner/solver;import{getAvailableTools}from/lib/ai/tools;exportasyncfunctionPOST(req:Request){const{messages}awaitreq.json();constuserQuerymessages[messages.length-1].content;// 1. 判断是否需要规划简单问题直接回答constneedPlanningawaitcheckIfPlanningNeeded(userQuery);if(!needPlanning){returnstreamText({model:openai(gpt-4o),messages}).toDataStreamResponse();}// 2. 生成计划constavailableToolsgetAvailableTools();constplanawaitcreatePlan(userQuery,availableTools);// 3. 执行计划constexecutornewExecutor();constresultsawaitexecutor.execute(plan);// 4. 整合结果constfinalResponseawaitsolve(plan,results);returnnewResponse(JSON.stringify({content:finalResponse}),{headers:{Content-Type:application/json},});}4. 测试验证验证清单复杂任务拆解输入“分析上周销售数据并生成周报”验证 Planner 是否生成了包含“查询数据”、“生成图表”、“撰写报告”等步骤的 DAG。依赖关系正确性验证“生成图表”任务是否正确依赖“查询数据”任务的结果。失败重规划故意让“查询数据”工具返回错误验证 Executor 是否触发 Replanner 并生成替代方案如“使用缓存数据”或“询问用户是否重试”。变量引用解析验证下游任务是否正确接收了上游任务的结果如 #E1 被正确替换为实际数据。5. 常见问题与踩坑分析问题 1Planner 生成的任务 DAG 存在循环依赖原因模型对任务依赖关系的理解出现偏差生成了 A→B→A 的环。解决在 Executor 的拓扑排序阶段增加环检测。如果发现环直接返回错误并触发 Replanner而不是尝试执行。问题 2变量引用解析失败导致工具调用参数错误原因Planner 生成的变量名格式不规范或 Executor 的解析逻辑未覆盖所有引用场景。解决在 Planner 的 Prompt 中明确变量命名规范如 #E{taskId}并在 Executor 中使用正则表达式进行严格匹配和解析。问题 3Replanner 陷入无限重规划循环原因Replanner 生成的新计划依然包含失败的任务且没有改变执行策略。解决在 Replanner 中记录历史失败任务强制要求新计划必须避开已失败的任务或工具或引入“最大重规划次数”限制。本章总结我们剖析了 ReAct 模式在处理复杂任务时的局限性并引入了 REWOO 架构。实现了 Planner、Executor、Solver 三阶段协作机制支持任务 DAG 生成、按图执行和结果整合。构建了变量池和拓扑排序机制确保任务依赖关系正确执行。实现了失败重规划能力增强了 Agent 的容错性和鲁棒性。至此我们的 AI Agent 已经具备了拆解复杂任务、按图执行和失败重规划的能力真正实现了从“单步执行者”到“项目管理者”的跨越。但企业级应用不仅是单个 Agent 就能满足的。从下一章开始我们将实现多 Agent让你的 Agent 复杂任务能一次完成。