这周我把公司内部的LLM调用流量全部切到了LiteLLM网关。之前各服务直连模型厂商SDK,OpenAI、Azure、Anthropic各自一套鉴权,密钥散落在十几个仓库的.env里,月初预算经常莫名烧掉一大截,有一次上游深夜降级,业务停摆40分钟才靠手工改配置切到备选模型。后来我把所有调用收口到LiteLLM,用两周时间补齐了限流、安全防护、缓存分流和模型容灾这四件事。这篇文章就是整个落地过程复盘,包含完整的config.yaml、参数选型思路和几个真实踩坑记录,适合正在接多个模型、或者已经上了LiteLLM但还没把网关能力用满的团队参考。
1. 为什么模型调用要收口到网关
1.1 直连模型时的真实混乱
接入一个模型是件简单事,接入两个也还行,接入到五个就开始失控了。最常见的一个场景是业务代码里塞满了厂商判断:模型A要传这样的header,模型B要把参数包成那样,模型C的错误码含义和另外两家完全相反。每接一个新模型,就要在服务里加一段适配逻辑,代码越来越像袜子补丁。
密钥管理更头疼。OpenAI的Key在后台服务里,Azure的Key在另一个仓库,Anthropic的Key放在某台机器的环境变量里。轮换一次Key需要跨部门沟通,发版窗口迁就好几个团队,中间隔一个晚上就可能有人用旧Key继续调,请求时而成功时而失败。审计的时候根本说不清哪个服务在调用哪个模型、花了多少钱。
还有故障处理。模型厂商偶尔会降级,或者某个模型因为负载太高开始疯狂报错。直连模式下没有统一的路由层,只能紧急改代码换模型,再走一遍构建、测试、发版流程。我见过太多团队在这种时刻手忙脚乱。这些痛点背后其实是同一个问题:调用关系太散,缺少一个统一的流量入口。
1.2 LiteLLM在链路里的位置
LiteLLM解决的就是“入口”这件事。它是一个用Python写的LLM网关代理,部署起来非常轻量,一条docker命令就能跑起来,对外暴露的是OpenAI兼容的/v1/chat/completions接口。业务方拿到这个地址后,完全不需要关心背后是OpenAI、Azure还是本地vLLM,甚至连SDK都不用换,只要原本会调OpenAI接口,把base_url指过来就行。
网关内部通过一个config.yaml把模型列表、上游密钥、限流阈值、缓存参数、fallback策略全部声明式管理起来。新增一个模型,往往只是往配置文件里加一段,然后reload,不需要改任何业务代码。这是一套很适合“中央集权”的治理模型:入口集中、策略集中、观测也集中。
我在选型的时候也对比过自己写网关或者用通用API网关改造。通用网关擅长流量管理,但不知道“token”是什么、“模型”是什么,做不了TPM限流,也无法理解模型失败时的容灾语义。自研网关听上去可控,但限流要做、缓存要做、密钥体系要做,很快会发现维护成本比模型费用还高。LiteLLM的定位正好卡在中间:懂LLM的路由和治理,又足够开放,可以插入自己的逻辑。
1.3 四件事的优先级怎么排
标题里的四个关键词——限流、安全防护、缓存分流、模型容灾——如果只看文档会觉得是一堆独立开关,实际落地要有优先级。
第一优先级是限流。这是和钱直接挂钩的:一个异常任务循环、一个数据同步脚本写错、一个刷子爬虫,都可能让账单在几小时内飞涨。把限流做了,预算基本守得住。第二优先级是安全。主密钥和业务密钥要分开,不然一次泄漏就是全盘失控。第三优先级是容灾,保证上游挂了业务还能跑。第四才是缓存,它解决的是成本和延迟优化,属于“过得更好”而不是“活下来”。
这四个功能在LiteLLM里不是独立的,它们会在同一份配置里互相咬合。比如缓存命中后不消耗限流额度,容灾切换后缓存key要跟着模型换,安全模块生成的虚拟密钥同时承载限流和预算的粒度。所以下文会按顺序逐项展开,但实际配置时一定要当成一个整体来看。
2. 限流:先把预算守住了
2.1 单机限流与Redis分布式限流怎么选
LiteLLM内置的限流能力分两种形态。单机模式下,它用进程内计数器做判断,配置简单,适合本地开发和单副本部署。但一旦生产环境开了多个副本,就必须换成Redis支撑的分布式限流,否则每个副本各自计数,N个副本等于把限流上限放大了N倍。
我见过一次事故:一个服务部署了6个副本,每个副本限制每分钟1000次请求,结果上游按整体维度限流,直接把服务商的key给封了。排查下来发现每个副本都认为自己没有超限,实际6个副本合计每分钟6000次。所以多副本场景下,Redis不是可选项,是必须项。
Redis配置也很直接,在general_settings里指定host和port即可。我的建议是Redis单独部署,别和业务共用实例,限流计数器对延迟敏感,业务高峰期可能互相拖累。
2.2 三层限流配置:全局并发、模型级、密钥级
LiteLLM的限流可以分成三个粒度,建议全部配齐。
全局并发上限用max_parallel_requests控制,防止同时打进来的请求数把网关或上游连接池压垮。模型级限流写在model_info里,有rpm和tpm两个维度,分别限制每分钟请求数和每分钟token数。密钥级限流在生成虚拟密钥时设置,每个接入方可以拿到独立的rpm_limit和tpm_limit。
RPM和TPM的关系要理解清楚:RPM限制的是“次数”,TPM限制的是“消耗量”。有的业务方每分钟只调用10次,但每次请求都是几十K token的长文本,RPM宽松也没用,TPM会先爆。所以两个维度都得配。
config.yaml里大概是这样:
general_settings: redis_host: localhost redis_port: 6379 max_parallel_requests: 200 model_list: - model_name: gpt-4o litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY model_info: rpm: 1000 tpm: 120000生成密钥时的限流参数是通过管理接口传的:
curl -X POST "http://localhost:4000/key/generate" \ -H "Authorization: Bearer sk-xxx-master-key" \ -H "Content-Type: application/json" \ -d '{ "models": ["gpt-4o"], "rpm_limit": 100, "tpm_limit": 50000, "max_budget": 50 }'三层限流的判断顺序是:全局并发先拦一道,然后到模型级RPM/TPM,再到密钥级RPM/TPM。任何一个维度超了,都返回429,并带Retry-After头。
2.3 TPM统计口径与流式请求的坑
RPM的实现很简单,进来一个请求计数加一,完成或失败减一。TPM就麻烦很多,因为只有在模型输出结束后才能精确知道一共消耗了多少token。尤其是流式响应,token是边生成边吐的,网关在响应结束的那一刻才能拿到准确总数。
LiteLLM的做法是先用估算值占位,响应完成后异步回写真实的token数。这意味着高并发场景下,瞬时TPM可能会略微超过设定值几个百分点。我在压测里看到过5%左右的超限,属于正常现象,不用太纠结。但如果长期跑下来超限明显,说明阈值配得太贴近上游上限了,要往下调。
2.4 限流踩坑记录
- Redis没配高可用。Redis一旦宕机,限流就退化成单机模式甚至放行。生产环境要给Redis做高可用,或者至少配上告警。
- 阈值没有留buffer。上游服务商给你的RPM是1000,你就配1000,高速流量一来先是网关429,再是上游429,两边都在报错。建议按90%配置,留10%给瞬时毛刺。
- 只限速率不限并发。有些耗时长的任务,RPM只有1,但能同时跑20个。如果上游并发能力有限,照样被打爆。所以要同时关注max_parallel_requests和各模型的并发表现。
- 客户端对429处理不当。有些SDK收到429后会立刻重试,反而加重压力。要在客户端做指数退避,尊重Retry-After。
- 限流日志要留全。至少记录请求id、虚拟密钥别名、命中的限流维度、被拒前的计数,方便事后复盘。
3. 安全防护:密钥、预算与内容审核
3.1 虚拟密钥:把主密钥关进保险箱
LiteLLM的密钥体系分两层:master_key和虚拟密钥,也就是Virtual Key。master_key权限最高,可以调用管理接口、生成和吊销虚拟密钥、查看所有密钥的消费记录。它只应该存在于网关服务端配置或运维手里,绝不能发给业务方。
虚拟密钥是给业务方用的。每个业务方可以生成一个或多个独立Key,并且每个Key都能单独绑定模型、单独设置预算和限流、单独做审计。出问题的时候直接吊销那一个Key就行,不用全局推倒重来。
我强烈建议所有外部调用都走虚拟密钥,即使只有一个业务方也别用master_key直连。真实场景里我踩过这样一个坑:业务方拿到master_key后直接在代码里写死,后来这个Key泄漏到前端仓库,整个网关等于裸奔。虚拟密钥至少能帮你把爆炸半径缩小到单业务、单Key范围。
生成虚拟密钥后,返回的sk-就是调用凭证,管理接口要记好。吊销用/key/delete接口,按key的hash值删除,删除后立刻生效。这个“立刻生效”在线上是很重要的能力。
3.2 预算控制:不允许任何人超支
除了限流,安全防护里和钱相关的另一层是预算。LiteLLM可以在密钥或团队维度设置max_budget,单位是美元,并且可以配置budget_duration设定结算周期,比如“30d”表示按月滚动。超过预算后,该密钥后续请求会被拒绝,并返回401之类的状态码。
预算的意义不只是防恶意,更是防遗忘。很多团队的模型Key是共享的,月底账单出来才发现某个内部工具在疯狂调用。拆到虚拟密钥之后,每个Key的消费通过接口一目了然,哪个业务烧钱一清二楚。
给预算设值的时候我建议按“预期消耗的两倍”来定。模型价格是波动的,一行代码也可能导致某个服务多调用三倍流量,预算卡得太死会让业务摸不到接口,卡得太松又等于没设。留个一倍余量,配合告警,是成本和安全之间的平衡点。
3.3 内容审核与敏感信息过滤
网关层做内容审核是个容易被忽视但很实用的能力。LiteLLM支持guardrails机制,可以在请求进入和响应返回时挂审核逻辑。典型做法是对输入输出做一次违规内容判断,命中色情、暴力等违规内容时直接拦截请求或终止流式输出。
接入审核后还有一个额外好处:把违规请求挡在模型之前,能省下不少token费用。有人觉得在自己的API接口里做审核就行,但业务方众多的时候,每个人写一套又不统一,网关集中做一遍是性价比最高的方案。审核逻辑可以是内置的OpenAI Moderation,也可以接自己训练的轻量分类器,LiteLLM提供了钩子位置,插进去就行。
敏感信息过滤是另一件事。日志里不应该出现完整API Key、请求里的身份证号手机号这类隐私字段。我在配置里会做日志脱敏:输出日志只保留模型名、token数、耗时、状态码,不打印请求body。
3.4 安全配置的几个隐蔽坑
- master_key泄漏一次等于全部泄漏。除了不要外传,还建议定期轮换,并把旧的立即吊销。
- 生成虚拟密钥时一定要显式指定models。如果models为空,LiteLLM默认允许该Key调用所有模型,等于限流和预算都形同虚设。
- 管理接口要藏好。/key、/team、/user这些管理端接口必须在防火墙或网关层做白名单,别暴露在公网上任人访问。
- config.yaml里的真实上游Key不要明文提交到git。用os.environ/前缀从环境变量读取,这是LiteLLM支持的标准做法。
- 内容审核的误杀率要灰度验证。审核模型阈值设太严会误伤正常请求,上线前用真实流量回放一遍,看拦截率和误杀率再调参。
4. 缓存分流:把重复请求挡在模型前面
4.1 先把流量分成“可缓存”和“不可缓存”
缓存分流这个词听起来玄,其实就是对进入网关的流量做一个分类。一类是高频重复的查询,比如天气、政策问答、固定模板生成;另一类是长尾个性化请求,每个人问的问题几乎没有交集。
分流的做法是:让网关在转发前先查缓存,重复请求直接返回缓存内容,不经过上游模型;只有缓存没命中的个性化请求才走模型。这相当于给模型前面加了一道“防洪坝”,把大量一模一样或高度相似的请求从账单里剔除。
我在一个FAQ场景实测过,加了缓存之后高峰期命中率能做到35%到45%,整体token成本下降约三成,P99延迟从2.8秒降到600毫秒左右。对于以重复查询为主的业务,这个数字还会更高。当然代价是需要接受“用户看到的是缓存答案不是新生成的”,所以实时性要求高的数据不适合缓存,这是选型时就要想清楚的。
4.2 精确缓存与语义缓存的取舍
LiteLLM提供两种缓存模式。精确缓存最简单,请求体的关键字段完全一致才算命中。它的优点是逻辑明确、没有误命中风险,缺点是用户措辞稍微变一下就会miss。
语义缓存更进一步,它先对请求做向量化,再计算相似度,超过阈值就判定为命中。比如“帮我查一下北京的天气”和“北京今天气温多少”在语义缓存看来是同一个问题,可以共用答案。生产环境配置语义缓存需要额外准备一个embedding模型,可以是OpenAI embedding,也可以是本地模型。
选择上我的建议是先上精确缓存。它零语义成本,命中规则透明,排错也容易。如果上线后命中率上不去,比如一直低于20%,再考虑加语义缓存。语义缓存需要调试的阈值参数更多,embedding模型和相似度阈值都会影响准确性,别一开始就把复杂度拉满。
Redis作为缓存存储时,配置是这样的:
cache: type: redis host: localhost port: 6379 ttl: 300 namespace: "litellm-cache-prod"4.3 缓存Key、TTL、流式回放与一致性
缓存看起来是开关一开就完事,实际有几个参数能明显影响效果。第一是TTL,缓存有效期。设短了命中率上不去,设长了会有过时内容风险。运营类知识问答我一般从300秒起步,然后根据数据变化节奏调整。天气预报这种实时数据,干脆不缓存。
第二是缓存Key的设计。LiteLLM默认把完整请求体hash后作为Key,包括model、messages、temperature等参数。这里有个容易忽略的点:同一个model_name下如果有多个上游部署,缓存Key里不应该带上游信息,否则同一段prompt会被路由到不同上游并各缓存一份,白白浪费内存。另外,如果业务方的temperature在0和0.1之间浮动,缓存也会miss,可以尝试在网关侧做参数归一化后再入缓存。
第三是流式响应。LiteLLM支持缓存流式请求,命中后按SSE格式回放缓存内容。这个功能很好,但注意回放时的chunk间隔不要太快,否则客户端可能因为接收速度异常而产生误解。
一致性上要诚实:相同prompt在这个模型身上每次输出的内容本来就可能有差异,缓存会把这个差异抹掉。如果你的业务对随机性敏感,就不要缓存所有对话,只挑适合缓存的场景。
4.4 缓存、限流、容灾的联动
缓存不只是省钱的工具,它还能缓解限流压力。缓存命中的请求不消耗上游RPM/TPM,也不消耗业务方预算,相当于给整条链路增加了一档“免费流量”。当某个Key达到限流阈值时,如果请求恰好是重复的公共问题,缓存依然可以返回答案,避免“限流把该答的问题也拦了”的尴尬。
容灾场景下缓存也能兜底。上游全部故障的时候,缓存里还有最近的有效答案,至少保证一部分高频请求可用。但要小心一个坑:缓存Key必须包含model_name,不能把模型A的答案缓存在模型B的命名空间下。否则容灾切换后,用户问同一个问题,可能拿到另一个模型生成的完全不同的回答,那才叫事故。
我习惯在每个模型和每个Key维度分开观察缓存命中率。命中率突然下降,往往是流量结构变了或者有人改了参数,命中率突然上升则可能是某个脚本在刷同一个请求,这两种情况都需要看一眼。
5. 模型容灾:上游挂了,服务不能跟着挂
5.1 重试、冷却、fallback是怎么配合的
模型容灾不是“配置一个备用模型”这么简单,它其实是一整套自动恢复机制,核心是四个参数:retries、allowed_fails、cooldown_time、fallbacks。
先看单个请求的路径:router把请求发给主模型,如果失败,先在本批次内自动重试,重试次数由retries控制。重试仍然失败,则根据错误类型判断是否适合切换——超时、5xx这类瞬时错误适合重试和切换,4xx无效请求重试没意义。fallbacks则指定当主模型不可用时,按优先级依次尝试的备选模型。
再看健康状态管理:一个上游连续失败次数达到allowed_fails时,router会把它标记为冷却状态,在cooldown_time设定的一段时间内不再向它路由流量。冷却结束后它自动恢复,但下一次路由前还会有一次预检查。
这套组合的效果是:单个请求失败不会影响整体,连续故障会被自动隔离,隔离期结束后又能自动回归。不需要运维半夜起来改配置,这是容灾最核心的价值。
配置示例:
router_settings: routing_strategy: usage-based-routing-v2 retries: 2 allowed_fails: 3 cooldown_time: 120 fallbacks: - gpt-4o: ["claude-3-5-sonnet"]5.2 多部署模型组与动态路由策略
LiteLLM支持把多个上游部署挂到同一个model_name下面。比如model_name叫gpt-4o,底下可以同时挂OpenAI的gpt-4o和Azure的gpt-4o,网关会按路由策略把请求分发到不同部署上。这样做的好处是单一上游挂了以后,另一个还能接着扛。
路由策略有很多种。simple-shuffle是轮询,适合同质化部署;least-busy倾向把请求发给当前等待最少的部署;usage-based-routing-v2则是根据历史错误率、延迟、当前负载做综合打分,适合异构部署,比如OpenAI、Azure、自托管vLLM混在一起用。
我在生产环境用的是usage-based-routing-v2,配合fallbacks。实测下来它对故障部署的感知比轮询快,因为错误率会实时影响路由权重,等于一个隐性的健康检查。不过路由策略更新需要观察一段时间才能稳定,刚切换配置的头几天要盯着流量分布,别一上来就全量压上去。
5.3 容灾切换的边界条件
容灾不是把所有请求都往备选模型上切就完事,有四个边界条件比路由本身更重要。
备选模型的能力要和业务需求匹配。如果主模型支持function calling,备选模型不支持,切过去以后业务方的工具调用直接报废。所以fallback列表要做能力矩阵测试,不只是测通不通,还要测关键特性。
超时时间不能设置得太长。如果主模型一直处于“挂起但不报错”的状态,一个30秒超时意味着请求要等30秒才能fallback,这个延迟对线上用户来说就是事故。我建议模型级timeout设在20到30秒之间,对于对话场景可以再短一点。
fallback链条不能形成环。A失败切B,B失败切A,两边都不行的时候请求在两个模型之间反复横跳,最后超时。配fallbacks之前要手画一遍依赖关系,确保它是DAG不是环。
成本要监控。主模型挂了切到备选,如果备选是更贵的模型,容灾期间的账单会明显上升。我在告警规则里专门加了一条:容灾期间的每分钟花费如果超过正常时段三倍,立刻通知。容灾可以接受短暂溢价,但不能无感烧钱。
5.4 一次真实的故障演练
配置完容灾不等于万事大吉,一定要演练。我的标准动作是每个月挑一个流量低谷时段,把某个上游的Key故意改错,模拟一次故障,然后观察整个链路的行为。
第一次演练就翻车了。我把OpenAI的Key改错,本以为请求会自动走Azure部署,结果流量确实切换了,但业务方那边报了一堆解析错误。排查下来发现业务方代码里对OpenAI的响应有一个特有的字段做了强依赖,换到Azure后的response结构基本兼容但那个多出来的字段变了,校验直接挂了。后来我让业务方把所有响应处理改成只依赖OpenAI兼容格式,并且把容灾用的备选模型也纳入了同样的兼容性测试。
演练完看效果:故障期间请求成功率维持在99%以上,P95延迟上升了不到300毫秒,冷却结束后流量自动回切到主模型。对比之前直连模式下40分钟的人工切换时间,这个自动化程度已经算可接受了。每次演练都要记录一个指标:从注入故障到全链路自动恢复的时长,这个数字应该越练越小。
6. 完整配置与上线自检
6.1 一份可以直接改的config.yaml
把前面四章的内容合并成一份可运行的配置,我贴一个精简但完整的版本。环境变量通过os.environ/前缀引用,密钥和数据库地址不要写死在文件里:
model_list: - model_name: gpt-4o litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY model_info: timeout: 30 rpm: 900 tpm: 100000 - model_name: gpt-4o litellm_params: model: azure/gpt-4o api_key: os.environ/AZURE_API_KEY api_base: https://my-azure-endpoint.openai.azure.com/ api_version: "2024-02-01" model_info: timeout: 30 rpm: 700 tpm: 80000 - model_name: claude-3-5-sonnet litellm_params: model: anthropic/claude-3-5-sonnet-20241022 api_key: os.environ/ANTHROPIC_API_KEY model_info: timeout: 30 rpm: 500 tpm: 60000 router_settings: routing_strategy: usage-based-routing-v2 retries: 2 allowed_fails: 3 cooldown_time: 120 enable_pre_call_checks: true fallbacks: - gpt-4o: ["claude-3-5-sonnet"] cache: type: redis host: localhost port: 6379 ttl: 300 namespace: "litellm-prod" general_settings: master_key: os.environ/LITELLM_MASTER_KEY database_url: os.environ/DATABASE_URL redis_host: localhost redis_port: 6379 max_parallel_requests: 200 alerting: - slack这份配置里gpt-4o挂了两个部署实现模型级容灾,claude-3-5-sonnet作为跨厂商fallback。缓存整链路开启,限流三层都配置了。database_url是必要的,虚拟密钥的持久化、预算记录都依赖数据库,建议用PostgreSQL。
6.2 上线前自检清单
配置写完不能直接上生产,我每次上线前都会过一遍自检清单,这里整理成表格:
| 检查项 | 操作方法 | 预期结果 |
|---|---|---|
| 限流是否生效 | 用压测工具打到2倍RPM | 网关返回429,计数器准确 |
| 多副本限流一致性 | 部署2个副本同时压测 | 合并后总量不超过限流值 |
| 虚拟密钥可吊销 | 生成测试Key,调用后删除 | 删除后立刻返回401 |
| 预算拦截 | 把max_budget设为0.01并调用 | 请求被拒绝,状态码符合预期 |
| 缓存命中 | 同一请求连续调用两次 | 第二次返回cache_hit标志,耗时明显下降 |
| 缓存key隔离 | 切换model_name后调用 | 不会复用其他模型的缓存 |
| fallback切换 | 临时改错主模型Key | 请求自动切到备选模型,无报错 |
| 冷却恢复 | 等待cooldown_time后观察 | 主模型恢复路由,不再持续失败 |
| 日志脱敏 | 查看线上日志 | 不出现完整API Key和请求body |
| 监控指标 | 访问/metrics | 有litellm_开头的deployment和spend指标 |
这份清单我每一行都踩过对应的坑,推荐至少全部执行一遍再正式切流。
6.3 监控指标与告警
LiteLLM自带/health/liveliness和/health/readiness探活接口,K8s部署可以直接挂到探针上。更好的观测入口是/metrics,Prometheus可以直接抓取,指标统一以litellm_开头。我会重点关注三类:deployment维度的success和failure数量、每个虚拟Key的spend金额、缓存命中率。
日志这块关键是请求级的trace信息,包括请求id、model_name、实际路由到的上游、耗时、token数、是否缓存命中、返回状态码。这些信息汇总之后既能做成本核算,也能排查限流误伤和缓存串key问题。
告警渠道我习惯用Slack的webhook接进来。告警规则除了常规的5xx比例升高,还要加两条:一个是某模型deployment连续失败超过allowed_fails且冷却触发,说明进入了容灾状态;另一个是spend金额在短时间内异常升高,通常是限流没拦住或者容灾切到了高价模型。告警不是越多越好,这几条是我压测和实际运行下来最有信号价值的。
最后想单独说一句我自己的体会:这一套系统刚落地时,最费时间的不是写配置,反而是把业务方的响应兼容性梳理清楚。缓存、限流、容灾本质上都是在给“不可靠的模型调用”增加缓冲,但如果下游只认某一种响应格式,容灾切换的价值就会大打折扣。我后来把所有内部服务对模型响应的解析全部收口到一个公共SDK里,只保留OpenAI兼容格式的字段,再去折腾网关的各种能力,明显顺了很多。这个顺序如果反过来,先把网关配置得花里胡哨,再回头改业务代码,会很痛苦。
如果你也正在做类似的事,我的建议是:先用最小的配置把网关跑通,再逐个叠加限流、缓存、容灾,每加一个能力都做一次压测和回放,不要急着一步到位。LLM网关的复杂度是慢慢长出来的,你越早开始观察线上流量的真实结构,后面的配置决策就越有底气。