如果你所在团队正在同时接入多家大模型厂商的API,比如OpenAI、Anthropic、Google Gemini,再混上几个国产模型,你大概率经历过同样的痛苦:各家请求参数不一致、流式返回格式五花八门、某家动不动就超时、限流策略还完全不一样,业务代码里写满了一堆switch-case判断走哪家厂商。我在做大模型应用时也踩过这个坑,最后沉淀下来的方案,就是一套从协议适配到智能路由的大模型API高可用网关。这篇文章把整套架构拆开讲一遍,包括为什么一定要有网关、核心模块怎么划分、协议适配层怎么做、智能路由如何实现、高可用保障有哪几板斧,最后再分享一些线上故障排查的实录。无论你是后端负责人还是AI应用开发者,这篇文章都能给你一套可以直接落地的实现思路。
1. 为什么大模型API网关不是“可选项”
1.1 协议碎片化是大模型应用的第一道坎
大模型厂商的API接口从设计第一天就没打算统一。OpenAI用的是/v1/chat/completions,请求体是messages数组;Anthropic 是/v1/messages,还多了system字段,角色体系也不完全兼容;Google Gemini 用/v1beta/models/{model}:generateContent,直接是contents和parts的结构。这还只是请求参数差异,流式响应更是各搞一套:OpenAI 是标准 SSE 里逐段抛出delta,Anthropic 拆成了message_start、content_block_delta多种事件类型,Gemini 又用的是类似 SSE 但字段完全不同的格式。
如果业务代码直接对接这些上游,每次新增一家模型厂商,就要在几个关键链路里打补丁:请求参数转换、鉴权头处理、流式解析、错误码映射。补丁打得越多,后面越不敢重构,因为一个小的协议兼容问题就可能引发线上事故。更现实的是,企业通常不会只签一家大模型厂商,渠道风险、成本谈判、不同模型在不同任务上的效果差异,都决定了你必须具备“随时在多供应商之间切换”的能力。而这个能力放在每个业务服务里重复实现,是极大的浪费。
1.2 稳定性和成本治理需要集中控制点
大模型API的稳定性天然比普通后端接口差。上游可能因为高负载导致延迟激增,可能因为地域网络问题连接超时,也可能因为限流策略变化直接返回 429 或 5xx。这些故障如果散落在业务代码里,每个服务各自处理,很难形成统一的应对策略。网关把“不可用”这件事集中处理,才能做重试、熔断、降级、灰度切换这些高级操作。
成本也是大问题。大模型调用是按 token 计费的,不同模型价格差几倍甚至十几倍。没有一个统一的汇聚层,你就很难统计每个部门、每个业务线在模型调用上花了多少钱,也很难从成本角度把流量动态调度到性价比更高的模型。网关天然是计费和审计的汇聚点,在这里记录 token 用量、单价、租户信息,比事后从各家平台导出账单再加工要准确得多。
1.3 自研还是选型:既要功能也要可控
现在市面上确实有 API 网关方案,比如基于 Envoy/Higress/APISIX 扩展 AI 能力,也有开源项目 LiteLLM 这样专注于模型代理的中间件。如果团队规模不大,接入的模型种类也不多,直接选成熟网关加插件是最高效的路径。但做大模型 API 网关这件事,并不只是“转发请求”这么简单,它涉及到企业内部的租户体系、成本核算、安全审核、私有化部署等定制逻辑,这种时候纯配置文件驱动的通用网关就很容易碰壁。
我倾向于把网关的核心链路做成自研,哪怕一开始功能阉割一点,也要保证模块边界清晰、可以独立演进。核心模块分为:接入鉴权层、协议适配层、智能路由层、容错降级层、观测审计层、动态配置层。每一层职责单一,层与层之间只通过内部标准模型通信。这套结构在后面加模型厂商、加路由策略、加故障演练时,都会让你庆幸当时没把代码都揉在一起。
2. 协议适配层:把“千厂千面”变成“内部统一标准”
2.1 适配层到底要处理哪些差异
协议适配是大模型网关里最枯燥但又最容易出错的部分。我总结下来的差异点主要有四类。
第一类是请求结构。OpenAI 风格是model + messages[{role, content}],Anthropic 把 system 单独提取,Gemini 则是system_instruction + contents[{role, parts}]。字段重命名还算简单,真正麻烦的是部分模型的特殊参数,比如 OpenAI 的response_format、Anthropic 的thinking、Gemini 的safety_settings,这些是各厂商的独家能力,统一模型里需要给扩展字段。
第二类是鉴权方式。OpenAI 用Authorization: Bearer sk-xxx,Anthropic 用x-api-key: sk-ant-xxx加anthropic-version头,Google 一般要求 API Key 放在 query 参数或者x-goog-api-key头里。适配层要屏蔽这种差异,让内部上游配置里统一维护“鉴权类型 + 密钥 + 附加请求头”。
第三类是流式响应。这是最折磨人的。SSE 并不是简单的整包 JSON,它是按行传输data: {json}的一个文本流,必须逐行解析;而且每个厂商在流里放的数据粒度不一样,OpenAI 每个 chunk 是choices[0].delta.content,Anthropic 在content_block_delta.delta.text,Gemini 在candidates[0].content.parts[0].text。如果不做适配,前端和后端就必须为每种上游实现独立的解析器,这显然不可接受。
第四类是错误码映射。上游返回 429、401、400、503,语义各有差异,但到了下游必须统一成业务能理解的错误结构。比如上游限流可能返回 429,也可能返回 200 但在流结束前报错,只有在适配层聚合处理才能保证错误格式一致。
2.2 内部统一模型的设计思路
我建议把 OpenAI 风格的请求结构作为“内部标准模型”,因为大部分模型厂商都主动兼容了 OpenAI 格式,而且业界生态工具最多,参考实现也多。但这不意味着你只能做 OpenAI 风格,而是要在内部定义一张中性结构表。
比如内部请求对象可以设计成:
{ "request_id": "751f44f4-1f1e-4a4e-8c16-1e9d1c0e1a2b", "model_alias": "fast-chat", "messages": [ {"role": "system", "content": "你是一个客服助手"}, {"role": "user", "content": "我的订单还多久到?"} ], "params": { "temperature": 0.7, "max_tokens": 1024, "stream": true }, "extensions": { "response_format": {"type": "json_object"}, "thinking": {"type": "enabled", "budget_tokens": 2048} }, "tenant_id": "t_1001", "trace_id": "tr_abc123" }这里有几个关键设计点:model_alias不是真实厂商模型名,而是路由层的输入;extensions用于存放各家特有参数,适配层决定哪些字段需要透传、哪些字段需要过滤;tenant_id用于成本核算和限流;trace_id贯穿全链路,排障必备。
适配层用策略模式实现,每一家厂商一个 Adapter,入站时把内部请求转换成上游协议,出站时把上游响应转回内部标准响应。内部响应结构也统一,普通响应包含content + usage + model_real_name,流式响应统一为delta事件流,在内部用自定义事件类型表示文本增量、结束事件、错误事件。
2.3 流式响应适配的工程细节
流式处理最大的坑在于“上游连接已经打开,但客户端可能随时断开”。我建议所有流式适配都基于上下文取消和背压控制实现,不要用同步逐行读取转发的简单方式,否则上游的推送速度远大于下游消费速度时,内存会被打满。
具体来说,内部Stream Event可以设计成三个类型:
text_delta:文本增量,统一从各个上游的增量字段提取。finish_reason:结束事件,统一携带停止原因和聚合后的 usage。error_event:错误事件,统一携带网关错误码、上游错误码、可读错误信息。
适配器在转换过程中要做一次 token 聚合,因为很多上游在流式结束时才返回 usage,但部分上游在中间就给了累积 token。如果业务有实时计费需求,可以按 delta 聚合自动累加,最后以finish_reason事件为准。
还有一个隐蔽问题:上游在流式中途返回错误时,HTTP 状态码可能还是 200,只在 SSE 内容的最后拼了一段错误信息。处理方式是适配层必须做“最后一帧校验”,如果流内容里带错误标记,要将整体状态改为失败,而不是让客户端以为自己拿到了完整结果。这个校验逻辑最好是放在适配层与路由层之间,避免每个上游各自实现一遍。
3. 智能路由层:不只是“分流”那么简单
3.1 路由维度拆解
大模型网关里的“路由”比传统网关复杂得多,因为你不只是按 URL 前缀分流,而是要根据模型能力、成本、稳定性、租户等级做多维决策。
最简单的是模型别名路由。业务方的请求不写死厂商模型名,只写fast-chat、smart-agent、embedding-zh这类别名,路由层维护一张“别名到真实模型池”的映射表。这张表可以动态修改,比如某天运营把fast-chat从 A 厂商切换到 B 厂商,业务方无感。
第二层是租户路由。不同租户可能有不同预算、不同合规要求。有的租户只允许走国内模型,有的租户可以在多个供应商间调度。租户信息在鉴权层解析后透传给路由层,路由规则中支持“按租户强制指定模型池”,比业务方自己在请求体里传参数更安全,避免配置泄露。
第三层是请求属性路由。比如根据 prompt 长度切分:超过一定 token 长度的请求走上下文更长的模型,短请求走低延迟模型。这一层做起来比较有意思,需要你在网关里预估 prompt 的 token 数,很多语言模型的 tokenizer 是可以通过轻量实现预判的,不需要调用上游。
第四层是成本优先路由。你可以给每个模型池打一个成本分,默认情况下流量优先走便宜、效果还不错的模型,只有业务方显式指定model_alias为高优先级时,才走高价模型。在这个机制下,技术团队可控成本,而不是让每个开发自己决定用哪个模型。
3.2 动态权重和健康探测
模型池内部不能永远平均分配流量,必须能实时感知上游健康状况。静态权重只适合作为初始值,生产环境一定要加动态权重调节。
动态权重的思路是:维护一个上游健康度评分,每个评分由滑动窗口内的错误率、P99 延迟、平均响应 token 速度共同计算。当某个上游的错误率高于阈值或者 P99 延迟超过基线 30% 以上时,权重自动调低;连续几个周期恢复后,权重再逐步回调。这样做的好处是流量不会瞬间全部切走,避免某个上游刚恢复又被压垮。
健康探测建议用主动探测和被动探测结合。被动探测就是根据真实请求统计错误率、超时率;主动探测是网关定时发起一个轻量请求,比如调用models/list或一个极短的 embedding 请求,确认上游进程活着。注意主动探测频率不要太高,建议 10 秒到 30 秒一次,并发数控制在 1 到 2,否则会影响上游正常业务配额。
3.3 故障切换的等价模型池
故障切换的前提是“有可切换的目标”。不同供应商可能托管同一个开源模型,比如同时有几个云厂商提供 Qwen、Llama 系列的托管服务;同一家供应商也可能提供同模型不同区域部署的接入点。把这些等价模型放进一个equivalence group,路由时按优先级列表尝试。
切换条件一般分为硬切换和软降级。硬切换针对“连接失败、连续 5xx、401 鉴权失败”;软降级针对“上游 P99 延迟超过设定值、错误率超过 1% 且持续 5 分钟”。硬切换要立即把该上游从当前请求的候选列表中剔除,软降级则是动态调低权重。
这里有一个经验:不要在鉴权失败时无脑重试。401 或 403 往往意味着 API Key 配置错了,重试一万次也是同样的结果,还会触发上游的账号级惩罚。网关应该把 401/403 归类为“不可重试错误”,立即返回并告警;只有 429、503、连接超时才走重试逻辑。
3.4 路由决策的缓存与预判
路由层如果每次都做一大堆计算再决定目标,这会增加网关自身的延迟。建议把路由决策做成两层:第一层是“快速路径”,根据 model_alias + tenant_id 直接查一张预计算好的映射表,命中则直接转发;第二层是“计算路径”,用于多条件路由和动态权重,产出的结果写回缓存。
同样地,token 预判结果也要缓存。一个很长的 prompt 反复出现时,不需要每次都重新算 token,可以按 message content 的哈希缓存预估结果。不过要注意缓存淘汰机制,避免缓存无限增长拖垮内存。
4. 高可用保障:限流、熔断、重试和降级
4.1 限流的多维度设计
网关入口必须做全局流量控制,但大模型场景的限流维度比普通 API 更多。除了 QPS,还要看 token 速率和并发连接数,因为一个流式请求可能占用上游连接很久,QPS 不高但连接数可能已经打满。
限流方案我建议用多级令牌桶:第一级是网关全局 QPS 限流,保护下游;第二级是按租户限流,确保一个租户的突发流量不影响其他租户;第三级是按上游供应商的配额限流,防止把某家厂商的配额打爆。第三级的配额要略低于实际购买量,留出安全缓冲。
流式输出的 token 限流需要单独处理。不要让网关一次性把上游吐出来的数据全部转发,可以在网关内做一个小型缓冲+平滑输出,控制每秒下发 token 数。这个能力在做企业级产品时特别有用,比如免费用户限制输出速率,付费用户不限制。但实现时要注意背压,如果下游消费慢,缓冲区不能无限增长,要主动降低从上游读取的速度,甚至触发流控断开。
4.2 熔断器不能一刀切
熔断器我推荐用经典的“三态模式”:Closed、Open、Half-Open。Closed 状态正常转发,用滑动窗口记录最近 N 个请求的错误率;错误率超过阈值,熔断器置为 Open,直接快速失败,不再请求上游;经过冷却时间后,置为 Half-Open,放少量探测请求,探测成功则关闭熔断器,失败则继续 Open。
关键点是熔断粒度。不要做全局熔断,否则一个供应商故障会让所有模型调用都失败。建议按“上游实例 + 模型名”维度做熔断,比如 A 供应商的 gpt-4o 熔断,不影响 A 供应商的 gpt-4o-mini 或 B 供应商的等价模型。同时熔断判断要基于调用量,如果某条路由 QPS 本来就低,错误率波动很大,需要用“最少请求数”做保护,比如只有最近 1 分钟内请求数超过 20 次才开始计算错误率,避免误判。
4.3 超时控制要分首包和总耗时
大模型请求和普通接口最大的区别是处理时间长。普通 HTTP 请求 3 秒没返回基本可以断开,但大模型流式请求可能需要几十秒甚至几分钟才能完整输出。如果统一用连接超时+读超时,很容易误杀正常的长任务。
我建议把超时拆成三组:连接超时、首包超时、总时超时。连接超时就是 TCP 建连时间,一般 3 到 5 秒;首包超时是从请求发出到上游返回第一个字节的时间,可以根据模型复杂度设置为 15 到 60 秒;总时超时是完整流式会话的最长时间,建议设置为 10 到 30 分钟,同时要允许业务方在请求参数中显式调整。
超时实现上要用上下文取消,不仅要关闭网关到上游的连接,还要告诉上游“这个请求被取消了”,释放上游的计算资源。很多供应商支持 cancellation token 或者主动关闭 SSE 连接来触发取消,网关要正确传递这个信号。
4.4 缓存和优雅降级策略
对大模型响应做缓存是比较敏感的话题,因为生成式输出通常不是幂等的。但在特定场景下缓存非常有效:比如 embedding 向量、简单的问答、固定 prompt 模板的输出,这些内容重复率很高,缓存能显著降低成本和延迟。
语义缓存是一个进阶做法,对 prompt 做 embedding,然后在向量数据库里找近似结果,相似度超过阈值就直接返回历史回答。这种做法在客服机器人和 FAQ 场景效果很好,但要注意设置相似度阈值,避免答非所问,同时要记录缓存命中的日志,便于回放和调整。
优雅降级是最后一道防线。当所有上游都不可用时,网关不能直接返回 500,而要返回业务可识别的降级结构。我建议降级分三级:优先返回缓存结果;没有缓存则返回预设的兜底话术;连兜底都没有,就返回一个明确标记了gateway_degraded的错误结构,让业务方能区分“模型能力不可用”和“网关自身故障”。这样才能让上层系统做合理的用户体验,而不是展示一个莫名其妙的“网络错误”。
5. 性能优化与动态配置的实现要点
5.1 连接池和 HTTP/2 多路复用
大模型网关是 IO 密集型服务,连接管理直接决定性能。上游 API 大多数支持 HTTP/2,建议网关到上游的连接都走 HTTP/2,这样可以在一条连接上并发多个流式请求,减少握手次数。连接池需要设置最大空闲连接和最大并发流数,不要一味调大,因为流式请求会长时间占用连接,连接池上限设置过大会导致系统文件句柄耗尽。
网关自身的对外服务建议也支持 HTTP/2,因为流式响应用 HTTP/2 可以更好地支持服务端推送和多路复用。不过要注意,部分老客户端只支持 HTTP/1.1,网关在回源响应时要兼容两种协议,特别是 SSE 在 HTTP/1.1 下只能用 chunked 传输,需要正确设置Transfer-Encoding。
5.2 异步流式转发模型
大模型网关的响应时间很长,如果每个请求占用一个线程或进程,系统并发能力会非常差。以 Go 为例,用 goroutine 可以轻松处理高并发;如果技术栈是 Java,请务必使用 Netty 或 WebFlux 这类非阻塞框架。我在早期用同步 Servlet 做网关时,仅仅 200 路并发就把线程池打满了,后面全部改为响应式后才真正具备生产可用性。
异步转发时要注意缓冲和上下游速度匹配。上游发送速度过快、下游消费速度慢,会导致写缓冲堆积。实现上要遵循“背压”原则,下游不可写时暂停读取上游的数据,避免把数据全部读入内存。这个控制在 Java 的 Project Reactor 里可以用limitRate,在 Go 里可以用带缓冲的 channel 配合 select 控制。
5.3 动态配置中心接入
网关的路由规则、上游密钥、权重配置绝对不能写死在代码里。我的做法是:所有配置在启动时从配置中心加载一次,本地缓存到内存;配置中心在更新时推送变更,网关收到变更后刷新本地缓存,刷新过程要求不影响正在处理的请求——也就是先写新配置到后备存储,在下一个请求进来时再切换读取。
具体实现可以用读写锁或者原子引用。比如路由表是一个AtomicReference<Map>,更新时创建一个新的 Map,然后原子替换引用,这样读请求永远只看到旧配置或新配置的完整状态,不会出现读到一半的情况。密钥信息建议加密存储,在配置中心保存密文,网关内存中保留解密后的明文,并定期轮换。
5.4 观测指标与日志审计
网关是政策执行的汇聚点,也是排查故障最关键的节点。我强烈建议一开始就把指标、日志、链路追踪三件套做好,否则等流量起来了再补会非常痛苦。
指标至少包含这几类:请求量(QPS、成功/失败/熔断/限流次数)、性能(TTFT、TPOT、总耗时分位数)、token 消耗(输入/输出 token、成本估算)、路由分布(每个 model_alias 到上游的流量比例)。这些指标打到 Prometheus,用 Grafana 看板展示,成本数据可以每天落库汇总。
日志层面,每个请求必须打印一条访问日志,包含 request_id、trace_id、tenant_id、model_alias、上游实例、耗时、token、错误码。同时要保留 prompt 和完整响应的摘要,但注意脱敏,不要记录完整的密钥信息和敏感个人数据,这也是企业合规的基本要求。流式请求的日志量会比普通接口大很多,建议抽样记录响应体,比如每成功 100 次记录一次完整响应,错误响应全部记录。
6. 线上问题排查实录与经验速查
6.1 案例:上游 401 鉴权失败导致重试风暴
我们曾遇到过一个诡异现象:某一天早上开始,网关到某供应商的错误率飙升,但上游控制台显示的调用量并没有明显上涨。排查日志后发现问题很呆——运维轮换密钥时把新 key 配置错了,网关收到 401 后,因为重试逻辑里把 401 归类为了可重试错误,于是一个失败请求重试了 3 次,每个请求叠加 3 倍调用量,最终触发了上游的账号级限流惩罚。
后来我们把所有上游错误码做了细分,401/403 直接归类为“致命错误”,不重试、不熔断、不降级,只告警;429/5xx 才走重试。同时在密钥配置更新时增加校验步骤,先手工调用一次上游连通性检查,再切换线上流量。这个案例说明,网关里最便宜、最有效的高可用设计就是“正确地不重试”。
6.2 案例:客户端断开导致上游连接泄漏
有一次我们发现网关的内存和连接数不断上涨,gc 压力很大。查监控发现是流式请求的响应管道没有在客户端断开时关闭。客户端在收到一半时点了停止,网关侧却还在继续从上游读取数据并写入响应缓冲,直到上游流结束才释放,而这些连接本来可以更早回收。
解决方法是网关在收到客户端断开的信号后,立即触发上下文取消,关闭上游的 SSE 连接,同时把连接池中的连接标记为可复用。这里要特别注意,连接池如果复用了一个没有完全读完的 SSE 连接,会把残留的流数据发给下一个请求,导致非常隐蔽的数据串线问题。我的经验是:只要是流式连接,客户端断开了就强制关闭底层 TCP,不要归还连接池。
6.3 案例:上游返回 200 但内容为空导致客户端悬挂
我们曾经遇到过某个模型供应商在某些 prompt 下会返回 200,但 SSE 流一直没有数据,或者发一个空包就结束。网关如果没有首包超时,客户端就会一直转圈。我们在适配层增加了“首包事件超时”和“流式空响应检测”,如果上游在指定时间内没发任何 data,或流已结束但内容为空,网关直接返回 502 并在错误信息里注明上游空响应。
要处理这种情况,需要网关在流式读取时自己做一层事件级别的超时控制,而不是完全依赖 HTTP 层的读超时。因为很多 HTTP 客户端在读超时时间内可能已经收到了一些 SSE 空行,不会触发读超时,但业务内容仍然为空。
6.4 典型问题排查速查表
| 现象 | 可能原因 | 排查手段 | 解决建议 |
|---|---|---|---|
| 全部请求超时 | 上游账号欠费/网络断连 | 查看网关到上游连通性,测试直连 API | 快速切换备用供应商,检查密钥额度 |
| 只有部分流式请求中断 | 客户端断开未取消上游 | 看网关内存和连接数是否持续增长 | 实现客户端断开感知,强制关闭底层连接 |
| 上游错误率不高但路由权重突变 | 动态权重评分因子设置太敏感 | 查看评分指标,看 P99 是否抖动 | 调整延迟权重系数,增加最小样本数 |
| 限流误伤正常用户 | 网关令牌桶容量/速率配置过小 | 看限流拒绝率与 QPS 曲线对比 | 提高容量,增加按租户多维限流 |
| 切换供应商后业务报错格式不兼容 | 适配层错误结构未转换 | 抓取网关错误日志,对比内部错误结构 | 补齐错误结构映射,并在测试环境做故障演练 |
6.5 上线前的故障演练建议
网关这类基础设施,最怕的是逻辑看着完整,但真正故障来临时发现某个环节没覆盖到。我强烈建议上线前做一次混沌演练:把某个上游供应商的 API Key 改成错的,观察网关是否会快速切换到备用路线;把某个供应商的接口延迟人为拉高到 10 秒,观察熔断器和动态权重是否生效;模拟一个流式请求中途断开,观察连接和内存是否持续增长。演练结果要记录成文档,同时把每次演练的结论固化到自动巡检脚本中,让这些能力不是“纸面高可用”,而是真正经受过考验的高可用。
我个人的体会是,做大模型 API 网关,最容易犯的错误是一开始把所有精力都放在功能和协议适配上,忽略了自身的高可用设计。网关一旦挂了,所有上游模型都不再可用,这比单个上游故障更致命。所以请务必做到网关自身的无状态化、水平扩展、多可用区部署,以及所有配置变更可回滚。最后再分享一个小技巧:把上游返回的原始错误响应体完整记录到日志中,不要只记录错误码。因为很多供应商的错误信息藏在一长串 JSON 里,没看原始响应,你很难判断到底是参数问题、内容审核问题还是账号问题,有了原始响应体,排障效率能提升一个数量级。