别让 Agent 盲目乱调用工具:Plan‑and‑Execute 解决任务混乱难题

    |     2026年8月17日   |   AI大模型应用, 原生agent   |     0 条评论   |    3

上篇你给 Agent 装上了 ReAct 引擎——它能反复思考、反复调工具,直到任务完成。但面对复杂任务时,边做边想容易跑偏。这篇讲 Plan-and-Execute——先规划再执行,让 Agent 做事更有章法。


小明遇到了复杂任务

小明兴冲冲地给 Agent 下了一个任务:

你:调研 AI 编程工具现状,写一份简要报告

ReAct 模式的 Agent 开始工作了:

[步骤 1/5] [调用工具]: web_search [参数]: {"query": "AI 编程工具"} [结果]: Copilot、Cursor、Codeium... [步骤 2/5] [调用工具]: web_search [参数]: {"query": "AI 编程"} [结果]: (跟上次类似...) [步骤 3/5] Agent:AI 编程工具主要包括 GitHub Copilot...(一段简短的总结)

小明皱眉头:”这就完了?它搜了两轮就给答案了,但我想的是——先搜工具列表 → 每个工具的详情 → 对比分析 → 写报告。它完全没按这个思路走。”

老张说:”ReAct 的特点是边做边想。模型在每一步判断’还需要什么信息’,但面对复杂任务时,这种自底向上的方式容易:

  1. 方向跑偏——搜着搜着就偏了
  2. 遗漏步骤——模型可能忘了某些环节
  3. 浅尝辄止——搜一两轮就认为’够了'”

“那怎么办?”

“你需要的不是边做边想,而是先想清楚再做——先制定完整的执行计划,用户确认后,再按计划一步步执行。”


Plan-and-Execute 是什么

“Plan-and-Execute 把一个任务分成两个阶段:”

┌─────────────┐ ┌─────────────────────────────────┐ │ Planner │ │ Executor │ │ (规划者) │ │ (执行者) │ │ │ │ │ │ 拆解任务 │────→│ 步骤1:web_search "AI编程工具列表" │ │ 输出步骤清单 │ │ ↓ 拿到结果 │ │ │ │ 步骤2:web_search "Cursor功能详情" │ │ │ │ ↓ 拿到结果(带前序) │ │ 用户确认 │ │ 步骤3:web_search "Copilot对比" │ │ │ │ ↓ 拿到结果 │ │ │ │ 步骤4:整合撰写报告 │ └─────────────┘ └─────────────────────────────────┘

“跟 ReAct 的关键区别:”

维度 ReAct Plan-and-Execute
策略 边做边想 先规划后执行
模型决策 每步决定下一步做什么 一次性决定全部步骤
可控性 依赖模型判断 用户可以审查计划
适合场景 单次复杂问答、动态任务 调研报告、多步分析
性能 单步等待时间长 计划可预览,执行可预测
失败处理 模型自适应调整 需要预设容错逻辑

小明懂了:”就像我写文章——先列大纲,确认方向对了,再逐段填充。”

“对。Plan-and-Execute 就是 Agent 的’先列大纲’。”


代码实战:Planner —— 只规划,不执行

老张打开 planner.py

PLANNER_PROMPT = """你是一个任务规划专家。 用户给你一个复杂任务,把它分解成 3-6 个清晰、可执行的步骤。 可用工具:web_search(搜索)、get_weather(天气)、 calculate(计算)、get_current_time(时间) 要求: - 每步要具体,能直接执行 - 需要工具的步骤标明工具名,不需要的填 null - 步骤数控制在 3-6 步 只返回 JSON,不要其他文字: { "goal": "任务总目标一句话描述", "steps": [ {"step": 1, "description": "具体步骤描述", "tool": "工具名或null"}, {"step": 2, "description": "具体步骤描述", "tool": "工具名或null"} ] }""" def make_plan(task: str) -> dict: messages = [ {"role": "system", "content": PLANNER_PROMPT}, {"role": "user", "content": f"请为这个任务制定执行计划:{task}"}, ] response = chat(messages, tools=None) # 注意:tools=None text = response.content or "" # 解析 JSON,容错处理... return json.loads(text)

