☰
用LiteLLM统一多模型API:从SDK到代理网关的实战指南
2026/10/14 8:41:10 网站建设 项目流程

如果你维护过一个要对接多家大模型 API 的 AI 应用,大概率经历过那种“每换一家模型就要重写一遍请求代码”的抓狂时刻。有的接口返回choices[0].message.content,有的返回output.text;有的鉴权要Authorization,有的要x-api-key;报错格式更是五花八门。这种痛苦我扛了很久,直到把litellm引入项目,才算是真正把“多模型接入”这件事理顺了。

Litellm 是一个开源的 Python 库和代理网关,核心能力就一句话:把几十家大模型服务商的接口统一成同一套 OpenAI 兼容格式。你在代码里只用写一种请求结构,剩下的事情它帮你翻译成各家 API 的真实格式,再返回一个统一结构的响应。它既能当 SDK 直接用,也能部署成一个独立的网关服务,统一管理密钥、限流、预算、重试和负载均衡。这篇文章不讲官网文档里已经写透的 Hello World,重点说我在实际接入和上线过程中做过的选型、踩过的坑、以及最终沉淀下来的配置方案。适合正在做 AI 应用开发、后端集成,或者需要在多个模型之间做评测与切换的工程师参考。

1. 为什么我最终把多模型接入全部交给 LiteLLM

1.1 多供应商 API 的“方言”问题

各家大模型服务商的接口本质上说不同的“方言”。拿一个最简单的“给用户发一条消息”来说,A 家的请求体要求用messages数组,B 家要求用prompt字符串,C 家可能要求inputs加一堆额外参数。返回结构也一样,有的返回choices[0].message.content,有的返回completion.text,还有的把答案放在response[0].generated_text里面。

如果只是在代码里通过条件分支挨个适配,短时间能跑,但时间一长就会变成维护噩梦。我这边最典型的场景是模型效果评测:同一个问题要发给三四个不同模型,然后对比答案质量。没有统一层的时候,我得为每个模型各写一套调用和解析函数,评测脚本的体积直接膨胀,而且每接入一个新模型,就要把前面的代码全部翻一遍。

鉴权方式的差异也同样让人头疼。有的服务商用 Bearer Token,有的要求把 key 放在自定义 Header 里,还有的更特殊,需要先拿临时凭证去换一个短期 token。这些逻辑一旦散落在业务代码里,后续想统一轮换密钥或者做审计,几乎无从下手。

1.2 OpenAI 兼容格式作为“共同语言”

LiteLLM 的设计思路很直接:把 OpenAI 的 ChatCompletion 请求格式当作中间标准。你在上层业务里永远只写messages=[{"role": "user", "content": "..."}]这种结构,LiteLLM 负责把它翻译成目标服务商要求的真实请求格式;收到上游响应之后,再统一翻译回 OpenAI 风格的choices[0].message.content结构。

这个“归一化”的价值,用过一次就回不去了。我当时做了一个内部聊天产品,后端服务需要比较多个模型的回答质量来选型。引入 LiteLLM 之后,评测脚本从原来每个模型一套逻辑,变成只需要改model参数名即可。比如跑 Claude 就传anthropic/claude-3-5-sonnet-20241022,跑某家的 GPT 模型就传openai/gpt-4o,上层读取返回值的代码完全不变。

我的体会是:这种抽象层的价值不在于省掉几十行代码,而在于让“换模型”这件事从一个半天的工作量变成一次参数修改。业务方今天说要用模型 A,明天说要对比模型 B,后端不再需要跟着改代码,只要确认新的模型厂商已经在统一路由里登记过就行。

2. LiteLLM 的两种使用形态:SDK 与代理网关

2.1 作为 SDK:把翻译层直接嵌进你的进程

LiteLLM 最轻量的用法是当 Python 库调用。安装很简单:

pip install litellm

然后就可以在代码里直接发起请求:

import litellm response = litellm.completion( model="anthropic/claude-3-5-sonnet-20241022", messages=[ {"role": "system", "content": "你是一个简洁的助手。"}, {"role": "user", "content": "用三句话介绍量子计算。"} ], api_key="你的真实密钥", temperature=0.7, max_tokens=1024 ) print(response.choices[0].message.content)

这里有个需要注意的命名规则:model参数通常写成厂商前缀/模型标识的形式。前缀决定了 LiteLLM 用哪一套厂商适配器去解析请求,冒号后面的部分才是真正发给上游的模型 ID。如果你不写前缀,比如直接传gpt-4o,LiteLLM 会默认当作 OpenAI 兼容接口处理。建议第一次接入的时候都带上前缀,避免因为厂商适配判断错误导致请求失败。

