先记住这个答案
在 Agent 队列架构中,消费者依赖下游 LLM 的速率配额。当消费者收到的 429 增多或响应延迟上升时,它应降低并发或拉取速率,同时将这一信息反馈给生产者。具体的背压传导机制包括:使用有界队列,在队列接近满时拒绝新任务并返回明确的错误码;或者消费者定期向生产者发送容量状态(如可用令牌数)。生产者收到信号后,必须暂停或调整生产速率。不推荐无限重试或依赖 TCP 背压。
- 消费者需显式反馈背压,而不是靠队列无限堆积。
- 有界队列与拒绝策略是应用层背压的关键手段。
- 背压决策必须结合消费速率与队列深度动态判断。
背压传导机制:速率、深度与拒绝联动
当消费者调用 LLM 遇到限流,其消费速率必然下降,导致队列中待处理消息增多。若生产者持续投递,队列深度将线性增长并可能耗尽内存或使消息过期。因此消费者不能仅依赖队列自身吸收压力,必须产生可被上游识别的信号。信号可以是显式的:队列满时返回 RejectedExecutionException 或 HTTP 429;也可以是隐式的:消费者通过心跳上报当前空闲程度,生产者据此减少发送量。
更通用的做法是让消费者按自身能力拉取:例如在 RabbitMQ 中用 basic.qos 限制未确认数,处理完一个再拉一个,队列深度会上升,最终生产者因队列满而投递失败。该方式无需额外通道,但生产者必须能解析并响应投递失败(如 publish confirm false)。
具体场景:RPM 打满时的降速与拒绝
假设有 20 个 Agent 消费者并发调用 LLM,供应商上限 100 RPM。当实际请求达 150 时,消费者收到多个 429,消费速率下降,队列深度从 100 涨到 800。队列超过水位 200 后,消费者暂停拉取,队列拒绝新任务。上游生产者收到 QueueFullException 不再入队,并向客户端返回 503,触发退避重试。
这个场景的关键是消费者在收到 429 后不是立即重试,而是主动降低并发并让队列拒绝物理上无法容纳的任务。通过这种方式,压力被直接传回生产者,生产者可以据此调整其调度策略。相比无限重试,拒绝策略避免了系统坠入重试风暴,同时给了 LLM 配额恢复的时间。
适用边界与失效条件
背压传导依赖生产者正确实现拒绝处理。如果生产者是定时批量任务而非实时响应,它可能忽略错误继续投递,此时需要额外的调解:例如消费者强制停用队列或使用死信。此外,若 LLM 限流持续很久,消费者可能整体停止消费,导致队列永远满,这时必须让生产者完全停机或降级任务。
另一个边界是反馈延迟:消费者发现限流到更新生产者状态之间有时延,可能造成生产者在限流刚恢复时过量涌入,形成震荡。缓解办法是设置低水位与高水位之间的滞回区间,并让消费者在恢复限流时逐步增加拉取速率。此外,TCP 级背压机制不适合应用层,因为队列通常采用异步 HTTP 或消息协议,应以上述显式信号为主。
容易答错的地方
- 队列无限长可吸收突发流量
- 许多人认为把队列容量设大就能吸收 LLM 限流,实则限流持续时队列深度只增不减,最终任务超时或内存耗尽。正确做法是让队列长度受控,把溢出转化为对生产的背压。
- 消费者收到 429 后反复重试直到成功
- 重试会继续消耗配额,可能触发更严限流。现实中应遵守 Retry-After 或指数退避,并把降速信息上报生产者,而不是在消费者内部死循环。
面试官还会怎么问?
如果生产者是第三方系统,不关心队列拒绝怎么办?
消费者只能保护自己,例如丢弃任务并记录指标,或扩展死信机制。真正治理需要生产者和消费者共享限流状态,否则难以阻止过量投递。
应用层背压信号如何标准化?
可采用自定义错误码如 429,或在队列协议中使用消息拒绝字段。要确保所有生产端都解析同一信号,否则背压不生效。
队列水位阈值如何确定才能避免死锁?
需要结合消费者平均处理时延、可容忍延迟和最大丢弃率。一般设置高水位=可排队最大任务数,低水位用于恢复;并留有余量防止一次性涌回。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。