首页 / 资讯中心 / 文章详情

并行工具调用:一次返回三个调用,执行顺序谁来保证

并行工具调用:一次返回三个调用,执行顺序谁来保证 ★ FEATURED ARTICLE
版权与内容来源声明本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容均在附表 A 中标注来源引用官方原文保持原样不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式也不对任何收益结果作承诺。转载请注明出处。一、三个调用、两条记录、零条报错先看一个虚构但每天都在发生的场景。某团队给客服做了一套下单助手用户说一句「帮我下这单券也用上」模型在一个回合里返回了三个工具调用建订单、扣库存、核销优惠券。客户端把三个调用并发丢进线程池三个都返回成功客服界面弹出「已为你下单」。当晚对账三张表对不上。订单表里有那笔单金额、商品、用户都对。库存表里商品可售数量从 10 变成 9看起来也扣了——但另一笔订单在同一分钟也把它写成了 9两笔只扣掉一件货盘点时账面比实货多一件。优惠券表里同一张券有两条核销流水同一个用户、同一个订单号额度扣了两次。而日志里是这样的三条success时间戳挤在同一毫秒区间没有异常栈没有重试记录没有告警。调用返回对应业务表的实际状态是否报错建订单成功订单已落库状态正常否扣库存成功基于下单前的快照写回与他单互相覆盖否核销优惠券成功同一张券产生两条核销流水否这张表最该盯的是最后一列。三个调用没有一个失败就没有任何一个环节会触发告警系统是「成功了一半」的——订单成了库存没真扣券扣了两次。三条链路各自自洽只有把三张表按订单号关联起来才看得出矛盾。这就是最危险的地方它不给你失败的信号只给你一个不一致的结果。失败会被重试机制接住不一致只会被下一次盘点接住。很多人第一反应是「模型太笨不知道这三个有先后」。这个判断不准确。它知道这是三个不同的动作但它不知道三件你系统内部的事扣库存读的是从库、券接口对重复请求没有去重、客户端超时后会重试。这三件事都在你的代码里不在它的上下文里。二、协议层只承诺「同一轮」不承诺「先后」和「一起成功」先看一次响应到底交出了什么。模型的一次回复是一个有序的内容块列表文本块和工具调用块可以混排。每个调用块带一个唯一标识、一个工具名和一组参数。客户端执行完把结果按标识逐个配回去连同对话再发一次请求。这个循环是所有工具调用框架的公共骨架。这个结构里被承诺的只有三件归属同属一轮标识唯一结果一一回填、参数完整返回即齐、可追溯标识可用于对日志。没有被承诺的是另外两件。第一件是顺序。响应里三个调用块的排列本质上是生成顺序——模型一个 token 一个 token 往外写写在前面不等于该先执行。协议里没有一句话规定「客户端必须按数组下标依次执行」也没有一句话规定「不得并发执行」。它只说了这三个属于这一轮轮内怎么跑是空白。没有被承诺的第二件是原子性。不存在「这一批要么全生效要么全不生效」。每个调用是独立请求打到独立服务各自成功各自失败副作用立刻落地。协议层没有跨调用的事务也没有跨调用的隔离级别两个调用改同一行数据时它不知道也不会替你加锁。流传很广的一个误区在提示里写「请严格按照顺序依次调用工具」。这约束的是生成顺序管不到执行顺序。即便模型老老实实按顺序写出三个调用块客户端并发提交执行仍然是乱的。你写的是建议不是契约而契约在执行器那一侧。还有一个更隐蔽的点是这个坑真正的入口。模型判断「这几个可以并行」的依据是参数之间有没有引用关系。如果第二个调用的参数要填第一个的返回值比如把订单号填进核销请求模型通常不会并行——它得先拿到那个值。真正危险的是语义上有强依赖、参数上却看不出依赖的组合扣库存只需要商品编号和数量不需要订单号按商品扣。核销优惠券只需要用户和券号不需要订单号按用户核。建订单只需要用户、商品、金额。三个调用参数零引用模型并行返回合理客户端并发执行也合理而业务上的因果链是硬的。参数依赖只是依赖的一种另一种叫状态依赖藏在服务端模型看不见。三、框架层并发还是串行开关不在模型手里第二个问题谁决定并发。答案很干脆——客户端的执行策略也就是你自己那几十行循环。客户端拿到几个调用之后常见的三条路执行策略代码大致长什么样什么场景合适顺序被打乱时会不会报错全串行按列表顺序 for 循环逐个 await写操作多、依赖链不清、调试期不会顺序天然正确全并发一次性提交到线程池或协程组等全部返回全是只读、彼此独立同时查三个库不会且结果可能互相覆盖有界并发信号量限制同时在飞的数量仍不排序只读为主、下游有承载上限不会只是乱得更温和三条路都可以塞进同一个框架用配置切换甚至按工具名分流只读走并发、写操作走串行。关键在于模型既看不到你的执行器也没有接口告诉它「我刚才是并发跑的」。它下一轮只看到三个结果块会认为世界是按它设想的顺序演进的。关于「默认并发还是串行」「有没有开关能关掉」我不写具体参数名。这类细节在不同框架、不同版本之间差异很大同一框架里只读与写还可能走不同分支截至 2026-10-23我没能直连到可以逐字引用的官方 reference凭记忆写参数名只会误导。附表 A 里如实记了这一条。不写参数名不代表没法判断。在你自己的执行循环里打点就行同一轮里如果几个调用的「提交时间」落在彼此的「完成时间」之前即区间有重叠那就是并发。这个方法在你机器上一定跑得出确定答案比翻文档可靠。框架偏爱并发是有道理的一轮里三个独立的只读检索串行就是三倍延迟。错不在默认值而在于「同轮调用彼此独立」这条假设在读操作上成立、在写操作上不成立框架本身分不清这两者。所以能落地的做法只有一句在你客户端的工具执行策略里把写操作显式改成串行。改的是你自己的代码不依赖厂商是否提供开关。如果连这个都改不了比如执行器封装在别人的 SDK 里那就退到下一节——不追求顺序追求乱序也不坏。四、业务层顺序只能靠显式依赖换回来真正的顺序不来自模型的自觉也不来自框架的善意只来自你在代码里写下来的显式依赖。三条路各有各的代价。4.1 拆轮次把「有依赖」翻译成「下一轮」做法很土但有效给每个工具打上读/写标签同一轮里如果出现多于一个写调用客户端只执行第一个其余不执行回一条结果块说明「其余调用已延后请基于最新结果在下一轮重新发起」。模型下一轮重新生成时已经看到了扣库存的真实返回值参数自然就变了。本质是用轮次边界换顺序确定性不动协议、不动下游服务改动量最小。代价是轮次变多。原来一轮跑完的事现在要两到三轮延迟和 token 都涨。副作用是模型不一定懂自己为什么被拒可能原样再发一次形成「提交—被拒—再提交」的循环所以拒绝信息必须写清原因和下一步不能只说「不允许」。延迟上涨和循环重试都能从监控上看出来算是不幸中的幸运。4.2 编排图把顺序从模型手里拿走如果这条业务链是稳定的下单必然要扣库存、必然要核销就不该让模型每一轮重新决定顺序。做法是把流程写成一张有向图节点是操作边是依赖模型只负责从对话里抽参数、填到节点上调度器按拓扑序执行。顺序由边决定不由输出顺序决定。模型这一轮返回三个还是一个图的形状不变。扣库存与核销券都只依赖建订单、彼此没有边就该同层并行。代价是灵活性下降。图外的诉求用户临时说「先别扣库存」要改图才能支持而图越细越像硬编码改到最后你会发现「那不如直接写脚本」。边界靠经验划稳定主干进图分支例外留给模型。失败时的现象反而好图写错了错误是确定性的、可复现的第一次跑就暴露不会等到盘点。确定性错误比静默不一致便宜得多。4.3 幂等键与结果校验不追求顺序追求乱序也不坏有些场景既改不了调用方也画不出稳定的图——写操作打在第三方接口上重试和乱序都无法避免。这时唯一能守的是结果让「重复执行」和「乱序执行」都不产生坏账。要做两件事。第一件是幂等键。给每个写操作带一个业务唯一键构成是「业务主体 操作类型 业务单据号」比如「订单 so-1001 的库存扣减」。不能用请求 id 当键——它每次重试都会变等于没做幂等。服务端存「键 → 首次结果」命中就直接返回首次结果不再执行。第二件是后置校验。调用返回后读一次真实状态确认等于期望值更稳的是条件写扣库存时带上版本号「只有当前数量还是 10 才允许写成 9」不匹配就失败重读。这一步防的是另一种病——幂等只防重复不防覆盖。两笔不同的订单各扣一件键不同、都合法但都基于旧快照写回库存仍然只少一件。这就是第一节里「9 而不是 8」的来源。条件怎么判断不满足时会发生什么无跨调用共享状态两个调用是否写同一行、同一键、同一账户后写的覆盖先写的数量对不上参数不引用上游返回值检查参数里有没有上一轮的输出模型不会并行问题被「自然地」绕开幂等同一键重复提交结果是否一致重试变成重复扣款、重复核销有后置条件可校验调用后能否读回期望状态出错了也没有判据只能等对账冲突可检测是否有版本号、条件写、乐观锁丢更新静默发生日志全绿副作用可补偿失败后能否反向操作退单、回补库存只能人工修数据代价是所有写接口都要改造存幂等记录、设过期、定义键。键最容易设计错——少一个维度比如只用用户 券、漏了订单号就把两笔合法请求判成重复第二笔被静默丢弃。这是另一种「成功了一半」比重复更难发现因为现象是「少了一笔」。4.4 三种手段怎么选做法适用场景代价失败时的现象拆轮次依赖关系随对话变化、链路不稳定改动预算小轮次变多延迟与 token 上升模型可能重试同一调用延迟翻倍被当性能问题优化掉模型陷入提交—被拒循环编排图主干流程稳定、写操作多、要求可复现灵活性下降粒度难定图外诉求要改代码图写错则确定性报错首次运行即暴露改图成本累积幂等键 结果校验接口不可改、重试无法避免、需要兜底所有写接口改造键设计与过期策略易错键太粗→合法请求被静默丢弃只做幂等不做校验→丢更新仍在选择顺序先看能不能并发——只读、无共享状态、结果不参与后续判断的尽管并发再看依赖稳不稳定稳定就进图不稳定就拆轮次最后写操作一律配幂等键与后置校验。它不解决顺序但能让顺序错了也不至于变成坏账。五、观测层没有调用 id 和时间戳失败是静默的前面讲怎么不出错这节讲出错之后你怎么知道。单独拎出来是因为这类问题的失败形态太特殊没有异常只有不一致。告警系统监听异常所以它一定不会响。字段从哪来缺了会怎样调用 id协议层给的每个调用块自带结果回填错配或无法把请求与结果对上轮次 id自己生成标识「这是同一次响应的第几批」多轮对话里分不清哪些调用属于同一批追踪 id编排层生成透传到每个下游服务只有客户端日志服务端查不到对应请求提交时间客户端发出请求前无法判断并发不知道谁先出发完成时间客户端收到响应后无法判断重叠不知道区间是否交叉服务端起止下游服务日志靠追踪 id 关联分不清「客户端并发」与「服务端排队」最关键的一个动作很多人没做分别记录「提交顺序」和「完成顺序」存成两个序号。两者一致说明这一轮串行不一致说明发生了并发。这是事后唯一能证明「到底并行没并行」的证据比任何配置都硬。对齐日志的正确姿势是按轮次 id 聚合、按调用 id 定位、按提交时间排因果而不是按打印顺序排。并发之下打印顺序是随机的按它排出来的时间线是假的——三条记录挤在同一毫秒你无法回答「扣库存那一刻订单提交了吗」。这就是「静默」的技术定义不是没有日志是日志里没有能排序的字段。比应用日志更有效的是业务不变量监控按订单号把订单表、库存流水、券核销流水 join 起来数量不相等就告警。它检查结果而不是过程不管并发怎么乱、日志怎么写数据歪了就一定被抓到。这条监控写起来比链路追踪简单建议每个有写操作的 Agent 都配。这份资料是什么一份 AI 大模型学习路线图按阶段标注了 Agent 工程这条线上先学什么、哪些可以跳过。顺序、幂等、可观测属于「框架之外、系统之内」的部分路线图可以帮你判断它该放在哪个阶段补。放在资料包里扫码即可获取六、一个示意实现拓扑分层 同层并发下面这段是示意实现不是任何框架的真实源码纯标准库只说明「依赖图怎么变成执行顺序」。真实调度器还要处理超时、取消、限流、重试和结果校验这里全省了。思路很直白把每个调用的上游依赖列出来每轮挑出「上游都已完成」的作为一层同层用线程池并发层与层之间串行。如果某一轮挑不出任何可执行的调用说明依赖成环或引用了未注册的名字直接抛错。importconcurrent.futuresascfimporttimedefrun_plan(calls,deps):calls: {名字: 函数(上游结果字典)}deps: {名字: [上游名字]}done,layers{},[]pending{k:list(v)fork,vindeps.items()}whilepending:readysorted(nforn,dinpending.items()ifall(xindoneforxind))ifnotready:raiseValueError(依赖成环或引用了未注册的调用: %s%sorted(pending))layers.append(ready)withcf.ThreadPoolExecutor(max_workersmax(1,len(ready)))aspool:futs{pool.submit(calls[n],{d:done[d]fordinpending[n]}):nforninready}forfutincf.as_completed(futs):done[futs[fut]]fut.result()forninready:pending.pop(n)returnlayers,donedefcreate_order(up):time.sleep(0.10)return{order_id:so-1001,qty:1}defdeduct_stock(up):time.sleep(0.10)return{sku:sku-77,left:10-up[create_order][qty]}defredeem_coupon(up):time.sleep(0.10)return{coupon:cp-31,order_id:up[create_order][order_id]}if__name____main__:deps{create_order:[],deduct_stock:[create_order],redeem_coupon:[create_order]}t0time.perf_counter()layers,outrun_plan({create_order:create_order,deduct_stock:deduct_stock,redeem_coupon:redeem_coupon},deps)print(执行层:,layers,| 耗时: %.2fs%(time.perf_counter()-t0))forkin(create_order,deduct_stock,redeem_coupon):print(k,-,out[k])跑法python2026-10-23_并行调用示意.py本机Python 3.13.14的实际输出执行层: [[create_order], [deduct_stock, redeem_coupon]] | 耗时: 0.22s create_order - {order_id: so-1001, qty: 1} deduct_stock - {sku: sku-77, left: 9} redeem_coupon - {coupon: cp-31, order_id: so-1001}两层前层一个调用后层两个并发。三个调用各睡 0.1 秒全串行会在 0.3 秒以上实测 0.2 秒出头同层确实重叠了。该并发的地方并发、该串行的地方串行就是这个样子。这段代码只保证「依赖满足后才执行」不保证重试安全。如果扣库存超时被客户端重试同一个扣减会执行两遍那仍然要靠 4.3 节的幂等键。图解决顺序键解决重复校验解决覆盖三件事各管一段谁也替代不了谁。七、上线前的检查清单与三个边界把前面的内容压成一张可执行的清单按顺序对一遍给每个工具打上读/写标签写操作单独列出来。在执行循环里打点看同轮调用的时间区间是否重叠这是判断并发的硬证据。同轮只允许一个写调用或在编排图里声明依赖。每个写调用带业务幂等键键里必须有业务单据号不能用请求 id。写后做后置校验能上条件写版本号、期望值就别用读后写。日志记全调用 id、轮次 id、追踪 id、两个时间戳以及提交顺序与完成顺序两个序号。加一条业务不变量对账按订单号 join 各表数量不等就告警。然后是三个必须说清的边界避免把上面的做法当成万能药。第一全串行不是答案。一轮里三个独立检索串行只是白白拉长延迟。关键不是「串行比并发安全」而是「有依赖的写操作必须串行」。第二幂等不等于顺序正确。它防重复不防覆盖。两笔合法请求基于同一份旧快照写回键不同、都过幂等检查数据照样错所以幂等必须配后置校验或条件写。第三图不是越细越好。画到每个字段都成一个节点你就把 Agent 写成了脚本那不如直接写脚本。稳定主干进图枝叶留给模型。回到标题那个问题一次返回三个调用执行顺序谁来保证。答案是——模型不保证协议没承诺框架的默认值也不站在你这边。能保证的只有你在客户端写下的那几行显式的依赖、显式的分层、显式的幂等键以及能在事后把三条日志排出先后的字段。顺序从来不是模型的能力问题是工程系统的责任划分问题。这份资料是什么一套 Agent 开发的实战课程目录覆盖工具调用、编排与落地环节。本篇讲的依赖分层与幂等兜底在真实项目里通常和编排框架一起用课程目录可以帮你对照自己缺哪一块。放在资料包里扫码即可获取附表 A本文引用事实与出处对照表编号事实出处本文位置1模型一次响应可包含多个工具调用块每块带唯一标识、工具名与完整参数客户端按标识回填结果连同对话再次请求形成循环多家厂商公开文档的通用描述工具调用/函数调用概览截至 2026-10-23 未能直连官方 reference 逐字核对字段名故正文只描述结构不写字段名第 2、3 章2协议层未对「轮内多个调用的执行顺序」与「跨调用原子性」作出承诺截至 2026-10-23 未能直连官方 reference 核到关于轮内顺序与跨调用事务的规范性表述故按「未承诺」表述未断言任何厂商的具体行为第 2 章3并行与否由客户端执行策略决定模型侧无法指定同上本文改用本机打点法判断时间区间是否重叠给出可验证判据不依赖厂商参数名第 3 章4具体并发开关的参数名与默认值未核验截至 2026-10-23 未能直连官方 reference故全文不列参数名只写「在客户端的工具执行策略里显式改串行」第 3 章5幂等idempotent同一请求重复执行与执行一次效果相同RFC 9110《HTTP Semantics》IETF2022 年对幂等方法的定义第 4.3 节6丢失更新两个写操作基于同一旧值写回后写覆盖先写数据库并发控制的标准概念见《数据库系统概念》事务一章Abraham Silberschatz 等第 1、4.3 节7拓扑排序对有向无环图求线性序使每条边的起点排在终点之前《算法导论》图算法一章Thomas H. Cormen 等第 6 章8ThreadPoolExecutor的提交与as_completed语义Python 官方文档concurrent.futures一节https://docs.python.org/3/library/concurrent.futures.html第 6 章9分层执行结果[[create_order], [deduct_stock, redeem_coupon]]约 0.22 秒Python 3.13.14本机实测work/2026-10-23_并行调用示意.py复跑命令python 2026-10-23_并行调用示意.py第 6 章10订单/库存/优惠券三张表对不上的业务场景虚构示例用于说明失败形态非任何真实系统的事故记录第 1、4 章附表 B术语速查表术语一句话解释在本文哪里用到工具调用块响应里一段结构化内容含唯一标识、工具名、参数代表模型的一个意图第 2 章轮次一次模型响应加上回填结果后再次请求构成一个回合第 2、4.1 节参数依赖后一个调用的参数里引用了前一个的返回值模型能看出来第 2 章状态依赖两个调用在服务端改同一份状态参数上看不出关联模型看不出来第 2 章拆轮次把有依赖的写调用推到下一轮用轮次边界换顺序确定性第 4.1 节编排图把流程写成有向图节点是操作、边是依赖调度器按拓扑序执行第 4.2 节幂等键业务唯一键让重复请求返回首次结果而不重复执行第 4.3 节后置校验写完读回确认真实状态等于期望值第 4.3 节条件写带版本号的写入当前值不匹配就失败用于防覆盖第 4.3 节业务不变量跨表必须成立的数量关系如订单数等于库存扣减数第 5 章写在最后这篇用到的资料写这篇文章时把相关的官方文档和源码又翻了一遍顺手也整理了几份配套的东西大模型学习路线图从零基础到能自己动手做 Agent按阶段说明每一步该学什么、哪些可以先跳过《LangChain LangGraph MCP 智能体开发实战》视频课7 个模块从私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知识库在线可查Agent Skills 从入门到落地、Claude Skills 完全指南等专题按目录浏览即可640 套 AI 大模型行业报告 经典 PDF 书籍看行业落地案例和别人怎么做的时候用得上大模型零基础到精通教学视频跟着敲一遍比只读文档快得多资料是我自己整理的放在下面这个码上扫码即可获取如果这篇对你有用评论区说说你卡在哪一步我按留言再补后续的拆解。
阅读完成 · 觉得有帮助?
咨询建站