Agent 并行工具调用快了 0.8 秒——200 组实测发现,多花的那 3100 token 全打了水漂

你有没有遇到过这种情况------Agent 说要调三个工具,你心想"反正它们互相不依赖,一起发出去不香吗",于是 Promise.all 一把梭。结果也没报错,响应确实快了,但月底一看账单,token 比上个月多了四成。

这不是你的错觉。我拿 200 组多步任务跑了一轮三轮对比,数据摆在下面。

三种策略,一张表说清楚

我搭了一个测试 Agent,配置了 6 个工具:searchDocsreadFilecheckCalendarsendEmailcreateTaskgetUserContext。200 组任务包含"根据会议纪要给参会人发邮件并建跟进任务"、"根据用户最近文档推荐日程"这类典型的多工具联动场景。

三种执行策略:

策略 平均延迟 平均 token/任务 无效调用率 任务完成率
全串行 3.2s 7420 3% 94%
全并行 2.4s 10560 27% 86%
智能混合 2.6s 8020 6% 93%

"无效调用"指的是工具被调用了,但返回结果对后续决策没有任何贡献------要么参数纯靠猜填错了,要么在并行窗口里调了其实完全不需要的工具。

全并行快了 0.8 秒,但平均多烧 3140 token。换算一下,就是每快 1 毫秒花 3.9 个 token。贵不贵,你心里有数。

全串行:慢,但每 1 个 token 都花了值

typescript 复制代码
async function runSerial(agent: Agent, toolCalls: ToolCall[]): Promise<ToolResult[]> {
  const results: ToolResult[] = [];
  for (const call of toolCalls) {
    const result = await agent.executeTool(call);
    results.push(result);
    // 每个工具的结果立即注入上下文,下一步的模型已包含全部前置信息
    agent.appendContext({ role: "tool", content: JSON.stringify(result), toolCallId: call.id });
  }
  return results;
}

串行没什么花活,就是一个接一个调。好处显而易见------每一步模型都看到了上一步的真实返回。createTask 的 deadline 参数可以从 checkCalendar 返回的空闲时段里精确取,而不是随便蒙一个。

代价就是慢。6 个工具串下来,网络往返时间累加,3 秒起步。

全并行:问题不在"并行"本身,在于模型在瞎猜

typescript 复制代码
async function runParallel(agent: Agent, toolCalls: ToolCall[]): Promise<ToolResult[]> {
  const results = await Promise.all(
    toolCalls.map(call => agent.executeTool(call))
  );
  // 全部拿到后再一次性注入上下文
  agent.appendContext(
    results.map((r, i) => ({
      role: "tool",
      content: JSON.stringify(r),
      toolCallId: toolCalls[i].id,
    }))
  );
  return results;
}

代码就这么几行。问题不在代码,在 LLM 生成这些 toolCalls 时发生了什么。

并行调用的前提是:模型在一次推理中同时 生成所有工具调用的参数。但此时它还没有任何工具的返回结果。举例:模型需要 createTask,deadline 是"明天下午有空的时间"。这个信息得从 checkCalendar 的返回里拿。并行模式下,模型还没拿到日历数据就得把 deadline 参数填了,它只能猜。

猜的结果呢?200 组里,有 54 次生成的 deadline 是错的(比如选了已经有会议的时间段),Agent 只好再次调用 updateTask 去修正。一来一回,多烧了一轮 token,相当于并行省下来的时间又被后续的纠错调用吃回去了------而钱是实打实多花了。

说白了,并行策略赢就赢在"让 LLM 在信息不完整的情况下做决策",这种事,你猜会怎样。

一个具体的翻车例子:

css 复制代码
工具调用(并行):
  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 的结果,纯属瞎猫碰上死耗子。下一组任务里就蒙错了。

智能混合:别问能不能并行,问"有没有依赖"

关键不在"能不能并行",而在"参数来源是什么"。

typescript 复制代码
function detectDependencies(toolCalls: ToolCall[]): {
  independent: ToolCall[];
  dependent: ToolCall[];
} {
  const invoked = new Set<string>();
  const independent: ToolCall[] = [];
  const dependent: ToolCall[] = [];

  for (const call of toolCalls) {
    const paramValues = Object.values(call.arguments).join(" ");
    // 检查参数是否引用了其他工具的输出(通过工具名或占位符标记)
    const hasRef = toolCalls.some(
      other => other !== call && paramValues.includes(`\${${other.name}}`)
    );
    if (hasRef) {
      dependent.push(call);
    } else {
      independent.push(call);
    }
  }
  return { independent, dependent };
}

