18. 并行 Tool Calling 和异步 Tool Calling 在工程上怎么实现?
整理 Parallel / Async Tool Calling 的执行机制、收益与陷阱。
简单回答
并行 Tool Calling 是 LLM 在一次响应里同时输出多个 tool_call,应用层并发执行后把所有结果一起回传,模型再基于聚合结果继续生成。它解决的是"独立工具调用串行等待浪费时间"的问题。异步 Tool Calling 是更进一步——某些工具的执行时间长(比如查 ETL 任务、人工审核),不阻塞 Agent 主循环,工具结果以回调或长轮询的形式异步回来。两者都需要应用层做并发调度、结果聚合、错误处理,模型本身不负责并发——它只负责"决定调什么"和"基于什么结果继续推理"。
详细解释
串行 Tool Calling 的瓶颈
最朴素的 Tool Calling(参见本专题第 03 篇文章)是一次只调一个工具。LLM 输出一个 tool_call → 应用执行 → 结果回传 → LLM 生成下一个 tool_call。每一轮工具调用之间都要等一次 LLM 推理。
这种模式在很多场景下浪费时间。比如用户问"帮我查一下北京、上海、深圳今天的天气",三次天气查询是完全独立的,但串行下要 3 次工具调用 + 3 次 LLM 推理,整体延迟可能要十几秒。再比如做一份简报需要同时查 5 个数据源——每个数据源都和其他无关,串行做完意味着延迟是单次调用的 5 倍累加。
并行 Tool Calling 的工作机制
OpenAI 在 2023 年底引入 Parallel Tool Calling,Anthropic、Google 等也都支持类似机制。流程上的关键变化是:模型在一次响应里可以输出一个 tool_calls 数组,包含多个独立的工具调用。
应用层拿到这个数组后并发执行——可以是 Promise.all、asyncio.gather、Goroutine + WaitGroup 等任何并发原语。三个调用同时发出,等所有调用都返回(或超时)后,把每个结果以独立的 tool message 拼回对话历史,再发起下一次 LLM 推理。
延迟从"3 次串行 LLM + 3 次串行工具"变成"1 次 LLM + 1 次并行工具(取最慢的那个)+ 1 次 LLM"。在工具调用本身耗时大于 LLM 推理的场景下收益尤其明显。
模型怎么决定并行还是串行
并行 Tool Calling 不是模型每次都该用——只有当多个调用相互独立时才适合并行。如果第二步依赖第一步的结果("先查这个用户的订单 ID,再用订单 ID 查物流"),就必须串行。
判断由模型自己做。模型基于 Prompt 和工具描述里的语义判断"这些调用之间有没有数据依赖"。但这个判断不总是准确——有时候模型会把本该串行的任务并行下发(比如把"查用户订单 → 查物流"两步同时输出,第二步的订单 ID 字段是模型猜的)。
工程上对此的兜底是:在工具的参数 schema 中尽量用强类型(订单 ID 写成 format: "order-id" 或带正则约束),让模型猜不出来时只能先调第一个工具拿真实值。或者在 Prompt 中明确说"以下两个工具有依赖关系,必须先调 A 拿到 X 再调 B"。
异步 Tool Calling:长时任务怎么办
并行解决的是"多个短任务能不能同时跑",异步解决的是"单个任务时间太长怎么办"。
有些工具的执行时间不可控——触发一个 ETL 任务可能要 10 分钟才能出结果,发起一次人工审核可能要小时级才能拿到反馈。让 Agent 主循环阻塞等这种工具明显不合理:用户连接会超时、HTTP 请求会被中断、模型上下文也会被无意义地占着。
工程上常见的设计是把这种工具拆成两部分:一个"启动"工具立刻返回 task_id,一个"查询"工具用 task_id 拉结果(或者用 webhook 主动通知)。Agent 调启动工具后可以先做别的事或先回复用户"已经在处理",过一段时间再回来调查询工具拉结果。
更先进一些的设计是本专题第 05 篇文章里讲的状态化 Agent——把整个会话的状态(包括 pending 的异步任务)持久化下来,后台轮询或回调触发时唤醒 Agent 继续推进。这种架构更接近真正的"长时运行 Agent",OpenAI 的 Assistants API、各种 Agent 框架(LangGraph、Temporal)都在朝这个方向走。
工程陷阱
结果回传顺序。 多个 tool_call 并发执行时,谁先完成不一定。工程上必须保证回传给 LLM 的 tool message 是用 tool_call_id 配对的,而不是依赖顺序。否则模型会拿错结果做后续推理。
部分失败处理。 三个并行调用里第二个失败了怎么办?常见的两种策略是"快速失败"(只要一个失败就整体放弃)和"部分回传"(把成功的结果和失败的错误信息一起回给模型,让它决定要不要重试)。后者更灵活但需要在 Prompt 里教模型"看到工具错误时怎么处理"。
超时和限流。 并发调用容易把下游打爆。需要在并发执行层加 max_concurrency 限制、超时控制、以及每个工具的限流配置。生产环境下经常遇到的是 LLM 一口气下发了 10 个并行调用,把内部 API 的限流配额瞬间打满。
Token 计费的隐藏成本。 多个并行 tool_call 各自的结果都会拼回对话历史,下一次 LLM 推理的输入 token 会显著膨胀。如果工具返回的内容很大(比如 5 个并行的搜索结果各几 KB),下一次推理的成本可能比省下的延迟更值得关注。
和 Multi-Agent 的边界
并行 Tool Calling 容易和本专题第 07 篇文章里的 Multi-Agent 协作混淆。两者的关键区别是"并发的实体是谁"。并行 Tool Calling 是同一个 Agent 在一次决策里下发多个独立工具,决策权和上下文都在主 Agent 这里。Multi-Agent 是多个有独立上下文和独立决策的 Agent 协作,每个 sub-agent 有自己的 ReAct 循环。
实际项目里,能用并行 Tool Calling 解决的问题就不要上 Multi-Agent——并行 Tool Calling 工程复杂度低一个数量级,调试也容易得多。
面试时可以这样答
并行 Tool Calling 是模型在一次响应里输出 tool_calls 数组,多个独立调用同时发出去,应用层用
Promise.all或asyncio.gather这类并发原语执行,所有结果以独立 tool message 一起回传,模型再基于聚合结果继续推理。本质是把"多次串行 LLM + 多次串行工具"变成"一次 LLM + 一次并行工具",在多个工具调用相互独立时延迟收益很大。但模型判断"该不该并行"不总是准确——遇到有数据依赖的调用可能也会被误并行下发。兜底手段是在参数 schema 上加强约束让模型没法瞎猜参数,或者在 Prompt 里显式说哪些工具有依赖关系。
异步 Tool Calling 解决的是另一个问题——单个工具执行时间太长不能阻塞主循环。常见做法是把工具拆成"启动"和"查询"两个,启动后立刻返回 task_id,过一段时间再调查询工具拿结果。再进一步就是状态化 Agent,把整个会话状态持久化,回调或轮询触发时唤醒继续推。
工程上的几个坑:tool_call_id 配对一定不能依赖顺序、部分失败要决定是整体放弃还是回传错误让模型自处理、并发要加超时和限流避免打爆下游、并行结果膨胀的 token 成本要关注。
常见追问
- 模型把本该串行的工具调用并行下发了,怎么检测和处理?
- 异步 Tool Calling 下用户体验怎么设计?是阻塞等还是先回复"在处理中"?
- 并行 Tool Calling 配合 streaming 输出有什么特殊处理?