☰
基于C#和YARP构建多模型AI网关的实践指南
2026/10/2 3:23:45 网站建设 项目流程

AI Gateway 这个词这两年热得发烫,但很多团队的理解还停留在“搭个反向代理,把请求转发到 OpenAI”这个层面。真正在多模型场景里跑过生产环境的人都知道,事情远没那么简单:模型厂商的 API 风格千奇百怪,鉴权方式各不相同,限流策略差异巨大,某个模型动不动就 5xx 超时,业务方隔三差五来加新模型,如果每次都在业务代码里硬编码适配,那维护成本会把你拖垮。

我最近用 C# 和 YARP 完整落地了一个多模型统一接入与路由网关,这里把设计思路、核心代码、踩坑过程一次讲清楚。这套方案解决的核心问题有三个:一是让上游业务方只用一套 API 协议就能访问所有模型;二是通过路由规则把请求分发到正确的模型供应商;三是在网关层统一处理鉴权、限流、缓存、可观测性等横切关注点。适合正在做 AI 平台基建、或者想给团队搭一个统一模型入口的开发者参考。

1. 为什么需要一个 AI Gateway:从“能用”到“好用”的必经之路

先聊聊我对 AI Gateway 这件事的理解,只有把需求想透了,选型才不会跑偏。

1.1 多模型接入场景下,业务方到底在痛什么

很多团队接入大模型是从“单项直连”开始的。业务方直接拿 API Key 去调 OpenAI、调通义、调文心,代码里散落着各家 SDK,每个 SDK 有自己的超时设置、错误码体系、重试策略。一开始只有一两个模型还能忍,一旦模型数量到了五个以上,问题就集中爆发了。

我见过一个真实的例子:同一套业务逻辑,因为模型厂商的接口返回结构不同,业务代码里要写三个 if-else 分支来解析不同格式的响应。加一个新模型,就要动业务代码,就要回归测试,就要重新发布。这还只是协议层面的问题,更麻烦的是统一治理完全无从下手——谁在调用哪个模型?调用量多大?失败率多高?成本花了多少?没有一个集中的观察点。

还有一层痛点是模型供应商自身的稳定性差异很大。有的模型高峰期响应要十几秒,有的偶尔返回 500,有的限流阈值很低。如果业务方直连,这些抖动全部要业务方自己消化,重试逻辑要自己写,熔断逻辑要自己写,缓存逻辑也要自己写,每个业务方写出来的质量还参差不齐。

1.2 AI Gateway 的四个核心职责:接入、路由、治理、观测

把痛点梳理完,AI Gateway 的职责就非常清晰了。我把它拆成四个层面:接入层解决协议统一,路由层解决请求分发,治理层解决限流鉴权配额,观测层解决可观测性和成本核算。

接入层的核心是设计一套中立的 API 规范,让业务方像调用一个模型一样调用所有模型。路由层的核心是把请求映射到具体模型供应商,既要支持静态配置,也要支持按策略动态调整。治理层要统一处理鉴权、限流、计量、配额等横切逻辑,把业务方从这些脏活里解放出来。观测层则负责输出调用日志、耗时分布、失败率、Token 消耗指标,让平台团队能看清楚全局。

这四个层面不是孤立的,它们共用同一个请求上下文,在一条处理链路里依次生效。这也是为什么我坚持用网关而不是写一堆工具类来解决问题的原因——只有把横切逻辑集中到一个入口,才谈得上统一治理。

1.3 什么时候你真正需要它

这里我给一个判断标准:如果团队只有一两个模型,业务代码里直连完全没问题,别为了架构而架构。但如果你面临以下情况中的任意两条,就应该考虑上网关:一是模型供应商数量超过三个;二是多个业务方都在调用模型,需要统一管控;三是模型调用成本已经到了需要精细核算的程度;四是需要灰度切换模型,比如从 A 模型切到 B 模型,希望优雅平滑。

我见过不少团队在模型只有两个的时候就急吼吼搞网关,结果抽象过度,反而拖慢了业务迭代。反向的例子也有——等到十几个模型全部接入后才想治理,为了迁移改接口花了两周。合理的时机从来不是“模型多”,而是“模型多了之后开始痛”。

