在 2023 年,一个应用通常只调用一个模型;到 2026 年,一个稍具规模的产品会同时使用 6 到 15 个模型:日常对话用便宜的小模型,复杂推理切到旗舰模型,图片理解走多模态模型,代码补全走专门的代码模型,再加上嵌入模型与重排模型。模型数量一多,密钥管理、限流、配额、审计、可观测与故障转移这些横切关注点就会在每一个业务服务里重复实现一遍,最终演变成一场灾难。
模型网关(Model Gateway)要解决的就是这个问题:把「如何选模型、如何调用模型、如何兜底」从业务代码里抽出来,收敛到一个统一入口。本文以 LiteLLM Proxy 为主线,给出从部署到路由策略再到收益量化的完整工程方案。
一、为什么需要模型网关
1.1 从散落调用到统一入口
没有网关时,典型代码是这样的:业务服务直接 requests.post() 到 OpenAI,另一处又直接调用 Anthropic,密钥硬编码在环境变量里,重试逻辑各写各的,某个供应商挂了只能等用户投诉。这种模式的成本随模型数量线性增长,而风险则是指数增长。
网关把这一切收敛到一层,业务侧只面向一个 OpenAI 兼容端点:
from openai import OpenAI
client = OpenAI(
base_url="http://llm-gateway.internal:4000/v1",
api_key="sk-team-a-virtual-key", # 网关签发的虚拟 key,不是真实供应商 key
)
resp = client.chat.completions.create(
model="chat-default", # 逻辑别名,由网关解析为真实模型
messages=[{"role": "user", "content": "用一句话解释什么是 RAG"}],
)
print(resp.choices[0].message.content)
业务代码里没有出现任何真实供应商的密钥、base_url 或重试逻辑,全部下沉到网关。这一层抽象是后面所有能力(路由、限流、配额、审计)的地基。
1.2 网关的核心能力矩阵
一个生产级模型网关至少要提供下表的能力。缺失任何一项,都会在规模上来之后变成痛点。
| 能力 | 解决的问题 | 没有它的后果 | LiteLLM 支持 |
|---|---|---|---|
| 统一接口 | 各供应商 SDK 与参数不一致 | 业务代码充满 if-else 分支 | OpenAI 兼容 /v1/chat/completions |
| 密钥托管 | 真实 key 散落各处 | 泄露风险、轮换困难 | model_list 集中配置 + 虚拟 key |
| 限流 | 供应商侧 RPM/TPM 被打爆 | 429 雪崩、级联失败 | rpm / tpm 逐模型限流 |
| 配额 | 团队/租户超预算 | 账单失控、无法归因 | 虚拟 key 绑定 max_budget |
| 审计 | 谁在什么时候调了什么 | 无法追责、无法合规 | 请求日志 + 回调 |
| 可观测 | 延迟/错误/成本盲区 | 故障排查靠猜 | Prometheus / Langfuse / OTel |
| 路由与兜底 | 单点供应商故障 | 全站不可用 | fallbacks + Router 策略 |
| 缓存 | 重复请求浪费钱 | 成本虚高 | Redis 语义缓存 |
1.3 网关与推理引擎的分工
需要澄清一个常见混淆:网关不等于推理引擎。网关负责「选哪个模型、怎么调用、怎么兜底」,而推理引擎(vLLM、SGLang、TensorRT-LLM)负责「单个模型怎么跑得快」。自托管模型时,网关把请求转发到推理引擎的 OpenAI 兼容端点;调用商业 API 时,网关直接转发到供应商。两者是上下游关系,详见 推理引擎对比 。
网关在高并发下必须做到几乎无状态、低开销:一次路由决策的 CPU 开销应控制在 1 毫秒以内,否则它自己就会成为 P99 延迟的瓶颈。
二、LiteLLM Proxy 落地
2.1 安装与启动
LiteLLM 是当前最成熟的开源网关实现,Proxy 模式提供完整的 OpenAI 兼容服务端。生产部署通常用 Docker Compose 或 Kubernetes:
export LITELLM_MASTER_KEY="sk-master-please-rotate"
export DATABASE_URL="postgresql://litellm:pass@localhost:5432/litellm"
pip install "litellm[proxy]==1.74.0"
litellm --config ./config.yaml --port 4000 --num_workers 4
--num_workers 4 让网关以多进程模式运行,单进程大约能扛 800 QPS 的纯转发(不含上游延迟),4 进程配合 4 核可稳定支撑 2500 QPS 左右。若需要横向扩展,多副本部署加一个共享的 Postgres 与 Redis 即可,网关本身无状态。这套部署形态在 Kubernetes 上做服务化
时尤为自然。
2.2 config.yaml 逐段解析
下面是一份可直接运行的生产级配置,覆盖了模型清单、路由策略、限流与回退:
model_list:
# ---- 旗舰模型:质量优先场景 ----
- model_name: chat-premium # 对外暴露的逻辑名
litellm_params:
model: openai/gpt-4.1 # 真实模型
api_key: os.environ/OPENAI_API_KEY
rpm: 5000
timeout: 60
model_info:
input_cost_per_token: 0.000002 # $2 / 1M tokens
output_cost_per_token: 0.000008
# ---- 主力模型:性价比之选 ----
- model_name: chat-default
litellm_params:
model: openai/gpt-4.1-mini
api_key: os.environ/OPENAI_API_KEY
rpm: 10000
timeout: 30
model_info:
input_cost_per_token: 0.0000004 # $0.40 / 1M tokens
output_cost_per_token: 0.0000016
# ---- 自托管模型:走本地推理引擎 ----
- model_name: chat-selfhosted
litellm_params:
model: openai/qwen3-32b
api_base: http://vllm-inference.svc:8000/v1
api_key: os.environ/VLLM_KEY
rpm: 2000
# ---- 备用供应商:故障转移目标 ----
- model_name: chat-backup
litellm_params:
model: anthropic/claude-sonnet-4-6
api_key: os.environ/ANTHROPIC_API_KEY
rpm: 3000
router_settings:
routing_strategy: cost-based-routing # 成本优先,见第三节
num_retries: 2
retry_after: 1
allowed_fails: 3 # 连续失败 3 次进入冷却
cooldown_time: 30 # 冷却 30 秒
timeout: 30
fallbacks:
- chat-premium: ["chat-backup", "chat-default"] # 旗舰挂了降级
- chat-default: ["chat-backup"]
context_window_fallbacks:
- chat-default: ["chat-premium"] # 上下文超限时切到大窗口模型
redis_host: os.environ/REDIS_HOST
redis_port: 6379
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
# 预算与配额
max_budget: 5000 # 全局月度预算(美元)
budget_duration: 30d
litellm_settings:
drop_params: true # 自动丢弃上游不支持的参数
cache: true
cache_params:
type: redis
ttl: 3600
request_timeout: 30
set_verbose: false
2.3 模型名映射与别名
映射关系必须做到「业务侧看到的逻辑名」与「真实模型名」严格解耦。下表给出一个典型的三层命名约定:
| 逻辑别名 | 真实模型 | 定位 | 输入价 $/1M | 输出价 $/1M |
|---|---|---|---|---|
| chat-premium | gpt-4.1 | 复杂推理、长文 | 2.00 | 8.00 |
| chat-default | gpt-4.1-mini | 通用对话 | 0.40 | 1.60 |
| chat-selfhosted | qwen3-32b (vLLM) | 高并发、可控成本 | 自托管算力 | 自托管算力 |
| chat-backup | claude-sonnet-4-6 | 故障转移 | 3.00 | 15.00 |
| embed-default | text-embedding-3-large | 向量化 | 0.13 | 0 |
| rerank-default | bge-reranker-v2-m3 | 重排 | 自托管 | 自托管 |
命名约定的价值在于:某天 gpt-4.1-mini 涨价或下线,只需要改 config.yaml 里的一行,业务代码零改动。这是网关相对于「硬编码模型名」最直接的收益。
三、路由策略
LiteLLM 的 Router 内置了多种策略。选择哪一种,取决于当前业务的首要目标:省钱、省时间还是保质量。
3.1 成本优先
cost-based-routing 会在候选模型里优先选择单位 token 成本最低者。适合大规模、质量容忍度高的场景,如客服摘要、分类打标。代价是可能把复杂请求也丢给弱模型,导致质量回退,因此通常配合语义路由或质量门禁使用。
3.2 延迟优先
latency-based-routing 依据历史响应延迟(默认滑动窗口)选择当前最快的部署。适合交互式场景,如输入联想、实时助手。注意它只看延迟不看质量,因此更适合在同一档位的多个等价部署之间做负载均衡,而不是跨档位选择。
3.3 质量优先与加权路由
当质量是硬约束时,用 simple-shuffle 加权或 least-busy 在多个高质量部署间分配。加权路由适合灰度:新模型给 10% 流量,观察一周再逐步放量。灰度期间必须同时观察质量与成本两类指标,确认无回退后再逐步加大权重。
下表总结了各策略的适用面:
| 策略 | 决策依据 | 适用场景 | 主要风险 |
|---|---|---|---|
| cost-based-routing | 单位 token 价格 | 批量、低成本容忍 | 质量回退 |
| latency-based-routing | 历史延迟 | 实时交互 | 忽略质量差异 |
| usage-based-routing-v2 | TPM/RPM 余量 | 多部署负载均衡 | 与成本无关 |
| least-busy | 当前并发 | 抖动大的自托管集群 | 需要准确并发计数 |
| simple-shuffle (权重) | 固定权重 | 灰度、A/B | 需人工调参 |
| 语义路由 | 请求语义分类 | 混合负载 | 分类器本身有开销 |
3.4 故障转移、重试与熔断
兜底是网关最重要的价值。fallbacks 定义降级链,num_retries 定义重试,allowed_fails + cooldown_time 实现熔断:某部署连续失败 3 次后被摘除 30 秒,期间流量走备用。
下面是一个在代码侧显式控制路由与回退的完整示例:
import os
from litellm import Router
router = Router(
model_list=[
{
"model_name": "chat-default",
"litellm_params": {
"model": "openai/gpt-4.1-mini",
"api_key": os.environ["OPENAI_API_KEY"],
"rpm": 10000,
},
},
{
"model_name": "chat-backup",
"litellm_params": {
"model": "anthropic/claude-sonnet-4-6",
"api_key": os.environ["ANTHROPIC_API_KEY"],
"rpm": 3000,
},
},
],
routing_strategy="cost-based-routing",
num_retries=2,
allowed_fails=3,
cooldown_time=30,
fallbacks=[{"chat-default": ["chat-backup"]}],
redis_host=os.environ.get("REDIS_HOST", "localhost"),
)
def ask(prompt: str) -> str:
resp = router.completion(
model="chat-default",
messages=[{"role": "user", "content": prompt}],
timeout=30,
)
# 通过响应头/元数据判断实际命中了哪个部署
served = resp._hidden_params.get("model_id", "unknown")
print(f"[route] served by {served}")
return resp.choices[0].message.content
if __name__ == "__main__":
print(ask("把下面这段话压缩成一句话:……"))
_hidden_params 里记录了实际命中的部署与重试次数,是排查路由行为的关键字段,务必打点到日志或追踪系统里。
四、语义路由
规则路由(按成本、按延迟)解决不了「同一个接口既要处理简单问答又要处理复杂推理」的问题。语义路由用一个小而快的分类器或嵌入模型判断请求的难度与类型,再决定交给哪个档位的模型。
4.1 原理
做法通常有两种:一是用一个轻量分类器(如 100M 级别的模型)把请求分为 simple / complex 两类;二是把请求向量化后与预先定义的意图原型做最近邻匹配。分类器本身的延迟必须远小于它节省的模型延迟,否则得不偿失。
4.2 可运行实现
下面用嵌入相似度做一个极简的语义路由,把「翻译、改写」这类简单任务导向便宜模型,把「推理、证明」导向旗舰模型:
import numpy as np
from openai import OpenAI
client = OpenAI(base_url="http://llm-gateway.internal:4000/v1",
api_key="sk-team-a-virtual-key")
PROTOTYPES = {
"chat-default": ["把这句话翻译成英文", "帮我改写这段文字", "提取关键词"],
"chat-premium": ["证明这个定理", "分析这段代码的并发缺陷", "设计一个分布式方案"],
}
def embed(texts: list[str]) -> np.ndarray:
r = client.embeddings.create(model="embed-default", input=texts)
return np.array([d.embedding for d in r.data], dtype=np.float32)
def route(query: str) -> str:
proto_vecs = {tier: embed(samples) for tier, samples in PROTOTYPES.items()}
q = embed([query])[0]
q = q / (np.linalg.norm(q) + 1e-9)
best, best_score = "chat-default", -1.0
for tier, vecs in proto_vecs.items():
sims = vecs @ q / (np.linalg.norm(vecs, axis=1) + 1e-9)
score = float(sims.max())
if score > best_score:
best, best_score = tier, score
return best
if __name__ == "__main__":
for q in ["帮我把这段话翻译成日语", "证明 P 是否等于 NP 的思路"]:
print(q, "->", route(q))
生产环境不要把原型向量每次重算,应离线构建并缓存在内存或 Redis 中。语义路由的分类准确率若低于 90%,带来的成本收益会被误判成本(简单请求被送到旗舰模型)抵消掉,上线前务必用真实流量做一次离线评测。
五、Token 计数与预算
5.1 精确计数
成本控制的前提是精确计数。不同模型的 tokenizer 不同,用错 tokenizer 会导致预算偏差 10% 到 30%。LiteLLM 提供 token_counter 按模型精确计数:
import litellm
def count_cost(model: str, messages: list[dict], max_output: int = 512) -> float:
in_tokens = litellm.token_counter(model=model, messages=messages)
# 按最坏情况预留输出 token,避免预算被击穿
in_cost = in_tokens * litellm.model_cost[model]["input_cost_per_token"]
out_cost = max_output * litellm.model_cost[model]["output_cost_per_token"]
return in_cost + out_cost
msgs = [{"role": "user", "content": "用 200 字总结这篇文章"}]
print(f"预估单次成本: ${count_cost('gpt-4.1-mini', msgs):.6f}")
5.2 预算控制
预算是网关的硬约束。为每个团队签发虚拟 key 并绑定 max_budget,超预算直接拒绝而不是等到月底看账单:
curl -s -X POST http://llm-gateway.internal:4000/key/generate \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"team_id": "team-a",
"max_budget": 200,
"budget_duration": "30d",
"rpm_limit": 600,
"models": ["chat-default", "embed-default"]
}'
关键设计是「按最坏情况预留」:在请求发出前就用 max_tokens 估算上限并扣减配额,请求结束后再按实际用量回补。这样即使出现异常长的输出,也不会击穿预算。预算与成本归因的完整体系见 LLMOps 成本治理
。
六、可观测与审计
网关是唯一能看到全部模型流量的地方,因此也是可观测的最佳埋点位置。若不做埋点,路由策略的好坏、成本的去向、故障的根因都无从判断。一个成熟网关至少应暴露三类信号:路由决策、成本归因、健康状态。
6.1 必须打点的路由元数据
每次请求结束后,应把下列字段写入日志或追踪系统。它们是后续所有分析与调优的原始数据:
| 字段 | 含义 | 典型用途 |
|---|---|---|
| logical_model | 业务侧请求的逻辑名 | 统计别名命中分布 |
| served_deployment | 实际命中的部署 | 排查路由是否符合预期 |
| retry_count | 重试次数 | 识别不稳定供应商 |
| fallback_triggered | 是否发生降级 | 监控主模型健康度 |
| prompt_tokens | 输入 token 数 | 成本归因、预算预警 |
| completion_tokens | 输出 token 数 | 成本归因 |
| cost_usd | 本次请求成本 | 按团队/模型聚合 |
| cache_hit | 是否命中缓存 | 评估缓存收益 |
| ttft_ms | 首 token 延迟 | 交互体验核心指标 |
| total_ms | 端到端延迟 | 路由策略效果评估 |
6.2 通过回调接入追踪
LiteLLM 的 callback 机制可以在不侵入业务代码的前提下完成打点。下面把每次调用写进结构化日志,并推送到 OpenTelemetry 兼容的后端:
import json
import logging
import time
from litellm.integrations.custom_logger import CustomLogger
logger = logging.getLogger("llm_gateway_audit")
logger.setLevel(logging.INFO)
class AuditLogger(CustomLogger):
def log_success_event(self, kwargs, response_obj, start_time, end_time):
meta = kwargs.get("litellm_params", {}).get("metadata", {})
hidden = getattr(response_obj, "_hidden_params", {}) or {}
record = {
"ts": time.time(),
"team": meta.get("user_api_key_team_id", "unknown"),
"logical_model": kwargs.get("model"),
"served_deployment": hidden.get("model_id"),
"retry_count": hidden.get("retries", 0),
"cost_usd": kwargs.get("response_cost", 0.0),
"total_ms": (end_time - start_time).total_seconds() * 1000,
"status": "success",
}
logger.info(json.dumps(record, ensure_ascii=False))
def log_failure_event(self, kwargs, response_obj, start_time, end_time):
record = {
"ts": time.time(),
"logical_model": kwargs.get("model"),
"error": str(kwargs.get("exception")),
"total_ms": (end_time - start_time).total_seconds() * 1000,
"status": "failure",
}
logger.warning(json.dumps(record, ensure_ascii=False))
audit = AuditLogger()
把 audit 注册到 litellm_settings.callbacks 后,成功与失败两条路径都会被记录。注意失败路径同样重要:只记录成功请求,会系统性低估错误率。
6.3 健康度告警阈值
打点之后需要阈值才能形成闭环。下表给出一组经过验证的告警线,可作为起步参考:
| 指标 | 正常区间 | 告警阈值 | 处置建议 |
|---|---|---|---|
| fallback 触发率 | < 1% | > 5% | 检查主模型可用性 |
| 平均重试次数 | < 0.1 | > 0.5 | 排查上游限流 |
| 缓存命中率 | 20% - 40% | < 10% | 检查缓存键设计 |
| 语义路由误判率 | < 8% | > 15% | 重新训练分类器 |
| 单请求 P99 延迟 | < 3 s | > 8 s | 检查超时与重试配置 |
| 日成本环比 | ±10% | +30% | 排查异常调用方 |
审计不只是运维需求,也是合规需求:谁在什么时候调用了哪个模型、输入了什么、花费多少,这些记录在受监管行业是硬性要求。把审计日志与业务请求 ID 关联,才能在出现问题时做到端到端追溯。
七、收益量化
下表来自一个真实迁移案例:某 SaaS 产品有 12 个业务服务各自直连 3 家供应商,迁移到统一网关后的前后对比。
| 指标 | 迁移前 | 迁移后 | 变化 |
|---|---|---|---|
| 月均模型支出 | $18,400 | $6,900 | -62.5% |
| P99 端到端延迟 | 3.2 s | 1.4 s | -56.3% |
| 供应商故障导致的全站不可用 | 3 次/月 | 0 次/月 | 消除 |
| 密钥轮换工作量 | 12 处代码 | 1 处配置 | -92% |
| 请求级成本归因 | 无 | 按团队/模型 | 从无到有 |
| 缓存命中率 | 0% | 31% | 新增 |
成本下降主要来自三块:语义路由把 68% 的请求从旗舰模型切到主力模型(贡献约 40% 节省)、语义缓存命中省掉 31% 的重复调用(贡献约 15%)、以及故障转移避免了重试风暴带来的重复计费。延迟下降主要来自延迟路由与缓存。
需要强调:这些收益不是「上了网关」自动获得的,而是路由策略与缓存配置得当的结果。只做统一接口而不配路由,成本几乎不会变。
八、常见坑清单
- streaming 中断后 fallback 静默接管:流式响应一旦开始吐出第一个 token,就无法再回退到别的模型,否则用户会看到两段拼接的文本。正确做法是只在「首个 token 之前」允许 fallback,首 token 之后失败就报错。
- fallback 掩盖真实错误:备用模型成功返回后,主模型的失败会被吞掉。必须把
_hidden_params里的重试次数与命中部署打点到监控,否则主模型持续劣化也无人察觉。 - 模型名映射不一致:
config.yaml里的逻辑名与业务代码里的model=参数拼写不一致时,网关会直接返回 400。建议用常量集中管理逻辑名,并在 CI 里校验所有引用都存在。 - 限流参数照抄:
rpm/tpm必须与供应商账号的实际配额对齐,设得过高等于没有限流,设得过低会误伤正常流量。 - token 计数用错 tokenizer:跨模型估算成本时用了默认 tokenizer,偏差可达 30%,导致预算严重失真。
- 语义路由分类器过重:分类器本身若耗时 200 毫秒,而它节省的模型延迟只有 100 毫秒,就是负优化。
- 冷却时间与重试次数相乘放大延迟:
num_retries=3配cooldown_time=30,最坏情况下用户要等 90 秒以上,必须给整个调用链设置总超时。 - 缓存键未包含模型与参数:不同模型、不同 temperature 的请求若共用一个缓存键,会返回错误结果。缓存键必须包含模型名、messages、temperature 等全部影响输出的参数。
小结
模型网关是 LLMOps 从「能跑」走向「可运营」的第一块基石。它把密钥、限流、配额、审计、可观测与故障转移这些横切能力从业务代码中抽出,收敛到一层 OpenAI 兼容入口。LiteLLM Proxy 用一份 config.yaml 就能覆盖模型清单、路由策略与回退链,配合虚拟 key 与预算实现细粒度成本控制。路由策略的选择取决于业务目标:成本优先省钱、延迟优先保体验、质量优先保正确性、语义路由在混合负载下兼顾三者。一个配置得当的网关可以带来六成以上的成本下降与过半的 P99 延迟改善,但前提是路由与缓存真正调优过,而不是仅仅做了接口统一。落地时务必盯紧 streaming 中断、fallback 掩盖错误与模型名映射不一致这三类高频坑。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。