1. 题目分析
背压要解决的是上下游处理速度不匹配的问题:当下游接不住更多工作时,把这一信息反馈给上游,使其减少接入、拉取或派发,而不是继续把任务堆进缓冲区。Agent 链路尤其需要这种协调,因为一次用户请求还可能在内部展开为多轮推理、检索和工具调用。
例如,模型响应变慢后,如果入口照常接单、编排器照常生成分支,新增工作就会积压在任务队列、连接池或协程中。即使给模型调用设置了并发上限,也只是限制同时执行的数量,未必限制等待中的任务总量。压力没有消失,只是转移到了上游。
因此,背压机制需要把真实容量与上游行为连接起来,明确什么时候等待、什么时候降低生成速度,以及什么时候拒绝或延期处理。重点是形成贯穿链路的反馈,而不是孤立地增加几个限流开关。
1.1 把压力传回去
沿着开头模型变慢的情况继续看:执行器已经限制了并发,新的模型调用进不去,但入口仍然接单,编排器也继续展开任务。此时系统并没有获得稳定性,只是把积压从模型服务移到了本地。队列越长,等待越久,超时后的重试又增加新工作,最终连尚未调用模型的请求也受影响。
所以,背压首先需要一条与任务流向相反的控制路径。执行器发现模型 A 没有可用容量,就减少对 A 的派发;编排器收到反馈后,暂停只会继续产生 A 请求的可选分支;如果待处理工作仍在增长,入口进一步收缩相关任务的受理。每一层都要改变行为,仅记录"下游繁忙"不是反馈闭环。

图:工作向下游流动,容量信号反向约束派发、展开与入口受理
这个控制应带上资源作用域。模型 A 拥堵,不必停止完全无关的工具 B;某个租户额度耗尽,也不应该拖停其他租户。反过来,两个模型地址若共用同一供应商配额,就不能当成独立容量。依赖、配额域与租户之间的映射,决定压力信号究竟应该传给谁。
信号从实际执行边界获取:在途数是否达到上限,许可等待是否增长,最老任务等了多久,缓冲字节是否逼近容量,以及下游完成速率是否下降。本地 CPU 空闲不能否定远端拥堵;参数或权限错误也不能当成整个资源域过载。统一调用层应返回明确原因、作用域和是否已发送,由运行时处置,而不是让 LLM 看到异常后重新规划更多调用。
Reactive Streams 强调跨异步边界按接收方能力传递需求。Agent 未必采用同一协议,但要落实同样的关系:下游承接能力变小,上游产生的相关工作就应减少。接下来需要把这个关系变成实际派发规则。
1.2 按承接能力派发
模型提出十个工具调用,并不意味着立即创建十个执行任务。编排器先保存候选节点,只有依赖就绪、任务展开宽度允许、目标资源有许可时才启动。如果当前阶段已经拥堵,就不继续触发只会增加积压的可选规划和子 Agent。
进程内可以用固定 Worker 和有界 Channel 承接工作,但生产者也必须受控。先启动十万个协程,再让它们阻塞在 Channel 上,只是把队列藏在协程和闭包里。任务应增量产生,发送等待支持取消;没有容量时,调用方等待一个有限窗口或者收到明确的暂不可派发结果,而不是无限创建等待对象。
跨进程消费同样遵循这个原则。拉取模式按本地剩余承接能力取消息;推送模式限制未确认窗口。图中示意窗口为四,三个任务执行、一个本地等待,就不再取第五个。这里约束的是消息承接量,模型调用仍有自己的并发限制,因为一条消息可能展开多个调用。

