大模型多供应商容灾完全指南:从一次"供应商一抽风我整个 AI 功能全挂"看懂故障转移、熔断与降级

2024 年我在产品里做一个 AI 功能接了某家头部大模型的 API 让它帮用户做内容生成。接大模型这件事我压根没多想。第一版我做得很省事接大模型不就是拿到一个 API key 找一个 SDK 在需要的地方调一下 chat 把结果拿回来。哪个功能要用就在哪个文件里 import 那个 SDK 调一下。本地开发时真不错我调一下模型秒回结果有模有样几行代码搞定。我心里很踏实接大模型嘛不就是调一个 API。可等这个功能真正上线被真实用户用起来一串问题冒了出来。第一种最先把我打懵某天那家供应商区域性宕机接口大面积返回 503 不是我某一个请求失败是我打过去的每一个请求都失败。我的整个 AI 功能在那段时间里完整地彻底地挂掉了而我除了等它自己恢复什么都做不了。第二种最难受供应商对我的账号限流流量一上来大量请求被 429 顶了回来 AI 功能不是挂了是一会儿能用一会儿不能用时好时坏。第三种最隐蔽有一阵子供应商没宕机也没限流可它的响应延迟从平时的两三秒飙到了三四十秒。功能还在可慢到没有任何用户愿意等还在但不可用这种状态比直接挂掉还难处理因为我的监控只盯着有没有报错而它压根没报错。第四种最说不清出了这些事我想加一个备用供应商顶上翻开代码才发现对那家 SDK 的直接调用散落在我几十个文件里想换想加备份等于要把整个代码库翻一遍。我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为接大模型就是调一个 API。这句话把第三方大模型 API 当成了一个像我自己系统里的函数一样稳定可靠永远在线的东西。可它不是。第三方大模型 API 不是一个稳定的永远在线的服务它是一个你不拥有不能控制会整体不可用的外部依赖。它会限流你会超时会区域性宕机会悄悄变慢会涨价会改接口而这每一件事你都施加不了任何影响只能被动承受。把你的核心 AI 功能百分之百地绑死在单一供应商身上本质上等于把你这个功能的可用性完完整整地交给了一家你毫无影响力的公司。这件事的解法不是去找一家更可靠的供应商没有这样的供应商每一家都会宕机。解法是不再依赖任何单独的一家用一个抽象层把业务代码和具体是哪家供应商解耦用故障转移让主供应商失效时自动切到备用用熔断别对一个已经挂掉的供应商反复死磕用降级在所有供应商都不可用时给用户一个体面的兜底。本文从头梳理为什么把大模型当稳定 API 是错的抽象层怎么搭故障转移怎么做熔断与慢失败怎么处理降级怎么兜底以及成本分级配额隔离灰度切换可观测这些把容灾真正做扎实要避开的坑。

