
做 LLM 应用开发的人迟早会遇到一件事模型接口不统一。先用 A 厂商的模型写完了逻辑老板说想试试 B 厂商的模型你以为只是换个 key 的事情真上手才发现请求体结构不一样、报错格式不一样、鉴权方式不一样翻了半天文档才跑通。三次换下来业务代码没写几行对接代码倒是堆了一大堆。litellmLiteLLM就是我在这个阶段遇到的工具它把 100 多家大模型厂商的接口统一成一个 OpenAI 风格的调用格式换模型基本只改一个字符串。这篇文章会从核心思路讲到实操落地再到生产环境里的坑适合正在做 LLM 应用、或想给团队搭统一模型出口的开发者参考。1. 为什么需要统一封装多模型接入的真实痛点1.1 各家模型接口的“方言”问题如果你仔细对比过几家主流模型服务商的 API会发现一个尴尬的事实它们长得很像但细节全不一样。OpenAI 这套接口用 messages 数组每条消息有 role 和 content有些厂商用 sender 或者 prompt 直接传字符串还有的要把历史对话拼成一段带格式的长文本。参数名更是各有各的脾气有的叫 max_tokens有的叫 max_new_tokens有的还区分 max_completion_tokens。你以为改个字段名就完了结果默认行为还不一样同一个 temperature 值在不同厂商那里生成的随机性完全不同。比格式更折磨人的是报错。某个厂商限流了返回 429另一个厂商同样的限流可能返回 503错误信息 body 里一个用 error.code另一个用 errors[0].code。你给用户写的提示语要为每一家维护一套告警系统要同时解析好几种错误结构日志追查的时候同样的问题在不同的服务商那里表现在不同的字段里。再往下走还有鉴权差异有的用 Authorization: Bearer有的用 X-Api-Key 放在 header还有的要在请求体里额外塞一个 user 字段。这些差异单看都不难处理但叠加在一起业务代码的复杂度就会快速膨胀。最要命的是这些问题不会只出现一次。只要你的应用动了“多模型对比测试”“切换供应商”“降级备用模型”任何一个念头上面这些工作就要重来一遍。我曾在一个内部工具里同时接了三家模型最后代码里充斥着“如果是 A 厂商就这么拼参数如果是 B 厂商就那样解析错误”的分支维护成本远超我的预期。后来我才意识到真正的问题不是某个厂商的文档写不清楚而是缺少一个把差异挡在业务之外的抽象层。1.2 litellm 的思路把“方言”统一成“普通话”litellm 解决这个问题的思路很直接业务代码只和一套标准接口打交道标准接口背后再去做各家厂商的适配。它默认采用 OpenAI Chat Completions 这套格式作为“普通话”。你调用 litellm.completion 的时候传入的 model、messages、temperature 这些参数都和 OpenAI 官方 SDK 的写法保持一致它会自己识别目标模型属于哪家服务商然后把请求翻译成那家的格式发出去再把返回结果统一成标准结构交还给你。这个设计最大的价值是把“厂商差异”隔离在了应用之外。你写了一套 prompt、一套工具调用逻辑、一套流式解析代码后面想换模型或者做对比测试只需要改 model 字段。今天默认用 openai/gpt-4o-mini明天想切到另一家差不多价位的模型把模型字符串换一下其它代码一概不动。这种切换成本从“改几十行兼容代码”变成了“改一个字符串”体验差距是非常直观的。用生活化的方式类比litellm 这层有点像同声传译业务代码说普通话litellm 实时翻成方言再把方言的回答翻回普通话交给你。你全程只听到普通话不关心对面说的是不是另一种语言。需要特别注意的是model 字符串里的 provider 前缀是有意义的习惯写法比如 openai/、anthropic/、bedrock/ 这种格式。如果你不给前缀litellm 默认按 OpenAI 协议直接转发这一点是很多新手第一次踩坑的地方后面我会专门展开。2. 核心能力拆解litellm 到底做了什么2.1 一次调用从进去到出来中间经历了什么假设我现在要调一个 OpenAI 模型代码写出来是这样的import litellm resp litellm.completion( modelopenai/gpt-4o-mini, messages[ {role: user, content: 帮我解释一下什么是依赖注入} ] ) print(resp.choices[0].message.content)这行代码背后litellm 实际上做了这么几件事。第一步解析 model 字符串根据前缀识别出 provider 是 openai。第二步从环境变量或配置里找对应的 API 密钥比如 OPENAI_API_KEY。第三步把标准格式的 messages、temperature、max_tokens 等参数翻译成 OpenAI 服务商期望的请求结构。第四步发 HTTP 请求并等待响应。第五步把响应统一包装成 ModelResponse 对象这个对象的结构跟你平时用 OpenAI SDK 拿到的结果一模一样也能直接取 choices[0].message.content。如果你开了 streamTrue返回的东西会变成一个迭代器每次迭代出来的都是 ChatCompletionChunk 对象。应用层做流式渲染的时候仍然按 OpenAI 的字段去取 delta.content。这就意味着你的流式前端代码不需要关心背后是流式返回还是非流式返回更不需要关心是哪家厂商在返回统一格式的好处在这里体现得很充分。还要提一下异步调用。litellm 提供 acompletion 这个异步版本用法和同步版几乎一样只是在 async 函数里要加 await。做批量处理或者在高并发服务里异步版本是必须的。我见过不少同学在同步代码里开了几十个线程去并发调接口折腾半天还是不如在 asyncio 事件循环里 await 来得干净。2.2 关键增强能力重试、错误标准化与模型降级格式转换只是 litellm 的基本功真正让我觉得这个工具靠谱的是它自带的一批“省心”能力这些能力在文档里常常只是一句话实际用起来却能帮你挡掉很多事。自动重试与退避。网络抖动和上游限流是常态尤其是跑大批量任务的时候请求一多429 几乎是每天见。litellm 内置了重试机制默认会按退避策略重试可重试的错误。你可以通过 num_retries 控制重试次数。实际用下来这个机制能把瞬时限流导致的失败率从 5% 以上拉低到千分之一以内前提是你把重试次数和退避间隔调整得合理。错误类型标准化。litellm 把所有厂商五花八门的错误响应翻译成统一的异常体系。比如 RateLimitError 代表限流AuthenticationError 代表密钥无效BadRequestError 代表请求参数有问题TimeoutError 代表超时。这样你在应用层只需要写一次异常处理逻辑不用为每个厂商各写一版 except。这个能力看上去不起眼但重构过异常处理代码的人都会明白它有多值钱。模型降级fallback。想实现“主模型挂了自动切换备用模型”再也不用自己写 while 循环了。你可以直接传一个模型列表response litellm.completion( model[ openai/gpt-4o-mini, gemini/gemini-1.5-flash, bedrock/claude-haiku ], messagesmessages )当前面的模型因为限流、配额、网络等原因失败时litellm 会自动尝试下一个。这个特性在批量信息抽取、内容分类这种容错性比较高的场景里特别好用。不过我要提前泼盆冷水fallback 用起来爽账单也可能会跟着翻倍。这个坑留到第 4 章细说。Token 统计和成本估算。litellm 的响应对象里会带上 usage 信息同时提供成本计算能力能根据模型价目表估算单次调用的花费。对做成本分摊的团队来说这个能力能省掉大量手工记账的活。把每次调用的 usage 和 cost 打到日志或者监控系统里月底对账基本不用费劲。3. 实操篇从 pip install 到代理网关3.1 五分钟跑通第一个请求先安装依赖。如果只是想在自己的 Python 脚本里调用直接装基础版就够了pip install litellm装完之后把对应厂商的密钥设置成环境变量。以 OpenAI 为例export OPENAI_API_KEYsk-...然后写调用代码from litellm import completion response completion( modelgpt-3.5-turbo, messages[{role: user, content: 你好简单介绍一下你自己}], ) print(response.choices[0].message.content)如果你用的是不带前缀的 model 写法直接写 gpt-3.5-turbolitellm 默认按 OpenAI 协议处理所以不用额外指定供应商。这里有一个小的经验分享如果你同时接多家模型建议从一开始就养成带前缀的习惯写 openai/gpt-4o-mini、anthropic/claude-3-5-sonnet、gemini/gemini-1.5-flash 这样完整的形式。这样环境变量再多也不会配错日志里也能一眼看出实际调的是哪家服务。如果你项目里已经在用 OpenAI 官方 SDK不想把所有调用改成 litellm还有一个非常轻的接入方式在构造客户端时把 base_url 指向 litellm 的代理服务。也就是说用 litellm 的 Proxy Server 模式当中间层现有的 openai.OpenAI(api_key..., base_url...) 代码不用大改只要把 base_url 换掉就能接入统一出口。这种方式特别适合那些已经写了大量 OpenAI 风格调用、暂时不想大规模改动代码的老项目。再有就是异步调用。因为批量场景实在太常用了这里直接给个最小例子import asyncio from litellm import acompletion async def main(): resp await acompletion( modelgpt-3.5-turbo, messages[{role: user, content: 用一句话解释什么是异步编程}], ) print(resp.choices[0].message.content) asyncio.run(main())把这个异步版本跑通之后再去做并发批处理体验会顺畅很多。3.2 用代理模式统一团队出口在代码里直接调用 litellm 库解决的是“我自己写的服务”接入多模型的问题。但如果你的团队里有十几个服务都在调大模型总不能要求每个服务都装一遍 litellm、各配各的密钥那样反而会造出新的混乱。这时候正确的做法是把 litellm 当做一个独立服务跑起来也就是它的代理服务器模式。启动代理服务器先安装带 proxy 扩展的版本pip install litellm[proxy]然后准备一个配置文件和启动命令litellm --config config.yaml --port 8000config.yaml 的核心结构是 model_list它定义了对外提供哪些模型、每个模型实际路由到哪个厂商model_list: - model_name: gpt-3.5-turbo litellm_params: model: openai/gpt-3.5-turbo api_key: os.environ/OPENAI_API_KEY - model_name: claude-sonnet litellm_params: model: anthropic/claude-3-5-sonnet api_key: os.environ/ANTHROPIC_API_KEY - model_name: local-llama litellm_params: model: openai/llama-3.1-8b api_base: http://localhost:8001/v1 api_key: dummy代理启动之后对内暴露的是 OpenAI 兼容的 /v1/chat/completions 接口。团队里其它服务不用关心背后接的是哪家厂商把请求 POST 到你这个网关就行完全按照 OpenAI 的格式来。这样做最大的好处是切换厂商、调整密钥、控制成本都集中在代理这一层操作业务服务不需要感知。代理模式真正的价值远不只是“统一格式”它把一套治理能力带进来了。首先是虚拟 API Key。你不必把各家厂商的密钥分发给团队成员每个人可以使用主密钥去生成自己的虚拟 Key后续可以按需回收和审计。其次是预算控制可以按 Key、按用户、按团队设置预算比如“这个 Key 这个月最多花 100 美元”超额直接拒绝请求。这个能力对防止测试阶段 token 失控特别有用我见过不止一次因为某个测试任务忘了关循环一个晚上烧掉几百美元的事故。再有就是速率限制可以按每分钟请求数或者每分钟 Token 数对 Key 做限流。部署上可以用官方镜像直接起容器docker run \ -v $(pwd)/config.yaml:/app/config.yaml \ -e OPENAI_API_KEY$OPENAI_API_KEY \ -p 8000:8000 \ ghcr.io/berriai/litellm:main-latest \ --config /app/config.yaml生产环境里建议把代理放在业务服务和大模型厂商之间前面加负载均衡后面按需接上 Redis 做缓存、接上数据库存调用日志。代理本身是一个相对无状态的逻辑层多开几个实例横向扩展问题不大。只要把配置管理好代理这一层可以是整个系统里最稳定、也最好维护的一环。4. 上线之后踩坑记录与排查清单4.1 模型返回 404多半是 provider 前缀没写对这是所有新手都会遇到的第一个坑。你想调 Anthropic 的模型于是在代码里写 modelclaude-3-5-sonnet结果返回 404。原因很简单没写 anthropic/ 前缀时litellm 会把它默认当成 OpenAI 的模型去请求 OpenAI 的服务。写成 anthropic/claude-3-5-sonnet 之后马上就好了。如果你用的是代理模式还要检查 config.yaml 里的 model_name 和 litellm_params.model 是否对应清楚。model_name 是对外暴露的别名不能随便定义。比如你在 model_list 里配了一个别名 gpt-4-turbo但 litellm_params 里的 model 字段拼错了代理启动的时候可能不会立刻报错直到实际请求发出去才发现不对。建议每次改完配置之后先用一个最小请求做冒烟测试不要直接切生产流量。4.2 fallback 配置不当账单直接翻倍第 2 章我卖了个关子现在展开说 fallback 的坑。默认情况下fallback 在主模型报错时会自动触发备胎模型。问题就出在“报错”这个条件的定义上。限流不一定意味着模型挂了它可能只是高峰期瞬时流量大导致的 429。如果你把 fallback 配置得很宽松高峰期原本一次请求只调一家现在变成先撞一次主模型限流再自动调用备胎模型同样一个任务被计费两次账单自然翻倍。我的建议是fallback 用来兜底硬错误和超时不要用来硬扛限流。真正的限流问题应该靠第 2 章说过的重试机制来解决。把“可重试”和“需要切换模型”这两类情况分开处理成本才不会失控。如果你确实需要在高峰期自动降级那么建议在日志里把每次 fallback 的触发原因记录下来定期复盘到底有多少次降级是“真故障”引起的。我实际操作中的体会是十次 fallback 里至少有六次都是限流误触发的。4.3 成本控制不能只靠事后看账单成本问题我反复提到了因为我觉得这件事值得单独说。代理模式里的预算控制功能一定要在业务上线前就配好不要等账单出来再后悔。我的经验是按项目拆分虚拟 Key每个服务用一把独立的虚拟 Key按项目维度设置预算。这样一旦某个 Key 的费用异常可以直接定位到具体项目不用到处排查。另外要把 Token 统计和成本估算用起来。litellm 每次调用的响应对象里都带着 usage把 usage 数据统一打进监控系统做成按小时、按模型维度聚合的曲线成本突然飙升的时候当天就能发现而不是月底收到账单才意识到问题。之前我有个任务在高峰期明明流量没涨多少费用却翻了三倍后来查日志发现是一个循环里没有走缓存重复调用了大量长 prompt 的请求。如果日志里没有 cost 字段这种问题排查起来会特别费劲。4.4 流式请求的隐藏问题流式请求看起来简单开了 streamTrue 就开始一个 token 一个 token 地接。但生产环境里有个容易被忽略的问题客户端断开连接之后代理侧并不总会立刻中断上游请求。如果跑的是批量任务长周期的流式请求可能在上游持续占用资源Token 数继续累积费用也跟着继续涨。排查思路其实不复杂看代理日志里的连接断开时间和上游最后一次响应时间是否对得上。如果发现断开之后上游还在吐数据那就说明代理侧需要更激进的空闲超时策略或者应用层要自己做超时控制。我个人的习惯是流式和非流式请求使用不同的超时配置流式请求的超时适当放宽但不能无限放宽结合业务场景设定一个上限既保证正常的慢响应能返回又不至于让异常连接拖着成本无限累积。4.5 本地模型和私有化服务怎么接对接本地模型其实是 litellm 里比较省心的场景。基本的思路是走 openai/ 这个通用通道把 api_base 指到你的本地推理服务。前提是本地服务必须兼容 OpenAI 的 /v1/chat/completions 协议。现在很多开源推理服务都做到了这一点所以配置上基本和接一个普通厂商没有区别。唯一要多留意的是本地服务的上下文长度和并发能力。公共 API 有排队机制和弹性扩容本地服务通常没有。如果并发一高就把本地服务打爆代理这边会出现大量超时。这种情况下我建议在 litellm 的配置里对本地模型单独设置更严格的速率限制和更长的超时时间同时在本地服务那一层也做好请求队列两边一起控制才能保证整体稳定。4.6 缓存是个好东西但别什么场景都开litellm 支持把响应缓存到 Redis重复请求可以直接命中缓存不发起真实模型调用。这个功能在“固定 prompt、低温度、高频重复调用”的场景里非常好用比如信息抽取、文本分类这类任务同样的输入在短时间内被反复请求的概率很高。开了缓存之后API 费用能肉眼可见地下降。但缓存也有坑。最怕的是把缓存开在“结果应该实时变化”的业务上比如轮询状态、生成随机内容或者依赖当前时间戳的 prompt。一旦缓存命中用户拿到的是上一次的结果效果就很奇怪。我吃过一次亏把一个开奖结果查询的接口误加了缓存用户连续几次查询拿到的都是同一期的数据排查了好一会才反应过来是缓存的问题。所以缓存开关本质上是一个业务语义决策不是性能优化决策。开之前先想清楚这个请求的结果在 TTL 内保持不变是可以接受的吗这个内容后续还可以这样扩展litellm 这个工具单独看是“一个 Python 库”但把它放到团队基础设施的角度去理解它的位置其实是一个模型网关。我个人的建议是不要一开始就上代理模式先在自己的脚本里把单条调用跑通把各家模型的差异感受一遍再一步步加代理、加缓存、加预算。顺序反过来的话你会同时面对接入问题、运维问题和成本问题排查范围太大会很痛苦。如果你正在做多模型接入或者打算给团队搭一个统一的模型出口希望这篇能帮你少走一点弯路。