最贵的 Agent 事故,往往从一个"看起来很健康"的仪表盘开始。请求还在返回,worker 还在忙,只是队列在悄悄变长。然后某个模型或工具慢下来,这条等待线就变成了超时、重试、重复劳动,以及一个看起来卡死的聊天窗口。

缺的那一环通常不是再加几个 worker,而是AI Agent 背压:一份明确的契约,让系统不再接收超出"有用期限内能做完"的工作量。这份契约要覆盖交互式和后台两类 Agent 工作流,包括租户公平、队列上限、重试规则,以及诚实的用户可见状态。

队列不等于背压

队列是生产者和 worker 之间的存储。背压改变的是:当存储满了、或下游容量掉了,系统该怎么办。

没有背压,API 会接收每一个请求,每个 Agent 再扇出成模型调用和工具调用,provider 一慢,worker 就一直在重试。系统表面上一直可用,直到大部分请求已经等得太久、失去意义。长队列不是韧性,是延迟的拒绝。

Agent 比普通请求处理器更吃这一套,原因有四条:

  • 一个客户请求可能生成大量模型、检索和工具调用;
  • 一个慢依赖占住并发的时间,远长于典型 API 调用;
  • 重试会在容量最低的时刻把负载成倍放大;
  • 一次批量导入可以吃掉服务交互用户的同一份模型配额。

近期的开发者讨论也反映了这种区分:限流是下游 provider 发出的信号,背压是应用自己的决定——在堆积形成之前,选择排队、延后、降级还是拒绝。AWS 的 Agent 编排指引同样要求在 workflow、agent、tool、model 各层都设控制。跨层设计才是真正的缺口:光有 token 预算或一个持久队列,并不能告诉 API 容量耗尽时该做什么。

先定过载契约,再写 worker

先决定每一类工作在压力下允许做什么,别让某个实现细节替你做这个决定。

关键问题是:这个请求如果等下去,还能产生价值吗?答案是否,快速回复比转圈更友善;答案是能,就返回一个任务引用,并展示排队、运行、暂停或失败状态——永远不要给一个假的百分比。

给每次运行一个统一的截止时间,从准入那一刻开始算,而不是从第一次模型调用开始。一个五分钟的工作流在队列里等了四分钟,只剩一分钟可执行。把这个截止时间传给每个阶段,才能防止过期任务继续消耗新鲜容量。

准入结果可以定义成四种:已准入(带 runId 和截止时间)、已排队(可带位置提示)、延后(带重试秒数)、拒绝(带重试秒数和原因)。

给每一处会堆积的地方设上限

Agent 系统往往不止一个队列:HTTP 连接池、进程内信号量、任务 broker、模型 provider、工具客户端、数据库连接池。只给 broker 设上限,过载只是换个地方堆。

限制要落在四个范围:

  • 全局并发,保护共享的 provider 和数据库;
  • 按租户并发,防止一个吵闹的客户吃掉所有槽位;
  • 按工作流并发,防止一个扇出计划同时发起五十个调用;
  • 按依赖并发,把慢浏览器、搜索 API 或模型路由与健康工作隔离。

队列长度同样要设上限。大输入文件和原始网页不要放进队列,任务记录里放对象引用即可。有界队列应该装够重启工作所需的信息,而不是无限增长的载荷。

一个刻意做小的准入函数大致是这样:设最大运行数 24、最大排队数 200;某租户已在运行 3 个,就返回延后、30 秒后重试;运行数已满,就排队或拒绝;排队数也满了,直接拒绝并给出重试秒数。生产环境里,这些计数器要原子化并跨副本共享,比如通过数据库、Redis 脚本或队列原生原语。

重试也要被信号量包住,包括重试决策的边界,这样无限重试就绕不过并发限制。对有副作用的工具,还要配上幂等键和对账;一次成功写入之后的超时,不能盲目重放。

延迟了就诚实地说

流式输出会掩盖过载。如果聊天连接一直开着,而真正的任务还在 worker 队列里等,用户看到的是空响应,只会以为模型在思考。

应该发的是状态迁移:已接受、已排队、运行中、等待依赖、等待审批、已完成、失败。文案要事实化:"正在等待容量,将自动重试至 10:42"是有用的,"快好了"不是。

对等不起的请求,在流开始之前就拒绝,返回稳定且有文档的错误载荷,包含错误码、重试秒数、请求 ID 和是否可安全重试。客户端应遵守这个延迟,不要每次重试都新建一个 run;复用幂等键,别让一次网络抖动把一次点击变成一堆排队任务。

盯住那些暴露坏承诺的信号

CPU 和模型延迟不够用。要按模型路由、工具、租户层级和工作类别分别追踪:

  • 准入、排队、延后、拒绝、完成的运行数;
  • 队列深度和最老排队时长;
  • 从准入到首个有用输出的时间;
  • 执行时间与排队等待时间的对比;
  • 重试次数、重试原因、截止时间之后的重试;
  • 取消数和执行前被移除的任务数;
  • 并发利用率和依赖错误率;
  • 完成率和每个已完成工作流的成本。

告警要看组合,不看单个数字。队列深度上升叠加最老排队时长上升,说明需求超过服务能力;队列深度平稳但执行延迟上升,指向下游依赖;队列满而 worker 空闲,多半是调度、租约或分区出了 bug。

做一次简单的演练:让一条模型路由变慢,灌入混合租户流量,再恢复。验证交互用户拿到的是快速回答或快速拒绝,批量工作停止扩张,重试没有尖峰,队列排空后没有第二波涌来。

上线计划别给用户惊喜

先只观察。算出每个请求本来会被准入、排队还是拒绝,但先不执行,再拿这个结果和真实排队时长、完成数据对照。

然后给一个非关键的后台功能加上小的全局上限和一个按租户上限。在拒绝变成常态之前,先把明确状态加进 UI 和 API。最后,在摸清正常容量和降级容量之后,再加优先级预留和按依赖的限制。

把契约写给客户端看:哪些错误可重试、等多久、任务何时会排队、如何轮询或订阅状态。背压成功,是调用方改变了行为;背压失败,是所有调用方同时重试。