async function runHybrid(agent: Agent, toolCalls: ToolCall[]): Promise<ToolResult[]> {
  const results: ToolResult[] = [];
  let remaining = [...toolCalls];

  while (remaining.length > 0) {
    const { independent, dependent } = detectDependencies(remaining);

    // 无依赖的工具并行执行
    if (independent.length > 0) {
      const batchResults = await Promise.all(
        independent.map(call => agent.executeTool(call))
      );
      batchResults.forEach((r, i) => {
        results.push(r);
        agent.appendContext({
          role: "tool",
          content: JSON.stringify(r),
          toolCallId: independent[i].id,
        });
      });
      remaining = dependent;
    } else {
      // 全都有依赖,串行执行
      const call = remaining[0];
      const r = await agent.executeTool(call);
      results.push(r);
      agent.appendContext({ role: "tool", content: JSON.stringify(r), toolCallId: call.id });
      remaining = remaining.slice(1);
    }
  }
  return results;
}

思路不复杂:先扫一遍所有工具调用,看参数里有没有引用其他工具的输出(比如 ${checkCalendar}),能分开的就并行,有依赖的等一等。

实测数据证明这个简单判断比盲开并行靠谱得多。无效调用从 27% 降到 6%,不是因为这算法多聪明,是因为它直接避免了"信息没到就填参数"的根本问题。

你可能想问:参数里没有显式的 ${toolName} 占位符怎么办?确实,大部分 LLM 生成的 tool_arguments 不会带这种标记。实际项目中我用了一层包装:在 system prompt 里要求模型在依赖其他工具返回值的参数处加 "depends_on": "toolName" 字段,执行层在路由前做依赖分析。

typescript 复制代码
// system prompt 片段
const SYSTEM_PROMPT_DEPENDENCY = `
当你调用工具时,如果某个参数的值依赖于另一个工具的返回结果,
在该参数旁添加 "depends_on" 字段,值为被依赖的工具名称。
示例:
{
  "name": "createTask",
  "arguments": {
    "title": "跟进客户需求",
    "deadline": "需要从 checkCalendar 获取",
    "depends_on": "checkCalendar"
  }
}
`;

// 依赖解析升级版
function resolveHybrid(
  calls: ToolCall[],
  context: Map<string, ToolResult>
): { batch: ToolCall[]; wait: ToolCall[] } {
  const batch: ToolCall[] = [];
  const wait: ToolCall[] = [];
  for (const call of calls) {
    const dep = call.arguments["depends_on"] as string | 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 协议,转载请注明出处。

相关推荐
新知图书1 小时前
多模态智能体开发工具链推荐(IDE/调试工具/日志工具)
人工智能·agent·多模态·ai agent·智能体
Sirius Wu1 小时前
OpenClaw 并发安全完整详解
网络·人工智能·安全·ai·架构·aigc
Databend1 小时前
AST Visitor API 迁移,怎么证明 AI 没改坏 SQL 语义?
数据库·ai编程
手写码匠1 小时前
从零实现大模型推理引擎:Continuous Batching 与动态调度系统深度解析
人工智能·深度学习·算法·aigc
思考着亮1 小时前
2.模型的创建与调用
agent
SelectDB技术团队1 小时前
Agent 可观测性:Apache Doris / SelectDB 的技术能力、选型对比与实践
数据库·人工智能·agent·可观测·ai-native·apache doris·selectdb
欢喜躲在眉梢里1 小时前
用飞算Java做了一套新能源充电运营管理系统:把站点、设备、订单拆成可运营的微服务
java·开发语言·微服务·ai编程·技术分享·java开发·飞算javaai炫技赛
无责任此方_修行中2 小时前
面对 300+ 仓库的重复劳动,我用 Guideline 优化了 Vibe Coding 工作流
后端·ai编程·vibecoding
小墨同学boy2 小时前
WorkBuddy零基础教程进阶篇
人工智能·github·aigc