串行工具调用瀑布:Agent循环中隐藏的延迟税
如果你曾剖析过一个莫名其妙跑得很慢的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架构通过三阶段设计将此形式化:
