LLM 故障处理:回退/重试/降级

LLM API 调用失败的方式与普通 HTTP 调用完全不同。REST 端点要么返回有效的 JSON,要么不返回。LLM 可以返回 HTTP 200、格式正确的 JSON 信封,以及混乱的响应、拒绝回答、句子中途切断,或者自信地回答一个与你所问完全不同的问题。处理 LLM 故障意味着预见那个梯子的每一级——网络错误、速率限制、模型拒绝和静默质量下降——并提前决定在每一级做什么。

这里有效的类比是有连接选项的航班。你首选的航班(主模型)延误了。登机口代理(你的重试逻辑)尝试为你预订同一航线的下一个航班。如果那也满了(速率限制或提供商中断),她会让你乘坐另一家航空公司的航班前往同一城市(回退模型)。如果所有航班都取消了,而你绝对必须到达那里,你就租一辆车(降级模式——更慢、能力更弱,但你能到达)。乘客(你的用户)经历的灾难比登机口代理只是说"抱歉,回家吧"要小得多。

注意:故障分类在编写任何重试代码之前很重要。重试速率限制错误是有意义的。用相同提示在相同模型上重试内容策略拒绝每次都会失败。在重试之前了解你在重试什么。

1、为什么这很重要

LLM 提供商不是完全可靠的基础设施。OpenAI 的状态页面在截至 2026 年第一季度的一年中报告了大约 16 小时的停机时间——这是头条数字,不包括性能下降、延迟增加和从未作为事件出现的静默质量下降。在高峰流量窗口期间,提供商有时会将请求路由到量化模型变体:你的 API 调用返回 HTTP 200 和结构上有效的响应,但输出质量在没有任何错误信号的情况下显著下降。

将 LLM 视为完美可靠数据库的应用程序会将每个故障直接作为硬错误呈现给用户。为故障设计的应用程序会将大多数故障转化为短暂的停顿或稍微不太强大的响应。这两种体验之间的差距几乎完全取决于你的重试、回退和降级逻辑的质量。

无所作为的成本

  • 每次提供商出现故障时都会出现面向用户的 500 错误——即使 30 秒的停机也会导致明显的故障,如果你完全没有重试的话。
  • 如果你在没有退避的情况下进行天真重试,则会出现雷群效应:所有客户端在同一时刻重试,冲击已经紧张的 API 并使停机时间更长。
  • 因重试不可重试的错误(内容拒绝、上下文长度超限)而浪费支出,这些错误永远不会成功。
  • 如果你只监视 HTTP 错误率,则静默降级无法被检测到——延迟峰值和质量下降需要单独的监控信号。

2、工作原理

强大的 LLM 故障处理结合了三层:重试逻辑(以更智能的时间再次尝试同一提供商)、回退路由(切换到不同的模型或提供商)和降级模式(当所有 LLM 路径都用尽时提供减少但有用的响应)。这些层按顺序工作——你只在当前层真正失败时才降级到下一层。

2.1 重试逻辑

并非每个错误都值得重试。第一个决定是错误是瞬态的(如果等待片刻可能会清除)还是永久的(无论尝试多少次都会再次失败)。瞬态错误是重试候选者:429 请求过多503 服务不可用、网络超时以及提供商事件期间偶尔的 500 错误。永久错误不是:带有上下文长度错误的 400 错误请求、内容策略拒绝和身份验证失败。

对于确实值得重试的错误,使用带有完全抖动的指数退避。退避将每次尝试的等待时间加倍(0.5 秒、1 秒、2 秒、4 秒...),直到达到上限(通常为 8-10 秒)。抖动添加一个随机分数,这样当数十个客户端同时失败时,它们不会全部同步重试——这种同步重试浪潮是雷群问题,它可以将 30 秒的停机延长为几分钟的级联。

提示:在 429 响应上读取 Retry-After 头,然后再计算你自己的退避。OpenAI 等提供商包含它,它告诉你速率限制窗口重置的确切时刻——忽略它并在重置为 0.3 秒时休眠 4 秒会浪费延迟。

