一步发多个工具:多 tool_call 的格式

发布时间:2026/9/30 3:56:43

一步发多个工具:多 tool_call 的格式 系列第四篇,源码密集篇。经典 ReAct 一次只能做一个动作,而函数调用式可以一步同时发多个工具。这篇看这些工具调用的流式碎片如何靠index归位、如何并发执行、如何回填历史。源码来自 LoopAgent ⭐。为什么要一步多工具回到登录超时的例子。有时候模型的最优策略是同时搜两个关键词:“login” 和 “session” 分头查,不用等第一个搜完再搜第二个。经典文本范式的Action:天生一次一个,而函数调用式的tool_calls本质是一个数组,天然支持一步多个。多个 tool_call 的格式,要分四个层面看——它们是同一批数据在不同阶段的形态。层面一:线上流式格式(wire format)模型不会一次性吐出完整的多个 tool_call,而是逐字符流式下发。LoopAgent 把每个流片段抽象成toolCallDelta事件(types.ts:65-71):|{type:toolCallDelta;index:number;// ★ 关键:第几个 tool_call(0,1,2...)id?:string;// 调用 id,只在该 call 的首片段出现name?:string;// 函数名,通常只在首片段出现argumentsDelta?:string;// JSON 参数的一小段,会分多次到达}✶ Insight ─────────────────────────────────────index是区分多个 tool_call 的唯一凭据。底层 SSE 流把 N 个 tool_call 的碎片交错发下来——index:0的名字、index:1的名字、index:0的参数片段、index:1的参数片段……全靠index才能把碎片归位到正确的那个调用。这是 OpenAI Chat Completions 流式tool_calls协议的标准设计。─────────────────────────────────────────────────假设模型决定同时搜 “login” 和 “session”,实际到达的事件流大致这样(简化):{ type:toolCallDelta, index:0, id:call_a, name:explore_code } { type:toolCallDelta, index:0, argumentsDelta:{\query\: } { type:toolCallDelta, index:1, id:call_b, name:explore_code } ← 第二个开始交错进来 { type:toolCallDelta, index:0, argumentsDelta:\login\} } { type:toolCallDelta, index:1, argumentsDelta:{\query\:\session\} } { type:finishReason, reason:tool_calls } ← 收尾信号注意index:0和index:1的碎片是穿插到达的。没有index,你根本没法把login}拼回给 call_a 而不是 call_b。层面二:组装态(按 index 聚合的 Map)openAiReactModelTurn.ts:25,52-58 用一个Mapnumber, PendingToolCall按index累积、拼接:constpendingCallsnewMapnumber,PendingToolCall();// ...constpendingpendingCalls.get(event.index)??{name:,arguments:};pendingCalls.set(event.index,{id:event.id??pending.id,// id 取首次出现的name:pending.name(event.name??),// 名字累加拼接arguments:pending.arguments(event.argumentsDelta??),// 参数字符串累加});上面那串事件跑完,pendingCalls变成:{ 0 { id:call_a, name:explore_code, arguments:{query:login} }, 1 { id:call_b, name:explore_code, arguments:{query:session} } }arguments此刻还是字符串(拼出来的 JSON 文本),尚未解析。层面三:结构化态(runner 拿到的形态)finishReason tool_calls且pendingCalls.size 0,于是走createToolRequests(openAiReactModelTurn.ts:100-145)。它按 index 排序遍历,产出两份并行的数组。先看它的完整性校验(openAiReactModelTurn.ts:105-116):for(const[,pending]of[...pendingCalls.entries()].sort(([l],[r])l-r)){if(!pending.id)thrownewError(Tool call did not include an id);if(!pending.name)thrownewError(Tool call${pending.id}did not include a name);if(ids.has(pending.id))thrownewError(Duplicate tool call id:${pending.id});ids.add(pending.id);letinput:unknown;letparseError:string|undefined;try{inputJSON.parse(pending.arguments);// ← 每个 call 各自解析}catch{parseErrorInvalid JSON arguments for tool${pending.name};}// ...}缺id、缺name、id重复都会throw,保证下发的每个 call 干净可执行。而 JSON 解析失败只影响该 call 自己(记一个parseError),不牵连兄弟 call。产出的两份数组:①ModelToolCall[]——存进消息历史用(types.ts:12-19):[{id:call_a,type:function,function:{name:explore_code,arguments:{query:login}}},{id:call_b,type:function,function:{name:explore_code,arguments:{query:session}}}]arguments保持字符串——这是协议原样。②ReactAgentToolRequest[]——runner 执行用(reactTypes.ts:24-30):[{id:call_a,name:explore_code,rawArguments:{query:login},input:{query:login}},{id:call_b,name:explore_code,rawArguments:{query:session},input:{query:session}}]input是JSON.parse后的对象,runner 直接拿来用。✶ Insight ─────────────────────────────────────这里有个刻意的双份设计:ModelToolCall(带type:function、arguments为字符串)要原样回写进 assistant 消息历史,下一轮发给模型时它才认得自己上次调了什么,必须符合协议格式;ReactAgentToolRequest(带解析后的input)是给 runner本地执行用的。同一次调用,一个面向和模型对话的协议,一个面向本地工具执行,职责分离。─────────────────────────────────────────────────最终返回给 runner 的对象(openAiReactModelTurn.ts:139-144):return{kind:toolRequests,...(reasoning?{reasoning}:{}),assistantMessage:{role:assistant,content:,toolCalls},// ← 含完整 ModelToolCall[]requests,// ← 两个待执行请求};层面四:runner 如何消费这批多 call回到 reactAgentRunner.ts:184-266。① 一条 assistant 消息挂 N 个 callmessages.push(result.assistantMessage);// 一条 assistant,携带 [call_a, call_b] 两个 call② 分批,并发安全的并行跑(reactAgentRunner.ts:229-231):constoutcomesbatch.concurrent?awaitPromise.all(batch.requests.map(({request})invoke(request)))// 并行:[awaitinvoke(batch.requests[0]!.request)];// 串行是否可并发由工具自己声明——ReactAgentTool的isConcurrencySafe(reactTypes.ts:58)。两个explore_code都是只读搜索,可以并行;但如果其中一个是apply_edit(要写文件),就会被排到串行批次里,避免并发写冲突。✶ Insight ─────────────────────────────────────这是函数调用式相对经典范式一个白捡的性能红利:同时 grep 三个关键词可以一轮打完,墙钟时间 最慢那个工具的耗时,而不是三个之和。文本标签范式要实现这个,得自己发明多动作的文本语法并解析,又把可靠性拉回坑里(见第 02 篇)。─────────────────────────────────────────────────③ 每个 call 各自回填一条 tool 消息(reactAgentRunner.ts:237-266):for(const[index,{request,call}]ofbatch.requests.entries()){constoutcomeoutcomes[index]!;// ... 更新记分板、发前端事件 ...messages.push({role:tool,requestId:request.id,// ← 靠 id 对应回各自的 callname:request.name,content:outcome.content,});}回填后,历史里的形态是:assistant(toolCalls: [call_a, call_b]) ← 一条 assistant,两个 call tool(requestId:call_a, content:login 搜索结果...) ← 各自一条 tool 消息 tool(requestId:call_b, content:session 搜索结果...)✶ Insight ─────────────────────────────────────“一条 assistant N 条 tool” 是多 call 在历史里的标准形态。协议硬性要求:每个 tool_call 都必须有一条toolCallId匹配的 tool 消息回应——少一条,下一轮请求就是非法的(服务端会报有未回应的 tool_call)。这就是为什么 runner 对批次里每个 call无论成败都要push 一条 tool 消息:成功填结果,失败填Tool error: ...,一个都不能漏。─────────────────────────────────────────────────一张图串起四个层面wire 层 交错的 toolCallDelta 流 ──靠 index 区分──┐ ▼ 组装层 Mapindex, {id, name, arguments} 按 index 拼接 ▼ 结构层 ModelToolCall[] (协议原样,arguments 为字符串) ReactAgentToolRequest[] (解析出 input,可能带 parseError) ▼ 历史层 assistant(toolCalls:[N个]) N 条 tool(requestId 对应)全程没有任何第一个动作/第二个动作的文本标签,多 call 的边界完全由index和id这两个协议字段划定。小结多个 tool_call 本质是一个数组,经历四层形态:wire 流 → Map 组装 → 双份结构化数组 → 历史消息;index负责在流式碎片交错时把它们归位;id负责在回填结果时把 observation 挂回对应 action;并发安全的工具可Promise.all并行跑,由工具的isConcurrencySafe声明;铁律:每个 call 无论成败都要回填一条role:tool消息,否则下一轮请求非法。最后一篇,我们看那些把能跑的 demo变成能用的产品的工程护栏:重复调用拦截、连续失败熔断、证据门禁、倒计时收尾。 上一篇 → 03 · 慢放一次完整的 ReAct 循环 下一篇 → 05 · 让 Agent 不翻车的工程护栏本文源码来自开源项目LoopAgent⭐ → https://github.com/oi12344/loopagent-vscode
延伸阅读

更多相关文章

2026/9/21 14:20:47

告别转码等待:用LosslessCut实现秒级无损视频剪辑

告别转码等待:用LosslessCut实现秒级无损视频剪辑 【免费下载链接】lossless-cut The swiss army knife of lossless video/audio editing 项目地址: https://gitcode.com/gh_mirrors/lo/lossless-cut 你是否厌倦了视频剪辑软件漫长的渲染等待?是…

2026/9/28 9:35:27

Wand-Enhancer:终极免费方案解锁Wand专业版完整功能

Wand-Enhancer:终极免费方案解锁Wand专业版完整功能 【免费下载链接】Wand-Enhancer Advanced UX and interoperability extension for Wand (WeMod) app 项目地址: https://gitcode.com/GitHub_Trending/we/Wand-Enhancer 还在为Wand(原WeMod&am…

2026/9/30 3:56:37

小程序商城的商品图,为什么直接影响复购率

小程序商城的商品图,为什么直接影响复购率做 B2B 订货的业务员普遍觉得图片不重要:客户都是老客户,知道货长什么样。但实际使用数据里,商品图的缺失带来的影响比想象得大很多,而且不体现在第一次下单上,体现…

2026/9/30 3:56:37

鸿蒙下React Native重渲染优化:useCallback与memo实战

React Native鸿蒙跨平台,放在两年前还是不太敢碰的方向,今年已经成了不少团队绕不开的课题。我做鸿蒙端React Native适配和性能优化小半年,最让我头疼的倒不是API差异,而是那些看似不起眼的"多余渲染"——页面卡顿、列表…

2026/9/30 3:56:37

华为OD机试高频题:用map+list实现支持优先级队列的容器实战

最近在带一波准备华为OD机试的朋友,发现C卷100分的题目里,“支持优先级队列 - map与list”出场率相当高。很多第一次刷到这道题的人,第一反应都是“不就是堆吗”,结果一上手就发现不对劲:题目里压根没让你直接用优先队…

2026/9/30 3:56:37

Qt QFont全解析:从字体匹配到DPI适配的实战指南

字体这事儿,说大不大,说小不小。做Qt界面开发的,十有八九都在QFont上吃过亏——要么中文显示成方块,要么高分屏下控件挤成一团,要么DPI一变化整个界面直接没法看。问题看起来五花八门,其实根子往往都落在同…

2026/9/30 3:56:37

从网格搜索到贝叶斯优化:超参数调优原理与实战

1. 为什么网格搜索和随机搜索迟早会撞墙1.1 一次让我怀疑人生的调参经历先说个真实的场景。几年前我在做一个工业设备故障预测的项目,数据量不算大,几万条样本、四十多个特征,模型选了 XGBoost。当时我的调参方式是教科书式的网格搜索&#x…

2026/9/30 3:51:37

Model-Optimizer:大模型推理全链路分层优化实践指南

1. 项目概述:Model-Optimizer不是工具名,而是一类工程实践的统称 “Model-Optimizer”这个词在当前技术社区里常被误认为是一个具体软件或开源项目——比如有人搜“Model-Optimizer下载”“Model-Optimizer GitHub”,结果却找不到官方仓库。…

2026/9/29 11:07:23

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/29 21:48:03

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 7:00:49

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 0:01:22

MATLAB+Yalmip+CPLEX实战:综合能源系统优化调度全流程解析

做综合能源系统优化调度这活儿,最痛苦的不是建模本身,而是模型写完之后不知道该怎么求解。看论文里轻飘飘一句“采用Yalmip调用CPLEX求解”,自己上手时却往往卡在环境配置、变量声明、约束写法和求解状态判读上,一耗就是两三天。这…

2026/9/30 0:01:22

I3C比I2C快10倍?RK3576实战:速率、DTS配置与混合总线避坑指南

I3C 比 I2C 快 10 倍?这句话在嵌入式群里传了很久,每次都能吵出一堆截图。前段时间我正好在 RK3576 上调板级 I3C 接口,从控制器寄存器一路摸到 Linux DTS 配置,踩了不少坑,也把这笔速度账彻底算明白了。本文就用 RK35…

2026/9/30 0:01:22

字符串转对象:JSON.parse、new Function与URLSearchParams

“字符串转对象”这几个字,我在技术群里见过的问法至少有十几种:有人拿着一串{a:1,b:2}说 JSON.parse 直接报错,有人要从 URL 里抠出参数,还有人只是想把abc变成能挂属性的东西。js 这门语言里,字符串和对象之间的转换…

2026/9/29 3:53:39

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/29 9:46:12

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/29 6:36:14

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