☰
一次并发 20 个工具调用:跑最快的那条路丢了 8 个结果
2026/10/8 11:44:34 网站建设 项目流程

版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。

摘要:2026 年 10 月,主流平台的函数调用文档都写明了「模型可能在一轮里调用多个工具」,也提供了parallel_tool_calls这类开关。本文不讨论该不该开,只算一笔并发账:同样是 20 个工具调用,一把全发与贴着上限分批发,谁的耗时更短、谁的返回结果是残缺的。

一、先把结论摆出来:并发全开看着最快,交回来的却可能是残缺结果

把并发开到最大,往往不是最快的做法——它只是最先返回,代价是把一部分调用直接打成了失败,等着重试去补。

我按「平台允许 4 路并发」建模,跑了 20 个互不依赖的工具调用:

发起方式耗时限流次数丢失调用拿到结果
一把全发(重试 3 次)1.26s38812 / 20
贴着上限分批(宽度 4)2.09s0020 / 20
逐个串行6.32s0020 / 20

单看耗时,一把全发最快,比守上限快近一倍。但同一行里还藏着两个数:38 次限流、8 个调用彻底丢失。

「并发越多越快」这句话的漏洞在于:它把「快」定义成了「先返回」,而不是「把活干完」。1.26 秒处理完 12 件事,并不比 2.09 秒处理完 20 件事更划算——如果那 8 件恰好是你真正需要的结果,快就是个假象。

本文给三条能核对的结论:并发宽度应当贴着平台上限设;并发返回的顺序不等于发起顺序,结果必须按标记回填;以及在什么规模下,老老实实串行其实够用。

二、测试条件先钉死:一个 4 路并发的网关,两种发起方式

先说清测的是什么,因为这决定了结论能用在哪儿。

被测对象是一个按「并发上限 4」建模的模拟网关:在途请求达到上限时,它直接返回限流错误,不替你排队。这一点和真实平台一致——官方文档把超限的返回写得很明确:

429 | rate_limit_error | slow_down | Your request rate increased too quickly. | FollowRetry-Afterwhen it’s present, reduce your request rate, and then increase it gradually.

任务是一次发起 20 个互不依赖的工具调用,单次处理耗时在 0.10~0.50 秒之间浮动。真实的工具本来就耗时不齐,把它设成固定值反而看不出问题。

发起方式说明是否尊重并发上限
逐个串行一个调用完成再发下一个是(峰值并发 1)
一把全发20 个同时发起,被限流就等一会儿再试,重试上限 3 次否
贴着上限分批一批只发 4 个,整批回来再发下一批是

口径:「耗时」是墙上时间(wall time);「限流次数」是网关返回限流错误的次数;「丢失」指重试用尽仍未成功的调用数。本文的网关是按并发上限建模的模拟对象,不是接入真实平台;它用来比较三种发起方式的相对关系,绝对耗时会随真实延迟变化。

2.1 为什么用「拒绝」而不是「排队」建模

如果网关会自动排队,那把并发开到多大都无所谓——反正它会替你限速。真实平台不是这样:超限就直接回错误,排队这件事得你自己做。分批发起,本质上就是你在客户端自己实现一个队列;而一把全发,等于把这个队列取消了,把压力全甩给平台,平台能做的只有拒绝。这才是三种策略会产生差异的根源。

2.2 一个只影响结论、不影响事实的前提

本文用的是模拟网关,不是真实接口。这样选的目的是把「并发控制」这一个变量单独拎出来看——真实环境里,网络抖动、工具自身超时、平台限额变化会混在一起,很难判断到底是哪个环节出的问题。模拟测的是机制,不是性能指标:数值本身没有意义,三种方式之间的倍数关系才是结论。

三、8 个调用看不出差别,20 个调用时才露馅

🧪 实测环境:Python 3.13.12 / macOS / 仅标准库(asyncio、random、time)

