一、先给结论:多模型不等于多故障域

很多所谓“多模型网关”只有下面这段逻辑:

models = ["gpt", "claude", "grok"]

for model in models:
    try:
        return call(model)
    except Exception:
        continue

这段代码在 Demo 中能切换,在生产中却可能造成五类事故:

  1. 同一个错误被重试四五次,用户等了几分钟才收到失败;
  2. 请求参数只适配 GPT,切到 Claude 或 Grok 后被静默忽略;
  3. 流式输出已经发了一半,又切模型生成第二个互相矛盾的后半段;
  4. Agent 已经执行过“退款”“发邮件”等工具,再重放一次导致重复副作用;
  5. 三个模型虽然名字不同,却都经过同一个中转、同一条出口网络或同一个网关进程,单点故障时一起消失。

所以,真正能切换的多模型网关至少要有四层:

业务应用
只认识稳定别名

统一入口
鉴权、预算、请求 ID

策略路由
能力检查、超时、熔断

独立云端故障域
OpenAI、Anthropic、xAI

本地故障域
Ollama 预下载模型

状态与证据
Redis、PostgreSQL、日志指标

本文最终实现的不是“任何请求都能无损切换”,而是更诚实的目标:

  • 基础文本请求可以在 GPT、Claude、Grok 和本地模型间自动降级;
  • JSON、工具调用、长上下文等请求只在能力兼容的模型组中切换;
  • 已开始流式输出或已产生外部副作用的请求不做盲目重放;
  • 所有云端提供商不可用时,本地模型只承担低风险文本任务;
  • 无法安全降级的任务明确返回“排队”或“失败关闭”,而不是生成质量未知的结果。

二、为什么 GPT、Claude、Grok 会“同时不可用”

三个厂商同时发生全球故障并不常见,更常见的是它们在你的系统中共享了某个故障点。

故障层典型问题换模型是否有效
模型部署单个模型限流、下线、容量不足通常有效
提供商账号余额不足、Key 撤销、项目权限错误换到独立提供商有效
聚合平台GPT、Claude、Grok 都经同一个中转无效,仍共享中转
出口网络DNS、代理、NAT、TLS、跨境线路故障云端模型全部可能失败
网关进程单实例崩溃、配置错误、连接池耗尽换模型无效
网关状态多 Worker 各自维护熔断和限流可能反复把请求送往坏节点
业务契约工具、JSON、图片、上下文不兼容“请求成功”也可能语义失败

一个很关键的判断是:

OpenAI、Anthropic、xAI 三个直连账号是三个提供商故障域;三个模型都通过 OpenRouter 或其他聚合平台,则至少共享一个中转故障域。

聚合平台仍有价值,例如统一结算、补充模型和绕开单个直连账号问题,但它不能自动等同于独立容灾。真正的第四层兜底应当位于不同的网络、计算和凭据边界中,例如预下载到本机或内网 GPU 节点的 Ollama/vLLM 模型。

同理,把网关和本地模型都部署在同一台低配服务器上,也只是提供商级降级,不是主机级高可用。主机宕机时两者仍会一起消失。


三、统一 API 只是起点,统一语义才是难点

xAI 的 REST API 官方说明兼容 OpenAI REST API;Ollama 也提供 /v1/chat/completions 等 OpenAI-compatible 接口。xAI Inference APIOllama OpenAI Compatibility

Anthropic 同样提供 OpenAI SDK 兼容层,但官方明确说它主要用于测试和比较,并非多数场景下的长期生产方案;Claude 的 PDF、引用、Thinking、Prompt Caching 等完整能力仍应使用原生 Claude API。其兼容层还会忽略 Function Calling 的 strict,也会重新处理 system/developer 消息。Anthropic OpenAI SDK Compatibility

因此,一个可靠网关不应向业务承诺“所有模型功能完全一致”,而应建立能力分层:

