
你有没有遇到过这种情况——Agent 说要调三个工具你心想反正它们互相不依赖一起发出去不香吗于是Promise.all一把梭。结果也没报错响应确实快了但月底一看账单token 比上个月多了四成。这不是你的错觉。我拿 200 组多步任务跑了一轮三轮对比数据摆在下面。三种策略一张表说清楚我搭了一个测试 Agent配置了 6 个工具searchDocs、readFile、checkCalendar、sendEmail、createTask、getUserContext。200 组任务包含根据会议纪要给参会人发邮件并建跟进任务、根据用户最近文档推荐日程这类典型的多工具联动场景。三种执行策略策略平均延迟平均 token/任务无效调用率任务完成率全串行3.2s74203%94%全并行2.4s1056027%86%智能混合2.6s80206%93%无效调用指的是工具被调用了但返回结果对后续决策没有任何贡献——要么参数纯靠猜填错了要么在并行窗口里调了其实完全不需要的工具。全并行快了 0.8 秒但平均多烧 3140 token。换算一下就是每快 1 毫秒花 3.9 个 token。贵不贵你心里有数。全串行慢但每 1 个 token 都花了值asyncfunctionrunSerial(agent:Agent,toolCalls:ToolCall[]):PromiseToolResult[]{constresults:ToolResult[][];for(constcalloftoolCalls){constresultawaitagent.executeTool(call);results.push(result);// 每个工具的结果立即注入上下文下一步的模型已包含全部前置信息agent.appendContext({role:tool,content:JSON.stringify(result),toolCallId:call.id});}returnresults;}串行没什么花活就是一个接一个调。好处显而易见——每一步模型都看到了上一步的真实返回。createTask的 deadline 参数可以从checkCalendar返回的空闲时段里精确取而不是随便蒙一个。代价就是慢。6 个工具串下来网络往返时间累加3 秒起步。全并行问题不在并行本身在于模型在瞎猜asyncfunctionrunParallel(agent:Agent,toolCalls:ToolCall[]):PromiseToolResult[]{constresultsawaitPromise.all(toolCalls.map(callagent.executeTool(call)));// 全部拿到后再一次性注入上下文agent.appendContext(results.map((r,i)({role:tool,content:JSON.stringify(r),toolCallId:toolCalls[i].id,})));returnresults;}代码就这么几行。问题不在代码在 LLM 生成这些toolCalls时发生了什么。并行调用的前提是模型在一次推理中同时生成所有工具调用的参数。但此时它还没有任何工具的返回结果。举例模型需要createTaskdeadline 是明天下午有空的时间。这个信息得从checkCalendar的返回里拿。并行模式下模型还没拿到日历数据就得把 deadline 参数填了它只能猜。猜的结果呢200 组里有 54 次生成的 deadline 是错的比如选了已经有会议的时间段Agent 只好再次调用updateTask去修正。一来一回多烧了一轮 token相当于并行省下来的时间又被后续的纠错调用吃回去了——而钱是实打实多花了。说白了并行策略赢就赢在让 LLM 在信息不完整的情况下做决策这种事你猜会怎样。一个具体的翻车例子工具调用并行 checkCalendar({ date: 2026-07-16 }) → 返回 [10:00-11:00 空, 14:00-15:00 空] createTask({ title: ..., deadline: 14:00 }) → 这个 deadline 是蒙的 sendEmail({ to: ..., body: 明天下午两点开会 }) → body 里的时间也是蒙的checkCalendar明明返回了 14:00-15:00 有空这是对的——但并行执行时这两个值被填进去的时候模型根本没见过checkCalendar的结果纯属瞎猫碰上死耗子。下一组任务里就蒙错了。智能混合别问能不能并行问有没有依赖关键不在能不能并行而在参数来源是什么。functiondetectDependencies(toolCalls:ToolCall[]):{independent:ToolCall[];dependent:ToolCall[];}{constinvokednewSetstring();constindependent:ToolCall[][];constdependent:ToolCall[][];for(constcalloftoolCalls){constparamValuesObject.values(call.arguments).join( );// 检查参数是否引用了其他工具的输出通过工具名或占位符标记consthasReftoolCalls.some(otherother!callparamValues.includes(\${${other.name}}));if(hasRef){dependent.push(call);}else{independent.push(call);}}return{independent,dependent};}asyncfunctionrunHybrid(agent:Agent,toolCalls:ToolCall[]):PromiseToolResult[]{constresults:ToolResult[][];letremaining[...toolCalls];while(remaining.length0){const{independent,dependent}detectDependencies(remaining);// 无依赖的工具并行执行if(independent.length0){constbatchResultsawaitPromise.all(independent.map(callagent.executeTool(call)));batchResults.forEach((r,i){results.push(r);agent.appendContext({role:tool,content:JSON.stringify(r),toolCallId:independent[i].id,});});remainingdependent;}else{// 全都有依赖串行执行constcallremaining[0];constrawaitagent.executeTool(call);results.push(r);agent.appendContext({role:tool,content:JSON.stringify(r),toolCallId:call.id});remainingremaining.slice(1);}}returnresults;}思路不复杂先扫一遍所有工具调用看参数里有没有引用其他工具的输出比如${checkCalendar}能分开的就并行有依赖的等一等。实测数据证明这个简单判断比盲开并行靠谱得多。无效调用从 27% 降到 6%不是因为这算法多聪明是因为它直接避免了信息没到就填参数的根本问题。你可能想问参数里没有显式的${toolName}占位符怎么办确实大部分 LLM 生成的 tool_arguments 不会带这种标记。实际项目中我用了一层包装在 system prompt 里要求模型在依赖其他工具返回值的参数处加depends_on: toolName字段执行层在路由前做依赖分析。// system prompt 片段constSYSTEM_PROMPT_DEPENDENCY当你调用工具时如果某个参数的值依赖于另一个工具的返回结果 在该参数旁添加 depends_on 字段值为被依赖的工具名称。 示例 { name: createTask, arguments: { title: 跟进客户需求, deadline: 需要从 checkCalendar 获取, depends_on: checkCalendar } };// 依赖解析升级版functionresolveHybrid(calls:ToolCall[],context:Mapstring,ToolResult):{batch:ToolCall[];wait:ToolCall[]}{constbatch:ToolCall[][];constwait:ToolCall[][];for(constcallofcalls){constdepcall.arguments[depends_on]asstring|undefined;if(dep!context.has(dep)){wait.push(call);// 依赖还没产生等等}else{batch.push(call);// 可以直接跑}}return{batch,wait};}模型配合度比预想的高。200 组任务里LLM 在 86% 的情况下正确标记了依赖关系剩下的要么漏标要么标错但不会导致崩溃只是退化为串行。顺便说一句你要是觉得让 LLM 自己标记自己的依赖有点魔幻实话说我也觉得——但它确实能用而且比我在代码里用 AST 分析参数活得多了。什么时候可以无脑并行有一种情况并行基本不会翻车所有工具都是纯查询不产生副作用且参数完全独立、不依赖任何上下文动态内容。比如同时搜文档、查日历、拉配置——这三个工具的 input 完全来自用户原始问题不存在互相依赖。一旦涉及写操作create/update/delete/send或者参数里出现了从其他工具应该返回的值老老实实用混合策略。给你个结论你的场景用什么全查询、参数独立并行省钱又省时间有写操作、参数可能互依赖混合策略预算极紧、延迟不敏感串行这三种策略的代码我在雷达鸭的客服 Agent 上跑了两周混合策略的效果跟测试数据一致——搜文档、查用户信息、发邮件、建工单这几件事儿延迟比全串行少了一截token 也没多烧多少。如果你也在做类似的多工具 Agent建议拿自己的场景跑一遍别看我数据就直接拍。作者老三10 年以上软件开发经验软件设计师、人工智能应用工程师专注鸿蒙 ArkTS 北向开发与 Web 前端业余折腾 AI 自动化。不定期在 CSDN 分享鸿蒙和 AI 方向的技术文章。本文遵循 MIT 协议转载请注明出处。