2. 技术选型:为什么是 C# 和 YARP

选型这件事我前后对比过几轮方案,包括自研网关、用云厂商的网关产品,以及基于开源网关二次开发。最后落定 C# + YARP,有一定的偶然性也有必然性,这里把关键考量讲清楚。

2.1 YARP 是什么,它的核心能力边界在哪

YARP 全称是 Yet Another Reverse Proxy,是微软官方维护的一个高性能反向代理组件,直接运行在 ASP.NET Core 管道里。它不是独立的服务进程,而是一个类库,你可以把它嵌进自己的 Web 应用中,这意味着你能用最小的侵入获得完整的反向代理能力。

YARP 的核心能力集中在四块:HTTP/1.1 和 HTTP/2 的代理转发、基于配置的路由匹配、基于集群的负载均衡、可扩展的中间件管道。与 Nginx 这类独立网关相比,YARP 最不一样的地方在于它本身就是 .NET 生态的一部分,你能在请求管道里随意插入自己的中间件,用 C# 编写任何定制逻辑,然后和代理转发无缝衔接。

它的性能在纯反向代理场景下相当能打,微软内部跑过压测,与 Nginx 的差距并不大,对于 AI Gateway 这种 IO 密集、下游延迟高的场景,网关本身的性能开销几乎可以忽略不计。真正拖慢整条链路的是模型端的响应时间,网关只要控制好连接管理、避免不必要的缓冲和序列化,就不会成为瓶颈。

2.2 对比:自研转发器、Nginx、还是 YARP

我实际评估过三条路线。

自研转发器看起来极简——拿到请求,改成目标格式,HttpClient 转发,拿响应回来。问题是,一旦要做多模型适配、限流、缓存、熔断,每项能力都得自己从头造轮子。路由匹配要考虑通配符和优先级,负载均衡要考虑健康检查和权重,这些细节看着简单写起来全是坑。自研的隐性成本是维护一个不断膨胀的代理内核,这不是普通业务团队该干的事。

用 Nginx 或 OpenResty 是另一种思路,优势是成熟可靠性能强。但对 C# 技术栈的团队来说,lua 脚本的扩展方式门槛不低,而且 Nginx 的配置模型和 AI 网关常见的按请求头路由、动态刷新模型列表这类需求贴合得不算好。在 Nginx 里做 JSON Body 级别的解析和动态路由,写起来相当别扭。

YARP 恰好卡在中间:它把代理内核做完了,路由匹配、负载均衡、健康检查、请求改造都是开箱即用的;同时它是 .NET 原生的,我能用最舒服的方式写定制中间件。你用中间件管道改请求、注入鉴权逻辑、采集指标,所有代码都是强类型的,写起来清清楚楚。对我这个长期写 C# 的人来说,这套组合拳没有理由不选。

2.3 从架构角度看:中间件管道与代理转发如何协同

YARP 最打动我的一点是它把“代理转发”和“中间件处理”做成了一条流水线。请求到达网关之后,不是先走完业务逻辑再转发,而是中间件和反向代理各自只处理自己关心的事情,整个链路在 ASP.NET Core 的管道里统一执行。

这个模型下我能做很优雅的事情。比如在代理转发之前放一个身份认证中间件,读到未授权请求直接短路返回 401,根本不会产生上游请求,资源消耗极低。在转发之后放一个响应缓存中间件,对匹配缓存规则的请求直接返回缓存副本,连上游都不用碰。中间件之间的协作关系是顺序执行的,我可以把鉴权、限流、路由改写、转发、响应缓存、日志记录依次串联,每一层只做一件事。

这也意味着一件事:你不需要在业务代码里通过引用来控制网关逻辑,所有的横切行为都发生在管道里。业务方看到的只是“一个地址、一套规范”,网关内部怎么折腾,对上游完全透明。

3. 核心设计:统一接入协议与模型 Provider 抽象层

架构骨架定下来之后,最难的其实是这个部分——协议设计。所有模型供应商的 API 都不一样,你必须在网关层做一个抽象,让业务方只认识一种接口。这一步做得好不好,直接决定网关的可用性上限。

