假如你运营着一个客服 Agent 服务,它自动处理退货、改价、物流查询。平时任务不多,一个实例就能轻松应对。但到了双十一,任务积压成山,用户的等待时间从 2 秒涨到 2 分钟。你急切地想:把 Agent 从 1 个扩到 10 个!

然后你发现,10 个 Agent 是跑起来了,但其中几个在排队等待同一个查询接口,另几个在重复读同一份订单数据,而最可笑的是,后半夜任务没了,10 个 Agent 依然坚挺地挂在那里,账单上的数字像心跳一样往上跳。
这就是动态 Agent 池要解决的问题:根据负载自动扩缩 Agent 实例。但你如果简单地把操作系统的线程池概念搬过来,十有八九会翻车。我最初就翻了。
Agent 不是 Web 服务,不能按 CPU 来扩缩
我给第一版设计用的指标是 CPU 使用率,理由很充分:Web 服务都是看 CPU。结果压测时发现,任务队列越来越长,CPU 却稳定在 5% 左右。我盯着监控看了半天,突然想通了——Agent 的大部分时间完全不是在计算,而是在等待。
等大模型生成 token,等工具调用的 HTTP 响应,等用户输入下一句话。这些等待不消耗 CPU,却吞掉了吞吐。所以对于 Agent 服务,负载指标应该是:任务队列长度、请求在队列中的等待时间、以及当前处于阻塞等待状态的工具调用数。像 Kubernetes 的HorizontalPodAutoscaler默认只支持 CPU 和内存指标,如果你用在 Agent 上,必须自定义为队列指标,或者用 KEDA 这类事件驱动组件做队列伸缩。
| 对比维度 | 传统 Web 服务 | Agent 服务 |
|---|---|---|
| 负载指标 | CPU、QPS、内存 | 队列深度、等待时间、外部调用阻塞数 |
| 冷启动成本 | 秒级 | 可能需要初始化模型、加载提示词、建立工具连接,耗时从数秒到数分钟 |
| 状态 | 通常无状态 | 可能携带上下文和工具缓存 |
| 伸缩副作用 | 一般可安全销毁 | 销毁未完成任务可能导致外部副作用悬空 |
一个既快又稳的 Agent 池,靠的是这三个部件
我之前把 Agent 池想象成一个简单的数组加几个计数器,后来发现它至少要有三层:任务队列、Worker 管理器、伸缩控制器。队列负责接收任务;Worker 管理器负责启动、监控、销毁 Agent 实例;伸缩控制器读取指标并决定增减。
while True:
pending = queue.depth()
workers = worker_pool.active_count()
if pending > 10 and workers < max_workers:
worker_pool.spawn()
elif pending < 2 and workers > min_workers:
if worker_pool.all_idle_for(5 * 60):
worker_pool.shutdown_one_idle()
sleep(evaluation_interval)
每次检查时,如果积压任务超过阈值(比如 10),就多开一个 Agent;如果任务少于阈值(比如 2),且所有 Agent 都闲了足够久,才缩掉一个。这里的微妙之处在于阈值要有迟滞:扩缩容的上下限不能是同一个值,否则队列在 9 和 10 之间抖动时,你的池子会在扩展和收缩之间反复横跳,每次缩容可能丢掉上下文缓存,比不扩还糟糕。
缩容才是真正的技术活
扩容很简单,无非是按需启动。但缩容的时候,你面对的是一个正在执行任务的 Agent。直接把它杀了,可能任务才执行到一半,例如它已经调用了第三方 API 完成转账,但还没把结果写回数据库。等到伸缩控制器判断需要缩容时,它选中的恰好是这个正在执行关键任务的 Agent。
我就在生产环境干过这种事。当时一个 Agent 正在处理退款流程,我手动销毁空闲实例,结果误杀了一个仍在处理中的。几分钟后用户投诉说退款没收到,查日志发现 API 请求已经发出去了,但 Agent 死后没人把结果写入系统。
因此,缩容必须是一个优雅过程:先把实例标记为 draining,不再接收新任务,等当前任务自然完成;或者支持任务状态保存,让另一个空闲实例接管。如果任务无法中断,你甚至要保留一个最小实例数,专门兜底。
把 Agent 变成无状态机器,才能自由伸缩
上面的问题本质上都来自 Agent 是有状态的。一个 Agent 可能记住了用户之前的对话、长文档的摘要、工具调用的临时凭证。你杀了它,状态跟着没了。
解法其实是老一套:把状态从 Agent 实例里搬出去。我现在会把会话上下文写到 Redis,每个 Agent 实例不保存任何私有的业务状态,处理任务时统一从 Redis 恢复。这样 Agent 退化成无状态执行器,就像 Web 的 stateless server 一样可以随时增减。
但注意,无状态只解决了内部状态;外部副作用依然要小心。因为任务在其他 Agent 上重试时,可能重复执行操作。所以工具调用必须支持幂等,比如每次请求带 request_id,API 端记录这个 id,重复请求直接返回第一次的结果。
动态 Agent 池不是摆设,但也不是银弹
说清楚了机制,摊牌时间。动态 Agent 池解决的是“大量相互独立、可并行、短任务”型场景,比如批量审核、数据清洗、异步客服。在这些场景下,它比固定池省钱,比手撸队列省心。
但如果你面对的是具有强状态依赖的会话式 Agent,比如用户和 Agent 持续聊天,要求上下文不丢失,动态池会给你带来巨大麻烦。为了保持会话连续,你要么做会话亲和性(同一个用户的请求永远路由到同一个 Agent),要么实时迁移整个上下文。前者会让池子退化成固定池,后者实现复杂度陡增。
还有一类情况不应该扩:当你的瓶颈不是计算而是外部 API 限速时。扩容非但提不了吞吐,反而会让 10 个 Agent 一起撞上限流导致集体超时。所以在扩缩容之前,先问自己:是 Agent 干活太慢,还是它等待的资源太慢?如果是后者,先解决资源侧的排队问题,而不是盲目加 Agent。
什么时候我劝你放弃动态池
- 单个任务执行时间超过几分钟,且任务之间没有并行收益——池子的管理开销让你得不偿失。
- 业务需要全局严格串行——再多的实例只会制造混乱。
- Agent 冷启动需要几十秒,而任务峰值只持续几秒——等新实例拉起来,峰值已经过去了。
所以,如果你也在做 Agent 服务,我的建议是:先让 Agent 无状态化,然后用任务队列测量真实负载,最后再考虑自动伸缩。扩缩容的粒度要细,冷却要足够长,外部依赖要记得限流。到了这一步,你会发现自己已经拥有一个比单实例稳健得多的系统。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/655.html