“三个关键设计:

  1. tools=None —— Planner 不调用任何工具。它只需要把任务拆成步骤清单,不需要执行。
  2. 每步声明工具名 —— 步骤里标注 "tool": "web_search""tool": null,这样 Executor 知道哪些步骤需要调工具、哪些步骤只需要模型推理。
  3. JSON 输出 —— 结构化计划,方便程序解析和步骤遍历。”

“注意容错:万一 JSON 解析失败(模型输出多了几句废话),用正则兜底提取 {...}。如果连正则都救不回来,回退为单步计划——把整个任务当一步执行。”


代码实战:Executor —— 按计划逐步执行

“计划有了,接下来 Executor 按步骤清单逐步执行:”

def execute_plan(plan: dict) -> str: goal = plan.get("goal", "未知任务") steps = plan.get("steps", []) collected: list[str] = [] # 收集每步结果 for step_info in steps: description = step_info["description"] tool_name = step_info.get("tool") if tool_name: # 工具步骤:用 FC 推断参数 → 执行 params = _infer_params_via_fc( tool_name, description, goal, prior_results=collected ) result = execute_tool(tool_name, params) else: # 非工具步骤:直接让 LLM 完成 msg = chat([{ "role": "user", "content": f"请完成这个步骤:{description}" }]) result = msg.content or "" collected.append(f"步骤{step_num}{description}):\n{result}") # 所有步骤执行完,整合成最终报告 summary = chat([{ "role": "user", "content": ( f"你完成了任务:{goal}\n\n" f"以下是每个步骤的执行结果:\n{all_results}\n\n" "请基于以上所有信息,给用户一个完整、清晰的最终答案。" ), }]) return summary.content or ""

“Executor 的核心逻辑就三层:

  1. 遍历步骤 → 每步判断是工具步骤还是推理步骤
  2. 执行 → 工具步骤调工具,推理步骤调 LLM
  3. 汇总 → 所有步骤结果拼成一个大 prompt,让模型写最终报告”

“关键数据结构是 collected 列表——每执行完一步就把结果追加进去。这个列表有两个用途:

  • 传给 _infer_params_via_fc(让下一步推断参数时能参考前序结果)
  • 最后汇总时喂给模型写报告”

核心技术:只暴露一个工具的 FC

“这里有一个关键技巧——_infer_params_via_fc。”

小明问:”为什么不让模型直接选工具、定参数?像 ReAct 那样自然地 tool_calls?”

老张说:”因为在 Plan-and-Execute 里,选工具这件事 Planner 已经做完了。Executor 的阶段,工具已经定了——你只需要让模型推断参数。”

他打开 executor.py 的这个函数:

def _infer_params_via_fc( tool_name: str, step_description: str, goal: str, prior_results: list[str] | None = None, ) -> dict | None: # 1. 从全部工具中,只取出当前这一个 all_tools = get_tools_schema() tools = [t for t in all_tools if t["function"]["name"] == tool_name] # 2. 拼接前序结果(让模型知道之前发生了什么) prior_block = "" if prior_results: joined = "\n---\n".join(prior_results[-3:]) prior_block = f"\n\n前序步骤结果(供参考):\n{joined}\n" # 3. 强制调用这一个工具 message = chat( messages=[{ "role": "system", "content": "你必须调用给定工具完成当前步骤,不要直接用文字回答。", }, { "role": "user", "content": ( f"任务总目标:{goal}\n" f"当前步骤:{step_description}\n" f"请调用工具 {tool_name}{prior_block}" ), }], tools=tools, # 只暴露一个工具 tool_choice={ # 强制调用这一个工具 "type": "function", "function": {"name": tool_name} }, ) if not message.tool_calls: return None # 推断失败,不盲调 return json.loads(message.tool_calls[0].function.arguments)

“三个关键点:

1. 只暴露一个工具

tools = [t for t in all_tools if t["function"]["name"] == tool_name]

“如果暴露全部 4 个工具,模型可能选错。比如 Planner 说’用 web_search‘,但模型看到 get_weather 也在工具列表里,可能自作主张选 get_weather。只暴露一个工具 = 杜绝选错的概率。”

2. tool_choice 强制调用