图:消费窗口约束正在执行与本地等待的消息,完成可靠交接后才确认
以 RabbitMQ 为例,Prefetch 控制未确认消息数,具体作用域需要按配置核对,而且零表示不限量,并不是暂停。若想暂停消费,应使用合适的消费控制机制,不能把数值改为零后以为已经关闭入口。
确认时机也会影响背压。若消息一进入进程就 ACK,消息系统会误以为已经有新容量,继续投递,而业务工作仍在内存里等待。应在完成处理或可靠移交到受控持久阶段后确认;如果移交给另一条无界队列,压力仍然只是换了位置。重复投递需要业务幂等,不能靠提前确认换取表面的队列下降。
下游繁忙时,反复立即 Nack 并重入队也不可取:同一消息刚退回又被取出,会消耗网络和调度资源,却没有完成任何工作。RabbitMQ 确认机制文档提示了这种重投循环。更合理的是减少新投递,按受控的延迟策略恢复,并把新请求和重试计入同一容量范围。
派发变得受控之后,还会剩下一批暂时不能启动的任务。它们在哪里等待、最多等多久,是背压能否保护上游的下一道边界。
1.3 给等待设边界
等待只能吸收短时供需差,不能解决持续过载。假设某阶段每秒收到一百二十个同等工作量的调用,只能完成八十个,没有其他退出路径,积压就每秒增加四十个。两百个空位约五秒用完。这只是供需算例,却足以说明扩大队列不会增加处理能力。
因此,等待需要同时满足空间和时间条件。空间上限制条目数、累计字节及单条大小;时间上限制最长等待,并检查剩余期限是否容得下排队、执行和必要收尾。只有任务数量上限不够,一条携带大文件的任务也可能耗尽内存。

图:队列容量、内存与截止时间共同决定是否值得等待
图中任务还剩六秒,预计排队四秒、执行三秒、收尾一秒,合计八秒,即使有空位,也不适合继续承诺同步完成。估计应按任务类别和输入规模校准,并用硬截止时间兜底;它不是对未来耗时的精确保证。入队、出队和真正启动前都要重新判断,避免过期任务仍占用下游。
等待位置还应尽量收敛。网关先等,SDK 再等,连接池继续等,会让单点队列看起来不长,整条任务却已经耗尽预算。选定主要排队点,其他层只保留必要的小缓冲,并让等待占用同一份任务期限。等待期间不提前占住后续数据库连接或执行槽。
多租户队列需要各自份额和公平调度,防止单个批任务把全部等待空间占满。低优先级可以等待更久,但不能永久没有执行机会;已经可靠受理的任务若过期,也要留下明确结局,而不是静默丢弃。
此时,系统已经能够判断某个请求值得短等还是必须退出等待路径。退出后如何响应用户,不能只统一返回一句"系统繁忙"。
1.4 明确上游承诺
先看仍有合理完成机会的任务:允许进入有界队列,提供真实排队状态;如果用户取消,未派发节点退出队列,已在途工作按契约传播取消。对于本来就设计为离线执行的业务,断开网页不等于取消任务,应有独立的查询和取消入口。
若等待窗口不够,再判断是否存在合格的降级结果。例如只读问答可以返回权限与时效符合要求的缓存或检索原文,明确哪些分析尚未完成。能删减的是可选增强,不是鉴权、审批和必要证据。备用模型也需要容量与能力检查,不能把主端积压整体转移过去。

图:短等、降级、异步与拒绝分别对应不同的业务前提和响应承诺
没有合格的即时结果,但业务允许延期,就先检查持久任务容量,可靠保存任务与投递意图,再返回任务 ID。HTTP 202 表示已接受,不代表已完成;工程上还应提供状态查询、过期规则及取消能力。任务表与消息投递可以通过 Outbox 等方式衔接,不能只是启动一个后台协程就宣称可靠受理。
异步也不是无限空间。持久队列需要容量和时效上限,无法在约定窗口完成的任务仍应拒绝。否则接口 RT 很低,后台任务却排几天,背压只是被"异步化"掩盖了。
既不能等待、降级,也不能延期,就尽早拒绝。调用方速率超额可使用 429,服务临时过载通常使用 503,并按接口契约说明是否可重试,有依据时附上 Retry-After。调用方应在原总期限和重试预算内延后尝试,而不是收到响应后立即再发。轻量准入应尽量发生在装载长上下文、Embedding 和推理之前,避免拒绝本身也消耗大量资源。
尤其要区分"没有受理新任务"和"任务已经执行了一部分"。订单已创建,只是后续总结被模型限流,不能诱导用户重新下单;已发送的写工具结果未知,也不能因背压返回一个模糊的失败就从头重跑。应保留任务与操作身份,继续查状态或依幂等契约恢复。过载处置负责减少新增工作,不负责改写已经发生的业务事实。
以上处理针对任务进入系统的方向。输出方向同样可能出现接收方变慢,只限制任务受理并不能解决这一段。
1.5 覆盖流式消费
模型不断产生 Token,而浏览器网络变慢时,服务端需要面对同样的供需问题。如果 SDK 持续读入、应用不断往数组追加,使用 SSE 也只改变了传输形式,没有消除缓冲增长。
应沿"模型生成—SDK 读取—服务端转发—代理—客户端"逐跳检查容量。应用设置有界字节缓冲,写入等待真正的下游可写状态,缓冲接近上限就放慢读取;同时核对 SDK 是否预读、代理是否缓冲,避免某一层无界存储截断了压力反馈。gRPC 流控文档也提醒,写调用返回并不等于数据已经送达接收方。

