1. 为什么接入十几家模型前,我会先搭一个统一调用层
1.1 从一次“多模型竞标”需求说起
去年我接手了一个多模型对比项目,需求本身不复杂:把几款主流大模型接入到同一个评分系统里,让它们跑同一批客服问题,再对比回答质量。刚开始我以为这事就是分别调几个 SDK,真正做起来才发现,难点根本不在模型能力,而在接入层。
第一个坑是每家 SDK 的返回结构都不一样。A 模型给的是data.choices[0].message.content,B 模型给的是data.content[0].text,C 模型可能还要先走一层鉴权换取临时 token。业务层想写一个通用的"取文本"函数,光是兼容这些结构就写了一堆 if-else。第二个坑是错误处理,有的模型限流返回 429,有的返回 400,有的干脆把错误信息藏在响应体深处,排查问题全靠肉眼。第三个坑是成本,等我把几家模型都接完才发现,想统计每个用户每次请求花了多少钱,得自己拼 token 用量和价格表。
后来我把这套重复劳动总结了一下:任何一个多模型项目,80% 的工作都花在"适配"上,而不是"调用"上。于是我开始找现成的抽象层,最后停在了一个叫 LiteLLM 的开源项目上。它给我的第一印象很朴素:把不同模型的请求和响应都转成 OpenAI 风格。但就是这一层转换,把我上面说的三个痛点一次全解决了。
1.2 LiteLLM 到底做了什么
LiteLLM 的核心能力说穿了不复杂:你给它一个标准的 OpenAI 风格请求,它会翻译成目标模型真正认识的格式发出去;目标模型返回后,它再翻译回 OpenAI 风格的结构还给你。模型名带上前缀,比如openai/gpt-4o、anthropic/claude-3-5-sonnet、ollama/llama3,它就知道该走哪条适配链路。
这里有一个容易误解的点:LiteLLM 不是一个"模型聚合市场",它不托管模型,也没有自己的模型。它更像一个翻译层或网关,负责把你的请求安全地转发给上游模型服务。真正发请求的仍然是各家模型的 API,只是你不再需要关心它们各自的协议细节。
它有两种主力形态:一是直接当 Python 库用,代码里调用litellm.completion(),适合写脚本、做后处理、跑评测;二是起一个本地代理服务,对外暴露一个 OpenAI 兼容的 HTTP 接口,适合团队共享一个入口,统一管理密钥、成本、限流。我在项目里两种形态都用了,先直接用 SDK 把链路跑通,再上代理让同事一起用。下两节就按这个顺序展开。
2. SDK 模式:最快把 100+ 模型变成 OpenAI 风格接口
2.1 安装与第一条 completion 请求
先装依赖,这一步没什么悬念。官方分发到 PyPI,直接 pip 拉就行:
pip install litellm如果你要跑代理模式,建议带上 proxy 扩展,后面会用到 Flask 相关的依赖:
pip install "litellm[proxy]"装完之后,配环境变量。各家模型的密钥最好都放在环境变量里,而不是硬编码到代码中:
export OPENAI_API_KEY="sk-..." export ANTHROPIC_API_KEY="sk-ant-..." export GEMINI_API_KEY="..."然后写第一段调用代码,你会发现和 OpenAI 官方 SDK 几乎一样:
from litellm import completion resp = completion( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是一个乐于助人的助手。"}, {"role": "user", "content": "你好,简单介绍一下你自己"}, ], ) print(resp.choices[0].message.content)如果要把模型换成 Claude,只需要把model参数改成claude-3-5-sonnet-20240620,其他代码一行都不用动。这就是 LiteLLM 最直接的价值:业务代码与具体模型解耦。后面哪怕把整个项目从某家模型切换到另一家,核心逻辑也不会受影响。
2.2 统一返回、流式输出和异步调用
统一返回结构是 LiteLLM 最让我舒服的地方。无论上游是哪个模型,返回的对象都长成 OpenAI 的样子:choices[0].message.content是正文,usage.prompt_tokens是输入 token,usage.completion_tokens是输出 token。这样下游写解析逻辑时只需要认准一套字段。
需要流式输出时也很直观,加一个stream=True即可:
resp = completion( model="gpt-4o-mini", messages=[{"role": "user", "content": "写一首十五字的诗"}], stream=True, ) for chunk in resp: delta = chunk.choices[0].delta.content if delta: print(delta, end="", flush=True)异步场景用acompletion,签名基本一致:
from litellm import acompletion resp = await acompletion( model="claude-3-5-sonnet-20240620", messages=[{"role": "user", "content": "异步调用示例"}], )在 FastAPI 这类异步框架里,我建议优先用acompletion,它能避免阻塞事件循环,高并发下更稳。
2.3 重试、超时和降级配置
模型 API 的稳定性远不如传统数据库,限流和超时是常态。LiteLLM 把重试和降级做成了配置项,省掉了我原来自己写的一堆重试装饰器。
import litellm # 全局重试 3 次,每次间隔递增 litellm.num_retries = 3 litellm.request_timeout = 60 # 主模型失败时降级到备用模型 litellm.fallback_models = ["gpt-4o-mini", "claude-3-5-haiku-20241022"]这里有一条经验:降级模型尽量选和主模型能力接近的,否则业务会因为结果质量差异产生波动。我最早把降级模型设成gpt-4o-mini,从 Claude 降级过来后,部分评测场景的评分波动很大。后来改成换厂商但定位相似的模型,波动明显小很多。
如果你只想对单次请求重试,不要设置全局参数,直接传 kwargs:
resp = completion( model="gpt-4o-mini", messages=..., num_retries=2, timeout=30, )优先级上,单次请求的 kwargs 会覆盖全局配置,这个顺序在实际排查问题时很重要。
3. 代理模式:一个进程管好全团队的模型路由
3.1 先用一行命令把代理跑起来
SDK 模式解决的是"我自己写得爽",代理模式解决的是"全团队用得爽"。当项目从一个人扩展到多个人,或者有多个服务都要接大模型时,每个服务都配一遍密钥、都实现一遍重试逻辑,就很浪费。
LiteLLM 代理模式本质上是一个本地 HTTP 服务,把 OpenAI 风格的请求转发到各个上游模型。启动方式很简单:
litellm --model gpt-4o-mini --port 4000然后任意语言都能用 OpenAI 的 SDK 访问它:
from openai import OpenAI client = OpenAI( base_url="http://localhost:4000/v1", api_key="sk-any-string", ) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "走代理测试"}], )注意这里api_key随便填都可以绕过本地检查,所以后面一定要加 master key,不能让代理裸奔在局域网里。
3.2 config.yaml 才是代理的灵魂
单模型启动只是快速验证,真正要管理多个模型,必须用配置文件。LiteLLM 代理启动时支持--config config.yaml,把模型列表、重试策略、密钥、预算全放在一个文件里,团队可以直接维护。
下面是我常用的最小配置:
model_list: - model_name: gpt-4o-mini litellm_params: model: openai/gpt-4o-mini api_key: os.environ/OPENAI_API_KEY - model_name: claude-sonnet litellm_params: model: anthropic/claude-3-5-sonnet-20240620 api_key: os.environ/ANTHROPIC_API_KEY - model_name: llama3-local litellm_params: model: ollama/llama3 api_base: http://localhost:11434 litellm_settings: num_retries: 3 request_timeout: 60 fallback_models: ["gpt-4o-mini"] general_settings: master_key: sk-123456启动:
litellm --config config.yaml --port 4000这里有几个细节容易踩坑。第一,model_name是你给业务方看的别名,可以随便起;litellm_params.model才是真正的上游模型标识,必须带对前缀。第二,os.environ/OPENAI_API_KEY是在配置里声明"从环境变量读密钥",这样密钥不会明文写入配置文件。第三,fallback_models里填的是model_name别名,不是上游模型名,别写反了。
3.3 多模型负载均衡和密钥分发
同一个model_name可以配置多个 deployment,LiteLLM 会帮你在它们之间做路由。比如你有两个 OpenAI 账号,都想接gpt-4o-mini,配置可以写成:
model_list: - model_name: gpt-4o-mini litellm_params: model: openai/gpt-4o-mini api_key: os.environ/OPENAI_API_KEY_1 - model_name: gpt-4o-mini litellm_params: model: openai/gpt-4o-mini api_key: os.environ/OPENAI_API_KEY_2代理收到gpt-4o-mini的请求后,会按负载均衡策略选一个上游转发,默认偏向选择当前在途请求更少的那一个。这样做的收益很直接:两个账号的限流可以互相缓解,成本也能分散。
密钥分发方面,配置文件里的general_settings.master_key是所有管理操作的通行证。之后你可以在数据库里创建多个子 key,分发给不同团队或应用。每个子 key 可以绑定不同的模型可见性、不同的预算上限。举个例子:A 团队只能用gpt-4o-mini,每月预算 100 美元;B 团队可以用所有模型,每月预算 500 美元。这个能力在多人协作时非常省心,省得每个人自己拿主密钥到处用。
3.4 生产部署时最容易忽略的环境配置
开发环境里litellm --config config.yaml一把梭就能跑,但生产环境会遇到几个隐蔽问题。
第一个问题是密钥没传进去。我用 systemd 部署时,明明.env里有OPENAI_API_KEY,代理却一直报 401。排查半天才发现 systemd 默认不会加载 shell 里的环境变量,必须在 service 文件里显式指定EnvironmentFile或者在启动命令前 source。这是生产环境最常见的低级错误。
第二个问题是日志。默认日志输出到 stdout,系统重启后历史日志就没了。建议至少配一个日志滚动,或者让 systemd 的 journal 保留足够长时间,方便事后排查。
第三个问题是进程守护。直接nohup跑代理,一旦服务器重启就断了。我见过同事手里的代理跑了好几天,看起来正常,其实内存已经涨到了好几个 GB,因为没人看护也没有重启策略。正经做法是用 systemd 的Restart=on-failure,或者直接容器化部署。
第四,如果你有内网 Docker 环境,把代理做成镜像也方便。启动命令还是那一条,只是把配置文件和密钥用挂载卷传进去,不要在镜像里烤死密钥。
4. 成本统计与限流:把每个请求的账单记清楚
4.1 内置价格表与自定义计费参数
多模型项目最容易被忽视的就是成本。刚开始大家只关心效果,等模型接多了才发现,每个月钱都烧在了哪里根本说不清。LiteLLM 内置了一份主流模型的 token 单价表,你调完一个接口,它可以按模型名和 token 用量直接估算费用。
对内置表里没有的模型,你可以自己在配置里补:
model_list: - model_name: my-local-model litellm_params: model: openai/my-local-model api_base: http://localhost:8000/v1 api_key: dummy model_info: input_cost_per_token: 0.000001 output_cost_per_token: 0.000002input_cost_per_token和output_cost_per_token是美元单价。如果内部模型不计费,填 0 也行。这里建议把自定义模型的成本参数全部显式配置出来,否则 LiteLLM 不知道该怎么算。
4.2 从响应里把 cost 捞出来
在 SDK 模式下,没有比直接看响应更快的成本统计方法。LiteLLM 提供了一个简便函数:
from litellm import completion, completion_cost resp = completion( model="gpt-4o-mini", messages=[{"role": "user", "content": "帮我总结一下这段话"}], ) cost = completion_cost(completion_response=resp) print(cost)这个函数会读取响应里的 token 用量,结合模型单价算出一个浮点数。返回的resp对象里还有一个_hidden_params字段,里面经常带着response_cost等原始信息,写日志时可以直接兜底消费。
代理模式下,请求通过网关转发后,日志会自动记录每次调用的模型、token 用量和估算成本。你不需要在业务侧写任何埋点,只要保留代理日志,月底拉出来就是一份粗粒度的账单。
4.3 按团队、按应用做预算上限
如果只是看账单,那还只是事后止损。LiteLLM 代理还支持提前限流:你可以给某个 API key 设置单日最大花费或最大请求数,超了之后代理直接拒绝调用,从源头控制成本。
做法是在数据库里维护 key 的预算字段,或者在管理后台给每个 key 设置 spending limit。具体字段名不同版本略有差异,但核心逻辑一致:每个 key 对应一个 budget,每次请求成功后记账,超出预算返回 403。这个能力对 SaaS 类产品很关键,它可以防止某个用户刷爆整个项目的模型额度。
我个人的建议是:正式上生产之前,先把每个 key 的预算上限配置好。哪怕只是设一个"相对宽松但存在"的上限,也比没有强。成本失控这种事情,通常不是慢慢涨上来的,而是某个循环或者某个测试脚本一次性打出来的。
5. 踩坑实录:从 401 到流式断连,我修复了什么
5.1 401 与模型名不对:先查配置文件
我在跑代理模式第二天就遇到了 401,最让人崩溃的是配置看起来都对。后来逐项排查发现,litellm_params.api_key用的是os.environ/OPENAI_API_KEY,但启动代理的进程里根本没有这个环境变量。也就是说,配置声明"从环境读",但环境里压根没写进去。
这个问题的排查链路其实很典型。第一步,先确认环境变量是否真的存在,终端里echo $OPENAI_API_KEY,能打印出来才算有。第二步,看代理进程是从哪个目录启动的,.env文件是不是在当前工作目录下,LiteLLM 是否会读取它。第三步,如果上一步不稳定,就在启动命令里显式 export 一遍,测试环境可以用,生产环境再收敛到 systemd 配置。
另一个高频报错是模型名不合法。比如直接传llama3而不是ollama/llama3,LiteLLM 会默认走 OpenAI 兼容协议去请求,结果上游地址不对,返回 404 或者模型不存在的错误。这类问题看日志最有效率,日志里会显示它最终请求的 URL,一眼就能看出前缀是不是配错了。
5.2 流式返回拿不到 token 用量
流式输出体验确实好,但它会带来一个副作用:默认情况下,流式响应的最后没有 token 用量汇总。没有usage字段,成本计算就无从谈起。
我当时满心以为completion_cost能直接算出来,结果传进去返回 None,一度以为模型不计费。后来翻文档发现,要在流式请求里显式开启用量返回,OpenAI 风格是stream_options={"include_usage": True},LiteLLM 对流式请求也可以透传这个参数:
resp = completion( model="gpt-4o-mini", messages=..., stream=True, stream_options={"include_usage": True}, )开启后,流式最后一个 chunk 会带上 usage 信息。注意解析时要做判空,不是每个 chunk 都有usage,只有最后一个才有。如果一开始就打算做成本统计,我建议所有流式接口都把这个参数打开,不然后面补数据非常痛苦。
还有一个相关坑:有些模型厂商的流式协议根本不放 usage,LiteLLM 也无能为力。这种情况下只能用模型输出的 token 数量自行反推,或者直接按请求日志估算,精度差一些但好过没有。
5.3 高并发下的连接池和超时策略
代理本身是 Python 进程,在单机上做几十路并发没问题,但我要同时给多个内部服务推量时,出现了两个现象:一是延迟变高,二是偶尔出现Connection reset by peer。这类问题多半不是 LiteLLM 本身挂了,而是上游连接没有被合理复用,或者超时设置不合理。
处理思路大概分两层。第一层,在 LiteLLM 配置里适当调大request_timeout,但不要无限大,我一般设在 60 秒左右。超过 60 秒还没返回的请求,继续等下去价值也不大。第二层,确保代理进程有足够的资源,不要和其他重型服务挤在同一个容器里。实测下来,4 核 8GB 的配置扛住一个小团队的路由转发是够的,前提是网络带宽正常。
另外,如果你的上游是自建 vLLM 或 Ollama 这类本地服务,别把它们的地址配错。用ollama/llama3这种前缀时,api_base要指到 Ollama 的端口,一旦配错,代理日志会一直出现连接失败。这里的排查方法就是看日志中的目标 URL 是否正常。
5.4 排查链路:从 DEBUG 日志到最小复现
碰到问题不要瞎猜,直接把 LiteLLM 的日志级别拉到 DEBUG,重复发一次请求就能看到完整调用链:请求体长什么样、转发到了哪个 URL、上游返回了什么、在哪一步报错。
LITELLM_LOG=DEBUG litellm --config config.yaml --port 4000日志看不到问题时,我会把请求体保存下来,做一次"最小复现"。就是绕过代理,直接用同一个 messages 和 model 参数,通过 SDK 模式请求上游模型。如果 SDK 模式正常而代理模式异常,问题大概率在代理配置;如果 SDK 模式也异常,那就是上游模型或密钥的问题。这个二分法能帮我快速缩小范围。
我遇到过一次奇怪的现象:同一个请求,用 OpenAI 官方 SDK 直接调用没问题,但走 LiteLLM 代理就报参数校验错误。后来发现是业务方在请求里带了某个非标准字段,官方 SDK 会悄悄忽略它,而 LiteLLM 的代理会严格透传,导致上游报错。解决方案是在配置里打开drop_params,让代理把不认识的参数丢给上游的自己处理:
litellm_settings: drop_params: true这类兼容性问题没有统一答案,只能靠日志和最小复现逐步排除。但万变不离其宗:先分清是接入层问题还是上游问题,再对症处理。
6. 什么项目我推荐 LiteLLM,什么项目我会劝退
用了大半年,我对它的边界也有了更清楚的认识。如果你的项目同时要接入两种以上不同厂商的大模型,或者团队里多人、多服务都要调用模型,那么 LiteLLM 几乎是为这个场景量身定做的。统一返回结构、重试降级、成本统计、密钥管理,这些不是锦上添花,而是绕不开的基建。
反过来,如果项目只使用唯一一家云厂商的模型,而且短期内没有换的打算,那我觉得没必要引入这一层,直接用官方 SDK 更轻。LiteLLM 毕竟多了一层转发,任何一个抽象层都不可能做到零损耗,能少一层就少一层。
还有一类场景我不太推荐:你的业务对响应时间极其敏感,所有请求都要在 100 毫秒内返回。代理转发会带来一点点额外网络开销,在极致性能追求下,这个开销可能不可接受。这种情况下,直接用官方 SDK 并发调用,或者干脆基于更底层的协议自建精简网关,也许更合适。
最后分享一个我自己的使用习惯:在开始写业务代码之前,先把模型的接入方式定死,不要让业务代码里到处散落着具体的模型名。哪怕经过 LiteLLM 统一之后,模型名仍然会出现在请求参数里。最好在业务系统中做一层模型别名映射,比如业务侧只传smart、fast、cheap,具体映射到哪个模型由配置决定。这样以后换模型、切换厂商,就不再是一个代码改动,而是一次配置变更。
LiteLLM 的价值从来不是"让你用某一个模型更简单",而是"让你在永远不确定要给哪个模型加量的时候,不用重写业务代码"。这一点,等你的项目真的需要接第二个模型时,会深有体会。