1. 为什么一个项目需要模型网关:我的接入困境与 LiteLLM 的位置
有段时间我在代码里同时维护着好几套模型客户端的适配层。单个平台看,每个都有自己的 SDK,认证要写、超时要单独配、错误码要手动翻译;放在一起看,同一个业务逻辑被复制改写了多遍,每接一个新模型就等于再写一遍胶水代码。最难受的是平台调整限流策略或升级接口,底层一改,上面所有业务代码全都得跟着动。后来我把调用层整个抽掉,在应用和模型平台之间加了一个东西——LiteLLM。它做的事情一句话就能讲清楚:把各家大模型平台的调用统一成一套协议,同时把密钥、预算、路由、日志这些横切关注点收拢到一个代理服务里。这篇文章是我从零搭 LiteLLM 网关的完整记录,包括选型理由、最小部署、模型路由、预算管理和生产环境里踩过的坑,适合正在做多模型接入的团队,也适合刚接触 LiteLLM 的开发者。
1.1 多模型接入的四个重复劳动
当应用只接一个模型时,接口适配问题不明显,因为平台只有一个,认证方式固定,错误码就那几类。一旦要接两个以上,重复劳动立刻冒出来。我对着手头代码总结过,基本逃不出四类:
- 认证方式不统一:有的平台把密钥放请求头,有的放查询参数,有的还要动态签名。
- 请求结构不统一:对话补全、文本生成、向量化接口的入参各有各的规则。
- 错误处理不统一:同一个"限流",有的返回 429,有的返回 503,还有的返回 200 但响应体里写着错误。
- 成本统计不统一:每个平台计费粒度不同,有的按 token,有的按次调,月中想汇总账单得手工导出再换算。
这些事单独拎出来都不难,但堆在一起就是纯消耗。更重要的是,适配层代码通常散落在多个业务模块里,想统一改造一次,牵一发动全身,风险很高。
1.2 LiteLLM 在技术栈中的定位:统一调用层的"翻译官"
LiteLLM 在技术栈里扮演的角色,我习惯叫它"翻译官"。应用侧只负责发一个标准的 chat/completions 风格请求,代理侧负责把这份请求翻译成目标平台真实需要的结构,再把响应翻译回统一格式。这层翻译不是简单映射,它还顺手完成了三件对架构有价值的事:
第一,集中管理上游密钥。所有模型平台的凭证只保存在网关这边,应用层根本不需要接触,泄露面大幅缩小。第二,集中做路由和降级。某个模型不可用时,由网关决定怎么切换,业务侧无感。第三,集中做预算和审计。每个虚拟密钥花了多少钱、调了哪些模型、成功率如何,都能查出来。
一个不太严谨但容易理解的类比:它很像支付行业里的收单网关。商户不需要分别对接每家银行的刷卡协议,只需要面对一个终端,后面接哪家银行由网关协调。业务系统面对的同样是"一个模型入口",而不是"一堆供应商 SDK"。
1.3 先分清边界:它不负责模型训练,也不负责编排业务
把 LiteLLM 放进选型之前,先弄清楚它不是什么。它不是模型训练平台,不会帮你微调权重;它也不是业务编排框架,不会帮你做复杂的 Agent 规划或工具调用组装。它专注的是"把模型的调用和管理做好"这一层。
从数据流的角度看,应用请求先到达自托管的 LiteLLM,再由它转发给对应的模型平台,LiteLLM 本身不做内容理解和存储。换句话说,对话内容只会经过这一层,不会停留在这一层,除非你自己显式开启日志或回调。这个边界想清楚,后面很多架构决策会简单很多,比如要不要在网关上做缓存、要不要在这里做内容过滤,答案基本都是"不在这里做"。
2. 最小可用代理:从安装到第一次完成真实的模型调用
很多人一看"网关"两个字就觉得是个重组件,其实 LiteLLM 的最小可用配置比想象中简单。我第一次跑通只花了十几分钟,下面按步骤拆开讲。
2.1 安装与启动:一条命令跑起代理服务
LiteLLM 提供 SDK 和代理两种形态,实际部署直接用代理模式。安装方式和装普通 Python 包一样:
pip install 'litellm[proxy]'为什么后面要带[proxy]?因为代理模式依赖的服务端组件比较多,官方建议用这个 extra 装齐全套,否则跑起来之后会缺依赖,再一个个补很麻烦。
装完后准备一份最小配置文件。我先解释清楚三个关键字段,这决定了你能不能看明白后面所有配置。model_name是给应用层看的逻辑别名,你爱叫什么叫什么,应用层只认它;litellm_params.model是真正的上游路由,标准写法是"平台前缀/模型名";api_key建议所有密钥都从环境变量读,别写死在 yaml 里。
model_list: - model_name: unified-chat litellm_params: model: platform-a/flagship-latest api_key: os.environ/PLATFORM_A_API_KEY general_settings: master_key: sk-master-123启动命令:
export PLATFORM_A_API_KEY="在这里填你的上游密钥" litellm --config config.yaml --port 8000看到启动日志里出现类似 "Proxy running on http://0.0.0.0:8000" 的信息,就说明服务已经起来了。这里我用platform-a、flagship-latest做占位,实际部署时把model字段替换成你所用平台的 provider 前缀和真实模型名即可。
2.2 最小配置:别名、上游与密钥的关系
这份配置虽然短,里面藏着三个关键设计。
第一个是别名与真实模型解耦。unified-chat是应用层看到的名字,真实模型是platform-a/flagship-latest。今天接的是平台A,下个月想换成平台B,只需要改配置文件里的映射,应用代码一个字符都不用动。这种解耦在日常迭代里的价值,远大于省那几行配置代码。
第二个是环境变量引用。os.environ/PLATFORM_A_API_KEY是 LiteLLM 专门支持的写法,启动时自动去读进程里的环境变量。相比直接把密钥写进 yaml,至少不会因为配置文件被意外分享而泄露凭证。
第三个是 master_key。它是代理的管理员密钥,用来调管理接口、生成虚拟密钥、查看统计。所有访问代理的请求都必须带 key,等于是网关的第一道鉴权。初次体验时可以直接用它调业务接口,但生产环境建议只留给管理员用,业务请求走后面生成的虚拟密钥。
2.3 第一次请求验证:curl 和 Python 两种方式
代理启动之后,先用 curl 验证一把:
curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-master-123" \ -d '{ "model": "unified-chat", "messages": [{"role": "user", "content": "你好"}] }'返回内容里会带上 token 数统计之类的元信息,这说明请求已经从代理正确转发到上游并原样返回了。
如果更习惯用脚本验证,也可以直接用常见的 HTTP 客户端库:
import requests resp = requests.post( "http://localhost:8000/v1/chat/completions", headers={"Authorization": "Bearer sk-master-123"}, json={ "model": "unified-chat", "messages": [{"role": "user", "content": "你好"}], }, ) print(resp.json()["choices"][0]["message"]["content"])注意这里我故意没有引入任何平台专用的 SDK,而是用一个普通 HTTP 请求连base_url,这正是网关的价值所在:你的业务代码只感知一个统一的入口。
2.4 接口收敛为什么能省下大量适配代码
单独讲接口收敛的收益,是因为我见过不少团队一听到"网关"就觉得是引入了复杂组件。实际收益非常直接:代理一旦跑通,上游从平台A切到平台B,业务侧只需要改配置文件的映射和密钥,代码零改动。
我在迁移一个旧服务时深有体会。原来三个 client 类加起来一千多行适配代码,迁移后整个调用层只剩一个公共函数,开发时间从预估的三天降到了半天。网关的复杂性是集中且一次性的,而适配代码的复杂性是分散且持续增长的。选型的核心逻辑就在这里:宁可在一处把复杂度管好,也不要让它在业务代码里四处蔓延。
3. 多模型路由与故障转移:让请求在"正确"的模型上命中
代理跑通只是第一步。真实环境里,单一模型随时可能因为限流、过载、接口调整而不可用,这时候真正体现网关价值的是模型路由和故障转移能力。
3.1 同一个别名挂两个上游:故障转移如何触发
在 LiteLLM 里做故障转移最朴素的方式,是在model_list里让两个模型共用同一个model_name:
model_list: - model_name: unified-chat litellm_params: model: platform-a/flagship-latest api_key: os.environ/PLATFORM_A_API_KEY - model_name: unified-chat litellm_params: model: platform-b/flagship-latest api_key: os.environ/PLATFORM_B_API_KEY当代理收到unified-chat请求时,它会按一定策略选择一个上游。只有当前上游明确失败,比如连接超时、返回 5xx、触发限流,代理才会自动尝试第二个上游。对应用侧来说,整个过程就是一次请求,不需要感知中间换了平台。
但这里有一个关键点很多人会误解:同一个别名下挂两个上游,默认并不等于"一个主一个备",实际是多个上游按路由策略分发。如果你想要"优先平台A、平台A不可用才走平台B"的效果,要配合调度参数来做,下一节细说。
3.2 按权重分流:让成本与效果可调节
想控制两个模型各自承接多少流量,可以在模型信息里加权重。比如 80% 的请求走平台A、20% 走平台B,配置长这样:
model_list: - model_name: unified-chat litellm_params: model: platform-a/flagship-latest api_key: os.environ/PLATFORM_A_API_KEY model_info: weights: 0.8 - model_name: unified-chat litellm_params: model: platform-b/flagship-latest api_key: os.environ/PLATFORM_B_API_KEY model_info: weights: 0.2这个特性在两种场景下特别实用。一种是模型效果灰度,先切一小部分线上流量给新模型,观察回报数据再逐步调大比例;另一种是成本控制,比如平台A更贵但更稳,平台B便宜但能力稍弱,就分别配 0.3 和 0.7,让整体成本处在可控区间。
3.3 路由策略选择:简单洗牌、用量路由与延迟路由
LiteLLM 的路由策略不是拍脑袋实现的,配置router_settings就能切换。我在实际项目里用过这么几类,各自适用场景差别挺大:
- 简单洗牌(simple-shuffle):默认策略,多个上游之间随机分发,适合各家模型能力近似、单纯想做负载均衡的场景。
- 用量路由(usage-based-routing):根据每个上游剩余的每分钟请求数或 token 数来决定优先级,适合各家都有配额上限、希望尽量不浪费配额的场景。
- 延迟路由(latency-based-routing):优先选最近时段响应延迟最低的上游,适合对外延迟敏感的实时业务。
举例,配置用量路由:
router_settings: routing_strategy: usage-based-routing-v2 cooldown_time: 30cooldown_time的意思是某个上游连续出错后会被关进"冷却区"多少秒,冷却期内不再往它上面发请求,避免把故障扩散成雪崩。这个参数我建议所有人都显式设置,不要用默认值,具体原因在第五部分展开。
3.4 流式响应与 400 错误:fallback 的两个边界
讲完配置,讲两个我在真实环境里碰到的边界。
第一个是流式响应。如果请求带了流式参数,且上游已经开始吐 token,这时候上游突然断掉,LiteLLM 不会切换到备用模型把整段内容重新生成一遍。原因不难理解:用户端已经收到了一半的流式内容,代理没法把已经发出去的 token 收回来重新排。所以对实时流式场景,不要把 fallback 当成绝对保障,应用层最好自己做一次完整失败后的重试。
第二个是错误类型。故障转移并不是所有错误都会触发。像 400 这类请求参数错误,代理一般不会去尝试另一个上游,因为问题出在请求本身,换了平台大概率还是同样报错。只有连接类错误、超时、限流和 5xx 这类"平台侧问题"才会触发切换。理解这个边界,排查问题时就不会对着日志一头雾水,也不会误以为配置出了问题。
4. 团队密钥、预算与限流:把代理当作内部 API 管理台来用
如果代理只做转发,那它和一个反向代理没有本质区别。LiteLLM 真正值钱的地方,是把密钥、预算、限流这些治理能力做进了网关,让一个技术组件同时变成内部 API 管理台。
4.1 上游密钥不能直接下发:代理层密钥的价值
接入网关之前,每个应用团队都持有上游平台自己的密钥,这会带来几个问题。一是无法控制成本,谁都能发起请求,月底账单爆炸了才知道;二是没法审计,出了问题不知道是哪个系统调用的;三是上游平台更换密钥后要逐个应用通知,运维成本直线上升。
有了代理层虚拟密钥,上游密钥只放在网关一侧,各团队拿到的是"虚拟钥匙"。这把钥匙能调用哪个模型、预算多少、速率多少,都由网关统一控制。密钥的生成、吊销、过期都可以通过管理接口操作,相当于给密钥加了一层生命周期管理。
4.2 创建虚拟密钥并设置预算上限
创建虚拟密钥用的是管理接口,带 master key 调用:
curl -X POST http://localhost:8000/key/generate \ -H "Authorization: Bearer sk-master-123" \ -H "Content-Type: application/json" \ -d '{"user_id": "team-a", "budget_limit": 20.0}'响应里的key字段就是给应用用的虚拟密钥。user_id我习惯按团队或应用名来填,方便后续看统计;budget_limit表示这把钥匙最多能花对应的预算额度。把它配置到应用的环境变量里之后,应用对真实上游密钥就彻底无感了。
4.3 按团队拆分预算与速率限制
实际接入中,预算和限流往往是组合拳。下面这张表是我自己反复用到的参数组合:
| 参数 | 作用 | 举例 |
|---|---|---|
| user_id | 归属标识,便于统计和分摊 | team-a |
| budget_limit | 总预算上限 | 20.0 |
| rpm_limit | 每分钟允许请求数 | 60 |
| tpm_limit | 每分钟允许 token 数 | 100000 |
| models | 允许使用的模型白名单 | unified-chat |
| expires | 密钥过期时间 | 2025-12-31T23:59:59Z |
生成带限流的密钥:
curl -X POST http://localhost:8000/key/generate \ -H "Authorization: Bearer sk-master-123" \ -d '{"user_id": "team-a", "rpm_limit": 60, "tpm_limit": 100000, "models": ["unified-chat"]}'设置之后,团队A即使写了循环调用,也被限制在每分钟 60 次以内,不会一不小心拖垮网关或者打爆账单。更细的团队拆分还可以用多个 user_id 来对应不同项目,每个项目拿到的预算和限流规则完全隔离。
4.4 预算用完了会发生什么:错误码与处理策略
预算超限和限流不是静默发生的,代理会返回明确的 HTTP 状态码。下面是我在生产环境实际捕获过的几种情况:
| 状态码 | 含义 | 处理方式 |
|---|---|---|
| 401 | 虚拟密钥无效或已删除 | 检查密钥是否还存在,重新生成 |
| 429 | 预算耗尽或 rpm/tpm 超限 | 退避重试,或换更高配额密钥 |
| 404 | 请求的 model_name 未配置 | 检查应用层传入的模型名 |
| 502 | 上游平台不可用或返回异常 | 查看代理日志,确认上游状态 |
给个小建议:应用侧收到 429 后不要无限重试,至少要加一个指数退避逻辑。预算类 429 通常不会在几秒钟内自己恢复,重试只会加重网关压力,还可能让本应更早暴露的成本问题被掩盖掉。
5. 接生产环境前必须知道的坑与调优
最后这部分,是真正决定你部署体验的地方。网上教程大多停在"能跑通",但生产环境跑的是另一套玩法:有并发、有超时、有故障、有成本账单。
5.1 超时、重试与冷却时间:别让故障转移变成雪崩
故障转移是一把双刃剑。如果某个上游平台持续故障,代理把所有请求都切到备用平台,备用平台很可能迅速跟着过载,然后整个网关变成下一个故障点。所以三个参数必须一起调,而不是只开开关:
timeout:模型级超时,建议按上游平台实际响应速度设置在合理范围(比如 30 到 60 秒),别用默认值。太短容易误判故障,太长让用户等到失去耐心。allowed_fails:连续失败多少次后把该上游标记为不健康。cooldown_time:标记为不健康后的冷却秒数,冷却期内不再选择它。
配置示例:
router_settings: allowed_fails: 3 cooldown_time: 30 num_retries: 2num_retries是代理在切换上游时最多重试几次。我见过有人把重试设成 10 次,结果单次请求延迟被拖到几十秒,比直接失败更伤用户体验。重试次数 2 到 3 次足够,再多就是拿用户的等待时间为代价去赌平台恢复。
5.2 流式请求、Embedding 与参数兼容性
流式请求要单独验证。我在前面提过流式场景下 fallback 语义会弱化,所以如果业务大量依赖流式输出,建议在应用层保留一个重试或降级逻辑,不要只依赖网关。
Embedding 类请求也容易被忽略。如果一个网关既接对话模型又接向量模型,建议给它们分别设置别名,比如用unified-chat和unified-embedding,不要复用同一个 model_name,否则限流统计和成本分摊都会混在一起,排查问题时全是脏数据。
还有一个非常好用的开关:drop_params: true。当请求里带了某个上游不支持的参数(比如有的平台不接收温度参数),默认情况下代理会把请求原样转发,上游直接报错;开启这个开关后,代理会自动丢弃不支持参数再转发,能消除很多莫名其妙的报错。配置位置在litellm_settings下:
litellm_settings: drop_params: true5.3 成本追踪与自定义回调:看到每一分钱去哪了
网关最大的隐性价值是成本可见性。LiteLLM 内置了多种回调方式,我最常用的是自定义回调:把每次请求的模型名、token 数和预估成本追加到一个日志文件。写一个简单的回调模块:
async def log_success_event(kwargs, response, start_time, end_time): cost = kwargs.get("response_cost", 0) model = kwargs.get("model", "") with open("/tmp/litellm_cost.log", "a") as f: f.write(f"{model} {cost}\n")然后在配置里注册:
litellm_settings: success_callback: ["log"] failure_callback: ["log"]之后用/key/info这类管理接口,就能看到某个虚拟密钥累计消耗了多少 token、预估花了多少钱。注意这里的成本是根据 token 数和模型单价估算出来的,不同平台的计费口径不完全一样,和真实账单对比时可能有少量偏差,做对账要留出余量。
5.4 私有化模型接入的注意点
最后提一下内网私有化模型的接入。现在不少团队会自己部署开源模型,LiteLLM 对这类场景是支持的。核心套路是:如果本地模型服务提供 chat/completions 风格接口,就在litellm_params里把api_base指到本地地址,并配置对应的 provider 前缀。
我的建议是,私有模型单独建一个别名,不要和云端模型混在同一个model_name下。否则 fallback 时会出现"内网模型和公网模型互切"这种很尴尬的网络路径,延时忽高忽低,问题还特别难复现。把别名拆开之后,紧急情况下手动切起模型来也更快,不会误伤正常的业务流量。
我实际把 LiteLLM 从实验工具变成生产网关,最大的体会是别急着上所有功能。先把代理跑起来,把虚拟密钥、预算、日志打开,这三件事足以让团队的调用习惯发生变化。模型路由、冷却策略、成本回调这些东西,是后面遇到真实故障再逐步补上的。另一个小建议:把代理的健康检查端点接进监控系统,并且在应用侧保留一个"切换模型别名的开关",宁可多一个手工应急手段,也不要完全依赖自动切换。网关只能处理它看得见的故障,应用层的兜底设计才是最后一道防线。