文章进阶AI 生成初稿 · deepseek-chat

多工具编排:让 Agent 完成多步任务

Agent 只有一两个工具时,循环很简单;工具一多,问题就变成编排:哪些步骤能并行、中间结果怎么传、循环什么时候停。本文讲清多工具编排的四件事,并给出可运行的最小骨架。

约 5 分钟发布于 2026年9月12日更新于 上次校对 2026年9月12日 · shelter123
本文目录

TL;DR

  • 多工具编排的核心不是“让模型多调几个工具”,而是你替它决定调用顺序、数据流向和停止条件
  • 串行与并行的分界线只有一条:后一个步骤是否依赖前一个步骤的输出;不依赖就该并行。
  • 中间结果不要靠模型“记住”,用显式的数据结构在步骤之间传递,模型只负责决定“下一步调什么”。
  • 循环必须有硬上限(最大步数)和终止条件(模型给出最终答案 / 无工具可调 / 超预算),三者缺一不可。
  • 把复杂任务拆成工具步骤时,拆分的粒度目标是:每一步的输入输出都能被程序校验,而不是“看起来像人话”。

前置知识

  • 你已经知道什么是工具调用(tool calling / function calling):模型返回一个结构化的调用意图,你的代码执行它,再把结果喂回去。如果你还没写过第一个工具,先读同课程的《给 Agent 装上第一双手:工具调用入门》。
  • 你会用一门语言写异步代码(本文示例用 JavaScript / TypeScript 风格伪代码,不依赖具体 SDK)。
  • 你理解 JSON Schema 的基本形状(type / properties / required),因为工具参数校验会反复用到它。

不需要:不需要你已经搭过完整 Agent 框架,不需要读过 LangChain / LlamaIndex 源码,不需要懂分布式调度。本文只讲单进程内、单 Agent 的多工具编排,不涉及多 Agent 协作。

零基础?先读小白故事线《如果 Agent 是新来的实习生》。

正文

为什么“工具变多”会突然变难

一个工具的时候,Agent 的循环几乎是平凡的:模型要么调用它,要么给出最终答案。两个工具时,你开始遇到“先查再写”的顺序问题。等到五六个工具,问题就不再是“模型会不会用工具”,而是编排(orchestration)——谁先谁后、哪些能一起做、上一步的结果交给谁、什么时候该停。

这两件事必须分开看:

  • 模型负责的部分:在每一步,根据当前上下文决定“下一步调用哪个工具、参数是什么”,或者决定“我已经能回答了”。
  • 你负责的部分:循环的控制流——步数上限、并行调度、中间结果的存储与传递、错误重试、终止判定。

把第二件事也交给模型,是新手最常见的坑。模型没有可靠的计数器,也看不见你的执行预算,它会在该停的时候继续调工具,在该并行的时候傻等。编排是你写的代码,不是提示词。

串行与并行:一条判断规则

判断两个工具调用能不能并行,只问一个问题:B 的入参里,有没有任何一项来自 A 的输出?

  • 有 → 串行,B 必须等 A 完成。
  • 没有 → 可以并行,两者同时发出。

举一个具体任务:“把用户这段会议记录整理成纪要,并查一下提到的项目当前状态”。拆开看:

  • extract_action_items(text):从记录里抽行动项。
  • get_project_status(project_id):查项目状态。
  • write_summary(items, status):合成纪要。

前两个工具都只依赖原始输入 text 和用户给出的项目名,互不依赖,可以并行;第三个依赖前两个的输出,必须串行收尾。

并行的价值不只是快。它还能减少一次“模型往返”:如果你串行地让模型先调 A、看到结果、再调 B,就多花了一轮推理。把无依赖的调用一次性发出去,是编排层能拿到的确定性收益。

但并行有代价,别无条件上:

  • 速率限制与配额:并行 5 个请求可能直接撞上 API 的 rate limit,反而触发重试。
  • 副作用:如果工具会写数据库、发邮件,并行执行会让失败回滚变复杂。只读工具优先并行,写操作优先串行。
  • 错误处理:并行分支里一个失败,你要决定是全部放弃还是部分降级,这比串行难写。

一个务实的默认策略:默认串行,识别出无依赖的只读调用后再并行。不要一上来就写通用 DAG 调度器,那是过度设计。

中间结果:不要让模型当内存

多步任务里,数据要在步骤之间流动。有两种传法:

传法一:塞回对话历史。 把每个工具的输出作为 tool 角色的消息追加进 messages。这是工具调用的标准做法,模型能看见全部中间结果。问题是:步骤一多,上下文迅速膨胀,而且模型可能“记串”——把第三步的返回值当成第一步的。

传法二:存在你自己的状态对象里,只把必要片段喂回模型。 例如:

// 编排层维护的状态,模型看不到全貌,只看到你选择暴露的部分
type RunState = {
  steps: number;              // 已执行步数,用于硬上限
  artifacts: Record<string, unknown>; // 工具产出,按 key 存放
  trace: string[];            // 审计用,不喂回模型
};