能力档位对外别名允许的功能降级原则
基础文本chat-default文本消息、温度、最大输出可跨四个后端
结构化输出chat-jsonJSON Schema、服务端校验只进入验证通过的模型
工具调用chat-toolsTool Calling、参数 Schema必须再次校验工具参数
长上下文chat-long大上下文、预调用 Token 检查超长时切长上下文组,不盲重试
原生能力native-*Thinking、引用、Web Search、Realtime固定提供商,不承诺透明切换

本文的可运行配置只开放 chat-default,故意把范围限制在最容易验证的文本共同子集。等基础链路通过故障演练后,再分别建设 chat-jsonchat-tools,不能用一个万能别名承接全部请求。

为什么不建议默认 drop_params: true

LiteLLM 可以删除目标模型不支持的参数,但“HTTP 200”不代表语义等价。假设业务要求严格 JSON,而切到某模型后 strict 被静默忽略,网关虽然成功返回,业务却可能拿到无法解析的结果。

生产默认应倾向:

litellm_settings:
  drop_params: false

不兼容参数先失败,再由明确的能力路由处理。只有确认某参数不影响语义时,才针对特定部署配置删除,而不是全局静默吞掉。


四、我们要搭建的网关

技术组件如下:

组件作用
LiteLLM ProxyOpenAI-compatible 入口、模型适配、Fallback、Cooldown
Redis多 Worker 共享限流、缓存和熔断状态
PostgreSQLVirtual Key、预算、用量、审计状态
Ollama云端全部不可用时的本地文本兜底
Nginx / LB生产环境 TLS、连接限制和多网关实例入口

LiteLLM 官方将 Retry 定义为在同一个 model_name 模型组内尝试其他 Deployment,将 Fallback 定义为离开当前模型组、进入下一个模型组。LiteLLM Request Architecture

本文设计为:

chat-default(OpenAI 直连)
    ↓ 失败
chat-claude(Anthropic 直连)
    ↓ 失败
chat-grok(xAI 直连)
    ↓ 失败
chat-local(Ollama 内网模型)

注意这里是“按顺序降级”,不是把不同能力和价格的模型随机负载均衡。每个模型组内部仍可配置同一能力、不同地域的多个 Deployment。


五、完整部署文件

1. 目录结构

multi-model-gateway/
├── .env
├── .env.example
├── config.yaml
├── docker-compose.yml
└── verify_gateway.py

2. .env.example

# 网关凭据:master key 必须以 sk- 开头
LITELLM_MASTER_KEY=sk-replace-with-a-long-random-value
LITELLM_SALT_KEY=replace-with-a-stable-random-value

# 基础设施密码
POSTGRES_PASSWORD=replace-with-a-long-random-value
REDIS_PASSWORD=replace-with-a-long-random-value

# 三家直接申请的 Key;不要把真实值提交到 Git
OPENAI_API_KEY=replace-me
ANTHROPIC_API_KEY=replace-me
XAI_API_KEY=replace-me

# 值中要包含 LiteLLM provider 前缀。
# 请替换成你的账号当前确实可用的模型 ID。
OPENAI_MODEL=openai/<your-openai-model-id>
ANTHROPIC_MODEL=anthropic/<your-claude-model-id>
XAI_MODEL=xai/<your-grok-model-id>

# 本地兜底示例;约 4B 参数仍需按机器内存和速度实测
LOCAL_MODEL=ollama_chat/qwen3.5:4b

生成随机值时可使用:

openssl rand -hex 32

将模板复制为 .env 后填写:

cp .env.example .env
chmod 600 .env

不要直接照抄网络文章里的模型名。可先调用三家的 Model List 接口确认账号权限:

# OpenAI
curl -sS https://api.openai.com/v1/models \
  -H "Authorization: Bearer ${OPENAI_API_KEY}"

# Anthropic
curl -sS https://api.anthropic.com/v1/models \
  -H "x-api-key: ${ANTHROPIC_API_KEY}" \
  -H "anthropic-version: 2023-06-01"

# xAI
curl -sS https://api.x.ai/v1/models \
  -H "Authorization: Bearer ${XAI_API_KEY}"