2.2 回退模型

当重试用尽时,调用会转移到回退模型。设计良好的回退链有两个维度:更便宜的同一提供商(例如同一模型系列的较小变体)和不同的提供商(完全独立的 API)。更便宜的同一提供商回退速度快且便宜;不同的提供商回退在主要提供商发生事故时提供真正的冗余。

具体的生产链可能如下所示:主要(前沿模型,主要提供商)→ 同一提供商的较小模型(成本较低,在软速率限制期间仍然可用)→ 不同提供商的等效模型(真正的独立性)→ 自托管或本地模型(完全没有外部依赖)。每一次跳跃都在能力和可用性之间进行权衡。并非每个应用程序都需要所有四次跳跃——客户支持机器人可能只需要两次,而高风险代理工作流可能需要所有四次。

2.3 降级模式

当每个模型路径都不可用时,问题是你仍然可以做什么有用的事情。降级模式不是故障——它是提供减少但诚实的响应而不是错误页面的有意决定。常见的降级模式策略包括:提供按提示哈希键控的最近缓存响应、返回带有人员升级路径的透明"AI 暂时不可用"消息,或为最常见的查询提供静态预写内容。

3、实现重试和回退

在实践中,大多数团队使用库而不是手动编写重试和回退逻辑。LiteLLM RouterPortkey 是 2025 年最广泛采用的两个选项。

3.1 LiteLLM Router

LiteLLM 暴露了一个 Router 类,该类包装多个模型部署。你声明一个模型列表、它们的重试次数和它们的回退链。路由器处理指数退避(从 0.2 秒开始,上限为 10 秒,带抖动)、context_window_fallbacks(当主要模型溢出时自动路由到更长上下文模型)和 content_policy_fallbacks(用于提供商拒绝)。cooldown_time 设置在重复故障后将部署标记为不健康一段可配置的时间窗口,防止路由器冲击死亡端点。

from litellm import Router

router = Router(
    model_list=[
        {
            "model_name": "gpt-5.5",
            "litellm_params": {"model": "gpt-5.5", "api_key": "..."},
        },
        {
            "model_name": "claude-sonnet-4-6",
            "litellm_params": {"model": "anthropic/claude-sonnet-4-6", "api_key": "..."},
        },
    ],
    fallbacks=[{"gpt-5.5": ["claude-sonnet-4-6"]}],
    context_window_fallbacks=[{"gpt-5.5": ["claude-sonnet-4-6"]}],
    num_retries=3,
    retry_after=5,          # 重试之间的秒数
    cooldown_time=60,        # 故障后将部署标记为不健康 60 秒
    set_verbose=False,
)

response = await router.acompletion(
    model="gpt-5.5",
    messages=[{"role": "user", "content": "总结这个文档。"}],
)

3.2 Portkey 网关

Portkey 在相同原语之上分层了可观察性。每个请求记录尝试了哪些模型、每个模型失败的原因、使用了哪个回退以及每次跳跃的成本。默认情况下,回退在任何非 2xx 状态时触发;你可以将其缩小到特定代码,如 429 和 503。Portkey 在升级到回退链之前使用指数退避重试最多五次。由于它每天处理超过 100 亿个令牌,其每个提供商的正常运行时间统计数据是最新且可操作的,可用于路由决策。

3.3 自己动手

如果你需要更紧密的控制,Python tenacity 库使重试/退避数学变得简单。下面的模式在 429 和 503 上重试,尊重 Retry-After 头,并在不可重试的错误上立即引发。

import time, random
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception

RETRYABLE = {429, 500, 502, 503, 504}

def is_retryable(exc: Exception) -> bool:
    status = getattr(exc, "status_code", None)
    return status in RETRYABLE

