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

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

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.gatherGo 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"]

四、生产避坑原则

  1. 写操作工具限制并发(Mutex 锁):对于“扣款、扣减库存、修改密码”等涉及强一致性事务的只写操作工具,若模型同时发出了针对同一资源的多次修改,工具网关层必须强制串行加锁处理,防止产生脏写与死锁;
  2. 连接池容量对齐(Max Connections):若允许单个会话并发触发 10 个 Tool,HTTP Client 和数据库连接池的 max_connections 必须相应调大,避免瞬间耗尽连接池导致自身被卡住;
  3. 精准统计端到端耗时:将每个子工具的耗时记录在 Trace 日志中,便于精准找出是哪个下游系统拖慢了整体 Agent 的响应速度。

把并行工具调用做到真正的全异步并发与局部容错,智能体在处理多维度复杂任务时才能展现出丝滑极致的响应体验。

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值