3.1 统一请求 / 响应规范:把千奇百怪的模型 API 收敛成一套

我先定义了一套“中立”的模型调用协议,它不偏向任何一家厂商。核心思想是:业务方只向网关提交模型 ID 和调用参数,网关负责把请求翻译成目标厂商的方言。

统一请求体我设计成这个样子:

{ "model": "gpt-4o", "messages": [ { "role": "user", "content": "你好,介绍一下你自己" } ], "temperature": 0.7, "max_tokens": 2048, "stream": false }

统一响应体长这样:

{ "id": "req_abc123", "model": "gpt-4o", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "你好,我是..." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 32, "completion_tokens": 128, "total_tokens": 160 } }

设计这套协议时有几个取舍。模型 ID 由网关侧统一维护,业务方填的是网关定义的模型逻辑名,而不是厂商的原始模型名,这样后续切换供应商时业务方不用改代码。流式响应和非流式响应共用同一套消息结构,保证 SSE 场景下 parsing 逻辑一致。usage 字段做归一化,让业务方不用去读每家厂商的 token 统计格式。

3.2 Provider 抽象:用抽象类屏蔽厂商差异

有了统一协议之后,我需要一个 Provider 抽象层,让每一种模型供应商都有对应的实现。抽象类的核心方法只有两个:BuildRequestContext 负责把统一请求转换为厂商请求格式,HandleResponseAsync 负责把厂商响应转换回统一响应格式。

public abstract class ModelProvider { public abstract string ProviderName { get; } public abstract HttpRequestMessage BuildRequest( UnifiedChatRequest request, ProviderEndpoint endpoint); public abstract ValueTask<UnifiedChatResponse> ParseResponseAsync( HttpContent content, CancellationToken ct); public abstract IAsyncEnumerable<UnifiedStreamChunk> ParseStreamAsync( HttpResponseMessage response, CancellationToken ct); }

这里的关键设计是每个 Provider 只和“转换格式”和“发送请求”打交道,不负责重试、不负责缓存、不负责鉴权。所有横切逻辑都收敛到中间件层。为什么这么分?因为横切逻辑对每个 Provider 是通用的,一旦放到 Provider 内部就会出现大量重复代码,而且行为会不一致。

我实现了几个常见 Provider:OpenAI 兼容系列、Azure OpenAI、以及几个国内主流模型。得益于大多数国产模型都做 OpenAI 兼容协议,抽象层的实现代价比想象中小得多。真正要单独处理的是鉴权头拼装方式和响应格式里微妙的差异。

3.3 端点的数据结构与健康管理

仅仅有 Provider 还不够,你还需要管理“模型 + 供应商 + 地址 + 密钥”的组合关系。我设计了一个 ProviderEndpoint 对象,字段包括模型逻辑名、供应商类型、BaseUrl、ApiKey、权重、健康状态。模型调用不是一个 Provider 对应一个地址,而是逻辑名对应一批地址,这批地址按权重调度,任何一个挂了都会被快速摘除。

健康检查这里有个容易忽略的细节:AI 模型接口通常没有专门的 health check 端点,你不能像探测普通服务一样靠 /healthz 去判断。我实际采用的是“被动健康检查”策略——每次上游调用结束后,根据响应状态码和耗时更新该端点的健康得分,连续失败超过阈值就标记为不健康,请求就不会再打过去。这种方式对模型供应商完全透明,不需要额外开发任何接口。

4. YARP 路由配置与多模型转发落地

抽象层设计完毕,接下来就是把模型调用接入 YARP 的转发链路。这一节是全项目的核心工程点,直接决定网关能否稳定跑在生产环境。

4.1 路由与集群:两种粒度怎么配合

YARP 用两个核心概念描述转发关系:Route 负责定义“什么请求匹配到这里”,Cluster 负责定义“转发到哪些上游地址”。Route 匹配成功之后,请求会被送到它指向的 Cluster,由 Cluster 做负载均衡选出一个目的地。