@retry(
    retry=retry_if_exception(is_retryable),
    wait=wait_exponential(multiplier=0.5, min=0.5, max=10),
    stop=stop_after_attempt(3),
    reraise=True,
)
async def call_llm(client, messages):
    try:
        return await client.chat.completions.create(
            model="gpt-5.5",
            messages=messages,
            timeout=30,
        )
    except Exception as exc:
        # 如果存在则尊重 Retry-After
        retry_after = getattr(exc, "headers", {}).get("Retry-After")
        if retry_after:
            time.sleep(float(retry_after) + random.uniform(0, 0.5))
        raise

4、断路器和质量信号

断路器位于你的重试逻辑之前。与其让每个失败的请求等待三次重试然后再显示错误,不如断路器在达到故障阈值后跳闸打开,并在可配置的冷却窗口内立即返回回退响应——不会对已知死亡端点进行浪费的重试。冷却后,电路进入半开状态:它让一个探测请求通过,如果成功则再次关闭。

LLM 断路器需要超越其分布式系统祖先所监视的内容。经典的断路器在 HTTP 错误上跳闸。但在高峰负载期间,LLM 提供商可能会返回 HTTP 200,但响应略有降级——比预期短、重复或语义上不正确。质量信号,生产团队将其添加到断路器逻辑中,包括:响应长度低于最小阈值、延迟攀升至基线的 3-4 倍以上(表明提供商在错误出现之前就在挣扎),以及模型停止发出有效 JSON 的结构化输出解析失败。

注意:监视延迟,而不仅仅是错误率,作为你的主要健康信号。在多个有记录的生产事件中,延迟从每次调用 2 秒攀升到 25 秒,然后才出现第一个 429。仅错误率的断路器在整个降级窗口期间保持关闭,同时成本飙升和用户等待。

面向用户的 LLM 服务的实用跳闸阈值:60 秒窗口内的 5 次连续故障(或质量违规)使电路跳闸打开;半开探测之前 60 秒冷却;在 >5% 错误率时提醒,在 >15% 时升级。这些数字来自多个生产部署的社区共识,应被视为起点,而不是绝对值。

5、常见陷阱和权衡

5.1 重试放大

每个请求三次重试听起来很温和。将其乘以你的并发数,你可以向已经限制你速率的提供商发送 3 倍的流量。将面向用户的请求的总挂钟时间限制在(30 秒是常见上限),并保持较低的重试次数(同步调用 2-3 次,后台作业最多 5-7 次)。对于同步调用,快速失败 + 回退优于对主要提供商的耐心重试。

5.2 回退能力不匹配

你的回退模型可能不支持与主要模型相同的功能。常见的不匹配:不同提供商之间的函数/工具调用模式不同,回退上的最大上下文窗口较小,结构化输出(JSON 模式)不受支持或使用不同的参数名称。显式测试你的回退路径,而不仅仅是正常路径。由于模式不匹配而崩溃的回退比原始停机更糟糕。

5.3 成本爆炸

每次重试和每次回退都要花钱。如果你的主要模型受到速率限制,而你的回退是不同提供商上更昂贵的模型,持续的事件可能会迅速增加你的 API 支出。设置明确的成本防护:每个请求的预算上限和每小时的支出警报。一些网关工具(LiteLLM、Portkey)允许你在配置而不是应用程序代码中强制执行这些。

5.4 将所有 429 视为相同

429 可能意味着三件不同的事情:你达到了每分钟请求数限制(几秒钟内重置)、每分钟令牌数限制(几秒钟内重置,但缩小你的请求),或配额限制(你的计费层级已用尽,无论等待多久都没有帮助)。前两个值得重试;第三个应立即触发运维警报和降级模式响应。读取错误正文和头——提供商包含结构化详细信息。

6、深入了解

一旦基本的重试和回退就位,更高级的模式在规模上变得相关。

6.1 尾部容忍重试策略

标准重试逻辑等待请求完全超时后再重试。对冲请求(也称为推测性重试)在第一个请求超时之前向第二个部署发出重复请求——通常在设置为第 90 百分位延迟的延迟之后。如果任何一个响应先到达,则使用获胜者,另一个被取消。这项技术以略高的平均支出为代价大幅降低 p99 延迟,对于长尾痛苦的延迟敏感型用户最有价值。