Anthropic 官方也提供 /v1/models 用于查询当前 Key 可访问的模型。Anthropic Models API

3. config.yaml

model_list:
  - model_name: chat-default
    litellm_params:
      model: os.environ/OPENAI_MODEL
      api_key: os.environ/OPENAI_API_KEY
      timeout: 25
    model_info:
      id: openai-primary

  - model_name: chat-claude
    litellm_params:
      model: os.environ/ANTHROPIC_MODEL
      api_key: os.environ/ANTHROPIC_API_KEY
      timeout: 25
    model_info:
      id: anthropic-primary

  - model_name: chat-grok
    litellm_params:
      model: os.environ/XAI_MODEL
      api_key: os.environ/XAI_API_KEY
      timeout: 25
    model_info:
      id: xai-primary

  - model_name: chat-local
    litellm_params:
      model: os.environ/LOCAL_MODEL
      api_base: http://ollama:11434
      timeout: 90
    model_info:
      id: ollama-emergency

router_settings:
  routing_strategy: simple-shuffle
  num_retries: 0
  cooldown_time: 60
  enable_pre_call_checks: true

  redis_host: os.environ/REDIS_HOST
  redis_port: os.environ/REDIS_PORT
  redis_password: os.environ/REDIS_PASSWORD

  fallbacks:
    - chat-default:
        - chat-claude
        - chat-grok
        - chat-local

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  database_url: os.environ/DATABASE_URL

  # GitOps 模式:避免 UI/数据库与 YAML 同时成为配置真相源
  store_model_in_db: false

  # 健康检查默认关闭,需要显式开启
  background_health_checks: true
  health_check_interval: 60
  enable_health_check_routing: true

  # 429/408 通常是瞬态过载,不因一次后台探测就永久摘除
  health_check_ignore_transient_errors: true

  # 避免将完整提示词写入用量日志
  store_prompts_in_spend_logs: false

litellm_settings:
  drop_params: false
  set_verbose: false
  json_logs: true

为什么把 num_retries 设为 0?

因为当前每个模型组只有一个 Deployment。在单节点上重复相同请求,只会增加延迟和消耗限额;先快速进入下一个独立提供商更合理。将来为 chat-default 配置两个不同地域的 OpenAI/Azure Deployment 后,可以再评估组内重试一次。

LiteLLM 支持按顺序配置跨模型组 Fallback、Cooldown 和后台健康检查。后台健康检查默认关闭;启用 enable_health_check_routing 后,可在用户请求到达前将不健康 Deployment 排除。LiteLLM FallbacksHealth Check Driven Routing

4. docker-compose.yml

services:
  gateway:
    image: ghcr.io/berriai/litellm-database:v1.99.1
    restart: unless-stopped
    command:
      - --config
      - /app/config.yaml
      - --port
      - "4000"
      - --num_workers
      - "2"
    volumes:
      - ./config.yaml:/app/config.yaml:ro
    environment:
      LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
      LITELLM_SALT_KEY: ${LITELLM_SALT_KEY}
      DATABASE_URL: postgresql://litellm:${POSTGRES_PASSWORD}@postgres:5432/litellm
      REDIS_HOST: redis
      REDIS_PORT: "6379"
      REDIS_PASSWORD: ${REDIS_PASSWORD}
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
      XAI_API_KEY: ${XAI_API_KEY}
      OPENAI_MODEL: ${OPENAI_MODEL}
      ANTHROPIC_MODEL: ${ANTHROPIC_MODEL}
      XAI_MODEL: ${XAI_MODEL}
      LOCAL_MODEL: ${LOCAL_MODEL}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "127.0.0.1:4000:4000"
    healthcheck:
      test:
        - CMD
        - python
        - -c
        - >-
          import urllib.request;
          urllib.request.urlopen('http://127.0.0.1:4000/health/liveliness', timeout=3)
      interval: 15s
      timeout: 5s
      retries: 10

  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: litellm
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U litellm -d litellm"]
      interval: 5s
      timeout: 5s
      retries: 20

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    environment:
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    command:
      - sh
      - -c
      - exec redis-server --appendonly yes --requirepass "$$REDIS_PASSWORD"
    volumes:
      - redis_data:/data
    healthcheck:
      test:
        - CMD-SHELL
        - redis-cli -a "$$REDIS_PASSWORD" ping | grep PONG
      interval: 5s
      timeout: 5s
      retries: 20

  ollama:
    image: ollama/ollama:0.33.3
    restart: unless-stopped
    volumes:
      - ollama_data:/root/.ollama
    healthcheck:
      test: ["CMD", "ollama", "list"]
      interval: 15s
      timeout: 10s
      retries: 10

