第09章-失败是常态:中断、重试与降级

5 阅读14分钟

第 9 章 失败是常态:中断、重试与降级

难度 ★3 | 约 3.5 学时 | 6 节

这一章把「模型挂了怎么办」从一句运维口号,变成界面上可验证的行为。它的判据不是「有没有兜底」,是「兜底发生时用户知不知道」。

学习目标:把「模型挂了怎么办」从一句运维口号,变成界面上可验证的行为。

本章交付物:一张降级分级表(触发条件 / 表现 / 用户可见说明)+ 四种故障的演示记录


9.1 失败率永远不是 0——但事故条数衡量的是公布政策

「按一定会失败设计,而不是按不要失败祈祷」——这句话对,但它容易被读成一句心态建议。它有更硬的一层:失败在你的接口契约里是已登记的正常返回,不是异常。

官方的错误码清单本身就是一份失败分类学:

类别在清单里的位置
限流429,而且下分四种成因(9.5 展开)
服务端错误500
模型暂时过载503,带一个专门的错误码
内容拦截有专门的错误码

请注意这张表的性质:这四类都有名字、有码、能分支。也就是说「失败」是一个你可以写 catch 的正常返回值,而不是「不该发生的事」。这就是「按失败设计」的工程含义——不是心态,是可分支。

那要不要给一个「失败率大概多少」的量级?可以,但必须写清口径,否则它会被当成结论引用。

本轮能拿到的是第三方独立监测的数据:多家监测站与一份跨服务商报告,把主流模型接口的可用性放在大致 98% 到 99.9% 的区间,并普遍低于成熟付费基础设施(支付类约 99.99%)。这些是第三方测量、不是官方,样本口径各不相同,只能当量级参照——正文里不能写成「官方数据显示」。

而这一节最反直觉、也最值得抄下来的一条观察是:

事故条数衡量的是「公布政策」,不是可靠性。

同一份第三方统计里,一家在 90 天里只列了 2 起接口事故,另一家列了 210 起、且「几乎全是自动化的按模型告警」——而两家的实际可用性差异远小于条数差异。

同一份统计还有一条能说明「时段分布也不可信」的:约一半的接口事故起始于工作日 9 点到 17 点,而这一时段只覆盖一周的 24%。这个比例说明的不是「上班时间更容易坏」,而是**「上班时间才有人看、才有人记」**。

还有一层:自报可用性与独立监测之间存在系统性差距(同一时期一家自报约 99.88%、独立测得约 98.16%)。原因是结构性的——服务商可以自行决定什么算事故,独立监测没有这层滤镜。

所以这一节的产出不是一串数字,而是三条引用纪律:

① 不引横向排名。 排名变化不等于你的风险变化。 ② 不引条数当可靠性。 条数是公布政策。 ③ 不引自报可用性当事实。 它和独立测量不是同一件事。

收口:这一节的真正结论是「怎么读可靠性数据」,不是「某家有多可靠」。 前者能用三年,后者三个月就过期。


9.2 降级要分级:说清「你拿到的是什么」,不说「服务端出了什么错」

四级降级——换模型 / 关功能 / 走缓存 / 给静态答复——是本书的归纳,但它对应到官方口径上是成立的:官方把优雅降级与安全重试并列为对中断流的正确处理方式。

四级里最值得单独盯的是换模型:它是最容易被悄悄做掉的一级,也是体验差异最明显的一级。

而这一节真正的判据,是一条方向相反的官方约束。

官方建议:错误态给用户通用文案(原话举的例子是「出了点问题」),理由是「避免泄露来自服务端的信息」。

所以「每一级都要在界面上说清楚用户拿到的是什么」这句必须拆成两句:

要说清不要说
你拿到的是什么:这是降级结果 / 这是缓存内容 / 这段不完整服务端出了什么错:上游错误原文、错误码、堆栈、内部服务名

这不是文字游戏。把上游错误原文贴到界面上,正是这条官方建议明确反对的做法——它既泄露了服务端信息,又对用户毫无用处(他不能拿一个 503 去做任何事)。

第二处要补的是:降级要可见。

「悄悄换了模型」比「明确说这次用的是备用模型」更伤信任——因为用户没有信息去解释「为什么这次答得不一样」,只会得到一个笼统的印象:这东西时好时坏。 而「时好时坏」这个印象,比「偶尔降级」严重得多——前者让用户不敢依赖,后者只是一次可理解的妥协。

第三处要补的是触发条件,否则分不了级。官方错误码清单里的四类失败正好可以当触发条件:

