Agent 一次返回多个 Tool Call,应该并行还是顺序执行?

一个 Agent 接入多个工具后,模型一次回复可能不只返回一个 tool_call

例如,用户要求同时分析两个文件,模型可能返回:

text 复制代码
read_file(a.js)
read_file(b.js)

这两个文件如果每个读取 2 秒,顺序执行大约需要 4 秒,并行执行则可能只需要 2 秒左右。

看起来,直接把所有工具都放进 Promise.all() 就能提高性能。

但把任务换成下面这样:

text 复制代码
创建项目
安装依赖
启动开发服务器

如果三步同时执行,安装依赖时项目可能还没有创建,启动服务时依赖也可能尚未完成。

因此,多 Tool Call 的关键问题不是"会不会使用 Promise.all()",而是:

text 复制代码
这些调用之间是否存在依赖关系?

本文对比项目里的两种真实 Agent Loop:

  • tool.mjs 使用 Promise.all() 并行执行
  • mini-cursor.mjs 和多 MCP Demo 使用 for...of + await 顺序执行

看清它们分别适合什么任务,以及工具结果怎样准确对应回原来的 tool_call_id

模型返回的是 Tool Call 数组

模型决定使用工具时,会在 AIMessage 中返回 tool_calls

js 复制代码
response.tool_calls

其中每一项通常包含:

js 复制代码
{
  id: 'call_xxx',
  name: 'read_file',
  args: {
    filePath: './src/app.js'
  }
}

当一次回复需要多个工具时,tool_calls 就是一个数组:

js 复制代码
[
  {
    id: 'call_1',
    name: 'read_file',
    args: { filePath: './src/app.js' }
  },
  {
    id: 'call_2',
    name: 'read_file',
    args: { filePath: './src/config.js' }
  }
]

模型只负责生成调用意图和参数,真正决定怎样调度这些任务的是 Runtime。

Runtime 可以选择:

text 复制代码
顺序执行:一个完成后再执行下一个
并行执行:同时启动,等待全部完成

方案一:使用 Promise.all() 并行执行

tool.mjs 中使用 Promise.all() 处理一轮里的全部 Tool Call:

js 复制代码
const toolResults = await Promise.all(
  response.tool_calls.map(async (toolCall) => {
    const tool = tools.find(
      item => item.name === toolCall.name
    );

    if (!tool) {
      return `错误:找不到工具 ${toolCall.name}`;
    }

    try {
      return await tool.invoke(toolCall.args);
    } catch (err) {
      return `错误:${err.message}`;
    }
  })
);

map() 会把每个 Tool Call 转换成一个 Promise,Promise.all() 同时等待这些异步任务。

执行过程可以理解为:

text 复制代码
tool_call 1 ──开始────────完成
tool_call 2 ──开始────完成
tool_call 3 ──开始──────────完成
                         ↓
              所有任务完成后继续

它最适合彼此独立的 I/O 任务,例如:

  • 同时读取多个互不相关的文件
  • 查询多个城市的天气
  • 请求多个独立数据接口
  • 对多份内容执行互不影响的检查

这些任务不需要等待前一个结果,也不会修改同一份共享状态。

Promise.all() 的结果顺序会乱吗?

不会。

即使第二个工具最先执行完成,Promise.all() 返回的结果数组仍然按照传入 Promise 的顺序排列。

text 复制代码
输入顺序:call_1、call_2、call_3
完成顺序:call_2、call_1、call_3
结果数组:result_1、result_2、result_3

因此,项目里可以通过相同的 index 把结果对应回原来的调用:

js 复制代码
response.tool_calls.forEach((toolCall, index) => {
  messages.push(
    new ToolMessage({
      content: toolResults[index],
      tool_call_id: toolCall.id,
    })
  );
});

这里有两个对应关系:

text 复制代码
toolResults[index] ↔ response.tool_calls[index]
tool_call_id       ↔ toolCall.id

数组下标帮助 Runtime 取到正确结果,tool_call_id 帮助模型确认结果属于哪一次调用。