2024 年我在产品里做一个 AI 功能——接了某家头部大模型的 API,让它帮用户做内容生成。接大模型这件事,我压根没多想。第一版我做得很省事:接大模型不就是拿到一个 API key、找一个 SDK、在需要的地方调一下 llm.chat(prompt)、把结果拿回来?哪个功能要用,就在哪个文件里 import 那个 SDK、调一下。本地开发时——真不错:我调一下,模型秒回、结果有模有样,几行代码搞定。我心里很踏实:"接大模型嘛,不就是调一个 API?"可等这个功能真正上线、被真实用户用起来,一串问题冒了出来。第一种最先把我打懵:某天那家供应商区域性宕机,接口大面积返回 503——不是我某一个请求失败,是我打过去的每一个请求都失败。我的整个 AI 功能,在那段时间里,完整地、彻底地挂掉了,而我除了等它自己恢复,什么都做不了。第二种最难受:供应商对我的账号限流,流量一上来,大量请求被 429 顶了回来——AI 功能不是挂了,是"一会儿能用、一会儿不能用",时好时坏。第三种最隐蔽:有一阵子供应商没宕机、也没限流,可它的响应延迟从平时的两三秒飙到了三四十秒。功能"还在",可慢到没有任何用户愿意等——"还在但不可用",这种状态比直接挂掉还难处理,因为我的监控只盯着"有没有报错",而它压根没报错。第四种最说不清:出了这些事,我想加一个备用供应商顶上,翻开代码才发现——对那家 SDK 的直接调用,散落在我几十个文件里,想换、想加备份,等于要把整个代码库翻一遍。我盯着这一连串问题想了很久才彻底想明白,第一版错在一个根本的认知上:我以为"接大模型,就是调一个 API"。这句话把第三方大模型 API,当成了一个像我自己系统里的函数一样稳定、可靠、永远在线的东西。可它不是第三方大模型 API,不是一个稳定的、永远在线的服务——它是一个你不拥有、不能控制、会整体不可用的外部依赖。它会限流你、会超时、会区域性宕机、会悄悄变慢、会涨价、会改接口,而这每一件事,你都施加不了任何影响,只能被动承受。把你的核心 AI 功能,百分之百地绑死在单一供应商身上,本质上等于:把你这个功能的可用性,完完整整地交给了一家你毫无影响力的公司——它好,你就好;它一旦出事,你就跟着一起出事,而且毫无还手之力。这件事的解法,不是"去找一家更可靠的供应商"——没有这样的供应商,每一家都会宕机。解法是:不再依赖任何单独的一家。你要用一个抽象层,把业务代码和具体是哪家供应商解耦;要用故障转移,让主供应商失效时自动切到备用;要用熔断,别对一个已经挂掉的供应商反复死磕;还要用降级,在所有供应商都不可用时,给用户一个体面的兜底。真正做好大模型接入,核心不是"调一个 API",而是把每一家供应商都看成会整体失效的外部依赖,用抽象层解耦、用故障转移自动切换、用熔断别死磕挂掉的供应商、用降级在全挂时兜住用户。这篇文章就把大模型的多供应商容灾梳理一遍:为什么"把大模型当稳定 API"是错的、抽象层怎么搭、故障转移怎么做、熔断与"慢失败"怎么处理、降级怎么兜底,以及成本分级、配额隔离、灰度切换、可观测这些把容灾真正做扎实要避开的坑。

问题背景

先把那串问题的现象和我的误判讲清楚,后面所有的设计都是冲着纠正这个误判去的。

现象:一个"把大模型当成稳定 API"、绑死单一供应商的 AI 功能,上线后冒出一串问题:供应商区域性宕机,整个 AI 功能彻底挂掉、束手无策;供应商限流,请求被 429 顶回、功能时好时坏;供应商没报错却变慢,功能"还在但不可用"、监控还发现不了;想加备用供应商,却发现对 SDK 的直接调用散落在几十个文件里、根本换不动

我当时的错误认知:"接大模型,就是调一个 API。"

真相:这个认知错在它把"调第三方 API"和"调自己系统里的函数"混为一谈了。"调一个 API"——这个说法,听起来就和调用自己代码里的一个函数一样:你调它、它返回、你拿到结果,稳定、可控、确定。可一个第三方大模型 API,根本不是你系统里的函数:它是另一家公司的服务,跑在你够不着的机器上,由你管不到的团队运维。这就意味着,它有一组你自己的函数永远不会有的失效模式:它会因为对方的事故而整体宕机,会因为对方的容量策略而限流你,会因为对方的负载而突然变慢,会因为对方的商业决策而涨价或下线某个模型。一旦你看清这一点,那串问题就都有了解释:它们没有一个是"你的代码写错了"——你的 chat 调用完全正确;它们全都是"你依赖的那个外部东西出事了,而你的代码对此毫无防备"。问题的根子清楚了:这不是"要不要加个 try-except"的小修补,而是要换一个根本视角——你接入的不是一个稳定服务,而是一个会整体失效的外部依赖;你要做的,是为"这个依赖整体不可用"这件迟早会发生的事,提前准备好一整套不依赖它的退路。

要把大模型的多供应商容灾做对,需要几块认知:

  • 为什么"把大模型当稳定 API"是错的——它是会整体失效的外部依赖,不是你的函数;
  • 供应商抽象层——用统一接口把业务代码和"具体哪家供应商"解耦;
  • 故障转移——主供应商失败时,沿一条链自动切到备用供应商;
  • 熔断与慢失败——别对挂掉的供应商死磕,把"变慢"也当成一种失败;
  • 降级——所有供应商都不可用时,退回缓存或友好提示,不把异常抛给用户;
  • 成本分级、配额隔离、灰度切换、可观测这些工程坑怎么处理。

一、为什么"把大模型当成一个稳定的 API"是错的