6.2 加权故障转移和金丝雀路由

LiteLLM 的 enable_weighted_failover 设置通过在升级到跨提供商回退之前使用现有权重重新选择不同的部署,在主要模型组内重试。这是同一模型多区域部署的正确默认设置:首先用尽你的区域副本,然后跨提供商。金丝雀路由——将 5% 的流量发送到新模型并在转移更多流量之前监视质量指标——使用相同的加权路由基础设施,但作为部署策略而不是可靠性策略。

6.3 可观察性不是可选的

你无法调整你无法观察的阈值。使用至少以下内容检测每个 LLM 调用:使用的模型、提供商、延迟、输入令牌计数、输出令牌计数、HTTP 状态、哪个重试尝试(0 = 第一次尝试),以及是否触发了回退。将这些聚合为每个提供商的错误率和每个提供商的 p95 延迟仪表板。你希望能够用不到 30 秒回答的信号:哪个提供商现在降级了,我的多少流量正在访问它?

6.4 幂等性和副作用

重试对于只读 LLM 调用是安全的。如果你的 LLM 调用有副作用——发送电子邮件、写入数据库或收费的工具调用——它们就变得危险。在向代理管道添加重试之前,请审计每个工具的幂等性。将非幂等工具包装在去重键中,以便实际成功但未收到响应的重试调用不会两次触发操作。

提示:在每个 LLM 调用上使用请求 IDX-Request-Id 或等效项)并将其传递到你的可观察性后端。当你需要调试故障或重复工具执行时,你可以从单个跟踪 ID 重建完整的重试/回退链。

7、常见问题

对于面向用户的 LLM 调用,我应该使用多少次重试?

对于同步的面向用户的请求,两到三次重试是典型的生产上限。三次尝试后,你已经增加了明显的延迟;如果提供商在该窗口内没有恢复,那么是时候回退到不同的模型,而不是重试更多。没有用户等待的后台作业可以安全地进行五到七次尝试。

回退模型和降级模式有什么区别?

回退模型仍然是实时 LLM 调用——你切换到不同或更便宜的模型。降级模式是当所有 LLM 路径都失败时你要做的事情:提供缓存响应、返回静态"AI 暂时不可用"消息,或路由到人员。回退保持高质量;降级模式使应用程序以降低的功能运行。

我应该总是重试 429 速率限制错误吗?

这取决于子类型。每分钟请求数或每分钟令牌数限制将在几秒钟内清除——在读取 Retry-After 头后重试。计费配额用尽(你的层级在本月已满)永远不会自行清除;重试它是纯粹的浪费。在编写重试谓词之前,读取错误正文和头以区分两者。

如果 HTTP 状态仍然是 200,我如何检测静默质量降级?

在 HTTP 状态之外添加辅助健康信号:输出长度(明显短于正常是金丝雀)、结构化输出解析失败(模型停止发出有效 JSON)和每个提供商的 p95 延迟(延迟攀升通常在错误出现前几分钟)。将这些信号与错误率一起馈送到你的断路器和警报管道。

在使用工具的代理管道中重试 LLM 调用安全吗?

只有当工具是幂等的时才安全。纯文本生成调用可以自由重试。发送电子邮件、写入数据库或发起付款的工具调用不能——重试实际上成功但未返回响应的调用将两次执行操作。在启用重试之前,请审计每个工具的幂等性,并将非幂等工具包装在去重键中。

我需要像 LiteLLM 或 Portkey 这样的完整 LLM 网关,还是可以在应用程序代码中处理?

应用程序代码在小规模下可以很好地处理简单的重试和回退。当你有多个模型、多个提供商、团队范围的成本可见性和每请求跟踪需求时,网关就能发挥其价值。对于大多数团队来说,转折点是在生产中有两三个模型——此时,网关的跨切面可观察性和集中配置比在每个服务中维护逻辑更快地获得回报。


原文链接:How to Handle LLM Failures: Fallbacks, Retries, and Degraded Modes

汇智网翻译整理,转载请标明出处