volumes:
  postgres_data:
  redis_data:
  ollama_data:

这里固定了 LiteLLM v1.99.1 和 Ollama 0.33.3,避免滚动 latest 在重启后改变行为。两个版本均来自当时的官方稳定发布记录。LiteLLM ReleasesOllama Releases

网关只绑定 127.0.0.1:4000,外网不能直接访问。若要给其他机器使用,应由 Nginx、Caddy 或负载均衡器提供 TLS 和访问控制,而不是把 4000 端口裸露到公网。

5. 启动

先检查 Compose 展开结果,确认没有 <your-...> 占位符:

docker compose config

启动基础组件:

docker compose up -d postgres redis ollama

预先下载本地兜底模型:

docker compose exec ollama ollama pull qwen3.5:4b
docker compose exec ollama ollama list

最后启动网关:

docker compose up -d gateway
docker compose ps
docker compose logs --tail=200 gateway

Ollama 官方提供 OpenAI-compatible Chat Completions、Streaming 和 JSON 等接口;示例使用的 qwen3.5:4b 也存在官方模型条目。Ollama OpenAI CompatibilityQwen3.5 4B


六、第一次调用:业务永远只请求稳定别名

业务端不应写真实模型名,只请求:

chat-default

使用 Curl:

curl -i --max-time 120 \
  http://127.0.0.1:4000/v1/chat/completions \
  -H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "chat-default",
    "messages": [
      {"role": "system", "content": "Reply with one short sentence."},
      {"role": "user", "content": "What is a circuit breaker?"}
    ],
    "temperature": 0,
    "stream": false
  }'

使用 OpenAI Python SDK:

import os

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["LITELLM_MASTER_KEY"],
    base_url="http://127.0.0.1:4000/v1",
    timeout=120.0,
    max_retries=0,
)

response = client.chat.completions.create(
    model="chat-default",
    messages=[
        {"role": "user", "content": "Reply with exactly: gateway-ok"}
    ],
    temperature=0,
)

print(response.choices[0].message.content)

客户端 max_retries=0 是为了避免“SDK 重试 × 网关重试 × 提供商 SDK 重试”层层放大。整个调用链只能有一个主要重试所有者;本文由网关负责跨提供商切换,业务端只设置总超时。

OpenAI 官方也提醒,失败请求仍会消耗每分钟限额,无上限地连续重发不能解决 429;正确做法是有限重试、指数退避并加入随机抖动。OpenAI Rate Limits


七、不要只看回答内容:必须记录实际由谁服务

config.yaml 中,我们为四个 Deployment 设置了稳定 ID:

openai-primary
anthropic-primary
xai-primary
ollama-emergency

LiteLLM 可通过响应头 x-litellm-model-id 标识实际命中的 Deployment。官方也建议用这个响应头验证 Fallback。LiteLLM Fallback Verification

创建 verify_gateway.py

import json
import os
import sys
import urllib.error
import urllib.request

url = os.getenv(
    "GATEWAY_URL",
    "http://127.0.0.1:4000/v1/chat/completions",
)
key = os.environ["LITELLM_MASTER_KEY"]
expected = os.getenv("EXPECTED_MODEL_ID")

payload = json.dumps(
    {
        "model": "chat-default",
        "messages": [
            {"role": "user", "content": "Reply with exactly: gateway-ok"}
        ],
        "temperature": 0,
        "stream": False,
    }
).encode("utf-8")

