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 都需要终止条件,否则就是死循环——只不过这里的死循环会烧钱。
终止条件应该同时设三条,任一满足即停:
- 模型给出最终答案:这一轮没有工具调用,只有文本回复。这是正常出口。
- 达到最大步数:
steps >= maxSteps,比如 10 或 15。这是防跑飞的硬闸。 - 无可用工具 / 参数校验连续失败:模型反复调用一个不存在或参数错误的工具,说明它卡住了,继续循环只是浪费。
一个最小骨架长这样:
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}是好拆分——每一环的输出形状确定,能被下一环直接吃下。
按这个标准,拆分时问自己三个问题:
- 这一步的输出,下一个工具能直接当输入用吗? 不能,就说明该在中间补一个“结构化”步骤,或者让这个工具返回结构化数据。
- 这一步失败时,能不能单独重试? 能单独重试的步骤,边界通常划对了。
- 模型需要在这两个工具之间做选择吗? 如果永远是 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 官方文档:工具参数与输出的校验基础,决定你的中间结果能否被程序可靠消费。