并行执行不代表可以省略 tool_call_id

Promise.all() 为什么经常"一错全错"?

标准 Promise.all() 有一个重要特点:只要其中一个 Promise 进入 rejected 状态,外层等待就会立即 reject。

js 复制代码
await Promise.all([
  taskA(),
  taskB(), // rejected
  taskC(),
]);

需要注意,外层提前失败并不会自动取消已经启动的其他异步任务。taskAtaskC 仍可能继续执行,只是当前代码不再等待它们的完整结果。

项目里的处理方式是在每个工具内部捕获错误:

js 复制代码
try {
  return await tool.invoke(toolCall.args);
} catch (err) {
  return `错误:${err.message}`;
}

工具失败后返回的是错误字符串,而不是继续抛出异常。

这样 Promise.all() 仍然可以拿到完整结果数组:

text 复制代码
[
  '文件 A 的内容',
  '错误:文件不存在',
  '文件 C 的内容'
]

模型在下一轮既能看到成功结果,也能看到失败原因,再决定是否重试或换一种方式处理。

方案二:使用 for...of 顺序执行

mini-cursor.mjs 中采用了另一种方式:

js 复制代码
for (const toolCall of response.tool_calls) {
  const foundTool = tools.find(
    tool => tool.name === toolCall.name
  );

  if (foundTool) {
    const toolResult = await foundTool.invoke(
      toolCall.args
    );

    messages.push(
      new ToolMessage({
        content: toolResult,
        tool_call_id: toolCall.id,
      })
    );
  }
}

await 位于循环内部,所以当前工具完成后,程序才会执行下一个工具。

text 复制代码
tool_call 1:开始 → 完成
tool_call 2:       开始 → 完成
tool_call 3:              开始 → 完成

顺序执行耗时可能更长,但更适合会产生副作用或共享状态的任务:

  • 写入同一个文件
  • 创建项目后再安装依赖
  • 安装依赖后再启动服务
  • 打开网页后再获取页面快照
  • 获取快照后再根据页面内容点击元素
  • 点击进入详情页后再截图

这些任务不能只看"有几个 Tool Call",还要看先后关系。

顺序执行就能解决所有依赖吗?

不能。

同一个 AIMessage 里的多个 Tool Call,是模型在看到工具结果之前一次性规划出来的。

即使 Runtime 按顺序执行,后一个调用的参数也已经提前确定:

text 复制代码
模型同时生成 call_1 和 call_2
→ Runtime 执行 call_1
→ Runtime 执行 call_2

执行 call_1 后,模型没有机会根据真实结果重新生成 call_2 的参数。

例如浏览器任务:

text 复制代码
打开搜索页
→ 获取页面快照
→ 根据快照找到酒店按钮
→ 点击按钮

模型必须先看到页面快照,才能知道应该点击哪个元素。

这种真正依赖上一步结果的任务,更适合拆成多个 Agent Loop:

text 复制代码
第 1 轮:模型调用打开网页
→ ToolMessage 返回页面状态

第 2 轮:模型根据状态调用页面快照
→ ToolMessage 返回快照内容

第 3 轮:模型根据快照决定点击哪个元素

所以需要区分两种"顺序":

text 复制代码
同一轮内按顺序执行:只保证副作用发生的先后顺序
跨模型轮次执行:后一步可以真正依据前一步结果重新规划

哪些工具不能轻易并行?

1. 修改同一个资源

两个工具同时写入同一个文件,最终结果可能取决于谁最后完成:

text 复制代码
write_file(app.js, 内容 A)
write_file(app.js, 内容 B)

这会产生竞态条件,不能简单放进 Promise.all()

2. 浏览器操作

点击、切换标签页、输入和截图依赖当前页面状态。

如果同时点击两个元素或在页面加载前截图,结果会变得不可预测。

3. 命令行任务

下面三步有明确依赖:

text 复制代码
pnpm install
pnpm build
pnpm preview

构建依赖安装结果,预览又依赖构建产物。

4. 共享数据库状态

同时执行扣库存、创建订单和更新支付状态,可能产生数据一致性问题。