request = urllib.request.Request(
    url,
    data=payload,
    method="POST",
    headers={
        "Authorization": f"Bearer {key}",
        "Content-Type": "application/json",
        "X-Correlation-ID": "gateway-drill-001",
    },
)

try:
    with urllib.request.urlopen(request, timeout=120) as response:
        body = json.loads(response.read().decode("utf-8"))
        actual = response.headers.get("x-litellm-model-id")
        text = body["choices"][0]["message"]["content"]
        print(f"status={response.status}")
        print(f"served_model_id={actual}")
        print(f"content={text}")
except urllib.error.HTTPError as exc:
    print(f"http_error={exc.code}")
    print(exc.read().decode("utf-8", errors="replace"))
    sys.exit(1)

if expected and actual != expected:
    raise SystemExit(f"expected {expected}, got {actual}")

正常情况下验证 OpenAI 主路由:

export LITELLM_MASTER_KEY='你的网关密钥'
EXPECTED_MODEL_ID=openai-primary python3 verify_gateway.py

同时还应保存:

  • 业务侧 request_id
  • 网关实际 Deployment ID;
  • 提供商返回的 Request ID;
  • 首 Token 延迟、总耗时、输入/输出 Token;
  • Fallback 次数和原始模型组;
  • 失败类型,而不只是 HTTP 状态码。

OpenAI 官方建议记录响应头中的 x-request-id 用于排障;其他提供商也有自己的请求标识,网关需要做统一字段映射,但不能覆盖原始值。OpenAI API Debugging


八、故障演练:真的把三家依次断掉

仅看配置文件无法证明 Fallback 可用。LiteLLM Proxy 已不再接受通过请求参数伪造 Fallback 的旧测试方式,官方建议在非生产环境中触发真实的可重试或提供商错误。LiteLLM Fallback Test说明

为避免改坏正式 .env,先创建演练副本:

cp .env .env.chaos
chmod 600 .env.chaos

场景 A:OpenAI 不可用,切 Claude

.env.chaos 中的 OpenAI Key 改为测试用无效值:

OPENAI_API_KEY=invalid-for-chaos-test

只重建网关,不动数据库和本地模型:

docker compose --env-file .env.chaos up -d --force-recreate gateway

验证:

EXPECTED_MODEL_ID=anthropic-primary python3 verify_gateway.py

场景 B:OpenAI、Claude 都不可用,切 Grok

继续修改:

ANTHROPIC_API_KEY=invalid-for-chaos-test
docker compose --env-file .env.chaos up -d --force-recreate gateway
EXPECTED_MODEL_ID=xai-primary python3 verify_gateway.py

场景 C:三个云端模型都不可用,切本地 Ollama

继续修改:

XAI_API_KEY=invalid-for-chaos-test
docker compose --env-file .env.chaos up -d --force-recreate gateway
EXPECTED_MODEL_ID=ollama-emergency python3 verify_gateway.py

此时建议同步断开测试机外网再测一次。如果本地模型已经下载,基础文本请求仍应完成;否则你验证的只是“三个 Key 坏了”,没有验证出口网络整体故障。

场景 D:云端和本地同时不可用

docker compose stop ollama
time python3 verify_gateway.py

正确结果不是永远等待,而是:

  • 在业务规定的总 Deadline 内失败;
  • 返回可分类错误;
  • 只读、可延迟任务可进入队列;
  • 有副作用或高风险任务失败关闭;
  • 告警中包含原始 Request ID 与所有尝试过的路由。

恢复正式环境:

docker compose --env-file .env up -d --force-recreate gateway ollama
rm .env.chaos

删除的是演练用的凭据副本;正式 .env 和数据卷不会被删除。

验收矩阵

演练预期路由必须断言
全部正常openai-primary回答成功,Fallback=0
OpenAI 失败anthropic-primary在 Deadline 内成功,告警记录主路失败
OpenAI+Claude 失败xai-primary请求契约仍满足
三家云端失败ollama-emergency明确标记降级模式
四路全部失败有界失败,不无限重试
Redis 断开按策略失败或单 Worker 降级不允许各 Worker 熔断状态分裂
Gateway 单实例退出第二实例接管客户端地址不变
返回非法 JSONSchema 校验失败,不能当成功

