跳到主要内容

LLM Agent 中的并行工具调用:你可能尚未意识到的耦合测试

· 阅读需 11 分钟
Tian Pan
Software Engineer

大多数工程师之所以选择并行工具调用,是因为他们希望自己的 Agent 运行得更快。工具执行占 Agent 总延迟的 35–60%,具体取决于工作负载——编码任务处于高端,深度研究任务则处于中端。同时运行独立的调用是显而易见的优化方案。但接下来的情况却让大多数团队感到意外。

!["https://opengraph-image.blockeden.xyz/api/og-tianpan-co?title=LLM%20Agent%20%E4%B8%AD%E7%9A%84%E5%B9%B6%E8%A1%8C%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8%EF%BC%9A%E4%BD%A0%E5%8F%AF%E8%83%BD%E5%B0%9A%E6%9C%AA%E6%84%8F%E8%AF%86%E5%88%B0%E7%9A%84%E8%80%A6%E5%90%88%E6%B5%8B%E8%AF%95"]

一旦你启用了并行执行,工具设计中隐藏的每一个假设都会变得显而易见。在顺序执行时可靠工作的工具,在并发运行时可能会悄无声息地失效。原本稳定的行为变得不可预测,而且失败往往不会产生错误——只是在充满自信地返回一个错误的答案。

并行工具调用主要不是一项性能特性。它是一次非自愿的架构审计。

并行工具执行的实际工作原理

在深入探讨失败模式之前,有必要精确了解其机制。并行工具执行是模型做出的决定,而不是你的编排层做出的决定。当模型在单次响应中发出多个 tool_use 块时,你的运行器(runner)应当调用所有这些工具,并在下一步推理之前统一返回它们的结果。模型看不到中间结果——它会一次性看到所有内容。

大多数框架都会提供一个标志来抑制这种行为。OpenAI 的 API 包含 parallel_tool_calls,默认值为 true。Anthropic 没有提供直接的标志——相反,Claude 会根据请求的工具是否看起来独立来做出决定。这种行为在不同模型系列中也不一致:OpenAI 的推理模型(o3、o4-mini)要么忽略,要么完全拒绝 parallel_tool_calls 参数,如果你尝试显式设置它,会返回 400 错误。

这种不一致性在生产环境中至关重要。如果你在多个提供商之间进行路由,或者在模型版本之间进行升级,你不能假设并行行为是稳定的。无论你是否请求过,你的编排层都需要处理多工具响应。

执行模式本身非常直接:当模型在单轮中发出 N 个工具调用时,你的运行器会同时分派所有 N 个调用,等待所有调用完成,并在继续推理之前返回整批结果。延迟的降低完全来自于墙上时钟时间(wall-clock time)的重叠——一批三个独立的 200 ms 工具调用只需 200 ms,而不是 600 ms。

耦合隐藏时的三种失败模式

顺序执行是宽容的。如果工具 A 对工具 B 有隐性依赖,顺序执行会自动强制执行该依赖。你通常甚至不知道这种依赖关系的存在,因为代码一直以相同的顺序运行。并行执行则取消了这种宽容。

上下文依赖(Context dependency):工具 A 悄悄地从一个共享上下文变量中读取数据,而该变量本应由工具 B 填充。在顺序执行中,B 总是运行在 A 之前。在并行执行中,A 在 B 填充上下文之前运行,读取了陈旧或空的数据,并返回了一个看起来有效但基于错误输入计算的结果。没有抛出异常。Agent 带着错误答案继续运行。

共享状态变更(Shared state mutation):两个工具根据在写入完成前读取的状态,向同一个资源(文件、数据库行、缓存值)写入数据。这是经典的“读取-修改-写入”竞态条件。工具 A 读取当前值 (100),计算增量 (+10),并写回 110。工具 B 读取相同的初始值 (100),计算不同的增量 (+20),并写回 120。最终值是 120,但预期的值应该是 130。两个工具都没有报错。这种不一致性是无声的。

执行时序依赖(Execution timing dependency):这是最微妙的失败。一个工具中的逻辑隐式地假设另一个工具已经运行——不是因为它读取了输出,而是因为第一个工具的副作用是第二个工具的前提条件。例如,一个工具创建数据库记录,另一个工具写入相关记录;一个工具初始化会话,另一个工具向该会话添加数据;或者一个工具获取资源锁,另一个工具在锁定的资源上操作。在顺序执行中,前提条件总是能得到满足。在并行执行中则不然。

这些失败都有一个共同点:它们不是崩溃。Agent 循环继续运行,模型处理结果,下一步在损坏的状态下进行。当错误的输出显现时,执行轨迹已经向前推进了好几步,与原始并行调用的因果联系已变得不可见。

幂等性测试:在并行化之前对工具进行分类

最简单且最可靠的分类方法是在决定是否并发运行每个工具之前,问三个问题。

它是原子的吗(Is it atomic)? 工具是否只做一件事,没有其他并发工具可以观察到的中间状态?搜索查询是原子的——它读取并返回,没有其他事情发生。文件重命名在大多数文件系统中不是原子的——存在一个旧名称已消失而新名称尚未显示的窗口。

它是幂等的吗(Is it idempotent)? 如果这个工具对相同的输入运行两次,最终世界的状态是否相同?GET 请求是幂等的。除非你显式处理去重,否则创建记录的 POST 请求不是幂等的。每次调用都发送通知邮件的工具不是幂等的。