先把这件最根本的事钉死:"把大模型当成一个稳定的 API",这个想法错在它对"依赖"这个词的理解。在它的世界观里,只有一种依赖——就像依赖自己代码里的一个函数:你写了它、它就在那儿、你调它它就执行,它不会"今天不在"。可第三方大模型 API 是另一种依赖:它是一个外部依赖,它的"在不在、快不快、让不让你用",完全由别人决定。这两种依赖,在本地开发时看起来一模一样——你调 chat,它返回结果,和调一个本地函数没有任何区别,所以你很自然地就把它当成了后者。可它们的失效模式天差地别:本地函数的失效,是"你的逻辑有 bug",改代码就能修;外部依赖的失效,是"对方出事了",你改多少代码都修不了——你只能选择"还依不依赖它"。绑死单一供应商之所以危险,就是因为它在"只依赖这一家"这个选择上,没有给自己留任何余地:这一家好,一切都好;这一家一旦整体出事,你的功能就跟着整体出事,而你手里没有任何别的牌可打。

下面这段代码,就是我那个"把外部依赖当成本地函数"的第一版:

# 反面教材:业务代码里到处直接调某一家供应商的 SDK
from acme_llm import AcmeClient

client = AcmeClient(api_key=API_KEY)

def generate_summary(text):
    resp = client.chat(f"请总结这段内容:{text}")   # 破绽 1:供应商一宕机,这里整片失败
    return resp.content

def generate_reply(question):
    resp = client.chat(f"请回答用户:{question}")    # 破绽 4:同样的直接调用,散落在几十处
    return resp.content
# 破绽 2:被限流 429,这里没有任何应对
# 破绽 3:供应商变慢,这里只会一直干等

这段代码在本地开发时表现不错,因为本地我调的那家供应商一直好好的——没宕机、没限流、也不慢,所以那四个破绽,在本地一个都不会发作。它的问题不在某一行语法上——拿 client、调 chat、取 content,语法都对——而在它对"这个 client 背后是个外部依赖"这件事的彻底无知:client.chat(...) 这一行,它当成了一次稳赚不赔的本地调用,从没想过这一行会因为对方宕机而整片失败、会因为对方限流而被顶回来、会因为对方变慢而干等到天荒地老;而 from acme_llm import AcmeClient 这种直接 import,散落在几十个文件里,又让"换一家"这件事的成本高到做不动。这四个破绽对应的,正是开头那四类问题。问题的根子清楚了:做对这件事,第一步不是给 chat 调用加 try-except,而是先在脑子里把"它是个外部依赖、它会整体失效"这件事认下来,然后为"这一家彻底不可用"准备好一整套不靠它的退路。下面五节,就是这套退路:抽象层、故障转移、熔断、降级,以及它们背后的工程考量。

二、供应商抽象层:把业务代码和具体供应商解耦

第一步,也是后面一切的地基:补的是破绽 4:对某一家 SDK 的直接调用散落在几十处、根本换不动。要能"换供应商""加备用供应商",前提是业务代码不能知道自己用的是哪一家。做法是:定义一个统一的供应商接口,业务代码只认这个接口;每接一家具体供应商,就写一个适配器去实现它:

import time
from abc import ABC, abstractmethod

class LLMResult:
    """统一的返回结构:不管底层是哪家供应商,业务拿到的都是这个。"""
    def __init__(self, text, provider, latency_ms):
        self.text = text
        self.provider = provider
        self.latency_ms = latency_ms

class LLMProvider(ABC):
    """供应商抽象:每接一家新供应商,就实现一个这样的适配器。"""
    name = "base"

    @abstractmethod
    def chat(self, prompt, timeout):
        """把 prompt 发给这家供应商,失败就抛异常 —— 由上层决定怎么处理。"""
        ...

有了这个接口,每一家具体供应商就是一个适配器——它的职责,是把那家供应商五花八门的 SDK,消化成统一的 chat 签名:

class AcmeProvider(LLMProvider):
    """A 家供应商的适配器:把它的 SDK 包成统一接口。"""
    name = "acme"

    def __init__(self, client):
        self._client = client

    def chat(self, prompt, timeout):
        start = time.monotonic()
        resp = self._client.chat(prompt, timeout=timeout)
        cost = (time.monotonic() - start) * 1000
        return LLMResult(resp.content, self.name, cost)