是否允许并发,应由业务事务和幂等设计决定,不能只由模型判断。

哪些工具适合并行?

通常满足下面条件的任务更适合并行:

  • 彼此不依赖返回结果
  • 不修改同一资源
  • 失败不会破坏其他任务
  • 调用顺序不影响最终结果
  • 服务端允许相应并发量

例如:

text 复制代码
读取 a.js、b.js、c.js
查询北京、上海、广州天气
检查三份独立配置文件
获取多个互不关联的公开接口数据

并行前仍要考虑接口限流、连接数量和机器资源。

如果模型一次返回几十个调用,直接全部启动也可能触发 API 限流或占满文件句柄。实际项目通常还要增加并发上限,而不是无限制使用 Promise.all()

Runtime 应该怎样决定?

不能把"并行还是顺序"完全交给 LLM 的自然语言推理。

更稳妥的做法是让 Runtime 根据工具属性和资源冲突做判断。

可以从四个问题开始:

  1. 后一个调用是否需要前一个调用的返回值?
  2. 多个调用是否会修改同一个文件、页面或数据库记录?
  3. 工具失败后是否可以安全重试?
  4. 外部服务是否有并发与频率限制?

简单决策可以整理成:

场景 建议方式
多个独立只读查询 并行执行
修改同一份状态 顺序执行
后一步需要前一步结果 跨 Agent 轮次执行
浏览器点击、等待、截图 顺序并分轮执行
大量独立调用 限制并发数量

ToolMessage 的顺序也要保持稳定

无论工具怎样调度,最终都要把每个结果放回 messages

text 复制代码
AIMessage(tool_calls)
→ Runtime 执行工具
→ ToolMessage(tool_call_id)
→ 再次调用模型

并行执行时,不能按照"哪个工具先完成就先随意塞进去"的方式丢失对应关系。

项目使用 Promise.all() 的结果顺序和原始 tool_calls 下标完成映射,再为每条消息保留 tool_call_id,这是并行工具执行能够稳定工作的关键。

总结

Agent 一次返回多个 Tool Call,并不意味着所有工具都应该并行。

text 复制代码
并行解决的是等待时间
顺序保证的是状态和依赖
跨轮次解决的是根据真实结果重新规划

可以把选择原则记成:

text 复制代码
独立、只读、无共享状态 → Promise.all()
存在副作用或资源冲突 → for...of + await
后一步依赖前一步结果 → 返回 ToolMessage,让模型进入下一轮

模型负责提出调用计划,Runtime 才应该负责并发控制、错误隔离和执行顺序。

这也是 Agent 从"能调用工具"走向"稳定完成复杂任务"时必须补上的工程能力。

相关推荐
问商十三载1 小时前
大模型GEO内容更新频率:3个节奏规律提收录权重,零成本提30%引用率附更新计划表
前端·人工智能·机器学习
Summer-Bright1 小时前
AI 软件简报 07.11-07.15:DeepSeek天价融资、Copilot用户叛逃、中国AI新规落地
人工智能·软件工程·copilot
哦哦~9211 小时前
西湖大学吴泰霖团队|GenCP:跳出迭代求解,开创多物理耦合生成建模路线
人工智能·耦合·流体
2601_956319881 小时前
最新量化软件怎么选,先按能力短板匹配工具类型
人工智能·python
前端开发江鸟1 小时前
从 messages 看懂 Agent 的工具调用循环:Runtime 如何衔接模型与工具
人工智能
天天代码码天天1 小时前
PP-OCR Linux 部署不再折腾:OpenCV、ONNX Runtime、OpenVINO 三版本开箱即用
人工智能
皓悦编程记1 小时前
【免费数据集009期】Urban Issues Dataset:10类城市公共场景异常目标检测数据集
人工智能·计算机视觉·目标跟踪
飘尘1 小时前
让AI能够预演现实的世界模型,到底是什么?
前端·人工智能
小帽子_1231 小时前
双碳背景下 PCS 技术迭代与储能产业发展机遇
人工智能·物联网