刚开始尝试的时候我把一个模型配成了一个 Route 加一个 Cluster,比如/v1/gpt4o/chat/completions路由到 gpt4o 集群。这种配置能工作,但问题很快暴露:业务方每接入一个新模型,就要在 YARP 配置里新增几个 Route,一堆路由看着眼花缭乱,而且和业务代码里的模型逻辑名对不齐。

后来我换成了更合理的组织方式:一个统一的入口 Route 匹配所有模型请求,然后通过自定义中间件解析模型逻辑名,动态决定转发到哪个 Cluster。Cluster 依然按模型供应商和端点组织,但 Route 的数量大幅减少。这样做的好处是入口规范非常干净:业务方永远只调用同一个 URL 前缀,模型差异全部由内部路由消化掉。

4.2 动态路由中间件:解析模型名并改写目标地址

动态路由实现的关键是改写好配置里的 Cluster ID。业务方请求进来后,我写了一个 RouteByModelMiddleware,从请求体里提取 model 字段,查配置中心拿到对应的 Cluster ID,然后赋给当前请求的集群标识。

伪代码大致是这样:

public async Task InvokeAsync(HttpContext context) { var requestBody = await ReadBodyAsync(context.Request); var modelId = ExtractModelId(requestBody); var targetCluster = _modelRegistry.GetClusterId(modelId); if (targetCluster is null) { context.Response.StatusCode = 404; await context.Response.WriteAsJsonAsync(new { error = "model not found" }); return; } context.Request.Headers[YarpHeaders.ClusterId] = targetCluster; await _next(context); }

这里要注意 Body 的读取方式。因为代理转发本身也需要读取 Body,如果你在中间件里提前读取了它,就必须重新放到请求流里,否则下游什么都收不到。我踩过这个坑:直接在中间件里 ReadBodyAsync 之后没重置 Position,YARP 转发时拿到一个空 Body,上游模型直接报 400。解决办法是用EnableBuffering()开启请求缓冲,读取完把 Position 重置为 0,让后面的转发器重新读到完整内容。

4.3 基于请求头的灰度分流与 A/B 切换

动态路由带来的一个很大的福利是你能实现灰度分流。比如新接入一个模型想先放 10% 流量测试稳定性,这时候你可以在路由中间件里加一个可选规则:请求头里带上X-Model-Version: preview,或者根据用户维度做哈希取模,把流量按比例分配到新旧两个集群。

我给灰度逻辑预留了一个策略接口,支持按请求头、按用户 ID 哈希、按百分比三种模式。灰度规则维护在配置中心里,改动规则不需要重启网关,热点加载配置后下一批请求就生效。这个能力在生产模型切换时真的太重要了,我从 A 模型切到 B 模型,就是靠它在半小时内完成了 10% 到 100% 的逐步放量,全程零故障。

4.4 Cluster 负载均衡与主动重试

在 Cluster 配置里我重点调了两个参数:负载均衡策略和重试策略。

负载均衡策略我选了 PowerOfTwoChoices,这是 YARP 默认策略的增强版,随机取两个可用目的地,再选其中负载更低的那个,在大多数场景下比纯随机更均匀,代码零成本。如果某个模型有多个端点,例如 OpenAI 兼容代理挂了多个地区地址,这个策略能把分布做得相对平滑。

重试策略这一块要多说两句。模型接口不能随便重试,因为大模型生成是不幂等的,同一段对话你重试两次,花了双倍 Token 钱还得到不同的结果。我设定的原则:只有连接层错误和 429/5xx 才触发重试,4xx 一律直接透传。且重试只限定在请求尚未开始流式输出之前,一旦响应已经开始流了,哪怕中途断了也绝不重试,否则业务方会收到一个被截断的响应再加上一个新的完整响应,拼都拼不回去。

YARP 的配置里要这样开启重试:

{ "RetryPolicy": { "MaxRetries": 2, "RetryableStatusCodes": [ 429, 500, 502, 503, 504 ] } }

这里我再补充一个容易踩的点:重试时默认会复用同一请求对象,而请求里的 HttpContent 只能被消费一次。如果你在重试前已经把 Body 读完了但不重置,第二次发送时会直接拿到一个空流。建议在配置重试之前,确保你的请求转换逻辑里每次都重新创建 HttpRequestMessage,而不是复用同一个实例。这个坑我花了一个晚上才定位到,现象是“第一次调用成功、失败重试时模型报空请求”,极其隐蔽。