class BoltProvider(LLMProvider):
    """B 家供应商的适配器:它的接口和 A 家完全不同,差异全消化在这里面。"""
    name = "bolt"

    def __init__(self, client):
        self._client = client

    def chat(self, prompt, timeout):
        start = time.monotonic()
        # B 家的入参名、返回字段都和 A 家不一样,但对外暴露的签名完全一致
        out = self._client.complete(text=prompt, deadline_s=timeout)
        cost = (time.monotonic() - start) * 1000
        return LLMResult(out["message"], self.name, cost)

这里的认知要点是:供应商抽象层的价值,不在"代码更整洁"这种泛泛的好处上,而在它把一个本来"做不到"的事,变成了"做得到"。在第一版里,"切换供应商"这件事之所以做不动,不是因为它有多难,而是因为它要改的地方太多、太散——几十个文件里每一处 import、每一处 chat 调用都要动,这种改动量本身就是一道劝退墙。抽象层做的事,是把"业务代码和具体供应商之间的接触面",从"几十处"收敛成"一处"——业务代码只接触 LLMProvider 这个接口,而"接口背后到底是哪家供应商""怎么从一家切到另一家",全部被关进了适配器和它的上层调度里。一旦接触面收敛成一处,故障转移、熔断、降级这些后面要做的事才有了下手的地方:它们全都可以做在"接口和适配器之间"那一层,而业务代码一行都不用改。所以这一节虽然没解决任何一个具体的可用性问题,它却是后面三节的地基——没有这个把接触面收敛到一处的抽象层,后面的故障转移根本无处安放。地基打好了,接下来就能在这一层上,装上真正的容灾机制——从故障转移开始。

三、故障转移:主供应商失败,自动切到备用

第二步,补的是破绽 1:供应商一宕机,整个功能整片失败。有了抽象层,"多备几家供应商"就成了可能——你可以同时接入 A、B、C 三家,把它们排成一条故障转移链。一次调用进来,先试链上第一家,它失败就试第二家,直到有一家成功:

log = get_logger("llm")

class AllProvidersDown(Exception):
    """所有供应商都失败了 —— 这个异常专门留给降级逻辑去接。"""

def call_with_failover(providers, prompt, timeout=15):
    """故障转移:按顺序试每一家,一家失败就换下一家,直到有一家成功。"""
    last_err = None
    for provider in providers:
        try:
            result = provider.chat(prompt, timeout=timeout)
            log.info("LLM 调用成功 provider=%s 延迟=%.0fms",
                     result.provider, result.latency_ms)
            return result
        except Exception as e:
            last_err = e
            log.warning("供应商 %s 失败,切换下一家:%s", provider.name, e)
            continue
    # 整条链都试完了还没成功,抛出去,交给降级逻辑兜底
    raise AllProvidersDown(last_err)

下面这张图,把一次调用在故障转移链上是怎么走的画出来:

这里的认知要点是:故障转移能成立,靠的是一个前提——不同供应商的故障,大概率是相互独立的。A 家因为自己机房的事故宕机时,B 家、C 家八成还好好的,因为它们是三家不同的公司、不同的基础设施。正是这个"独立性",让"多备几家"这件事真的有意义:如果三家会同时挂,那备再多也没用;正因为它们通常不会同时挂,所以只要 A 挂了能立刻切到 B,你这个功能的整体可用性,就被几家供应商各自的可用性"撑"了起来——只要还有一家活着,你就活着。但故障转移有一个必须想清楚的前提:被转移的那个调用,得是可以安全重试的。对"内容生成"这类无副作用的纯读调用,换一家重做一遍,没有任何问题。可如果某个 LLM 调用带着副作用——比如它会触发一次扣费、会写一条数据——那你在 A 家失败后切到 B 家重试,就有可能让那个副作用发生两次。所以故障转移和"幂等"是一对:要么你转移的调用本身无副作用,要么你得让那个副作用幂等,否则故障转移就会从"容灾"变成"把一次操作做成两次"。故障转移让"一家挂了能切下一家",可如果那家挂掉的供应商每次都还要先试一下、等它超时再切,代价就太大了——这要靠下一步。

四、熔断与慢失败:别对挂掉或变慢的供应商死磕

第三步,补的是破绽 2 和破绽 3。上一节的故障转移有个隐藏的浪费:供应商 A 已经整体宕机了,可每一次调用进来,链条还是老老实实先去试一遍 A、等它超时失败、再切到 B——这一下白白搭进去的超时等待,每个请求都要付一次。解法是给每家供应商配一个熔断器:它连续失败到一定次数,就在一段冷却时间内,被直接跳过、连试都不试:

