最近后台和群里频繁有人问同一个词:litellm。问法大概都是“我们团队要接好几个模型,代码里全是if/else,能不能用这个”“LiteLLM到底是库还是服务”“听说它能自动切换模型,是真的吗”。我去年在团队里把整个AI调用层从手写适配改成了LiteLLM代理,前后踩了小半年的坑,今天把这件事一次讲透。
简单说,LiteLLM是一个开源的LLM网关和统一接口层,它把市面上一大堆模型供应商的API全部翻译成同一种通用接口风格,暴露给调用方。你只需要用一套代码发请求,由它负责路由、重试、限流、成本统计这些脏活。这篇文章适合正在纠结要不要上统一网关的团队,也适合那些已经被多模型接入搞得头大、想找一套省心方案的技术负责人。我会从真实踩坑出发,讲清楚它解决的问题、核心能力、落地配置,以及哪些坑必须避开。
1. 为什么需要LLM网关:一个接入痛点到一套方案
1.1 多模型接入的真实战场
在没上LiteLLM之前,我们团队接三个不同来源的模型,代码状态惨不忍睹。每家供应商的鉴权方式不一样,有的要请求头里塞自定义前缀,有的要换专用SDK;消息格式也有微妙差别,上一家把系统提示放在顶层字段,下一家非要放到数组里;错误码更是千奇百怪,有的限流了返回429,有的却用一个200包裹着错误提示,导致客户端完全感知不到服务异常。
这种局面最坑的不是写适配代码,而是每次模型升级、字段变更或者供应商调价,都可能导致线上服务炸一圈。需求从“帮我把某模型的响应速度调快一点”变成“我们要切到另一家模型,你评估一下”。实际上模型切换的成本大部分不在模型本身,而在所有围绕旧API写好的日志、监控、重试、限额逻辑。下表是我整理过的典型差异点,真实项目里只会更复杂:
| 差异维度 | 供应商A风格 | 供应商B风格 | 开源本地模型 |
|---|---|---|---|
| 鉴权方式 | Header自定义密钥 | SDK内置客户端 | 无鉴权或Token |
| 请求体结构 | 顶层放系统消息 | 统一消息数组 | 变化最多,各有各的怪 |
| 可用参数 | 支持温度、频率惩罚 | 参数名不同 | 部分参数不支持 |
| 限流反馈 | 标准状态码 | 包装后的错误体 | 直接断连或超时 |
| Token计费 | 自带统计字段 | 需要自己数Token | 取决于部署形态 |
可以这么说,多模型接入不是“多一个请求分支”的问题,而是把一套原本简单的调用逻辑拖进各家供应商的兼容深水区。代码里那些看似高级的适配函数,本质上是手写了一个不够格的网关。
1.2 LiteLLM的定位与核心价值
LiteLLM解决的就是上面这个深水区问题。它有两种使用形态:一种是Python库,你直接调用它的completion()函数,由它完成各家API的转换;另一种是代理服务,你把它当成一个网关部署在身边,所有调用方都请求它的HTTP接口,再由它转发给真正的模型供应商。
它的核心价值可以拆成四件事。第一,统一接口层:不管后端是闭源模型、开源模型还是本地部署模型,对外都暴露同一种请求格式,调用方不需要关心供应商是谁。第二,路由与容灾:同一个别称可以绑定多个实际模型,某个限流或挂掉时自动切换,还能按权重做负载均衡。第三,成本治理:每次请求的模型、Token消耗、费用都会被记录,支持按API Key分配预算。第四,密钥收敛:真正的供应商密钥只存在网关这层,业务方拿到的是一把虚拟Key,泄露了随时吊销,不碰底层账户。
用一个生活化的类比:以前每家公司都要直接去拉不同的水管,自己处理水压、水质、计费;LiteLLM就是社区建好的统一供水站,你只需要关心水龙头流出来的水是不是够用,水管另一头是谁你不用管。一句话总结它的定位:LiteLLM = 一个服务网关 + 一套客户端库 + 一张模型供应商适配表。
2. LiteLLM核心能力拆解:统一接口、路由容灾与成本控制
2.1 统一接口:把通用标准格式变成团队通用语言
LiteLLM对外采用业界最流行的Chat补全接口风格,也就是现在几乎所有模型厂商都在兼容的那种请求格式:一个messages数组,里面塞role和content。这样做的好处是生态成熟,很多现成框架、工具、客户端脚本都能直接复用。
先看一段使用LiteLLM Python库的代码:
from litellm import completion response = completion( model="provider-a/model-1", messages=[ {"role": "user", "content": "你好,帮我总结一下今天的会议纪要"} ], )这段代码看起来平平无奇,但它有一个隐藏优势:假如某天你不想用provider-a/model-1了,想把所有流量切到provider-b/model-2,你只需要把model参数换掉,调用代码一行都不用改。如果是在代理模式下,你连这一行都不用改,因为调用方永远只知道一个别名。
实际用下来,统一接口真正省钱的地方不是写起来方便,而是团队协作的边界清晰了。后端不用关心你接了几家模型,前端不用为了一个流式字段的差异做特殊处理,测试环境可以直接把模型替换成低成本版本。很多同事觉得“模型供应商”是后端的事,上了LiteLLM之后,它变成了配置项,而不是代码里的硬逻辑。
2.2 模型路由、重试与降级:让错误请求自动切换
模型是有脾气的高可用组件,但它比普通数据库更让人头疼——你无法预判它什么时候超过并发上限、什么时候因为上游服务波动直接超时、什么时候因为内容安全策略返回不可用的结果。LiteLLM在路由层面把这些事接住了。
它提供一个Router对象,可以同时管理多个模型配置。正常调用时代码长这样:
from litellm import Router router = Router(model_list=[ { "model_name": "primary-model", "litellm_params": { "model": "provider-a/model-1", "api_key": "key-a", }, }, { "model_name": "primary-model", "litellm_params": { "model": "provider-b/model-2", "api_key": "key-b", }, }, ]) response = router.completion( model="primary-model", messages=[{"role": "user", "content": "路由测试"}], )这里最核心的设计在于model_name相同,但litellm_params不同。LiteLLM会把所有配置了同一个model_name的模型视为一组,默认按照顺序或权重做负载均衡。当第一个模型连续报错、超时或者触发限流时,它会自动把请求转向同组里的下一个模型,业务方感知不到发生过切换。
我建议团队在配置层面至少保留一主一备两个模型。主模型负责日常调用,备模型可以做成本稍低但也能干活的替代。这里有一个容易踩的误区:不要把重试和降级混为一谈。重试是同一个请求再打一次原模型,主要应对瞬时抖动;降级是换一个模型处理,主要应对持续性故障。LiteLLM的fallback机制处理的是降级,重试参数单独有配置,别指望fallback帮你解决超时瞬断。
2.3 成本追踪与预算控制:每一分钱花在哪
绝大多数团队对AI成本的管理方式,是月底收到账单才发现“怎么超了这么多”。LiteLLM把成本追踪前移到了每一次请求。默认情况下,代理会把每条请求的模型名、Token数、估算金额、请求来源写入数据库,你随时可以查“昨天哪个应用调了多少次,花了多少钱”。
代理模式下,虚拟API Key体系提供了比裸厂家Key更好的控制力。你可以给每个团队、每个项目或者每个用户发一把独立的Key,然后这样配置预算:
general_settings: master_key: sk-master-123 database_url: sqlite:///litellm.db在这套配置下,你通过代理的/key/generate接口生成一把子Key,同时指定budget为100,那么这把Key累计消耗达到100后,所有请求会被直接拒绝,直到你手动提升额度。这个能力对我的价值主要在两方面:一是防线上程序出错时疯狂刷Token,预算兜底能自动熔断;二是方便按月拆分成本,不用在财务面前一笔一笔翻日志。
3. 实操过程:从零搭建LiteLLM代理并接入客户端
3.1 安装与环境准备
搭建代理服务不需要太复杂的环境,一个能跑Python的机器就行。官方推荐用虚拟环境隔离,避免污染系统Python。我自己习惯用venv:
python3 -m venv .venv source .venv/bin/activate pip install -U "litellm[proxy]"安装时要注意两点。第一,一定要把[proxy]后缀带上,否则只装库,不带代理服务相关依赖。第二,别直接在开发环境裸装最新版,生产环境最好锁定版本号,LiteLLM迭代速度很快,有些小版本之间的配置格式会变,锁版本能救你一次。
接着把各模型供应商的API密钥配置成环境变量。不要把密钥写死在配置文件里,否则一提交代码就泄露。我维护一个env文件,启动服务时手动加载:
export KEY_A="sk-xxxx" export KEY_B="sk-xxxx"这里有个细节:LiteLLM的配置读取环境变量时,习惯上使用os.environ/KEY_NAME这种写法,你在config.yaml里对应字段写os.environ/KEY_A就行。这样配置文件里看不到任何明文密钥。
3.2 配置文件的核心参数解析
安装完成后,核心工作就是写一份配置文件。以下是一份最小可用的config.yaml,所有厂商名称和模型名我已经替换成示意,实际使用时换成你对应的标识:
model_list: - model_name: primary-model litellm_params: model: provider-a/model-1 api_key: os.environ/KEY_A - model_name: primary-model litellm_params: model: provider-b/model-2 api_key: os.environ/KEY_B - model_name: cheap-model litellm_params: model: provider-c/tiny-model api_key: os.environ/KEY_C litellm_settings: drop_params: true set_verbose: false general_settings: master_key: sk-master-123 database_url: sqlite:///litellm.db分开解释几个关键字段。model_list是核心,每个条目包含对外名称model_name和真实配置litellm_params。model_name是暴露给业务方的名字,它不一定等于厂商模型ID。比如你在代码里调primary-model,代理会自动在provider-a/model-1和provider-b/model-2之间做路由。litellm_params.model才是真正请求上游时用的完整模型标识,格式是“供应商/模型ID”。
litellm_settings.drop_params解决的是一个经典问题:不同模型支持的参数不完全一致。比如你给模型A传了frequency_penalty,模型B不支持,如果开启drop_params,代理会把这个参数静默丢弃而不是报错;不开启则直接失败。生产环境强烈建议打开。
general_settings.master_key是管理代理的主密钥,生成子Key、查看成本、修改配置都需要它。database_url用来持久化成本记录和Key信息,本地测试可以用sqlite:///litellm.db,生产环境一定要换到独立的网络数据库,不然并发一高SQLite性能就成瓶颈。
配置文件准备好后,一条命令启动:
litellm --config config.yaml --port 4000看到Uvicorn running on http://0.0.0.0:4000就说明代理已经就绪。
3.3 客户端接入:把代理当普通API来用
代理启动后,它就有了一个通用的HTTP接口。你不需要再引入任何专用SDK,用普通的HTTP请求库就能调通。下面是一个requests调用的最小示例:
import requests response = requests.post( "http://localhost:4000/chat/completions", headers={ "Authorization": "Bearer sk-1234", "Content-Type": "application/json", }, json={ "model": "primary-model", "messages": [{"role": "user", "content": "写一封请假邮件"}], "temperature": 0.7, }, ) print(response.json())如果你不想直接写HTTP请求,也可以继续用LiteLLM的Python库,只是把api_base指向代理地址:
from litellm import completion response = completion( model="primary-model", api_base="http://localhost:4000", api_key="sk-1234", messages=[{"role": "user", "content": "写一封请假邮件"}], )这段代码里,调用方只知道一个primary-model别名,不知道后端到底用的哪家模型。以后就算你把primary-model指向一个完全不同的模型供应商,业务代码依然不需要动。这就是网关层的价值:把变化收敛在配置里,而不是散落在各处代码里。
4. 团队落地避坑指南:常见问题、部署经验与适用边界
4.1 高频问题排查速查表
我在实际使用中收集了几条出现频率极高的问题,先给一张速查表:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 401 Unauthorized | 供应商Key没配置或配置错 | 检查环境变量名和litellm_params.api_key |
| 模型不存在 | model_name或真实模型标识拼写错误 | 核对model_list里的完整标识 |
| 请求超时 | 上游供应商整体变慢 | 给路由配置合理超时时间,触发fallback |
| 参数报错 | 某个模型不支持传入的参数 | 开启drop_params: true |
| 成本没记录 | 数据库没配或连接不上 | 检查database_url,确认数据库可写 |
| 频繁切换模型 | fallback配置过宽 | 区分瞬时错误和持续错误,收窄降级条件 |
这里特别提醒一个隐蔽问题:当你把同一个model_name配置了多个实际模型时,默认负载是轮流分发,不是“第一个优先”。如果你想让某个模型作为主力、另一个只兜底,一定要研究一下路由策略的strategy字段,设置成优先级模式或权重模式,否则会出现“备用模型和主模型承担相同流量”的意外。
4.2 部署与性能经验
生产环境最好把LiteLLM跑成一个独立服务,由进程守护工具托管,而不是在交互式终端里开着。我用过比较顺的方式是systemd托管,配置模板大致如下:
[Unit] Description=LiteLLM Proxy After=network.target [Service] User=litellm WorkingDirectory=/etc/litellm ExecStart=/usr/local/bin/litellm --config /etc/litellm/config.yaml Restart=always RestartSec=5 EnvironmentFile=/etc/litellm/env [Install] WantedBy=multi-user.target这里有个安全细节:EnvironmentFile指定的文件权限要收紧,建议设为600,只允许运行用户读取,防止密钥泄露。
性能方面,我会提三个重点。第一,不要开set_verbose: true,它会打印非常详细的上游请求和响应体,日志量巨大,还可能在日志里带出业务数据。第二,并发高的场景建议在上游模型接口前设置合理的超时时间,避免请求全部堆积在代理层。第三,SQLite只适合单机小流量,一旦你的代理服务开始承载多个业务方,就要尽快把数据库换成真正的网络版数据库来存成本记录和虚拟Key,否则并发写入会拖垮整体响应。
4.3 什么时候不该上LiteLLM
不是所有团队都适合立刻上代理层。如果你只用一个模型供应商,项目是内部小工具,就没必要引入额外组件。多一层代理就意味着多一个故障点,也让延迟多一跳,这个代价换不回多少收益。
如果你的应用对单次请求延迟极其敏感,比如每一次调用都要在100毫秒内返回,那中间网关带来的开销可能无法接受。虽然LiteLLM本身的纯转发开销很小,但加上网络往返、日志写入、数据库记录之后,局部抖动会被放大。
还有一种我经常看到的情况:团队没有专职运维,却硬要部署一个复杂的代理,结果配置文件写错、数据库连不上、日志没人看。这时候更务实的做法是先用LiteLLM的Python库,只在服务端代码里完成统一调用,不启动代理服务。等团队有一定运维能力和多供应商需求时,再平滑迁到代理模式。
5. 进阶玩法:预算配额、自定义回调与智能切换策略
5.1 按团队拆分预算与配额
代理跑起来以后,你会收到一把master_key。下一步建议不要把这把主Key发给所有开发,而是通过代理的/key/generate接口生成若干子Key,每个子Key绑定自己的预算、模型范围、过期时间。
curl -X POST "http://localhost:4000/key/generate" \ -H "Authorization: Bearer sk-master-123" \ -H "Content-Type: application/json" \ -d '{ "budget": 50, "models": ["primary-model", "cheap-model"], "expires": "2024-12-31T23:59:59Z" }'这段请求会返回一把类似sk-abc123的子Key。团队A用sk-abc123,团队B用sk-def456,月底在数据库里按Key聚合就能看到各团队消费。最重要的收益是,当某个测试脚本不小心进入死循环,预算耗尽自动熔断,主账户不会跟着受害。
这里有一个容易被忽略的权限点:子Key能调用哪些模型是受控的。你可以在生成Key的时候限定models数组,防止某个调用方误用高价模型。我们当时就把“读操作”和“写操作”分成了两套Key,写操作Key只允许调用便宜的模型,极大降低了成本失控风险。
5.2 自定义回调与监控集成
成本记录满足不了所有监控需求,LiteLLM提供了回调机制,可以在请求成功或失败时触发你自己的处理器。常见的用途包括:把调用日志写入内部业务系统,或者根据失败率触发告警。
下面是一个最小回调实现:
from litellm.integrations.custom_logger import CustomLogger class MyCallback(CustomLogger): def log_success_event(self, kwargs, response_obj, start_time, end_time): # 这里拿到的是代理转发的原始请求和响应 model = kwargs.get("model") latency = (end_time - start_time).total_seconds() print(f"模型调用成功: {model}, 耗时: {latency:.2f}s") def log_failure_event(self, kwargs, response_obj, start_time, end_time): print("模型调用失败,进入排查流程")定义完回调类后,把它挂到全局:
from litellm import callbacks callbacks = [MyCallback()]我自己习惯把失败事件单独汇总到一个消息队列,再定时推送给告警系统。这样不用等用户反馈,就能尽早发现某个上游模型开始不稳。相比自己写一层拦截器,回调机制的好处是你不需要改动任何业务代码,纯观测逻辑和转发逻辑完全解耦。
5.3 基于成本的智能切换策略
成本数据攒一段时间后,就能做更精细的路由决策。LiteLLM支持在同一个模型别名下配置多个模型,并给它们分配不同权重。你可以把高成本模型权重调低、低成本模型权重调高,让大部分流量走便宜方案,只有当便宜方案无法满足需求时,才让少量流量落到高质量模型。
我实际跑过一个策略:平时业务请求都走成本低的轻量模型,只有涉及复杂推理的少量请求才走高质量模型。配合每条请求记录的成本数据,一个月下来整体费用比原来单一用高质量模型省了大约三成,效果上大多数用户感知不到差别。
做这个策略的另一个关键是设置好模型能力边界。不要把需要复杂意图理解的任务盲目切到便宜模型上,否则省下的钱会变成客服投诉和返工成本。比较稳妥的办法是先切10%流量观察半个月,对比成功率和用户满意度,再逐步加大切换比例。
用了一年多LiteLLM,我最大的体会是解决多模型接入问题不能靠堆适配代码,而是要早点把“接入”变成“配置”。但我也踩过“功能铺太快”的坑——一开始就把所有能力都打开,结果光排配置问题就花了一个星期。如果你现在正准备上这套方案,我的建议是先统一入口,打开路由和成本记录,跑通两个模型的主备切换,其他高级功能等数据攒够了再逐步加。这套顺序走下来,团队的适应成本会低很多。