1. 从一个真实困境说起:多模型调用的碎片化之痛
如果你做过任何跟大语言模型相关的应用开发,大概率经历过这样一个阶段:项目初期只接了一家模型服务,代码写得干净利落,几十行就能跑通。可一旦业务方说“能不能也试试另一家的模型,听说那个在中文场景下效果更好”,或者“我们想做个对比测试,同时跑三个模型看哪个性价比最高”,事情就开始变得不对劲了。
每家模型服务都有自己的接口规范、认证方式、参数命名习惯、返回结构。有的用messages数组传对话历史,有的要求把system提示单独拎出来;有的流式输出用 SSE,有的用 WebSocket;有的返回finish_reason叫stop,有的叫end_turn。你不得不为每一家写一套适配层,代码里充斥着if provider == "xxx"这样的分支判断。更麻烦的是,当你想加一个新模型时,又得把整套适配逻辑再写一遍。
这就是litellm要解决的核心问题。它是一个用 Python 编写的开源库,目标非常明确:用一套统一的 OpenAI 格式接口,去调用市面上几乎所有主流的大语言模型服务。你只需要把base_url和api_key换一下,模型名称改一改,其余代码几乎不用动。对于需要频繁切换模型、做多模型对比、或者构建模型路由层的开发者来说,这个工具能省掉大量重复劳动。
这篇文章我会从实际使用角度出发,把 litellm 的核心机制、配置细节、踩坑经验和进阶用法讲清楚。不管你是刚接触多模型调用的新手,还是已经在生产环境里折腾过一阵子的老手,应该都能从中找到对自己有用的东西。
2. litellm 到底做了什么:统一接口的底层逻辑
2.1 核心设计思路:格式转换层而非代理层
很多人第一次听说 litellm,会以为它是一个类似网关的代理服务,需要单独部署一个进程,所有请求先打到它那里再转发出去。其实 litellm 的定位更轻量——它本质上是一个Python 库,运行在你的应用进程内部,负责把统一的输入格式转换成各家模型服务要求的格式,再把各家返回的结果转换回统一格式。
这个设计选择带来的直接好处是:你不需要额外维护一个中间服务,不需要考虑代理层的性能瓶颈和单点故障,也不需要担心网络拓扑变复杂。它就是一个pip install litellm就能用的依赖包,跟你引入requests或httpx没有本质区别。
当然,litellm 也提供了一个可选的代理模式(Proxy Server),可以把它当成一个独立的 HTTP 服务来跑,对外暴露统一的 OpenAI 兼容接口。这个模式适合多语言技术栈的团队——比如你的主服务是 Java 写的,但想用 litellm 的适配能力,就可以单独部署一个代理,让 Java 服务通过 HTTP 调用。不过对于大多数 Python 项目来说,直接当库用是最简单也最高效的方式。
2.2 统一格式的具体含义:以 OpenAI 为基准
litellm 选择 OpenAI 的接口格式作为“通用语言”,这个决策很务实。原因很简单:OpenAI 的 Chat Completions 接口是目前事实上的行业标准,绝大多数模型服务商都在自己的 API 设计中参考甚至完全兼容这套格式。这意味着 litellm 需要做的转换工作相对可控——对于本身就兼容 OpenAI 格式的服务,几乎不需要转换;对于格式差异较大的服务,才需要写专门的适配逻辑。
具体来说,一次典型的调用长这样:
from litellm import completion response = completion( model="gpt-4o", messages=[{"role": "user", "content": "用一句话解释什么是递归"}] ) print(response.choices[0].message.content)如果你想换成另一家模型,只需要改model参数:
response = completion( model="claude-sonnet-4-20250514", messages=[{"role": "user", "content": "用一句话解释什么是递归"}] )注意这里的model参数命名规则:litellm 用前缀来区分不同的服务商。比如gpt-开头默认走 OpenAI,claude-开头走 Anthropic,gemini/开头走 Google,ollama/开头走本地 Ollama 服务。这个前缀机制是 litellm 路由的核心,后面我会详细讲怎么自定义和扩展。
2.3 支持的模型范围:比你想象的更广
litellm 官方文档里列出的支持列表非常长,涵盖了几十个服务商和数百个模型。从大类上分,主要包括这几类:
- 商业闭源模型:OpenAI 系列、Anthropic Claude 系列、Google Gemini 系列、Mistral 系列、Cohere 系列等。
- 开源模型托管平台:Hugging Face Inference API、Replicate、Together AI、Fireworks AI 等。
- 云厂商平台:AWS Bedrock、Azure OpenAI、Google Vertex AI 等。
- 本地推理框架:Ollama、vLLM、LM Studio 等。
- 其他兼容 OpenAI 格式的服务:任何声称“OpenAI-compatible”的服务,理论上都可以通过自定义
api_base接入。
这个覆盖面意味着,无论你的模型来源是什么,大概率都能找到对应的接入方式。而且由于接口统一,你可以在不同服务商之间做 A/B 测试、故障转移、成本优化,而不需要为每家写一套代码。
3. 环境搭建与第一次调用:少走弯路的配置细节
3.1 安装方式的选择与依赖管理
安装 litellm 本身很简单:
pip install litellm但这里有个细节值得注意:litellm 的依赖项比较多,因为它要适配各种服务商的 SDK。如果你只打算用其中一两家,可以通过 extras 来精简安装:
# 只安装 OpenAI 和 Anthropic 的支持 pip install "litellm[openai,anthropic]"这样做的好处是减少依赖冲突的概率,也能加快安装速度。我在一个容器化部署的项目里就遇到过因为 litellm 全量安装导致镜像体积膨胀的问题,后来改成按需安装 extras,镜像小了将近 200MB。
另外,litellm 的版本迭代比较快,建议在requirements.txt或pyproject.toml里锁定一个具体版本,避免因为自动升级导致接口行为变化。我一般会写成litellm==1.x.x这样的形式,升级时手动测试后再改版本号。
3.2 API Key 的管理:环境变量是底线
litellm 读取 API Key 的方式遵循一个约定:每个服务商有对应的环境变量名。比如 OpenAI 是OPENAI_API_KEY,Anthropic 是ANTHROPIC_API_KEY,Google 是GOOGLE_API_KEY,以此类推。你只需要在环境变量里设置好,调用时就不用显式传 key。
export OPENAI_API_KEY="sk-..." export ANTHROPIC_API_KEY="sk-ant-..."注意:千万不要把 API Key 硬编码在代码里,也不要在 Jupyter Notebook 里直接写明文。我见过太多因为把 key 提交到代码仓库而导致被盗刷的案例。用
.env文件配合python-dotenv加载,或者用密钥管理服务,都是更稳妥的做法。
如果你确实需要在代码里动态传入 key(比如多租户场景下每个用户用自己的 key),可以在调用时传api_key参数:
response = completion( model="gpt-4o", messages=[{"role": "user", "content": "你好"}], api_key="sk-..." )但即便如此,也建议 key 的来源是安全的配置中心或数据库,而不是硬编码。
3.3 第一次调用的完整示例与常见报错
环境配好之后,跑一个最小示例验证链路:
import os from litellm import completion os.environ["OPENAI_API_KEY"] = "sk-..." response = completion( model="gpt-4o", messages=[ {"role": "system", "content": "你是一个简洁的助手"}, {"role": "user", "content": "用一句话说明什么是向量数据库"} ], temperature=0.7, max_tokens=100 ) print(response.choices[0].message.content) print(response.usage.total_tokens)如果这一步报错,常见原因有这几个:
| 报错信息 | 可能原因 | 排查方向 |
|---|---|---|
AuthenticationError | API Key 无效或未设置 | 检查环境变量名是否正确,key 是否过期 |
NotFoundError | 模型名称拼写错误 | 对照 litellm 文档确认模型标识符 |
RateLimitError | 触发服务商限流 | 降低并发或等待后重试 |
APIConnectionError | 网络不通或代理配置问题 | 检查网络连通性,确认是否需要配置代理 |
InvalidRequestError | 参数不符合该模型要求 | 检查 temperature 范围、max_tokens 上限等 |
其中InvalidRequestError比较隐蔽,因为不同模型对参数的限制不一样。比如有些模型不支持temperature和top_p同时设置,有些模型的max_tokens上限只有 4096。litellm 会尽量做参数映射,但无法完全消除差异,遇到这类报错时需要查对应服务商的文档。
4. 模型路由与参数映射:那些文档里没写清楚的细节
4.1 模型名称的解析规则与自定义路由
litellm 判断一个模型该走哪个服务商,靠的是一套前缀匹配规则。默认规则大致是这样的:
gpt-3.5-turbo、gpt-4、gpt-4o等 → OpenAIclaude-*→ Anthropicgemini/*→ Google AI Studiocommand-*→ Cohereollama/*→ 本地 Ollamahuggingface/*→ Hugging Face Inference APIbedrock/*→ AWS Bedrock
这套规则覆盖了大多数常见场景,但实际项目中经常遇到需要自定义的情况。比如你公司内部部署了一个兼容 OpenAI 格式的推理服务,模型名称是internal-llm-v2,litellm 默认不认识这个名字。这时候有两种解法:
第一种是使用openai/前缀强制指定走 OpenAI 兼容接口:
response = completion( model="openai/internal-llm-v2", api_base="http://your-internal-service:8000/v1", api_key="your-internal-key", messages=[{"role": "user", "content": "测试"}] )第二种是通过custom_llm_provider参数显式指定:
response = completion( model="internal-llm-v2", custom_llm_provider="openai", api_base="http://your-internal-service:8000/v1", messages=[{"role": "user", "content": "测试"}] )这两种方式效果类似,但第一种更简洁,推荐优先使用。需要注意的是,api_base的末尾要不要加/v1取决于你的服务实现,有些服务要求加,有些不要求。我建议先不加,报 404 再加,这样能快速定位问题。
4.2 参数映射的坑:temperature、top_p 与 max_tokens
虽然 litellm 做了大量参数映射工作,但不同模型对参数的支持程度差异很大,这是实际使用中最容易踩坑的地方。
以temperature为例,OpenAI 系列模型的范围是 0 到 2,Anthropic Claude 的范围是 0 到 1,而某些开源模型只支持 0 到 1 之间的特定值。如果你在代码里统一写temperature=1.5,调 OpenAI 没问题,调 Claude 就会报错。litellm 不会自动帮你截断或缩放这个值,它会把参数原样传给服务商,由服务商决定是否接受。
max_tokens也是类似的情况。不同模型的上下文窗口和最大输出长度各不相同,有些模型甚至用max_output_tokens这样的不同参数名。litellm 会做名称映射,但不会做数值校验。如果你设置了一个超过模型上限的值,要么报错,要么被静默截断。
我的做法是在应用层维护一份模型能力表,记录每个模型的参数约束:
MODEL_CONSTRAINTS = { "gpt-4o": {"max_tokens": 16384, "temperature_range": (0, 2)}, "claude-sonnet-4-20250514": {"max_tokens": 8192, "temperature_range": (0, 1)}, "gemini/gemini-2.0-flash": {"max_tokens": 8192, "temperature_range": (0, 2)}, }调用前根据模型名查表,对参数做一次校验和裁剪。这样虽然多写了几行代码,但能避免很多运行时错误。
4.3 流式输出的统一处理
流式输出是多模型调用中差异最大的部分之一。好在 litellm 把这块也统一了,你只需要设置stream=True,然后用同样的方式迭代:
response = completion( model="claude-sonnet-4-20250514", messages=[{"role": "user", "content": "写一首关于秋天的短诗"}], stream=True ) for chunk in response: content = chunk.choices[0].delta.content if content: print(content, end="", flush=True)不管底层服务商用的是 SSE 还是其他流式协议,litellm 都会把它转换成统一的 chunk 格式。每个 chunk 的结构和 OpenAI 的流式返回一致,choices[0].delta.content就是增量文本。
不过有一个细节需要注意:不同服务商的流式返回中,最后一个 chunk 的行为可能不同。有些会在最后返回一个包含finish_reason的 chunk,有些则直接结束流。如果你需要在流结束后做一些处理(比如记录 token 用量),不能依赖最后一个 chunk 一定包含完整信息。稳妥的做法是在流结束后单独获取 usage 信息,或者使用 litellm 提供的回调机制。
5. 生产环境中的实战经验:重试、降级与成本控制
5.1 重试策略:不是所有错误都值得重试
在生产环境调用外部模型服务,网络抖动、限流、服务端临时故障都是常态。litellm 内置了重试机制,可以通过参数控制:
response = completion( model="gpt-4o", messages=[{"role": "user", "content": "你好"}], num_retries=3, retry_strategy="exponential_backoff_retry" )但这里有个关键判断:不是所有错误都适合重试。像AuthenticationError(认证失败)和InvalidRequestError(请求参数错误)这类问题,重试多少次都不会成功,反而浪费时间和配额。真正值得重试的是RateLimitError、APIConnectionError和InternalServerError这类临时性故障。
litellm 默认会对所有异常做重试,这在某些场景下反而不够优雅。我的做法是自定义重试逻辑,只对特定异常类型做重试:
from litellm import completion from litellm.exceptions import RateLimitError, APIConnectionError, InternalServerError RETRYABLE_EXCEPTIONS = (RateLimitError, APIConnectionError, InternalServerError) def call_with_retry(model, messages, max_retries=3): for attempt in range(max_retries): try: return completion(model=model, messages=messages) except RETRYABLE_EXCEPTIONS as e: if attempt == max_retries - 1: raise time.sleep(2 ** attempt) except Exception: raise这样既保证了临时故障能自动恢复,又避免了无意义的重试。
5.2 降级方案:主模型不可用时的备选路径
对于关键业务,单一模型服务不可用可能导致整个功能瘫痪。litellm 支持配置多个模型作为备选,当主模型调用失败时自动切换到备用模型:
response = completion( model="gpt-4o", messages=[{"role": "user", "content": "你好"}], fallbacks=["claude-sonnet-4-20250514", "gemini/gemini-2.0-flash"] )这个机制的实现逻辑是:先尝试主模型,如果抛出异常,则按顺序尝试 fallbacks 列表中的模型。每个模型都会走完整的重试流程,所以整体延迟可能会比较高。在实际使用中,我建议把 fallbacks 的数量控制在 2 个以内,并且选择响应速度较快的模型作为备选,避免用户等待时间过长。
另外需要注意,fallbacks 机制只处理调用失败的情况,不处理“调用成功但结果质量差”的情况。如果你需要基于结果质量做路由,那需要自己在应用层实现判断逻辑。
5.3 成本追踪:token 用量与费用估算
多模型混用时,成本核算会变得复杂,因为不同模型的计费方式不同。litellm 提供了一个成本追踪功能,可以在返回结果中拿到估算费用:
response = completion( model="gpt-4o", messages=[{"role": "user", "content": "你好"}] ) print(response.usage.total_tokens) print(response._hidden_params.get("response_cost"))response_cost是 litellm 根据内置的价格表估算出来的,单位是美元。这个估算值不一定完全准确(因为服务商可能调价或有不定期折扣),但用于内部成本监控和预算预警已经足够了。
如果你需要更精确的成本数据,建议结合服务商提供的用量 API 做定期对账。litellm 的成本追踪更适合做实时监控和异常告警,比如某个模型的日消耗突然翻倍,可以及时发现并排查原因。
6. 代理模式与团队协作:什么时候该上独立服务
6.1 代理模式的适用场景
前面提到 litellm 可以当库用,也可以当独立代理服务跑。那什么时候该用代理模式?我总结了几条判断标准:
- 多语言技术栈:团队里有 Python、Java、Go 等多种语言的服务,不可能每个服务都引入 Python 库,这时候统一走 HTTP 代理最方便。
- 集中管理密钥:不希望每个服务都配置一遍 API Key,而是由代理层统一管理,各服务只跟代理通信。
- 统一限流与审计:需要在网关层做请求限流、日志记录、用量统计,代理模式天然适合这个位置。
- 快速切换模型:运维层面改一下代理配置就能切换底层模型,不需要重新部署业务服务。
如果只是单个 Python 项目内部使用,直接当库引入更简单,少一层网络跳转,延迟也更低。
6.2 代理服务的启动与配置
启动代理服务的基本命令:
litellm --model gpt-4o --port 8000这会启动一个监听 8000 端口的 HTTP 服务,对外暴露 OpenAI 兼容的/v1/chat/completions接口。你可以用任何 OpenAI 客户端来调用它:
from openai import OpenAI client = OpenAI(base_url="http://localhost:8000", api_key="any-string") response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "你好"}] )注意这里的api_key可以填任意字符串,因为代理层会用自己的密钥去调用上游服务。当然,生产环境建议给代理层加上认证,避免被未授权访问。
更复杂的配置可以通过 YAML 文件管理:
model_list: - model_name: gpt-4o litellm_params: model: gpt-4o api_key: os.environ/OPENAI_API_KEY - model_name: claude litellm_params: model: claude-sonnet-4-20250514 api_key: os.environ/ANTHROPIC_API_KEY general_settings: master_key: sk-your-master-key然后用litellm --config config.yaml启动。这个配置文件支持定义模型别名、负载均衡、限流规则等,功能相当丰富。
6.3 团队协作中的权限与配额管理
代理模式下,litellm 支持基于虚拟密钥的权限管理。你可以为每个团队成员或每个业务线生成一个虚拟 key,设置不同的预算和模型访问权限:
curl -X POST http://localhost:8000/key/generate \ -H "Authorization: Bearer sk-your-master-key" \ -H "Content-Type: application/json" \ -d '{ "models": ["gpt-4o", "claude-sonnet-4-20250514"], "max_budget": 100, "budget_duration": "30d" }'这样每个 key 只能访问指定的模型,且总消费不超过 100 美元(30 天内)。对于需要控制成本或做多租户隔离的场景,这个功能非常实用。
不过要注意,虚拟密钥的预算控制是基于 litellm 自己的成本估算,不是服务商的真实账单。如果对成本精度要求很高,还是需要结合服务商后台做定期核对。
7. 踩坑记录与排查思路:那些让我加班到深夜的问题
7.1 超时设置:默认值可能不够用
litellm 的默认超时时间比较短,对于长文本生成或复杂推理任务,很容易触发超时。我遇到过一次生成 2000 字以上的分析报告时频繁超时,排查了半天才发现是默认超时太短。
解决办法是显式设置timeout参数:
response = completion( model="gpt-4o", messages=[{"role": "user", "content": "写一份详细的市场分析报告"}], timeout=120 )timeout的单位是秒。对于大多数对话场景,30 到 60 秒足够;对于长文本生成,建议设置到 120 秒以上。但也不要设得太大,否则一旦服务端卡住,你的应用会一直挂着。我的经验是设置一个合理的上限(比如 180 秒),配合应用层的异步超时控制,双重保险。
7.2 并发调用时的连接池问题
当你的应用需要同时调用多个模型(比如做多模型对比),可能会遇到连接池耗尽的问题。litellm 底层用的是 HTTP 客户端,默认连接池大小有限。在高并发场景下,请求会排队等待可用连接,导致延迟飙升。
解决方案是调整连接池参数:
import litellm litellm.client_session = httpx.Client( limits=httpx.Limits(max_connections=100, max_keepalive_connections=20) )或者在代理模式下,通过启动参数调整 worker 数量和并发限制。具体数值需要根据你的实际并发量和模型响应时间来测算。一个粗略的估算方法是:并发数 = 目标 QPS × 平均响应时间(秒)。比如目标每秒处理 10 个请求,平均响应 3 秒,那至少需要 30 个并发连接。
7.3 返回结构差异导致的解析错误
虽然 litellm 尽力统一了返回格式,但某些服务商的返回中会包含一些额外字段,或者在某些边界情况下返回结构略有不同。我遇到过一次调用某个开源模型服务时,finish_reason返回了None,导致下游代码判断逻辑出错。
防御性的做法是在解析返回时做空值检查:
finish_reason = response.choices[0].finish_reason or "unknown" content = response.choices[0].message.content or ""另外,建议在应用层对返回结果做一次规范化处理,把可能为空的字段填充默认值,避免下游代码到处写if xxx is not None。
7.4 模型名称变更与版本兼容
模型服务商经常会更新模型版本,旧版本下线后,原来能用的模型名称可能突然报错。我就遇到过某天早上发现定时任务全部失败,排查后发现是某个模型版本被服务商下线了。
应对策略有两个:一是尽量使用服务商承诺长期支持的稳定版本,而不是最新的实验版本;二是在配置中维护一个模型名称映射表,当需要切换版本时只改配置不改代码:
MODEL_ALIASES = { "default-chat": "gpt-4o", "fast-chat": "gpt-4o-mini", "long-context": "claude-sonnet-4-20250514", }业务代码里用别名调用,实际模型在配置层决定。这样服务商下线某个版本时,只需要更新映射表,业务代码完全不用动。
8. 进阶玩法:回调、缓存与可观测性
8.1 回调机制:记录每次调用的完整信息
litellm 提供了回调机制,可以在每次调用前后执行自定义逻辑。这对于日志记录、性能监控、成本追踪非常有用:
from litellm import completion import litellm def log_callback(kwargs, response, start_time, end_time): duration = end_time - start_time print(f"模型: {kwargs.get('model')}, 耗时: {duration:.2f}s, " f"tokens: {response.usage.total_tokens}") litellm.success_callback = [log_callback]回调函数可以拿到请求参数、返回结果、开始和结束时间,足够做详细的调用日志。如果你用第三方监控平台,也可以在这里把数据推送到对应的 SDK。
8.2 缓存策略:重复请求的优化空间
对于某些场景,同样的输入可能会被多次请求(比如用户反复问同一个问题,或者测试时重复调用)。litellm 支持接入缓存层,避免重复调用浪费配额:
import litellm from litellm.caching import Cache litellm.cache = Cache(type="redis", host="localhost", port=6379) response = completion( model="gpt-4o", messages=[{"role": "user", "content": "什么是机器学习"}], caching=True )缓存 key 是基于模型名和消息内容生成的,所以只有完全相同的请求才会命中缓存。对于 temperature 大于 0 的场景,缓存的意义有限,因为同样的输入本来就可能产生不同的输出。缓存更适合 temperature=0 的确定性场景,或者用于开发测试阶段减少重复调用。
8.3 可观测性:把调用链路串起来
在生产环境中,单次模型调用只是整个请求链路的一环。为了排查问题,你需要把模型调用和上下游的日志关联起来。litellm 支持传入自定义的 metadata,这些信息会出现在回调和日志中:
response = completion( model="gpt-4o", messages=[{"role": "user", "content": "你好"}], metadata={ "trace_id": "abc-123", "user_id": "user-456", "session_id": "session-789" } )这样在排查问题时,可以通过 trace_id 把模型调用日志和业务日志关联起来,快速定位是模型响应慢还是业务逻辑有问题。
9. 选型建议与个人体会
litellm 不是唯一的多模型调用方案,市面上还有一些类似的工具,比如直接使用各家官方 SDK 自己封装适配层,或者用某些云厂商提供的模型网关服务。我的看法是:
如果你只需要接一两家模型,且不打算频繁切换,自己写适配层的成本并不高,反而更可控。但如果你需要接入三家以上的模型,或者需要做模型对比、故障转移、成本追踪,litellm 能省掉大量重复工作。它的统一接口设计让代码更干净,回调、缓存、代理这些附加功能也省去了自己造轮子的时间。
不过 litellm 也不是银弹。它的抽象层在某些情况下会掩盖底层服务商的特性,比如某些模型独有的参数或返回字段,通过 litellm 调用时可能拿不到。遇到这种情况,你可能需要绕过 litellm 直接调官方 SDK,或者通过extra_headers和extra_body参数传递自定义内容。
我在实际使用中的体会是:把 litellm 当作一个“翻译层”而不是“全能层”。它负责把通用请求翻译成各家能理解的形式,把各家返回翻译回通用格式。对于通用场景,它工作得很好;对于需要精细控制的场景,保留直接调用官方 SDK 的能力作为补充。这样既能享受统一接口的便利,又不会在特殊需求上被卡住。
最后分享一个小技巧:在项目初期就建立一份模型能力清单,记录每个模型的参数限制、价格、响应速度、适用场景。这份清单会随着你接入的模型增多而越来越有价值,无论是做技术选型还是排查问题,都能帮你快速定位。