class ProviderBreaker:
    """供应商级熔断:一家连续失败到阈值,就在冷却期内直接跳过它。"""
    def __init__(self, fail_threshold=5, cooldown=60):
        self.fail_threshold = fail_threshold
        self.cooldown = cooldown
        self._fails = 0
        self._open_until = 0.0

    def is_open(self):
        """熔断打开 = 这家供应商暂时被判定为不可用,别再去试它。"""
        return time.monotonic() < self._open_until

    def on_success(self):
        self._fails = 0                       # 一次成功,失败计数清零

    def on_failure(self):
        self._fails += 1
        if self._fails >= self.fail_threshold:
            self._open_until = time.monotonic() + self.cooldown
            self._fails = 0                   # 进入熔断,计数归零,等冷却后重新观察

把熔断器接进故障转移链——已经熔断的供应商,直接跳过,从还健康的里面挑:

def call_with_breaker(entries, prompt, timeout=15):
    """带熔断的故障转移:跳过已熔断的供应商,只在还健康的里面逐个尝试。"""
    last_err = None
    for provider, breaker in entries:
        if breaker.is_open():
            log.info("供应商 %s 处于熔断中,跳过", provider.name)
            continue                          # 已知它挂着,连试都不试,直接下一家
        try:
            result = provider.chat(prompt, timeout=timeout)
            breaker.on_success()
            return result
        except Exception as e:
            last_err = e
            breaker.on_failure()              # 记一次失败,攒够阈值就熔断这家
            log.warning("供应商 %s 失败:%s", provider.name, e)
    raise AllProvidersDown(last_err)

还有破绽 3——供应商没报错,但慢得没法用。这种情况,chat正常返回,熔断器根本不会记它一次失败。解法是:把"太慢"也当成一种失败——响应虽然回来了,但延迟超过阈值,就主动把它当失败抛出去,让它一样触发熔断和转移:

SLOW_THRESHOLD_MS = 12000

class ProviderTooSlow(Exception):
    """供应商没报错,但慢到不可用 —— 视同一次失败。"""

def chat_or_slow_fail(provider, prompt, timeout):
    """把"慢"也当成失败:响应虽回来了,但慢到没法用,照样触发转移。"""
    result = provider.chat(prompt, timeout=timeout)
    if result.latency_ms > SLOW_THRESHOLD_MS:
        # 没报错,但慢得没法用 —— 主动抛异常,让它和真失败一样被处理
        raise ProviderTooSlow(provider.name, result.latency_ms)
    return result

这里的认知要点是:这一节其实在纠正两个认知偏差。第一个偏差是"失败了就重试,总没错"。不对——对一个已经整体宕机的供应商,"再试一次"不是容错,是浪费:它这一次会失败,下一次、下下一次也都会失败,你每试一次,就让一个本可以快速失败的请求,多背一次超时等待。熔断的本质,是把"这家供应商现在是死是活"这个信息记下来、复用起来:既然刚刚连着五次都失败了,那就别再假装"这次可能行",直接跳过它,等冷却期过了再重新观察。熔断省下的,是那些注定失败的尝试所白白消耗的时间。第二个偏差更隐蔽:"没抛异常,就等于成功"。也不对——一个响应慢到三四十秒才回来的调用,它在技术上"成功"了,可在用户眼里它和失败没区别,没人会等。如果你只把"抛异常"算作失败,这种"慢性失败"就会永远绕过你所有的容错机制——熔断不会触发、转移不会发生,用户就那么干等着。所以你必须主动地、显式地给"可用"下一个定义:不只是"没报错",而是"没报错,而且快到能用"。把"太慢"显式地转换成一次失败,你的熔断和转移才能真正覆盖到这种最难缠的故障。转移和熔断,处理的都是"还有别的供应商能顶上"的情况;可万一所有供应商都挂了呢——这要靠最后一步。

五、降级:所有供应商都不可用时给用户兜底

第四步,处理那个最坏的情况:故障转移链上每一家供应商都失败了(call_with_breaker 抛出了 AllProvidersDown)。这时候绝不能让这个异常原样冒到用户面前——变成一个 500 错误页、或者一个红色的报错弹窗。要做降级:给用户一个虽然不完美、但体面的兜底:

