LiteLLM 回退/故障转移配置:三种 fallback 分开配才有用
我见过太多这样的配置:LiteLLM Proxy 的 config 里写了一行 fallbacks,主模型挂了就切到备用模型,配完心里踏实了,觉得容灾这块算是做完了。然后真出事那天,日志里明明白白写着回退触发了,可请求还是全线失败——因为那天挂掉的不是模型本身,是有一批请求被内容策略拒了,网关老老实实地把它们换到了同一家厂商的另一个模型,然后被同样的策略又拒了一遍。还有一次是上下文超限,回退目标是个窗口一样小的模型,换过去照样装不下。
回退这件事的关键不在于”配没配”,而在于失败的原因和回退的目标匹配不匹配。LiteLLM Proxy 官方文档里之所以把 fallback 拆成好几个字段,就是因为这几类失败在语义上根本不是一回事,混在一起处理会让回退变成一个看起来跑起来了、实际上毫无作用的动作。这篇文章把这几个字段、以及配套的重试和熔断参数讲清楚。所有字段名和示例都取自 LiteLLM 官方 reliability 文档(核对于 2026-08-07),版本会变,落地时请以官方文档当次为准。
一、先把失败分类,再谈回退
在动手写 YAML 之前,先花两分钟把你的失败类型列一列。一个 LLM 调用失败,粗分下来大致是这么几类:
| 失败类型 | 典型现象 | 换个模型有用吗 |
|---|---|---|
| 上游故障 / 超时 / 限流 | 5xx、连接重置、429、长时间无响应 | 有用,换一家就行 |
| 内容策略拒绝 | 请求或响应被安全策略挡下 | 换同厂商的另一个模型多半没用 |
| 上下文超限 | 输入 token 数超过模型窗口 | 只有换更大窗口的模型才有用 |
| 模型名配错 | 路由表里根本找不到这个模型 | 需要一个兜底目标 |
这张表就是 LiteLLM 那几个 fallback 字段的设计动机。你可以把它理解成:网关在失败发生时先看”这是哪一类失败”,再去对应的回退表里找目标。你不配对应的表,它就只能走通用那条路,而通用那条路对后三类失败常常是无效的。
二、四个 fallback 字段各管一类失败
以下字段嵌套在 router_settings 或 litellm_settings 下。我只给字段片段,不给完整的 config 文件结构——完整结构以官方文档为准,凭印象拼一个完整配置文件出来是很容易把人带沟里的。
2.1 fallbacks:普通失败时的备选链
fallbacks: [{"primary-model": ["fallback-model-1", "fallback-model-2"]}]
这是最常见的那条链:主模型出了一般性的故障(上游 5xx、超时、限流),就按顺序去试备选模型。注意它是一个列表,可以给多个备选,网关会依次尝试。
配这条链的时候有个容易忽略的点:备选模型和主模型应该尽量不共享同一个故障域。如果你的主模型和备选模型来自同一家服务商、走同一个区域的接入点,那么服务商整体抽风的时候两个一起挂,回退等于没配。真正管用的备选链是跨厂商的,哪怕能力略差、价格略高——回退目标的价值在于”还能出结果”,不在于”结果一样好”。
哪些业务最容易撞上这一类?所有业务都会撞上,这是基础款。高峰期流量集中的对话类应用撞得尤其频繁,因为限流和排队通常在同一时间段发生。
2.2 content_policy_fallbacks:内容策略拒绝时的回退
content_policy_fallbacks: [{"model-a": ["model-b"]}]
这是最值得单独拿出来讲的一个。内容策略拒绝的特点是:它不是随机故障,是确定性的判定。同一条请求,你重试一百次,结果一百次都一样被拒。所以对这类失败,num_retries 那种重试逻辑完全是浪费——重试不会改变判定结果,只会白白烧掉时间和额度。
更关键的是回退目标的选法。同一家厂商的不同型号,安全策略往往是同一套或高度相似的一套,把请求从这家的 A 型号换到这家的 B 型号,被拒的概率几乎不变。要让 content_policy_fallbacks 真正起作用,回退目标应该是一家策略取向不同的服务商,或者是一个策略更宽松的自建/开源模型部署。
哪些业务最容易撞上?我的经验是这几类:内容审核与舆情分析(要处理的输入本身就带敏感内容)、医疗健康和法律咨询类问答(专业表述会被泛化的安全规则误伤)、安全研究和渗透测试相关的工具、以及做小说 / 剧本 / 游戏对白生成的内容平台(暴力冲突情节是叙事刚需)。这几类业务如果不单独配这条回退,会出现一种很难排查的现象:整体成功率还行,但某一小撮用户的请求几乎百分之百失败。
2.3 context_window_fallbacks:超上下文长度时回退到大窗口模型
context_window_fallbacks: [{"small-model": ["large-model"]}]
这个字段为什么必须单独配?因为在所有失败类型里,它是唯一一种”换一个更大的模型就一定能成功”的失败。上下文装不下是个纯粹的容量问题,没有任何随机性、没有任何服务商差异,你只要把它路由到一个窗口足够大的模型上,请求就能跑通。
反过来说,如果你不单独配它,让超上下文的请求走通用 fallbacks 链,那么这条链上的模型只要窗口不比主模型大,就会一个接一个地失败,最后你付出的代价是:N 次失败的延迟叠加,加上一个照样失败的结果。这是我说”混配会让回退变成无效动作”时最典型的场景。
配置上的要点很直白:回退目标的窗口必须确实比主模型大,而不是”看起来像个高级型号”。各模型的上下文窗口以各家官方文档为准,不要凭印象排序。
哪些业务最容易撞上?长文档问答与合同审阅、RAG 检索结果拼接(召回条数一多,拼出来的 prompt 长度是会失控的)、多轮客服会话(历史消息累积到某个回合突然爆掉)、代码库分析类工具(一个大文件就能顶掉半个窗口)。这几类业务的共同特征是输入长度不由你控制,所以必须有这条兜底路径。
顺带说一句排查经验:这类失败在监控图上很有辨识度——整体成功率轻微下滑,但失败请求的输入 token 数分布明显偏向长尾。看到这个形状,先去查上下文回退配没配。
2.4 default_fallbacks:兜底
default_fallbacks: ["fallback-model"]
这是模型配错时的兜底。典型场景是客户端传了一个路由表里不存在的模型名——可能是拼写错误,可能是你下线了某个模型但某个老版本客户端还在调,也可能是别的团队照着一份过期的文档在接。
它的价值不在于容灾,在于降低”配置漂移”的爆炸半径。一个 SaaS 场景下,你不可能保证所有调用方都在你下线模型的同一天跟着改。有个兜底目标,至少这些请求还能返回结果,而不是直接给用户一个错误页。当然,兜底不是掩盖问题的借口,命中兜底的请求应该在日志里明确标记出来,好让你知道谁还在调不存在的模型。
三、重试与熔断:num_retries、request_timeout、allowed_fails、cooldown_time
回退解决”换谁”的问题,重试和熔断解决”什么时候放弃”的问题。这两组配合起来才是完整的可靠性策略。
num_retries: 3
request_timeout: 10
allowed_fails: 3
cooldown_time: 30
先把最重要的话说在前面:上面这四个数字(3 / 10 / 3 / 30)是官方文档里的示例值,既不是默认值,也不是推荐值。 我把它们原样抄在这里是为了让你看清字段写法,不是让你照抄。这几个值该取多少,完全取决于你自己的 SLA:你的接口对外承诺多长时间内响应?你的用户能忍受多久的等待?你的上游有多不稳定?把这几个问题回答清楚再填数,不要抄任何人的配置——包括这篇文章的。各字段的实际默认值以官方文档为准。
3.1 num_retries:每个模型的重试次数
它管的是”对同一个模型试几次”。重试对随机性失败(瞬时网络抖动、偶发 5xx)是有效的,对确定性失败(内容策略拒绝、上下文超限、鉴权错误)完全无效——所以前面才会强调那两类失败要靠对应的 fallback 而不是靠重试。
设置这个值时要意识到一件事:重试次数会乘进你的最坏延迟。如果单次超时是 T,重试 N 次,再叠加 M 个回退目标,最坏情况下用户等待的时间可能接近 T × (N+1) × (M+1)。这个乘法是很多”网关配好了但线上体验更差了”的根源。算一遍这个乘积,看它有没有超出你能接受的响应上限。
3.2 request_timeout:单次调用最长时间
这是防挂死的那道闸。没有超时约束的调用,在上游变慢(而不是变挂)的时候是最要命的:连接一直开着,你的连接池、你的协程、你的下游线程全都被拖住,而监控上看不到任何”错误”,只看到吞吐掉下去。
设这个值的思路是从用户能接受的端到端延迟倒推:先定端到端上限,减去你自己的处理开销,再除以前面算过的那个重试×回退乘积,剩下的才是单次调用能给的时间预算。倒推出来的数字如果小得离谱,说明你的重试和回退层级配太深了,得砍。
流式和非流式的时间特征差别很大,长文本生成天然就慢,做批处理的离线任务和做实时对话的在线接口不该共用一套超时值。有条件的话按业务线拆开配。
3.3 allowed_fails + cooldown_time:这两个合起来是熔断器
单看这两个字段容易觉得平平无奇,但它们组合起来的语义是标准的熔断器:某个模型连续失败次数达到 allowed_fails 设定的阈值,网关就把它冷却掉(在 cooldown_time 这段时间内不再往它发请求),冷却期过后再把它放回路由池里试。
为什么熔断比”多重试几次”重要得多?看一个雪崩是怎么形成的:
- 上游因为负载升高开始变慢,响应时间从正常水平往上飘;
- 你这边的超时开始触发,重试逻辑启动,同一批请求被重复发到已经吃不消的上游;
- 上游收到的请求量因为重试而变多,于是更慢;
- 更慢导致更多超时,更多超时导致更多重试……
这个环里每一步都是”合理”的局部决策,合起来就是一台加速上游死亡的机器。而且被拖垮的不只是上游:你这边等待中的连接越积越多,最后自己的服务也一起躺下。这就是为什么无限重试(或者次数虽有限但没有熔断配合的重试)在分布式系统里是个反模式。
熔断做的事情是打断这个环——失败到一定程度就干脆别发了。冷却期里的请求直接走回退目标,既给了上游喘息的机会,也让你这边的请求不再排队等一个注定超时的调用。等冷却期过了再放回去试探,如果上游缓过来了就自然恢复,不需要人工干预。
配这两个值有几点经验:allowed_fails 太小会让偶发抖动误伤好模型(刚上线的模型可能因为一次网络抖动就被冷却),太大又起不到保护作用;cooldown_time 太短等于没冷却,上游还没缓过来就又被打满,太长则会在上游其实已经恢复的情况下继续把流量压在备选模型上,白白多花钱或者多等延迟。这两个值是要根据你上游的实际抖动特征调的,先按保守值起步、观测一段时间再调整,比一次性拍一个”最优值”靠谱。
关于路由策略:LiteLLM 还提供了路由策略的配置能力,但具体取值枚举我这里不列——取值以官方文档为准,凭印象写路由策略名是最容易把配置写成静默失效的地方之一。
四、enable_pre_call_checks:把注定失败的请求挡在发送前
enable_pre_call_checks: true
这个开关的作用是在把请求发出去之前先做校验。它的价值可以用一句话概括:一个注定会失败的请求,越早失败越好。
想想一个超上下文的请求如果不做预检会经历什么:完整地序列化、完整地通过网络发出去、上游完整地接收并解析、然后返回一个”太长了”的错误。这一趟里,网络传输的时间花了,上游的处理时间花了,如果计费口径把这部分算进去,钱也花了——而结果是零。请求越大,浪费得越狠,而恰恰是大请求最容易触发这类失败。
预检把这个判断提前到了发送之前。省下的是三样东西:时间(不用等一个来回)、钱(不发出去就不产生上游侧的开销)、以及路由决策的质量——网关在发送前就知道这个请求装不下,可以直接路由到窗口更大的目标,而不是先失败一次再回退。这和前面讲的 context_window_fallbacks 是一对搭档:预检负责识别,回退负责安置。
代价是每个请求多一点点本地计算开销。对绝大多数业务来说,这个开销和它省下的失败调用相比完全不值一提。具体校验哪些内容、开销多大,以官方文档为准。
五、一个完整的回退策略该怎么设计
把上面的东西串成可执行的步骤:
第一步:按失败类型分流。 打开你最近一个月的错误日志,把失败请求按原因分桶:上游故障 / 限流、内容策略拒绝、上下文超限、模型名不存在、其他。不要凭感觉,要看真实占比。很多团队做完这一步会发现,自己一直在优化的那类失败其实只占 5%,而占大头的那类压根没配回退。
第二步:每类失败配对应的 fallback。 通用失败配 fallbacks,并确保备选跨故障域;内容策略拒绝配 content_policy_fallbacks,回退目标选策略取向不同的一家;上下文超限配 context_window_fallbacks,回退目标的窗口必须确实更大;再配一条 default_fallbacks 兜住配置漂移。第一步里占比为零的桶可以先不配,但要在文档里写明”暂未覆盖”,别让后来的人以为已经考虑过了。
第三步:加熔断防雪崩。 配 allowed_fails 和 cooldown_time。这一步不是可选项——只要你配了重试,就必须配熔断,否则你配的是一台雪崩加速器。值按你上游的抖动特征定,保守起步。
第四步:加超时防挂死。 配 request_timeout,从端到端 SLA 倒推。同时把 num_retries 和回退层数的乘积算一遍,确认最坏延迟在可接受范围内。超了就砍层级,不要指望”最坏情况不会发生”。
第五步:开预检。 打开 enable_pre_call_checks,让注定失败的请求在发送前就被拦下并正确路由。
第六步:演练验证。 这一步最容易被跳过,也最容易在真出事那天让你付出代价。回退配置有个讨厌的性质:平时完全看不出配没配对,只有在故障发生时才暴露。所以必须主动制造故障来验证。
演练可以这么做:故意配一个错误的模型名,看是不是走了 default_fallbacks;构造一个明显超长的输入,看是不是被路由到了大窗口模型而不是在备选链上连撞几次;把某个模型的凭证临时改错,看熔断是不是按你设的阈值把它冷却掉了、冷却期结束后是不是自动恢复。每一项都要在日志里能看到清晰的回退轨迹——如果日志里看不出请求最终是被哪个模型处理的,那么先去把可观测性补上,因为你连”回退有没有生效”都无法验证。
演练的价值不只是发现配错,还在于给你一组真实的延迟数据。回退发生时用户实际等了多久,这个数字只有演练能告诉你。
六、上线前自查清单
- 四类失败(通用 / 内容策略 / 上下文超限 / 模型名错误)是不是各自配了对应的 fallback 字段,而不是全靠一条
fallbacks链? fallbacks里的备选模型和主模型是不是跨故障域的(不同厂商、不同接入点),而不是同一家的兄弟型号?context_window_fallbacks的回退目标窗口,是不是确实比主模型大(按各家官方文档核对过,不是凭印象)?content_policy_fallbacks的回退目标,是不是策略取向不同的一家,而不是同厂商换个型号?num_retries× 回退层数 ×request_timeout的最坏延迟乘积算过没有,结果在你的 SLA 之内吗?allowed_fails和cooldown_time配了吗——只要有重试就必须有熔断,否则上游一变慢你就在给雪崩添柴。- 配置里的数值是按自己的 SLA 定的,还是从某篇文章(包括这一篇)里抄来的示例值?官方示例里的 3 / 10 / 3 / 30 不是默认值也不是推荐值。
- 回退演练过吗——故意配错模型名、故意发超长输入、故意改错凭证,三种失败各跑一遍,确认回退轨迹在日志里清晰可见?
最后再说一遍所有字段的归属:这些配置嵌套在 router_settings 或 litellm_settings 下,完整的 config 文件结构以 LiteLLM 官方文档为准。字段名会随版本演进,落地前请对着当次版本的官方文档核一遍,别拿一篇文章(包括这篇)当配置说明书用。
延伸阅读:AI 网关容灾与自动降级:failover 机制详解 讲的是回退背后的通用容灾思路;LiteLLM 相关介绍 可以先建立整体印象再回来配这些字段;如果你正在被限流折磨,429 错误处理 和超时排查这两篇里的处理手法可以和这里的重试、熔断配置对照着看。