跳到主要内容

智能体从未接收到的服务降级信号

· 阅读需 10 分钟
Tian Pan
Software Engineer

当下游 API 开始出现波动时,人类操作员在任何事情真正崩溃之前,就会通过十几种方式察觉到。状态页变为黄色。变更日志邮件飞进收件箱。提供商的仪表板上出现警告横幅。值班频道因有人在日志中发现 429 错误而热闹起来。队友发帖询问:“还有人看到写入变慢吗?”这些都不是对请求的响应。它们是围绕 API 的环境运行信号,人类几乎是被动地吸收了这些信号。

调用同一个 API 的智能体(agent)只收到一样东西:它刚刚发出的请求的响应。状态码、Header、Body。这就是全部的渠道。它没有收件箱,没有仪表板,没有 Slack,没有外围视野。它察觉不到最后十个调用每个耗时都是之前十个的两倍。它读不了状态页,因为没人给它 URL,它也没有查看状态页的常规指令。当依赖项降级时,智能体是系统中最后一个知情方——而且它通常是通过失败才知晓的。

这种不对称性并非模型能力问题。更聪明的模型也解决不了。智能体对运行信号是盲目的,因为底层设施(plumbing)从未传递过这些信号,而且大多数智能体架构在出厂时,甚至没人注意到缺失了这些底层设施。

智能体实际看到了什么

想象一下观察依赖项的最窄窗口:单个 HTTP 响应。这就是工具调用返回给智能体的内容。如果调用成功,智能体看到 200 和一个 Body。如果失败,它看到 500 或超时。在这两者之间——即“在运行,但不太好”的整个光谱中——智能体几乎看不到任何它被训练去应对的信息。

人类操作员生活在那个中间地带。他们深知系统很少会干脆利落地失效;它们会先降级。延迟悄悄爬升。错误率从 0.1% 升至 2%。队列开始积压。提供商发布公告称“错误率上升,正在调查”。操作员读取这些微弱的信号并做出调整:减速、改变批处理方式、切换到备用方案,或者干脆等待。这种调整早在硬性故障发生之前就开始了。

智能体没有等效的机制。它的循环是请求、响应、决策、重复。除非你刻意为之,否则每一次迭代对于运行健康状况来说都是无状态的。第 200 次调用的响应并不携带第 190 到 199 次调用正在变慢的记忆。智能体将 1.8 秒的响应和 180 毫秒的响应视为同样的结果:成功。因此它会继续做之前做过的事,只不过现在它是在向一个正通过智能体听不见的方式、安静地请求“别打扰我”的系统发起请求。

智能体直接冲进的“棕色故障”

最危险的故障模式不是 API 宕机,而是 API 在维持运行的同时变慢。工程师称之为“棕色故障”(brownout):服务仍然返回 200,但每个请求都要消耗更多的时间、更多的连接和更多的容量。棕色故障是系统在技术上仍在提供服务的同时,恳求减少负载。

人类能立即发现棕色故障,因为人类会看时钟。除非你为它构建一个时钟,否则智能体不会看时钟。因此,当依赖项发生棕色故障时,智能体仍会全速调用。更糟糕的是,如果棕色故障导致少数请求超时,智能体的重试逻辑就会介入——而重试就是加速剂。

在任何多层系统中,这种算术都是残酷的。如果一个由五个服务组成的调用链中的每一层都重试三次,一个原始用户请求就可能扩散为 3^5 = 243 个后端调用。单个 429 触发重试,这些重试消耗了本就稀缺的配额,而这种消耗又触发更多的 429,循环往复形成重试风暴(retry storm)。智能体只看到单个响应,根本不知道自己就是产生风暴的人。它将该事件体验为“API 不太稳定”,并以更多的请求——恰恰是导致不稳定的行为——作为回应。

大家都会采用的修复方案是带抖动的指数退避(exponential backoff with jitter),这确实是必要的。加倍两次尝试之间的等待时间,加入随机性以防大量智能体同步形成“惊群效应”(thundering herd),并将尝试次数限制在五到七次。但退避只有在失败后才会触发。它是对 429 的反应,而不是避免它的方法。一个正确退避的智能体仍然是先撞了墙。为了不撞墙,智能体需要预见墙的到来——而这些信息是存在的。它们只是被丢弃了。

响应中已被忽略的 Header

这里有一个让你感到不安的事实:智能体不需要新的数据源来预见棕色故障。信号就在它手头的响应中。

