Parallel Function Calls(并行函数调用)是大模型工具调用能力的一次重要升级。在早期的Function Calling实现中,模型一次推理只能输出一个工具调用请求,客户端执行完毕并把结果喂回模型后,模型才能决定下一步动作。如果任务需要查询多个互不依赖的数据源,比如同时查三个城市的天气,串行模式会浪费大量等待时间。并行调用机制让模型在一次输出中直接生成多个工具调用,客户端可以并发执行这些调用,整体延迟大幅降低。本文将围绕并行工具调用的原理、API用法和工程实践展开,帮你真正把这个能力用好。

一、并行工具调用的底层原理与工作机制
要理解并行调用,先要看清楚模型是怎么输出工具调用请求的。当对话中携带了工具定义时,模型并不真正执行任何函数,它做的只是根据用户意图生成一段结构化输出,标明想调用哪个函数、参数是什么。真正执行函数的是调用方(你的应用代码),执行完再把结果以tool消息的形式追加到对话历史中。这个协作模式决定了:只要模型的输出格式支持在一次回复里塞进多个调用块,并行就有了基础。
在串行模式下,模型的输出只包含一个tool_calls条目。客户端执行后回传结果,模型进入下一轮推理,再输出下一个调用。假设每次工具执行需要500毫秒,模型生成每次调用需要800毫秒,三个独立调用串行走完就需要接近4秒。而并行模式下,模型一次生成三个tool_calls条目,客户端用线程池或异步IO同时执行三个工具,总耗时约等于最慢那一个工具的执行时间加上模型两轮推理时间,通常能压缩到一半以下。
需要特别注意的是,并行不等于模型会自己并发执行。模型只负责“声明”要调用什么,执行层面的并发完全由你的客户端代码控制。如果你拿到三个调用请求后仍然写了个for循环逐个执行,那并行调用带来的收益就只剩下减少了模型推理轮次,工具执行阶段的延迟一点没省。这是很多人接入后没感觉到提速的首要原因。
另一个容易忽略的细节是依赖关系判断。模型在生成多个调用时,会尽量判断这些调用之间是否存在参数依赖。比如“先查北京天气,再根据天气推荐穿搭”这种场景,第二个调用的参数依赖第一个调用的结果,模型通常会拆成多轮串行输出。而“查北京、上海、广州的天气”这种互不依赖的请求,才会触发并行输出。理解这一点有助于你设计工具接口,尽量让模型能看清调用之间的独立性。
二、主流API的parallel_tool_calls支持与代码实现
OpenAI在API层面提供了parallel_tool_calls参数,默认值为true。当开启时,模型可能返回包含多个条目的tool_calls数组。以下是一个完整的Python实现示例,包含工具定义、并发执行和结果回传三个环节。
import json
import asyncio
from openai import OpenAI
client = OpenAI()
# 定义两个互不依赖的工具
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "get_exchange_rate",
"description": "查询指定货币对的汇率",
"parameters": {
"type": "object",
"properties": {
"base": {"type": "string"},
"target": {"type": "string"}
},
"required": ["base", "target"]
}
}
}
]
# 本地工具实现,模拟耗时操作
async def get_weather(city: str) -> str:
await asyncio.sleep(0.5) # 模拟网络请求
return json.dumps({"city": city, "temp": "22", "condition": "晴"}, ensure_ascii=False)
async def get_exchange_rate(base: str, target: str) -> str:
await asyncio.sleep(0.5)
return json.dumps({"base": base, "target": target, "rate": "7.24"}, ensure_ascii=False)
async def run_agent(user_message: str):
messages = [{"role": "user", "content": user_message}]
for _ in range(5): # 限制最大轮次防止死循环
resp = await client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
parallel_tool_calls=True # 显式开启并行调用
)
msg = resp.choices[0].message
messages.append(msg)
if not msg.tool_calls:
return msg.content # 没有工具调用,返回最终回答
# 关键:用asyncio.gather并发执行所有工具调用
async def execute_one(tc):
fn = {"get_weather": get_weather,
"get_exchange_rate": get_exchange_rate}[tc.function.name]
args = json.loads(tc.function.arguments)
return await fn(**args)
results = await asyncio.gather(
*[execute_one(tc) for tc in msg.tool_calls]
)
# 按顺序回传每个调用的结果,tool_call_id必须对应
for tc, result in zip(msg.tool_calls, results):
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
# 测试:两个独立查询应被并行调用
if __name__ == "__main__":
answer = asyncio.run(run_agent("帮我查一下杭州的天气,顺便看看美元兑人民币汇率"))
print(answer)
这段代码有几个关键点值得展开。第一,asyncio.gather是并发的核心,如果改成逐个await,就退化成了串行执行。第二,回传结果时tool_call_id必须与模型返回的调用ID一一对应,顺序乱了或漏了都会导致API报400错误。第三,结果回传数量必须等于tool_calls数量,即使某个工具执行失败,也要回传一个包含错误信息的字符串,不能直接跳过,否则模型无法继续推理。
除了OpenAI,其他主流框架也跟进了这个能力。LangChain的0.2版本之后,AgentExecutor默认支持并行工具执行,底层通过ThreadPoolExecutor并发执行各工具;Anthropic的Claude API会在响应中返回多个tool_use内容块,客户端负责并发执行。各家API的结构略有差异,但整体协作模式一致:模型生成多个调用请求,客户端并发执行并统一回传。
三、串行与并行的性能对比及常见坑
我们来算一笔性能账。假设模型每次推理输出耗时1秒,每个工具执行耗时800毫秒。串行模式下完成三个独立工具调用需要:三次模型推理(3秒)加三次工具执行(2.4秒),总计约5.4秒。并行模式下:两次模型推理(第一轮生成三个调用,第二轮基于结果生成回答,共2秒)加一轮并发的工具执行(约0.8秒,取决于最慢的工具),总计约2.8秒,提速接近一半。工具执行耗时越长、调用数量越多,并行收益越明显。
实际接入中有几个高频问题需要留意。首先是缓存击穿问题:并行调用会在同一瞬间打出多个外部请求,如果目标服务有速率限制,很容易触发限流。建议在客户端加信号量控制并发上限,例如用asyncio.Semaphore(5)限制最多同时五个请求。其次是幂等性要求:并行执行放大了超时重试的影响,如果工具接口不是幂等的,重复调用可能产生脏数据。
# 用信号量控制并发上限,避免触发下游限流
sem = asyncio.Semaphore(5)
async def execute_one(tc):
async with sem:
fn = TOOL_MAP[tc.function.name]
args = json.loads(tc.function.arguments)
try:
return await fn(**args)
except Exception as e:
# 失败也要回传,让模型感知到错误
return json.dumps({"error": str(e)}, ensure_ascii=False)
第三个坑是模型幻觉出不存在的参数组合。开启并行后,模型偶尔会为同一个函数生成多次调用但参数重复或残缺,建议在执行前做参数校验,缺必填参数时直接回传错误信息让模型自纠。第四个坑是强行并行的风险:有些业务场景要求调用顺序严格,比如先扣库存再创建订单,这时应该显式设置parallel_tool_calls为false,并在工具描述中写清楚依赖关系,避免模型擅自并行导致业务逻辑错乱。
最后谈谈适用边界的判断。并行调用适合的是多个互不依赖、以数据查询为主的工具,典型场景包括多源信息聚合、批量检索、多维度监控查询等。而当调用之间存在数据依赖、状态变更或顺序敏感的操作时,串行反而更安全。工程实践中比较稳妥的做法是:默认开启并行,在工具的description字段里明确标注哪些函数必须串行,让模型自己学会区分。通过合理设计工具接口和控制并发策略,并行调用能把多工具场景的整体延迟压缩30%到60%,是构建高性能Agent应用不可或缺的一环。
Parallel Function Calls并行工具调用大模型推理加速修改时间:2026-09-16 22:26:50