tool_choice={"type": "function", "function": {"name": tool_name}}

“如果没有 tool_choice,模型可能不调工具、直接返回文字——比如’我觉得用 web_search 搜索 AI 工具比较好’。这等于没执行。

tool_choice={"type":"function","function":{"name":"web_search"}} 强制模型必须调用 web_search必须产出合法的 JSON 参数。不调就报错——从协议层面保证执行。”

“注意:tool_choice 在有些模型上有限制。比如通义千问的 thinking 模式下不支持 tool_choice=required 或指定函数对象,需要临时关掉 thinking。代码里做了兼容:”

if tool_choice == "required" or isinstance(tool_choice, dict): kwargs["extra_body"] = {"enable_thinking": False}

3. prior_results 传递前序信息

“假设计划是:

  1. 搜索’AI 编程工具列表’
  2. 搜索’Cursor 详细功能’
  3. 搜索’GitHub Copilot 对比’
  4. 撰写报告

执行第 3 步时,如果不告诉模型前两步搜到了什么,模型可能:

  • 搜一个跟前面没关系的关键词
  • 重复搜已经搜过的内容
  • 提出一个跟前序结果矛盾的搜索方向

prior_results 把前面的步骤结果拼接成 前序步骤结果(供参考) 区块,喂给模型:

前序步骤结果(供参考): 步骤1(搜索AI编程工具列表): 1. Copilot | ... 2. Cursor | ... --- 步骤2(搜索Cursor详细功能): Cursor 支持多语言、内联补全、Agent模式...

模型看到这些上下文后,推断搜索参数会更有针对性。比如第3步它会搜 GitHub Copilot vs Cursor 而不是泛泛地再搜一遍 AI 编程工具

注意只传最近 3 步的 prior_results[-3:],避免 context 过长。”


完整运行示例

“看一个完整的 /plan 运行过程:”

你:/plan 调研 AI 编程工具现状,写一份简要报告 正在制定计划... 目标:调研当前主流 AI 编程工具,分析功能特点并生成报告 共 5 步: 步骤 1:搜索 AI 编程工具列表和概况(工具:web_search) 步骤 2:搜索 GitHub Copilot 详细功能介绍(工具:web_search) 步骤 3:搜索 Cursor 和 Windsurf 功能介绍(工具:web_search) 步骤 4:搜索 AI 编程工具对比和趋势(工具:web_search) 步骤 5:整合信息,撰写简要报告 确认执行?(y/n):y 开始执行计划:调研当前主流 AI 编程工具... ──────────────────────────────── 步骤 1:搜索 AI 编程工具列表和概况 [调用工具 web_search],参数:{'query': 'AI 编程工具 2025 现状 列表'} [结果]: 1. GitHub Copilot | ... 2. Cursor | ... 3. Windsurf | ... ──────────────────────────────── 步骤 2:搜索 GitHub Copilot 详细功能介绍 [调用工具 web_search],参数:{'query': 'GitHub Copilot 功能 代码补全 2025'} [结果]: Copilot 支持多语言、Chat 模式、Agent 模式... ──────────────────────────────── 步骤 3:搜索 Cursor 和 Windsurf 功能介绍 [调用工具 web_search],参数:{'query': 'Cursor IDE Windsurf AI 编程 功能对比'} [结果]: Cursor 基于 VS Code、支持整个项目上下文... ──────────────────────────────── 步骤 4:搜索 AI 编程工具对比和趋势 [调用工具 web_search],参数:{'query': 'AI 编程工具 对比 趋势 2025'} [结果]: AI 编程市场快速增长,从补全到自主编程... ──────────────────────────────── 步骤 5:整合信息,撰写简要报告 [AI 完成]: 这是一份关于 AI 编程工具现状的简要报告... ──────────────────────────────── [整合所有结果,生成最终答案...] Agent:《AI 编程工具现状调研报告》 一、主流工具概览... 二、核心功能对比... 三、发展趋势... 四、推荐建议...

“对比之前 ReAct 模式的表现——搜两轮就给一段简短总结——Plan-and-Execute 的输出明显更结构化、更完整。”

小明问:”但如果第 2 步搜 Copilot 时失败了怎么办?”

