跳到主要内容

智能体集群并发:在没有死锁或惊群效应的情况下协调数十个智能体

· 阅读需 13 分钟
Tian Pan
Software Engineer

十一个智能体在同一秒内启动。在第一个工具调用返回之前,就有三个阵亡了。那 27% 的失败率不是模型问题、提示词问题或工具问题。这是一个调度问题 —— 就像操作系统在五十个进程同时唤醒并争抢单个 CPU 时所解决的问题一样。区别在于,操作系统拥有四十年的智慧积累,而智能体运行时只有大约两年。

任何连接过超过几个并发 LLM 工作节点的人都见过类似的情况。你在 02:00 启动一个定时任务,三十个智能体同时启动,它们在 200 毫秒内同时请求同一个提供商,结果大多数都以 429、502 和连接重置告终。幸存者只能获得承诺的一半速率配额,因为提供商的公平共享逻辑已经开始对你的 API 密钥进行节流了。到 02:05 时,幸存的智能体运行结束,你的仪表盘显示的完成率足以让一个刚写出第一个生产者-消费者的计算机专业大一学生感到汗颜。你的值班人员会争论是该增加重试、增加队列,还是干脆减少运行数量。

这些方法本身都不是正确答案。正确答案是:一个智能体集群是一个小型分布式系统,需要按照分布式系统的方式进行设计。

集群不仅仅是智能体

单个智能体是循环中的语言模型。它接收提示词,调用工具,再次调用工具,然后停止。你可以将其延迟视为 Token 生成时间加上工具调用时间的总和。你可以将其故障模式归类为“模型困惑”或“工具超时”。你可以通过对话记录来调试它。

但智能体集群并非如此。它是三十个智能体在争夺共享的速率配额、共享的数据库连接池、共享的文件系统以及一个根本不在乎你的提示词有多聪明的共享下游服务。集群会产生单个智能体自身无法产生的涌现行为:车队效应(convoy effects)、优先级反转(priority inversion)、活锁(livelock)、惊群同步(herd synchronization)以及公平共享饥饿(fair-share starvation)。智能体本身并不是有趣的部分,竞争才是。

这种视角的转变至关重要,因为它告诉你应该把工程精力放在哪里。如果你把集群视为“一个智能体的三十个副本”,你就会把时间花在调整提示词上,并纳闷为什么可靠性在并发超过十个工作节点后就无法扩展。如果你把集群视为调度问题,你就会把时间花在准入控制、背压(backpressure)和协调原语上 —— 而这些才是真正能解决问题的手段。

惊群效应的重新发现

惊群效应(thundering herd)不是流量问题,而是同步问题。它是指大量工作节点在同一瞬间被唤醒并尝试使用共享资源时发生的情况。经典的例子 —— 缓存项过期、故障转移导致新主节点选出、定时任务触发 —— 正是智能体集群正在重复的模式。

02:00 的 Cron 触发器是最明显的例子。不那么明显的是在执行通用步骤结束时出现的隐式“惊群”。集群中的每个智能体往往会在大致相同的时间完成步骤 N 并开始步骤 N+1,因为它们的运行时间主要受同一个模型支配,具有大致相同的延迟分布。因此,即使你交错启动它们,它们也会在每个工具边界重新同步。这与并行 HTTP 获取在 TCP 慢启动时堆积的原因相同:它们都同时挤在同一个瓶颈处。

缓解措施并不新鲜。为每个定时器添加抖动(jitter)。刻意交错启动时间,而不是任其随机。使用带随机性的指数退避,而不是固定步长的退避。限制在任何给定时间内处于“模型调用”阶段的智能体数量,将其与集群中存在的智能体总数分开管理。一个拥有 100 个智能体但只有 20 个可以同时向提供商发起请求的集群,其表现将优于一个 50 个智能体同时发起请求的集群。因为第一种设计在时间上摊销了速率配额,而第二种设计则会产生 429 错误和重试带来的锯齿状负载。

共享工具的速率限制竞争

最棘手的竞争不在于 CPU 或内存 —— 那些是廉价且本地的。真正的竞争在于共享下游的速率配额,这是一个全局资源,单个智能体无法观测到。提供商的速率限制通常同时从三个维度进行约束:每分钟请求数(RPM)、每分钟 Token 数(TPM)以及并发进行的请求数。每个维度都是独立节流的。你可能远低于 RPM 限制,但仍会因为耗尽了并发请求上限而收到 429 错误。

这就是操作系统类比发挥作用的地方。操作系统通过准入控制(admission control)解决这个问题:调度程序在进程拥有完成一个时间片所需的资源之前,不会允许其运行。对于智能体集群,类似的方案是一个集中的限制器,每个智能体在进行模型调用前都会咨询它。限制器持有共享配额,并返回“执行”或“等待若干毫秒”。通过实现为令牌桶算法,并将补充速度调整为提供商声明的 RPM,同时配合一个用于并发请求的信号量,它可以消除两种最常见的故障模式 —— 唤醒时的惊群和并发请求的瓶颈。

有几点会让这比听起来更难。首先,提供商的速率限制是近似值;它们在执行时有一定的松紧度和爆发空间,你在文档中看到的限制通常是保障的下限,而非绝对上限。其次,速率配额并非你独享;同一 API 密钥下的其他应用程序也在共享它,你的限制器需要留出安全边际。第三,限制器本身不能成为新的单点故障 —— 一个崩溃并阻塞整个集群的限制器比它要防范的惊群效应更糟糕。

可行的答案是自适应限制器:从保守开始,当调用成功时线性增加容量,当调用失败时成倍减少容量。这就是 AIMD(加性增/乘性减)—— 与 TCP 用于拥塞控制的算法相同 —— 它会收敛到提供商当前的实际容量,而无需预先知道具体数值。当提供商出现“吵闹的邻居”或区域性故障时,AIMD 会在几秒钟内退避。当情况改善时,它会在接下来的几分钟内逐渐恢复。你不需要为每个提供商单独调优;你只需根据可观测的信号(错误率、延迟 p99、明确的 429 错误)进行调优。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates