构建200+AI模型统一API网关:语义适配层设计与实践
发布时间:2026/10/20 4:42:14 锦皓数字建站

先说个背景吧。我所在的小组主要负责给公司内部各种业务方提供 AI 模型接口最开始只有 10 来个模型大家各自调各自的业务代码里塞了十几套 SDK还能勉强运转。后来模型数量一路膨胀不到两年涨到了 200而且大部分是不同云厂商、不同开源项目、不同微调版本的模型有的走 HTTP 原生接口有的走 gRPC有的能流式返回有的只能一次性吐完。到这个时候我每天干得最多的事情已经不是写代码而是帮业务同学查“为什么某个模型又超时了”“为什么同一个模型在两个渠道返回格式不一样”以及最让人头疼的——“这个模型是不是已经下线了业务代码里还在调它”。于是项目就顺理成章启动了为 200 个 AI 模型构建统一 API 网关。这篇文章不打算讲那种“从零到一的高大上架构升华”我尽量把真实的问题、选型逻辑、参数设计、线上踩坑都摊开来说。目标读者是那些正在被多模型、多渠道、多协议折磨的后端工程师或者准备做模型接入平台、AI 基础设施的人。看完你至少能回答几个问题为什么需要统一网关网关内部到底分层分哪些路由怎么做限流怎么做以及最关键的当你接入第 201 个模型时为什么新增成本可以低到“只改两行配置”。下面是正文。1. 为什么必须自己搞一个统一网关先说结论我们并没有直接从开源方案里挑一个拉起来就用而是花了大量时间梳理需求最终做了一套带着“模型归一化”能力的网关。核心原因在于常规 API 网关解决的是“路由、鉴权、限流”但 AI 模型接入场景里最痛的点反而是“协议不一样、参数语义不一样、返回结构不一样”。1.1 多个模型混用的真实痛点我举一个很典型的例子。早期业务方调用模型代码里大概长这样# 调模型A resp model_a_client.chat( modelmodel-a-v2, messages[{role: user, content: 你好}], temperature0.7, ) # 调模型B resp model_b_sdk.send( modelmodel-b, prompt你好, top_p0.9, max_new_tokens512, )这两个还只是参数名不同有的模型用temperature有的叫temperature_scale有的最大生成长度叫max_tokens有的叫max_new_tokens有的返回choices[0].message.content有的直接返回data.text有的错误信息里带 HTTP 状态码有的所有异常都返回 200 然后把错误码塞在 body 里。这不是接口风格差异这是灾难。当模型数量到 20 个的时候业务方还能忍到 100 个的时候业务方直接拒绝在代码里再新接一套 SDK。他们不关心底层是哪个模型他们只想要一个稳定的接口传进去一段话拿到一段回复。我们的核心任务就是把上面这种混乱收敛成一个标准接口所有底层差异由网关内部消化。1.2 为什么普通 API 网关解决不了这个问题我并不是说现成的 API 网关不好而是它们大多数在设计时没有考虑“模型调用”的特殊性。通用网关擅长的是流量治理路由到某个后端服务、灰度、限流、鉴权。这些特性我们也要但它们只解决了外层问题。内层问题有几个是 AI 场景独有的参数语义归一化不同模型的temperature、top_p、max_tokens虽然含义相似但取值范围、默认值和行为存在细微差别。比如有的模型 temperature 支持 0 到 1有的支持 0 到 2直接透传会导致某些模型返回奇怪结果。返回格式归一化把所有上游响应统一成 OpenAI 兼容格式是业务方最喜欢的一个设计。他们不用再关心上游是“文本直接返回”还是“choices 嵌套结构”。流式协议差异有的模型用 SSE 流式有的用 WebSocket有的原本不支持流式但我们要在网关侧做伪流式。这个后面专门说。模型下线和替换今天 A 模型还跑得好好的明天因为生态原因要全部切到 B 模型。没有归一化层这种切换只能靠业务方改代码有了网关只需要改一条路由规则。所以我们的定位很明确它不是传统 API 网关的替代品而是在传统网关能力之上叠加了一层“模型语义适配层”。2. 统一网关整体架构核心是语义适配层如果你现在打开我们网关的架构图能看到下面这几层。我按从外到内的顺序讲这样更好理解一次请求从进入网关到返回给用户到底经历了什么。2.1 整体分层概览分层大概是这样的接入层负责接收客户端请求维护 HTTP/HTTPS 连接支持 SSE 长连接。这一层只做最基本的连接管理、TLS 终止、header 解析不碰业务逻辑。解析与鉴权层解析 API Key、识别租户、解析统一请求体校验参数格式做基础限流。路由决策层根据请求里的模型名结合路由表和上游健康状态决定转给哪个具体的模型实例。这一层还负责版本调度、灰度切流。语义适配层这是最核心的一层它把统一协议的请求转换成上游模型自己的协议再把上游返回转换成统一协议。参数映射、提示词模板注入、错误归一化都在这里。出口转发层维护到各个上游的连接池、超时控制、重试策略负责把适配后的请求真正发出去。可观测与计量层记录 token 消耗、请求耗时、错误码、成本信息输出指标和日志。2.2 为什么把协议统一成 OpenAI 兼容格式选择哪种协议作为对外统一协议我们内部吵过几次。有人说直接用 gRPC效率更高有人说用自己的内部协议不受外界影响。最后还是选了 OpenAI 兼容的 HTTP 协议。原因特别现实业务方对 OpenAI 的请求结构已经很熟了外部生态里也有大把现成的 SDK、工具链可以直接用。我们的内部协议再优雅业务方学习成本高生态工具用不上注定推广困难。统一格式的关键路径是这样请求体标准格式 - model: 虚拟模型名比如 qwen-max、maid-plus - messages: 消息列表role content - temperature: 0~2 浮点数不传用默认 - max_tokens: 最大生成长度 - stream: true/false是否流式 响应体标准格式 - id: 请求ID - choices: [{message: {role: assistant, content: ...}, finish_reason: stop}] - usage: {prompt_tokens, completion_tokens, total_tokens} - model: 实际被路由到的模型名所有上游返回不管原本是 JSON 直接返回还是 lines 分割最终都要映射到这个结构。这样对外部来说200 个模型只是一个 model 字符串的区别请求和响应结构完全一致。2.3 适配器模式新增一个模型为什么只要写配置这里要展开讲一个核心设计Provider Adapter。网关内部不允许出现“if 模型名 xxx 则处理”这种代码。因为 200 个模型如果写成几百个 if代码会膨胀到完全没法维护。我们用的是适配器模式每种上游协议风格对应一种适配器实现模型之间的差异通过配置项来完成而不是靠写新代码。对于不同渠道我们抽象了以下基础接口build_request(model_config, unified_request)把统一请求转换成上游协议请求。parse_response(model_config, upstream_response)把上游响应解析成统一响应。build_stream_request(...)和parse_stream_chunk(...)处理流式情况。map_error(...)把上游错误码映射成统一错误码。如果你要新接入一个风格完全一样的模型比如同一家云厂商的新模型很可能只需要在配置中心添加一条模型映射记录不用写任何代码。只有当上游协议出现新风格时才需要新增一个适配器实现这大概是我们能支撑 200 模型的底气。3. 路由层设计模型名背后的虚拟映射路由层是整个网关容易被低估的部分。很多人觉得路由就是把 model 字段直接拿来转发但实际上我们用的是一套“虚拟模型名 路由策略”的设计。3.1 虚拟模型名与实际模型实例解耦业务方看到的 model 名是一个虚拟模型名。它不是某个物理模型的标识而是一条路由规则的入口。举个例子route: - name: chat-zh # 业务方看到的虚拟模型名 priority: 1 strategy: weighted # 权重策略 targets: - provider: aliyun model: qwen-plus weight: 70 - provider: self-host model: qwen-14b-gptq weight: 30 fallback: - provider: openai model: gpt-4o-mini当一个请求带着modelchat-zh进来时网关会根据当前权重和目标可用性分派到两个实例中一个。如果这两个实例都失败再降级到 fallback。业务方完全感知不到这个灰度过程。这种设计给我们的运维带来了极大弹性。比如某厂商模型定价突然上调了我们想切一部分流量到自建模型只需要调整一下权重不需要通知业务方改任何代码。这在直接调用上游的模式下是做不到的。3.2 路由决策不只看权重还要看可用性权重只是静态配置线上运行还要考虑目标上游是否健康。我们在路由决策层维护了一张“上游健康状态表”由后台定时探活任务更新。每次路由时如果某个目标连续失败次数超过阈值会在短时间内自动摘除直到探活恢复。摘除逻辑借鉴了熔断的思路连续失败 5 次标记为不健康进入探测期每分钟探测一次探测成功恢复到健康状态从摘除到恢复全程业务无感。这套机制曾经帮我们扛过一次很大的事故某个外部模型在下午突然开始大面积超时路由层在几分钟内自动把流量摘走切到了备用通道。业务方只看到 P95 延迟稍微涨了一点没有看到大面积报错。4. 流式响应处理200 模型里最容易被忽略的硬骨头统一网关开发周期里流式处理消耗的时间比想象中多得多。流式背后的坑太细了不做大规模接入根本意识不到。4.1 三种流式协议的统一上游流式风格主要有三种OpenAI 风格 SSE按data: {json}分隔最后发data: [DONE]。Claude 风格 SSE事件类型很多有message_start、content_block_delta、message_delta等。普通增量文本没有标准事件格式直接吐文本片段。我们在适配层做了一层“统一流式语法”网关向上游发送流式请求时根据适配器转换为对应协议上游返回流式数据后适配层把每个 chunk 归一化为{choices: [{delta: {content: 片段}, finish_reason: null}]}然后由网关接入层对外统一输出 SSE 格式。这里有一个特别容易踩的坑SSE 格式中换行符和空行不能随便处理。很多上游会在流式数据里杂带注释行或者把多个 JSON 拼在同一行如果接入层直接按行切割就会出现解析失败。我们最终实现的是一个相对健壮的 SSE 解析器核心逻辑是按事件类型区分数据行和注释行并且兼容 CRLF 和 LF。4.2 流式转发里的背压与缓冲流式接口的另一个难点是背压。上游生成速度是我们无法控制的如果下游客户端读取速度慢网关如果无脑把数据往连接里写内存会被拉满。我们用了两套策略缓冲区上限每个流式响应连接维护一个有限缓冲队列超限时暂停从上游读取让上游自然阻塞。客户端断连感知如果客户端断开必须立刻取消上游的流式请求而不是继续读完整个生成。这两个策略看着简单但实现细节很磨人。尤其是 Go 语言场景下要特别小心 goroutine 泄漏客户端断连后上游 stream 的终止信号没有及时传递导致后台一直在读数据直到把上游的生成流程读完才释放连接。这个问题在高峰期可能表现为连接数暴涨、内存持续增长排查起来非常隐蔽。4.3 伪流式上游不支持流式也得让它“能流式”还有一批上游模型压根不支持流式但业务方的需求是“所有接口都要支持流式输出产品交互统一。”我们做了伪流式方案网关向上游发送非流式请求拿到完整文本后模拟成多个带延迟的 SSE chunk 一个个推给客户端。这样产品体验没问题但代价是首字延迟变高了因为必须等上游完整生成完才开始推送。这个方案的取舍很清晰对延迟不敏感的场景如写邮件、生成离线文案完全可用。对实时交互场景聊天对话建议还是接原生流式。所有伪流式请求必须在指标上打上fake_streamtrue的标签避免和真流式数据混在一起造成评估偏差。5. 鉴权、限流与配额多租户场景怎么做才不翻车统一网关一旦被多个业务方共用权限和配额就不再是一个简单维度。我们踩过很多坑下面说几个关键设计。5.1 API Key 与租户隔离每个业务方通过网关的 API Key 标识。同一租户下可以创建多个 Key支持不同权限范围比如有的 Key 只能访问模型 A有的 Key 只能访问模型 B。实现上用中间件统一处理解析 Key - 查租户 - 加载权限 - 绑定上下文。这里唯一的忠告是权限校验结果一定要缓存而且要设置合理过期时间。一开始我们每个请求都实时查数据库压测时数据库直接成为瓶颈后来改成本地缓存 分布式缓存两级权限变更最多 1 分钟生效性能提升了不止一个量级。5.2 限流的粒度不只有 QPSAI 场景的限流和普通 API 限流差别很大因为一个请求可能持续几十秒且消耗的资源不是固定值。我们最终采用了多维限流限制维度单位说明QPS个/秒限制每秒请求次数并发数个限制同时在途的请求数Token 速率tokens/分钟限制每分钟消耗的 token 总量成本配额元/天限制每天的调用成本上限每一个维度单独计数任何一个维度超限都会触发拒绝或排队。特别是成本配额这个一开始我们没有做结果某个业务方的脚本跑飞一次性调用了大量高价格模型账单一出来我们都傻眼了。限流算法我们用的令牌桶支持平滑突发流量。比如 QPS 限流 100允许短时间冲到 150但如果持续超限就会开始拒绝。为什么不用固定窗口固定窗口在窗口边界容易产生双倍突发令牌桶更平稳。5.3 配额超限后的降级提示限流触发后的错误信息必须足够清晰不能只给一个 429 状态码。我们要告诉调用方是哪个维度超限了。比如{ error: { code: rate_limit_exceeded, message: concurrency limit exceeded, detail: {limit: 20, current: 21, reset_after: 30s} } }这样业务方才能自动化处理比如重试或者降低并发。很多网关限流只给一个笼统的 429用户看一眼完全不知道怎么办这是体验的败笔。6. 可观测性没有多维指标根本无从维护 200 模型如果网关只有一两个接口随便打打日志就够了。但面对 200 个模型可观测性直接决定了你能不能快速定位问题。6.1 核心指标维度我们最终沉淀下来一套“四维指标”模型维度、渠道维度、租户维度、错误维度。模型维度虚拟模型名、实际模型名关注延迟、成功率、token 消耗。渠道维度按上游提供方聚合关注可用性、超时率、流式中断率。租户维度按业务方聚合关注 QPS、配额使用率、成本消耗。错误维度按错误类型聚合关注参数错误、上游返回错误、限流拒绝、超时。任何一个线上问题我们第一反应就是打开看板先按模型看有没有异常再按渠道看是不是某个上游在抖动再按租户看是不是某条业务线流量异常。这套路径几乎能覆盖 80% 的日常事故定位。6.2 token 计量为什么那么重要token 计量不能依赖上游返回的 usage 字段因为有些上游不返回完整 usage有些模型返回的 token 数明显不对。我们统一采用“自己估算 上游校准”的模式请求侧用 tokenizer 对输入 messages 做估算记录 prompt_tokens。响应侧优先用上游返回的 completion_tokens如果没有用字符数除以一个经验系数估算。成本侧根据单位 token 价格换算成本按租户、模型、时间聚合。这套数据对技术运营太重要了。比如某租户这个月成本涨了 30%我们可以很快看到底是哪个模型涨了、哪个时间窗口涨的、是调用量涨了还是价格涨了。没有这层统计财务对账就是一笔糊涂账。6.3 链路追踪与日志采样网关内部一次请求会经历接入、鉴权、路由、适配、转发、响应六个阶段任何一个阶段出问题都需要能快速定位。我们给每个请求分配了唯一的 request_id并在全链路日志中携带这个 ID所有阶段日志都打上去。日志采样分两层所有限流拒绝和错误请求全量记录正常请求按 1% 比例采样。切忌正常请求也全量打日志流量一上来磁盘 IO 和日志系统的压力会非常恐怖。7. 稳定性建设多级降级和容灾预案200 个模型里总有几个上游不够稳定。我们把稳定性建设拆成多级降级链让不同级别的故障对应不同策略。7.1 下游模型故障时的路由降级这个在路由层说过。核心流程是当主目标连续失败次数超阈值自动触发熔断熔断后进入降级路由优先选同渠道同系列的备选模型如果备选也不可用再降级到跨渠道模型如果全部失败返回统一的 502 错误并且错误信息里带上“当前模型均不可用请稍后重试”。为了避免降级逻辑把故障扩散降级路由的尝试次数有限制最多试两个备选再失败就直接返回防止请求耗时被拖到十几秒。7.2 模型下线时的动态迁移AI 模型的生命周期比传统服务短得多经常有模型发布新版本、旧版本强制下线的情况。如果某个旧模型被上游强制下线而我们还在往它那里发请求会持续报错。我们备了一套“模型下线迁移”的流程先通过配置把该模型从路由表中标记为deprecated同时设定一个过渡用的目标模型映射关系比如旧版 all-migrate 到新版。在过渡期内旧模型名依然可用但实际流量已经全部转到新模型。业务方完全无感等旧模型名彻底没有流量后再从代码中移除。7.3 连接池与超时参数连接池参数需要反复压测。我们最终用的几个关键参数是MaxIdleConnsPerHost: 64MaxConnsPerHost: 256客户端拨号超时: 5s上游响应头超时: 30s整体请求超时: 分场景设置不同模型的响应时间天差地别有的模型 2 秒就出完了有的模型可能要 60 秒。给所有模型一个固定的 30 秒超时会导致长任务模型大量失败。我们的做法是按模型类型设置超时模板比如对话类 60 秒、向量化类 10 秒、图像生成类 120 秒。这里有一个血泪教训一开始我们把整体超时设得太大当上游模型假死时大量请求挂在网关这边占着连接不释放最终把整个网关的连接池打爆后面所有正常请求都被阻塞。后来我们把超时拆成“等待响应头超时”和“整体响应超时”两个维度假死情况很快就能在上游响应头阶段被识别并掐断。8. 踩过的坑线上事故记录与排查思路写完上面这些我觉得还应该把这两年遇到的几个典型问题完整记录下来这些问题单靠看文档是碰不到的。8.1 上游返回非标准 JSON解析直接崩溃现象某个模型偶尔返回的流式 chunk 里混了业务原始文本文本里含换行符、注释行直接导致我们的 SSE 解析器崩掉后续所有 chunk 都无法解析最终客户端只能拿到半截回复。排查先抓现场报文发现不是我们的解析器不兼容而是上游根本没有按标准 SSE 协议输出。有些自建模型在生成过程中会打印调试日志到标准输出这些日志顺着流式响应混进了 data 通道。解决解析器兼容“非 JSON 数据行直接跳过”同时对不完整 JSON 的 chunk 做缓存拼接等一个完整 JSON 出现后再解析。这能兜住大部分非标准输出。8.2 gRPC 上游并发不足导致雪崩现象压测时发现某个自建 gRPC 模型服务的 P99 延迟从 200ms 一路涨到 5s随后大量请求超时。排查看网关指标发现连接池连接数很高但服务端 p95 延迟也高。最后定位到是 gRPC 服务端的最大并发流数量限制客户端一高并发就排队。解决在适配层给 gRPC 上游加并发信号量限制同时发往该服务的请求数宁可让客户端排队也不要让上游服务被压垮。类似的做法我们在很多模型服务上做了适配避免单个模型影响全局。8.3 客户端断连后网关还在继续生成现象有业务方在用户取消对话后直接关闭连接但网关日志显示上游那个模型调用仍然跑了很久token 还在继续涨。排查SSE 转发逻辑中忽略了客户端连接关闭事件没有立刻向上游发送取消信号导致上游继续生成完整个回复。解决在流式转发时监听客户端连接上下文一旦context.Context被取消立即向上游发出一个cancel请求并关闭对应的上游流式连接阻断 token 继续消耗。这个问题既是性能问题也是成本问题后面做了专项测试。8.4 限流参数太紧误伤正常业务现象某个租户反馈部分请求报 429但我们看他们的 QPS 远没有达到限制。排查发现并发数和 token 速率两个维度同时生效租户平时有大量长请求并发数很高大量长请求同时运算导致 token 消耗速率瞬时突增触发了 token 维度限流。解决把维度之间的触发关系改成“任一维度超限则只对该维度告警连续超限超过 N 秒才真正拒绝”拉长容忍窗口避免瞬时波动直接拒绝请求。长期来看还是需要按租户的业务形态做个性化配额设置不能一刀切。9. 常见问题速查表问题可能原因快速排查方法解决方案某个模型频繁超时上游服务不稳定看该模型渠道延迟指标开启自动熔断降级到备用模型流式回复半截SSE 解析失败抓原始报文流解析器支持非标准行跳过客户端断连后还在计费取消信号未传递看流式连接释放日志监听 Context立即向上游发送取消请求偶发 429 但 QPS 不高并发数或 token 维度超限看限流拒绝详情调整维度阈值和容忍窗口新增模型接入成本高无适配器看该模型协议类型优先写配置其次写适配器token 计数不准确上游 usage 缺失对比 token 估算值自研估算 上游校准上游假死堵塞网关超时设置不合理看响应头耗时指标拆分响应头超时和整体超时10. 一点个人体会如果你问我构建 200 模型统一 API 网关最重要的东西是什么我会说不是技术栈选得多新、不是结构设计得多炫而是“语义适配层”这个东西要做得足够抽象。它才是所有模型差异的缓冲区也是后续无限扩展的立足点。只要这层能保持稳定哪怕明天接入的新模型风格完全不一样我们也只需要新增一个适配器实现而不需要改任何对外协议。另一个体会是网关这类中间件永远不要追求“一步到位”。我们的第一版也很简陋路由表甚至放在 JSON 文件里。但是边接模型、边踩坑、边优化架构是在演进过程中自然长出来的。与其一开始就设计一个完美但没法落地的方案不如先把一条链路跑通再逐步把适配层、路由层、可观测层一层层加厚。踩过这么多坑之后我现在的原则很简单对外协议永远保持统一对内差异全部隔离在配置和适配器里。剩下的事就是让模型们在网关后面安安静静地干活别让业务方费心去理解它们各自的小脾气。这也算是这些年最值得分享的一点经验吧。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。