九、哪些错误应该切换,哪些不应该

不能写一个 except Exception 处理所有问题。

错误类型是否自动切换原因
网络连接失败、DNS、TLS、超时是,受总 Deadline 限制更换故障域可能恢复
429 临时限流是,有限退避或换提供商重放次数必须有上限
5xx 提供商错误典型瞬态或服务故障
401/403可切换但必须立即告警独立 Key 可保业务,配置问题不会自行恢复
余额耗尽可切换但不要重试原路重试只会继续失败
400 参数错误通常不切换多半是调用契约错误,应修请求
上下文超限只切 chat-long不是普通服务故障
内容安全拒绝默认不跨策略绕过避免把 Fallback 变成安全策略逃逸
已输出首个 Token 后断流不透明切换第二模型无法无缝续写同一序列
工具已执行后模型超时不自动重放工具可能产生重复副作用

OpenAI 的 429 可能表示临时请求/Token 限额,也可能表示余额耗尽,处理方式完全不同。应读取错误体的 codetypeRetry-After,而不是看见 429 就睡眠重试。OpenAI Error Codes

给每次请求一个总时间预算

假设四条路各自允许 25 秒并串行尝试,最坏耗时可能超过 100 秒,还没有计算 DNS 和客户端重试。更合理的做法是:

业务总 Deadline:45 秒
OpenAI 尝试:最多 15 秒
Claude 尝试:最多 12 秒
Grok 尝试:最多 10 秒
本地模型:使用剩余预算;不足则排队或失败

LiteLLM 负责路由,但业务仍需设置端到端 Deadline。网关配置里的单路 Timeout 不是整个业务链路的 SLA。


十、流式输出为什么不能中途“无感换模型”

非流式请求在向用户返回内容前失败,网关可以换路;流式请求一旦已经发送:

data: 第一个模型生成的前半句

HTTP 状态和响应头通常已经提交。此时切到第二个模型会遇到三个问题:

  1. 第二个模型不知道第一个模型内部准备如何续写;
  2. 将已输出文本追加到 Prompt 会改变上下文和 Token 计费;
  3. 两段输出可能重复、矛盾或破坏 JSON/Tool Call 结构。

因此,流式降级应遵循:

  • 首 Token 前失败: 可以透明 Fallback;
  • 首 Token 后失败: 终止当前流,返回明确错误事件;
  • 需要继续: 由业务创建新请求,附带已确认的对话内容,并标记为“续答”而非原请求重试;
  • 结构化输出和工具调用: 优先使用非流式,完成校验后再交给下游。

这也是为什么“Curl 能切换成功”只是第一层验收,真正的聊天、Coding Agent 和 WebSocket 场景还要单独测试。


十一、Agent 工具调用:必须有幂等账本

普通问答重放一次,最多多花钱;带工具的 Agent 重放一次,可能重复退款、重复下单或重复发送通知。

模型生成的 tool_call_id 不能直接当成跨提供商幂等键,因为切换模型后 ID 和参数表达可能改变。业务需要自己生成稳定的逻辑调用 ID,并将副作用写入数据库。

PostgreSQL 示例:

CREATE TABLE agent_tool_execution (
    task_id             uuid        NOT NULL,
    logical_call_id     varchar(128) NOT NULL,
    tool_name           varchar(128) NOT NULL,
    args_sha256         char(64)     NOT NULL,
    status              varchar(20)  NOT NULL,
    provider_model_id   varchar(128),
    provider_request_id varchar(256),
    result_json         jsonb,
    error_json          jsonb,
    created_at          timestamptz  NOT NULL DEFAULT now(),
    updated_at          timestamptz  NOT NULL DEFAULT now(),
    PRIMARY KEY (task_id, logical_call_id)
);

执行工具前先占位:

