loading

Loading

探索与发现

首页 Agent 工程

并行工具调用暴露的工具设计耦合问题

分类:Agent 工程
字数: (2534)
阅读: (4)
0
摘要:并行工具调用能砍掉四成延迟,但它更重要的价值在于暴露工具之间的隐性耦合。本文拆解上下文依赖、共享状态竞争与时序前提这三类静默失败,给出读写分类规则、工具合并的判据,以及什么时候该上依赖图编排。

很多团队开并行工具调用是为了省时间。工具执行在 Agent 总延迟中占到 35%–60%,编码类任务靠近上限,深度研究类贴近中位。一轮里几个 200 毫秒的独立调用并发跑,墙上时钟就从 600 毫秒压到 200 毫秒。这笔账很好算,于是开关一开,行为开始变得不可预测。

我先说结论:并行工具调用主要不是性能特性,它更像一次你并不自愿的架构审计。顺序执行一直在替你掩盖工具之间的隐性依赖;一旦并发,这些依赖就以数据错误的形式浮出来,而且多数时候不抛异常。

执行机制:决定权在模型手里,不在编排层

并行工具调用是模型做出的决定。当模型在单次响应里输出多个 tool_use 块,运行器应当把这批工具都调起来,等全部返回后再进入下一轮推理。模型在第二轮才开始之前看不到任何中间结果——它一次性看到一整批。

各家的开关并不一致。OpenAI 的接口有 parallel_tool_calls,默认 true;其推理模型 o3、o4-mini 并不接受显式设置这个参数,强行传会返回 400。Anthropic 没有对应的开关,Claude 自己判断这批工具看起来是否独立。这意味着你在做多提供商路由或者升级模型版本时,不能假设并行行为是稳定的,编排层必须能处理多工具响应,无论你有没有主动要求。

ADK 里的 ParallelAgent 是另一种形态:执行不受模型控制,在代码里确定性地并发启动子 Agent 的 run_async(),每个子 Agent 在自己的分支里跑,分支之间不自动共享对话历史和状态,结果收集顺序也不保证确定。做固定扇出(比如同时查三个数据源)时这种模板工作流更省心,但它和模型自治的并行调用是两套东西,别混着讨论。

三种不会崩的失败

顺序执行对工具很宽容:如果 A 隐式依赖 B,顺序会自动强制这个依赖,你甚至不知道它存在。并行撤掉了这层宽容。常见的失效有三种。

  • 上下文依赖。工具 A 从共享上下文变量里读值,而那个变量本该由工具 B 写入。顺序执行时 B 总在 A 之前;并行时 A 先跑,读到空值或旧值,返回一个格式完全正确、输入却错了的结果。没有异常。
  • 共享状态竞争。两个工具对同一资源做读-改-写。A 读到 100、加 10 写回 110;B 也读到 100、加 20 写回 120。最终是 120,而正确结果应该是 130。两边都没有报错,最终状态就这么静默地错了。
  • 时序前提。最隐蔽的一类。工具并不读对方的输出,而是依赖对方的副作用:一个建了会话,另一个才往里写数据;一个拿到资源锁,另一个才操作那部分资源。顺序下前提总成立,并行下不成立。

共同点是:都不是崩溃。Agent 循环继续转,模型拿着被污染的结果往下推理。等错误出现在最终输出上,执行轨迹已经推进了好几步,跟最初那次并发调用的因果关系基本看不见了。

上并行之前先给工具分类

最简单可靠的规则是三个问题:原子吗——是否只做一件事,没有别的并发工具能观察到的中间状态;幂等吗——相同输入跑两次,世界状态是否一致(GET 是,未去重的 POST 不是,每次发邮件的工具不是);独立吗——是否需要先知道对方有没有跑过。三问全过的可以并发,任何一问不过的相对其交互对象要串行。

这个分类不是对整个 Agent 二元判定,而是对每一批调用判定。纯读批次并行通常能砍掉 40%–50% 的延迟且没有正确性风险;只要混进写入,就退回顺序,因为写入可能依赖同批读的结果,也可能跟同批的同级操作冲突。

「读批次并行、写批次串行」是我目前最愿意守的一条线,它的好处是检查成本低,坏处是会损失一部分本可并行的延迟收益。

有时正确答案是两个工具合成一个

一个 Agent 在同一轮里调 get_customer_id 和 get_recent_orders,看起来独立,但后者需要前者作为查找键。顺序下先拿到 ID 一切正常,并行时订单查询可能在 ID 可用之前就发出去了。

你可以加排序逻辑,也可以反过来问:这两个工具本来是不是就不该存在?合成 get_customer_summary,依赖直接消失,接口更干净,往返次数还少了一次。这也是个可复用的信号——那些总是被一起调用、顺序固定、互相有依赖的工具,通常只是伪装成两个工具的一个工具。顺序执行让这种设计坏味道隐形,并行把它逼出来。

这里判断标准是:如果一个工具只在特定执行上下文或者特定前置调用之后才有意义,它属于编排逻辑,不该作为公开工具暴露出去。把顺序约束从工具实现搬进编排层,是让并行在大规模下安全的前提。

依赖图值得吗

更长期的方案是别再靠执行顺序去推断依赖,而是显式编码成依赖图。LangGraph 把工作流表示为图,节点是 Agent 或工具调用,边是数据流,你能直接声明两个读节点并行、写节点等两个读依赖都完成才启动。LLMCompiler 把模型生成的计划编译成 DAG 执行,据其论文报告,多步任务在保持正确性的前提下比顺序执行快最多 2.2 倍。

代价是真实的:维护依赖图要么对工具声明做静态分析,要么运行时做依赖追踪,动态 Agent 的工具集一变就要跟着改。工具库小且稳定的 Agent,读写分类规则更简单也更够用。工具数量到几十个、或者多步工作流已经成为主要负载时,显式 DAG 才是有扩展性的那种架构。顺带一提,依赖图的另一个好处是可观测:出问题时你能看清哪个节点失败、收到什么输入、同时有哪些同批节点在跑、有没有谁写了共享状态;隐式顺序循环里排查竞态,你只有错误输出倒推这一条路。

发布前的检查清单

  1. 列出全部工具,按只读、只写、读-改-写三类分开。
  2. 对每个写工具列清它触及的资源,确认有没有可能同时跑的工具碰同一份资源。
  3. 逐个检查工具实现里是否有隐式前置依赖——不看签名,看共享变量、会话状态和文件系统路径。
  4. 在开发环境显式打开并行跑测试。一直顺序测试的团队,测试的系统跟线上跑的不是同一个。

工具设计到位时,并行给的是实打实的延迟收益,那些省下来的时间可以换成每步更多的工作量。但跳过上述审计的团队通常是在生产环境里第一次遇到耦合,往往已经有不少状态被静默弄坏了。耦合一直都在,顺序执行只是让它没露头。

转载请注明出处: 皮哈皮阿

本文的链接地址: https://blog.phpat.com/agent-engineering/613.html

手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

您可能对以下文章感兴趣
1 / 1
预览图