跳到主要内容

串行工具调用瀑布: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架构通过三阶段设计将此形式化:

加载中…
Let's stay in touch and Follow me for more thoughts and updates