前阵子维护一个多模型项目,被各家模型服务商的不同接口折磨到怀疑人生。后来把一层叫 litellm 的网关架在业务和模型之间,问题才算真正理顺。简单说,litellm 是一个把常见大模型接口统一成同一套请求/响应契约的开源项目,既能当 SDK 在代码里直接调用,也能以代理服务形态独立部署,顺带把路由、重试、限流、预算追踪这些生产环境刚需一起解决。这篇不是官方文档的复读,而是我从试验代码推到准生产环境这一路的实战总结,适合正在为“多模型接入、频繁切换、成本失控”头疼的人参考。
1. 为什么需要litellm:一堆模型API把我的代码拆成了毛线
1.1 多模型接入的真实痛点
先还原一个很常见的场景。业务方说:这个功能要用厂商A的旗舰模型,那个功能要用厂商B的响应式模型,还要留一个厂商C的开源模型做本地降级。于是代码里出现三套 SDK、三套鉴权方式、三套消息结构、三套错误处理。表面看是多了几段 import,实际上每多一个厂商,整个请求链路都要跟着复制一份。
最痛苦的是流式输出。厂商A的流式返回里有choices[0].delta.content,厂商B可能是另外一套字段,厂商C走的又是 EventSource 风格。为了让前端拿到统一的流,你得在网关层写“方言翻译器”。模型一升级、厂商一变,翻译器就坏一次。
我做过的某项目里,这种现象导致每次模型迭代都会带出一个新适配器,最终形成一张混乱的适配器网。代码没做错任何事,但它把“模型接入”这件事变成了一堆互相牵连的 if-else,这在产品快速迭代期是致命伤。
1.2 直接在业务代码里做适配为什么走不远
很多人第一反应是写几个封装函数,把不同厂商的调用包一层。这个方案在小规模下没问题,但到一定规模会暴露三个成本:
- 接入成本:每加一个新模型,要重读一遍厂商文档,写一套结构转换、鉴权、重试逻辑。
- 切换成本:想从厂商A切到厂商B,所有调用方都得跟着改。
- 运维成本:线上出了超时、限流、配额不足,你没法在一个地方统一观察。
这三个成本叠加起来,就会出现一种荒唐情况:业务代码里写满了模型厂商的名字,而业务其实根本不需要关心底层是谁。把“模型”抽象成一种可配置资源,而不是散落在各处的 SDK 调用,才是长期方案。
而我用 litellm 的理由就是它已经替我们把这层抽象做好了:对外给统一契约,对内翻译各家方言,中间还能插入生产和运维需要的横切能力。与其自己维护一套半成品网关,不如站在现成引擎上做配置。
2. litellm到底做了什么:统一请求契约,而不是简单套层皮
2.1 SDK模式:把“厂商方言”翻译成同一种语言
litellm 最基本的用法是 SDK 调用,我经常拿来测试连通性。它用一套接近业界通用 Chat/Completions 风格的消息结构发请求:
import litellm response = litellm.completion( model="provider-a/llm-1", api_key="你的密钥", messages=[ {"role": "user", "content": "你好"} ] ) print(response.choices[0].message.content)这里的关键是model参数里的前缀。provider-a/llm-1中的provider-a是厂商标识,llm-1是你在该厂商侧开通的模型名。litellm 识别前缀后,会自动把统一请求翻译成对应厂商的格式。
这个设计最重要的价值是:调用方眼里只有一种消息结构,不再关心厂商内部怎么组织请求。流式、异步、重试、超时这些通用能力也在这一层统一处理。只要有一个能用的密钥,十几行代码就能跑通一个新模型,这在做概念验证时特别爽。
2.2 代理模式:让所有调用方只认识一个端点
SDK 模式适合单服务自己用,但多团队、多服务共享模型资源时,更值得用代理模式。litellm 可以启动一个独立服务,对外暴露统一 HTTP 端点,例如:
/v1/chat/completions /v1/embeddings /v1/models所有调用方只需要把 base_url 指到这个代理地址,把模型名填成你在配置里定义的业务别名。厂商密钥不需要发给每个开发者和服务,而是集中保存。本质上,代理模式是把上一节的翻译能力包装成了一个可独立部署的网关。
这带来的好处远不止统一接口。因为所有请求都从这一个入口经过,你可以在入口加认证、限流、预算控制、日志采集。如果未来厂商D出了更合适的模型,你在配置里改一行,调用方什么都不用动。
2.3 请求之外的能力:认证、限流、预算、可观测
litellm 真正吸引我的不是“能转发请求”,而是它在转发路径上顺手补齐了网关该有的一系列能力。我用一张表来概括:
| 能力 | 作用 | 适合场景 |
|---|---|---|
| 统一认证 | 用主密钥和虚拟Key校验请求 | 多团队共用一个网关 |
| 路由与故障转移 | 同一业务别名可对应多个底层模型 | 厂商故障、模型限流时自动切换 |
| 限流与配额 | 按Key控制每分钟请求数/每分钟令牌数 | 防止单个调用方打爆后端 |
| 预算管理 | 按Key/团队设置最大消费 | 避免月末账单失控 |
| 日志与回调 | 记录每次调用的模型、Token、耗时/失败原因 | 定位线上问题和复盘 |
这套能力如果自己写,每个单项都不难,但合在一起并保持稳定就是另一回事。litellm 把常见模式做成了开箱即用的配置项,省掉的是整整一条网关开发线。
3. 起步三件事:SDK调用、代理模式、第一份config.yaml
3.1 安装与最小调用
第一次尝试,我建议先在本地装好依赖,用 SDK 模式验证厂商密钥是否可用:
pip install litellm安装完成后,用一个最简单的脚本发请求。上面已经写过litellm.completion,这里补几个细节:
api_key可以传明文,也可以从环境变量读,生产环境千万别写死。- 如果厂商支持流式,传
stream=True,返回的生成器可以逐段输出。 - 超时可以用
timeout参数控制,默认行为不一定符合你的业务要求。
我觉得 SDK 模式最适合做“连通性验收”。一次能通过,说明密钥、模型名、网络链路都没问题;如果这一步都失败,后续代理模式大概率也起不来。
3.2 代理一行起来,curl先验收
代理模式才是生产部署的主角。启动方式非常简单:
litellm --config config.yaml --port 4000然后旧调新用,直接打 HTTP 接口:
curl http://localhost:4000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-master-key" \ -d '{ "model": "my-production-model", "messages": [ {"role": "user", "content": "你好"} ] }'注意这里model参数填的不是厂商模型名,而是你配置文件里的model_name。这一点我后面会专门说,因为它是我最初踩得最深的坑。
这样起来之后,任何语言只要能发 HTTP 请求就能用模型,不再受厂商 SDK 限制。前端、后端、脚本、定时任务,统一走同一个端点,这是代理模式最有吸引力的地方。
3.3 config.yaml的骨架与别名机制
litellm 的核心配置都在一个 YAML 文件中。我第一份配置长这样:
model_list: - model_name: my-production-model litellm_params: model: provider-a/llm-1 api_key: os.environ/PROVIDER_A_KEY litellm_settings: drop_params: true num_retries: 3 general_settings: master_key: sk-master-key这段配置说明三件事:
model_list定义了当前网关可用的模型。model_name是业务别名,调用方只能看到这个名字。litellm_params.model是真正发给厂商的模型标识。api_key用os.environ/XXX语法引用环境变量,避免密钥进版本库。
这个“业务别名”机制是整个配置的灵魂。一次迁移时,我只是把litellm_params.model从厂商A改到厂商B,调用方一行代码没动,问题就结束了。对比以前改 SDK 的体验,这种配置化切换堪称治愈。
3.4 SDK和代理不要混着用的原因
既然 litellm 既能当 SDK 又能当网关,很多人会下意识“两个都用”。比如服务里先litellm.completion()一把梭,又觉得应该走代理,于是在 Http 客户端里再拼一次请求。我的建议是:选一条路走。
SDK 模式适合单个服务、少量调用、不需要集中管控的场景;代理模式适合多服务、多团队、需要限流和追踪的场景。两者底层虽然都叫 litellm,但配置路径、鉴权方式、错误语义并不完全一致。混用容易让人误解请求到底从哪里发出去,排障时浪费大量时间。
我现在的做法是:本地实验用 SDK,线上统一走代理。这条规则简单清晰,也方便观察请求是否都经过网关。
4. 生产环境最值钱的三块能力:路由、限流、预算管理
4.1 多路模型与故障转移
单独一个模型接入只是入门,多个模型组成一组才体现网关价值。litellm 支持让同一个model_name对应多个底层模型:
model_list: - model_name: my-production-model litellm_params: model: provider-a/llm-1 api_key: os.environ/PROVIDER_A_KEY - model_name: my-production-model litellm_params: model: provider-b/llm-2 api_key: os.environ/PROVIDER_B_KEY在这种配置下,同样的业务别名后面挂着两套真实模型。litellm 可以根据路由策略选择其中一个,比如随机、最少请求数、按延迟、按用量。我最常用的是故障转移:第一个模型报错或超时,自动切到第二个。
这解决了一个真实场景:厂商A偶尔出现限流,如果请求直接打过去,用户看到的就是一个转圈失败页。有了 fallback,网关先尝试主模型,失败后走备模型,用户体验几乎无感。配置大致是:
litellm_settings: fallbacks: [ {"my-production-model": ["provider-b/llm-2"]} ]路由策略没有绝对最优,关键看业务偏好。赶成本选便宜模型优先,赶体验选低延迟优先,稳定优先就希望主用固定模型。litellm 把这些决策从代码提出来变成了配置项,这本身就是一种降低变更风险的方式。
4.2 限流、配额与虚拟Key
代理建好后,首要问题就是如何防止调用方滥用。litellm 的虚拟 Key 机制很实用:不是一个共享主密钥到处飞,而是为每个团队、每个应用生成独立 Key。
创建虚拟 Key 之后,可以给这个 Key 单独设置每分钟请求数、每分钟令牌数,甚至总预算。请求打过来时,网关先检查这个 Key 是否超限,超了就返回限流错误。这样即使某个团队代码写了个死循环,最多打爆自己的配额,不会拖垮整个网关和上游厂商。
我遇到过最典型的情况是:一个报表脚本因为重试逻辑写错,短时间内发出了几十倍流量。如果没有配额兜底,厂商侧会直接封禁整个账号。加了虚拟 Key 配额后,问题被限制在一个可接受范围内,后续只要修脚本逻辑就行。
提示:虚拟 Key 和主密钥要分清楚。主密钥是管理员用来管理配置、创建 Key 的,虚拟 Key 是给业务调用方用的。不要图省事把主密钥直接给前端或第三方。
4.3 预算追踪与成本告警
大模型应用最大的隐性风险是账单。模型调用是“按 Token 计费”,一次异常流量可能产生无法忽视的费用。litellm 在代理层记录每次调用的模型、Token 数,并把花费累加到对应的 Key 或团队上。
我一般会给核心业务 Key 设置“日预算”和“月预算”,超阈值后直接停用或告警。这样成本问题不再等月末看账单才发现,而是在快超时就被拦住。
预算追踪依赖配置中的计费数据。litellm 内置了一些常见模型的单价映射,但如果用的模型不在库中,务必在模型的litellm_params里补充价格参数。这一步不做,预算数字就只是心理安慰。
5. 我踩过的一些坑:模型名、重试与成本统计偏差
5.1 模型名坑:业务别名和厂商模型名是两套东西
这个坑是我最想强调的。第一次配代理时,我直接在请求里填了厂商模型的原始标识,结果返回“模型不存在”。后来才意识到,代理端点校验的是model_list里的model_name,也就是业务别名;而厂商原始标识只存在于litellm_params.model中。
换句话说,“模型名”这个概念在 litellm 里被拆成了两层。一层是面向调用方的稳定别名,一层是面向厂商的真实名称。调试时如果发现找不到模型,先确认请求参数是别名而不是厂商标识。
这也解释了为什么模型从厂商A切到厂商B时,调用方不需要改代码:因为调用方只认识别名。只要别名不变,底层换几个模型都不影响业务。
5.2 重试策略:别让两层重试叠加成“三倍流量”
litellm 自带重试能力,比如num_retries和退避策略。但同时,你的业务代码里可能也有自己的重试,HTTP 客户端可能也有默认重试。三层重试一叠加,一个小抖动就可能被放大成成倍流量。
我的建议是明确重试责任:网关负责内部的一次重试,业务侧只负责最终失败提示。如果再细分,需要对“哪些错误值得重试”有判断。鉴权错误、请求参数错误这类 4xx 问题,重试一百次也不会成功;限流、超时、5xx 问题,才值得重试。
另外,做针对模型的幂等控制很重要。如果业务请求本身不是幂等的,重试前要考虑会不会重复处理。网关只能帮你转发,不能替你想清楚业务副作用。
5.3 成本统计偏差:预算功能能不能信,取决于计费配置
litellm 的预算功能好用,但默认成本计算不一定准。因为不同厂商返回的usage结构不同,某些模型可能不返回 token 数,某些返回的计费口径和实际账单口径不一致。
我遇到过一次:某个模型在配置里没有对应价格表,网关就按零成本记账,导致预算控制完全失效,月底看到账单才发现。从那之后,每接入一个新模型,我都会先核对两件事:模型名称是否匹配价格表,或者是否已手动填写价格参数。
预算功能是一道防线,不是账本本身。真要核对成本,还是要定期从厂商侧拉账单对比。过去一个月的经验是,把网关日志中的 token 统计导出来,和厂商账单差不太多,但前提是价格配置没有偷懒。
5.4 代理裸奔:不加密钥直接公开端口的结果
litellm 默认启动时如果不配置主密钥,代理会直接开放。开发环境无所谓,一旦暴露到外网,等于任何人都可以借你的代理免费调用模型,最后账单由你买单。
这不是危言耸听,我见过不止一次有人把调试用的代理端口不小心映射到公网,结果几小时内产生大量异常流量。安全配置其实就一行:
general_settings: master_key: sk-你随机生成的强密钥同时配合虚拟 Key 使用。主密钥只用于管理,业务调用一律用虚拟 Key。网关真正对外提供服务之前,至少要先确认没有使用默认配置,这应该成为上线检查单里的一项。
6. 部署形态怎么选:依赖库还是独立网关
6.1 三种部署形态各自的适用场景
我总结下来,litellm 在项目里可以按三种形态落地:
| 形态 | 做法 | 适合场景 |
|---|---|---|
| 内嵌库 | 直接在业务进程里调用 litellm SDK | 单服务、模型少、不需要多团队共享 |
| 独立网关 | 单独部署一个代理服务,业务通过 HTTP 访问 | 多服务、多团队、需要统一治理 |
| 网关集群 | 把代理部署到多个节点,后面挂公共存储 | 高并发、需要横向扩展 |
独立网关是大多数人应该优先考虑的方案。它既保留了统一契约的好处,又能集中管 Key、管预算、管日志,升级模型配置时不用碰各业务服务。
如果流量很大或核心业务对可用性要求高,再考虑网关集群。这时需要把 Key、预算、速率限制的存储从进程内挪到公共存储(比如关系型数据库或内存缓存),让多个网关节点共享状态。
6.2 不建议用litellm的场景
不是所有项目都需要引入 litellm。如果你只有一个模型、一个调用方、对成本和限流没需求,直接使用厂商原版 SDK 反而更简单,少一层代理就少一个故障点。
另外,如果业务有非常特殊的请求流程,比如强依赖厂商独有参数,而且这些参数在 litellm 的标准模型参数里没有对应项,强行套统一契约可能让代码变得更绕。litellm 提供了一些透明传递参数的手段,但这类场景总归是少数,不要为了“统一”而统一。
我的判断标准很简单:两三个模型、一个团队、预算可控,SDK 直接调;模型一多、团队一多、成本要管,就上代理网关。
6.3 我的实践清单与一个小技巧
最后分享一份我现在上线 litellm 时的实践清单:
- 锁版本,不要每次部署都拉最新镜像,兼容性变化容易无声无息。
- 配置文件里所有密钥都用环境变量引用,不进版本库。
- 所有调用方使用虚拟 Key,主密钥只给管理员。
- 预算和限流在接入第一天就配好,不要等出问题再补。
- 日志打开,至少能看到模型名、Key、耗时、成功失败状态。
- 先行跑通 SDK 连通性,再启动代理,不要一步到位。
小技巧有两个,一个关于路由,一个关于调试。路由方面,我习惯把一个“质量较高但贵”的模型和“质量稍低但便宜”的模型放进同一个别名里,日常用低成本模型,高成本模型留作 fallback;这样既控制成本,又保证主模型出问题时体验不会太差。
调试方面,litellm 启动时如果发现请求异常,先把litellm_settings里的日志等级调高,看网关实际转发给上游的请求体是什么。很多时候问题根本不在你的业务代码,而是配置里某个参数没对上。能直接看到“翻译后的请求”比瞎猜高效得多。
我自己的体会是,litellm 这类工具最大的价值不是把代码变少,而是把“模型接入/切换/治理”这件事从每次都要重做的体力活,变成了改一行配置的常规操作。对一个需要不停试验新模型、迭代新能力的团队来说,这种稳定性带来的收益,远比省下的几十行代码重要。