最近一年我一直在折腾 AI 应用的集成,最大的感受是:只用一个模型的时候,日子很舒服;一旦你想在多个模型之间切换、对比和迁移,麻烦就成倍增长。各家接口长得都很像,但细节差异能让你改一天代码。我最后选了 Litellm 这个开源工具把这些事统一掉,现在无论接 OpenAI、Anthropic、Gemini,还是本地部署的 vLLM、Ollama,在代码里都只是一套调用方式,在服务端只需要一个代理网关就能全部消化。这篇文章把我从原理拆解、落地步骤到生产环境踩过的坑都整理出来,适合准备做多模型接入、自建统一网关的开发者参考。
1. 当要接的模型超过两个,"格式地狱"就开始了
1.1 各家 API 差异,比想象中大得多
先说一个真实的场景。你的业务要接入三个模型:一个主流闭源模型,一个开源模型,一个本地私有化模型。单纯看官方文档,每个都写得清清楚楚,一旦把代码放一起,问题就出来了。
最常见的差异就是鉴权方式。某个模型服务用Authorization: Bearer就能搞定,另一个服务可能要求你先换取临时凭证、再调用正式接口,甚至还有/query、/submit两步操作的。你在调用层写死一种鉴权,换一个供应商就要重新写一遍。
然后是消息结构。OpenAI 系用的是messages数组,里面是role和content;有些模型服务直接收一个prompt字符串;还有些为了兼容安全审查,要求你包装一层额外的系统提示字段。如果不做适配层,业务代码就得在各种结构之间反复切换。
参数命名更是五花八门。同样是限制生成长度,有的叫max_tokens,有的叫max_new_tokens,有的叫max_completion_tokens,还有的藏在generate_kwargs里。温度参数也是,有的取值范围 0 到 1,有的 0 到 2,有的居然还支持负值。你按照 A 模型的写法传给 B 模型,轻则不生效,重则直接报参数错误。
返回体差异也很让人头大。主流模型一般是choices[0].message.content,但有的服务是response.text,有的是outputs[0].text,流式输出时事件字段更是各家有各家的风格。再加上 token 计费口径不统一,有的按 token 算,有的按字符算,有的按请求时长算,你想准确核算成本,就得去解析完全不同格式的账单数据。
拿一个非常典型的例子来说,同样是控制生成长度,A 平台叫max_tokens,B 平台的兼容层叫max_new_tokens,C 平台为了区分新旧模型接口又搞出一个max_completion_tokens。如果你只是在内部跑跑 demo,写死也就写了;一旦做成了产品,用户希望在不同的模型之间自由切换,你就会发现整个代码里到处都是 if-else 和 switch,每加一个模型都要改一遍核心调用逻辑。
1.2 自研适配层:看起来简单,维护起来很痛
有人会说,我写一个统一的客户端类,把所有差异封装起来不就行了?思路完全正确,但真正落地之后你会发现这是一条没有尽头的路。
我见过太多团队的做法:先写一个ModelClient类,里面有call_openai、call_anthropic、call_xxx方法,每个方法做一次参数转换和返回归一化。项目初期确实够用,一个月后需求开始复杂起来,要支持流式输出、工具调用、图片输入、异常分类重试,每个功能都必须在每家供应商的接口上重新适配一遍。
更麻烦的是模型服务的迭代速度。厂商经常会调整参数名、废弃旧字段、改变某个默认行为。你的适配层是照着某个时间点的接口文档写的,可上游一直在动。今天它加了个新参数,明天它把某个返回值从字符串改成了对象。只要上游变更,你就要去翻文档、改代码、重新发版。一年下来,你维护的根本不是业务功能,而是各家 API 的变更公告。
还有一个隐性成本:自研适配层会绑定你的团队认知。你团队里的新成员得先熟悉你造的这套封装,才能开始写业务。这本应是你的核心竞争力吗?不是。这只是一层和业务无关的翻译工具,却占用了大量研发时间。
1.3 为什么选 LiteLLM 而不是自己写
我第一次了解到 LiteLLM 的时候,第一反应是这不就是一个 API 聚合库吗。后来仔细看它的能力,才发现它不光是让你在代码里少写几个 if-else,它其实把整个"接入多家模型"这件事做成了标准产品。
它的核心能力可以拆成三层:
第一层,统一调用接口。你用同一套messages结构,同一个completion函数,就可以调用 OpenAI、Anthropic、Gemini、vLLM、Ollama,以及各种兼容 OpenAI 协议的服务。它内部维护了一张厂商映射表,把各家请求格式、返回格式、参数名差异全部消化掉。
第二层,代理网关模式。LiteLLM 可以跑成一个独立的 HTTP 服务,部署在业务和大模型厂商之间。业务侧只需要把原来的 API 地址指向这个代理,后端模型怎么配置、怎么切换、怎么轮询,一概不用业务方关心。
第三层,生产级辅助能力。密钥管理、成本追踪、预算限制、多 Key 轮询、请求缓存、重试和限流,这些能力通过配置文件就能开启。我后来之所以把几乎所有 AI 请求都改成走 LiteLLM proxy,最主要的原因就是这些能力太省心了。
选择它而不是自研,本质上是因为它把"多模型接入"这个通用问题变成了一张配置表,而不是一堆业务逻辑。你要操心的是"接哪几个模型、配多少预算",而不是"怎么解析某家的返回体"。
2. LiteLLM 的工作逻辑:一个网关把 API 差异消化掉
2.1 以 OpenAI 兼容协议作为"普通话"
LiteLLM 做了一件很关键的事:它把 OpenAI 的请求和返回格式作为整个系统的中间语言。
你可以这么理解:上游各家说各自的方言,下游业务团队说的是 OpenAI 的普通话。LiteLLM 在中间做同声传译,把任意方言翻译成普通话,再把普通话翻译回任意方言。
具体到技术上,LiteLLM 对每个模型提供商都有一套适配器。你传入的model参数带了一个前缀,比如openai/gpt-4o、anthropic/claude-3-5-sonnet-20241022、gemini/gemini-2.0-flash,它看到前缀就知道该走哪套适配逻辑。适配器负责三件事:请求参数转换、密钥注入、返回结构归一化。
参数转换其实很复杂。比如某个模型的温度上限是 1,另一个是 2,LiteLLM 会尽可能做范围匹配;有些模型的系统提示要拼进用户消息,它就自动拼;有些模型的工具调用格式和 OpenAI 不同,它也会在底层转换。你从业务侧看,所有模型都表现得像 OpenAI API,这对研发团队来说是巨大的减负。
有一点我要特别提醒:请养成写完整前缀的习惯。比如openai/gpt-4o,不要只写gpt-4o。虽然 LiteLLM 对很多主流模型也能通过不写前缀来猜测路由,但遇到别名冲突、自定义部署的时候,不写前缀很容易认错模型。显式声明是最稳妥的做法。
2.2 代理模式:一个独立网关
LiteLLM 的代码调用模式适合单应用、单语言环境。但当你的团队有多个后端服务、多个项目都要接模型时,每个服务都去放一个 SDK 再维护一遍 Key,就非常混乱。这时候就轮到代理模式出场。
代理模式就是你部署一个独立的 LiteLLM 服务,它对外只暴露一个 OpenAI 兼容的 HTTP 接口。业务侧不用再安装 LiteLLM 库,也不用持有任何真实厂商的 API Key,只需要把原来的 client 地址改成一个内部地址,把 Key 换成你在 LiteLLM 管理后台创建出来的虚拟 Key。
这个设计的好处非常明显。模型升级的时候,你只需要在代理的配置文件里改一行,把模型版本从旧版切到新版,业务代码一行不动。灰度切换也一样,你可以在代理层面把一个模型名映射到新模型上,给一小部分流量先走新链路。
更典型的是做数据隔离。后端服务不应该知道真实模型 Key,否则一旦某台机器被拖库,所有模型额度都泄露了。有了代理,真实 Key 只存在于代理这一层,业务侧只能拿到受控的虚拟 Key。
2.3 重试、限流、缓存这些能力也内建
我刚开始以为代理只是一个流量转发器,后来发现 Litellm 代理把很多生产环境必备的能力都做进去了。
重试机制。上游接口经常会出现限流和瞬时不可用,LiteLLM 可以配置自动重试次数和重试策略,遇到 429、5xx 之类异常时自动换 Key 或等待重试。你不需要在业务代码里各自实现一套重试逻辑。
限流与配额。你可以给某个虚拟 Key 配置一分钟最多多少次请求,也可以配置一天最多花多少钱,超过之后走拒绝或降级策略。这个能力在控制内部滥用和给客户开试用时特别有用。
缓存。LiteLLM 支持把相同请求的响应缓存起来,命中缓存的话直接返回结果,不消耗上游额度,延迟也大幅下降。对办公室里高频重复的 QA 类请求,缓存能省一大笔钱。
这些能力如果全部在应用层自己写,工程量不小,而且每个项目都要重新实现一遍。放在代理里统一做,业务方真的是只需要关心业务。
3. 从安装到客户端改造:一次完整的落地过程
3.1 安装与基础调用
安装非常简单,直接用 pip 装带代理功能的版本:
pip install 'litellm[proxy]'装完之后,先做一次最基础的在代码里直接调用。以 OpenAI 模型为例:
import os os.environ["OPENAI_API_KEY"] = "sk-你的Key" from litellm import completion response = completion( model="openai/gpt-4o", messages=[ {"role": "user", "content": "用一句话介绍你自己"} ], ) print(response["choices"][0]["message"]["content"])切换到 Anthropic 模型,代码结构几乎一样:
import os os.environ["ANTHROPIC_API_KEY"] = "sk-ant-你的Key" from litellm import completion response = completion( model="anthropic/claude-3-5-sonnet-20241022", messages=[ {"role": "user", "content": "用一句话介绍你自己"} ], ) print(response["choices"][0]["message"]["content"])注意看,唯一的变化就是model字符串和对应的环境变量。messages、返回值结构完全一致。这就是 LiteLLM 最核心的价值:你只需要知道你想用哪个模型,然后改一个字符串就够了。
如果你接的是 Gemini,只需把模型写成gemini/gemini-2.0-flash之类的前缀格式,设置好对应的 API Key 环境变量,同样可以跑通。
这里要注意,LiteLLM 的更新速度很快,不同版本对模型名称的兼容性有细微差别。遇到某个新出的模型识别不了,先去升级一下包,大概率能解决,不用急着查配置。
3.2 代理模式 Server 配置 config.yaml
代码内调用适合个人脚本和快速原型,真正到了团队协作场景,推荐直接上代理。代理的所有配置都集中在config.yaml里,这个文件我建议纳入版本管理。
先看一个最小可用的配置:
model_list: - model_name: gpt4 litellm_params: model: openai/gpt-4o api_key: os.environ["OPENAI_API_KEY"] - model_name: claude litellm_params: model: anthropic/claude-3-5-sonnet-20241022 api_key: os.environ["ANTHROPIC_API_KEY"] - model_name: local-llm litellm_params: model: openai/local-model api_base: http://127.0.0.1:8080/v1 api_key: fake-key litellm_settings: drop_params: true set_verbose: false这里的model_name是暴露给你的业务的别名,业务侧代码里只认这个名字。litellm_params决定了这个别名背后真实接的是哪家模型、哪个 Key、哪个地址。model_name和litellm_params.model解耦,这个设计是整个配置的灵魂。
启动代理也非常直接:
litellm --config config.yaml --port 4000启动后监听在 4000 端口,业务侧把这个地址当成 OpenAI 的地址用就行。
drop_params: true是我强烈建议开启的。它的作用是,当某个模型不支持你传进来的某些参数时,自动丢弃而不是报错。比如某些模型不支持frequency_penalty,你不开这个开关,请求直接失败;开了它,LiteLLM 会把这个参数吞掉,让请求正常走下去。这个对跨模型切换非常实用。
3.3 Docker 部署
实际部署我不会直接裸跑进程,而是用 Docker,这样方便迁移和横向扩容:
docker run -d --name litellm-proxy -p 4000:4000 \ -e OPENAI_API_KEY=sk-你的Key \ -e ANTHROPIC_API_KEY=sk-ant-你的Key \ -v ./config.yaml:/app/config.yaml \ ghcr.io/berriai/litellm:main-latest \ --config /app/config.yaml --port 4000有两个细节值得注意:
第一,密钥通过环境变量传入,不要写死在config.yaml文件里。上面示例里api_key: os.environ["OPENAI_API_KEY"]就是在 config 里读取环境变量,这样 config 文件可以放心进 Git 仓库,里面没有任何真实密钥。
第二,main-latest这个镜像 Tag 更新非常频繁,因为 LiteLLM 基本跟着模型厂商的节奏发版。如果你追求稳定,锁定一个经过验证的版本号,而不是一直追 latest。我吃过一次亏,某次 latest 升级后,旧配置里某个字段的默认行为变了,花了不少时间排查。
3.4 客户端改造:把 base_url 指过来
等代理跑起来,业务侧改造其实就一步:把 API 地址换成代理地址,把真实 Key 换成虚拟 Key。
以 Python 的 OpenAI SDK 为例:
from openai import OpenAI client = OpenAI( base_url="http://localhost:4000/v1", api_key="sk-any-key", ) resp = client.chat.completions.create( model="gpt4", messages=[ {"role": "user", "content": "你好"} ], ) print(resp.choices[0].message.content)注意这个api_key可以随便写一个字符串,因为代理内部有自己的虚拟 Key 逻辑,只要你填的东西符合格式就行。不过我还是建议你在 LiteLLM 的后台创建一个真正的虚拟 Key 填进来,这样后续可以在后台按 Key 做统计和限额。
最关键的是model字段填的是config.yaml里的model_name,也就是别名gpt4,不是真实的openai/gpt-4o。业务侧对接的是你的内部命名,日后你把这个别名从 gpt-4o 换成别的模型,业务代码不用改。
如果你用的是 JavaScript、Go 或者其他语言的 OpenAI SDK,原理一模一样,都是改base_url。
3.5 接入本地私有化模型:vLLM 与 Ollama
很多团队会有私有化部署需求,模型放在内网。LiteLLM 对本地模型支持也很成熟。
以 vLLM 为例,假设你在http://127.0.0.1:8080启动了一个 vLLM 服务,并且它本身暴露的就是 OpenAI 兼容接口。那么只需要在配置里加一项:
- model_name: local-llm litellm_params: model: openai/local-model api_base: http://127.0.0.1:8080/v1 api_key: fake-key这里model用openai/前缀,是因为 vLLM 的接口是 OpenAI 兼容协议,LiteLLM 会按 OpenAI 格式去请求,对接的却是api_base指向的本地地址。
如果用的是 Ollama,配置更简单,LiteLLM 对它有专门支持:
- model_name: ollama-llm litellm_params: model: ollama/llama3.1本地模型接入后要特别注意一个问题:很多本地推理框架默认不返回上游账单所需的usage字段,或者返回的 token 数和语义准确性完全不可靠。这会导致 LiteLLM 的成本统计里这张 Key 的花费是 0,不要以为出 bug 了,这是符合预期的,后面我会细说怎么补。
4. 生产环境实测:成本追踪、多 Key 轮询与稳定性
4.1 虚拟密钥与按团队隔离
代理跑起来之后,我第一个实际使用的功能就是虚拟 Key。LiteLLM 的管理后台可以创建多个虚拟 Key,每个 Key 绑定不同的预算、速率限制和有效期。
你可以按团队来分,比如算法团队一个 Key,前端联调一个 Key,测试环境一个 Key。每个 Key 的花费、请求量在后台都看得一清二楚。一旦某个环境出了问题,比如测试环境有人写了死循环疯狂调用,直接在后台撤销那个 Key 就能立刻止血,完全不影响其他团队。
虚拟 Key 还有个好处,它让"颁发密钥"变成了一种运营操作,而不是发放真实厂商 Key。以前你给同事发的是一个有全部额度的真实 Key,风险太大;现在发出去的是一个有预算上限的虚拟 Key,爆了也只是那个 Key 自己挂掉。
从安全角度看,这也减少了密钥在工作区流传的诱惑。真实的厂商 Key 被限制在代理配置里,普通研发解决完问题甚至都不需要碰它。
4.2 成本追踪:把每次请求的价格量化
LiteLLM 代理自带成本追踪功能,你不需要自己拿着账单去对着日志算。后台的 Spend 页面会展示当前总消费、请求数量、按模型分类的花费占比、按 Key 分类的费用排行。
这里要说下它的实现逻辑:LiteLLM 内置了一张模型价格表,里面维护了各家模型每百万 token 的输入输出单价。每次请求结束后,它会根据上游返回的 usage 字段计算这次调用的预估成本,然后累加到对应的虚拟 Key 和模型条目下。
我实际使用中最喜欢的功能是预算封顶。比如一个客户要试用你的 AI 产品,你不想让他无限调用,可以在配置里设置:
litellm_settings: budget_duration: "1d" max_budget: 100这个配置的含义是,在 1 天的时间窗口内,所有通过代理的请求总花费不能超过 100 美元,达到上限后新的请求会被拒绝。我拿这个功能去做对外试运营,心里踏实很多,不用担心某天突然收到一张让人眼前一黑的大账单。
你还可以按单个虚拟 Key 来设置预算,这样隔离性更好,一个团队爆了也不会拖垮整体额度。
4.3 多 Key 轮询与负载均衡
很多朋友会有这种场景:为了更高的并发上限,公司买了好几个模型账号,但不知道怎么做负载均衡。LiteLLM 原生支持多 Key 轮询,配置方式是在同一个model_name下列出多个litellm_params,每个带权重:
model_list: - model_name: gpt4 litellm_params: model: openai/gpt-4o api_key: os.environ["OPENAI_API_KEY_1"] weight: 1 - model_name: gpt4 litellm_params: model: openai/gpt-4o api_key: os.environ["OPENAI_API_KEY_2"] weight: 3两个条目的model_name都叫gpt4,业务侧请求gpt4时,LiteLLM 会按照权重比例把流量分发到两个真实 Key 上。上面配置里第二个 Key 的权重是 3,所以它会被派到约 75% 的请求,第一个 Key 承担约 25%。
这个配置配合重试逻辑,还能实现高可用。当某个 Key 因为限流或者余额不足而失败时,LiteLLM 可以自动把失败请求转到另一个 Key,甚至 fallback 到另一个模型。业务侧感知不到这些切换。
我在给客户做并发测试时,十几个 Key 挂在一个model_name下,压力测试打上去,请求在各 Key 之间平滑分布,单 Key 限流带来的抖动几乎消失,这个体验比自己在代码里写队列轮询好太多。
4.4 缓存对成本与延迟的实际影响
缓存是我建议每个团队都认真评估的功能。LiteLLM 的缓存策略可以简单配置:
litellm_settings: cache: true cache_params: ttl: 86400开启后,对于完全相同的请求体,代理会直接把上一次的响应返回,不再打到上游。这样有两个直接效果:成本下降、响应时间大幅变短。对那种"同一批用户问相同问题"的场景特别有效。
但缓存必须慎用。如果你的业务对结果实时性要求很高,或者同一段 prompt 配上不同上下文会产生不同答案,那缓存就可能把旧答案发给用户,造成看起来像 bug 的问题。
所以我实际部署时的建议是:先对用户的请求做分类,只有确定性比较高的请求才走缓存;不确定的业务不要开全局缓存。LiteLLM 支持按请求粒度做缓存控制,你可以针对某类接口关闭缓存,而不是一刀切全开。
如果你确实要做缓存,建议在正式开放前做一轮命中率测试,比如抽样记录 1000 个真实请求,看有多少请求是完全重复的。命中率不到 10% 的接口,缓存意义不大,反而引入数据一致性的风险。
5. 上线前必看的踩坑清单与排查思路
5.1 模型名冲突与 alias 解析
我在配置代理的过程中遇到的第一个坑,就是模型名匹配规则和直觉不一样。
LiteLLM 在做路由的时候,主要靠请求里的model字段去匹配config.yaml里的model_name。如果你在客户端直接把openai/gpt-4o写在model字段里,而model_name写的是gpt4,那代理会怎么处理?它会先把请求里的完整模型字符串拿去匹配,匹配不到再尝试解析前缀,结果可能会出现"找不到模型"的报错。
我最终的解决方法是立下两条规矩:
第一,model_name一律起内部别名,不要使用带斜杠的原始模型名。比如用gpt4、claude3、local-llm,不用openai/gpt-4o这种容易混淆的字符串。
第二,客户端请求时只填model_name,绝不带前缀。这样路由逻辑最干净,也方便以后换模型。只要你遵守这两条,模型名冲突基本不会出现。
另外还要注意大小写敏感问题。有些同学配置里写了大写,请求里写了小写,匹配不上就开始怀疑代理有问题。LiteLLM 的匹配是区分大小写的,配置和请求要保持完全一致。
5.2 token 统计口径差异:别让成本报表骗了你
成本报表看起来很爽,但你要搞清楚它的统计口径。LiteLLM 计算费用依赖上游返回的usage字段。问题来了,不是每个模型服务都会认真返回 usage。
有的开源模型服务默认不返回 token 数,LiteLLM 拿不到 usage 就只能把成本记为 0;有的厂商返回的 token 数其实只统计了生成部分,输入部分没算进去,费用就会偏低;还有一些服务把 usage 单位定义得和标准不同,直接对比两天报表会得出完全错误的结论。
本地模型尤其明显。我在 vLLM 接入后,成本明细一直是 0,一开始以为配置出了问题,后来查文档才知道,LiteLLM 对未收录定价表的本地上游默认不计算费用。
解决办法是手动补充成本信息。你可以在litellm_params里给本地模型加一个model_info配置,设置按百万 token 计算的价格:
- model_name: local-llm litellm_params: model: openai/local-model api_base: http://127.0.0.1:8080/v1 api_key: fake-key model_info: cost_per_token: 0.000001这样成本报表才会有相对真实的数据。不过我建议把这里的成本理解为"内部折算成本",它是你为了统计资源消耗设置的虚拟单价,和真实账单不是一个概念。
5.3 流式输出、超时与参数继承
流式输出是另一个容易踩坑的地方。当你把stream=True传给代理时,LiteLLM 会把上游的流式事件转换成 OpenAI 风格的事件流返回给客户端。多数情况下这样做没问题,但如果你对接的是一个对流式支持不完整的本地模型,就会出现上游半天不发事件、客户端干等的情况。
我建议对流式请求单独设置超时时间。LiteLLM 默认超时其实比较宽容,但对某些耗时较长的本地模型,宽的超时会抢占太多连接资源。
超时配置放在litellm_params里:
- model_name: local-llm litellm_params: model: openai/local-model api_base: http://127.0.0.1:8080/v1 api_key: fake-key timeout: 60还有一个参数继承陷阱。LiteLLM 的代理会把业务侧的请求参数直接传给上游,但你不知道上游是不是真的支持这些参数。如果不支持,默认会报错。这就是我前面让你开drop_params: true的原因,它会在请求失败前先帮你把不支持的参数丢掉,避免上游直接拒绝。
但开drop_params也有副作用,比如你想限制某一个模型的输出长度,结果这个模型不支持你的参数,代理悄悄把参数丢了,限制就没生效。这个行为很隐蔽,所以开这个开关之前,最好对每个模型做一轮参数支持测试,确认哪些参数会被丢弃。
5.4 本地模型的并发瓶颈
如果你把本地私有化模型也接进 LiteLLM 代理,一定要提前想好并发问题。本地模型和闭源 API 最大的区别是资源有限,推理吞吐量受显存和算力限制。代理可以把请求源源不断地转发给本地模型,但本地模型一旦忙不过来,就会大量超时和排队。
我在实践中出现过类似情况:内部几个服务同时打一个 vLLM 模型,代理以为上游很稳定,持续并发转发,结果 vLLM 吞吐被打满,P95 延迟直接飙到几十秒。
解决办法有两个方向。一个是在代理侧给本地模型设置最大并发数,超过后请求进入队列或直接走 fallback 模型;另一个是给本地模型单独配置限流规则,限制每分钟的请求量,把压力控制在一个安全阈值内。
另外建议在本地模型前加一层 vLLM 或推理引擎自己的并发队列配置,让它在上游就具备排队能力,这样代理侧的超时判断更准确。直连本地模型时,timeout 不要设得太短,否则长请求很容易被代理误杀重试,造成重复推理浪费。
最后再说一个我自己的经验。经过这几个月的折腾,我对工具选型的态度变得非常务实:如果业务只有一两个模型,直接用官方 SDK 接就行,完全没有必要引入代理这一层;一旦模型数量开始变多,或者有预算控制、团队隔离、多环境管理这些需求,Litellm 代理的价值才会慢慢体现出来。我现在几乎把所有的模型请求都改成了走代理网关的方式,发布新的模型服务时只改配置不改代码,回滚也只需要把配置恢复到上一个稳定版本,整个过程非常轻量。如果你正准备做多模型接入,建议先把最小代理跑起来,真正用一个月后再判断它适不适合你的团队。