我的 Agent 要调十几个工具才能完成一个任务,它老老实实一个接一个地串行调,结果慢得用户都快等睡着了的深度复盘
这是一个让我对"并行"在 Agent 里有多重要刻骨铭心的故事。我做了一个 AI Agent,它要完成一个稍微复杂点的任务——比如,生成一份报告,需要去查好几个不同的数据源:查销售数据、查用户数据、查库存数据、查竞品数据……加起来,要调用十几个工具。Agent 本身工作得很正确,该调的工具都调了、结果也对。可它慢得令人发指:用户点一下,要等好久好久,才出结果,体验差到了极点。
我一开始以为是某个工具慢,去优化单个工具,可效果甚微。直到我把 Agent 执行的全过程、每一步的耗时拉出来一看,才发现了那个刺眼的真相:我的 Agent,是严格地、一个接一个地"串行"调用那十几个工具的!它调完第一个、拿到结果,才去调第二个;调完第二个,才去调第三个……整个任务的总耗时,就是这十几个工具耗时的"总和"。而其中,每个工具(查个数据库、调个接口),自己其实都不算太慢(比如各 1 秒);可十几个 1 秒,串行加起来,就是十几秒——用户,可不就等到快睡着了。我当时一拍脑袋:可这十几个查询,明明大多是互相独立的啊!查销售数据,和查库存数据,之间没有任何依赖,完全可以同时进行!为什么要傻傻地等一个查完、再查下一个?这一问,让我看清了问题的本质,补上了关于 Agent 性能最重要的一课:问题的核心,是我把一堆"本可以并行"的独立操作,串行地执行了。一个 Agent 完成任务,常常需要调用多个工具;而这些工具调用之间,有的有依赖(B 要用 A 的结果,必须 A 完了才能 B),但很多是相互独立的(查销售和查库存,谁也不用等谁)。对于那些相互独立的工具调用,如果你还一个接一个地串行执行,那就是极大的浪费:总耗时,白白变成了"各项之和";而本可以让它们同时进行,让总耗时,缩短到约等于"最慢的那一项"。我那个慢得要命的 Agent,正是因为,把十几个本该并发的独立查询,排成了一条长长的、串行的队;用户的每一秒等待里,大部分时间,都浪费在了"等一个本不需要等的、独立操作"上。
故障现场:十几个独立工具,排成串行的长队
我把这个"串行调用"的现场,用代码和耗时摊开给你看:
# ✗ 灾难: 串行调用一堆"本可并行"的独立工具
async def gen_report():
sales = await call_tool("query_sales") # 1s
users = await call_tool("query_users") # 1s
stock = await call_tool("query_stock") # 1s
competitor = await call_tool("query_competitor") # 1s
# ... 十几个
# ✗ 每个都"await 等它完成, 再调下一个" → 严格串行!
return summarize(sales, users, stock, competitor, ...)
# 耗时: 1 + 1 + 1 + 1 + ... = 十几秒!(各项之和)
# 而这些查询, 大多互相独立(查销售不依赖查库存)→ 本可同时进行!
# 关键观察: 工具调用之间的"依赖关系"
# - 独立的(无依赖): 查销售、查用户、查库存、查竞品 —— 谁也不用等谁。
# - 有依赖的: "先查到用户 id, 再用 id 查订单" —— 后者必须等前者。
# → 独立的, 本该并行; 有依赖的, 才必须串行。
# 我的错: 把"独立的"也串行了 → 总耗时 = 各项之和(本可 ≈ 最慢一项)。
# 正解方向(下一节细讲):
# - 独立的工具调用 → 并发执行(asyncio.gather / 并行 tool call)。
# - 总耗时从"求和", 降到"最慢的一项"。
# 根因: 把"本可并行的独立操作", 串行执行了。
# 总耗时白白变成"各项之和", 而非"最慢一项"。
# (现代 LLM 还支持"并行工具调用"——一次返回多个独立的 tool call)
看着这十几个串行排队的工具调用,我才算真正理解了这个"慢得要命"的根源。问题的核心,是我把一堆"本可以并行"的独立操作,串行地执行了。一个 Agent 完成任务,常常需要调用多个工具;而这些工具调用之间,存在两种关系:一种是有依赖的(比如"先查到用户 id,再用这个 id 去查订单"——后者必须等前者完成);另一种是相互独立的(比如"查销售数据"和"查库存数据"——谁也不用等谁,可以同时进行)。而道理很清楚:独立的,本该并行;有依赖的,才必须串行。可我犯的错,是把那些"相互独立"的工具调用,也一个接一个地串行执行了。这就造成了巨大的浪费:我的报告任务,需要十几个独立的查询,每个各耗时 1 秒;串行执行,总耗时就是它们的"总和"——十几秒;而如果让这些独立的查询同时进行(并发),总耗时,就能缩短到约等于"最慢的那一项"——可能就 1~2 秒。这中间,差了将近一个数量级。用户感受到的那漫长的等待里,绝大部分时间,都白白浪费在了"等一个本不需要等的、独立的操作"上。归根结底:我那个慢得要命的 Agent,病根不在于某个工具慢,而在于我把十几个本该并发的独立查询,排成了一条长长的、串行的队;总耗时,被白白地,从"最慢的一项",拉长成了"所有项之和"。(而且,现代的 LLM,本身就支持"并行工具调用"——它可以在一轮里,一次性返回多个相互独立的 tool call,就是为了让你能并发地执行它们;我连这个能力,都没用上。)
第一件事:搞懂独立操作要并行、依赖操作才串行
定位到根源,我必须把"什么时候该并行、什么时候该串行"彻底想清楚:
独立的操作要"并行", 有依赖的才"串行"
# 多个操作(工具调用)之间, 有两种关系:
# 1. 独立(无依赖): 互不需要对方的结果。
# - 例: 查销售 / 查库存 / 查用户 —— 谁也不用等谁。
# - 它们"可以同时进行"。
# 2. 有依赖: 后者需要前者的结果。
# - 例: 查用户 id → 用 id 查该用户的订单。后者必须等前者。
# - 它们"必须按顺序"。
# 串行 vs 并行的耗时差异:
# - 串行(一个接一个): 总耗时 = 各项耗时之和。
# - 并行(同时进行): 总耗时 ≈ 最慢的那一项。
# - 例: 10 个各 1s 的独立操作:
# 串行 = 10s; 并行 ≈ 1s。差 10 倍!
# 黄金法则: 独立的操作, 要并行; 只有"有依赖"的, 才串行。
# - 别把"本可并行的独立操作", 傻傻地串行执行(浪费时间)。
# - 也别把"有依赖的操作"强行并行(会出错——后者拿不到前者的结果)。
# 在 Agent 里, 尤其重要:
# - Agent 常要调"很多个"工具, 串行的浪费会被放大(十几个 = 十几倍延迟)。
# - 现代 LLM 支持"并行工具调用": 一轮返回多个独立的 tool call,
# 就是让你"同时执行"它们 → 充分利用这个能力。
# 怎么判断能不能并行? 看"数据依赖":
# - B 用到 A 的输出吗? 用到 → 有依赖, 串行。 没用到 → 独立, 可并行。
# - 把任务拆成"依赖图", 没有依赖关系的, 都可以并发。
# 核心: 独立操作并行(耗时降到最慢一项), 有依赖的才串行。
# 别让"本可并发的独立操作", 排成串行的长队。
原理终于清晰了。多个操作(工具调用)之间,有两种关系:独立(无依赖)——互不需要对方的结果(如查销售/查库存/查用户,谁也不用等谁),它们"可以同时进行";有依赖——后者需要前者的结果(如"查用户 id → 用 id 查该用户的订单",后者必须等前者),它们"必须按顺序"。而串行和并行的耗时差异,是巨大的:串行(一个接一个),总耗时 = 各项耗时之和;并行(同时进行),总耗时 ≈ 最慢的那一项。举例:10 个各 1 秒的独立操作,串行要 10 秒,并行只需约 1 秒——差了 10 倍!由此,就有了一条黄金法则:独立的操作,要并行;只有"有依赖"的,才串行。一方面,别把"本可并行的独立操作",傻傻地串行执行(那是巨大的时间浪费,正是我的坑);另一方面,也别把"有依赖的操作"强行并行(那会出错——后者会拿不到前者的结果)。而这在 Agent 里,尤其重要:Agent 常常要调用"很多个"工具,所以串行的浪费会被放大(十几个工具串行,就是十几倍的延迟);而现代的 LLM,本身就支持"并行工具调用"——它能在一轮里,返回多个相互独立的 tool call,就是为了让你同时执行它们,要充分利用这个能力。那怎么判断能不能并行?看"数据依赖":B 用到了 A 的输出吗?用到了,就是有依赖、得串行;没用到,就是独立、可并行。把任务拆成一张"依赖图",其中没有依赖关系的节点,都可以并发执行。归根结底:独立操作要并行(让总耗时降到最慢的一项),只有有依赖的才串行;别让"本可并发的独立操作",排成一条串行的长队——这,是我用一个"慢得用户快睡着"的 Agent,补上的、关于性能最关键的一课。
第二件事:正解——独立的工具调用并发执行
搞懂了根因——"独立操作被串行了"——正解就清晰了:把那些相互独立的工具调用,并发地执行(用 asyncio.gather/Promise.all 等);只有有依赖的,才按顺序串行。把总耗时,从"各项之和",降到"最慢的一项"。同时,充分利用 LLM 的"并行工具调用"能力。
# 正解1: 独立的工具调用, 并发执行(asyncio.gather)
async def gen_report():
# 这几个查询互相独立 → 一起发出去, 同时进行!
sales, users, stock, competitor = await asyncio.gather(
call_tool("query_sales"), # 这些
call_tool("query_users"), # 同时
call_tool("query_stock"), # 进行
call_tool("query_competitor"), # !
)
return summarize(sales, users, stock, competitor)
# 耗时: ≈ 最慢的那一项(约 1s), 而不是它们的总和(十几秒)!
# (JS: await Promise.all([...]); Go: errgroup / 多个 goroutine + WaitGroup)
# 正解2: 有依赖的串行, 独立的并行(混合)
async def task():
user = await call_tool("get_user") # 先拿到 user(后面要用它)
# 下面两个都依赖 user, 但它俩之间独立 → 并发
orders, prefs = await asyncio.gather(
call_tool("get_orders", user.id), # 用 user.id
call_tool("get_prefs", user.id), # 用 user.id, 但和 orders 独立
)
return combine(user, orders, prefs)
# → 按"依赖图"组织: 有依赖的串行(user → orders/prefs), 独立的并行(orders||prefs)。
# 正解3: 利用 LLM 的"并行工具调用"
# - 现代模型(支持 parallel tool calls)会在一轮里返回"多个 tool call"。
# - 你的执行器, 要"并发地"执行这多个 tool call(而不是循环里一个个串行调)!
for tc in response.tool_calls: # ✗ 别这样串行执行多个 tool call
results.append(await call_tool(tc))
results = await asyncio.gather(*[call_tool(tc) for tc in response.tool_calls]) # ✓ 并发
# 正解4: 控制并发度(别一下并发几百个压垮下游)
# - 用信号量/并发池限制"同时最多 N 个"(见连接/资源那几篇)。
# sem = asyncio.Semaphore(10)
# 核心: 独立的工具调用并发(gather/Promise.all), 把总耗时从"求和"降到"最慢一项";
# 有依赖的才串行; 并充分用上 LLM 的并行工具调用能力。
这套正解,核心就一句话:把相互独立的工具调用,并发地执行。正解1(独立调用并发):用 asyncio.gather(Python)、Promise.all(JS)、errgroup/多 goroutine(Go)这类机制,把那几个互相独立的查询,一起发出去、同时进行;这样,总耗时,就从它们的"总和"(十几秒),降到了约等于"最慢的那一项"(1 秒左右)。正解2(混合:有依赖串行、独立并行):真实任务往往是混合的——比如"先拿到 user(后面要用它),再用 user.id 去查 orders 和 prefs";那就按"依赖图"来组织:有依赖的部分串行(user → orders/prefs),而那两个都依赖 user、但彼此独立的查询(orders 和 prefs),则并发执行。正解3(利用 LLM 的并行工具调用):现代模型支持 parallel tool calls——它会在一轮里返回多个 tool call;而你的执行器,要并发地执行这多个 tool call(gather(*[...])),而不是傻傻地在循环里一个个 await 串行调——很多人,正是在这里,把模型给的"并行机会"又写成了串行。正解4(控制并发度):并发也要有度,别一下子并发几百个、把下游压垮——用信号量/并发池,限制"同时最多 N 个"(这和连接池、资源管理那几篇的道理一致)。归根结底:独立的工具调用,并发执行(gather/Promise.all),把总耗时从"求和",降到"最慢的一项";只有有依赖的,才串行;并充分用上 LLM 的并行工具调用能力。我那次的错误,正是把一堆独立查询排成了串行的队;而正解,就是把它们,并排放出去、同时跑。
下面这张图,对比了"全串行"和"独立并行"两条路径:
这张图的对比很清楚:左边红色那条,把所有工具调用都当成有依赖、一律串行,一个接一个 await,总耗时是各项之和、十几秒,用户等到睡着;右边绿色那条,分清依赖关系,把独立的用 gather 同时跑、只有有依赖的才按顺序串行,总耗时约等于最慢的一项、1~2 秒,飞快。两条路的根本分野,在于你有没有把"相互独立"的操作,并发起来。
第三件事:Agent 性能优化的其它手段
填平了"串行"这个坑,我系统梳理了一遍 Agent 性能优化的其它手段:
Agent 性能优化的其它手段:
# 1. 独立工具调用并发(本文): gather/Promise.all, 耗时从求和降到最慢一项。
# 2. 减少不必要的步骤/轮次:
# - 每多一轮"LLM 思考", 就多一次 LLM 调用(慢且贵)。
# - 让 Prompt/工具设计得好, 让 Agent 用更少的步数完成 → 既快又省。
# 3. 流式输出(streaming):
# - 别等全部生成完才返回; 边生成边流式给用户 → 感知延迟大降。
# 4. 缓存:
# - 相同的工具调用/LLM 调用, 结果可缓存(别重复算)。
# - (注意缓存一致性, 见缓存那篇)
# 5. 用更快/更小的模型做简单步骤:
# - 不是每步都需要最强的模型; 简单判断用小快模型, 难的才上大模型。
# 6. 预取 / 推测执行:
# - 在等待时, 预先做一些"很可能要用"的工具调用。
# 7. 减少上下文(也影响速度):
# - 上下文越长, LLM 处理越慢(也越贵)。精简上下文(见上下文那篇)。
# 8. 超时 + 兜底: 慢工具设超时, 别让一个慢调用拖垮整体(见超时那篇)。
# 综合: Agent 的延迟 = LLM 调用次数 × 每次延迟 + 工具调用(可并发)。
# 优化: 减少轮次、并发工具、流式、缓存、模型分级、精简上下文。
# 核心: Agent 性能, 首要是"独立操作并发"; 再加上减轮次、流式、缓存等。
# 别让用户, 为本可省下的等待买单。
这一梳理,让我对 Agent 的性能优化,有了体系化的认识。除了"独立工具调用并发"(本文),还有不少手段:减少不必要的步骤/轮次(每多一轮"LLM 思考",就多一次慢而贵的 LLM 调用;把 Prompt 和工具设计好,让 Agent 用更少的步数完成,既快又省);流式输出(别等全部生成完才返回,边生成边流式给用户,大幅降低感知延迟);缓存(相同的工具调用/LLM 调用结果可缓存,别重复算);模型分级(不是每一步都需要最强的模型,简单判断用小快模型、难的才上大模型);预取/推测执行(在等待时,预先做一些"很可能要用"的调用);精简上下文(上下文越长,LLM 处理越慢,精简它也能提速);超时 + 兜底(慢工具设超时,别让一个慢调用拖垮整体)。综合来看:Agent 的延迟 ≈ LLM 调用次数 × 每次延迟 + 工具调用(可并发)的耗时;所以优化的方向就是:减少轮次、并发工具、流式输出、缓存、模型分级、精简上下文。归根结底:Agent 性能优化,首要的,是"独立操作并发";再加上减轮次、流式、缓存等手段——别让用户,为那些本可以省下的等待,白白买单。
第四件事:从"依赖图"看任务的可并行性
这次踩坑,让我学会了用"依赖图"的视角,去分析一个任务里,什么能并行、什么必须串行:
用"依赖图"分析任务: 什么能并行, 什么必须串行
# 把任务里的操作, 画成一张"依赖图(DAG, 有向无环图)":
# - 节点 = 一个操作(工具调用)。
# - 边 = 依赖(B 依赖 A, 则 A → B, B 必须等 A)。
# 例: 生成报告
# 查销售 ─┐
# 查用户 ─┤
# 查库存 ─┼─→ 汇总 → 生成报告
# 查竞品 ─┘
# (4 个查询互相独立 → 可并行; "汇总"依赖它们全部 → 等它们都完成)
# 例: 有依赖链
# 查用户id → 查该用户订单 → 查订单详情
# (一条链, 全是依赖, 只能串行)
# 例: 混合
# 查用户 ─→ 查订单 ─┐
# └→ 查偏好 ─┴→ 汇总
# (订单、偏好都依赖用户, 但彼此独立 → 那两个并行; 都依赖"查用户")
# 从依赖图能读出:
# - 没有依赖关系的节点(同一"层")→ 可并发执行。
# - 有依赖的(前后层)→ 必须串行(等前面的完成)。
# - 总耗时 ≈ "依赖图中最长的那条路径(关键路径)"的耗时。
# (而不是所有节点之和——只要你把能并行的都并行了)
# 实践: 把"能并发的同一层", 用 gather 一起跑; 层与层之间串行。
# → 总耗时 = 各层"最慢节点"之和 ≈ 关键路径, 远小于"所有节点之和"。
# 核心: 用依赖图看任务——同层(无依赖)并行, 跨层(有依赖)串行。
# 优化目标: 让总耗时逼近"关键路径", 而非"所有操作之和"。
这一思考,让我有了一个分析任务并行性的强大工具——依赖图。把任务里的操作,画成一张"依赖图(DAG,有向无环图)":节点是一个操作(工具调用),边是依赖(B 依赖 A,则 A→B,B 必须等 A)。看几个例子就清楚了:"生成报告"——查销售、查用户、查库存、查竞品四个查询互相独立(可并行),而"汇总"依赖它们全部(要等它们都完成);"有依赖链"——查用户 id → 查订单 → 查详情,一条链全是依赖,只能串行;"混合"——查订单和查偏好都依赖查用户、但彼此独立,所以那两个可以并行,而都得等"查用户"先完成。而从依赖图里,能读出关键的信息:没有依赖关系的节点(在同一"层"),可以并发执行;有依赖的(前后层),必须串行;而最重要的——整个任务的总耗时,约等于"依赖图中最长的那条路径(关键路径)"的耗时(只要你把能并行的,都并行了),而不是所有节点耗时之和。落到实践:把"能并发的同一层",用 gather 一起跑;层与层之间,才串行;这样,总耗时 = 各层"最慢节点"之和 ≈ 关键路径,远远小于"所有节点之和"。归根结底:用依赖图来看任务——同层(无依赖)并行,跨层(有依赖)串行;而优化的目标,是让总耗时逼近"关键路径",而不是"所有操作之和"。我那次的错,正是把一张本可以"压扁成几层"的依赖图,拉成了一条长长的串行链。把串行和并行(关键路径)的差异,整理成一张表:
| 场景 | 串行总耗时 | 并行(关键路径) | 差异 |
|---|---|---|---|
| 10 个各 1s 独立查询 | 10s(求和) | ≈1s(最慢一项) | 10 倍 |
| 查用户→(订单‖偏好各1s) | 3s | ≈2s(用户1+并行1) | 1.5 倍 |
| 纯依赖链 A→B→C 各1s | 3s | 3s(无法并行) | 无 |
| 本质 | 所有节点之和 | 依赖图最长路径 | 看可并行度 |
第五件事:别让"本可并行"的事情,白白串行
这次踩坑,在认知层面给了我最大的纠偏——它让我形成了一种"找并行机会"的性能直觉。我把这层反思,沉淀了下来:
认知纠偏: 别让"本可并行"的独立工作, 白白串行排队
# 我的误解(错误的):
# 我"按代码书写的顺序", 一行行 await 地写下来, 自然就成了串行;
# 没有主动去想"这些操作之间, 到底有没有依赖、能不能同时做"。
# → 我默认了串行, 而没有主动寻找"并行的机会"。
# 真相: 很多"等待", 是不必要的——独立的事, 本可以同时做
# - 串行写法最直观(一行行往下), 所以人们"默认"就串行了。
# - 但只要操作之间"没有数据依赖", 它们就"本可以并行"。
# - 把本可并行的串行了 = 白白浪费时间(等一个不需要等的)。
# - 这种浪费, 在"操作多、又慢(IO/网络)"时, 尤其巨大(被乘以数量)。
# 这是一个普遍的性能优化视角: "找出可并行的, 让它们并行"
# - Agent 调多个独立工具(本文)。
# - 页面要请求多个独立接口 → 并发请求, 别瀑布式串行。
# - 批处理多个独立任务 → 并发/并行处理。
# - 任何"一堆独立的、慢的操作", 都该考虑并行。
# 正确的习惯:
# 1. 看到"多个慢操作"时, 主动问: 它们之间有依赖吗? 能并行吗?
# 2. 把"独立的"并行(gather/Promise.all/并行池), 只串行"有依赖的"。
# 3. 用"依赖图/关键路径"思维, 让总耗时逼近"最长依赖链", 而非"求和"。
核心: 别让"本可并行的独立工作", 白白串行排队。
主动寻找并行机会, 把独立的事同时做——这是性能优化最朴素也最有效的一招。
这层反思,是这次踩坑给我最高维度的收获。复盘我的误解,根源是:我"按代码书写的顺序",一行行 await 地写下来,自然而然就成了串行;我没有主动去想"这些操作之间,到底有没有依赖、能不能同时做"。我默认了串行,而没有主动去寻找"并行的机会"。可真相是:很多"等待",其实是不必要的——独立的事,本可以同时做。串行的写法,最直观(一行行往下),所以人们就"默认"了串行;但只要操作之间"没有数据依赖",它们就"本可以并行";把本可并行的串行了,就是白白浪费时间(在等一个根本不需要等的东西);而这种浪费,在"操作多、又慢(IO/网络)"时,尤其巨大(因为它被乘以了数量)。而这,是一个普遍的性能优化视角——"找出可并行的,让它们并行":Agent 调多个独立工具(本文);页面要请求多个独立接口(并发请求,别瀑布式串行);批处理多个独立任务(并发/并行处理)——任何"一堆独立的、慢的操作",都该考虑并行。由此,我形成了几个习惯:第一,看到"多个慢操作"时,主动问:它们之间有依赖吗?能并行吗?第二,把"独立的"并行(gather/Promise.all/并行池),只串行"有依赖的";第三,用"依赖图/关键路径"的思维,让总耗时逼近"最长的依赖链",而不是"所有操作之和"。归根结底:别让"本可并行的独立工作",白白串行排队;主动寻找并行机会,把独立的事同时做——这,是性能优化里,最朴素、也最有效的一招。我那个慢吞吞的 Agent,正是栽在了"默认串行、没找并行机会"上。把"默认串行"和"主动找并行"对比成一张表:
| 维度 | 默认串行(踩坑) | 主动找并行(成熟) |
|---|---|---|
| 写代码 | 顺手一行行 await | 先想依赖、再决定并行/串行 |
| 独立操作 | 傻傻串行排队 | 并发同时做 |
| 总耗时 | 各项之和 | 逼近关键路径 |
| 视角 | 没想过并行 | 主动找并行机会 |
| 典型 | Agent串行调工具/瀑布请求 | gather/并发请求 |
一套"多个工具调用该怎么编排"的决策流程
把这次踩坑的全部教训,我浓缩成了一张"Agent 要调多个工具、该怎么编排"的决策图,贴在了团队做 Agent 的文档里:
这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:Agent 要调多个工具,先画依赖图(谁依赖谁);再看这些调用有没有依赖——相互独立的用 gather/Promise.all 并发、有依赖的按顺序串行、混合的则"同层独立的并发、层间串行";并发时若会压垮下游就加并发上限控制。而当 LLM 一轮返回了多个 tool call 时,要并发执行它们,别在循环里串行调。最终,让总耗时逼近"关键路径"。这条"先画依赖图、独立的并发、有依赖才串行"的决策链,现在是我们团队编排 Agent 工具调用时的准则。
我立下的几条 Agent 性能规矩
这次"Agent 串行慢"的踩坑,让我把 Agent 性能的注意事项,认真地立成了几条规矩:
- 独立的工具调用要并发。用 gather/Promise.all,总耗时从"各项之和"降到"最慢一项"。
- 先理清依赖关系再编排。画依赖图:独立的并行、有依赖的串行、混合的同层并发层间串行。
- LLM 返回多个 tool call 要并发执行。别在循环里一个个 await 串行调,浪费了模型给的并行机会。
- 控制并发度。用信号量/并发池限制同时数,别一下并发太多压垮下游。
- 减少 LLM 轮次。每轮都是一次慢而贵的调用;好的设计让 Agent 用更少步数完成。
- 用流式输出 + 缓存 + 模型分级。流式降感知延迟、缓存避重复、简单步用小快模型。
- 主动找并行机会。见到多个慢操作就问能不能并行;让总耗时逼近关键路径,而非求和。
写在最后
这次"我的 Agent 串行调十几个工具、慢得用户快睡着"的经历,是我在 AI Agent 开发路上,一次很典型、也很受用的成长。它教给我的,远不止"工具调用要并发"这一条具体的技术经验,更是一个朴素而强大的性能优化视角——别让"本可并行的独立工作",白白串行排队;主动去寻找并行的机会。我那个慢吞吞的 Agent,根源就在于,我顺着代码书写的顺序,一行行 await 地写下来,默认就成了串行;却从没主动想过,那十几个查询,大多是互相独立的、本可以同时进行——我让用户的每一秒等待,都浪费在了"等一个本不需要等的操作"上。
所以,当你的代码,需要做"多个比较慢的操作"时(无论是 Agent 调多个工具、页面请求多个接口、还是批处理多个任务),请别顺手就一行行串行地写下去——而要停下来,主动问一句:"它们之间,真的有依赖吗?哪些是相互独立、本可以同时做的?"然后,把那些独立的,并发起来。就像那个 Agent,你只要把十几个独立的查询,用一个 gather 并排放出去,总耗时就从"它们的总和",一下子,缩到了"最慢的那一个",用户的等待,瞬间从十几秒,变成了一两秒。从"默认串行"到"主动找并行",从"总耗时是求和"到"总耗时逼近关键路径",是从一个"能让 Agent 跑通"的开发,走向一个"能让 Agent 又快又好"的工程师,必经的修炼。愿你编排的每一个任务,都该并行的并行、该串行的串行,把时间用在刀刃上;也愿你我,永远对那些"本可省下的等待",保持一份敏锐——别让用户,为我们的"默认串行",白白买单。共勉。
—— 别看了 · 2026