最近半年我一直在折腾多模型接入的事。团队里业务方时不时提需求:这个场景想用某厂商的中文模型,那个场景想用另一家的长上下文模型,还有一批自部署的本地小模型要给内部工具用。一开始我图省事,直接在业务代码里写 if-else,每个服务商一套 SDK、一套密钥,环境变量越堆越多,等到月底一看账单,连哪些功能花了多少钱都拆不出来。后来我把整条链路改成了 litellm 统一代理,总算把这块烂摊子收拾干净了。
这篇东西不是官方文档的翻译,而是我结合自己的部署和使用体验,把 litellm 最核心的价值、真正的实战配置和容易踩坑的地方讲清楚。如果你也在做 AI 应用开发,正被多个模型 API 的接入、密钥、成本问题搞得头大,那这篇文章应该能帮你省下不少试错时间。
1. LLM 接口接入的乱象,以及代理层解决的根本问题
1.1 为什么直接在代码里接多家模型 API 会越来越痛苦
先说一个很现实的问题:做 AI 应用,几乎不可能只接一家模型服务。有的模型擅长中文写作,有的模型上下文窗口特别长,有的模型在成本上有优势,还有些内部数据不能出内网,必须用本地部署的开源模型。于是代码里就会出现一堆“根据不同业务场景切换到不同模型”的逻辑。
听起来好像也不难,真正动手才发现处处都是坑。每个厂商的接口地址不同,鉴权方式不同,请求体字段命名不同,返回结构也不同。一家要求用某种方式传系统提示词,另一家可能用的是数组格式;一家的错误信息是标准 JSON,另一家可能给你返回一个不透明的错误码。SDK 也各不一样,为了接入三家模型,你可能要引入三套客户端库,维护三套初始化逻辑,再写一堆适配层把返回结果统一成自己内部的对象。
更麻烦的是密钥管理。真实项目的密钥不能硬编码在代码里,只能用环境变量或者配置中心,但环境变量一旦多了,维护起来就是灾难。再加上不同密钥可能属于不同项目、不同预算额度,谁用了哪个密钥、产生了多少费用,全部要靠人工记录。我见过最夸张的情况是,一个查询功能因为配置的密钥额度耗尽,下午三点开始报错,工程师用了一个小时才定位到是密钥问题,因为日志里只显示了一个含义模糊的错误码。
这种复杂度会随着模型数量线性增长。每加一个模型,业务代码、密钥、监控、成本统计全都要跟着改。等到改了五六家之后,你会发现自己大部分时间不是在写业务逻辑,而是在做接口翻译和密钥搬运工。
1.2 litellm 的本质:一个能路由、翻译、记账的 LLM 网关
litellm 解决这个问题的思路,和支付网关、短信网关很像:它不生产任何模型,只是在你的业务服务和上游模型之间加了一层代理。业务侧只需要和这一个代理打交道,然后由代理去和真正的模型提供方通信。
这样一来,原来“业务代码直接对接 N 家模型服务”的结构,就变成了“业务代码只对接 1 个代理,代理再对接 N 家模型服务”。多对多的混乱关系被收敛成了多对一,复杂度被统一收口到了代理这一层。
用生活里的例子类比:以前你出差要去好几个不同城市,每个城市都得自己找当地的出租车司机,讲当地的方言,用当地的付款方式;litellm 相当于给你安排了一个总调度中心,你只要告诉调度中心目的地,剩下买票、转车、付款都是后台的事。
litellm 本身是一个开源项目,Python 写的,支持命令行启动,也支持作为 Python 包嵌入到现有服务里。它最核心的部分是一个路由层,当你发一个请求过去,它会根据配置决定把请求转发给哪个上游模型,把请求格式翻译成上游能理解的格式,收到响应后再翻译回你熟悉的样子。同时,它还顺手做了密钥管理、预算控制、成本统计、负载均衡和故障转移,这些能力对生产环境来说甚至比“统一格式”更重要。
2. litellm 的核心能力拆解:路由、密钥、预算、审计一个都不少
2.1 模型路由与别名机制:业务侧只认一个“模型名”
litellm 最基础也最实用的功能就是模型路由。在配置文件里,你可以定义一张模型列表,给每个真实的上游模型起一个自己喜欢的别名。业务侧发起请求时,只需要关心别名,完全不用知道真实的上游模型是什么。
比如我在配置里可以这样写:把某个厂商提供的聊天模型命名为internal-chat,把另一个厂商的中文增强模型命名为cn-chat,把一个本地部署的轻量模型命名为light-local。业务代码里调用的时候,model字段就填internal-chat或者cn-chat,litellm 收到之后会自动映射到真实的上游模型并完成调用。
这个机制带来的好处非常直接:如果某一天你觉得当前这个上游模型不行了,想换成另一个厂商的同类模型,只要改 litellm 的配置,把internal-chat对应的上游模型换掉即可,业务代码一行都不用动。这种解耦能力在高频迭代的 AI 应用里特别值钱,因为模型市场变化太快,今天的好模型可能三个月后就变得不划算了。
我在实际使用中通常还会把相同能力的模型配置成多个上游实例,比如同一个厂商的两个不同区域接口,或者不同厂商但能力接近的两个模型,全部挂到同一个别名下面。这样 litellm 在转发时就可以做自动分配和故障切换,业务侧完全无感。
2.2 负载均衡与故障转移:多个密钥和多个实例的自动调度
一个很容易被忽略的细节是,当你调用量上来之后,单一密钥很可能会碰到厂商侧的速率限制。最原始的办法是申请多个密钥,然后在代码里自己写一个轮询逻辑,定期检查哪个密钥快超限了,再切换过去。这套逻辑写起来非常啰嗦,而且容易出错。
litellm 把这件事做进了代理内核。你可以在配置里为同一个模型声明多个 deployment,每个 deployment 可以有自己的api_key、api_base、权重等参数。litellm 会根据你设定的策略(比如轮询、随机、最少连接等)把请求分配到不同的 deployment 上。
更关键的是故障转移。如果某个 deployment 因为密钥额度耗尽、服务商故障或者网络波动导致请求失败,litellm 会自动把当前请求重试到下一个可用的 deployment,而不是立刻把错误返回给业务方。配合重试次数和退避策略,整个链路的可用性会明显提升。
我在生产环境里把同一边上的两个模型服务商配成了同一个别名下的两个 deployment,结果其中一个下午发生了持续半小时的故障,业务侧几乎没察觉到任何异常,只有我自己从监控面板上发现那半小时的流量全部自动切到了另一个厂商。如果没有这层代理,这种故障是没法做到用户无感的。
2.3 虚拟密钥与预算阀值:把真实密钥从业务环境里“赶出去”
过去业务服务要调用模型,就得在服务的环境变量里配置真实的上游密钥。这样带来两个问题:一是密钥暴露面太大,任何一个同事拿到了服务器权限就能看到;二是没法细粒度控制某个业务方到底能用多少额度。
litellm 引入了“虚拟密钥”的概念。你可以在这个代理里创建多个虚拟密钥,每个虚拟密钥可以关联特定的模型、特定的速率限制、特定的预算上限。然后把虚拟密钥下发给你内部的不同系统,这些系统只需要知道 litellm 的地址和虚拟密钥,完全不需要接触上游的真实密钥。
这个设计非常符合权限管理的最小化原则。具体来说,我给内部数据组下发了一个虚拟密钥,限制只能调用本地模型;给对外服务下发另一个虚拟密钥,限制每天最多消耗一定费用,超过就自动拒绝。这样就算某个虚拟密钥泄露了,影响范围也是可控的,不会牵连到所有上游资源。
预算控制也是我很喜欢的功能。你可以在虚拟密钥上设置max_budget,按美元、按请求数或者按 token 来限制。一旦超过阈值,litellm 会直接拒绝新的请求并返回一个明确的原因,方便审计。这个能力对于防止“半夜一个 bug 把千万 token 烧光”这种事故非常有效。
2.4 成本追踪与调用审计:从一团乱账到按项目分摊
在接入 litellm 之前,我们每个月的模型费用都只能看厂商后台的账单,完全不知道是哪条业务线、哪个功能、哪次调用产生的。不同团队做成本分摊的时候只能靠估,非常憋屈。
litellm 在代理层天然可以看到每一次调用的完整信息:Request ID、模型名、虚拟密钥标识、提示词 token 数、返回 token 数、总耗时、花费估算、以及你自定义的 Metadata 标签。你可以通过这些数据做多维度的成本分析。
我一般会在请求里带上metadata: {"team": "search", "scene": "summary"}这样的标签,之后在 litellm 的查询接口里按team分组,就能算出每个团队、每个场景各花了多少钱。还能看出哪个模型的性价比最低、哪个模型的错误率最高,这些数据对后续模型选型非常有参考价值。
如果你已经有了监控体系,litellm 还支持把日志和指标推送到一些外部存储或监控平台。不过这一步不用一上来就做,先用它自带的管理端把账单看明白了,再去折腾对接也不迟。
3. 从零部署一个 litellm 代理服务:完整可复现的步骤
3.1 环境准备与安装
litellm 是 Python 包,建议用独立的虚拟环境或者容器来跑,别直接装在宿主机系统里,省得依赖冲突。我本地的常用做法是创建虚拟环境,然后用pip安装。
python -m venv llm-proxy source llm-proxy/bin/activate pip install "litellm[proxy]"这里[proxy]表示安装带代理服务所需的额外依赖。如果网络访问不了 PyPI,你可以在 Docker 里部署,litellm 也有官方容器镜像,直接映射端口就能用。我推荐至少装完以后锁定版本,别用latest,因为这种工具迭代很快,升级可能导致配置格式变化。
装完以后可以验证一下版本号:
litellm --version能正常显示版本号,说明安装成功了。
3.2 编写基础配置文件
litellm 的代理配置文件一般用 YAML 格式。最核心的字段是model_list,它定义了所有可供代理调用的模型集合。下面是一个最简配置的示例:
model_list: - model_name: "internal-chat" litellm_params: model: "provider-vendor/chat-pro" api_key: "sk-real-upstream-key" api_base: "https://api.example-vendor.com/v1" rpm: 100这里要特别注意两个字段的区别:model_name是暴露给业务侧的别名,你可以随便起,但要保证唯一;litellm_params里的model才是真实上游的模型标识,格式一般是“厂商前缀/模型名”,不能写错。api_key是上游真实密钥,api_base是上游接口地址,rpm表示这个部署每分钟最多接受多少请求,用来做本地限速。
如果你想把两个密钥都挂到同一个别名下面做负载均衡,只需要在model_list里写两个条目,model_name相同,litellm_params里分别写不同的密钥和资源。litellm 会自动认为它们是同一个模型的多个 deployment。
还有一块常用配置是litellm_settings,可以放一些全局参数,比如通用超时时间、重试次数等。这些后面生产场景再展开。
3.3 启动代理服务并用 curl 验证
配置文件写好后,启动服务只需要一行命令:
litellm --config config.yaml --port 4000启动成功后,日志里会显示监听地址。接着我用curl快速验证一个最简单的对话请求:
curl http://localhost:4000/v1/chat/completions \ -H "Authorization: Bearer sk-my-virtual-key" \ -H "Content-Type: application/json" \ -d '{ "model": "internal-chat", "messages": [{"role": "user", "content": "你好,简单介绍一下你自己"}], "max_tokens": 50 }'这里sk-my-virtual-key是先临时设置的虚拟密钥。如果你还没有创建虚拟密钥,也可以用--master-key参数在启动时指定主密钥,主密钥默认拥有所有权限。响应如果正常,应该是一个标准的补全对象,里面包含choices、usage等字段。
从这一步开始,代理就已经具备转发能力了。你可以在model字段里换上其他你在配置里定义的别名,逐一验证路由是否正常。
3.4 业务侧接入:改一个 base_url 就够了
litellm 对外暴露的接口格式完全兼容目前市面上最常见的那套模型服务规范。也就是说,如果你的业务代码已经用了那个标准的 Python 客户端,接入 litellm 基本不需要改代码,只要改base_url和api_key。
from your_model_sdk import Client client = Client( api_key="sk-my-virtual-key", base_url="http://localhost:4000" ) response = client.chat.completions.create( model="internal-chat", messages=[{"role": "user", "content": "写一封简短的请假邮件"}] ) print(response.choices[0].message.content)原来业务代码里声明的多个客户端可以直接删掉,只保留这一套。以后要加新模型,在代理配置里加一条记录就行,业务侧连重新部署都不需要。我自己的项目里大概花了一个下午就把所有调用方切换到了代理,后续新增模型的时间从“改一天代码”降到了“改十分钟配置”。
4. 生产环境实战:负载均衡、预算策略与监控告警的配置级讲解
4.1 多上游实例的负载均衡与故障转移配置
在生产环境里,单密钥单实例通常撑不住真实的流量波动。litellm 支持在同一个别名下配置多个上游实例,形成一个资源池。下面是一个双供应商高可用的配置示例:
model_list: - model_name: "production-chat" litellm_params: model: "vendor-a/chat-large" api_key: "key-a-1" api_base: "https://api.vendor-a.example.com" rpm: 200 weight: 2 - model_name: "production-chat" litellm_params: model: "vendor-a/chat-large" api_key: "key-a-2" api_base: "https://api.vendor-a.example.com/backup" rpm: 200 weight: 1 - model_name: "production-chat" litellm_params: model: "vendor-b/chat-pro" api_key: "key-b-1" api_base: "https://api.vendor-b.example.com" rpm: 100 weight: 1 router_settings: routing_strategy: "simple-shuffle" num_retries: 2 retry_after: 2.0 timeout: 60.0这里有几个参数需要解释。weight控制流量分配的比例,权重越大接收的流量越多;routing_strategy是路由策略,simple-shuffle表示随机但按权重分配;num_retries表示单个请求失败后的最大重试次数;retry_after是重试前的休眠时间;timeout是整体请求超时时间。
这样配置以后,如果 vendor-a 的第一个密钥突然被限流,litellm 会自动把重试请求发到第二个或者 vendor-b 上。你不需要在业务代码里做任何判断,只要业务侧等待时间允许,代理就会尝试用备选资源完成这次调用。
我在设置重试参数时有一个教训:num_retries不是越高越好。如果上游服务本身已经过载,大量重试反而会放大压力。所以我把num_retries控制在 2 左右,并且把timeout调成与业务可接受的延迟上限匹配,避免用户等待太久。
4.2 按用户、按项目做预算和限流
预算这块,litellm 的控制粒度非常灵活。你可以在全局设置总预算,也可以在虚拟密钥上单独设置预算,还可以通过team/user维度做区分。
我在内部系统里比较常用的组合是这样:
litellm_settings: max_budget: 2000 max_budget_per_user: 100 max_parallel_requests: 50 rpm_limit: 200max_budget是代理整体一个月的总预算,达到之后所有请求都会被拒绝,相当于总闸;max_budget_per_user是单个用户的上限,避免某个测试脚本意外消耗大量费用;max_parallel_requests限制全局最大并发请求数;rpm_limit限制每分钟总请求数。
创建虚拟密钥时,还可以把某个密钥限定在某个模型或某个成本范围内。比如我给一个“批量摘要任务”下发密钥时,限制了这个密钥max_budget: 10,并且只允许访问production-chat这一个模型。那边任务跑歪了也不会烧穿预算。
预算被触发后,litellm 返回的错误信息里会带着原因,比如BudgetExceeded,这比我们自己写一堆 if-else 判断要省事得多。
4.3 日志、指标与监控告警的落地思路
litellm 自带一个管理页面和查询接口,你可以看到每次调用的历史记录。不过生产环境里更重要的是一套自动化的检查机制,总不能每天都去翻页面看数字。
比较轻量的做法是写一个定时脚本,每隔几分钟调用 litellm 的查询接口拿最近的调用统计,按模型和虚拟密钥聚合,然后接到自己的告警体系里。我在项目中用 Python 写了一个小任务,每天早上跑一次,拉取前一天的消耗总金额和每个模型的成功率,如果某个模型成功率低于阈值,就推送一条预警消息。
如果你愿意把日志落到数据库里集中查询,litellm 也支持配置日志回调,可以把每次调用的详细记录同步到你的数据表里。这适合需要做长期成本趋势分析或者用量预测的场景。不过初期不用做太重,先保证能看到报错日志和费用日志,把监控闭环跑通比追求全面的指标更重要。
一个特别要提醒的点:日志级别别用DEBUG跑生产。DEBUG会把请求体里的原始内容打出来,如果用户输入里有敏感信息,那这些信息就全部留在日志里了。我一般用INFO,既能记录关键的模型调用标识、耗时、结果状态,又不会输出完整的请求和响应内容。
5. 真实部署中的坑与化解方案
5.1 模型别名和上游模型名搞混,导致路由失败
这是我第一次部署时最容易踩的坑。看到配置里既有model_name又有litellm_params.model,有时候一顺手就会在model_name里面填上游真实模型名,然后在请求里也用真实模型名。结果看起来配置没问题,可实际请求时 litellm 会告诉你找不到这个模型。
原因很简单:litellm 把model_name作为对外路由的唯一标识。真要校验它存不存在,应该在配置里检查,而不是查上游模型列表。
我的习惯是:所有model_name统一使用小写字母加连字符,比如internal-chat;所有真实上游模型名只出现在litellm_params.model字段里。配置文件的顶部写清楚注释,哪些是业务别名,哪些是厂商标识,避免团队其他人改的时候搞混。排查路由问题时,第一步就是看日志,确认最终发给上游的实际model字段到底是什么。如果日志里显示的是别名而不是真实模型名,那基本就是配置写反了。
5.2 长上下文请求超时,业务方收到一堆报错
第一次把 litellm 接到一个知识库问答场景时,用户上传一篇长文档让模型总结,结果请求过去不到 30 秒就被代理断开。我们业务侧的等待时间拉得很长,仍然不断失败。
原因在于 litellm 默认的超时配置比较保守,而长上下文模型的响应时间往往超过这个阈值。尤其是不用流式输出的请求,必须等模型把完整回复都生成完才返回,时长很容易超出默认值。
解决办法是在router_settings里调大timeout,同时在业务侧也要相应调整等待时间。如果产品允许,更推荐启用流式输出,让用户看到的是一段一段生成的内容,而不是干等。流式的情况下,代理只需要保证连接不中断,对总耗时的容忍度会高很多。
我在配置里通常会把timeout设为 60 秒,个别超长文本场景甚至设置到 120 秒。注意这个值是代理到上游的等待时间,不是业务侧的整体超时,两边要匹配好,否则业务侧先超时了也是白搭。
5.3 流式输出(SSE)在代理层断流或“卡住”
流式调用是另一个高频问题。有个同事反馈,用 litellm 代理访问某个提供流式输出的模型,客户端经常收到一半就断流了,而且没有任何错误信息。
检查下来有两个原因。第一是代理层对 SSE 流的缓冲处理,如果代理或中间的网关做了数据缓冲,就可能导致用户收到的第一个数据包特别晚,看起来像卡住了。这种情况需要关闭代理层的数据缓冲,或者确认 litellm 是即时转发流式数据块。
第二个原因更隐蔽:上游模型在流式输出过程中如果遇到内容审核或内部错误,会先发一个错误的数据块,再中断连接。如果代理把连接直接掐断,业务侧只能看到“空手而归”,误以为网络问题。
处理方案是,在业务侧捕获流式回复时,要对 stream 过程中的异常事件做显式处理;如果在代理日志里看到错误码,需要把这个错误码透传到业务侧,而不是吞掉。litellm 本身对流式的支持是好的,但你不能假设所有上游服务商的中途错误行为都正常,得自己做好兜底日志。
5.4 切换上游模型后,提示词格式出现兼容性问题
一个很容易被忽略的坑是:你以为同一个别名换到新的上游模型,所有东西都会照常工作,但实际上不同厂商对提示词的支持细节差异很大。
比如有的上游支持系统提示词用字符串数组,有的只支持字符串;有的对max_tokens字段的限制范围不同;有的会把temperature映射到完全不同的采样逻辑。如果你不关心这些细节,切换后极有可能出现“响应质量明显变差”或者“直接报字段错误”。
我现在的做法是,在代理配置里把同一类模型的别名按能力做分区:chat-standard、chat-summary、embedding-upgrade等。每个别名内部要求上游模型的能力对齐。另外,在业务代码和模型之间再加一个轻量提示词适配层,专门负责把公共的提示词格式转换成不同模型更合适的格式。这个适配层原来是系统里最混乱的部分,但放到 litellm 后面之后,反而成了很干净的一层,因为上游只有 litellm 一个入口。
5.5 密钥和敏感信息泄露的风险点
最后聊聊安全。litellm 把大量密钥集中到一个配置文件里,这既是优点也是风险点。优点是好管理,缺点是如果配置文件泄露,上游密钥全都会暴露。所以你要把配置文件的权限收得很紧,不要提交到代码仓库,也不要放到公开的配置中心里。
我自己的习惯是:虚拟密钥和上游密钥全部用环境变量引用,配置文件里只留os.environ/AZURE_API_KEY这样的引用。启动服务的时候从密钥管理服务里去读取真实值,这样即使有人拿到配置文件,看到的也不是明文密钥。
另外,虚拟密钥也需要定期轮换。litellm 的管理界面可以创建和吊销虚拟密钥,我一般每三个月轮换一次,对于涉及外部客户的场景会缩短到一个季度。日志里也检查过很多次,确保没有真实密钥出现。这种代理工具本身就是安全边界,别让边界内部先漏了水。
这个内容后续还可以这样扩展:如果你们团队有多个模型网关的需求,可以在 litellm 上面再做一个统一的路由层,或者用它的查询 API 搭一个成本看板。我个人在实际操作中的体会是,先别急着把所有模型一股脑全接上来,挑两个最重要的场景迁移到代理上,把负载均衡、预算控制和日志跑顺了,再逐步扩大覆盖面。这样既不会让线上风险突然变大,也能最快感受到它带来的收益。