1. 多模型时代,应用架构里为什么突然多了一层
过去两年我做后端架构评审,被问得最多的问题从“你们用哪个大模型”变成了“你们怎么管这么多大模型”。这个变化本身就说明了一件事:单一模型打天下的阶段已经过去了。现在一个稍微像样的AI应用,背后往往挂着三到五个模型——便宜的用来做意图识别和分类,贵的用来做复杂推理,还有一个专门处理长文本或者多模态输入。问题随之而来:每个模型的接口协议不一样,计费方式不一样,Token计量口径不一样,连返回格式都各有各的脾气。
你当然可以让业务代码直接去调各家SDK,但那样做的代价我踩过。去年一个项目里,前端要加一个“切换模型”的开关,结果后端改了六个文件,测试回归了两天,上线后还因为某个模型的超时策略没对齐导致雪崩。那次之后我就认定,应用和模型之间必须有一个中间层。这个中间层,行业里现在普遍叫它AI网关,英文常写作LLM Gateway。
AI网关到底是什么?用一句话说,它是所有大模型调用的统一入口。你的应用只跟它对话,它负责把请求翻译成各家模型能听懂的格式,再把结果统一翻译回来。它管的不只是转发,还包括Token计量、密钥管理、限流熔断、缓存、日志审计、成本核算,以及最近越来越热的MCP协议对接。你可以把它理解成公司前台:所有访客都找前台,前台知道该把谁领到哪个部门,也顺手记了一笔访客台账。
这篇文章适合谁看?如果你正在做AI应用的后端架构,或者你是一个独立开发者,手里已经接了不止一个模型,被密钥管理和账单搞得头大,那这篇就是写给你的。我会从设计思路讲到实操落地,把选型逻辑、核心参数、踩坑经验都摊开说。哪怕你现在只用一个模型,看完也会明白为什么早点加这层中间层,后面会省很多事。
2. 拆解AI网关的核心设计思路
2.1 为什么不是“加个代理”那么简单
很多人第一反应是:不就是个反向代理吗,Nginx配一下不就行了。我一开始也这么想,直到发现事情没那么简单。普通反向代理处理的是无状态、同构的HTTP请求,而大模型调用是有状态、异构、且带成本的。举几个具体的差异点你就明白了。
第一,请求体结构差异巨大。OpenAI风格的接口用messages数组,某些模型用prompt字符串,多模态模型还要塞图片的base64或者URL。代理层如果只是透传,业务代码就得自己拼装每种格式,那中间层就白加了。第二,响应是流式的。大模型普遍用SSE(Server-Sent Events)流式返回,代理层要能正确处理分块传输,还要在流式过程中做Token计数,这比普通代理复杂得多。第三,成本是实时产生的。每一次调用都在烧钱,网关必须能精确计量每个请求消耗了多少Token,并且能按应用、按用户、按模型维度归集,否则月底对账就是一笔糊涂账。
所以AI网关的设计目标不是“转发”,而是“抽象”和“治理”。抽象是指把多模型的差异屏蔽掉,对上提供统一接口;治理是指在这个统一入口上做限流、鉴权、缓存、监控、计费。这两件事决定了它必须是一个有业务逻辑的应用层组件,而不是一个纯网络层的代理。
2.2 统一接口的抽象层次怎么定
设计网关时第一个要拍板的问题是:抽象到什么程度。我见过三种做法,各有取舍。
最浅的一种是协议转换层,只做格式映射。你传OpenAI格式进来,它转成目标模型的格式发出去,回来再转回OpenAI格式。好处是实现简单,业务代码改动小;坏处是它假设所有模型能力对等,但实际上不同模型的参数支持度差别很大,比如有的支持temperature,有的支持top_p,有的两个都支持但默认值不同。
中间一种是能力抽象层,把模型能力抽象成“对话”“补全”“嵌入”“多模态理解”几类,每类定义一套标准参数,网关负责把标准参数映射到具体模型。这种做法的好处是业务代码面向能力编程,换模型时不用改代码;代价是网关要维护一张能力映射表,新模型接入时需要配置。
最深的一种是路由决策层,网关不仅做转换,还根据请求内容自动选择模型。比如简单问题走便宜模型,复杂问题走贵模型,或者根据用户等级走不同模型。这种做法最智能,但也最复杂,需要一套可靠的分类或评分机制,否则容易把重要请求路由到能力不足的模型上。
我的建议是:从协议转换层起步,逐步演进到能力抽象层,路由决策层作为可选增强。一上来就做智能路由,大概率会因为分类不准而翻车。先把统一接口跑通,把计量和限流做扎实,等业务稳定了再考虑路由优化。
2.3 密钥与凭证的管理策略
这是最容易被低估、但出事最要命的部分。我见过把API Key硬编码在前端的,也见过所有应用共用一个Key的。前者等于把钱包扔在大街上,后者一旦某个应用被刷爆,全部业务一起挂。
网关在这件事上的价值是凭证收敛。所有模型的Key只存在网关里,应用侧只持有网关自己签发的凭证。这样带来三个好处:一是Key不落地到业务代码,泄露面大幅缩小;二是可以按应用分配不同的配额和权限,A应用只能用便宜模型,B应用才能调贵模型;三是Key轮换时只改网关一处,业务无感。
具体实现上,网关内部要维护一张凭证表,记录每个模型供应商的Key、Base URL、可用模型列表、配额上限。对外签发凭证时,可以用JWT,把应用ID、允许的模型范围、过期时间编进去。这里有个细节要注意:JWT的过期时间不要太长,我一般设2小时,配合刷新机制。太长了泄露后风险窗口大,太短了刷新频繁增加开销。另外,网关到模型供应商的那一跳,Key要用环境变量或密钥管理服务注入,绝对不能写在配置文件里提交到代码仓库。
2.4 Token计量为什么是网关的必修课
Token是大模型世界的计价单位,也是网关最核心的计量对象。但Token计量有个坑:不同模型的分词器不一样,同一段文字在不同模型下Token数可能差20%以上。如果网关只是简单转发,就没法给出准确的成本预估。
我的做法是在网关里内置各模型的分词器,或者至少内置一个估算规则。对于精确计量,可以在请求发出前用对应模型的分词器算一遍输入Token,响应回来后算输出Token。对于流式响应,要在每个chunk到达时累加。这里有个实操技巧:流式响应的Token计数不要等全部结束再算,而是边收边算,这样即使连接中断也能记录已消耗的部分。
计量的粒度也很重要。我一般按“应用+模型+用户”三个维度记录,每次调用写一条日志,包含输入Token、输出Token、耗时、是否命中缓存、是否失败。这些日志汇总起来,既能做成本分摊,也能做容量规划。比如你发现某个应用的输入Token特别大,可能是提示词写得太啰嗦,优化一下就能省钱。
3. 核心功能模块与实操要点
3.1 请求路由与模型选择
网关收到请求后,第一件事是决定发给哪个模型。最简单的策略是显式指定,请求里带model字段,网关按字段路由。这种适合业务明确知道要用哪个模型的场景。稍微复杂一点的是别名映射,业务传model: "fast",网关根据配置把fast映射到当前性价比最高的模型。这样换模型时只改网关配置,业务无感。
再往上就是条件路由。我做过一个场景:用户提问长度小于200 Token走小模型,大于200走大模型。实现方式是在网关里加一个前置判断,根据输入长度选择目标模型。这个逻辑要小心,因为长度不等于难度,短问题也可能很复杂。更稳妥的做法是结合意图分类,但这需要额外的小模型调用,会增加延迟和成本。
路由模块的实操要点有几个。第一,要有降级链。主模型超时或报错时,自动切到备用模型,而不是直接给用户报错。降级链的配置要支持优先级和超时阈值。第二,要记录路由决策。每次请求走了哪个模型、为什么走这个模型,都要记下来,方便排查“为什么这次回答质量差”这类问题。第三,路由规则要可热更新。模型价格和可用性变化很快,规则写死在代码里就得频繁发版,最好做成配置中心下发。
3.2 限流、熔断与重试的配合
限流和熔断是保证网关自身不被拖垮的关键。限流我一般做两层:全局层按模型供应商的配额限,防止超额被供应商封禁;应用层按应用限,防止某个应用占用过多资源。限流算法用令牌桶比较合适,因为大模型调用本身耗时较长,固定窗口容易在窗口边界产生突刺。
熔断的触发条件要仔细设计。我见过只按错误率熔断的,结果供应商返回大量429(限流)时没触发,因为429不算“错误”。实际上429、超时、5xx都应该计入熔断统计。熔断后的处理也有讲究:可以返回缓存结果,可以降级到备用模型,也可以直接返回友好提示。我倾向于降级到备用模型,用户体验最好,但前提是备用模型确实可用。
重试要特别小心。大模型调用不是幂等的,重试可能导致重复计费。我的原则是:只对明确的网络错误和5xx重试,且最多重试一次。429不要立即重试,要等退避时间。流式请求一旦开始返回内容就不要重试,否则用户会看到重复内容。这些规则都要在网关里写清楚,不能交给业务代码各自处理。
3.3 缓存策略:省钱的隐形冠军
缓存是网关里投入产出比最高的功能之一。大模型调用又贵又慢,如果相同或相似的请求能命中缓存,直接省下真金白银。但缓存大模型响应有几个特殊之处。
第一,缓存键怎么定。不能只用请求体的哈希,因为同样的提示词在不同模型、不同参数下结果不同。我一般用“模型+参数+提示词”的组合做键,参数里temperature为0时才缓存,大于0的结果有随机性,缓存意义不大。第二,语义缓存。有些请求字面不同但意思一样,比如“今天天气怎么样”和“今天天气如何”。这种可以用嵌入向量做相似度匹配,但阈值要调好,太松会返回不相关结果,太紧命中率低。第三,缓存过期时间。事实类问题可以缓存久一点,时效性问题要短。我一般默认1小时,可配置。
这里有个坑:流式响应缓存。流式响应是一段段吐出来的,缓存时要完整收集后再存,命中时再模拟流式吐出。实现上要注意chunk的边界,不能把两个chunk的内容粘在一起导致格式错乱。
3.4 MCP协议对接的现状与实操
MCP(Model Context Protocol)是最近热度很高的一个方向,它试图标准化模型与外部工具、数据源的交互方式。简单说,MCP让模型能“调用工具”,比如查数据库、读文件、调API。网关在这个环节的角色是工具调用的统一代理。
为什么网关要管MCP?因为工具调用同样涉及鉴权、限流、审计。如果每个应用自己实现工具调用,安全边界就散了。网关可以统一管理工具注册、权限校验、调用日志。实操上,网关需要维护一个工具注册表,记录每个工具的MCP服务地址、所需权限、超时设置。模型返回工具调用请求时,网关先校验该应用是否有权限调这个工具,再转发到对应的MCP服务,拿到结果后回传给模型。
目前MCP生态还在早期,各家实现有差异。我的建议是:先支持最基础的stdio和HTTP两种传输方式,把工具注册和权限做扎实,等协议稳定了再扩展。不要一上来就追求全功能,容易陷入兼容性泥潭。
4. 从零搭建一个最小可用网关
4.1 技术选型与目录结构
假设你现在要动手搭一个,我推荐的技术栈是:Python + FastAPI,因为异步支持好,生态里大模型相关的库也全。如果你团队是Java背景,Spring Boot WebFlux也可以,但异步流式处理写起来会啰嗦一些。数据库用PostgreSQL存配置和日志,Redis做缓存和限流计数。
目录结构我一般这样组织:
gateway/ app/ main.py # 入口,注册路由 config.py # 配置加载 models/ # 各模型适配器 base.py # 抽象基类 openai_adapter.py claude_adapter.py core/ router.py # 路由决策 limiter.py # 限流 cache.py # 缓存 tokenizer.py # Token计量 api/ chat.py # 对话接口 admin.py # 管理接口 db/ models.py # ORM模型 session.py tests/ requirements.txt这个结构的关键是适配器模式。每个模型一个适配器,继承同一个基类,实现chat和stream_chat两个方法。新增模型时只加一个文件,不改核心逻辑。
4.2 统一请求与响应格式定义
统一格式我建议直接对齐OpenAI的Chat Completions格式,因为它是事实标准,大部分客户端库都支持。请求体核心字段:
{ "model": "fast", "messages": [ {"role": "system", "content": "你是一个助手"}, {"role": "user", "content": "你好"} ], "temperature": 0.7, "stream": false, "max_tokens": 1024 }响应体也对齐OpenAI格式,包含choices、usage等字段。usage里要有prompt_tokens、completion_tokens、total_tokens,这是计费依据。对于流式响应,每个chunk的格式也要对齐,最后一个chunk带上usage。
这里有个细节:不同模型的停止原因(finish_reason)要统一。OpenAI用stop、length、tool_calls,有的模型用别的词。网关要做映射,否则业务代码要处理多种枚举值。
4.3 适配器实现的关键代码
以OpenAI适配器为例,核心是处理流式和非流式两种情况。非流式比较简单,发请求、拿响应、转换格式、返回。流式要复杂一些,我用伪代码说明关键逻辑:
async def stream_chat(self, request): async with httpx.AsyncClient() as client: async with client.stream("POST", url, json=payload, headers=headers) as resp: async for line in resp.aiter_lines(): if line.startswith("data: "): data = line[6:] if data == "[DONE]": break chunk = json.loads(data) # 累加Token计数 self.token_counter.add(chunk) # 转换格式后yield yield self.convert_chunk(chunk)关键点有三个:一是用aiter_lines逐行读,不要一次性读全部;二是遇到[DONE]要正确终止;三是每个chunk都要过一遍Token计数。Token计数在流式场景下不能依赖响应里的usage字段,因为很多模型流式返回不带usage,得自己用分词器算。
4.4 限流与计量的落地配置
限流用Redis的令牌桶实现,核心是INCR加过期时间。伪代码:
def check_rate_limit(app_id, limit, window): key = f"rate:{app_id}:{int(time.time() // window)}" current = redis.incr(key) if current == 1: redis.expire(key, window) return current <= limit这个简单实现有个问题:窗口边界突刺。比如限制每分钟100次,第59秒来了100次,第61秒又来100次,实际两秒内200次。要精确控制得用滑动窗口或者漏桶,但实现复杂。我的经验是:对于大模型调用,突刺影响不大,因为单次调用耗时长,天然平滑。所以简单实现够用,别过度设计。
计量落库我建议异步写,不要阻塞请求。用消息队列或者后台任务把计量日志批量写入,避免每次调用都同步写数据库。日志表按天分区,方便清理和归档。
5. 常见问题与排查技巧实录
5.1 流式响应中断与Token计数不准
这是最高频的问题。表现是用户看到一半内容停了,或者账单上的Token数对不上。原因通常有三个:一是网关到模型的连接超时设置太短,模型还在生成就被掐断了;二是网关到客户端的连接被中间层(比如Nginx)缓冲了,导致流式变成一次性返回;三是Token计数在异常路径上没走到。
排查思路:先看网关日志里这次请求的耗时和状态码,如果网关侧正常但客户端断了,多半是中间层缓冲问题。Nginx要配proxy_buffering off和proxy_cache off。如果是Token计数问题,检查异常处理分支里有没有调用计数逻辑,我一般把计数放在finally块里,确保无论成功失败都记录。
5.2 模型返回格式不一致导致的解析失败
不同模型对同一语义的返回格式可能不同。比如工具调用,OpenAI放在tool_calls字段,有的模型放在function_call,还有的放在content里用特定标记。网关如果只按一种格式解析,遇到别的就崩。
解决办法是在适配器里做格式归一化,把所有变体都转成统一格式。同时加一层容错解析,遇到不认识的字段不要直接抛异常,而是记录原始响应并返回一个降级结果。我踩过的坑是:某模型升级后改了返回结构,网关没做容错,导致整个应用不可用。后来加了原始响应日志,排查起来快很多。
5.3 密钥泄露与权限越界
密钥泄露的常见路径:日志里打印了完整请求头、错误信息里带了Key、配置文件提交到了仓库。防范措施:日志脱敏,Key只显示前4位和后4位;错误信息统一处理,不要把底层异常直接透出;配置文件用.gitignore排除,用环境变量注入。
权限越界是指A应用调用了它不该调的模型。防范靠网关的凭证校验:JWT里带上允许的模型列表,路由前先校验。这里有个细节:模型别名也要校验,不能只校验实际模型名,否则业务传个别名绕过检查。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决措施 |
|---|---|---|---|
| 流式响应一次性返回 | 中间层缓冲 | 检查Nginx配置 | 关闭proxy_buffering |
| Token数对不上 | 异常路径未计数 | 检查finally块 | 计数逻辑放finally |
| 模型切换后报错 | 格式不兼容 | 对比原始响应 | 适配器做归一化 |
| 429频繁触发 | 限流阈值过低 | 看配额配置 | 调整令牌桶参数 |
| 缓存命中率低 | 缓存键太严格 | 看键的构成 | 引入语义缓存 |
| 密钥疑似泄露 | 日志或仓库 | 搜代码和日志 | 脱敏+环境变量 |
5.5 几个我踩过的坑
第一个坑:超时设置一刀切。不同模型响应速度差别很大,小模型可能1秒返回,大模型要30秒。如果网关统一设10秒超时,大模型全挂。正确做法是按模型配置超时,或者用动态超时。
第二个坑:重试导致重复计费。有次网络抖动,网关重试了一次,结果供应商那边两次都计费了。后来改成只对连接建立失败重试,一旦请求发出去了就不重试。
第三个坑:缓存了带随机性的响应。temperature大于0时结果有随机性,缓存后用户每次拿到一样的答案,体验很怪。后来严格限制只有temperature=0才缓存。
第四个坑:MCP工具调用没做超时。某个工具服务挂了,网关一直等,把线程池占满。后来给每个工具调用加了独立超时,超时后返回错误让模型自己处理。
6. 网关的扩展方向与个人体会
网关搭起来之后,能扩展的方向不少。往深了做,可以加成本预算控制,给每个应用设月度预算,超了自动降级到便宜模型。往宽了做,可以加多租户支持,让不同团队各自管理自己的模型和配额。往智能了做,可以加A/B测试,同一请求按比例分流到不同模型,对比效果和成本。
我个人在实际操作中的体会是:网关的价值不在于技术多复杂,而在于把散落各处的治理逻辑收拢到一处。没网关的时候,限流在业务代码里,密钥在配置文件里,计量在日志里,出了问题到处找。有了网关,这些都在一个地方,改一处全生效。这个收敛带来的维护效率提升,比任何单个功能都值钱。
最后分享一个小技巧:网关上线初期,先跑影子模式。所有请求照常走原路径,同时复制一份到网关,对比两边结果和耗时。跑一周没问题再切流量。这样能把风险降到最低,也能发现适配器里的隐藏bug。这个做法我在三个项目里用过,每次都提前发现了问题,值得一试。