跳到主要内容

串行工具调用瀑布:Agent循环中隐藏的延迟税

· 阅读需 10 分钟
Tian Pan
Software Engineer

如果你曾剖析过一个莫名其妙跑得很慢的AI Agent,大概率会发现一个瀑布。Agent调用工具A,等待,再调用工具B,等待,再调用工具C——即便B和C根本不依赖A的结果。你为1倍的工作量付出了3倍的延迟。

这个模式并非边缘情况,而是几乎所有Agent框架的默认行为。模型在单次响应中返回多个工具调用,执行循环则逐一按顺序运行它们。修复并不复杂,但前提是要有一种可靠的方法来识别哪些调用真正相互独立。

这个问题在结构上与ORM中的N+1查询问题如出一辙。在传统后端,缺乏经验的开发者先获取100条订单列表,再对每条订单发起100次独立的数据库查询来获取客户信息——而一个JOIN就能搞定。代码看起来没问题,性能却是灾难。在Agent循环中,等价情形是:LLM生成5个独立的search()调用来回答一个对比问题,而你的编排器尽职尽责地把它们串成链式执行,而不是扇形展开。

为什么Agent框架默认串行执行

机制很简单。当模型返回包含多个工具调用的响应时,最简单正确的实现就是一个for循环:遍历列表,逐一执行,收集结果,继续下一步。大多数框架出厂就是这样设计的,而且它能用——就"产出正确结果"这个意义上而言。

问题在于,当工具调用是I/O密集型时,"正确"和"快速"并不等价。每个访问外部API、数据库或向量存储的工具调用,大部分时间都在等网络。串行循环会把这些等待时间叠加起来:

  • 工具A:300ms网络I/O
  • 工具B:200ms网络I/O
  • 工具C:400ms网络I/O
  • 串行总计:900ms
  • 并行总计:400ms(最慢调用的耗时)

在多轮Agent循环中,实际影响会叠加放大——模型在多个推理步骤中都会发起工具调用。对执行研究类任务的Agent进行生产追踪,通常会发现60%–70%的总延迟来自本可并行化的串行I/O。

Anthropic内部针对多Agent系统的研究发现,在复杂研究查询中,用具有并发工具执行能力的并行子Agent替换单个串行Agent,任务完成时间最多可降低90%。LLMCompiler架构是学术界针对此问题的一种经过充分基准测试的方案,在标准基准测试中显示延迟改善了1.4倍到3.7倍,更值得注意的是成本降低了4.65倍——因为将更多工作批量压缩到每次LLM调用中,减少了LLM的往返次数。

依赖图:你的诊断工具

在并行化工具调用之前,你需要知道哪些调用可以安全并发执行。答案来自对每对工具调用的一个简单问题:调用B的输入是否依赖调用A的输出?

如果是,它们必须串行。如果否,它们可以并行。将其表示为有向无环图(DAG),每个节点是一个工具调用,每条边表示依赖关系。没有入边(无依赖)的节点可以同时执行。该批次完成后,再执行图的下一层——依赖项已满足的节点——依此类推。

在实践中,构建这个图并不需要复杂的机制。大多数Agent任务属于以下三种形态之一:

纯扇出:多个独立调用,无共享依赖。"比较五个城市的天气"会生成五个get_weather调用,彼此互不依赖。全部同时运行。

线性链:每个调用依赖前一个的结果。"获取用户账户ID,然后获取订单历史,再推荐产品"本质上是串行的——没有账户ID就无法查订单。这里没有可用的并行性,这是正确的。

混合DAG:两者的组合。"获取公司数据,然后并行获取CEO简介、营收数据和专利申请,最后综合分析竞争态势。"第一个调用是串行的,因为其他所有调用都依赖它。接下来三个是可并行的。最终的综合步骤是串行的,因为它依赖前三个。

最重要的诊断习惯是学会区分混合DAG与伪线性链。Agent经常生成看似线性链实则是混合DAG的调用序列——LLM恰好按依赖顺序列出了调用,却没有标明哪些可以重叠。

实践中识别依赖关系

对于简单的工具调用,依赖分析归结为检查参数值。如果工具调用B使用的是提示或上下文中硬编码的字面量参数,它是独立的。如果工具调用B使用$result_of_A作为参数——引用前一个调用输出的占位符——它是依赖的。

