ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

多模型接入网关架构设计与工程实践:从协议适配到成本控制

多模型接入网关架构设计与工程实践:从协议适配到成本控制 1. 项目整体定位与网关架构的选型思路1.1 为什么需要做多模型接入网关做技术的人基本都遇到过这种场景项目里最开始只接了某家大模型厂商的接口代码里直接写死一套 OpenAI 兼容协议。跑了两周老板提了新需求说要把市面上几个主流模型都接进来做智能路由让用户按需选择。这个时候再回看原来的代码问题就摆在面前了——每家厂商的鉴权方式不一样、请求格式有差异、流式返回的格式也有区别单测还好说一上生产就是一堆兼容分支。我当时给这套系统定的内部代号就叫 4stoken.cn主域名后来也沿用了这个。名字里带 token 不是偶然因为这类网关最关键的一件事就是把“token 的消耗”管明白。光是把多模型接进来并不难难的是接入之后怎么统一调度、怎么计量、怎么在故障的时候优雅切换以及出了问题时怎么快速定位到具体是哪一家厂商的哪一次请求出了问题。这些就是“工程观测”要解决的事。多模型接入网关不是一个新概念但真正把它落地到生产环境并且能扛住真实流量需要想清楚的细节远比想象中多。它解决的问题其实有三层第一层是协议适配让上层业务用一个统一的接口去对接不同厂商第二层是策略执行包括路由、限流、重试和熔断第三层是数据闭环也就是把每一次请求的关键信息记录下来形成成本、延迟、成功率这些可度量的指标。1.2 技术选型自研还是基于开源改在正式动手之前我们花了不少时间在“自研”和“开源网关二次开发”之间做权衡。团队里有人倾向直接用现成的 API 网关比如 Apache APISIX、Kong 这些它们确实功能丰富插件生态也完整。但实际一评估就发现通用网关更多面向的是 REST API 的治理场景对 LLM 多厂商接入的特殊需求支持并不算好比如不同厂商的 Tokenizer 对上下文的计算方式不一样需要按厂商分别计算 token 用量流式输出场景下网关需要做流式转发而不是简单的 HTTP 代理多模型路由经常需要读取请求体里的目标模型字段再动态决定转发给谁这不是所有网关都支持得很顺手按用户或按 API Key 做精细化的成本分摊通用网关往往需要自己写一堆插件。如果基于开源网关二次开发等于要在别人画好的框架里塞一堆私有逻辑很多底层行为不可控。尤其像流式转发这种高频场景一旦在代理层出问题排查成本反而更高。所以最终我们选择自研一个轻量网关只聚焦多模型接入这一件事。技术上没有追求大而全反而是刻意做“窄”牺牲一部分通用性换来了对业务场景的深度适配。当然这个决策有条件团队当时有几位对网络编程和异步模型比较熟的人也有足够的测试环境做压测。如果团队人数少、场景又比较标准直接用开源网关加插件可能是更稳的选择。自研不是为了炫技是为了后面做计量和观测时不至于被框架卡住手脚。1.3 4stoken.cn 的整体架构网关的整体结构并不复杂核心是一条请求链业务侧统一请求到网关网关经过鉴权、参数校验、路由选择、限流判断再把请求转换成目标厂商的格式发送到上游拿到响应后统一回传给业务侧。在这个主链路之外还有三个旁路系统配置中心、观测数据收集、成本计量。链路最前端是接入层只负责接收请求和返回响应不参与任何业务逻辑。接入层后面是逻辑层包含鉴权模块、模型路由模块、限流模块、上下文字段处理模块和流式转发模块。逻辑层之上有一个元数据配置模块保存了厂商信息、模型列表、计费单价、API Key 池、路由策略等。这个配置模块是所有动态行为的基础。旁路的观测系统里我们定义了统一的日志结构每条请求无论是成功还是失败都会记录下完整的时间线网关收到请求的时间、完成鉴权的时间、路由决策耗时、上行请求发出时间、首包返回时间、流式传输结束时间。这些字段组合起来就能还原一次请求从进入到返回的全过程。架构上最需要注意的一点是网关必须是无状态的。所有动态配置都从配置中心读取所有限流计数都放在 Redis 里做节点本身只保存不可变的静态配置。这样才能在流量突增时随意扩缩容不至于半夜加机器还要先改一轮配置文件。2. 多模型接入的核心链路拆解2.1 协议层的统一与转换主流的模型厂商大多兼容 OpenAI 协议但实际去看细节差异还是不少。有些厂商支持的功能参数在另一个厂商那里直接报错同样是 SSE 流式输出有的在事件字段里带 usage有的不带鉴权方式有的是 Bearer Token有的是自定义 header。网关里做了一个轻量的转换层思路参考了适配器模式。每种厂商一个适配器对外暴露统一接口。上层业务不用关心你在背后接的是哪家它只需要发起一个标准请求比如指定模型名网关会自动把这个模型名映射到对应的厂商和具体模型。这里举一个实际例子。同样的一个对话补全请求厂商 A 接受参数max_tokens厂商 B 要求用max_new_tokens厂商 C 可能两个都不认只认max_output_tokens。如果让业务层自己兼容每次新增厂商都要改业务代码。网关的做法是统一对外使用max_tokens作为标准参数在内部转换时再根据目标厂商映射成对应字段。业务层只认一套协议新增厂商时只需要在适配器里多写一个转换函数。协议转换层最容易踩的坑是流式格式。OpenAI 风格的 SSE 数据每一行是data: {...}最后是data: [DONE]。但某些厂商的流式返回里夹杂了自定义的 event 字段或者最后没有[DONE]标记而是一个空白行。网关如果不对这些差异做归一化业务侧拿到流会非常痛苦。我们当时在转换层做了一层 SSE 格式校正统一以 OpenAI 格式流出但保留完整的原始数据在日志里方便排查。2.2 路由决策与模型调度路由是网关里面最有业务价值的部分。它解决的不只是“这个请求该发给谁”还牵扯到成本、延迟、可用性三个维度的平衡。最初实现的路由规则很简单配置表里写死“模型A对应厂商X”请求来了按表查。但用了不到两个星期就发现不够用。真实场景里同一个模型可能有多家厂商提供价格不同、延迟不同、稳定性也不同。这时候就需要一个打分机制。我们的打分策略综合了三类信息静态权重、动态健康状态、实时延迟。静态权重是配置中心里人工设定的比如优先使用某厂商的官方渠道动态健康状态来自流量统计如果某厂商在最近五分钟内的错误率超过阈值自动给它扣分实时延迟来自滑动窗口内的平均首包时间。每次路由决策时把可用的厂商候选按综合分排序取最高分且有可用配额的那个作为目标。这种动态路由的设计核心出发点不是追求“智能”而是追求“可控”。模型能力再强如果没有退出机制出了问题你是没法向老板交代的。所以网关里每个路由策略都强制要求配置降级目标。主厂商超时了自动发给备选厂商备选厂商也失败了再返回明确的错误码而不是让业务侧无限等待。这里还要强调一点路由不能只看模型名还必须看用户身份。不同客户可能绑定了不同的厂商白名单。比如某个客户的合规要求比较严格数据只能走特定厂商的国内节点那路由模块就必须支持按用户维度过滤目标厂商。我们的实现是三层筛选用户白名单过滤模型映射确定候选集打分排序选出最终目标。三层各自独立逻辑清晰后面调问题也好定位。2.3 限流、配额与成本控制接入多个模型之后成本失控是最大的潜在风险。一个大模型请求如果写错了循环几秒钟可能就烧掉几百块。我们不能等账单出来了再去反查必须在网关层面做实时控制。网关里的限流分成两层接入层限流和成本配额控制。接入层限流用的是经典的令牌桶算法按用户和按模型两个维度分别计数。用户维度是为了防止某个业务方异常调用拖垮整体资源模型维度是为了保护上游厂商的并发配额避免厂商那边被限流反噬。成本配额控制是另一套机制。我们在配置中心里维护了一张计费表记录每个模型每千 token 的单价。每次请求结束网关做了 token 统计之后实时计算本次消耗金额写入 Redis 中该用户的当日累计消耗。如果累计消耗超过了预设的日限额后续请求直接拒绝。这套机制上线之后最明显的效果就是再也没有出现过半夜被厂商短信提醒“余额不足”的情况。成本控制的下面还有一层是 token 估算的问题。请求进来时还不确定会消耗多少 token但我们可以按 prompt 长度估算一个上限在请求发送给上游之前再判断一次“如果这个请求成功预计消耗是否超过剩余额度”。这里用到的估算算法各家有差异严格的方式是本地加载对应模型的 tokenizer 做离线计算但这样内存占用很大实际工程上更常用的是字符数粗略估算误差控制在 15% 以内对成本拦截已经够用了。3. 核心模块的工程实现记录3.1 动态上游管理与 API Key 池多模型接入之后API Key 的管理立刻成了一个头疼的问题。每个厂商可能有多个 Key分别属于不同的账号每个账号又有独立的余额和限流。如果 Key 是写死在配置文件里的每次换 Key 都要重启网关这在生产环境里是不可接受的。我们的方案是做一个独立的 API Key 池模块把 Key 当成资源来调度。每个 Key 池对应某个厂商的某个账号池子里保存多个 Key网关发请求时从池里取一个可用 Key。Key 的使用状态由令牌桶和错误标记共同维护如果某个 Key 频繁返回 401 或 429它的权重会自动降低甚至暂时摘除等冷却时间后再放回来。这个设计还有一层好处可以按 Key 分别统计消耗和余额做到成本分摊。客户 A 用了哪个 Key月底账单就能精确到具体某一天某一个小时的消耗。初期没有这个能力时财务那边拿着总账单根本没法分解到项目每次对账都得很痛苦。动态上游管理的另一边是 upstream 配置的实时生效。网关运行时不直接读数据库而是从配置中心订阅变更事件。新增一个厂商或者修改某个上游地址只需要在控制台操作网关这边秒级生效不用发版本。我们用的配置中心是 etcd主要看重它的 watch 机制变更推送延迟低。网关启动时全量拉取、运行时增量监听这样既保证了交付速度也保证了配置最终一致。3.2 流式转发的实现与超时控制如果只是做普通的 HTTP 代理那网关写起来三天就能上线。真正拉开差距的地方在流式转发也就是 SSE 场景下的处理。第一次做流式转发时我们天真地把上游的响应流直接 pipe 给下游代码量很少看起来也没问题。但一个周末过去线上反馈说某些请求的响应“卡住不动”日志里也没有报错。后来查下来是上游连接挂了之后网关这侧没有正确感知连接关闭下游一直等着后续数据。正确的流式转发必须自己做缓冲和检测。我们改造之后的实现流程是收到上游响应后先检查响应头确认是 SSE 格式然后启动一个协程不断读取上游数据每读到一个完整事件先经过转换层做格式标准化再写入下游响应同时记录最后一次读到数据的时间如果超过空闲超时阈值一般是 30 秒强制断开连接并记录监控。这里有一个容易忽略的细节流式转发的“超时”和普通请求的超时不是一个概念。普通请求关心的是整包时间流式请求关心的是“首包时间”和“包间间隔”。上游模型在思考过程中可能长时间没有输出 token这不是故障但如果超过一定时间没有任何数据大概率连接已经异常了。我们最终把这两个超时分开了首包超时 60 秒包间空闲超时 90 秒同时可以通过请求参数覆盖全局默认值。3.3 重试与容错策略多厂商接入之后单个厂商的抖动被放大了因为你不再依赖单一链路。这时重试策略必须设计得非常谨慎否则一个小抖动就能引发雪崩。我们的重试策略分三层。第一层是连接级别重试针对“连接失败”这种瞬时错误DNS 解析失败、TCP 握手失败等场景可以立刻换一个上游重试最多重试 1 次。第二层是请求级别重试针对 429 限流和 5xx 服务不可用错误但如果请求已经开始返回流式数据绝不重试因为下游可能已经收到了部分数据重试会破坏消息顺序。第三层是厂商降级也就是前文提到的路由打分机制当主厂商连续失败达到阈值后续请求自动切换备选厂商。重试策略里最重要的是一个取舍重试能提升可用性但也会放大流量。如果一个厂商本身就处于过载状态你的重试只会加重对方的压力。所以重试必须带退避策略。我们使用 300ms 的指数退避同时限制总重试次数不超过 2 次这样一次失败请求最多给上游增加两次额外请求不会形成重试风暴。4. 工程观测体系的设计4.1 全链路日志的结构化标准工程观测听起来是个很大的词落到地上其实就是三件事日志、指标、追踪。这三件事在 4stoken.cn 里都有对应的实现而实现的核心原则是“一次请求一条主日志”。最初大家写日志的习惯不一致有人记录在业务层有人记录在调用厂商的 SDK 里出了问题很难把散落的日志拼起来。后来我们统一规定网关在处理每个请求的开始阶段生成一个 request_id并把它注入到日志上下文里。整条请求链路的所有旁路信息都带上这个 request_id包括厂商返回的 trace_id 也会被关联存储。主日志的结构是标准化的 JSON 格式包含以下核心字段request_id全局唯一请求编号user_key调用方身份标识model统一模型名provider实际厂商标识upstream_latency上游响应耗时毫秒first_byte_latency首包耗时毫秒total_latency网关总耗时毫秒prompt_tokens / completion_tokenstoken 明细estimated_cost估算成本美元status最终结果状态码error_class错误分类如 upstream_timeout、rate_limited有了这个规范之后排查问题的方式从“猜”变成了“查”。线上反馈哪个模型反应慢直接按时间范围和模型名检索主日志一眼就能看到是哪个环节慢了。后来我们又加了一层日志采样策略成功请求按 10% 比例采样错误和慢请求 100% 保留在控制存储成本的同时保证关键信息不丢。4.2 核心指标与监控告警日志解决的是“单个请求出了什么问题”指标解决的是“系统整体正在经历什么”。我们监控的核心指标不多但每一个都能对应到具体的动作。第一个是请求量按用户、模型、厂商三个维度统计。请求量的异常涨跌往往意味着业务方在发布新功能或者上游在调整策略。第二个是成功率细分为网关自身错误和上游错误告警规则也分两层单厂商五分钟内错误率超过 20% 触发提醒超过 50% 触发紧急 oncall。第三个是延迟分布重点关注 p50、p95、p99 三档流式场景下额外关注首包延迟。这些指标全部通过 Prometheus 抓取Grafana 做可视化。告警规则里我比较看重一个原则先看错误率再看延迟最后看流量。因为延迟升高有时只是流量正常的波动但如果错误率在升那延迟大概率也会跟着升先处理错误率才能治本。有一个印象很深的例子某个模型厂商在当地时间下午经常出现 p99 延迟飙升但是错误率一直正常。如果不看指标很难发现这是厂商侧的资源争抢导致。后来我们针对该厂商增加了一条调节规则下午高峰期自动把部分流量调度到备选厂商p99 延迟就回落了。没有观测数据这种调度决策是完全无从下手的。4.3 成本计量的可视化与账单体系成本计量这块单独拿出来说是因为它在多模型接入里的重要性被严重低估了。网关接入了多家模型之后老板见你的第一句话大概率不是问延迟而是问“这两个模型哪个更划算”。我们在观测系统里增加了一个成本看板按天展示每个模型的总消耗、请求次数、平均单次成本、token 总量。数据来源是每条请求主日志里记录的 estimated_cost 字段通过汇总任务定时聚合到 ClickHouse再在前端展示。这里没有用很复杂的架构但解决了一个很实际的问题——业务方以前只能等月底账单现在可以实时看到调用模型花的每一分钱。成本计量的精度由 token 统计决定。网关在请求结束后会获取厂商返回的 usage 字段优先使用官方数值如果厂商没有返回这个字段我们再根据 prompts 和 completions 做本地估算。估算逻辑是字符数除以系数系数来自该模型 tokenizer 的经验值误差虽然比官方值大但不会让你被账单吓一跳。5. 常见问题与排查技巧实录5.1 流式响应“半路卡死”的定位方法这类问题是最难排查的一类。表面上看下游一直在等数据但网关日志里显示连接已经关闭下游却没有感知到。我们反复排查后确认核心原因是某些 SDK 对 SSE 流的处理逻辑是读取到空行或非 data 开头的行时会自动忽略但不会主动断开连接。而网关侧已经把数据发完了只是没有显式调用关闭方法。后续在流式转发模块里增加了一个收尾逻辑当上游流结束且已经把所有数据转发给下游后网关主动刷新缓冲区并关闭下游连接。这个操作听起来很简单但在异步框架里漏掉是很常见的。另一个排查经验是出现“半路卡死”时优先看日志里的 last_chunk_time 和 stream_sent_end 两个字段能快速判断到底是上游没发完还是网关没转发完。5.2 上游 429 限流的应对多模型接入后429 限流是最常见的问题。很多厂商的限流策略是按“每分钟请求数”和“每分钟 token 数”双重控制的用同一个 Key 并发多了很容易触发。我们的应对手段有两层。第一层是全局限流在网关里根据厂商返回的 429 响应头中的 Retry-After 字段自发调整当前 Key 池的并发令牌速度主动平滑请求。第二层是 Key 池拆分每个厂商按账号拆成多个 Key轮询使用。这个方案从结果上看很稳但要注意一个细节——同一个账号下多个 Key 的限流往往共享拆 Key 并不完全等同于拆额度需要提前在厂商文档里确认清楚。如果触发了 429 还被上游拒绝网关会返回一个标准化的 429 响应并在响应体里带上建议重试时间。业务方拿到这个错误时可以走自己的重试逻辑而不是无限重试。5.3 token 统计口径不一致的问题各家厂商对 token 统计的口径不太一样有时同一个 prompt 在不同厂商那里统计出的 token 数能差 20%。这个问题在成本对账时暴露得很明显。我们的处理方式是不以任何一家的统计为准而是建立一份“标准模型 token 单价表”在计费时使用估算值计算成本。真正的账单仍以厂商给出的 usage 为准但网关看板里的成本预估只采用本地估算模型保持一致的口径。这样出现了一个好处看板上的数据是自洽的不会有今天看这个数、明天看另一个数的混乱感。5.4 网关重启导致的连接池雪崩这是一个非常经典的生产事故。某次我们升级网关版本后业务方突然报大量请求超时。原因并不复杂网关重启后到上游的连接全部重建而业务方又在同一个瞬间涌入了大量请求所有连接同时在创建瞬间打爆了上游的连接数限制。解决这个问题的标准做法是连接预热。网关启动时主动向配置的所有上游发起一个健康的空请求把连接池填满再开始接收外部流量。另外在滚动发布时保留至少一个旧节点在流量中等新节点完成预热后再切流量能有效避免重建连接的代价被集中到一个时间点。连接池的另一个坑是长连接闲置过期。有些厂商的网关会在 60 秒后关闭空闲连接而你的连接池还认为连接可用发出去的请求直接失败。解决方式是定期对池里的空闲连接做探活并在拿到连接时检查最后一次使用时间超过阈值就新建连接。6. 几点体会与后续演进项目推进到后期我的体会是多模型接入网关真正难的不是转发请求而是建立一套围绕模型调用的数据闭环。接入渠道再多如果没有标准化日志、没有成本计量、没有健康状态监控网关就是一条昂贵的数据管道出了问题还是两眼一抹黑。4stoken.cn 这套架构目前还在持续演进。下一步计划是把路由打分策略接入厂商实时状态数据源以前只靠网关自身统计判断健康状态其实有滞后性如果厂商有自己的状态页和事故通知可以合并进路由决策让降级触发得更早。另外流式场景下的 token 使用量实时统计也还在优化目标是在流式传输过程中就动态更新成本估算而不是等请求结束后再计算。如果你也要做个类似的网关我的建议是别一开始就追求把所有功能做齐。先把接入层和日志做好足够支持业务方跑起来然后加成本控制避免预算失控最后再补动态路由和自动故障转移。每一步都有独立的收益不会出现“做到一半发现前面全白做”的情况。远程排查方面还有一个经验给网关留一个 debug 开关在请求参数里带上debugtrue时网关会在响应头里返回这次调用的完整路径包括命中的厂商、重试次数、耗时分解。这个功能看起来不起眼但实际沟通时省了太多事对方发一个请求回来自己就能定位问题在哪个环节。
返回列表