GPT、Claude、Grok 同时不可用怎么办?设计一个真正能切换的多模型网关
一、先给结论:多模型不等于多故障域
很多所谓“多模型网关”只有下面这段逻辑:
models = ["gpt", "claude", "grok"]
for model in models:
try:
return call(model)
except Exception:
continue
这段代码在 Demo 中能切换,在生产中却可能造成五类事故:
- 同一个错误被重试四五次,用户等了几分钟才收到失败;
- 请求参数只适配 GPT,切到 Claude 或 Grok 后被静默忽略;
- 流式输出已经发了一半,又切模型生成第二个互相矛盾的后半段;
- Agent 已经执行过“退款”“发邮件”等工具,再重放一次导致重复副作用;
- 三个模型虽然名字不同,却都经过同一个中转、同一条出口网络或同一个网关进程,单点故障时一起消失。
所以,真正能切换的多模型网关至少要有四层:
本文最终实现的不是“任何请求都能无损切换”,而是更诚实的目标:
- 基础文本请求可以在 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 API、Ollama OpenAI Compatibility
Anthropic 同样提供 OpenAI SDK 兼容层,但官方明确说它主要用于测试和比较,并非多数场景下的长期生产方案;Claude 的 PDF、引用、Thinking、Prompt Caching 等完整能力仍应使用原生 Claude API。其兼容层还会忽略 Function Calling 的 strict,也会重新处理 system/developer 消息。Anthropic OpenAI SDK Compatibility
因此,一个可靠网关不应向业务承诺“所有模型功能完全一致”,而应建立能力分层:
| 能力档位 | 对外别名 | 允许的功能 | 降级原则 |
|---|---|---|---|
| 基础文本 | chat-default | 文本消息、温度、最大输出 | 可跨四个后端 |
| 结构化输出 | chat-json | JSON Schema、服务端校验 | 只进入验证通过的模型 |
| 工具调用 | chat-tools | Tool Calling、参数 Schema | 必须再次校验工具参数 |
| 长上下文 | chat-long | 大上下文、预调用 Token 检查 | 超长时切长上下文组,不盲重试 |
| 原生能力 | native-* | Thinking、引用、Web Search、Realtime | 固定提供商,不承诺透明切换 |
本文的可运行配置只开放 chat-default,故意把范围限制在最容易验证的文本共同子集。等基础链路通过故障演练后,再分别建设 chat-json、chat-tools,不能用一个万能别名承接全部请求。
为什么不建议默认 drop_params: true
LiteLLM 可以删除目标模型不支持的参数,但“HTTP 200”不代表语义等价。假设业务要求严格 JSON,而切到某模型后 strict 被静默忽略,网关虽然成功返回,业务却可能拿到无法解析的结果。
生产默认应倾向:
litellm_settings:
drop_params: false
不兼容参数先失败,再由明确的能力路由处理。只有确认某参数不影响语义时,才针对特定部署配置删除,而不是全局静默吞掉。
四、我们要搭建的网关
技术组件如下:
| 组件 | 作用 |
|---|---|
| LiteLLM Proxy | OpenAI-compatible 入口、模型适配、Fallback、Cooldown |
| Redis | 多 Worker 共享限流、缓存和熔断状态 |
| PostgreSQL | Virtual 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 Fallbacks、Health 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 Releases、Ollama 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 Compatibility、Qwen3.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 单实例退出 | 第二实例接管 | 客户端地址不变 |
| 返回非法 JSON | 无 | Schema 校验失败,不能当成功 |
九、哪些错误应该切换,哪些不应该
不能写一个 except Exception 处理所有问题。
| 错误类型 | 是否自动切换 | 原因 |
|---|---|---|
| 网络连接失败、DNS、TLS、超时 | 是,受总 Deadline 限制 | 更换故障域可能恢复 |
| 429 临时限流 | 是,有限退避或换提供商 | 重放次数必须有上限 |
| 5xx 提供商错误 | 是 | 典型瞬态或服务故障 |
| 401/403 | 可切换但必须立即告警 | 独立 Key 可保业务,配置问题不会自行恢复 |
| 余额耗尽 | 可切换但不要重试原路 | 重试只会继续失败 |
| 400 参数错误 | 通常不切换 | 多半是调用契约错误,应修请求 |
| 上下文超限 | 只切 chat-long | 不是普通服务故障 |
| 内容安全拒绝 | 默认不跨策略绕过 | 避免把 Fallback 变成安全策略逃逸 |
| 已输出首个 Token 后断流 | 不透明切换 | 第二模型无法无缝续写同一序列 |
| 工具已执行后模型超时 | 不自动重放工具 | 可能产生重复副作用 |
OpenAI 的 429 可能表示临时请求/Token 限额,也可能表示余额耗尽,处理方式完全不同。应读取错误体的 code、type 和 Retry-After,而不是看见 429 就睡眠重试。OpenAI Error Codes
给每次请求一个总时间预算
假设四条路各自允许 25 秒并串行尝试,最坏耗时可能超过 100 秒,还没有计算 DNS 和客户端重试。更合理的做法是:
业务总 Deadline:45 秒
OpenAI 尝试:最多 15 秒
Claude 尝试:最多 12 秒
Grok 尝试:最多 10 秒
本地模型:使用剩余预算;不足则排队或失败
LiteLLM 负责路由,但业务仍需设置端到端 Deadline。网关配置里的单路 Timeout 不是整个业务链路的 SLA。
十、流式输出为什么不能中途“无感换模型”
非流式请求在向用户返回内容前失败,网关可以换路;流式请求一旦已经发送:
data: 第一个模型生成的前半句
HTTP 状态和响应头通常已经提交。此时切到第二个模型会遇到三个问题:
- 第二个模型不知道第一个模型内部准备如何续写;
- 将已输出文本追加到 Prompt 会改变上下文和 Token 计费;
- 两段输出可能重复、矛盾或破坏 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/业务验收失败”。
十六、最终判断:真正的切换不是换模型名,而是控制语义和副作用
一个可用的多模型网关,不能只回答“主模型挂了以后调用谁”,还要回答:
- 备用模型是否位于独立故障域?
- 当前请求的能力在备用模型上是否等价?
- 这类错误是否值得重放?
- 总时间预算还剩多少?
- 是否已经向用户输出内容?
- 是否已经执行过外部工具?
- 实际由哪一个 Deployment 服务,证据在哪里?
- 四条路都失败时,应该本地降级、排队,还是失败关闭?
LiteLLM 解决了统一接口、适配、路由和一部分状态管理;Redis 解决多 Worker 的共享状态;PostgreSQL 保存持久化审计;Ollama提供独立的本地故障域。但最终决定一套系统是否可靠的,仍然是业务侧的能力契约、幂等账本、总 Deadline 和故障演练。
所以,“同时接入 GPT、Claude、Grok”只是完成了第一步。只有当你真的依次断掉三家、断掉出口、断掉本地模型和断掉一个网关实例,系统仍能按照预定状态降级或有界失败,才能说它是一套真正能切换的多模型网关。
说明: 示例用于自有或已授权环境。模型 ID、费率、上下文窗口与网关配置会随版本变化;上线前应固定版本、重新查询账号可用模型,并在非生产环境完成全文中的故障演练。
更多推荐



所有评论(0)