importasyncioimportrandomimporttime LIMIT=4# 平台允许的并发上限rng=random.Random(11)classGateway:"""按并发上限建模的网关:在途请求达到上限时直接返回限流错误。"""def__init__(self,limit):self.limit=limit self.inflight=0self.peak=0self.rejected=0self.done=[]# 完成顺序asyncdefcall(self,i,lat):ifself.inflight>=self.limit:self.rejected+=1return(i,429)self.inflight+=1self.peak=max(self.peak,self.inflight)awaitasyncio.sleep(lat)self.inflight-=1self.done.append(i)return(i,200)deflatencies(n):return[round(rng.uniform(0.10,0.50),2)for_inrange(n)]asyncdefserial(gw,lats):"""逐个串行。"""return[awaitgw.call(i,l)fori,linenumerate(lats)]asyncdefnaive(gw,lats,retries=3):"""一把全发:所有调用同时发起,被限流就等一会儿重试。"""asyncdefone(i,l):for_inrange(retries):r=awaitgw.call(i,l)ifr[1]==200:return(i,200)awaitasyncio.sleep(l)return(i,None)# 重试用尽,这次调用丢了returnawaitasyncio.gather(*(one(i,l)fori,linenumerate(lats)))asyncdefsized(gw,lats,width):"""贴着上限分批发:一批全部回来再发下一批。"""idx=list(enumerate(lats))out=[]forsinrange(0,len(idx),width):out+=awaitasyncio.gather(*(gw.call(i,l)fori,linidx[s:s+width]))returnout

运行结果原样贴出:

【本轮 8 个工具调用】 策略 N 耗时 峰值 被拒 丢失 -------------------------------------------------------------- 串行 8 2.47s 1 0 0 一把全发(重试3) 8 0.84s 4 5 0 守上限 宽度=4 8 0.80s 4 0 0 守上限 宽度=2 8 1.29s 2 0 0 【本轮 20 个工具调用】 策略 N 耗时 峰值 被拒 丢失 -------------------------------------------------------------- 串行 20 6.32s 1 0 0 一把全发(重试3) 20 1.26s 4 38 8 守上限 宽度=4 20 2.09s 4 0 0 守上限 宽度=2 20 3.35s 2 0 0

三行读法:

  1. 8 个调用时,一把全发和守上限几乎一样快(0.84 秒 对 0.80 秒),差别只是多了 5 次限流。规模小的时候,这个坑不容易被发现——这也正是它常被留到线上才炸的原因。
  2. 20 个调用时,一把全发仍然最快,但丢了 8 个结果。它用 1.26 秒交了 12 份活,另一条路用 2.09 秒交了 20 份。
  3. 串行并不是最差的兜底:宽度 2 用 3.35 秒,串行要 6.32 秒。真正拖慢整体的是串行,不是「并发低一点」。

这里有个反直觉的地方值得点出:丢调用的那一行,恰恰是耗时最短的那一行。如果监控只看「平均耗时」,你看到的是一条漂亮的曲线;只有把「丢失数」也画上去,才知道代价付在了哪里。

本节的资料:把函数调用、并发与重试的官方文档整理成了一份核对清单,另外也放了 LangChain + LangGraph 的实战视频和一份大模型学习路线图。扫码即可获取:

四、第二层坑更隐蔽:并行返回的顺序不是发起顺序

并发发起时,结果按「谁先完成」回来,而不是按你发起的顺序。

🧪 实测环境:同上,只把单次调用耗时的随机分布固定下来

=== 返回顺序:并行时按「谁先完成」回来,不是按发起顺序 === 各调用耗时: [0.4, 0.48, 0.18, 0.48, 0.45, 0.34, 0.27, 0.14] 完成顺序 : [2, 0, 1, 3, 7, 6, 5, 4] 发起顺序 : [0, 1, 2, 3, 4, 5, 6, 7] 是否一致 : False

8 个调用的耗时各不相同(0.14~0.48 秒),完成顺序是[2, 0, 1, 3, 7, 6, 5, 4],和发起顺序完全对不上。

这为什么危险?因为调用方很容易按位置取结果。比如你同时发了「查天气」和「查汇率」,第一个回来的其实是查汇率;如果你按下标 0 取,就会把汇率当成天气填进提示词。这类错误不会抛异常,只会让模型拿到错的输入,再给出一段看起来正常的答案。排查的时候,模型那边是干净的,错在你这里。

