
做了两年多 AI 平台的研发我发现一个特别普遍的问题很多人一开始做 AI 应用的时候都默认只接单一模型的 API路由、鉴权、计费这些事全在应用代码里写死。等到业务要接第二个模型、第三个模型的时候代码开始失控等接到几十个、上百个模型的时候整个接入层就是一团乱麻。我最近把我们团队为 200 个 AI 模型构建统一 API 网关的完整过程做了一次复盘这篇文章想把里面的架构决策、关键实现和踩过的坑都讲清楚给正在被同样问题困扰的同学一个可参考的落地思路。这个网关项目要解决的问题其实很直接公司内部有多个业务方在同时使用不同供应商的模型服务有闭源的对话模型、开源的部署模型也有多模态和向量模型。每个供应商的接口格式不一样、限流规则不一样、计费方式也不一样。业务方如果各自接各自的光是维护协议适配代码就够喝一壶。所以我们要做一个统一的接入层把差异全部挡在网关内部对外只暴露一套干净、稳定的 API同时把路由、限流、容灾、成本控制这些横切能力全部沉淀在网关里。适合读这篇文章的人我觉得主要是两类一类是后端工程师正在给自己的团队搭建 AI 模型的接入层另一类是架构师在评估到底是给业务方发 SDK 还是做统一网关。我会把方案为什么是网关而不是 SDK、架构怎么分层、每个核心组件为什么要这么设计、实际运行中遇到哪些坑都写清楚。1. 先想清楚网关到底要解决什么1.1 早期接入多家模型时的真实混乱先还原一下没有网关时的场景。我们最早的时候每个业务方自己写一套接入代码A 业务方对接模型供应商甲的接口B 业务方对接模型供应商乙的接口。两家供应商的鉴权方式不一样一个用 Header 里的静态密钥一个用临时签名请求体和响应的字段也完全对不上。一个叫 prompt一个叫 messages一个是纯文本的补全格式一个是带角色的对话格式。业务方每次要换供应商都得改自己的业务代码改完还要灰度。最头疼的是模型版本的升级节奏各不相同一个上游供应商改了响应字段所有接了它家接口的内部系统都要跟着联动。有一次某供应商把一个错误码从 400 改成 422我们整整排查了两天才发现是几套业务系统各自解析错误码的逻辑不完全一致。这种状态本质上不是工程质量问题而是架构问题。业务方的关注点是业务逻辑但它被迫承担了供应商协议的适配工作这就是职责放错了位置。1.2 网关要解决的四个核心问题在动手做之前我先把问题收敛成了四个这四个问题决定了整个架构的形态协议归一化不管上游是哪种格式下游只看到一种统一的请求和响应结构。路由与切换业务方能通过同一个入口按需访问不同供应商的模型支持配置化的切换和灰度策略。横切治理鉴权、限流、配额、审计、成本统计统一在网关层做业务方不需要自己实现。容灾兜底供应商故障、超时、限流时网关能自动重试、切换、降级而不是直接把错误抛给调用方。这四个问题里我最看重的是第一个。因为如果协议归一化做不到位后面所有的路由、治理、监控都无从谈起——你连统一的数据结构都没有怎么在网关层做通用的统计和策略控制1.3 为什么是网关而不是 SDK关于做一个公共 SDK 还是做一个网关我们内部其实是吵过一轮的。SDK 的好处是接入成本低业务方加一个依赖就行但 SDK 有个致命问题它跑在调用方的进程里升级要靠调用方主动配合策略变更也没法实时生效。比如我们要调整一个模型的超时时间如果用 SDK你得先发版本再等业务方升级根本做不到快速止血。网关是集中式的策略可以实时下发故障可以在一个点处理。而且网关对协议归一化的约束力更强它天然是一个边界所有流量都从这里过谁也别想绕过标准。代价是多一跳网络延迟但这点延迟在模型动辄几秒的响应时间面前完全可以接受。所以最后的选择很简单网关为主SDK 只做一个非常薄的 HTTP 客户端封装不承载任何策略逻辑。2. 整体架构设计怎么把层级划清楚2.1 一条请求的完整路径先讲整体结构。整个网关对外只暴露一个标准接口请求进来以后依次经过接入层、通道层、治理层、路由层、适配层最后到供应商后端。响应返回时走同一条链路但每个环节做的事情不太一样。接入层负责最基础的 HTTP 接入做 TLS 终止、请求体大小限制、基本鉴权。通道层把一次请求抽象成一个独立的任务处理同步调用和流式调用两种模式。治理层做限流、配额检查和审计日志。路由层根据模型名、调用方身份、当前供应商的健康状态选出真正要打的供应商。适配层做协议转换把标准请求翻译成各家 API 的格式再把各家响应翻译回标准格式。这个分层的最重要原则是单向依赖上层依赖下层但下层永远不要感知上层。比如路由层不该知道业务方的租户 ID 是什么意思适配层更不该知道当前是灰度还是正式流量。一旦出现下层依赖上层的情况这个系统的可维护性就会快速崩掉。2.2 各个模块的职责边界我把具体模块的职责边界也定了下来模型注册表维护所有可路由模型的信息包括模型名、供应商、上下文长度、单位成本、当前状态。路由引擎根据配置的策略成本优先、可用性优先、权重分配决定路由目标。协议适配器每个供应商一套只负责格式转换不做任何业务判断。限流器按租户和按模型两个维度做配额控制。熔断器跟踪供应商的健康度失败率达到阈值时暂时摘除。事件总线把每次调用的完整生命周期事件发出去供统计和监控消费。最开始我犯过一个典型的错误就是忍不住在适配器里加业务逻辑比如在某个供应商的适配器里偷偷做模型映射。后来发现这样做会导致适配器之间逻辑不一致排查起来非常痛苦。适配器应该是纯粹的函数式转换层所有业务策略必须放到更上层的通用模块里。2.3 数据模型标准化的中间层是地基在网关内部我定义了一套标准的数据模型。一次请求被表达为一个 StandardRequest包含模型名、调用方身份、消息列表、参数列表、流式标记一次响应被表达为 StandardResponse包含回复内容、消耗 token 数、时延、供应商原始响应。所有模块都基于这套模型工作路由引擎读它、限流器统计它、审计模块记录它。只有最底层的适配器才和具体的供应商格式打交道。这样设计以后新增一个模型供应商的模式就固定成了三步写一个适配器做格式转换在模型注册表里登记模型信息在测试环境跑一遍协议兼容性测试。这套数据模型的设计给了整个架构确定性不管上游多乱内部永远是同一套语言。这也是这个项目里最核心的“统一”二字的落地方式后面我会专门展开。3. 协议归一化如何把 200 模型的差异挡在适配层3.1 适配器模式怎么落地协议归一化是整个网关里面最繁琐、也最容易出细节问题的一块因为每个供应商的 API 差异远不止字段命名不同。有的模型是纯文本补全接口传的是 prompt有的是对话接口传的是 messages 数组有的把 system、user、assistant 分得很细有的只支持有限轮数的上下文。所以在适配层我不建议用一堆 if-else 去判断供应商类型。我采用的是适配器模式每个供应商实现同一套接口class BaseAdapter: def build_request(self, std_request: StandardRequest) - ExternalRequest: raise NotImplementedError def parse_response(self, external_response) - StandardResponse: raise NotImplementedError def parse_error(self, external_error) - GatewayError: raise NotImplementedError适配器只做三件事把标准请求翻译成外部请求、把外部响应解析成标准响应、把外部错误映射成统一的错误类型。拿某家以 messages 格式著称的模型供应商来说它的 system 指令和普通消息字段是分开的那我就在 build_request 里把 StandardRequest 里的 system 字段单独提出来另一家模型要求所有历史对话拼成一个长字符串那 build_request 就要做拼接和截断。这里有个非常容易忽略的点上下文窗口的换算。不同模型对 token 的计算方式不一样有的按字符数估算有的按精确的 tokenizer 计算。适配器必须知道每个模型的上下文上限在 build_request 阶段就把超长部分截掉或者报错否则请求打到上游会被拒绝浪费一次昂贵的调用。3.2 统一请求与响应结构的设计取舍我们的 StandardRequest 大致长这样{ model: ai-assistant-chat-pro, request_id: req_8f2a1c..., tenant: core-app, messages: [ {role: system, content: 你是一个客服助手}, {role: user, content: 我的订单什么时候发货} ], params: { temperature: 0.7, max_tokens: 1024, stream: true } }StandardResponse 长这样{ request_id: req_8f2a1c..., model: ai-assistant-chat-pro, content: ..., usage: { prompt_tokens: 128, completion_tokens: 56, total_tokens: 184 }, latency_ms: 2340, provider: supplier-a }这套结构在设计时有两个原则一是保留足够的元信息request_id、tenant、model 这几个字段是每个模块都需要访问的必须顶层平铺不能嵌套在某个子对象里二是面向业务场景而不是面向供应商接口。我没有把某个供应商的专有字段放到标准结构里而是允许适配器把供应商的原始响应挂到 raw_response 字段上这样既不影响通用处理又保留了调试和排查的入口。提示凡是涉及标准数据结构的改动都要走内部评审。这个结构是所有模块的协议边界一旦定下来改动的成本极高。第三个容易忽视的原则是版本兼容。供应商接口经常升级但不可能所有适配器同步改完。所以标准结构里要预留扩展位新字段必须带默认值绝不允许老适配器因为解析不了新字段而报错。3.3 流式输出的特殊处理同步调用相对简单但流式输出是个大坑。大模型基本都是边生成边吐 token走的是 SSE 技术。如果网关直接逐 token 透传业务方确实能收到流但网关就只剩一个转发的作用后面做 token 统计、审计、用量计算时根本无从下手。我在网关里用了一个异步流式处理器适配器从供应商那里拿到一个流网关逐块累积每得到一小块就下发给调用方同时把这块的 token 用量增量记录到统计里。等流结束的时候再把最终的标准响应补发到事件总线里用于审计和成本核算。这个方案有两个技术细节要注意。一是超时的判定长回复的完整流可能持续几十秒甚至几分钟不能简单用固定读超时要用空闲超时——即流多久没出新 token 就算超时。二是背压控制如果调用方消费流的速度慢于上游生产的速度网关要做缓冲或者断开连接否则内存会撑不住。流式处理的第三个隐患是事件边界。上游把一条回复拆成多个 SSE 事件事件之间有空行分隔。网关在转发时必须针对一个完整事件做操作不能按固定缓冲大小直接切否则会把一个事件切碎业务方解析时会觉得回复莫名其妙地断裂。后面我会在问题排查部分再讲一个真实案例。4. 路由、限流与容灾的工程化实现4.1 模型路由策略不是简单映射业务方在调用时传的模型名比如 ai-assistant-chat-pro并不是某个供应商的真实模型名而是一个逻辑模型名。路由层要做的事就是把这个逻辑模型名映射到一个具体的供应商模型上。最简单的实现是查表但真正跑起来就会发现需求没这么简单。我们有三类路由策略成本优先优先选择单价最低的可用供应商。适合对成本敏感、对延迟不敏感的批处理场景。可用性优先优先选择健康分最高的供应商。适合实时交互场景绝不希望因为省几毛钱把用户请求打到快挂掉的模型上。权重轮询按配置的权重比例分发流量用于灰度切换和 A/B 对比。比如新模型先放 5% 流量观察一段时间无异常再逐步提升到 50%。路由策略是通过配置中心实时下发的不需要重启网关。每次调用都在路由决策前做一次健康检查查询把不可用的供应商剔除出候选池。模型路由配置大致是这样的{ logical_model: ai-assistant-chat-pro, candidates: [ {provider: supplier-a, model: pro-1.5, weight: 80, cost_per_1k_tokens: 0.002}, {provider: supplier-b, model: pro-v2, weight: 20, cost_per_1k_tokens: 0.0018} ], strategy: weighted, fallback: {provider: supplier-c, model: stable-3, enabled: true} }这里必须提醒一个细节路由结果必须能被审计。每次调用完成后路由引擎要记录“逻辑模型名、实际供应商、实际模型名、选中策略、候选池当时的状态”这一整套信息。否则将来出了问题你连当时为什么选了这个供应商都查不出来。4.2 三层限流从租户到供应商都要管限流在 AI 网关里比普通 API 网关更难做因为既要限 QPS又要考虑 token 配额不是简单的次/秒计数就能完事。我做了三层限流。第一层是按租户限流控制每个业务方调用网关的总频率防止某个业务方把整个网关打满。第二层是按模型限流控制单个逻辑模型的总并发防止一个热门模型占了所有供应商的配额。第三层是按供应商配额限流每家供应商都有自己的并发上限网关必须把已经分配出去的并发算清楚避免被上游先限流。算法我选的是滑动窗口而不是令牌桶。因为令牌桶在突发流量下的体验过于平滑但这里还有一个更麻烦的维度一个请求可能消耗几百到几万 token。单纯按 QPS 限流挡不住大面积的高 token 请求。所以每层的限流条件都是多维的每分钟最多 N 次调用同时每分钟最多消耗 M 个 token。我把限流信息统一放在内存加后端存储里网关节点本地做高频判断决策结果异步同步到全局统计。因为限流判断如果每次都查远程存储在高峰期会拖垮响应但如果完全不做全局同步节点扩容后限流就失效了。初期可以不追求完美一致性限流本来就是概率控制能在 2~3 秒内收敛就够用了。4.3 超时、重试与熔断的正确姿势这一节是我觉得最有实战价值的部分因为网上讲网关的文章大多在讲协议和路由很少回答一个问题上游真的挂了你怎么办超时设计上我分了三档连接超时、首包超时、空闲超时。连接超时是连接建立阶段的外呼时间一般设 2~3 秒首包超时是发出请求后等到第一个 token 的时间模型排队严重时可能很久我设的是 30~60 秒空闲超时是流式响应中两个 token 之间的最长间隔刚才说过了。这三档分开设才能在模型卡住和正常慢响应之间做一个合理的判断。重试是最容易出事的地方。我见过不少平台把重试写得很粗暴失败就重来一次。但大模型的调用是有真实成本的一次失败也许已经消费了几千 token。有些错误不能重试比如 400 参数错误、401 鉴权失败重试一万次也是白搭有些错误可以重试比如 429 限流、5xx 服务端错误、网络超时。而且 429 的重试要带退避不能用固定间隔狂打否则会加重供应商的故障。熔断我是用半开状态实现的。一个供应商的连续失败率达到阈值熔断器就把它从候选池摘除掉过一段时间之后放一个探针请求进去试试成功就恢复正常失败就继续断开。这个东西不要设得太敏感模型偶尔超时一两次是正常的阈值设得太低会导致供应商被频繁摘除业务方反而会投诉你不稳定。4.4 成本治理配额与自动降级200 个模型每个模型的单价差异极大成本治理在网关架构里必须是一等公民。我给每个租户设置了模型维度的月度预算和日度预算。预算检查发生在路由之前如果某租户今天的预算已经烧完了网关不是直接拒绝请求而是做自动降级——把请求路由到更便宜的模型上或者把超长输入截断以节省 token。降级动作要明明白白告诉调用方响应头的备用字段里带上“当前已降级到备用模型”的标记。账单统计我依赖事件总线。每次调用的用量、成本、租户、模型信息都作为一个事件发出去统一落库。这不是为了做多复杂的报表而是因为成本问题是业务方最关心的问题没有准确的账单业务方根本不敢大胆用模型。成本事件和请求事件必须用同一个 request_id 关联对账时才不会出现缺口。5. 落地过程中的常见问题与排查实录5.1 流式转发把响应切伤的问题上线初期我们遇到过一个诡异的现象业务方收到的流式回复经常在中间断掉但网关日志里没有任何异常。排查了很久发现是我们的网关层在读取上游 SSE 流时把数据和事件边界混在一起读了。供应商发来的流里事件之间有一条空行分隔我们的转发逻辑按照固定字节数缓存一次 flush 就可能把一个事件切成了两半业务方解析时就断了。解决办法是让转发逻辑严格按事件边界缓存读到一条完整的事件再 flush不要提前输出。这个坑在自测时没有暴露是因为我们自己测试时用的回复太短事件切分概率低。现实中模型一长事件一多问题立刻就显现了。建议所有做流式网关的同学第一次联调就把 2000 token 以上的长回复作为必测用例。5.2 重试导致重复扣费这大概是所有 AI 网关系里最容易踩的坑。某供应商偶尔在 30 秒后才返回一个超时错误但实际上请求在供应商那边已经成功生成了内容只是回包慢。网关按照规则自动重试了一次结果是同一笔请求被扣了两次费用业务方的账单里出现了双倍费用。这个问题的本质是超时语义的不确定性没法彻底解决只能尽量避免。我把重试策略改成了只有连接阶段失败和明确的 5xx 错误才自动重试超时类型的失败默认不自动重试而是交给路由层做供应商切换。同时每次重试都会生成一个 retry_of 字段指向原始请求方便对账时识别重复扣费的情况。5.3 连接池被打满网关对内调用供应商的 HTTP 连接池早期用的是默认连接池配置高峰时连接池很快就满了大量请求阻塞在获取连接的位置。排查后发现问题出在模型供应商的上游响应普遍偏慢单个连接被占用的时间很长默认的最小连接数根本不够用。解决办法是把连接池参数调大并对同一个供应商按模型维度做连接池隔离。不然一个热门模型的调用可能把所有连接都占完冷门模型反而拿不到资源。这个隔离逻辑听起来简单但要把连接池维度和路由维度对齐否则连接池的指标根本没法排障。5.4 观测体系怎么搭才算够用网关层必须能回答三个问题当前这个请求卡在哪了这一分钟整体成功率和延迟是多少哪个供应商最近不稳定我的观测体系分成三块结构化日志、指标、事件审计。结构化日志必须有 request_id 贯穿全链路从接入到路由到适配都透传同一个 ID指标主要关注成功率、错误分布、P50/P95/P99 延迟、限流触发次数、熔断摘除时长事件审计则是前面提到的每一次调用完整生命周期事件。这里给一个实用的建议不要把供应商返回的原始错误直接抛给调用方。网关要把上游错误映射成统一错误码同时把原始错误塞进日志和审计事件。既保证调用方能稳定处理错误又保证排查时有原始信息可用。否则调用方接的是标准错误你需要的信息却丢在供应商那边定位一个问题要两个系统来回翻效率极低。6. 沉淀下来的一些体会整个网关从立项到稳定运行给我最大的一个感受是它的价值不在于代码写得多漂亮而在于用架构把一个混乱的外部世界封装成了一个稳定的内部边界。业务方从此只面对一套 API供应商的变动被隔离在一个个适配器里。如果要我给正要开始做这件事的团队几个建议第一优先级是先把标准化数据模型定下来这是整个架构的地基地基歪了后面全歪第二优先级是观测体系一开始就建不要等出问题再补没有全链路 request_id 的网关排查故障时会让人崩溃第三优先级是成本治理别放到最后模型账单混乱带来的业务投诉很快就会找上门。最后再分享一个小技巧每次新增供应商时先把它的 API 文档完整读一遍再写适配器然后拿真实模型跑至少 20 条覆盖各类错误码的用例。不要只看正常路径错误路径的兼容性才是适配器质量的关键。把错误映射做扎实了网关的稳定性就成功了一大半。