Function Calling 批量工具调用:OpenAI 并行 Tool Calls 的并发执行与结果聚合

在接入支持 并行工具调用(Parallel Function Calling) 的主流大模型(如 GPT-4o、Claude 3.5、Qwen2.5)时,当用户提出一个复合型查询需求:
“帮我查一下北京、上海、深圳三个城市明天的天气,并且查询从北京飞往深圳的最早一趟航班信息。”
大模型会在单次推理响应中,直接在 tool_calls 数组中一次性返回 4 个独立的工具调用请求(3 个查天气 + 1 个查航班):
"tool_calls": [
{"id": "call_001", "function": {"name": "get_weather", "arguments": "{\"city\": \"北京\"}"}},
{"id": "call_002", "function": {"name": "get_weather", "arguments": "{\"city\": \"上海\"}"}},
{"id": "call_003", "function": {"name": "get_weather", "arguments": "{\"city\": \"深圳\"}"}},
{"id": "call_004", "function": {"name": "get_flight", "arguments": "{\"origin\": \"北京\", \"dest\": \"深圳\"}"}}
]
在很多缺乏高并发意识的后端实现中,代码依然用 for tool in tool_calls: 进行串行同步执行:
每个外部工具接口耗时 800ms,4 个工具顺序执行下来耗时高达 3.2 秒!如果其中一个接口稍微卡顿,整个用户的响应延迟直接突破 6~8 秒!
今天我们拆解如何利用 Python asyncio.gather 与 Go errgroup 实现并行工具的高性能并发执行、部分超时容错与上下文聚合回传。
一、串行 vs 并发调用的物理时序对比
gantt
title 4 个复合工具调用耗时对比 (ms)
dateFormat X
axisFormat %s ms
section 串行顺序调用
北京天气 (800ms) :a1, 0, 800
上海天气 (750ms) :a2, 800, 1550
深圳天气 (850ms) :a3, 1550, 2400
航班查询 (900ms) :a4, 2400, 3300
section 异步并发调用 (gather)
北京天气 (800ms) :b1, 0, 800
上海天气 (750ms) :b2, 0, 750
深圳天气 (850ms) :b3, 0, 850
航班查询 (900ms) :b4, 0, 900
通过并发执行,端到端总耗时由原本的 $T_1 + T_2 + T_3 + T_4 = 3300\text{ ms}$ 骤降至 $\max(T_1, T_2, T_3, T_4) \approx 900\text{ ms}$,耗时直接压缩 73%!
二、生产级 Python 异步并发调度器实现(带单任务超时与局部容错)
在并发执行多个 Tool 时,最怕的是“木桶效应”:其中某一个第三方工具网络挂起,把其他已经执行成功的工具全部拖下水。
必须为每个 Tool 注入独立的超时看门狗与局部降级机制:
import asyncio
import json
from typing import List, Dict, Any
class ParallelToolDispatcher:
def __init__(self, tool_registry: Dict[str, Any], default_timeout: float = 2.5):
self.registry = tool_registry
self.default_timeout = default_timeout
async def _execute_single_tool(self, tool_call: dict) -> dict:
"""单工具安全执行单元"""
call_id = tool_call["id"]
func_name = tool_call["function"]["name"]
raw_args_str = tool_call["function"]["arguments"]
handler = self.registry.get(func_name)
if not handler:
return {
"tool_call_id": call_id,
"role": "tool",
"name": func_name,
"content": json.dumps({"error": f"Tool {func_name} not registered"})
}
try:
args = json.loads(raw_args_str)
# 单工具注入超时上下文,防止单任务拖死全局
async with asyncio.timeout(self.default_timeout):
# 若 handler 是普通同步函数,放入线程池;若是 async 函数直接 await
if asyncio.iscoroutinefunction(handler):
result = await handler(**args)
else:
result = await asyncio.to_thread(handler, **args)
return {
"tool_call_id": call_id,
"role": "tool",
"name": func_name,
"content": json.dumps({"status": "SUCCESS", "data": result}, ensure_ascii=False)
}
except asyncio.TimeoutError:
return {
"tool_call_id": call_id,
"role": "tool",
"name": func_name,
"content": json.dumps({"status": "DEGRADED", "error": "查询超时,已跳过"})
}
except Exception as e:
return {
"tool_call_id": call_id,
"role": "tool",
"name": func_name,
"content": json.dumps({"status": "ERROR", "error": str(e)})
}
async def dispatch_parallel(self, tool_calls: List[dict]) -> List[dict]:
"""核心:并发分发所有 Tool Calls 并保序返回"""
tasks = [self._execute_single_tool(tc) for tc in tool_calls]
# gather 并行执行,即使个别失败也保证其他结果正常返回
results = await asyncio.gather(*tasks, return_exceptions=False)
return results
三、大模型上下文聚合回传标准协议
并发执行完毕后,必须按照 OpenAI 标准格式,将所有工具的执行结果按照原始的 tool_call_id 一一配对,作为多条连续的 role: "tool" 消息一次性追加进消息列表并回传给大模型:
async def handle_agent_turn(llm_client, messages: list, tools_def: list, dispatcher: ParallelToolDispatcher):
# 1. 第一次向模型请求决策
first_response = await llm_client.chat_completions(messages=messages, tools=tools_def)
choice = first_response["choices"][0]
# 2. 检查是否有并行工具调用返回
tool_calls = choice["message"].get("tool_calls")
if not tool_calls:
return choice["message"]["content"] # 无工具调用,直接输出回答
# 将助手的决策消息(包含 tool_calls)追加进上下文
messages.append(choice["message"])
# 3. 核心:瞬间并发执行所有工具!
tool_results = await dispatcher.dispatch_parallel(tool_calls)
# 4. 将所有执行结果批量追加进消息历史
messages.extend(tool_results)
# 5. 第二次调用模型,让模型基于完整的并发结果生成最终答复
final_response = await llm_client.chat_completions(messages=messages, tools=tools_def)
return final_response["choices"][0]["message"]["content"]
四、生产避坑原则
- 写操作工具限制并发(Mutex 锁):对于“扣款、扣减库存、修改密码”等涉及强一致性事务的只写操作工具,若模型同时发出了针对同一资源的多次修改,工具网关层必须强制串行加锁处理,防止产生脏写与死锁;
- 连接池容量对齐(Max Connections):若允许单个会话并发触发 10 个 Tool,HTTP Client 和数据库连接池的
max_connections必须相应调大,避免瞬间耗尽连接池导致自身被卡住; - 精准统计端到端耗时:将每个子工具的耗时记录在 Trace 日志中,便于精准找出是哪个下游系统拖慢了整体 Agent 的响应速度。
把并行工具调用做到真正的全异步并发与局部容错,智能体在处理多维度复杂任务时才能展现出丝滑极致的响应体验。

513

被折叠的 条评论
为什么被折叠?