SDK 模式适合的场景很明确:个人脚本、单服务应用、或者在现有 Python 代码里快速接入多模型能力。缺点也明显——每个调用方都需要自己持有真实密钥,密钥分散在多个项目里,统一管理和审计比较费劲。

2.2 作为代理网关:把翻译层部署成独立服务

当团队里有多个服务都要接入模型时,我更推荐用 LiteLLM 的代理网关模式。它本质上是一个独立运行的轻量服务,所有业务服务把请求打到这个网关,由网关统一完成鉴权、路由、限流、重试和返回归一化。

一个最简配置长这样,放在config.yaml里:

model_list: - model_name: my-claude litellm_params: model: anthropic/claude-3-5-sonnet-20241022 api_key: os.environ/ANTHROPIC_API_KEY - model_name: my-gpt litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY general_settings: master_key: sk-my-master-key database_url: os.environ/DATABASE_URL

然后启动:

litellm --config config.yaml --port 4000

model_name是给业务方看的别名,litellm_params里是真实厂商路由信息。业务服务只需要知道自己用的别名,不用关心真实厂商和密钥。客户端配置里把base_url指向http://localhost:4000,把 API Key 换成网关发的虚拟 key 即可。

这个模式解决了一个很现实的问题:真实厂商密钥不再散落在各业务服务里。业务方拿到的虚拟 key 权限可以由网关控制,比如只允许调用某些模型、每分钟最多多少次、总额度多少,全部可以在代理层配置。

3. 从安装到跑通:一组可以直接抄的配置

3.1 最小 Python 接入:先让单次请求通起来

我习惯先把一次同步请求跑通,再考虑代理、缓存、回调这些进阶能力。建议在项目里建一个.env文件存放密钥,LiteLLM 本身会读取环境变量,所以用os.getenv传给api_key即可:

echo "ANTHROPIC_API_KEY=你的密钥" >> .env
import os import litellm from dotenv import load_dotenv load_dotenv() response = litellm.completion( model="anthropic/claude-3-5-sonnet-20241022", messages=[ {"role": "user", "content": "你好,请介绍一下自己。"} ], api_key=os.getenv("ANTHROPIC_API_KEY"), timeout=30 ) # 完整响应结构建议先打印出来看一次 print(response) print("答案内容:", response.choices[0].message.content)

第一次跑通了之后,我建议把返回对象完整打印出来看看。你会发现 LiteLLM 虽然把内容放到了choices[0].message.content,但model、usage、finish_reason这些字段也都是齐的,方便你直接取 token 消耗量做成本统计。

这里有个细节容易被忽略:timeout参数。默认超时时间有时候不够用,尤其是调用长思考类或大输出类模型时。我一般设置成 30 秒起步,测试阶段甚至给到 60 秒。后面讲到生产坑的时候还会再展开。

3.2 代理模式配置:模型列表与路由策略

代理模式的核心配置文件就是上一节那个config.yaml。实际用起来,有几个参数值得认真对待。

第一个是litellm_settings里的重试和回调配置:

litellm_settings: num_retries: 2 request_timeout: 45 retry_on_timeout: true fallbacks: []

num_retries控制同一个请求失败后最多重试几次,request_timeout控制单次上游请求的超时时间。这里有个经验:重试次数不宜设太大,否则上游接口万一长时间抖动,请求会被重试吃成好几倍的等待时间。我在生产环境一般设 2 次重试,再结合网关前面的客户端超时兜底。

第二个是预算和限流配置。在general_settings里加上:

general_settings: master_key: sk-my-master-key database_url: os.environ/DATABASE_URL max_budget: 1000 budget_duration: 1mo

这里的budget_duration支持1d、30d、1mo这类时间窗口,max_budget是窗口内的总花费上限。配置了database_url之后,LiteLLM 会把请求记录存到数据库里,方便在后台页面查看每个虚拟 key 的消耗。这块我刚开始没仔细看文档,结果成本核算一直对不上,后面专门列一节说。

启动网关之后,先用 curl 验证一下:

curl --location 'http://localhost:4000/chat/completions' \ --header 'Authorization: Bearer sk-my-master-key' \ --header 'Content-Type: application/json' \ --data '{ "model": "my-claude", "messages": [ {"role": "user", "content": "测试一下"} ] }'

看到返回里有choices[0].message.content,就说明网关已经正常工作了。

3.3 选型建议:什么时候用 SDK、什么时候上代理

我把两种形态的适用场景总结成一张表,方便你直接对照:

形态适用场景优点缺点
SDK 模式个人脚本、单服务接入、快速原型部署简单,无额外组件密钥分散,缺少统一审计,没有中心化限流
代理模式多服务共享模型、团队协作、生产环境统一密钥和权限、限流预算、集中日志、故障回退方便需要多部署一个服务,第一次配置成本略高

我的建议是:如果只是自己在本地脚本里偶尔调一次模型,用 SDK 模式就够了;但如果你做的系统会长期演进、有多个后端服务要接模型,那就直接上代理模式。代理模式多出来的一台服务部署成本,相对它带来的“业务方不感知模型变化”这个收益,非常划算。

4. 生产环境里我踩过的坑与对应解法

4.1 超时和重试:别让默认值坑了你的长任务

上线第一个版本的时候,我以为模型接口超时重试这种事框架都处理好了,结果第一次线上事故就来自这里。当时调用某个长文本生成模型,默认的请求超时时间太短,导致正常的长输出请求频繁报超时错误;而重试机制一旦被触发,等于同一份请求被发送了多次,上游模型计算又慢,直接把网关进程的并发占满了。

后来我把超时和重试调成了一套明确方案:

  • 普通对话模型超时设 30 秒,长文本生成模型设 60 秒。
  • 重试次数统一为 2 次,且仅在连接错误、429、500 这类明确可重试的状态码时触发。
  • 对于非幂等场景,比如一个请求可能会触发上游多个操作,我宁可不重试,宁可让客户端拿到错误后自己决定要不要重新发起。

这是一个原则性问题:重试机制是给“临时性故障”准备的,不是给“业务逻辑问题”兜底的。如果上游返回 400 参数错误,重试一万次也没有意义。

4.2 输出长度和上下文窗口的隐形差异

不同模型对max_tokens的默认值差异很大。有的模型默认输出上限只有 500 多 token,你在业务里没显式设置,结果生成的答案总是被莫名截断;还有的模型对上下文总长度有限制,超过了直接返回 400。这些错误并不会由 LiteLLM 自动帮你纠正,它只是把上游的报错翻译回来。

做法很朴素:对每个接入的模型,在建路由时就把默认参数写清楚。比如在代理配置里,可以在litellm_params中追加:

- model_name: my-gpt litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY max_tokens: 2048 temperature: 0.7

这样业务方不用关心每个模型的输出上限差异,网关统一兜底。同时我会在接入阶段用一条超长输入测试上下文窗口边界,把实际能承受的最大 token 记录下来,写进团队内部接口文档里。这个信息在后续出现短线截断问题时非常有用。

4.3 流式输出的归一化陷阱

流式输出是另一个容易出错的地方。开启stream=True之后,LiteLLM 返回的是迭代器,你需要自己遍历并拼接内容:

import litellm response = litellm.completion( model="anthropic/claude-3-5-sonnet-20241022", messages=[{"role": "user", "content": "写一段关于海边的散文"}], api_key=os.getenv("ANTHROPIC_API_KEY"), stream=True ) collected = [] for chunk in response: delta = chunk.choices[0].delta if delta and delta.get("content"): collected.append(delta["content"]) print(delta["content"], end="") full_text = "".join(collected)

这里需要注意,不同上游服务商对流式事件的字段使用习惯不一样,LiteLLM 做了归一化,但个别厂商的delta字段里可能同时包含content和reasoning_content。如果只取content,可能会丢掉一部分推理内容。我在实际对接时发现,有些新模型会返回思考过程字段,业务侧如果只是展示给用户,保留content就好;但如果你想记录完整推理日志,就需要把delta里所有文本字段都存下来。

另一个流式场景的坑是超时判断。传统的timeout参数只覆盖首字节到达时间,不覆盖整个流式会话的总时长。如果客户端一直不收数据,连接可能被网关或上游提前断开。我建议在流式场景里,由网关配置stream_timeout或依赖底层 HTTP 库的空闲超时参数,同时要求客户端按固定频率消费流,避免长时间不读导致连接被回收。

4.4 密钥、虚拟 key 与预算:别让测试 key 烧掉账单

代理模式的一个重要便利是可以用后台 API 创建虚拟 key,不需要把真实厂商密钥给每个业务方。实际使用的时候我踩过一个比较痛的坑:测试环境里创建了一批不限额度的虚拟 key,结果某个联调脚本死循环触发模型调用,一晚上烧掉了不少费用。

后面我严格固定了一套密钥管理规范:

  • 真实厂商密钥只出现在网关环境变量或配置引用中,业务代码里一律不允许出现。
  • 每个业务线单独创建一个虚拟 key,并设置明确的速率限制和总额度。
  • 测试环境 key 的预算上限设置得很低,保证即使出问题损失也可控。
  • 每周看一次后台的 token 消耗报表,与实际账单核对偏差。