LLMCompiler架构通过三阶段设计将此形式化:

  1. 规划阶段:模型输出一个结构化计划,工具调用使用变量语法显式引用前序结果(例如search(query=$1.output)),使依赖关系机器可读。

  2. 任务获取单元:对依赖图进行拓扑排序。在每一步,它识别所有依赖项已满足的节点并调度并行执行。

  3. 合并器:每批执行后,将结果反馈到上下文中,解析占位符变量,要么返回最终答案,要么对原始计划规格不足的情况触发第二轮规划周期。

你不需要完整实现LLMCompiler也能从核心洞察中受益。即使是一个简单的检查——"这个工具调用的参数列表是否包含对前一个工具调用输出的引用?"——也足以将一批调用拆分为独立和依赖两组,并并发执行独立组。

现代框架支持也有了显著改善。OpenAI的Agents SDK在模型设置中暴露了parallel_tool_calls=True。AutoGen默认在模型单次响应生成多个工具调用时并行运行。LangGraph支持无共享状态的图分支并行节点执行。Google的Agent Development Kit内置了一流的并行Agent原语。如果你使用这些框架之一,能力已经具备——你可能只需要显式启用它。

协调开销的数学

并行不是免费的。有两个具体的成本决定它是否合算:协调开销和资源竞争。

协调开销是你的编排器扇出调用、跟踪完成状态、重组结果所做的工作。对于5个简单API调用的纯扇出,这个开销可以忽略不计——微秒级的异步记账对抗数百毫秒的I/O。数学强烈支持并行化。

当你扩展到很多Agent或工具时,这个计算会改变。管理50个并发操作的编排器必须跟踪所有操作的状态、在依赖项解决时路由新任务、并维护一个连贯的工作空间模型。超过某个Agent数量,编排器本身就成了瓶颈。在多Agent系统上工作的生产团队一致报告,初始部署的甜蜜点是3–5个并发Agent。从20个以上Agent起步的团队往往发现协调开销抵消了理论上的大部分加速。

实践建议:在尝试多Agent编排之前,先在单个Agent内部的工具调用层面实现并行执行。单个Agent并行执行5个工具,比5个Agent各执行一个工具,更容易推理、运行成本更低,而且往往速度一样快。

资源竞争是另一个失效模式,而且更隐蔽。如果你的5个并行工具调用都访问同一个受速率限制的外部API,你已经把延迟优化变成了限流触发器。每秒1个请求的串行执行完全没问题;5个同时对同一API发出请求可能突破速率限制,触发指数退避,最终延迟比你开始时还要糟糕。

解决方法是检查并行调用是否针对独立或共享的受速率限制资源。独立资源——不同的API、向量存储和SQL数据库、网络搜索和文件读取——可以干净地并行化。对同一个受速率限制端点的调用需要并发上限或请求错峰。

何时串行才是正确的

并非所有情况都是瀑布。存在一个反方向的失效模式:因为感觉像是优化,就把并行化强加给本质上串行的工作流。

串行执行是正确选择的最清晰信号:任务是一条推理链,其中每个步骤都会实质性地改变你在下一步的行为方式。如果你在调试一个错误,你会查看堆栈跟踪,形成假设,检查相关代码,根据所见更新假设,然后决定下一步看什么。每个工具调用都以静态依赖图无法捕捉的方式塑造后续调用的意图。试图将此预先规划为DAG会产生脆弱的计划,当现实与预先假设不符时就会崩溃。

次要信号:对于轻量级工具有非常严格的延迟要求。如果每个工具调用耗时不到20ms(进程内函数调用、本地缓存查找),并行化的异步协调开销实际上可能超过节省的时间。优化前先测量。

决策框架

当你在Agent循环中遇到一批工具调用时:

  1. 检查参数依赖。任何调用的输入是否引用了另一个调用的输出?如果是,这两个调用必须有序。如果否,它们是并行化的候选。

  2. 检查资源重叠。并行候选是否针对同一个受速率限制的API或共享可变状态?如果是,应用并发上限。如果否,完全并行运行。

  3. 检查推理依赖。即使没有硬性参数依赖,看到调用A的结果是否会改变你对调用B的期望?如果是,串行运行并让模型重新规划。这主要适用于探索性或调试工作流。

  4. 衡量调用的形态。纯扇出从并行化中获益最大。混合DAG在每个并行阶段获得部分收益。线性链什么都得不到——这是正确答案,而非失败。

N+1查询问题在后端系统中持续了多年,因为瀑布模式在代码审查中看起来正确,性能问题只有在负载下才会显现。Agent工具调用瀑布具有相同的特性:它们通过所有功能测试,产出正确答案,却在每次请求上悄无声息地向你收取3倍的延迟费用。解决方案——依赖分析和并发执行——并不复杂。前提是在你的追踪中识别出这个模式。

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