智能体面试准备(五):规划与任务分解——从 Plan-and-Execute 到动态重规划的工程实现
智能体面试准备(五):规划与任务分解——从 Plan-and-Execute 到动态重规划的工程实现
Agent 面试里,"工具调用"考的是基本功,"规划能力"考的才是深度。面试官常用的开场是:"你的 Agent 拿到一个复杂任务,比如'调研三个竞品并输出对比报告',它怎么知道先做什么后做什么?"如果你的回答只有"让模型自己想",那基本就暴露了没做过复杂 Agent。这篇把任务规划的主流范式、Plan-and-Execute 的完整实现、动态重规划的触发机制讲透,配一段可直接运行的 planner 代码。
一、为什么需要显式规划:ReAct 的天花板
ReAct(每步"想一下→做一下→看结果")是 Agent 的入门范式,但它在复杂任务上有三个结构性缺陷:
- 短视:每步只基于当前观察决定下一步,缺乏全局视野,容易在中途偏离最终目标;
- 上下文膨胀:所有中间观察都塞进历史,长任务后期模型"忘了"最初目标,且 token 成本线性上涨;
- 错误累积:某一步走偏后没有全局参照,后续步骤在错误方向上越走越远。
显式规划(Planning)的思路是把"想清楚整体怎么做"和"逐步执行"分离:先生成任务分解(plan),再逐项执行(execute),执行中按需修订计划(re-plan)。这就是 Plan-and-Execute 范式,LangChain 的同名实现、BabyAGI 的任务队列、OpenAI Deep Research 的多步研究流程,内核都是它。
二、主流规划范式对比
| 范式 | 核心机制 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| ReAct | 逐步交错推理与行动 | 实现简单、灵活应变 | 短视、上下文膨胀 | 短链任务(≤5步) |
| Plan-and-Execute | 先全局分解再逐项执行 | 全局一致、可并行、省 token | 计划可能过时 | 中长任务、可预估结构 |
| ReWOO | 计划中用变量引用未来结果(#E1、#E2) | 一次规划零中断,token 最省 | 完全无法应变 | 步骤确定的流水线 |
| LLMCompiler | 生成任务 DAG,无依赖任务并行执行 | 延迟最低 | 实现复杂 | 多工具可并行场景 |
| Tree of Thoughts | 树状展开多分支+回溯 | 探索性最强 | 成本爆炸 | 解谜、搜索类问题 |
面试高频追问:ReWOO 的变量引用机制是什么?答:planner 一次性生成所有步骤,用占位符(#E1)表示"第一步的执行结果",后续步骤直接引用占位符;executor 按序执行并做变量替换,最后 solver 汇总。全程只调用两次 LLM(规划+汇总),token 消耗远低于 ReAct 的 N 次调用。代价是中途无法根据实际结果调整——所以它只适合结构确定的任务。
任务分解的两个正交维度也值得记住:分解粒度(粗粒度里程碑 vs 细粒度动作)和分解时机(一次性全部分解 vs 递归按需分解)。递归分解(把大任务拆成子任务,子任务执行时再拆)更接近人类做法,也是 Manus、Devin 这类长程 Agent 的实际策略。
三、Plan-and-Execute 的工程要素
一个生产可用的 planner 至少要处理四件事:
计划的结构化表示。不能是自然语言段落,必须是结构化对象:每个步骤有 id、描述、依赖(depends_on)、状态(pending/running/done/failed)、产出。有了依赖关系,无依赖的步骤可以并行,失败可以精确定位影响范围。
执行器与规划器分离。planner 用强模型(规划质量决定上限),executor 可以用便宜模型或纯代码。executor 只看到"当前步骤+相关上下文",而非全部历史——这是控制上下文膨胀的关键手段。
重规划触发机制。三种标准触发条件:步骤执行失败且重试无效;执行结果与计划假设不符(比如"搜索竞品A的财报"发现竞品A已被收购);用户中途修改需求。重规划时把"原计划+已完成步骤+失败原因"喂给 planner,生成修订计划,已完成的工作要保留。
终止与预算控制。最大步数、最大 token 预算、最大重规划次数三道保险,防止 Agent 无限循环烧钱——这是面试官特别爱听的工程意识。
四、可运行代码:带依赖管理与重规划的迷你 Planner
下面用纯 Python 标准库实现一个可运行的 Plan-Execute 框架:DAG 依赖调度 + 失败重试 + 重规划钩子。真实项目里把fake_llm_plan和execute_step换成 LLM 调用即可,骨架完全一致。
import json from dataclasses import dataclass, field from enum import Enum class Status(Enum): PENDING = "pending" DONE = "done" FAILED = "failed" @dataclass class Step: id: str desc: str depends_on: list = field(default_factory=list) status: Status = Status.PENDING result: str = "" retries: int = 0 def fake_llm_plan(goal: str) -> list: """模拟 planner LLM:返回结构化计划(真实场景换成 LLM+JSON 输出)。""" return [ Step("s1", "搜索竞品A的公开资料"), Step("s2", "搜索竞品B的公开资料"), Step("s3", "提取两家竞品的定价与功能", depends_on=["s1", "s2"]), Step("s4", "生成对比报告", depends_on=["s3"]), ] def execute_step(step: Step, context: dict) -> str: """模拟 executor:s2 第一次执行会失败,用于演示重试与重规划。""" if step.id == "s2" and step.retries == 0: raise RuntimeError("竞品B官网无法访问") return f"[{step.desc}] 的执行结果" def replan(goal: str, steps: list, failed: Step) -> list: """模拟重规划:把失败步骤换成替代方案,保留已完成的工作。""" print(f" >> 触发重规划:{failed.desc} 失败原因已提交 planner") failed.desc = "改用第三方数据库查询竞品B信息" failed.status = Status.PENDING return steps def run(goal: str, max_rounds: int = 10, max_retries: int = 1): steps = fake_llm_plan(goal) context = {} for round_i in range(max_rounds): # 找出所有依赖已满足的待执行步骤(真实场景可并行) ready = [s for s in steps if s.status == Status.PENDING and all(next(x for x in steps if x.id == d).status == Status.DONE for d in s.depends_on)] if not ready: break for step in ready: try: step.result = execute_step(step, context) step.status = Status.DONE context[step.id] = step.result print(f" [OK] {step.id}: {step.desc}") except Exception as e: step.retries += 1 print(f" [FAIL] {step.id}: {e} (第{step.retries}次)") if step.retries > max_retries: step.status = Status.FAILED steps = replan(goal, steps, step) done = all(s.status == Status.DONE for s in steps) print(f"任务{'完成' if done else '未完成'},共 {round_i+1} 轮调度") return context if __name__ == "__main__": result = run("调研竞品A和B并输出对比报告") print(json.dumps(result, ensure_ascii=False, indent=2))运行后可以看到完整过程:s1/s2 并发就绪 → s2 失败重试 → 重试再失败触发重规划 → 替代方案执行成功 → 依赖满足后 s3、s4 顺序完成。这段代码覆盖了面试手撕 planner 的全部得分点:DAG 依赖调度、失败重试、重规划、预算控制。
五、规划质量的评估与提升
评估:规划是中间产物,直接评估的常用维度是——计划可执行率(每步是否对应可用工具)、步骤冗余率、依赖正确率;端到端则看任务成功率与平均步数。学术界有 PlanBench 等基准,工业界更多用人工抽检 + 端到端 A/B。
提升手段(按性价比排序):
- Few-shot 计划示例:在 planner prompt 里放 2~3 个高质量计划范例,是最便宜有效的手段;
- 结构化输出约束:JSON Schema 强制输出步骤对象,杜绝自然语言计划的解析失败;
- 工具清单注入:规划时把可用工具及其能力描述给 planner,避免规划出"无法执行的步骤"——计划与工具脱节是新手 Agent 最常见的失败模式;
- 计划评审(plan critique):让另一个 LLM 调用审查计划的完整性与可行性再执行,成本换质量;
- 微调专用 planner:积累了足够的(任务,优质计划)数据后,微调小模型做规划,降本增效。
六、面试答题框架与高频题
"设计一个能完成'订机票+订酒店+生成行程单'的 Agent",推荐作答结构:
- 范式选择:结构较确定,Plan-and-Execute 优于纯 ReAct;机票和酒店查询无依赖可并行,行程单依赖前两者;
- 计划表示:JSON 步骤对象 + depends_on 字段,DAG 调度;
- 异常路径:航班无票→重规划改时间;酒店与航班日期冲突→触发一致性校验步骤;
- 人机交互点:支付前必须人工确认(高风险动作卡点);
- 预算控制:最大步数与重规划次数上限。
其他高频题速答要点:
- "ReAct 和 Plan-and-Execute 怎么选?"——任务步数少、探索性强用 ReAct;步骤可预估、要控成本用 P&E;实践常用混合:全局 P&E,单步内部 ReAct。
- "计划过时怎么办?"——答重规划三触发条件(失败、假设不符、需求变更)+ 保留已完成工作。
- "怎么防止 Agent 死循环?"——步数/token/重规划次数三重预算 + 重复动作检测(同一工具同参数连续调用 N 次强制终止)。
自检清单:能说清 ReAct 的三个结构性缺陷吗?能手写带依赖的任务调度吗?知道 ReWOO 的变量引用机制吗?能列出重规划的三个触发条件吗?这四点齐了,规划环节就能答出区分度。
下一篇讲记忆系统:短期记忆、长期记忆、向量化检索记忆的设计与代码实现。