图:输出链路的慢消费需要逐跳处理,暂停读取不等于远端模型停止计算
本地能放慢读取,却未必能让第三方模型停止推理和计费。除非供应商协议明确支持需求传播,否则远端仍可能继续生成。因此还要设置缓冲上限、写阻塞上限和总期限:超过边界时,按产品契约显式中断,或把结果转存到容量受控的存储后供查询,不能靠无限内存维持连接。
哪些数据可以合并,也要看语义。重复的进度通知可以保留最新状态,最终答案、工具参数和业务回执不能随意丢弃。已经发送 HTTP 200 的流式响应,不能中途把状态码改成 429;可以通过约定的结束或错误事件说明中断,连接已不可写时则关闭,并让客户端依据任务 ID 查询。
至此,入口、内部派发和输出都具备了减速手段。但如果容量刚恢复就同时放开所有等待者,系统仍可能马上回到过载状态。
1.6 平稳恢复与验收
恢复不能只用同一个阈值反复开关。队列达到高水位时收缩,回落到较低水位并稳定一段时间后再逐步放量,高低水位之间留出回差。反馈传递需要时间,高水位还应为传播期间继续到达的工作留余量,不能等缓冲全部填满才通知上游。

图:高低水位形成回差,恢复阶段逐步释放工作而非同时唤醒全部积压
调低并发上限后,已在途工作不会立即消失,新的派发应等真实占用回落。恢复阶段的新请求、延期任务、重试和探测也必须共享容量,不得各自判断"只增加一点"。重试加入退避与抖动,并控制总量;主端恢复不意味着所有实例可以同时补齐积压。
释放资源所需的控制消息应保留处理能力。例如完成回执和取消通知不能被普通执行任务挤到同一条满队列末尾,否则负责回收资源的动作反而无法运行。跨实例的容量信息也要防过期和重复,失联时使用保守限制;监控显示有空位,只用于调整策略,实际派发仍需取得有效许可,避免多个实例同时抢占同一份余量。
验收应顺着反馈方向看行为,而不只看日志里有没有"已背压"。模拟模型降速后,相关派发是否下降,编排扇出是否收缩,入口是否减少受理;同时检查队列字节、等待对象和内存是否停止持续增长。再模拟慢客户端,验证压力没有藏进 SDK 或代理缓冲。解除故障后,看积压是否渐进清空、正常流量是否获得服务。
指标应同时记录到达、受理、拒绝、降级、延期和最终有效完成,并观察最老任务年龄与恢复时间。大量快速拒绝会让 RT 变好,却不代表用户拿到了更多结果。压测还要保留持续到达场景,不能因为服务变慢,压测端自己就自动降低发包速度。
背压的最终目标由此明确:下游处理不过来时,系统能减少相关工作生成,为已接受任务保留可控的处置路径;容量恢复后,又能在不制造第二次拥堵的情况下恢复服务。限流、排队和拒绝是其中的手段,只有上下游行为真正联动,才形成完整机制。
2. 参考回答
我会把背压做成从依赖到入口的反馈链。先按模型、工具和租户识别压力域,观察在途数、等待时间、最老任务年龄和缓冲字节。下游饱和时减少派发,编排器暂停相关扇出,消费者按承接额度少拉取,必要时入口收缩受理,不能只把请求堆到协程里。
等待同时限制数量、字节和时间,还要判断剩余期限能否覆盖执行与收尾。来得及的短等,有合格替代结果就明确降级,允许延期的任务可靠落盘后返回任务 ID,其他情况尽早拒绝并给出重试建议。已执行的写操作不能从头重跑。流式输出也要逐跳限制缓冲,暂停读取不等于远端模型停算。
恢复时通过高低水位和渐进放量,让新请求、积压和重试共享容量。最后用持续超载、慢客户端和恢复测试检查上游是否真的减速,结合有效完成率、拒绝率和内存曲线验收,而不是只看 RT。