拿虚拟 key 来说,LiteLLM 代理的/key/generate接口可以生成带配额的新 key:

curl --location 'http://localhost:4000/key/generate' \ --header 'Authorization: Bearer sk-my-master-key' \ --header 'Content-Type: application/json' \ --data-raw '{ "models": ["my-claude", "my-gpt"], "max_budget": 30, "budget_duration": "30d" }'

返回的key就是业务方可以使用的虚拟 key,配额耗尽之后网关会自动返回 429,不再产生上游费用。这样做的好处是,钱的边界由网关控制,而不是靠业务开发人员自觉控制循环次数。

5. 更进一步的玩法:缓存、回退和模型编排

5.1 模型故障时的自动回退

生产环境里不可能保证每次上游调用都成功。如果你不希望某个模型抖动时业务直接报错,可以在配置里指定fallbacks。以代理配置为例:

litellm_settings: fallbacks: [ {"my-claude": ["my-gpt"]} ]

这表示当my-claude路由调用失败时,网关自动尝试用my-gpt完成这次请求。实现上其实是在请求出错的边界做了一次路由重发。需要注意,fallback 会引入额外延迟,因为必须先等待首个模型失败或超时才能切换。我把 fallback 作为兜底而不是常规手段,正常情况下还是依赖各厂商自身的健康度。

5.2 用 Redis 缓存重复请求

LiteLLM 支持把相同的请求结果缓存起来,默认支持本地内存缓存、Redis 缓存等不同后端。基本开法是在配置里加上:

litellm_settings: cache: true cache_ttl: 3600 cache_params: type: redis host: localhost port: 6379

缓存适用场景我自己用得比较多的是两类:一类是文档问答这类固定知识型问题,答案不会频繁变化,缓存可以大幅降低重复费用;另一类是高频的简单分类、抽取任务,输入输出高度相似,命中率很高。

不推荐用缓存的场景是个性化对话和流式输出。个性化内容几乎不会有完全相同的请求,命中率低还占用 Redis 连接;流式输出如果要缓存,需要额外处理缓存数据和流式协议之间的转换,复杂度和收益不成正比。

5.3 与现有 OpenAI SDK 生态无痛集成

LiteLLM 代理暴露的是 OpenAI 兼容端点,这意味着你现有的、基于 OpenAI SDK 写的代码可以零逻辑改动接进来,只需要改两个地方:base_url和api_key。示例:

from openai import OpenAI client = OpenAI( base_url="http://localhost:4000", api_key="sk-my-virtual-key" ) resp = client.chat.completions.create( model="my-claude", messages=[{"role": "user", "content": "测试"}] ) print(resp.choices[0].message.content)

这个能力在团队里非常实用。只要大家熟悉 OpenAI 的调用方式,就完全不需要学 LiteLLM 自己的 SDK,所有请求统一走 OpenAI 风格,由代理层把语义转译给不同厂商。团队协作时,我可以把这份统一调用规范写进接口文档,业务方按这个格式对接即可,不需要关心实际后端模型是哪一个。

5.4 混合本地模型:把 Ollama 也纳入统一路由

LiteLLM 也可以把本地模型接进统一路由。比如你用 Ollama 在本地跑了一个 Llama 系列模型,配置里可以这样写:

model_list: - model_name: local-llama3 litellm_params: model: ollama_chat/llama3 api_base: http://localhost:11434

这样你的业务代码不需要区分请求是发到云端还是本地,只要指定模型别名,由网关判断路由。本地模型的上下文窗口和云端模型差异很大,我在实际使用时遇到过本地模型对超长输入直接截断的问题,后来也是在路由参数里显式设置了max_tokens和截断逻辑,才把行为统一起来。

要做混合架构的话,我建议先规划清楚哪些流量走本地、哪些走云端。如果本地机器性能有限,可以把耗时的离线任务走本地,在线低延迟任务走云端;或者在模型评测阶段用本地模型做初筛,候选结果再交给云端做增强,这样能省不少成本。


最后说点个人体会。LiteLLM 不是银弹,它解决的是“接口多样性”问题,解决不了模型本身的能力差异。如果你只是偶尔在脚本里调用一次某个厂商的模型,那引入这个依赖反而显得多余;但如果你维护的系统需要长期演进、可能要换模型、需要给团队开通权限、控制成本,那花半天时间把 LiteLLM 的路由层搭起来,回报会非常明显。我在实际配置中的感受是:技术选型这件事,等你的业务变成“每家厂商都要接一遍”的时候,再回头做统一抽象就晚了。与其到时候重写代码,不如在一开始建立一个薄薄的网关层,把复杂留给自己,把简单留给业务。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询