周会汇报的幻灯片上写着 99.9%。发票则显示账单翻了三倍。这两个数字在相邻的仪表盘上共存了数月,却没人发现它们衡量的是不同的世界。可靠性数值是重试后的结果——每一个最终返回 200 的调用都被计为成功——而成本数值则是客户端进行的每一次尝试,按 Token 计费。在两者之间,是一个慷慨的五次重试循环,以及一个尾部延迟正在悄悄恶化的供应商。第一次有人同时观察这两个数字是在一次故障期间,当时成本异常告警在可用性告警之前就触发了。
这就是整个模式。一个看起来像是可靠性机制的重试预算,本质上也是一个成本-质量调节旋钮,而那些只关注其中一面的团队,正在为一个发票最终会修正的可用性数字买单。
渐行渐远的两种“调用”定义
重试循环是分布式系统中最古老的模式之一。十多年来,AWS Builders' Library 一直主张,对于任何与远程服务通信的客户端来说,带有抖动的指数退避(exponential backoff with jitter)是不可或缺的。《SRE 实践手册》(The SRE Workbook)则从另一个角度提出了同样的观点:重试会将低错误率放大为更高水平的流量,一个跨越三个重试层的单一用户操作可能会对数据库产生 64 次尝试。标准的建议是为重试设置预算,以便为这种放大效应设定上限。
建议是对的。问题在于预算所隐藏的指标。
在重试循环下游计算的成功率 SLO 衡量的是客户端是否 最终 得到了答案。在任何重试发生前计算的成功率 SLO 衡量的是 供应商 是否在第一次尝试时就给了客户端答案。这是关于不同层面的不同问题,多年来它们足够接近,以至于没人需要去区分它们。但 LLM 工作负载让它们产生了分歧。
原因是重试的成本不再可以忽略不计。在微服务中重试一个 GET /healthz 几乎是免费的——几个字节,几毫秒,以及自动扩容组件根本察觉不到的 CPU 周期。但在使用工具(tool-using)的 LLM 调用中,重试意味着重新序列化系统提示词(system prompt)、对话历史、检索到的 Payload 以及用户消息——两万个输入 Token,每一次尝试都要全额计费。Anthropic 和 OpenAI 的 SDK 在遇到 429 和 5xx 错误时默认都会重试两次,这意味着一个性能略有波动的供应商可以将你的输入 Token 账单增加 1.5 倍,而你的重试后成功率指标甚至连一个基点(basis point)都不会改变。
CloudZero 的可观测性文章将这种差距描述为买单者缺失的一层。工程师看到的是可用性数字,财务看到的是发票,而双方没有一个共同的指标来解释为什么一个平稳而另一个在攀升。成本是领先指标。可用性是滞后指标。
99.9% 成功率背后隐藏了什么
想象一个供应商,其首试成功率在六周内从 99% 线性下降到 95%。由于有五次重试预算和相对独立的失败概率,重试后的成功率在整个窗口期内都保持在 99.99% 以上。仪表盘纹丝不动。周会汇报显示一切正常。
与此同时,每次成功调用所需的平均尝试次数从 1.01 次上升到 1.20 次。在一个每月运行一千万次调用、每次调用包含两万个输入 Token 的工作负载中,这意味着多出了 400 亿个计费输入 Token,这大约相当于按之前的尝试率计算的整个输入 Token 预算。发票翻倍并不是因为业务量增长,而是因为同样的业务量现在每次成功的成本更高了。
第一次有人将这两个数字联系起来通常是在故障期间。供应商的首试成功率掉下悬崖——一小时内从 95% 降至 60%——重试预算再也无法吸收失败,重试后的数字终于产生了波动。成本异常告警比可用性告警早触发四个小时,因为成本信号是尝试次数的连续积分,而可用性信号是跨越预算上限的阶跃函数。
这正是《SRE 实践手册》在告警指南中讨论的模式:告警必须对你真正关心的症状敏感,而重试后的成功率并不是那个症状。它是重试层为了平滑症状而设计的重度平滑后的转换结果。平滑后的指标是你放在状态页(Status Page)上的。它们不是你用来告警的。
重试前和重试后是两种不同的 SLO
修复方法在概念上很简单,但在操作上很复杂:将重试前成功率和重试后成功率作为独立的指标公开,每个指标都有自己的阈值和告警。
重试前成功率是客户端看到的供应商真实错误率。当它恶化时,这个数字应该驱动你与供应商的沟通。当它超过阈值时,这个数字应该触发模型或区域的故障切换(failover)。SRE 团队应该将这个数字视为依赖项的 真实 可用性,因为它每损失一个基点,你的重试预算就必须在成本上吸收一个基点。
重试后成功率是用户感知到的产品可用性。这个数字应该驱动你与领导层的 SLO 对话。它是衡量你的错误预算(error budget)是否被消耗的指标。它是状态页应该反映的数字。
这两个指标回答的是不同的问题。将它们混为一谈——这是每个默认仪表盘都会做的事——正是各团队最终在为无人察觉的供应商服务降级买单的原因。
AWS SDK 多年来一直通过令牌桶(token bucket)重试预算对这两端进行监测,它能区分瞬时错误(14 个令牌)和频率限制响应(5 个令牌),并将桶的水平作为可观测指标。SDK 将重试失败本身作为一个指标暴露出来。大多数 LLM 客户端封装库则没有这样做。区分“调用”这两种定义的工作落在了应用团队身上,而推卸这项工作的团队,交付的仪表盘正如《SRE 实践手册》所警告的那样,是在撒谎。
每次成功的重试次数是先行指标
一旦将重试前和重试后的指标分开,最有用的新指标就变成了总尝试次数与成功调用次数的比率——暂且称之为 RAPS(Retry-Attempts-Per-Success,每次成功的重试次数)。
RAPS 是一个连续的小数。在健康的供应商那里,RAPS 维持在 1.01 或 1.02。而在性能下降的供应商那里,在重试后的成功率发生任何变化之前,RAPS 会攀升至 1.10、1.20 甚至 1.50。RAPS 是直接对应到发票上的数字,因为每一次增量都是一笔 Token 账单。
如果团队将 RAPS 放在其可观测性仪表盘上并设置一个阈值(例如 1.10),就能获得供应商性能下降的先行指标,这比任何其他可靠性信号都要早几个小时甚至几天触发。RAPS 也是衡量自动故障转移或负载削减(load shed)的正确指标:当它超过预算阈值时,说明重试预算的工作量超出了团队的预期,此时正确的反应是停止成本流失,而不是等到重试预算耗尽且重试后的成功率最终崩溃。
Truefoundry 的可观测性指南在讨论“结束原因分布”(finish-reason distribution)作为质量信号时也提到了类似的观点。RAPS 是可靠性的模拟:它是尝试次数的分布,其中分布的尾部才是真正关键的变量。两次尝试的调用通常没问题。四次和五次尝试的调用才是预算真正被消耗的地方。跟踪每次成功的尝试次数的分布(distribution),而不仅仅是平均值,这决定了你是只知道供应商在缓慢退化,还是知道供应商正在以“双峰”方式退化——即现在一小部分调用吃掉了全部预算,而大部分流量仍然保持正常。
单次对话预算约束最坏情况
大多数重试预算都是按单次调用(per-call)计算的。这是一个有用的默认设置,但作为上限还不够。在 Agent 类产品中,单次对话可能会发出数十次或数百次 LLM 调用——工具选择、工具执行、中间规划、最终生成——而由于在整个对话中叠加的最坏情况重试模式,在任何单次调用预算察觉之前,它就可能将单次调用的上限翻上一个数量级。
解决方法是引入单次对话重试预算,用以约束整个 Agent 运行生命周期内的总尝试次数,此外再配合约束每一步重试的单次调用预算。AWS Bedrock 的扩展指南在讨论多个范围内的令牌桶(token buckets)时暗示了这一点,但这种框架尚未普及到大多数应用端的 LLM 客户端中。
其机制并不复杂:在对话上下文对象中跟踪总尝试次数,每次重试时扣除预算,一旦预算耗尽,无论单次调用的配置如何,都拒绝重试。这样做的好处是为单次对话的最坏情况成本设定了硬上限。这是预算应该存在的正确范围,因为对话才是用户真正关心的单元。单次用户交互就消耗了 50 美元重试费用的对话,其结果远比在 30 秒后返回错误的对话要糟糕得多。
首次尝试成功率 SLO
最后一部分是最重要的,也是最容易被忽略的:将首次尝试成功率目标写下来,作为一个正式的 SLO,并投入与团队处理面向用户的可用性指标时相同的严谨性。
首次尝试成功率 SLO 可以同时起到多个作用。它迫使团队承认供应商的实际错误率是一个值得治理的量,而不是一个埋在客户端 SDK 内部计数器中的数字。它创建了一个允许重试循环进行补偿的预算,但仅限于一定程度。它建立了一套词汇,使团队能够与供应商进行对话,而不是简单的“你的服务宕机了”,而是“在 SLA 为 99% 的情况下,你的服务在三周内的首次尝试成功率仅为 96%,这部分差值让我们损失了可衡量的金额。”这种对话之所以有效,是因为数字是具体的,测量是团队自己的,且成本转化是可审计的。
只要你将首次尝试 SLO 视为一个“成本-质量调节旋钮”,说服领导层的理由就很直观了。重试预算将供应商的不稳定性转化为供应商的成本。预算越大,团队为了向用户隐藏不稳定性而愿意承担的成本就越高。这是一个合理的权衡,但这种权衡必须由团队明确选择,而不是由重试层的默认设置替团队做决定。没有首次尝试 SLO 的重试预算,就是应用程序给供应商开出的一张支票,预先签名且金额不限。
在旧世界中免费的可靠性原语,在 LLM 世界中并非免费。重试预算就是其中之一。它产生的指标——平滑处理后的重试成功率——作为状态页面的数字还可以,但作为内部仪表盘的数字则具有误导性,因为它将两个问题混为一谈,且对任何一个问题的回答都不诚实。只盯着这个指标而不关注同一个循环中成本端的团队,正在为最终会被发票修正的可用性数字买单。
修复工作在代码层面很小,但在规范层面很大。将重试前和重试后的成功率作为独立指标展示,并设置各自的阈值。将每次成功的重试次数(RAPS)作为连续的先行指标进行跟踪。在任何单次调用预算之上,划定单次对话的重试预算。写下首次尝试成功率 SLO,并将针对该指标的供应商退化视为值得进行的正式对话。这些改变都不会触及重试循环本身。它们改变的是重试循环的行为所传达给你的信息,而这正是该循环最初值得存在的唯一理由。
会员专享
余下内容仅对会员开放。
会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
- —完整文章,包含未公开存档的部分
- —可落地的工作框架,附带权衡与决策依据
- —新文章抢先看,先于公开发布
随时取消 · 一次订阅,畅读全部