def call_llm_safe(entries, prompt, cache, timeout=15):
    """全链路兜底:供应商全挂时,退回缓存或一句诚实的降级提示,绝不把异常抛给用户。"""
    try:
        result = call_with_breaker(entries, prompt, timeout=timeout)
        cache.set(prompt, result.text)        # 成功的结果顺手缓存,留作将来的兜底
        return result.text
    except AllProvidersDown:
        cached = cache.get(prompt)
        if cached is not None:
            log.warning("供应商全部不可用,返回缓存里的旧结果")
            return cached                     # 兜底一:有旧结果,先用旧的顶上
        log.error("供应商全部不可用,且无缓存,返回降级提示")
        return "AI 服务暂时繁忙,请稍后重试。"   # 兜底二:连旧的都没有,给一句诚实的提示

这里的认知要点是:降级要做对,关键是想清楚"全挂的时候,到底该给用户什么"。一个很自然但是错的想法是:全挂了,那就把错误如实抛给用户呗——让他看到"服务异常"。这错在它把"系统内部的失败"直接等同于"用户该承受的失败"。用户不关心你接了几家供应商、它们是不是都挂了,用户只关心"我点的这个功能,现在到底能给我什么"。降级要做的,就是在"完美结果"和"赤裸裸的报错"之间,塞进几个层次的退路。这些退路是有优先级的:最好的退路,是返回一个稍旧的、但仍然有用的缓存结果——用户多半根本察觉不到它是旧的;退而求其次,如果连缓存都没有,那就给一句诚实、友好、明确的提示——"AI 服务暂时繁忙,请稍后重试",它至少告诉了用户"这不是你的错,等会儿再来",而不是一个冷冰冰的 500。这里还藏着一个良性循环:你在每次成功时顺手写缓存,其实是在为"将来某次全挂"悄悄地攒退路——平时跑得越多,缓存里能兜底的内容就越丰富。降级的终极目标是:让"所有供应商都挂了"这件本该是灾难的事,对用户而言,最多只是一次"结果不够新"或"稍后再试"的轻微不便。四步退路齐了,最后是几个把多供应商容灾真正用到生产里才会撞见的工程坑。

六、工程坑:成本分级、配额隔离、灰度切换、可观测

四步退路之外,还有几个工程坑,不处理就会让你的容灾在边角上出问题坑 1:转移链的顺序,要体现成本与质量的取舍。故障转移链不是随便排的——它既是"挂了往哪切"的顺序,也是"平时优先用谁"的顺序。通常把质量最好的放第一位当主力,把便宜或一般的放后面当备胎。这个顺序该集中成一份配置,而不是散在代码里:

# 供应商转移链:顺序既是"故障转移的次序",也体现质量与成本的取舍
PROVIDER_CHAIN = [
    {"name": "acme", "tier": "primary", "note": "质量最好,首选主力"},
    {"name": "bolt", "tier": "backup",  "note": "质量够用,主供应商挂了顶上"},
    {"name": "echo", "tier": "last",    "note": "便宜但一般,前两家全挂才用"},
]

def build_entries(registry):
    """按链上的顺序,把每家供应商和它各自的熔断器配成一条故障转移链。"""
    entries = []
    for cfg in PROVIDER_CHAIN:
        provider = registry[cfg["name"]]
        entries.append((provider, ProviderBreaker()))   # 每家一个独立的熔断器
    return entries

坑 2:每家供应商一个独立的熔断器。注意上面 build_entries 里,每家供应商配的是各自独立ProviderBreaker()绝不能让几家供应商共用一个熔断器——那样 A 家的失败会污染 B 家的计数,A 一挂、B 也跟着被误判熔断,容灾直接失效。坑 3:配额与限流要按供应商隔离。每家供应商的限流额度是各算各的。你做客户端限流时,要给每家维护一套独立的配额计数,而不是一个全局的——否则 A 家的高频调用,会把 B 家本来还充裕的额度也"算"掉。坑 4:换主供应商要灰度,别一刀切。想把主力从 A 换成 B,别直接改配置全量切过去——B 家在你真实流量下的质量、延迟、稳定性,你其实没底。正确的做法是先切一小部分流量给 B、观察指标,再逐步放大(这正是灰度发布那一套)。坑 5:不同供应商的输出有差异,要做一致性兜底。同一个 prompt,A 家和 B 家给的回答风格、格式可能不一样。如果你的下游依赖某种固定格式(比如要求返回 JSON),那么每家适配器的输出都要做一遍格式校验和规整,别让"换了供应商"导致下游解析崩掉。坑 6:容灾过程必须可观测。故障转移最危险的地方,恰恰是它太安静——A 家挂了、自动切到了 B 家,功能表面上一切正常,你可能好几天都不知道 A 已经挂了。必须给每家供应商单独记账:成功率、失败数、平均延迟、被熔断了几次,分供应商统计:

