过去两年,我基本把市面上叫得上名字的大模型 API 都接了一遍。如果让我给团队推荐一个最值得先装上的工具,答案会是 litellm。简单来说,它是一个面向大模型 API 的统一接入层,可以把不同模型服务商的接口统一成同一种风格。它不是模型本身,也不是官方 SDK,而是一层格式转换与请求调度服务,用生活化的类比就是家里的电源转换头——不管插座长什么样,到你手里只有一个标准的插头。
它在实际工程里的价值非常直接:业务代码只需要维护一种消息结构,就能调用 GPT 系列、Claude 系列、Gemini 系列,以及国内常见的通义千问、文心一言等模型服务。想从 A 模型切换到 B 模型,改一行model参数就行。更进阶一点,可以用它的代理模式搭统一网关,把密钥、预算、限流都管起来,团队协作时不用再把各家服务商的 key 分发给每个开发。
这篇文章不堆抽象概念,我会从设计思路讲起,给可直接跑的接入示例,再说清楚格式转换、参数映射、流式输出这些核心机制,最后分享路由重试、密钥管控和排查问题的实战经验。适合正在做 AI 应用落地、或者打算给团队搭一套多模型接入平台的人。
1. 核心设计思路:litellm 到底在解决什么问题
1.1 多模型接入的真实痛点
先还原一个真实的开发场景。假设你要给公司做一个智能客服机器人,第一版用了某一家大模型,效果不错,但老板说要对比一下另外两家的效果。这时候你会发现,每个模型服务商的接口都有自己的脾气:鉴权方式不同,有的是Authorization: Bearer,有的要求自定义 header;消息结构不同,有的把系统提示词放在单独字段里,有的直接放在消息列表里;返回结构更是五花八门,有的返回choices[0].message.content,有的返回content[0].text,还有的参数名都完全不一样。
如果不管不顾地硬写,代码里很快就会出现一堆 if-else。if provider == "A"走一套请求构造,elif provider == "B"走另一套解析逻辑。最坑的是参数名不统一:A 家叫max_tokens,B 家叫max_tokens_to_sample,C 家叫max_output_tokens。写适配层的人还得去翻每一家的文档,确认取值范围、默认值、是否支持流式、是否支持工具调用。这套脏活累活,litellm 帮你做了。
1.2 统一接口的转换层思维
litellm 的思路其实很朴素:把所有模型服务商的接口都包装成大家最熟悉的 OpenAI Chat Completions 风格。请求进来之后,它先解析成内部统一的中间表示,再根据目标服务商的规范做一次"翻译";响应回来之后,再做一次反向翻译,最终以标准结构返回给你。
这意味着你的业务代码只依赖一种协议。做消息历史、上下文管理、流式解析、工具调用处理时,都只需要写成一套。后续要接入新模型,核心工作就变成"确认 litellm 是否已经支持",而不是"再写一套适配代码"。这种转换层思维在工程上非常常见,就像数据库访问层屏蔽掉 MySQL 和 PostgreSQL 的差异一样,litellm 把模型服务商的差异挡在了业务代码之外。
2. 快速上手:安装、两种运行模式与第一次调用
2.1 安装与密钥配置
安装很简单,一个 pip 命令就搞定:
pip install litellm装完之后,先把各家的密钥配好。litellm 约定了一套环境变量命名规则,大部分服务商的 key 都是XXX_API_KEY这种格式。比如我常用的几个:
export OPENAI_API_KEY="sk-你的key" export ANTHROPIC_API_KEY="sk-ant-你的key" export GEMINI_API_KEY="你的key"实际项目中千万别写在代码里,也别提交到仓库。我一般用.env文件管理,然后在启动入口加载。litellm 自己也支持加载.env,但它不会主动帮你读,需要配合python-dotenv这类工具,或者在命令行启动代理时用它的--env-file参数。
2.2 代码调用模式实战
装好之后,最基础的使用方式是当成 Python 库直接调用。下面这段代码我经常拿来验证环境是否配通:
import litellm resp = litellm.completion( model="claude-3-5-sonnet-20241022", messages=[ {"role": "system", "content": "你是一个简洁的助手,回答不超过两句话。"}, {"role": "user", "content": "解释一下什么是回调函数。"} ], max_tokens=300 ) print(resp.choices[0].message.content)注意看,messages用的就是 OpenAI 风格的消息结构,system、user、assistant三种角色。不管底层调的是哪家,你写的都是这一套。返回值也能用resp.choices[0].message.content直接取文本,resp.choices[0].message.tool_calls取工具调用。
想切到另一家的模型,改model参数就行。比如把model换成gpt-4o,或者gemini/gemini-1.5-pro。这里有个小细节:litellm 支持带前缀的模型名,比如gemini/gemini-1.5-pro是为了避免同名模型在不同服务商之间冲突。当你拿不准的时候,建议用这种带前缀的写法。
2.3 以代理网关方式接入存量项目
代码调用适合新项目,但如果是存量项目,里面已经写死了 OpenAI SDK 的base_url,改动最小的方法是让 litellm 跑成一个代理服务。命令非常直接:
litellm --model gpt-3.5-turbo --port 8000这样在本机的 8000 端口就起了一个完全兼容 OpenAI 接口格式的服务。你用纯 HTTP 请求试一下:
curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-demo" \ -d '{ "model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "你好"}] }'如果你项目里用的是 OpenAI 官方 Python SDK,只需要把base_url改成http://localhost:8000,其他代码几乎不用动。这个模式对团队太友好了,尤其是多个服务共用一套模型接入的时候,大家都连这一个网关,不用各自维护密钥。
3. 核心机制拆解:格式转换、参数映射与流式处理
3.1 请求和响应的适配过程
litellm 内部并不是魔法般"自动懂"每家接口,而是维护了一张服务商协议映射表。每个服务商都有一套独立的"翻译器",负责三件事:请求构造、请求发出、响应解析。
以 Anthropic 的模型为例。你传给 litellm 的是 OpenAI 风格消息,它会在发出请求前把消息结构转成 Anthropic 风格,特别是 system 角色要单独提取出来,作为system顶层字段。收到响应后,再把 Anthropic 的content[0].text结构转回 OpenAI 的choices[0].message.content。这个过程对调用方完全透明。
从工程实现的角度看,这套翻译器通常包含两个阶段:prepare阶段处理请求,包括消息转换、参数填充、鉴权头准备;transform阶段处理响应,包括结果提取、错误码归一化。如果服务商返回了非 200 状态码,litellm 还会把错误映射成统一的异常类型,比如RateLimitError、AuthenticationError、TimeoutError,业务方做异常处理时就非常省心。
3.2 参数映射背后的隐性规则
每个模型服务商的超参数都不一样,这往往是新手最容易懵的地方。temperature还算通用,但max_tokens就很有意思:有的服务商叫max_tokens,有的叫max_tokens_to_sample,有的叫max_output_tokens。litellm 在内部维护了一张参数别名表,你写max_tokens,它会自动翻译成目标服务商认识的字段名。
更贴心的是drop_params参数。有些模型服务商不支持某些参数,比如部分国产模型不支持frequency_penalty,如果你传了,服务商可能直接报错。把drop_params=True打开之后,litellm 会自动丢弃目标服务商不支持的参数。我在实际项目中几乎总是开着它,因为各家模型的参数兼容性是动态变化的,与其手动维护黑名单,不如交给转换层去判断。
还有一个容易被忽略的点:默认参数填充。比如某些服务商要求必须传max_tokens,不传就报错,而你的业务代码往往只传了messages。litellm 会为这类服务商填入一个合理的默认值。这个细节让你写出来的代码可以非常"干净",但也提醒你:在追求统一接口的同时,心里还是得对目标模型的默认行为有个概念,该显式传的参数不要偷懒。
3.3 流式输出与工具调用的坑
流式输出是真实项目里的高频需求,ChatGPT 那种一个字一个字往外蹦的效果就是靠 SSE 流实现的。litellm 处理流式也做了统一:你只需要传stream=True,返回的迭代器会按块产出内容。下面是我常用的写法:
resp = litellm.completion( model="gpt-4o", messages=messages, stream=True # 关键是这个 ) for chunk in resp: delta = chunk.choices[0].delta if delta and delta.content: print(delta.content, end="")我踩过的坑是:流式模式下,有些服务商返回的第一个 chunk 里没有choices字段,或者delta是空的,如果代码里没有判空就直接取delta.content,很容易崩溃。所以做流式解析时,一定要对choices列表和delta字段都做空值判断。
工具调用(Function Calling / Tool Use)的转换更是重头戏。各家实现方式差异很大,OpenAI 的functions/tool_calls、Anthropic 的tool_use块、Gemini 的function_call,底层结构完全不同。litellm 会把它们统一成 OpenAI 风格的tool_calls列表,让你只用写一套工具调度逻辑。但这里必须提醒:工具的 schema 格式各家兼容性并不完美,有些服务商对 JSON Schema 的某些关键字支持有限,遇到报错时先试试简化 schema,别一上来就怀疑 litellm。
4. 进阶玩法:路由、重试、密钥与预算管控
4.1 用 Router 做多模型负载均衡
当你需要同时管理多个模型时,直接用litellm.completion就有点吃力了。这时候可以上litellm.Router,它相当于一个自带路由策略的客户端,可以配置多个模型、权重、优先级、失败回退。
我用一个简化例子说明配置方式:
from litellm import Router router = Router(model_list=[ { "model_name": "gpt-3.5-turbo", "litellm_params": { "model": "gpt-3.5-turbo", "api_key": "sk-你的key" } }, { "model_name": "claude-3-5-sonnet", "litellm_params": { "model": "claude-3-5-sonnet-20241022", "api_key": "sk-ant-你的key" } }, ]) resp = router.completion( model="gpt-3.5-turbo", # 这个是你自定义的 model_name messages=[{"role": "user", "content": "推荐一部电影"}] )这里要注意model_name和litellm_params.model是两层:前者是你在业务代码里使用的"别名",后者是真正传给服务商的模型标识。路由层还支持权重轮询,比如让 80% 请求走成本更低的模型,20% 走效果更好的模型,这样可以控制整体成本。这类配置我用下来最省心的场景是:日常轻任务走轻量模型,复杂推理自动切到旗舰模型。
4.2 失败重试与降级策略
模型服务商接口不可能百分百稳定,偶发 429(限流)和 5xx(服务端异常)太常见了。litellm 内置了重试机制,默认对特定状态码自动重试。你可以用num_retries控制重试次数,用retry_after控制间隔。
更实用的是 fallback 配置,也就是"主模型挂了自动换备用模型"。我还记得有次生产事故:主力模型服务商突然出问题,整个客服系统瘫了半小时。后来用 Router 配了 fallback,主模型请求失败后自动转到一个备用的国产模型,虽然效果略有下降,但至少服务没断。
配置方式也很简单,在 model_list 里给同一model_name配多个litellm_params,并用routing_strategy="usage-based-routing"之类策略控制选择顺序。或者更直接地在请求里传fallbacks=["modelB"]。重试策略需要有一点克制,别把重试次数调得太大,否则流量高峰时会形成重试风暴,反而把服务商接口打得更堵。我一般把num_retries控制在 2 到 3 次,配合指数退避。
4.3 网关模式下的密钥治理与成本控制
litellm 的代理模式不止是格式转换,它还是一个完整的 API 网关。启动代理时可以通过配置文件管理多个模型、密钥、预算和用户权限。这样团队里所有业务线都访问同一个网关,由网关统一调用各模型服务商,密钥不会泄露给每个开发者的本地环境。
在代理模式下,管理员可以生成虚拟 key 分配给不同业务线,每个 key 可以单独设置预算上限、速率限制、允许访问的模型范围。比如给某个数据分析团队分配一个 key,只允许调用某些模型,每月预算 100 元,用完了就自动拒绝请求。这个能力对多团队协作非常有价值。
我自己常用的一条经验是:网关层面的限流和成本告警一定要提前配。很多团队是出了账单才发现某个业务线在疯狂调用,那就晚了。litellm 的管理接口可以查看每个 key 的调用量和费用明细,建议固定每周看一次,并针对异常突增设置告警。
5. 常见问题与排查技巧实录
5.1 高频报错速查表
我把日常接入中遇到最多的几类报错整理成一个速查表,方便对照排查:
| 错误类型 | 可能原因 | 排查方向 |
|---|---|---|
AuthenticationError | 环境变量未加载、key 错误 | 检查XXX_API_KEY是否已正确设置;确认 key 是否有效 |
NotFoundError | 模型名拼写错误或服务商不存在该模型 | 核对 litellm 支持的模型名称列表;尝试带前缀的写法 |
TimeoutError | 请求超时 | 调大timeout参数;确认目标服务商当前负载 |
RateLimitError | 触发服务商限流 | 加重试和退避策略;检查网关 key 的速率限制配置 |
ContextWindowExceededError | 输入 token 超出模型上下文长度 | 做消息截断、压缩;换更长上下文的模型 |
ValueError: Unsupported params | 传了目标服务商不支持的参数 | 开启drop_params=True;检查参数名是否通用 |
表格里的报错类型其实都是 litellm 归一化之后的统一异常类型。它把各家服务商乱七八糟的错误码翻译成了统一的 Python 异常,业务代码只需要捕这些异常类型就够了。这也是统一接入层很重要的一个价值。
5.2 一个绕了很多弯的排查案例
有次帮某团队排查问题,现象是代码调用 litellm 的 Claude 模型时报 401 认证错误。开发同学说"密钥明明填了",我一看代码,发现他把ANTHROPIC_API_KEY直接写在字典里传给了api_key参数,理论上应该能过。但报错依旧。
排查步骤大概是这样的:先确认代码里传的 key 和我在配置后台看到的是否一致,结果一致。然后怀疑是环境变量干扰。因为代码里虽然显式传了api_key,但 litellm 内部解析模型时可能先去环境变量里找ANTHROPIC_API_KEY,如果环境变量里有一个过期 key,优先级问题就会导致用了错误密钥。这是一个非常隐蔽的坑:显式传参和环境变量同时存在时,不同版本的 litellm 可能处理方式不同。
最后我把环境变量里的 key 清掉,让代码只走显式传参,问题立刻解决。这个案例给我的教训是:排查认证问题第一件事就是确认"代码里实际生效的 key 到底是从哪来的",可以用litellm.print_verbose()打开内部日志,看请求头里实际带的是什么。litellm 提供了一个非常实用的能力:把环境变量LITELLM_LOG设为DEBUG,就能看到每一次请求的目标地址、请求体、响应体和耗时,我排查问题时几乎必开。
5.3 我的几条独家避坑心得
第一条,多模型项目里,一定要在测试环境把所有模型都跑一遍冒烟用例。不要只测主模型,因为你不知道服务商哪天会调整接口格式,偶尔一个小版本更新可能导致某个模型的翻译器失效。冒烟用例不用复杂,就测三件事:普通文本补全、流式输出、工具调用。
第二条,本地开发时用litellm.mock_response功能,可以模拟一个成功响应,不用真实消耗 API 额度就能把业务逻辑跑通。这个对前端联调和后端自测特别有用。用法是在请求里传mock_response="模拟的返回内容",这样即使没有配真实密钥也能拿到一个标准格式的响应。
第三条,把 litellm 版本锁死。这个库更新频率很高,新版本经常加新模型适配,但也偶尔调整配置项。生产环境务必固定版本号,升级前先在测试环境验证。我有一次就是没注意升级,结果某个模型参数映射行为变了,线上请求直接失败。现在我的习惯是至少锁到大版本。
第四条,善用litellm.model_list工具。接入家庭前,先打印出 litellm 支持的所有模型列表,确认你打算用的模型确实在里面,别想当然认为"文档里写了的模型就一定能调通"。命令是:
import litellm print(litellm.model_list)实际上,litellm 对模型的覆盖已经非常广,绝大多数主流模型服务商都在支持列表里。但即便如此,接入前做一次快速验证永远值得,毕竟省下的调试时间是完全属于你的。
我个人的工作习惯是:任何涉及多个大模型的项目,第一个环节就引入 litellm,哪怕初期只需要一家模型。因为切换成本极低,一旦模型效果不达预期,换模型就是个改参数的动作,而不是重写一遍接入层。这一点,在项目越往后走,体感越明显。