表现良好的 API 在每一次调用中都会广播其健康状况。新兴的 IETF 标准定义了携带剩余配额和重置窗口的 RateLimit Header;许多提供商仍在使用旧的 X-RateLimit-RemainingX-RateLimit-LimitX-RateLimit-Reset 三元组。429 响应应该携带 Retry-After,而当 Retry-After 和重置 Header 同时存在时,以 Retry-After 为准。GitHub 的 REST API 在每个响应中发布你的剩余配额,并告诉你重置的确切秒数。Zendesk、Jira 和大多数大型平台也是如此。依赖项在第 190 次调用时就告诉智能体,在重置前只剩下 3 个请求的配额——这比它即将触发的 429 提前了 10 次调用。

智能体忽略了这一切。大多数工具集成在响应到达模型之前,就解析了 Body 并丢弃了 Header。递给智能体的是一封撕掉了顶部警告的信。然后它一路冲过第 191 到 199 次请求,在第 200 次请求时撞上限制,直到现在——通过一个生硬的 429——它才发现了一个其实已经拿在手里十次调用的事实。

同样的浪费也存在于延迟上。没有什么能阻止工具层对每次调用进行计时,并察觉到最近十次调用的平均耗时增加了一倍。没有什么能阻止它读取宣布端点弃用的 Sunset Header,或者响应字段中的弃用通知。这些不是奇特的信号。它们是标准的、结构化的,并且客观存在。它们只是没出现在 HTTP 客户端和模型上下文之间的路径上。

为 Agent 构建侧信道

这种修复是一种架构层面的决策,而非提示词。将操作元数据(Operational metadata)视为 Agent 的一等公民输入,与响应体(Response body)处于同等地位。具体而言,这意味着工具层——即模型与真实 API 之间的包装器、网关或 MCP 服务端——不再是一个简单的透传通道,而是转变为一个翻译器。它的职责是将面向人类的操作信号转化为 Agent 可以处理的信息。

工具层应该做以下几件事:

  • 将速率限制(Rate-limit)状态显式化到上下文中。 当响应包含 RateLimit-RemainingX-Limit-Remaining 时,不要丢弃它。在工具结果中附上一条通俗易懂的说明:“配额:5000 次请求中还剩 3 次;将在 47 秒后重置。”现在模型可以对此进行推理——推迟非紧急调用、进行批处理,或者单纯地等待——而不是通过撞上限制才发现它。
  • 追踪并报告性能下降。 让工具层针对每个依赖项维护一个简短的滑动窗口,记录延迟和错误率。当移动平均值超过阈值时,对结果进行标注:“此 API 的延迟是正常基准的 3 倍;请考虑减少调用量。”Agent 无法在多次无状态调用中自行计算这一点;而包装器可以。
  • 在模型下方机械地执行 Retry-After 模型不应该负责解析 Retry-After 并进入休眠。应该在工具层通过真实的退避(Backoff)和抖动(Jitter)来强制执行,并在连续出现 429 错误的次数超过阈值后触发熔断器,以防异常循环持续运行。模型的工作是处理任务;管道的工作是处理协议。
  • 为 Agent 提供健康状况反馈,而不仅仅是响应结果。 对于关键依赖项,暴露一个 Agent 可以调用的 dependency_health 工具,或者定期向上下文中注入健康状况摘要。状态页、/health 端点和已知故障列表通常都是机器可读的,或者接近机器可读。Agent 需要将依赖项的健康状况作为一个可以轮询的输入——而不是在发生故障后才被动接受的事实。
  • 将弃用(Deprecation)转化为指令。 对于模型来说,Sunset 响应头或弃用字段作为原始元数据毫无意义。工具层应将其转化为一行可执行的指令:“此端点将于 2026-08-01 停用;现已提供替代工具 search_v2。”模型无法处理的元数据是噪声;它能处理的指令则是信号。

MCP 生态系统正朝着这个方向迈进——2026 年关于服务器元数据和周知探测 URL 的路线图工作,是让依赖项能够描述自身健康状况的一大步。但你不需要等待协议成熟。任何团队今天都可以在他们现有的包装器代码中构建这种翻译层。

底层原理

Agent 的情境感知能力完全取决于你为其提供的上下文。它没有余光,没有传呼机,也没有同事会告诉它今天早上 API 看起来响应很慢。人类操作员从环境中获取的所有信息,只有当你将其放入上下文窗口时,Agent 才能获取。

因此,对于 Agent 接触到的每个依赖项,需要问的问题很简单:人类如何知道该服务正在降级——Agent 是否也接收到了同样的信息?如果答案是状态页、变更日志邮件,或者是工具层静默丢弃的响应头,那么 Agent 就是在盲目运行,并且它会一直盲目下去,直到下一次局部停电(Brownout)发生。这项工作的核心不在于让模型变得更聪明,而在于构建侧信道,来传递那些模型从未有机会听到的信号。

References:Let's stay in touch and Follow me for more thoughts and updates