def record_provider_call(stats, provider_name, ok, latency_ms):
    """给每家供应商单独记账:成败、延迟分开统计,才看得清谁在拖后腿。"""
    s = stats.setdefault(provider_name, {"ok": 0, "fail": 0, "latency_sum": 0.0})
    if ok:
        s["ok"] += 1
        s["latency_sum"] += latency_ms
    else:
        s["fail"] += 1

def provider_health_report(stats):
    """汇总每家的成功率与平均延迟 —— 故障转移有没有在悄悄兜底,一看便知。"""
    for name, s in stats.items():
        total = s["ok"] + s["fail"]
        ok_rate = s["ok"] / total if total else 0.0
        avg_lat = s["latency_sum"] / s["ok"] if s["ok"] else 0.0
        log.info("供应商 %s 成功率=%.1f%% 平均延迟=%.0fms 总调用=%d",
                 name, ok_rate * 100, avg_lat, total)

坑 7:别忘了把 prompt 也做成可移植的。有些 prompt 写法是针对某一家模型调优的,换一家可能效果就差了。重要的 prompt,要在每家供应商上都验证一遍效果,别假设"在 A 家好用的 prompt,在 B 家一样好用"。

关键概念速查

概念 / 手段 说明
把大模型当稳定 API 的错 它是会整体失效的外部依赖,不是你系统里的函数
外部依赖的失效模式 会宕机、限流、变慢、涨价、改接口,你都无法控制
供应商抽象层 统一 LLMProvider 接口,业务代码不知底层是哪家
适配器 每家供应商一个适配器,把 SDK 差异消化在内部
故障转移链 主供应商失败,沿链自动切到下一家备用供应商
转移依赖独立性 不同供应商通常不同时故障,这是多备份的意义
供应商级熔断 一家连续失败就在冷却期内直接跳过,不再死磕
慢也是一种失败 响应虽回但延迟过高,主动当失败,触发熔断转移
降级兜底 全部供应商不可用时,退回缓存或友好提示
按供应商分别可观测 成败延迟分供应商记账,容灾才不会悄无声息

避坑清单

  1. 别把第三方大模型 API 当稳定函数,它是会整体失效的外部依赖。
  2. 用统一的 LLMProvider 抽象层,把业务代码和具体供应商解耦。
  3. 每家供应商写一个适配器,把各家 SDK 的差异消化在适配器内部。
  4. 多接几家供应商排成故障转移链,一家失败自动切到下一家。
  5. 带副作用的 LLM 调用做故障转移前,必须先保证它是幂等的。
  6. 给每家供应商配独立熔断器,挂掉的供应商在冷却期内直接跳过。
  7. 把"响应太慢"也显式当成一次失败,让它一样触发熔断和转移。
  8. 供应商全挂时要降级,退回缓存或友好提示,绝不把异常抛给用户。
  9. 转移链顺序集中配置,体现质量与成本取舍,换主供应商要灰度。
  10. 给每家供应商单独记账,别让故障转移悄无声息地掩盖问题。

总结

回头看那串"供应商宕机整个功能全挂、被限流时好时坏、变慢却无人察觉、想换供应商却换不动"的问题,以及我后来在容灾上接连踩的坑,最该记住的不是某一段转移代码的写法,而是我动手前那个想当然的判断——"接大模型,就是调一个 API"。这句话错在它把一个"外部依赖",当成了一个"本地函数"。我以为调用大模型,和调用我自己代码里的一个函数,是一回事——它永远在那儿,我调它它就执行。可我忽略了一件最要紧的事:第三方大模型 API,跑在我够不着的机器上,由我管不到的团队运维。它的"在不在、快不快、让不让我用",完全是别人说了算。它有一组我自己的函数永远不会有的失效模式:整体宕机、限流、突然变慢、涨价、改接口。而面对这每一种失效,我能做的事只有一件——选择"还要不要继续依赖它"。绑死单一供应商,等于在这个选择上放弃了所有余地:把整个功能的可用性,焊死在了一家我毫无影响力的公司身上。