INSERT INTO agent_tool_execution (
    task_id,
    logical_call_id,
    tool_name,
    args_sha256,
    status
)
VALUES ($1, $2, $3, $4, 'RUNNING')
ON CONFLICT (task_id, logical_call_id) DO NOTHING;

只有插入成功的执行者可以调用外部工具。发生冲突时:

  • SUCCEEDED:直接读取已保存结果;
  • RUNNING:等待、抢占超时租约或返回处理中;
  • FAILED_RETRYABLE:在明确策略下重试;
  • FAILED_FINAL:停止,不自动换模型重放副作用。

网关只负责“哪一个模型生成下一步”,无法替业务保证退款、发信、写库等操作的 Exactly Once。幂等必须落在工具执行层。


十二、Redis 和 PostgreSQL 为什么不是可有可无

单 Worker Demo 可以把 Cooldown 放在内存里;一旦启动两个 Worker,就会出现:

Worker A:OpenAI 已熔断
Worker B:不知道,继续把请求发给 OpenAI

LiteLLM 官方建议多 Worker 或多副本部署使用 Redis,因为限流、预算、Cooldown、缓存和配置失效等状态需要共享;否则四个 Worker 可能各自执行一份限额,撤销的 Key 也可能只在部分 Worker 生效。LiteLLM:What Needs Redis

PostgreSQL 则用于持久化 Key、预算和用量等状态。需要区分两种健康:

  • /health/liveliness:网关进程是否活着;
  • /health/readiness:网关是否准备好接流量,配置了数据库时还会检查数据库连接。

这两个探针都不能证明某个模型真的能完成请求。模型可用性要看后台 Provider Health Check 或一次受控的小请求。LiteLLM Health Checks

本文设置 store_model_in_db: false,让 config.yaml 成为模型路由的唯一真相源。LiteLLM 支持 YAML 与数据库配置叠加,而且数据库写入的部分设置可能覆盖 YAML;如果团队同时使用 UI 和 Git 修改配置,很容易出现“文件明明改了,重启却不生效”。LiteLLM Config Source of Truth


十三、本地模型兜底应该做什么,不应该做什么

本地 4B 模型与云端旗舰模型能力并不等价,因此不能在告警、风控或代码修改任务中悄悄替代。

建议分级:

任务云端失败后本地处理处理方式
文本摘要、格式整理可以返回中标记 degraded=true
内部知识库只读问答可以限制引用范围,保留来源
简单分类、草稿可以后续可异步重算
严格 JSON条件允许必须服务端 Schema 校验
代码自动提交不建议只生成建议,禁止自动合并
退款、下单、发信不应自动排队或人工确认
法律、医疗、财务高风险结论不应静默降级明确不可用或转人工
云端专有 Web Search/引用不可等价替代走原生通道或失败关闭

要成为真正的离线兜底,本地模型必须提前拉取并完成冷启动演练。故障发生后才执行 ollama pull 没有意义,因为那时外网可能正是不可用的。


十四、生产部署还缺哪些东西

本文 Compose 能用于单机验证,但不能直接宣称生产高可用。上线前至少补齐:

1. 网关自身双实例

  • 两个 Gateway 实例部署到不同主机或可用区;
  • 前面使用 TLS Load Balancer;
  • Redis 和 PostgreSQL 使用高可用服务;
  • Liveness 只决定进程重启,Readiness 决定是否接流量。

2. 独立出口

  • 至少两条可切换的出站网络或代理;
  • 分别监控 DNS、TCP、TLS、首 Token 和完整响应;
  • 不要让三家直连最终都落到同一个脆弱的本地代理进程。

3. 密钥与权限

  • Provider Key 放 Secret Manager,不写入仓库;
  • 业务使用网关 Virtual Key,不拿 Provider Key;
  • 按团队限制模型、RPM、TPM 和月度预算;
  • Master Key 只用于管理,不分发给普通应用;
  • 网关 4000 端口不裸露公网。

