AI SDK 工具调用并发可控
AI SDK 7.0.140 把工具调用执行从固定并发改成可配置并发,副作用工具可按模型顺序串行运行。
正文
这次改动解决的不是模型并行,而是执行并行
AI SDK 7.0.140 新增了 toolCallConcurrency,让 generateText、streamText 和 ToolLoopAgent 可以限制同一步骤里由 SDK 执行的工具调用数量。省略这个参数时,行为仍是并发执行;设置为 1 时,工具会按照模型生成调用的顺序逐个执行;设置为大于 1 的正整数时,则把同时运行的本地工具数量限制在这个值以内。对开发者最重要的变化是:工具调用的“生成顺序”和“实际执行顺序”终于可以在 SDK 层分开控制,不再需要为每个有副作用的工具另写一套互斥队列。
这个能力出现在 Vercel AI SDK 的 ai@7.0.140。对应的合并请求在 2026 年 10 月 13 日合并,发布记录在 2026 年 10 月 14 日公布。它不是把模型改成只能一次调用一个工具,也不是把所有调用都改成串行,而是给执行器增加了一个正整数上限。已经依赖默认并发的应用,升级后不传参数,原有的并发行为会保留;只有明确配置了 toolCallConcurrency,执行策略才会改变。
如果项目还要在不同模型或 API 渠道之间切换,可以先通过 模型与平台目录 核对实际使用的模型入口;但要注意,模型是否能生成多个工具调用,与 SDK 如何执行这些调用,是两个不同层次的问题。
为什么 Promise.all 会让工具读到旧状态
在这个选项出现以前,同一步骤里生成的多个工具调用通常会被一起交给 Promise.all。这对互相独立的读取操作很合适,却不适合“前一个调用改变状态,后一个调用读取状态”的工作流。例如,模型在同一步里生成了“写入文件”和“读取文件”两个调用,开发者从调用列表的顺序看到写入在前、读取在后,很容易误以为读取必然发生在写入完成之后。实际并发执行时,读取可能先完成,拿到的仍是旧内容。
Vercel AI 仓库中的问题记录给出了一个最小复现:模型先生成一个耗时较长的 sleep 工具,再生成一个很快的 read 工具。旧执行路径会出现 sleep:start → read:start → read:end → sleep:end,也就是说,第二个工具已经结束,第一个工具才完成。这个问题不依赖某一个模型供应商,也不只会影响文件操作。数据库写入后查询、创建资源后读取资源、更新配置后做校验、发出事件后读取事件状态,都可能受到同类竞态影响。
新执行器把工具调用按输入顺序分配给有限数量的 worker。toolCallConcurrency: 1 时,只有前一个调用完成,后一个调用才会启动,因此“更新后读取”可以表达成明确的执行约束。toolCallConcurrency: 2 时,前两个调用可以并行,结果数组仍按原调用顺序返回,但这不代表两个副作用之间存在先后关系。换句话说,限制值控制的是同时执行的数量,不是给任意两个并发调用增加事务语义。
这也是为什么不能把所有 Agent 的默认值都机械地改成 1。只读搜索、独立文件读取和互不相关的 API 查询保留并发,通常更符合它们的依赖关系;会写文件、修改数据库、提交任务或触发外部动作的工具,则需要根据业务顺序决定是否串行。并发参数是执行层的边界,不会替代数据库事务、幂等键、锁或工作流状态机。
最小配置方式
generateText 的配置可以直接写在请求参数中。下面的例子假设模型在一个步骤里生成 updateFile 和 readFile 两个调用,并且后一个调用必须看到前一个调用的结果:
import { generateText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const result = await generateText({
model: openai('gpt-6-astra'),
prompt: '先更新配置文件,再读取并验证更新后的内容。',
tools: {
updateFile: tool({
description: '更新配置文件',
inputSchema: z.object({ value: z.string() }),
execute: async ({ value }) => {
// 写入文件或调用内部配置服务
return { updated: value };
},
}),
readFile: tool({
description: '读取配置文件',
inputSchema: z.object({}),
execute: async () => {
// 读取刚刚更新的内容
return { value: 'current-value' };
},
}),
},
toolCallConcurrency: 1,
});
这里的关键不是提示词里出现了“先”和“再”,而是执行器明确收到 toolCallConcurrency: 1。提示词只能影响模型如何规划调用,不能单独保证 JavaScript 中两个异步函数的完成顺序。若工具执行依赖共享文件、共享会话或同一条业务记录,应该把顺序约束写进执行配置,并在测试中断言实际事件顺序。
如果工具之间互不依赖,可以设置一个有限值,例如 toolCallConcurrency: 3。这会让最多三个 SDK 本地工具同时运行,排队的调用在前面的 worker 空闲后再启动。这个模式适合保护下游连接池、文件句柄或内部服务的并发上限,同时保留一部分并行能力。传入 0、负数、非整数或 NaN 都不符合参数要求,SDK 会抛出 InvalidArgumentError,所以不要把环境变量直接转成数字后不做校验。
streamText 使用相同的参数名;ToolLoopAgent 则可以在 Agent 设置或准备调用时提供这个值。迁移时不需要为三种 API 各写一套调度器,但要确认配置是否被动态的 prepareCall 覆盖,并用类型检查保证选项落在公开 API 的类型定义中。
不要把 parallelToolCalls 和它混为一谈
parallelToolCalls 解决的是模型输出层的问题:它影响模型是否可以在一次响应中提出多个工具调用,属于供应商选项,且并非每个供应商都支持。toolCallConcurrency 解决的是 SDK 执行层的问题:当模型已经提出多个调用后,SDK 本地的 execute 函数允许多少个同时运行。
因此,下面两种配置的含义不同:
const result = await generateText({
model,
tools,
providerOptions: {
openai: {
parallelToolCalls: true,
},
},
toolCallConcurrency: 1,
});
第一项允许供应商在一次模型输出中给出多个调用,第二项再把这些调用按顺序执行。前者为“能不能生成多个”提供提示,后者为“生成后怎么跑”提供约束。把 parallelToolCalls 设为 false,并不能替代 toolCallConcurrency: 1:它可能改变模型输出的形态,却不负责处理 SDK 已经收到的多个调用,也不能跨供应商保证相同语义。
官方文档还明确限定了作用范围。这个选项只调度 SDK 执行的本地工具,不会调度由模型供应商执行的工具,也不会改变客户端工具的执行方式。接入搜索、代码执行或其他供应商托管工具时,不能看到这个参数就推断所有工具都遵循同一并发上限。需要统一顺序时,应先画清工具究竟在哪一侧执行,再决定是在 SDK、供应商配置,还是业务服务内部加约束。
中止、流式工具和失败时会发生什么
并发上限还会影响中止路径。官方文档规定,操作被中止时,正在执行的工具会收到 abort signal,尚未启动的排队工具不会再开始。对长时间运行的工具来说,这意味着工具本身必须正确监听并传播 AbortSignal;仅仅设置了上限,不会自动回滚已经写入的外部状态。已经完成的副作用仍需要依靠幂等设计或补偿逻辑处理。
流式工具会一直占用一个并发槽位,直到产生最终输出或错误。不能把“已经开始返回几个中间 chunk”当成工具已经完成,再手动启动依赖它的下一个调用。否则,虽然表面上使用了 toolCallConcurrency: 1,实际仍可能在最终状态写入前发生读取。
如果某个工具执行失败,调度器会停止启动新的排队调用;已经开始的其他调用不会因为一个 Promise 失败就自动撤销。这个语义适合防止失败后继续扩大影响,但它不是事务。生产代码仍应记录每个 toolCallId、工具名称、开始和结束时间、错误类型以及外部写入结果,才能在重试或人工恢复时判断哪些动作已经发生。
建议把中止和失败测试写成三组断言:第一,abort 之后不再出现新的工具启动事件;第二,流式工具只有收到最终输出或错误后才释放并发槽;第三,一个调用失败后,排队调用没有被偷偷执行。若应用还接了 流式输出工程排查 中涉及的 SSE、重连或背压逻辑,则要分别记录“模型响应流是否结束”和“工具是否完成”,不能用网络连接关闭代替工具生命周期。
升级后的落地检查
第一步是确认依赖版本,而不是只看锁文件里是否出现了 AI SDK。发布记录对应的包是 ai@7.0.140,如果项目通过框架或二次封装依赖它,要检查最终安装树中的版本,并确认 generateText、streamText 和 Agent 的类型定义确实包含 toolCallConcurrency。
第二步是给工具分类。只读工具可以继续使用默认并发或一个有限上限;读写同一资源的工具要么设置为 1,要么把依赖关系拆成多个模型步骤;写入外部系统的工具必须额外具备幂等键、重复调用检测和失败补偿。不要为了让一次测试通过,就把整个 Agent 的所有工具统一串行,这会掩盖真正的资源依赖,也会让无关调用失去并行机会。
第三步是把供应商能力和本地执行分开验收。测试模型是否会返回多个调用,测试 parallelToolCalls 或等价供应商选项;再用一个不依赖真实模型的 mock,验证 toolCallConcurrency 的执行顺序、并发上限、结果顺序和 abort 行为。官方合并请求已经覆盖了默认并发、串行、有限并发、中止、参数校验、生成和流式场景,项目仍应针对自己的副作用工具补充回归测试。
最后,部署时应把这个参数作为业务语义的一部分记录下来。一个“更新后读取”的 Agent 需要可审计地知道为什么选择串行;一个批量搜索 Agent 需要知道并发上限与下游服务容量的关系。这样做的价值不是追求某个固定的并发数字,而是让工具的安全边界从隐藏在 Promise 调度里的偶然行为,变成代码中可读、可测、可复查的配置。
参考来源
以下来源均于 2026 年 10 月 15 日打开并核对;版本与发布时间以仓库发布记录为准,参数语义以官方文档、源码和测试为准。
- AI SDK 7.0.140 发布记录 —— 确认版本、发布时间和
toolCallConcurrency发布变更。 - AI SDK 官方工具调用文档 —— 确认默认并发、串行配置、供应商工具边界、中止和流式工具槽位语义。
- AI SDK 官方 generateText 文档 —— 确认
generateText的公开参数与工具执行配置。 - 功能请求 #22125 —— 记录旧版
Promise.all执行路径造成的读写顺序问题及最小复现。 - 合并请求 #22135 —— 确认新增参数覆盖
generateText、streamText和ToolLoopAgent,以及测试范围。 - AI SDK 7.0.140 的并发调度实现 —— 确认正整数校验、有限 worker、结果顺序和中止后的排队行为。
- AI SDK 7.0.140 的串行工具示例 —— 确认共享文件读写场景下使用
toolCallConcurrency: 1的完整示例。 - AI SDK 7.0.140 的并发测试 —— 确认默认、串行、有限并发和 abort 行为。