4.5 完整的 YARP 配置清单参考

给一份我实际在跑的基础配置示例,字段都做了精简,核心配置模型就是看四个关键部分:路由匹配规则、集群地址列表、重试策略、HttpClient 超时控制。

{ "ReverseProxy": { "Routes": { "ai-gateway-entry": { "ClusterId": "dynamic", "Match": { "Path": "/v1/ai/{**catch-all}" } } }, "Clusters": { "gpt4o-primary": { "Destinations": { "gpt4o-d1": { "Address": "https://api.example.com/v1/chat/completions" }, "gpt4o-d2": { "Address": "https://api.example.com/v1/chat/completions" } }, "LoadBalancingPolicy": "PowerOfTwoChoices" }, "qwen-primary": { "Destinations": { "qwen-d1": { "Address": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation" } }, "LoadBalancingPolicy": "PowerOfTwoChoices" } }, "HttpClient": { "Timeout": "00:05:00", "MaxConnectionsPerServer": 512, "EnableMultipleHttp2Connections": true } } }

注意 HttpRequestMessage 的 Timeout 和 HttpClient 的 Timeout 是两个东西。YARP 的集群级 HttpClient 配置管的是连接池和整体超时,而单次请求的超时要单独设。大模型接口动辄几十秒,我建议把网关到业务方的响应超时设置为 5 分钟以上,网关到上游的 HttpClient Timeout 再放宽一个量级,尽量避免因为超时设置导致长请求被无辜掐断。

5. 横切能力:鉴权、限流、缓存、可观测性

网关的价值一半体现在转发,另一半体现在治理。YARP 只负责把请求转发到正确的地方,但让它成为一个“网关”的,是围绕它长出来的一圈横切能力。这一节我把每个能力的实现思路和关键取舍都过一遍。

5.1 API Key 鉴权:两个层级都要做

鉴权分两层。第一层是业务方调用网关的鉴权,第二层是网关调用模型供应商的鉴权。这两层不能混为一谈。

业务方鉴权我用的是最简单的 API Key 方案。网关启动时从配置中心加载密钥列表,请求头里必须带X-Api-Key,对应一个业务方标识。这个标识后面会贯穿整个请求链路,限流和成本核算都要用它来区分维度。实现方式就是注册一个中间件,放在 YARP 转发逻辑之前,校验失败直接返回 401,不会产生任何上游调用。

模型供应商的鉴权因为每家协议不同,统一在 Provider 里处理。比如 OpenAI 兼容协议是Authorization: Bearer sk-xxx,Azure OpenAI 则是api-key: xxx头加上 query 参数。每个 Provider 在 BuildRequest 时就组装好对应鉴权头,从密钥管理系统读取真实 Key,避免把 Key 以明文形式放在配置文件里提交到仓库。

这里有一个实践细节:业务方的 Key 尽量做成“一业务一 Key”,并且可以随时吊销、随时加备注。这样当某条 Key 对应的调用量异常上涨,你可以立刻识别是谁在超量调用,而不需要去翻日志猜。

5.2 基于令牌桶的限流:业务方级别和模型级别双维度

限流一定要分维度。业务方维度防止某个业务把网关流量全部吃掉,模型维度防止某个模型供应商的配额被打爆。我分别维护了针对业务方和针对模型 ID 的限流策略。

限流算法没搞花活儿,直接用的是令牌桶。令牌桶的好处是允许一定程度的突发流量,同时保证了长期平均速率。参数上我主要控制两个数:桶容量决定瞬时能扛的峰值,补充速率决定持续吞吐上限。比如某个业务方配额是每分钟 3000 次,我设置桶容量 500,补充速率 50 次每秒,这样业务方突发调用 500 次内都合法,超过则被限流。

限流这里犯过一个经典错误:只在网关单实例内做内存限流,结果部署了两个副本后,实际放行量翻了一倍。解决方向是引入分布式限流,需要一个共享存储做令牌计数。在 Redis 上我用 Lua 脚本实现令牌桶逻辑,保证原子性。如果团队暂时没有 Redis,你也可以先接受单机限流的误差,但心里要有数,容量规划时按实例数打折计算。

429 响应体我要特别提醒一下:不要只返回一个裸 429 状态码,应当带一个 JSON 错误体,向业务方说明被哪个维度限流了、大概多久能恢复。业务方接入体验会好很多,他们拿到错误信息就知道是调整自己调用节奏,还是找平台提额。

5.3 响应缓存:不要把省钱变成降智

模型接口能不能缓存?能,但只适合那些对一致性要求不高的场景。我把缓存范围严格限定在“相同请求参数 + 相同模型 + 相同温度 + 不流式”的唯一子集上,也就是说只有完全一模一样的请求才可能命中缓存。不能像普通 HTTP 缓存那样拿 URL 做 key,因为 URL 一样、POST Body 不一样,结果也完全不同。

缓存 key 我用的是模型名 + 请求 Body 的 SHA256 哈希。命中缓存直接返回统一响应格式,完全不产生上游调用。缓存介质我选了 Redis,设置了 TTL,一般几十分钟到几小时不等,根据业务对时效的要求配置。

但这块我一定提醒一件事:千万别对“和用户个性化相关的”对话类结果做缓存,比如“根据我的历史记录推荐一部电影”。一旦缓存了,用户第二次来请求就会拿到自己旧的结果,体验完全不能接受。缓存只适合放纯通用知识问答、系统提示词完全相同的场景。省钱可以,不能通过降智来省。

5.4 结构化日志、链路追踪与成本核算指标

可观测性是网关上线之后最容易被低估的部分。没有它,出了问题你两眼一抹黑,根本不知道是哪个环节挂了。

我统一使用了 ASP.NET Core 内置的 ILogger,结构化日志把模型名、业务方、状态码、耗时、Token 消耗全部打成结构化字段。同时接入了 OpenTelemetry 的 trace 能力,把网关内部“中间件处理+代理转发”和上游模型调用的耗时链路串起来。定位问题的时候,一眼就能看到一个请求从进入网关到上游返回到底哪一段花了大头时间。

成本核算这里要专门提一下。我在网关统计了每个模型每次调用的 prompt_tokens 和 completion_tokens,定时汇总到监控系统,再和模型单价做乘法就能算出成本。这样一套下来,每个月每个业务方在哪个模型上花了多少钱,一查便知。这对控制 AI 项目成本至关重要,很多团队年底被账单吓一跳,根子就在从头到尾没做过 token 维度的计量。

6. 实战踩坑记录:症状、定位、根治全过程

最后这一部分我重点讲坑。这部分内容不是查文档能查得到的,全部来自真实上线过程中踩出来的血泪。整理成一个个独立的问题快照,方便你遇到类似情况时快速对症下药。

6.1 请求 Body 被提前消费,上游收到空请求

症状:网关日志显示转发成功,但上游模型返回 400 bad request,报“empty request body”。偶尔成功偶尔失败,让人完全摸不着头脑。

定位过程:对比失败和成功的请求,发现失败全部集中在经过自定义路由中间件的路径上,直连后端的路径不受影响。最终定位到问题出在中间件读取了请求 Body 用于解析模型名,但读取完成后没有把 Body 的 Position 重置回 0。

根治方式:读取前调用HttpContext.Request.EnableBuffering(),读取完把 Position 设置为 0。这样 YARP 在后续转发时能重新完整读取 Body。这个处理是纯中间件开发的标准动作,但偏偏很容易被漏掉。

6.2 响应重试导致模型输出与业务方拿到的不一致

症状:上游第一次请求返回 500,自动重试后返回 200,但两次生成的文本内容不同。业务方本来收到的是第一次生成的部分内容,看到重试后出现一个莫名其妙的响应。

定位过程:这是生成式 AI 接口重试的经典副作用——模型结果不确定,重试永远不是在“重复同一件事”。我的第一次实现里重试策略覆盖了所有 5xx 状态码,且没有判断响应是否已经开始流式返回。

根治方式:重试策略只保留连接层错误和明确的重试安全码,并且一旦进入流式响应阶段就禁止重试。同时重试之前要兜底检查响应头是否包含 content-type text/event-stream,如果已经进入流,当前请求直接结束,绝不发起第二次请求。

6.3 两个网关实例导致限流放行量翻倍

症状:网关限流配置明明设置了每分钟 1000 次,压测时发现实际放行超过 1900 次。

定位过程:检查代码逻辑没问题,后来发现网关部署了两个副本,内存限流是每实例独立的,两个实例各自放行 1000 次,总量自然翻倍。当时如果你只观察单个实例的指标,会误以为限流完全正常。

根治方式:引入 Redis 做分布式限流,用 Lua 脚本保证令牌操作的原子性。上线后实测同一个压测场景,两个副本的总放行量严格被压在 1000 次附近。

6.4 SSE 流式响应被网关缓冲,用户迟迟等不到第一个 token

症状:流式模式下用户感觉响应“卡住”,等了很久才开始显示内容。排查发现第一字节到达网关的时间很快,但网关转发给浏览器的时机非常迟。

定位过程:问题出在 ASP.NET Core 的默认响应缓冲行为。YARP 代理转发时,如果没有显式关闭响应缓冲,HTTP 响应体在部分场景下会被聚积到一定大小才会冲刷给客户端。

根治方式:在转发中间件对流式请求显式设置context.Response.Headers.ContentType、调用context.Features.Get<IHttpResponseBodyFeature>()?.DisableBuffering(),确保代理转发不做缓存直接流出。加上这个后,SSE 模式基本像直连一样顺畅,首 token 时间显著缩短。

6.5 模型 ID 不存在时把错误吞掉,导致 5xx 满天飞

症状:业务方传了不存在的模型名,网关内部尝试路由失败后抛异常,最终返回 500,监控面板上 5xx 数量暴涨。

定位过程:原因是动态路由中间件里没有对未知模型 ID 做短路处理,而是让异常一路向上抛,YARP 把未处理异常统一转成了 500。

根治方式:在路由解析阶段就显式判断模型 ID 是否在注册表里存在,不存在直接返回 404,并附带明确错误信息:“model not found: xxx”。这类校验看起来只是几行代码,但对业务方的排障效率提升很有帮助,他们能立刻知道是自己填错了模型名,而不是网关出了问题。

6.6 连接复用参数调优:高并发下的性能杀手

症状:压测到 200 并发时网关 CPU 不高但吞吐上不去,大量请求卡在建立 TCP 连接阶段。

定位过程:抓取网关日志发现每个请求都在走完整的 TLS 握手流程,连接完全没有复用,连接建立的开销成了瓶颈。

根治方式:在 YARP HttpClient 配置里调大了 MaxConnectionsPerServer 并开启 HTTP/2 多连接复用。调整后相同压测条件下吞吐提升非常明显。这也说明网关层对连接池的管理直接决定性能上限,重在把复用策略调优,而不是盯着单个请求的 CPU 占用。

7. 方案如何继续演进

这套网关跑起来之后,模型接入的时间成本压缩到了以小时计。再往后如果你有更高的需求,可以在这些方向继续演进:引入多级缓存架构成熟的热点管理能力;把路由策略上推到配置中心,让模型切换完全不依赖发布;在网关层增加对 RAG 检索的代理能力;甚至接入模型评测模块,对每个模型的输出质量做实时监控。

我个人实际使用下来的体会是:AI Gateway 的价值定位不是“转发工具”,而是“模型治理平台”。它把供应商差异挡在门外,把横切关注点收敛到一处,让业务方的接入成本降到最低。无论你最终技术栈是不是 C# + YARP,只要想清楚接入、路由、治理、观测这四个维度,这套设计思路都能直接迁移。

最后分享一个我始终记着的实践原则:网关层绝不做业务语义决策,只做转发、治理与观测。一旦网关开始插手“什么场景该用哪个模型”这类业务判断,它就变成了业务代码,维护成本和变更风险都会成倍上升。守好网关的边界,模型接入这件事就会一直很清爽。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询