LiteLLM 官方生产建议同样要求设置长随机 Master Key、稳定 Salt Key,并针对 Redis、数据库、Worker 和部署拓扑做生产配置。LiteLLM Production Best Practices

4. 隐私与日志

  • 默认不记录完整 Prompt 和模型输出;
  • 对手机号、身份证、Token 等字段做脱敏;
  • 日志保留请求 ID、模型 ID、错误类、耗时、Token 和成本即可;
  • 需要内容审计时使用独立权限、加密和保留周期。

5. 发布与回滚

  • 锁定镜像版本和镜像摘要;
  • 新配置先执行 docker compose config
  • 用固定测试集做四路故障演练;
  • 5% 流量灰度,比较成功率、P95、成本和答案契约;
  • 保留上一版 config.yaml 与一键回滚路径。

LiteLLM 官方发布页还提供 Cosign 镜像签名验证方法。生产环境不仅要固定 Tag,最好进一步固定镜像 Digest 并验证签名。LiteLLM v1.99.1 Release


十五、上线前的完整验收清单

功能契约

  • 基础文本在四条路上都能返回;
  • JSON 输出逐条通过同一个 Schema 校验;
  • Tool Call 参数在执行前再次校验;
  • System Prompt 在不同提供商上的合并方式已验证;
  • 不支持的参数不会被静默丢弃;
  • 本地模型降级会显式标记。

故障转移

  • OpenAI 失败可切 Claude;
  • OpenAI+Claude 失败可切 Grok;
  • 三家云端失败可切预下载本地模型;
  • 全部失败能在 Deadline 内结束;
  • 429、余额耗尽、401、5xx、Timeout 能被正确分类;
  • Cooldown 到期后以小流量探测恢复,而不是瞬间放满流量。

流式与 Agent

  • 首 Token 前断路能 Fallback;
  • 首 Token 后断流不会拼接第二模型输出;
  • 每个工具调用有业务级幂等键;
  • 已成功的工具结果能从账本恢复;
  • 模型切换发生在 Turn 边界,而不是工具副作用中间。

基础设施

  • Gateway 至少两个实例;
  • 多 Worker 共享 Redis;
  • PostgreSQL、Redis 有备份和恢复演练;
  • 本地模型已拉取并验证冷启动时间;
  • 出口网络不是单点;
  • Provider Key、Master Key 未出现在 Git 和普通日志中。

可观测性

  • 记录业务 Request ID;
  • 记录原始模型组与最终 Deployment ID;
  • 记录各次尝试的错误类和 Provider Request ID;
  • 统计成功率、P50/P95/P99、Fallback 率、Token、成本;
  • 单独统计“HTTP 成功但 Schema/业务验收失败”。

十六、最终判断:真正的切换不是换模型名,而是控制语义和副作用

一个可用的多模型网关,不能只回答“主模型挂了以后调用谁”,还要回答:

  1. 备用模型是否位于独立故障域?
  2. 当前请求的能力在备用模型上是否等价?
  3. 这类错误是否值得重放?
  4. 总时间预算还剩多少?
  5. 是否已经向用户输出内容?
  6. 是否已经执行过外部工具?
  7. 实际由哪一个 Deployment 服务,证据在哪里?
  8. 四条路都失败时,应该本地降级、排队,还是失败关闭?

LiteLLM 解决了统一接口、适配、路由和一部分状态管理;Redis 解决多 Worker 的共享状态;PostgreSQL 保存持久化审计;Ollama提供独立的本地故障域。但最终决定一套系统是否可靠的,仍然是业务侧的能力契约、幂等账本、总 Deadline 和故障演练。

所以,“同时接入 GPT、Claude、Grok”只是完成了第一步。只有当你真的依次断掉三家、断掉出口、断掉本地模型和断掉一个网关实例,系统仍能按照预定状态降级或有界失败,才能说它是一套真正能切换的多模型网关。


说明: 示例用于自有或已授权环境。模型 ID、费率、上下文窗口与网关配置会随版本变化;上线前应固定版本、重新查询账号可用模型,并在非生产环境完成全文中的故障演练。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