触发条件降到哪一级界面文案方向
限流 429(速率类)退避后重试,仍不行则换模型「正在重试,稍等」
过载 503换模型(可用的备用模型)「这次用了备用模型,结果可能略有不同」
超时走缓存 / 给静态答复「这一步比较慢,先给你上次的结果」
内容拦截关功能(不重试)「这个请求没法处理」——不解释规则细节

这张表的第三列是产品与文案的决策,不是工程能单独定的——所以它必须在设计阶段就填,不能等上线后补。 这是本节最实用的一句话:分级表的价值在第三列,而第三列最容易被留空。

收口:降级的目标不是「不失败」,是「失败时用户知道自己拿到了什么」。 前半句是运维的活,后半句是这一节的活。


9.3 超时与「看起来卡住」:收尾方法不调用,界面就必然卡住

大纲说「长响应必须给进度或可中断——沉默的等待比明确的失败更伤信任」。这句对,但在生成式 UI 里,「卡住」不是一个概率事件,而是一条由接口契约决定的必然路径。

官方参考文档对那个「往前端推界面」的流,给它用来收尾的方法写了一句很硬的话:

「这个方法总是必须被调用,否则响应会卡在加载状态。」

请把这句话的强度读出来:不是「建议调用」,是「总是必须」;不是「可能卡住」,是「会卡住」。 于是「界面卡住」从体验问题变成了工程问题——它是一条可以静态推演出来的路径。

由此得到这一节最重要的动作,也是它改变排查顺序的地方:

「界面卡住」的第一排查顺序不是「加个进度条」,而是先把代码路径走完。

任何一个提前 return、任何一个抛异常没被接住、任何一个分支忘了收尾,都会让那个方法不被调用——界面就永远停在加载态。 所以那句判据要写死在这里:

进度条救不了没走完的代码路径。

同一页还给了一个配套事实:出错也有它该走的出口。 出错方法的语义是「把错误抛到客户端、由最近的错误边界接住」——也就是说,出错路径不调用它,错误就不会被接住,界面同样停在加载态。

第二种「看起来卡住」的成因,是连接级上限:流式接口的套接字模式有一条 60 分钟的连接上限,超过会返回一个专门的错误,需要重开连接继续。长会话与长任务会撞上它,而它表现为「好好的突然不动了」——这种「没有报错的不动」最容易被误判成前端 bug。

第三种成因才轮到体验层,而它有一条底线:进度与可中断是两件事,至少要有一件。

只有进度只有可中断两样都没有
用户盯着一个永远到不了的百分比用户会在三秒后按下停止用户会以为功能坏了,然后刷新页面

所以这一节压成三句话:先查路径走完没有;再查是不是撞了连接上限;最后才谈进度条与停止按钮。


9.4 部分成功要保留——但你的用量账会同时缺一块

大纲原来的写法是:「已经生成的内容不该因为最后一段失败就全部丢弃——但要标出它不完整。」

这一句是对的,官方也确实这么说:「对中断的流,要把它当作不完整的响应来处理,并为安全重试或优雅降级设计客户端。」

但它只写了一半。同一份官方指导紧跟着写的是另一句:

「如果你依赖流式的用量数据,当流被中断时,最后那个用量分片可能不会到达,所以你的记账路径应该容忍缺失的最终用量,并回退到估算或平台侧的指标。」

把两句并排看,结论就很清楚了:「保留部分内容」与「拿不到完整用量」是同一个事件的两面。

按原来那句写,读者会在界面上把部分内容留下来(这一步是对的),然后在成本账上拿不到那一笔的用量——两个各自正确的本地决策,合起来得到一个错误的总账,而错误的方向是「少算」。

为什么这条错得比「写错一个事实」更危险?因为错的判据会被争论,缺的判据只会被默默执行。 少了这半句,读者看到一个对不上的账,第一反应会是「模型把这次调用的用量报错了」或者「我们的统计代码有 bug」——而实际原因是那个分片根本没到。 这是一个典型的「判据不完整 ⇒ 排查方向错」:它会持续消耗排查时间,而永远查不到东西。

所以这条判据必须拆成两句,两句都要落到具体动作:

① 界面侧:保留,并标出「不完整」。 标记要具体到用户能理解——不是一个小图标,而是一句话(「这次回答没有生成完」)+ 一个动作(「继续」或「重试这一段」)。只标不给出路,等于把「不完整」变成「不能用」。

② 记账侧:把「没有用量分片」当成一种正常结局。 用估算值补上,并且在账上标出来它是估算。这后半句不能省——否则估算值会和真实用量混成一片,下一次对账时没人知道哪一笔是补的,而你刚刚把「少算」换成了「查不清」。

