Agent 并发调用多个 LLM 时,如何正确处理超时和取消?
Agent 并发调多个 LLM 时,超时和取消的核心难点是:既要保证整体响应速度,又不能留下“孤儿任务”白白消耗 token 和连接。
我直接说我们常用的处理方式👇
🎯 核心思路:结构化并发 + 超时 + 取消传播¶
原则只有一个:所有子任务的生命周期必须嵌套在父任务之内,超时或某个子任务失败时,取消必须级联传播,确保资源全部释放。
🛠️ 方案一:asyncio.wait + FIRST_COMPLETED + timeout(Python 原生灵活方案)¶
适合:部分结果即够用,超时后直接放弃未完成者。
import asyncio
async def call_llm(prompt, model):
# 模拟调用大模型
...
async def agent_orchestrate():
tasks = {
asyncio.create_task(call_llm("...", "gpt-4")),
asyncio.create_task(call_llm("...", "claude-3")),
}
done, pending = await asyncio.wait(
tasks, timeout=10.0, return_when=asyncio.FIRST_COMPLETED
)
# 关键:取消所有还在跑的任务
for task in pending:
task.cancel()
# 收集已完成的结果(过滤取消异常)
results = []
for task in done:
try:
results.append(task.result())
except asyncio.CancelledError:
pass
return results
✅ 传播链:wait 的超时会直接让循环退出,我们显式调用 task.cancel(),内部 call_llm 中的 await 会抛出 CancelledError,如果底层 http 库支持取消(如 httpx 的 aclose),连接也会被中断。
🛠️ 方案二:asyncio.gather + asyncio.wait_for(需要全部结果时)¶
async def agent_all_needed():
try:
results = await asyncio.wait_for(
asyncio.gather(
call_llm("...", "gpt-4"),
call_llm("...", "claude-3"),
),
timeout=15.0
)
except asyncio.TimeoutError:
# gather 会自动取消内部所有任务,无需手动 cancel
raise
⚡ 注意:gather 在收到取消(或超时引发的 TimeoutError)时,会自动取消它创建的所有子任务,这就是结构化并发的好处——不用手动打扫战场。
⚠️ 必须避开的坑¶
- 忽略
CancelledError导致孤儿任务
async def call_llm(...):
try:
async with client.post(...) as resp:
...
except asyncio.CancelledError:
# 必须手动关闭未完成的连接/资源,不能吞掉
await client.aclose()
raise # 重新抛出,保证取消信号继续传播
-
混用同步阻塞代码 在协程里调用
time.sleep(5)会让整个线程卡住,cancel无法中断。必须用await asyncio.sleep(5)。 -
底层 http 库不支持异步取消 选型时确认 SDK 支持
httpx.AsyncClient或原生aiohttp,否则cancel只是单方面在 Python 层面停止,网络连接可能还挂着占用资源。
🧠 一句话总结¶
并发 LLM 调用的超时与取消,靠的是结构化并发(任务树)+
await传递CancelledError+ 底层异步 IO 支持。 只要任务间层次清晰,取消就能自动向下传播,干净利落。
处理得当,Agent 既能快速失败,又不会泄露半个连接。如果有具体的编排库(如 LangGraph、Dify),可以再聊聊它们内置的超时重试机制。