所以做好大模型接入,真正的工程量不在"调一次 API"那一行上。调 API 本身不难。真正的工程量,在于你要承认"任何一家供应商都迟早会整体不可用",然后为这件迟早会发生的事,准备好一整套不依赖它的退路:你不能让业务代码绑死某一家,就用抽象层把它们解耦、让业务只认统一接口;你不能赌一家永不宕机,就多接几家排成故障转移链、一家挂了自动切下一家;你不能对一个已经挂掉的供应商反复死磕,就给每家配熔断器、还把"变慢"也当成失败;你不能让"全挂"直接砸到用户脸上,就用缓存和友好提示做降级兜底。这篇文章的几节,其实就是顺着这套退路展开的:先想清楚"把大模型当稳定 API"为什么错,再讲抽象层怎么搭、故障转移怎么做、熔断与慢失败怎么处理、降级怎么兜底,最后是成本分级、配额隔离、灰度切换、可观测这几个把容灾守扎实的工程细节。

你会发现,这件事和现实里"一家店铺该怎么安排自己的进货渠道"完全相通。一个没有风险意识的老板会怎么做?他找到一家供货商,价格合适、货也好,于是把店里所有的货,全部、长期地,只从这一家进。平日里这看起来再省心不过——一个电话,货就到了。可天有不测:这家供货商某天工厂失火停产了,他的货架第二天就空了一片,他除了干等对方恢复,毫无办法;供货商旺季供不应求、对他限量供应,他的店就三天有货、两天断货;供货商没断货,但发货慢了、十天半个月才到,他的货名义上"在路上",实际上等于没有;等他终于想换一家供货商,才发现自己连别家的门朝哪开都不知道。而一个有风险意识的老板怎么做?他手里始终攥着三四家供货商的联系方式:平时,他从货最好的那家进(这就是转移链上的主供应商);这家一旦出问题,他立刻打给第二家、第三家顶上(这就是故障转移);他心里清楚哪家最近老是掉链子,那家就先不找了、过阵子再说(这就是熔断);真碰上几家一起出事的极端情况,他也先把仓库里的存货摆上架、再在门口诚实地贴张告示,而不是让顾客对着空货架一脸茫然(这就是降级兜底)。同样是开店进货,没有风险意识的老板把全店的命脉系在一根别人手里的绳上,有风险意识的老板让任何一家出事都动摇不了自己的生意——差别不在"找到的供货商好不好",只在老板有没有把"供货商迟早会出事"当成一件必然会发生、必须提前安排退路的事

最后想说,大模型接入做没做对,差距永远不会在"本地开发、自己调几下测试"时暴露——本地你调的那家供应商恰好一直好好的:没宕机、没限流、也不慢,你那行直接 client.chat 每次都干净利落地返回结果,你自然觉得"接大模型,不就是调一个 API"一点问题都没有。它只在真实的、长期运行的、要扛真实流量的生产环境里才显形——因为在足够长的时间里,你依赖的那家供应商,一定会有那么一天出事。那时候它会用最难堪的方式给你结账:做不好,你会因为绑死单一供应商,在它宕机的那几个小时里,眼睁睁看着整个 AI 功能彻底瘫痪而无能为力,会因为没有熔断,让每个请求都白白搭上一次超时等待,会因为没有降级,把一个赤裸裸的报错直接糊到用户脸上;而做了,某一家供应商宕机时,你的流量会在用户毫无察觉之间,平静地切到下一家,某一家变慢时,它会被当成失败、被熔断跳过,哪怕真的全部供应商一起出事,用户得到的也最多是一个稍旧的缓存结果、或一句客气的"稍后再试"。所以别等"一次供应商大宕机把你的 AI 功能打挂一整天"那一刻找上门,在你写下每一行调用大模型的代码时就该想清楚:这次调用绑死单一供应商了吗、主供应商挂了能自动切吗、挂掉的供应商会被熔断跳过吗、全挂了用户能得到一个体面的兜底吗,这一条条退路,我是不是都替它铺好了?这些问题有了答案,你交付的才不只是一个"本地调得通"的 AI 功能,而是一套任凭哪一家供应商出事都照样稳稳运转、让人放心的 AI 系统。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

灰度发布完全指南:从一次"新版本一上线就砸中全部用户"看懂流量切分、渐进放量与自动回滚

2026-5-22 15:03:21

技术教程

金额计算完全指南:从一次"对账差了一分钱、查了三天"看懂浮点数陷阱与 Decimal 实践

2026-5-22 15:18:38

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索