还有一处区分必须写清,否则账会更难对:「流被中断」会让用量缺一块;而「配额耗尽」是用量完整的(请求根本没被处理,或者返回了明确的额度错误)。把这两种混在一起,会让「缺用量」这个信号失去意义——因为你分不清是漏了一笔,还是压根没发生。

于是这一节的交付物也变成两样,缺一不可:界面上「不完整」的呈现(一句话 + 一个动作),账上「估算」的呈现(数值 + 一个「这是估算」的标记)。

收口:「部分成功」不是一个界面状态,它是界面与账本同时进入的一种状态。 只处理界面那一半,你会在月底发现账少了一块,而所有日志都显示一切正常。


9.5 成本熔断:429 里有三种根本不是「请求太快」

看到 429,大多数人的第一反应是「限流了,等一下再试」。而官方错误码清单把这个状态码拆得很细,其中三种根本不是「请求太快」:

429 的成因它其实是什么
额度耗尽预付余额用完了
组织支出上限到达了你们设置的支出上限
组织用量上限到达了平台分配的用量上限
请求速率上升太快这一种才是「请求太快」

四种成因,只有一种能靠等解决。 而官方给的处理方式是最硬的一句:

「重试计费、支出或配额错误不会恢复接口访问。在发送下一个请求之前,先更新相应的余额或上限。」

这句话直接推翻了「429 一律退避重试」这条通用经验。而它还有一个更隐蔽的陷阱,官方专门提醒了:计费类错误的通用类型字段可能仍然显示成「额度不足」——所以要看具体错误码才能分辨成因。

于是这一节的核心判据要改写——熔断的判据不是「错误率」,而是「错误里有多少种是重试无效的」。

为什么这个改写重要?因为「错误率 5% 就熔断」这个规则,会把「余额用完」这种必须先处理的错误当成「等一下就好」,于是产生一个无限重试循环。而这个循环是自我加重的:官方明确说失败的请求照样计入每分钟配额——你越重试,配额越少,越重试。

第二条要补的是:平台侧的支出上限与用量上限是现成机制,要把它当成功能的一部分来配置,而不是等账单出来才发现。这是大纲那句「额度上限与自动关闸是功能的一部分,不是运维的事后补救」唯一能拿到的硬支撑——它不是一句态度,是两个可配置的上限。

第三条要补的是熔断之后用户看到什么:

按 9.2 的口径,这时候该给的是这一级的降级结果,而不是把额度错误码贴出来。用户不需要知道「organization_spend_limit」,他需要知道「这个功能今天暂不可用,你可以先用缓存结果」。

收口:这一节要防的失败模式很具体:一个把「余额用完」当成「限流」的无限重试循环。 它不会报错,只会让你的账单和用户一起消失。


9.6 一个可观测的验证:四种故障各演一遍

这一节的身份要分两半说清,否则读者会把整节当成官方清单。

四种场景——断网、超时、限流、内容拦截——是本书的切法,官方没有给过这个清单。而每一种在接口层对应哪个错误码,是官方的:

场景接口层对应什么归属
断网没有对应项——它是传输层的事,不是一个接口错误本书场景 + 必须自己制造
超时请求超时官方清单
限流429(四种成因,9.5)官方清单
内容拦截专门的错误码官方清单

这张表最重要的信息是第一行:四种里有一种必须自己制造。 断网不在官方清单里,因为它不是「接口返回了一个错误」,而是「请求根本没到」。这也解释了为什么它最容易被漏测——你没有办法通过「等接口报错」来测它。

每种场景演一遍,记录三项:

① 触发方式。 具体到可复现(断网是哪个开关、超时是哪个阈值、限流打哪个接口、拦截用哪句话触发)。 ② 界面表现。 用户看到什么:转圈、错误文案、还是空白。 ③ 用户可见文案。 一字不差地抄下来。

第三项最容易被漏,也最难事后补——因为它需要产品与文案的决策,不是工程能单独定的。所以要么在演练时就把它写下来,要么上线后没人能回头补。这是本节最实用的一句提醒:前两项工程自己能拍,第三项必须拉人。

这条验证与 9.2 的关系也要写清:四种演练记录正是「降级分级表」的输入。 没有演练,分级表只能靠想象填——而想象出来的「触发条件」通常比真实情况乐观一档。

收口:演练的产出不是「我们测过了」,而是四个方框被填满。 空着的那一格,就是上线后第一个被用户问到的问题。