推荐的做法是两者结合:工具输出照常回灌(否则模型无法决定下一步),但关键中间结果同时落到 artifacts,后续步骤直接从 artifacts 取值,而不是从模型复述的自然语言里解析。模型负责“决定调什么”,你的代码负责“把值准确传过去”。

这带来一个重要的设计约束:每个工具的输出应该是结构化的、可被程序消费的。如果 extract_action_items 返回的是一段散文,你就没法可靠地把它喂给 write_summary。让它返回数组,让 JSON Schema 约束它。

循环上限与终止条件

Agent 的主循环本质上是一个 while。任何 while 都需要终止条件,否则就是死循环——只不过这里的死循环会烧钱。

终止条件应该同时设三条,任一满足即停:

  1. 模型给出最终答案:这一轮没有工具调用,只有文本回复。这是正常出口。
  2. 达到最大步数steps >= maxSteps,比如 10 或 15。这是防跑飞的硬闸。
  3. 无可用工具 / 参数校验连续失败:模型反复调用一个不存在或参数错误的工具,说明它卡住了,继续循环只是浪费。

一个最小骨架长这样:

async function runAgent(input: string, maxSteps = 10) {
  const state: RunState = { steps: 0, artifacts: {}, trace: [] };
  let messages = [{ role: "user", content: input }];

  while (state.steps < maxSteps) {
    state.steps += 1;
    const reply = await callModel(messages, tools);

    if (!reply.toolCalls?.length) return reply.content; // 出口 1:最终答案

    for (const call of reply.toolCalls) {
      const result = await executeTool(call); // 内部做 schema 校验与错误捕获
      state.artifacts[call.id] = result;
      messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify(result) });
    }
  }
  return "达到最大步数仍未完成,请缩小任务范围。"; // 出口 2
}

关于 maxSteps 的取值:没有一个普适数字。经验上,你预期的步骤数 × 2 是个合理起点——留出重试和绕路的余量,又不至于让一次失控跑掉几十轮。把它设成可配置项,而不是硬编码。

还有两个容易被忽略的点:

  • 步数 ≠ 工具调用次数。一轮里并行调 3 个工具算 1 步还是 3 步?建议按“模型往返次数”计步,因为那才是成本和延迟的主要来源。
  • 超时要单独设。步数上限管不住“某一次工具调用卡住 60 秒”,需要给 executeTool 加超时。

把复杂任务拆成工具步骤

拆分粒度是编排质量的上限。拆得太粗(一个工具干完所有事),模型没有决策空间,编排退化成固定流水线;拆得太细(每个小动作一个工具),模型要在十几个工具里选,选错的概率上升,步数也膨胀。

一个可操作的判断标准:每一步的输入输出都能被程序校验

  • “搜索并总结网页”是个坏工具——输出是自由文本,下一步没法可靠消费。
  • search(query) -> [{title, url, snippet}]fetch(url) -> {text}summarize(text) -> {bullets} 是好拆分——每一环的输出形状确定,能被下一环直接吃下。

按这个标准,拆分时问自己三个问题:

  1. 这一步的输出,下一个工具能直接当输入用吗? 不能,就说明该在中间补一个“结构化”步骤,或者让这个工具返回结构化数据。
  2. 这一步失败时,能不能单独重试? 能单独重试的步骤,边界通常划对了。
  3. 模型需要在这两个工具之间做选择吗? 如果永远是 A 之后必接 B,那它们可以合并成一个工具,省一次模型往返。

最后一条尤其反直觉:编排不只是“拆”,也包括“合”。确定的、无分支的连续步骤,合并进一个工具里执行,比让模型来回决策更快也更稳。把模型的决策能力留给真正需要判断的地方。

什么时候该上框架

上面这套骨架,几十行代码就能跑起来,而且你能完全掌控每一步。什么时候需要引入编排框架(LangGraph、Temporal 之类)?

  • 步骤图是动态的、有环的,且分支复杂到手工 while 难以维护;
  • 需要持久化:任务跑一半进程挂了,重启后要从断点继续;
  • 需要跨进程 / 跨机器的调度与可观测性。

如果只是“串行几步 + 偶尔并行 + 步数上限”,手写循环更透明。框架解决的是状态持久化和复杂拓扑,不是“让 Agent 更聪明”。先用最小骨架跑通,撞到上述边界再换工具,是更省事的路。

截至 2026-09-12,主流 SDK(OpenAI、Anthropic、Google)都内建了工具调用的循环辅助,但编排控制流仍然由你负责——SDK 帮你发请求和解析工具调用,不帮你决定并行与终止。

延伸阅读

  • OpenAI Function Calling 指南:讲清工具调用的请求/响应结构,是编排层解析 tool_calls 与回灌结果的依据。
  • Anthropic Tool use:另一家主流实现,对照阅读能看清“工具调用协议”里哪些是通用约定、哪些是厂商细节。
  • Anthropic《Building effective agents》:把编排模式归纳为 prompt chaining、routing、parallelization 等几类,本文的串行/并行判断规则可以对照它的分类理解。
  • JSON Schema 官方文档:工具参数与输出的校验基础,决定你的中间结果能否被程序可靠消费。

站内搜索

    搜索为本站本地索引增强;正文浏览不依赖 JavaScript。