动态 Agent 池:根据负载自动扩缩 Agent 实例——从单实例到弹性集群

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

AI technology illustration

然后你发现,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

(0)
上一篇 11小时前
下一篇 11小时前

相关推荐