
做 AI 应用两年多真正让我烦躁的往往不是模型效果而是接入方式。今天从一个叫 LiteLLM 的开源网关项目说起——它不是某一家大模型厂商的官方 SDK而是一个把各家模型服务的调用方式拉平的库也能单独跑成代理服务。简单说你只需要学会一种写法就能接上主流大模型服务商包括普通 API、云厂商托管的模型、本地部署的开源模型等。这篇文章我不做泛泛介绍直接讲它解决什么问题、核心机制怎么运作、生产环境怎么用以及我踩过的几个坑。1. LiteLLM 是什么它把多家大模型的接入方式拉成了一条线1.1 我为什么会去找这么一层中间层最开始做 AI 功能时我属于典型的哪个模型好用就用哪个的做法项目里同时塞了好几个 SDK。这种做法的代价短期内看不出来等业务逻辑跑起来以后就非常难受。首先是接口风格不统一。有的 SDK 用prompt作为输入字段有的用messages还有的需要把对话上下文拆成system、user、assistant三段结构。参数名也一样有的叫max_tokens有的叫max_output_tokens有的叫temperature但语义还略有差异。返回结果更乱有的直接返回纯文本有的套一层choices[0].message.content有的把内容放在一个叫output的字段里。其次是切换成本高。某一天我发现供应商 A 的模型效果不够好想换成供应商 B或者想加一个备用模型作为降级方案结果光是 SDK 替换、超时处理、错误码转换就花了一个下午。改完接口还要重新测一遍不同场景因为两家服务对同样错误的表达方式都不一样。痛定思痛我决定在业务代码和模型服务之间加一个抽象层。这个抽象层要能统一接受我的请求、统一返回结果、统一处理异常并且能随时增加新厂商的模型进来而不动业务代码。LiteLLM 基本就是按这个思路设计的。1.2 从项目视角看它的核心能力可以归纳成一块表我看一个开源项目习惯先列能力再动手。LiteLLM 最常见的几个能力点如下能力作用我的使用场景统一调用接口用一套messages格式请求不同厂商的模型业务代码只写一次不再关心底层厂商差异自动重试遇到限流、临时错误时按规则重试高峰期的偶发 429 错误基本不用手动处理模型回退主模型失败时切到备用模型核心链路加了降级模型稳定性提升明显流式输出把流式响应统一成同一种 chunk 结构构建类聊天应用时前后端只需要解析一种结构成本计算根据 token 使用量估算费用每次调用结束直接记录成本月底对账省事代理模式起一个本地服务团队共用统一入口多个后端服务共享密钥和限流策略这张表要说明的很关键的一点是LiteLLM 不是一个转发器那么简单。它以统一为切入点把重试、回退、成本、密钥管控这些工程问题也一并归到了接入层。尤其适合两类团队一类是产品还在快速试错今天可能换一个模型另一类是多业务线共用模型资源需要一个集中管控的入口。2. 核心机制拆解一次适配处处调用2.1 统一输入输出约定的价值LiteLLM 的调用方式长这样你给它一个model参数格式是厂商前缀/模型名称比如supplier_a/model-7b再给它一组messages它就会自动把参数翻译成目标厂商需要的格式然后把返回结果标准化成统一结构再交给你。用户侧看到的永远是{ choices: [ { message: { role: assistant, content: ... } } ] }这意味着业务代码里不需要写如果调用 A 厂商就取这个字段调用 B 厂商就取那个字段这种分支。之前我的代码里有七八种字段解析逻辑后来全部删掉只剩一种。这里有个容易忽略的好处因为输入输出统一了你可以把一次模型调用封装成一个函数整个公司不同项目都能复用。新人接手时也不需要先搞清楚各家 SDK 的差异只要知道messages怎么组织就行。2.2 厂商适配器在工作时到底做了什么如果只是简单转发LiteLLM 做不了这么多厂商的适配。它内部像插件一样给每家服务商做了一层适配器负责三件事第一参数翻译。你传max_tokens有的服务商管这个叫max_output_tokens适配器会把它转成目标服务的参数名。你传temperature有的服务商不支持或者取值范围不同适配器也负责转换或者降级处理。第二请求鉴权。不同服务商的鉴权方式不一样有的用请求头有的要求把密钥放在特定字段里有的还要求附带额外的项目 ID 或区域信息。这些细节全部被适配器消化掉了你在业务代码里只需要在环境变量里配好密钥。第三错误码归一化。这是最容易被低估的部分。服务商报错时有的返回 429 表示限流有的返回 503有的返回一段很长的 JSON里面藏着真正的错误原因。LiteLLM 会把它们翻译成相对统一的异常类型和错误信息。我做排障时先看错误码属于哪一类再去对应厂商文档里查细节比之前直接在几十种返回格式里捞原因效率高得多。2.3 模型名、密钥与额外参数的处理逻辑很多人第一次用 LiteLLM 时会好奇它怎么知道model-a到底该走哪家厂商答案就是靠model参数里的前缀也就是斜杠左边那一段。举个例子supplier_a/model-7b前缀supplier_a走供应商 A 的适配器supplier_b/model-tiny前缀supplier_b走供应商 B 的适配器。密钥也不是混在一起的。传统做法是每个 SDK 用自己的环境变量比如A_PROVIDER_KEY、B_PROVIDER_KEY分开存。LiteLLM 兼容这种思路你只要把对应的密钥放在环境变量里它会在调用时自动按前缀去取。如果需要传递厂商专属参数比如某个云厂商的部署名称、区域 IDLiteLLM 也留了api_base、api_version这类通用参数位。我的经验是先看默认适配器能不能满足需求如果某个厂商有特殊要求再这些参数传给适配器不要让这些细节渗入业务代码。3. 从零开始先用 Python 库打通一次真实模型调用3.1 安装和最小可运行示例最快的上手方式是直接装 Python 包在本地脚本里完成第一次调用。pip install litellm[proxy]我推荐把proxy扩展一起装上因为后面大概率会用到代理模式。装完之后先配置一个密钥环境变量比如要接供应商 A就设置export SUPPLIER_A_API_KEY你的密钥然后写一个最小例子from litellm import completion response completion( modelsupplier_a/model-7b, messages[ {role: system, content: 你是一个简洁的助手。}, {role: user, content: 用一句话解释什么是 API 网关。}, ], ) print(response.choices[0].message.content)只要密钥有效、模型名前缀对应得上就能跑通。第一次跑通后你会发现response对象的结构很接近那些主流 Chat 类接口连usage里的输入 token、输出 token 数量都能直接拿到。3.2 流式输出、异步调用与函数调用对话类应用里流式输出基本是标配。LiteLLM 把流式也做了统一处理from litellm import stream resp stream( modelsupplier_a/model-7b, messages[{role: user, content: 写一段 100 字的产品介绍}], ) for chunk in resp: delta chunk.choices[0].delta.content if delta: print(delta, end)如果你的业务是异步框架比如 FastAPI那就用acompletionimport asyncio from litellm import acompletion async def main(): response await acompletion( modelsupplier_a/model-7b, messages[{role: user, content: 你好}], ) print(response.choices[0].message.content) asyncio.run(main())这段代码有两个细节值得注意。第一异步和同步的返回结构一致代码迁移成本很低。第二流式调用时别直接把整个chunk打日志因为每个 chunk 都包含完整字段日志量会非常大我习惯只记录增量内容或者只记录首末 chunk 的时间。关于函数调用也就是tools能力各家的实现差异其实不小。LiteLLM 的做法是让你按熟悉的方式组织tools适配器负责转成目标厂商要求的格式。我的建议是先在小流量场景验证一次确认工具调用参数能正常往返再全面铺开。3.3 密钥管理环境变量比硬编码靠谱得多有人图省事直接把密钥写在代码里然后提交到仓库。这种事一旦发生基本等于把模型预算的钥匙交了出去。我在项目里都是环境变量加上本地配置文件代码里不出现任何真实的密钥。export SUPPLIER_A_API_KEYsk-xxx export SUPPLIER_B_API_KEYsk-yyy如果用的是代理模式还可以在服务端统一管理密钥客户端调用时无需知道任何厂商密钥。这个细节后面展开。4. 进阶把 LiteLLM 跑成团队网关服务4.1 为什么要用代理模式而不是各自引用Python 库模式适合单个服务内部使用但一旦你服务多了问题就来了每个服务都要配密钥每个服务都要单独设置重试和超时。万一供应商临时要换一个模型名你得把所有服务的配置都改一遍。代理模式就是解决这个问题的。LiteLLM 可以启动一个本地服务对外暴露一个统一的 HTTP 接口团队内部所有服务都通过这个接口访问模型。业务服务不需要再关心接的是哪家服务商也不需要持有任何厂商密钥。我用下来最大的感受是这层代理把模型路由和业务代码彻底拆开了。模型怎么切、怎么降级、怎么限流是网关层面的事业务服务只需要知道网关地址和虚拟密钥。4.2 一份可以上手的配置示例代理模式的管理核心是配置文件。我用的是 YAML大致长这样model_list: - model_name: chat-main litellm_params: model: supplier_a/model-7b api_key: os.environ/SUPPLIER_A_API_KEY - model_name: chat-fast litellm_params: model: supplier_b/model-tiny api_key: os.environ/SUPPLIER_B_API_KEY启动方式litellm --config ./config.yaml --port 4000启动后内部请求地址是http://localhost:4000/v1/chat/completions请求体沿用统一的 Chat 格式。调用方只需要把 base_url 指向这个地址model 参数填chat-main或chat-fast这样的别名就可以。这里有一个我很推荐的用法给模型起别名时不要直接用真实模型名而是按用途命名。比如chat-main表示主线对话模型chat-cheap表示低价备选模型。哪天你想换供应商只要在配置文件里改映射关系业务方完全无感。4.3 团队接入方式与统一管控代理模式下团队内部可以给每个业务方发一个虚拟密钥用来区分调用来源和权限。比如 A 服务用自己的虚拟密钥B 服务用自己的每一笔请求量、花销、错误率都能归到对应密钥上。实际运营时我还会在代理层做三个事给不同业务方设置调用频率上限给每个密钥设置月度消费上限把厂商原始密钥统一锁在网关环境变量里开发机不需要也不应该接触厂商密钥。这个模式对安全也有好处。厂商密钥一旦分发出去很难追踪谁在用而虚拟密钥可以随时吊销权限回收的成本低很多。5. 生产环境踩坑记录限流、超时和格式不一致5.1 限流和自动重试默认行为没那么可靠第一次上生产时我以为代理层自动重试就够了结果高峰期还是出现了大量超时。后来看日志原因是限流触发后上游服务还没缓过来请求又在同一时间被重试把原本已经拥堵的通道顶得更满。后来我在配置里做了两件事限制单实例并发请求数设置冷却时间。比如某个厂商账号并发上限是 10我就把单实例并发控制在 8 左右留出余量一旦上游返回限流错误短时间内不再把新请求发给这个实例而是直接走备用模型或返回降级结果。这个先保护上游再谈重试的思路比盲目重试稳妥得多。5.2 超时与慢请求网关也需要止损模型服务有个特点正常情况下 1 到 3 秒返回但偶尔会拖到十几秒甚至更久。如果不设超时调用方会一直占着连接池最终把整个服务拖垮。我的做法非常朴素在网关层给每次调用设置一个总超时比如 30 秒内部再做细分连接超时短一些读取超时长一些。用户侧对慢响应一般能容忍 3 到 5 秒超过这个时间就可以直接提示系统繁忙。关键是超时之后还要记住一点上游可能已经生成了结果只是响应没回来。所以涉及扣费、状态变更的操作要设计成幂等避免超时重试导致重复扣费。5.3 token 和上下文相关报错的几个常见现场生产上最常遇到的错误不是密钥失效而是上下文长度超限。不同厂商的上下文窗口不一样有的返回非常详细的错误说明有的只给一个泛泛的失败信息。我的排查惯例是先看是不是消息数量太多把系统提示词、示例对话、历史消息都清点一遍再用 token 估算工具确认实际占用最后看厂商文档给的上限值。切模型时尤其要小心同一个对话在供应商 A 能跑换到供应商 B 可能就超限因为两者的上下文策略不同。还有一个细节有些模型服务对messages里的角色有严格限制比如某类角色不能连续出现或者最后一条必须是用户消息。这类错误报错信息不一定明确如果你遇到请求格式错误但看不出原因优先检查消息序列是否符合目标厂商的规则。6. 成本治理与可观测性让每笔模型调用都有账可查6.1 单次调用的成本怎么算模型调用成本往往不是按次收费而是按输入、输出 token 分别计价。不同厂商的计价规则差异很大靠人肉估算容易算错。LiteLLM 自带成本计算能力你可以在一次调用结束后直接算出参考费用from litellm import completion_cost cost completion_cost(response) print(cost)这个值可以随日志一起落库也可以作为监控指标上报。我的习惯是给每个业务场景打上标签比如客服问答内容总结代码生成这样月底能一眼看出哪条业务线消耗最大。需要提醒的是成本计算依赖模型名称和厂商的价目表。如果你的模型不在内置列表里或者用的是私有化部署的服务价格需要单独配置。不要拿一个未知模型的估算成本去做财务决策先核实计费口径再上线。6.2 自定义日志与线上追踪很多人把日志系统搭得很重但模型调用的日志往往被忽略。实际上模型调用的日志价值极高它能告诉你每次请求发了什么、返回了什么、用了多少 token、耗时多久、成本多少。我一直保持一个习惯每条日志至少记录请求 ID、模型名称、业务标签、输入 token、输出 token、耗时、成本、返回状态。请求 ID 还可以关联到上游链路排查问题时能从头追到尾。LiteLLM 支持注册自定义回调把调用详情推送到已有的日志或监控平台。比如每次调用完成时把response.usage和cost写进数据表。这样即使模型调用分散在不同服务里只要都走统一网关日志格式还能保持一致。6.3 我的日常监控习惯落到实操上我日常会盯三类指标请求成功率、P95 耗时、单日成本。成功率低于阈值就报警P95 耗时过高说明某个模型服务变慢需要考虑切流量到备用模型单日成本异常飙升通常对应某条业务线流量上涨或者某个模型价格调整。还有一个习惯建议也给到大家线上切模型不要一次性全部切过去。我会先在网关配置里把新模型的流量比例调到 10%观察两天看成功率和耗时没问题再逐步提高比例。这个过程完全不需要业务方参与对用户也没有感知但能避免很多事故。我现在的日常流程基本固定了凡是需要新接入的模型先本地脚本试通再放到网关里小流量验证稳定后全量切换最后把成本监控同步接上。模型更新速度那么快真正决定工程质量和工作效率的往往不是模型本身而是接入层能不能干脆利落地更换。