它是独立的吗(Is it independent)? 工具的行为是否依赖于当前批次中任何其他工具的输出或副作用?如果你需要回答“这取决于 X 是否先运行”,那么该工具就不是独立的,并行化它将产生非确定性的行为。

通过所有三项测试的工具可以安全地并行化。未能通过其中任何一项的工具,相对于与其交互的工具,需要顺序执行。这种分类对于整个 Agent 来说不是二元的——它是针对每一批次的。一对只读搜索工具可以并行运行。一个搜索工具和一个涉及同一资源的写入工具则不能并行。

这种分类为你提供了一个具体的规则:读取批次并行化;写入批次序列化。一批纯读取操作通常能通过并行化减少 40–50% 的延迟,且没有正确性风险。包含任何写入操作的混合批次都应回退到顺序执行,因为写入可能依赖于读取结果,或与同级的读取操作发生冲突。

当合并优于并行时

有时,针对并行失败的正确对策并非增加锁定或序列化,而是合并工具。

考虑一个在同一轮对话中调用 get_customer_idget_recent_orders 的智能体(agent)。其意图很明显:两者都是回答有关客户订单历史问题所必需的。它们看起来是独立的,但 get_recent_orders 可能需要客户 ID 作为查找键。在顺序执行中,如果 get_customer_id 先完成,一切正常。但在并行执行中,get_recent_orders 可能会在 ID 可用之前就触发。

一种解决方法是添加排序逻辑。另一种方法是思考这两个工具是否根本不应该作为独立的基元存在。将它们合并为 get_customer_summary 可以完全消除依赖关系,提供更简洁的接口,并减少往返次数。并行失败暴露了顺序执行所掩盖的设计坏味道(design smell)。

这种情况经常发生,甚至形成了一种模式:那些总是被可靠地一起调用、顺序固定且存在依赖关系的工具,通常只是一个伪装成两个的工具。顺序执行让这种耦合变得隐形,而并行执行迫使你正视它。

对于工具库中的每个工具,都值得问这样一个问题:“这真的是一个工具,还是智能体控制流的一部分?”如果答案是后者——即该工具仅在特定的执行上下文或特定的先前调用之后才有意义——那么它应该属于编排逻辑,而不是作为一个公开的工具。将顺序要求从工具实现中移出并放入编排层,是让工具并行在大规模应用中保持安全的关键。

DAG 编排:让依赖关系显式化

长期来看,最稳健的方法是停止从执行顺序中推断工具依赖,而是开始将它们显式编码为依赖图。彼此之间没有依赖关系的工具获得并行边,一个工具为另一个工具提供输入的则获得串行边。编排器强制执行该图,执行顺序就变成了结构的属性,而不是历史的巧合。

一些框架直接提供了这种能力。LangGraph 将智能体工作流表示为有向图,其中节点是智能体或工具调用,边代表数据流。这意味着你可以声明:在两个读取依赖项完成之前,写入节点不得启动;而两个独立的读取节点可以同时运行。LLMCompiler 将 LLM 生成的计划编译为 DAG(有向无环图),并以相同的语义执行它们——研究人员发现,在保持正确性的同时,多步任务的执行速度比顺序执行快了多达 2.2 倍。

实际的好处不仅限于正确性。显式依赖图也是可观测的。当出现问题时,你可以检查哪个节点失败了、它接收到了什么输入、哪些并行兄弟节点在同一时间运行,以及其中是否有节点写入了共享状态。相比之下,在隐式顺序循环中调试竞态条件,你只能在没有任何结构信息的情况下,从错误输出中倒推当时发生了什么。

复杂性成本是真实存在的。为动态智能体构建和维护依赖图需要对工具声明进行静态分析或运行时依赖追踪。对于拥有小型、稳定工具库的智能体,这种开销通常不值得——读/写分类规则更简单且足够。而对于拥有数十个工具或复杂多步工作流的智能体,显式 DAG 编排是能够支撑系统扩展的架构。

发布前应进行的审计

在生产环境中的智能体上启用并行工具调用之前,请检查以下内容:

  • 列出工具库中的每个工具并进行分类:只读、只写或读-改-写(read-modify-write)。
  • 对于每个写入工具,识别它触及的资源。是否有其他可能并发运行的工具也会触及这些资源?
  • 对于每个工具,询问它是否对先前的工具有任何隐式依赖——不是在它的签名中,而是在它的实现中。检查共享变量、会话状态和文件系统路径。
  • 在开发环境中显式启用并行执行进行测试。如果你一直进行顺序测试,你可能正在测试一个与生产环境运行状况不符的系统。

当你的工具设计精良时,并行工具执行能带来真实的延迟提升。针对读密集型批处理,40%–50% 的挂钟时间缩减是实打实的,它创造的性能空间允许你在每个推理步骤中完成更多工作。但是,只有当工具真正满足并行所需的契约时,这种性能提升才可行。

跳过审计的团队通常会在生产环境中发现耦合,因为竞态条件已经默默地损坏了数十次状态,直到错误报告最终送达。耦合一直都在,顺序执行顺序只是将其隐藏了起来。

References:Let's stay in touch and Follow me for more thoughts and updates