Agent程序通常要同时访问多个外部服务、监听消息通道或者执行多步推理。同步模式下,每一次网络请求、数据库读取都会让整个执行流停下来等待,大量时间被浪费在无意义的IO等待上。例如一个订单处理Agent需要调用库存接口、风控接口和用户服务接口,如果依次串行调用,总耗时就是三次调用之和。AsyncIO通过事件循环和协程,让Agent在一个线程内就能重叠这些等待时间,显著提升吞吐量。

一、AsyncIO的事件循环与协程如何支撑Agent并发
AsyncIO的核心不是多线程,而是单线程下的事件循环。协程通过async def定义,执行到await表达式时会挂起当前协程,把控制权交还给事件循环。事件循环随即调度其他已经就绪的协程继续执行。这种协作式调度避免了线程切换的内核开销,也没有共享数据的锁竞争问题。
对于Agent这种IO密集型场景,协程的优势尤其明显。Agent的大部分时间都在等待远程API返回、读取消息队列或者写日志,这些操作天然可以被中断和恢复。用asyncio.gather同时发起多个协程,等待时间会被最大程度地重叠。下面这段代码同时请求三个服务,总耗时取决于最慢的那个请求,而不是三个请求的累加。
import asyncio
async def call_service(name: str, delay: float):
await asyncio.sleep(delay)
return f"{name} 返回结果"
async def main():
tasks = [
call_service("库存", 0.6),
call_service("风控", 0.8),
call_service("用户", 0.4),
]
results = await asyncio.gather(*tasks)
for item in results:
print(item)
asyncio.run(main())
这段代码中,三个协程同时启动,各自在asyncio.sleep处挂起,事件循环会在它们之间轮转。相比同步版本需要1.8秒,异步版本只需要0.8秒左右。Agent并发任务的规模越大,这种时间压缩效果越明显。
需要注意的是,AsyncIO并不能让CPU密集型计算变快,它优化的是等待时间。如果一个Agent任务内部有大量的数学计算、图像处理或正则匹配,应该把这些部分交给进程池或线程池,而不是放在事件循环里执行。这一点在后面会展开。
二、构建一个可扩展的Agent异步任务调度器
实际Agent系统往往需要处理动态到达的任务,而不是一次性把任务全部放进gather。这时可以用asyncio.Queue配合asyncio.Semaphore设计一个调度器。队列负责接收任务,信号量限制同时执行的任务数量,避免对下游服务造成过大压力。
下面是一个精简版Agent调度器的实现。它启动固定数量的worker协程,每个worker从队列中取出任务,在信号量保护下执行,并把结果或异常收集起来。
import asyncio
class AgentScheduler:
def __init__(self, max_workers: int = 5):
self.queue = asyncio.Queue()
self.semaphore = asyncio.Semaphore(max_workers)
self.results = []
async def worker(self, worker_id: int):
while True:
task_id, coro = await self.queue.get()
async with self.semaphore:
try:
result = await coro
self.results.append((task_id, result))
except Exception as exc:
self.results.append((task_id, exc))
finally:
self.queue.task_done()
async def start(self, worker_count: int = 3):
workers = [
asyncio.create_task(self.worker(i))
for i in range(worker_count)
]
await self.queue.join()
for w in workers:
w.cancel()
def submit(self, task_id, coro):
self.queue.put_nowait((task_id, coro))
async def sample_task(name: str):
await asyncio.sleep(0.3)
return name.upper()
async def main():
scheduler = AgentScheduler(max_workers=3)
for i in range(10):
scheduler.submit(i, sample_task(f"task-{i}"))
await scheduler.start(worker_count=3)
print(scheduler.results)
asyncio.run(main())
这段代码里,max_workers控制并发上限,Semaphore确保同一时间最多只有3个任务在运行。queue.join会等待所有任务被标记完成,之后再取消worker协程。这样即使后续还有新任务加入,调度器也能保持稳定。
工程上还可以在这个调度器上增加超时、重试和优先级。例如用asyncio.wait_for包裹协程执行,超过指定时间直接抛出TimeoutError并记录日志;重试逻辑可以捕获异常后把任务重新放回队列。这些机制能让Agent在面对不稳定外部服务时更加健壮。
三、避开AsyncIO常见阻塞陷阱
AsyncIO最容易被误用的地方,就是把同步阻塞调用直接写在协程里。time.sleep、requests.get、数据库驱动的同步查询方法都会卡住整个事件循环。因为事件循环运行在主线程中,一旦某个协程执行了阻塞操作,其他协程就完全得不到调度,异步并发会退化成串行阻塞。
对比下面两种写法:
import asyncio
import time
async def bad():
# 错误:阻塞事件循环2秒
time.sleep(2)
return "bad"
async def good():
# 正确:挂起2秒,让出控制权
await asyncio.sleep(2)
return "good"
如果Agent里必须使用同步的HTTP客户端,比如某些第三方库只提供阻塞接口,可以借助run_in_executor把阻塞调用扔到线程池中执行,主线程的事件循环继续运转。代码示例如下:
import asyncio
import requests
async def fetch_blocking(url):
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, requests.get, url)
async def main():
urls = ["https://ipipp.com/api/a", "https://ipipp.com/api/b"]
tasks = [fetch_blocking(u) for u in urls]
responses = await asyncio.gather(*tasks, return_exceptions=True)
for resp in responses:
if isinstance(resp, Exception):
print(resp)
else:
print(resp.status_code)
asyncio.run(main())
线程池的默认大小有限,适合IO密集型阻塞调用。如果是CPU密集型任务,则需要用进程池执行器,否则线程池中的任务同样会争抢GIL,难以充分利用多核。通过concurrent.futures.ProcessPoolExecutor配合run_in_executor,可以把计算任务隔离到独立进程,避免拖慢事件循环。
另一个常见优化点是设置合理的任务取消与异常隔离。使用asyncio.gather(..., return_exceptions=True)可以避免一个任务抛异常导致整个批次失败;对单个任务可以用asyncio.wait_for限制最长执行时间。调度器中的信号量也不宜设置过大,过高并发会触发下游服务限流,反而降低整体吞吐。
四、AsyncIO与多线程、多进程的选型边界
并不是所有Agent并发场景都适合AsyncIO。如果任务内部主要是纯Python计算或大量CPU操作,多进程往往更合适;如果任务已经封装成阻塞库且无法改造,多线程加线程池是更直接的方案。AsyncIO真正的价值在于大量IO等待且任务数量很多的情况下,用极低资源消耗完成高并发。
可以从资源占用和编程复杂度两个维度做判断。AsyncIO只需要一个线程,几万个并发协程也只占用少量内存;多线程虽然简单直观,但线程栈占用和上下文切换成本会随线程数快速上升。不过AsyncIO要求代码全程异步化,对已有同步库的改造有一定成本。Agent团队在选型时可以先用同步库快速验证业务,再针对瓶颈模块逐步替换为异步实现。
最终,Agent异步编程的关键在于理解事件循环的调度边界:IO等待可以异步化,阻塞调用必须隔离。只要守住这个边界,AsyncIO能把Agent的并发处理能力提升到新的量级。

