前阵子接手一个内部工具的改造,需求写得很简单:把原先只接一个模型的后端,改成能同时接多家模型、还能自动切换。真动手才发现,光是各家 API 的鉴权方式、返回结构、失败重试、计费口径就够写一个小型 SDK 了。后来我把目光放到 litellm 上,问题一下子变得简单很多。
litellm 是一个用 Python 写的统一大模型接入层,只要在代码里调它,就能用 OpenAI 风格的消息格式去访问几十种不同的模型服务:GPT 系列、Claude、Gemini、本地 Ollama、vLLM 部署的模型都能接。它既能当一个 Python SDK 用,也能以代理服务器的方式帮你部署一个 AI 网关,适合团队协作、成本管理、灰度切换这类场景。如果你正在做多模型集成、模型路由、预算管控,或者只是想少改几套 SDK,这篇文章值得看完。
我会从实际的接入过程讲起,把配置、路由、成本、坑位一个个拆开,尽量说人话,也尽量把参数后面的原理讲清楚。
1. 先看清问题:为什么多模型接口会变成维护噩梦
很多人刚开始接大模型 API 时,都觉得“不就是发个 HTTP 请求吗”。确实,单看任何一家的接口都不复杂,但一旦数量多起来,事情就会失控。
1.1 你以为只是多接一个 API,实际上多了三套逻辑
我见过一个项目,代码里并着三套模型调用逻辑:一套走 GPT 风格的 chat completions,一套走 Anthropic 风格的 messages 接口,还有一套走本地方案。每套都有自己的鉴权头、请求体结构、错误码体系、返回字段命名。到了业务层,你还得写三个转换函数,把它们统一成自己内部的 Message 结构。
这还只是“接进来”的部分。接进来之后还有重试问题:A 家限流返回 429,B 家超时返回 408,C 家直接把连接断开。没有统一封装的话,每种错误都要单独写处理分支。再往后是计费:每家按 token 计价,但 token 的统计口径还不一样,有的按字符、有的按词、有的按消息轮次。真要精细对账的时候,你会发现这些数据根本不是一套 SD 体系能算清的。
这个阶段我最大的感受是:AI 模型本身的推理能力反而不是瓶颈,瓶颈全在“接口开发区”里。你越是想灵活切换模型,越需要一个能把差异吞掉的中间层。
1.2 litellm 切入的方式:统一协议、统一返回、统一参数
litellm 的核心思路不复杂:对外只暴露一套 OpenAI 兼容协议,对内负责把请求翻译成各家模型真正认识的格式。你在代码里构建的 messages 列表,对 litellm 来说是标准输入;它拿到之后,根据你指定的模型名把请求改写成对应服务商需要的结构,再把返回结果统一成 OpenAI 风格的 response 对象。
参数层面也一样。OpenAI 体系的 temperature、max_tokens、stream 这些字段,很多模型服务都支持,但命名略有区别。litellm 做了一个参数映射层,把常见参数标准化。遇到某些模型不支持的参数,可以选择报错,也可以选择自动丢弃,这就对应后面要讲的 drop_params 开关。
鉴权方面,litellm 支持通过环境变量管理 API Key。它内部有一套优先级逻辑:显式传入的 api_key 优先,其次是环境变量,再其次是代理服务器里保存的密钥。你不需要在业务代码里到处散落密钥,集中管理反而更安全。
2. SDK 还是代理:先选对姿势再谈配置
litellm 有两种使用方式,很多人第一次接触时容易搞混。一种是在 Python 项目里直接 import 它当作 SDK,另一种是把它作为独立进程跑起来当作代理网关。两种方式底层都是同一套封装,但适用的架构完全不同。
2.1 最小可用 demo:pip 装完就能在代码里调
如果你是个人脚本、单服务、或者只想快速验证某个模型好不好用,我推荐直接用 SDK 模式。安装很简单:
pip install 'litellm[all]'我一般建议装 all 版本,它会把常见服务商的依赖一起带进来,省得后面报缺失模块。接下来设置环境变量,再写一段调用代码:
import os from litellm import completion os.environ["OPENAI_API_KEY"] = "sk-..." os.environ["ANTHROPIC_API_KEY"] = "sk-ant-..." response = completion( model="gpt-4o", messages=[ {"role": "system", "content": "你是一个简洁的助手"}, {"role": "user", "content": "用一句话解释什么是统一接口"}, ], ) print(response["choices"][0]["message"]["content"])跑完你会发现,返回结构确实和 OpenAI SDK 几乎一样。这就是 litellm 的第一个价值:不管你在 model 参数里填的是 gpt-4o、claude-3-5-sonnet、还是某本地模型的名称,代码里那部分 messages 和 response 的结构都不用变。
2.2 代理模式:把 litellm 变成团队共用的 AI 网关
代理模式是 litellm 真正拉开差距的地方。它本质上是把上面那套 SDK 封装成一个带路由、鉴权、预算管理能力的本地服务,默认监听 4000 端口。你只需要一个配置文件,就能让全团队的代码像调用 OpenAI 官方接口一样调用这个网关,而网关背后可以接任意多家模型。
先写一个简单的 config.yaml:
model_list: - model_name: gpt-4o litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY - model_name: claude-3-5-sonnet litellm_params: model: anthropic/claude-3-5-sonnet api_key: os.environ/ANTHROPIC_API_KEY litellm_settings: drop_params: true num_retries: 2 request_timeout: 60然后在命令行启动:
litellm --config config.yaml --port 4000启动之后,任何能够调用 OpenAI 接口的客户端,只要把 base_url 指到http://localhost:4000,把 API Key 换成你在网关里配置的 master key,就能直接使用。比如用 OpenAI 的 Python SDK:
from openai import OpenAI client = OpenAI( api_key="sk-1234", base_url="http://localhost:4000/v1", ) resp = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "你好"}], ) print(resp.choices[0].message.content)这里有一点需要注意:代理里的 model 参数填什么,取决于你 model_list 里怎么命名,不一定要和上游模型名一致。你可以把 model_name 命名为任意字符串,团队内部只认这个名字,后面想换模型时只改网关配置,业务代码一行都不用动。
2.3 怎么选:看你的调用方有几个
如果调用方只有一个 Python 服务,SDK 模式足够,还少一层网络开销。但如果调用方是多个后端服务、定时任务、数据分析脚本、甚至前端直连,那我就强烈建议上代理模式。统一网关之后,密钥管理、调用审计、预算控制、模型灰度都能在一个地方完成,而不是散落各处。
代理模式还有一个隐性优势:底层模型配置对于业务团队是黑盒。业务方只知道 model 名叫“gpt-4o”,不知道网关后面其实已经悄悄把流量切了一部分到其他模型。这种解耦在做模型切换演练时非常有用。
3. 代理模式的配置细节:读懂 model_list 的组合逻辑
代理模式是所有高级功能的基础,所以值得把配置讲细一点。很多人拿到 config.yaml 就照抄,出了问题不知道怎么排查,归根结底是没有理解 model_list 的结构。
3.1 model_name 与上游模型的关系
在 config.yaml 里,model_name 是你对外暴露的逻辑名,litellm_params 里的 model 才是真正的上游模型标识。比如:
model_list: - model_name: my-assistant litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY - model_name: my-assistant litellm_params: model: anthropic/claude-3-5-sonnet api_key: os.environ/ANTHROPIC_API_KEY注意这里我写了两个条目,model_name 都是 my-assistant,但上游分别指向 GPT 和 Claude。这就是 litellm 做负载均衡和故障转移的入口:多个上游可以共用一个逻辑名,网关会按策略选择到底把请求发给谁。
实际配置时,litellm_params 里除了 model 和 api_key,还能放 api_base、organization、temperature 等参数。如果你接的是内部部署的 vLLM 服务,经常需要指定自定义的 api_base。如果是 Ollama,本地默认地址是 8000,你甚至不需要 api_key。
3.2 环境变量、master_key、虚拟 Key 的权限层次
代理启动时需要一个主密钥,用法是在请求头里带Authorization: Bearer sk-1234。这个 master key 拥有全部管理权限,可以创建新的“虚拟 Key”。虚拟 Key 是给团队里的不同成员、不同服务使用的,可以分别设置额度、限速、过期时间。
从安全角度讲,我不建议所有人都拿 master key。之前在项目里吃过亏,有人拿主密钥去调管理接口,不小心把一个模型配置改了,整组服务半夜开始报错。后面我把虚拟 Key 机制用起来,团队里每个人一个 key,最小权限原则立刻落实。
创建虚拟 Key 最简单的方式是直接调管理接口:
curl -X POST http://localhost:4000/key/generate \ -H "Authorization: Bearer sk-1234" \ -H "Content-Type: application/json" \ -d '{"budget": 20}'返回结果里会带上新的 key 和 key 的 ID。这个 key 可以限制总预算、每分钟请求数、每分钟 token 数,足够做到比较细的管控。
3.3 OpenAI 客户端接入时最容易错的三个地方
跑通代理模式之后,大部分人会在接入环节翻车。我总结出三个高频点。
一是 base_url 忘加/v1。OpenAI 官方 SDK 默认会在 base_url 后面拼接口路径,比如 chat/completions 实际请求的是{base_url}/chat/completions。如果你只填http://localhost:4000,它不会像我示例里那样带上/v1,因为 litellm 的兼容端点监听在/v1/...。正确写法是http://localhost:4000/v1。
二是自定义模型名没有出现在可用模型列表里。你要先确认 config.yaml 里写的 model_name 和调用代码里填的 model 完全一致,包括大小写和特殊符号。代理启动日志里通常会显示已加载的模型列表,先看一眼再调试,能省不少时间。
三是上游密钥写错但代理不报错。因为代理只是转发,它把请求发到上游之后,如果上游返回 401,你收到的错误信息可能比较含糊。排查时先直接 curl 一下上游接口确认密钥本身有效,再确认代理配置里的 api_key 是从哪个环境变量读进来的。
4. 故障转移与负载均衡:从单点调用变成冗余调度
做多模型接入的最终目的,通常不只是“能用”,而是“坏了也能用”。模型服务是外部依赖,任何一个服务商都可能限流、断连、报 5xx。litellm 的 Router 和代理配置都支持把多个上游绑到同一个逻辑名上,让请求自动分发。
4.1 同一个逻辑模型挂多个部署
故障转移的前提是同一个 model_name 对应多个 litellm_params 条目。比如:
model_list: - model_name: main-llm litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY model_info: id: gpt-primary - model_name: main-llm litellm_params: model: anthropic/claude-3-5-sonnet api_key: os.environ/ANTHROPIC_API_KEY model_info: id: claude-fallback如果你通过 SDK 的 Router 对象来使用,也可以把 model_list 直接喂给 Router:
from litellm import Router router = Router(model_list=model_list, routing_strategy="usage-based-routing") response = router.completion( model="main-llm", messages=[{"role": "user", "content": "测试"}], )这里的核心是:调用方始终只认逻辑名 main-llm,网关自己决定把请求发给哪个上游。当某个上游连续失败达到阈值,它会被自动标记为冷却状态,后续请求改走其他可用上游。
4.2 路由策略的取舍:不止随机轮询
litellm 提供了多种路由策略,常见的有:
- simple-shuffle:简单打乱请求分配,适合上游能力差不多的场景。
- least-busy:按当前未完成的请求数分配,适合上游响应速度差异较大的场景。
- usage-based-routing:按每个上游过去一段时间的 token 用量占比分配,适合想控制成本占比的场景。
- latency-based-routing:偏向当前延迟更低的上游,适合对响应时间敏感的场景。
我实际用的最多的是 usage-based-routing,尤其是接了多个价格差异明显的模型时。比如想把 70% 的流量给便宜模型、30% 给高质量模型,就可以通过 cost 配置和策略参数来影响分配权重。
不过要注意:路由策略不是越复杂越好。least-busy 和 latency-based-routing 都需要代理具备持续观测能力,在小流量场景下统计噪声很大,反而会造成抖动。先想清楚你到底要优化什么,再选策略。
4.3 重试与冷却参数:阈值设得太小反而容易误伤
litellm_settings 里有两个参数经常被忽略:allowed_fails 和 cooldown_time。
router_settings: routing_strategy: usage-based-routing allowed_fails: 3 cooldown_time: 60 num_retries: 2allowed_fails 表示一个上游连续失败多少次后进入冷却;cooldown_time 表示冷却多少秒。这两个值我一开始按官方默认的 2 次和 30 秒设,结果发现生产环境里某个模型偶尔一次超时就导致它被标记成故障,紧接着大量请求被切到另一个模型,造成另一个模型也跟着限流。后来把 allowed_fails 调到 3、cooldown_time 调到 60,情况才稳定。
另外要注意,失败不单单指 HTTP 5xx。超时、连接重置、上游返回内容格式异常,都可能被计算为失败。如果你把 request_timeout 设得太小,比如 10 秒,而某个复杂任务本身需要 40 秒,那么正常请求也会被当成失败。超时值和 allowed_fails 必须一起调,否则故障转移机制会反过来制造新故障。
5. 成本追踪与预算护栏:多模型网关的必修课
大模型调用不像传统 API 那样计价清晰,token 消耗和单价是动态的。多人共用一个网关后,成本会快速变复杂。litellm 在成本管理上做得比较完整,但前提是你得先理解它的计费逻辑。
5.1 litellm 是怎么算钱的
litellm 本身不产生账单,它只是把每次调用的输入输出 token 数乘上对应模型的单价,算出一个估算成本。模型单价来自内置的模型信息表,大部分主流模型的每百万 token 价格都有维护。如果你用的是内部部署模型或自定义模型,也可以自己指定 price 信息,让成本估算更准确。
在代理模式下,每次请求完成后,litellm 会记录元数据:用户、模型、输入 token、输出 token、估算成本、延迟、上游状态。这些记录可以通过管理接口查询,也可以通过回调推到外部系统。我在部署时习惯把成功回调接到日志平台,这样每个调用都有完整审计链路。
5.2 用虚拟 Key 给团队划预算
虚拟 Key 最实用的地方就是预算隔离。某团队要跑实验、某业务要上线、某同学只是临时测试,都可以各发一个 key,分别设置 budget。
举个例子,给测试环境创建一个总预算 50 美元的 key:
curl -X POST http://localhost:4000/key/generate \ -H "Authorization: Bearer sk-1234" \ -H "Content-Type: application/json" \ -d '{ "key_alias": "test-env-key", "budget": 50, "max_parallel_requests": 10, "model_max_budget": {"main-llm": 30} }'budget 是总预算上限,model_max_budget 可以针对单个模型再设上限。当某个 key 的用量达到上限,代理会直接拒绝后续请求并返回 429。这个机制用来防止无人看守的定时任务半夜跑到天亮,非常有效。
预算快到上限时,litellm 还可以通过回调发告警,比如在 budget_remaining 低于某个阈值时调用 webhook。你可以把告警接到内部消息机器人,实现“花到八成就报警”。
5.3 把用量接到现有可观测体系里
litellm 的成本和请求日志不是只能留在它自己数据库里。在 litellm_settings 里加上回调,就能把数据推给外部系统:
litellm_settings: success_callback: ["prometheus"]如果你用 Prometheus 那套体系,litellm 可以直接暴露指标端点。如果团队更习惯用日志平台,也可以配置自定义回调,把每次请求的 dict 原样写入日志流。这样做的最大好处是:网关上的现象和业务侧的调用日志能关联起来,排查问题时不用两头切。
我当时就是把 spend 日志和业务日志打到同一个采集目录,用 trace ID 关联。出了问题,先看业务侧是哪次调用,再去 spend 日志里翻对应模型、对应 key、对应 token 数,整个链路非常清晰。
6. 踩坑复盘:参数、并发、超时这些隐藏问题
最后集中说说我实际踩过、也帮别人排过的坑。这些点官方文档都写过,但一般不到报错时刻不会真正重视。
6.1 drop_params 是省心助手,也可能成为静默杀手
drop_params=true 的含义是:如果某个参数上游模型不支持,litellm 会自动把它从请求里丢掉,而不是报错。好处显而易见,同一套参数可以安全地发给不同模型。坏处也很明显:你会失去报错提醒,某些业务关键参数可能被悄悄忽略。
比如你同时接了一个支持 reasoning_effort 的模型和一个不支持的模型。开启 drop_params 后,不支持的模型那边不会报错,但它也不会执行你期望的推理控制,输出的质量可能和你预期的差一大截。如果你对参数有强要求,宁可让调用失败也不要静默降级。我的建议是:可以先开着跑通流程,等业务稳定后关掉,让异常暴露出来。
6.2 异步与并发:Router 对象不是默认并发安全的
litellm 提供异步接口:acompletion、arouter.completion。如果你的服务是 FastAPI 这类异步框架,却直接在线程池里调同步方法,很容易把事件循环卡住。正确做法是使用异步版本,并且用信号量限制并发,防止突发流量把上游压垮。
import asyncio from litellm import Router router = Router(model_list=model_list) sem = asyncio.Semaphore(20) async def ask(messages): async with sem: return await router.acompletion( model="main-llm", messages=messages, )并发数不宜直接拉满。大模型推理是资源密集型服务,上游通常有 RPM/TPM 限制,你在网关侧把并发压得太高,收到的 429 会比成功响应还多。
6.3 代理本身的超时与健康检查
代理模式跑一段时间后,可能会遇到“网关没问题但请求卡住”的情况。常见原因是某个上游长时间不响应,而请求没有设置超时,连接被一直占用。务必在 litellm_settings 里显式设置 request_timeout,同时配合 num_retries,避免一个慢请求把所有 worker 拖死。
如果你是 Docker 部署,还要记得给容器设置健康检查,探测/health/liveliness或/health/readiness接口。编排平台会根据健康检查自动重启异常实例,否则代理进程僵在那里你只能手动救。
6.4 密钥优先级:显式 api_key 覆盖环境变量
litellm 读取密钥的优先级是:litellm_params 里显式写的 api_key 优先于环境变量。这个看起来理所当然,但配置多了以后容易出现“凭据张冠李戴”的情况。比如你明明在 config 里写了 A 家的 key,但环境变量里也有一个 A 家的 key,而且格式一样,你很难判断实际生效的是哪个。
排查方法是启动代理时加 verbose 日志,或者临时在配置里删掉显式 api_key,再看行为是否变化。如果行为没变,说明之前生效的是环境变量里的那一个。
对我个人来说,litellm 真正省下的不是第一次接入的时间,而是后续每次“换模型”都只需要改网关配置的从容。多模型并存这件事,做一两次是临时方案,做多了就会变成基础设施。如果你正在从单模型走向多模型,或者被团队里五花八门的调用方式搞得头疼,可以先用一套最小配置把网关跑起来,再逐步把成本、预算、故障转移这些能力加进去。后面你会发现,真正重要的从来不是某个特定模型有多强,而是你切换模型时的代价有多低。