正确的做法只有一条:每个调用带一个唯一标记——一般用「工具名 + 参数哈希」或一个自增序号——结果回来后按标记回填,位置只用来展示,不用来取数。这条规则在并发数只有 2 的时候看不出价值,一旦并发上到两位数,它就是唯一能保证结果不错配的办法。

还有一条官方提醒,直接决定了「一把全发 + 立即重试」为什么是负和:

unsuccessful requests contribute to your per-minute limit, so continuously resending a request won’t work.

翻成人话:失败的请求照样占配额。所以「全发 → 被限流 → 立刻重试」这条路,是在用自己的配额反复抽自己。官方给的替代做法是遵循Retry-After;没有这个字段时,退避并加一点随机抖动:

Treat this value as a minimum: wait at least that long and add a small random delay so multiple clients don’t retry at the same time.

本篇涉及的官方文档与示例:把并发、限流、重试这几份材料与代码示例整理进了资料包,配合视频课看更顺。扫码即可获取:

五、并发宽度怎么定:贴着上限设,并且给每个结果打标记

把上面两段实测收敛成一张能直接对号入座的表:

你的情况做法理由
调用 3~5 个,且都很快逐个串行就够并发收益会被协调成本吃掉,实测 8 个以内三种方式差异很小
调用数明显超过并发上限贴着上限分批,宽度 = 平台上限实测宽度 4 用 2.09 秒拿全结果,宽度 2 要 3.35 秒
一次要发几十个调用先分批,再对失败项单独退避重试一把全发实测丢失 8 个调用,靠加大重试次数补不回来
结果要拼进同一段上下文必须按唯一标记回填完成顺序与发起顺序不一致,按下标取会错配

什么时候该停手:先看「丢失」这一列。只要它不是 0,就说明你的并发宽度已经超过平台上限,此时再多试几次也不会变好——应当降低宽度,而不是加大重试次数。判断标准很具体:当宽度调到平台上限、丢失数归零之后,继续往上加只会增加限流次数而不会减少耗时,就停在这个宽度。

最后一条容易被忽略的经验:并发宽度别写死,让它跟着上限走。平台上限会变——不同模型、不同账户等级都不一样——把宽度硬编码成 8 或 16,换一个环境就会重新踩一遍坑。把上限做成配置项,宽度从它推导出来,才是能跨环境复用的做法。

附表 A:本文引用事实与出处对照表

事实出处本文位置
「The model may choose to call multiple functions in a single turn.」《Function calling》;OpenAI;https://developers.openai.com/api/docs/guides/function-calling摘要 / 第 1 章
parallel_tool_calls设为false可确保「exactly zero or one tool is called」同上第 2 章
「Keep the number of initially available functions small for higher accuracy…Aim for fewer than 20 functions available at the start of a turn」同上第 5 章
超限返回429 / rate_limit_error / slow_down,需遵循Retry-After并逐步降低请求速率《Rate limits》;OpenAI;https://developers.openai.com/api/docs/guides/rate-limits第 2 章
「unsuccessful requests contribute to your per-minute limit, so continuously resending a request won’t work.」同上第 4 章
「Treat this value as a minimum: wait at least that long and add a small random delay」同上第 4 章
20 个调用下三种发起方式的耗时 / 限流次数 / 丢失数;完成顺序与发起顺序不一致本文实测,脚本见第 3 章第 3、4 章

表下口径:本文实测的耗时是在按并发上限 4 建模的模拟网关上得到的,不是接入真实平台的账单;它用来比较三种发起方式的相对关系,绝对耗时取决于真实调用延迟与账户限额。


写在最后:这篇用到的资料

写这篇文章时,把并发控制、限流与重试的官方材料又翻了一遍,顺手也整理了几份配套的东西:

  • 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
  • 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
  • AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
  • 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
  • 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多

资料是我自己整理的,放在下面这个码上,扫码即可获取:






添加时备注「AI」,优先通过。

资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询