“Executor 会捕获失败,标记为’步骤失败’,然后继续执行后续步骤。在最后的整合报告里告诉用户’第 2 步未成功获取 Copilot 详情’——任务不因单步失败而崩溃。”


两个模式,一个 CLI

老张打开 main.py

while True: user_input = input("你:").strip() if user_input.startswith("/plan "): # Plan-and-Execute 模式 task = user_input[6:].strip() plan = make_plan(task) print_plan(plan) confirm = input("\n确认执行?(y/n):") if confirm == "y": print(f"\nAgent:{execute_plan(plan)}\n") else: print("已取消。\n") continue # 默认:单任务 ReAct 模式 print(f"\nAgent:{agent.run(user_input)}\n")

“两种模式共存:

  • 直接输入 → 单任务 ReAct(适合快速问答、动态决策)
  • /plan → Plan-and-Execute(适合调研报告、多步分析)

用户确认的环节很重要——复杂任务不要直接执行。让用户看完计划、觉得方向对了再执行,避免浪费 API 费用。”

“另外注意:这里的 ReAct 每次 run() 新建 memory,不跨轮。跨轮记忆要到 Day7 才实现。”


一个容易混淆的点

小明想了想:”Plan-and-Execute 里面也有 Function Calling,那跟 ReAct 里的 FC 有什么区别?”

老张在白板上画出来:

ReAct 里的 FC: 模型 → "我需要信息" → 看全部工具列表 → 选择 + 推断参数 → 执行 角色:工具选择者 + 参数推断者 Plan-and-Execute 里的 FC(Executor 中): 计划已规定工具 → 只看这一个工具 → 只推断参数 → 执行 角色:参数推断者(工具选择权已上交 Planner)

“区别在于决策权的分配

  • ReAct:模型同时掌握’选什么工具’和’用什么参数’——权限大,但可能选错
  • Plan-and-Execute:Planner 先选好工具,Executor 只推断参数——权限拆分,更可控”

“这就是为什么 _infer_params_via_fc 只暴露一个工具 + tool_choice 强制——它不是在让模型’选工具’,而是在让模型’在已知工具上填参数’。”


总结

老张说:”从 ReAct 到 Plan-and-Execute,Agent 的决策方式发生了一次升维。”

“ReAct 是自底向上——遇到什么问题解决什么问题。Plan-and-Execute 是自顶向下——先把全局看清楚,再步步为营。”

问题 ReAct 的做法 Plan-and-Execute 的做法
怎么开始? 走一步看一步 先列完整计划
用户怎么干预? 无法干预中间过程 审查计划、确认后执行
步骤间怎么协同? 消息链自然传递 prior_results 显式传递
失败了怎么办? 模型自适应 标记失败,继续下一步
适合什么任务? 动态决策、快速问答 调研报告、固定流程

“三个核心理解:

  1. 规划与执行分离 —— make_plan() 只规划不执行,execute_plan() 只执行不规划。职责单一,各自干净
  2. FC 角色转变 —— 从’选工具 + 推断参数’降级为’只推断参数’,通过只暴露一个工具 + tool_choice 强制实现
  3. /plan 是交互模式 —— 用户看计划 → 确认 → 执行,把人的判断力嵌入 Agent 工作流”

小明说:”我明白了。ReAct 和 Plan-and-Execute 不是非此即彼,是两种策略——简单的用 ReAct,复杂的用 /plan。”

“对。而且这两种模式可以组合——比如 /plan 的某个步骤内部,如果需要灵活决策,又可以嵌套 ReAct。”

“下一篇呢?”

“下一篇是整个系列的收尾——跨轮记忆的完整 Agent。把你学到的 Function Calling、工具注册表、MCP、记忆、ReAct 循环、Plan-and-Execute 全部串起来,做成一个真正的生产级 Agent。”

Agent 的进化路线:会调用工具 → 会自主决策 → 会规划执行。每一步都在从「执行者」走向「规划者」。

转载请注明来源:别让 Agent 盲目乱调用工具:Plan‑and‑Execute 解决任务混乱难题
本文链接地址:https://ai.zhousir.top/?p=3775
回复 取消