☰
SGLang Model Gateway 架构与缓存感知调度实践
2026/10/29 7:04:25 网站建设 项目流程

1. 网关层为什么成了推理服务的必需品

模型网关(Model Gateway)这个词,最早在传统微服务架构里是标配,搬到推理场景之后,玩法完全变了。传统网关主要处理 HTTP 路由、鉴权、熔断,而 SGLang Model Gateway 这类组件要额外扛住三件传统网关根本没见过的事:KV 缓存的位置是有状态的、一次请求可能跑几十秒还在吐字、同一个模型名背后可能是十几台机器的不同副本。我最初把它当成一个简单的反向代理来接,结果第一周就被线上投诉打脸——同一个用户的多轮对话被随机打到不同副本,前缀缓存全部失效,首字延迟从 300ms 飙到 2s 以上。

所以这篇内容想讲清楚的是:SGLang Model Gateway 这一层到底承担了什么职责,它内部的核心功能是怎么设计的,以及你在自己搭一套的时候,哪些参数必须算、哪些坑必须提前避开。适合已经跑通过单机推理、准备把服务推给多个业务方使用的同学;如果你还在本地跑 demo,前面两章可以快速扫过,第四章的实操和第五章的排查直接抄就行。

我个人的判断是,网关这一层的价值不在"转发",而在"调度决策"。转发是零成本的,调度决策才是决定你整机吞吐、缓存命中率、故障影响面的关键。下面我从架构开始,一层一层往下拆。

2. 整体架构拆解:一次请求的完整旅程

2.1 组件分层与职责边界

把 SGLang Model Gateway 拆开看,大致能分成四层,各层职责不能混,混了后面调优会非常痛苦。

第一层是协议适配层,负责把外部各种形态的请求(OpenAI 兼容格式、自定义 JSON、流式 SSE)统一成内部的规范请求对象。这一层做的是"翻译"工作,不做任何调度。第二层是路由决策层,它拿到请求之后,根据模型别名、请求的 prompt 前缀特征、各后端的实时负载和缓存热度,选出一个目标 worker。第三层是连接与会话层,负责维持到后端的连接池、处理流式响应的分块转发、处理超时与重试。第四层是策略层,包含限流、配额、鉴权、审计,它通常是旁路执行的,不阻塞主链路。

这四层分开的好处很直接:路由策略要改,只动第二层;限流规则要调,只动第四层。我见过有团队把限流逻辑写进路由函数里,结果压测时发现限流计数器成了全局锁,QPS 上不去,排查了两天才定位到。

注意:协议适配层千万不要做任何业务判断,比如"这个用户等级高所以给他更长的超时"。这类逻辑一旦写进适配层,后面做多租户隔离时会变成一团乱麻。

2.2 请求生命周期逐段拆解

一个典型的流式请求,从进门到结束大概经历这么几步。

接入与解析:网关收到 POST 请求,解析 body,提取model字段、messages、max_tokens、stream等。这一步的耗时通常在 1ms 以内,但如果 body 里有几万 token 的超长 prompt,JSON 解析本身就能吃掉 20-30ms,这个数字在压测时很容易被忽略。

模型解析与路由:用model字段去注册表里查真实的模型实例列表,再从中挑一个。挑的过程就是整个网关最核心的部分,后面 3.2 节单独讲。

准入控制:检查该租户的令牌桶余量、并发连接数、当日 token 配额。任何一项不通过就直接返回 429,不要让它进入排队,否则会把后端队列撑爆。

转发与流式回传:建立到后端的连接,把请求体发过去,然后一边收 SSE 分块一边往客户端写。这里有个细节:网关必须做背压处理,如果客户端读得慢,网关不能无限制地往内存里堆 chunk,否则一个大模型 + 慢客户端就能把网关内存打满。

收尾与计量:请求结束后,统计输入 token 数、输出 token 数、首字延迟(TTFT)、每 token 间隔(ITL)、总耗时,写入指标系统,同时扣减配额。

这条链路上,任何一个环节的阻塞都会放大成全链路问题。我在实际项目里给每一段都埋了独立的耗时打点,后来定位一个"偶发 5s 延迟"的问题,就是靠打点发现时间全花在准入控制的分布式计数器上——那个计数器用了同步调用,网络抖动一次就是几百毫秒。

2.3 几个关键设计取舍

有状态路由 vs 无状态路由。传统网关喜欢无状态,随便挑一台就行。但推理场景必须做**缓存感知(cache-aware)**路由,因为 SGLang 的 RadixAttention 把前缀 KV 缓存存在 worker 本地显存里,请求打到没缓存的机器上,等于前缀部分要重新 prefill 一遍。一个 4000 token 的系统提示词,重新 prefill 在 7B 模型上大概要多花 80-150ms。多轮对话场景下这个损失会被放大好几倍。

同步转发 vs 异步队列。同步转发实现简单,但后端一旦过载就会雪崩。异步队列能削峰,但会破坏流式体验,因为请求要排队。我的做法是混合:短请求同步直连,超过阈值的排队,并且队列长度严格受控,宁可早拒绝也不让队列无限增长。

网关内做 batch vs 交给后端。绝对不要在网关层做请求合并成 batch,这是后端调度器的活。网关只负责把请求送到正确的 worker,batch 怎么组、什么时候发,由后端根据当前显存和队列情况自己决定。网关插手 batch 会导致缓存失效和后端调度混乱。

3. 核心功能逐项剖析

3.1 模型注册表与别名路由

模型注册表看着简单,实际上是整个网关的配置中枢。它要维护三组映射关系:对外别名 → 内部模型名、内部模型名 → worker 列表、worker → 实时状态。

对外别名这层不能省。业务方调用时写chat-prod,你后台可能对应着qwen-7b-v3和qwen-7b-v2两个版本在做灰度。如果让业务方直接写真实模型名,每次换版本都要通知所有人改代码,这在多团队协作里是灾难。

worker 状态这层要包含至少这些字段:当前排队请求数、正在处理的请求数、已用显存比例、最近 60 秒的平均 TTFT、缓存树的大小。少了任何一个,路由决策都会拍脑袋。

一个简化的注册表结构长这样:

{ "aliases": { "chat-prod": { "targets": ["qwen-7b-v3", "qwen-7b-v2"], "weights": [90, 10], "fallback": "qwen-7b-v2" } }, "models": { "qwen-7b-v3": { "workers": [ {"id": "w-01", "addr": "10.0.1.11:30000", "state": "healthy"}, {"id": "w-02", "addr": "10.0.1.12:30000", "state": "healthy"} ], "max_context": 32768, "quant": "fp16" } } }

权重灰度这块我建议按请求数比例而不是按用户 ID 哈希。按用户哈希虽然能保证同一用户始终走同一版本,体验一致,但会破坏缓存复用——不同用户的前缀本来就不一样,按用户分反而没损失;问题在于灰度比例调整时,哈希环重分布会导致大量用户切换版本,缓存全冷。按请求比例随机分,配合 90/10 这种小流量比例,影响面反而更可控。

3.2 Cache-aware 调度:把请求送到缓存最热的那台机器

这是 SGLang Model Gateway 最值得讲的功能,也是最容易被做错的地方。

原理不复杂:每个 worker 维护一棵 radix tree,记录自己当前缓存了哪些 token 前缀。网关在路由时,拿请求的 prompt 去和各 worker 的缓存摘要做匹配,算出"如果打到这台机器,能复用多少 token"。复用越多,prefill 越省。

难点在于网关不可能持有完整的 radix tree,那玩意儿动辄几百 MB。实际做法是 worker 定期上报一份压缩摘要,比如每棵子树只上报前 N 层的哈希值,或者用布隆过滤器记录"我缓存过哪些前缀块的哈希"。网关拿到摘要后做近似匹配,命中率通常在 85%-95% 之间,够用了。

路由打分的公式我一般是这么写的:

score = w1 * cache_hit_ratio - w2 * normalized_queue_len - w3 * recent_ttft_ms / baseline_ttft

三个权重按业务调。对话类业务把w1调到 0.6 以上,因为前缀复用收益最大;一次性长文本生成类业务w1可以降到 0.3,因为每次 prompt 都不一样,缓存基本无意义,这时候队列长度才是主导因素。

这里有个反直觉的点:缓存命中率高不等于一定要选它。假设 A 机器能复用 3000 token,但队列里有 20 个请求在等;B 机器只能复用 500 token,队列是空的。算下来 A 的排队时间可能比省下的 prefill 时间还长。所以打分必须把队列长度考虑进去,纯看缓存命中率是新手最容易犯的错。

还有一个坑:新 worker 上线时缓存是空的,如果按缓存打分,它会永远拿不到请求,形成"冷启动死循环"。解决办法是给新 worker 一个临时的加分项,或者在启动时预热一批公共前缀(比如系统提示词、few-shot 示例)。我在项目里是让新 worker 前 5 分钟强制分配 10% 流量,5 分钟后回归正常打分。

3.3 结构化输出的网关侧配合

SGLang 的结构化输出能力是它的招牌之一,靠压缩有限状态机做约束解码,速度比逐 token 校验的方案快很多。但网关这一层如果不配合,这个优势会被吃掉一半。

具体来说,结构化输出需要在请求里带上 JSON Schema 或者正则约束。网关要做两件事:一是校验 schema 的合法性,不合法的 schema 直接拒掉,不要让后端去解析;二是对 schema 做归一化和缓存。

为什么要缓存 schema?因为 schema 编译成 FSM 是有成本的,一个中等复杂度的 JSON Schema 编译大概要 5-20ms。如果每个请求都重新编译,这部分开销就白扔了。网关可以按 schema 的哈希值做缓存,命中后直接把编译结果的引用传给后端。实际测试下来,高频使用的十几个 schema 能让平均延迟降低 15-25ms。

另一个细节是流式输出下的部分 JSON 校验。结构化输出是边生成边返回的,客户端拿到的中间态往往是残缺 JSON(比如{"name": "张)。网关不要在流式过程中做完整性校验,那必然失败;只做透传,等流结束后再对完整结果做一次校验并记录指标。我曾经为了"提前发现异常"在流式中间加了校验,结果每条流都报错,日志刷屏。

3.4 多租户限流与配额

限流这一层,很多团队只做了 QPS 限制,这在大模型场景下远远不够。因为一个请求可能是 100 token,也可能是 30000 token,QPS 相同的情况下资源消耗差 300 倍。

我的做法是三层限流:

层级指标典型阈值触发后行为
连接层并发流式连接数200 / 租户直接拒绝 429
请求层每分钟请求数600 / 租户令牌桶排队,超时拒绝
资源层每分钟 token 数(输入+输出)按套餐定超额后降级到小模型或拒绝

资源层限流最难做,因为输出 token 数是不可预知的。你没法在请求进来时就知道它要吐 500 还是 5000。实际方案是:请求进来时按max_tokens预扣,流式结束后按实际用量结算,多退少补。这就要求网关维护一个"预扣池",而且预扣的额度要设置上限,防止一个max_tokens=100000的请求把别人的额度全占光。

代码上大概是这个逻辑:

def reserve_quota(tenant_id, max_tokens): cost = estimate_input_tokens() + max_tokens if not quota_pool.try_reserve(tenant_id, cost): raise QuotaExceeded() return cost def settle_quota(tenant_id, reserved, actual_output): refund = reserved - actual_output if refund > 0: quota_pool.refund(tenant_id, refund)

注意:结算一定要放在finally里,客户端中途断开、后端异常、网关重启,任何一条路径漏了结算,预扣额度就永久泄漏了。我见过一个服务跑了三周之后所有租户额度都耗尽,查下来就是断连路径没结算。

3.5 可观测性三件套

网关是所有请求的必经之路,指标埋点做得好,排查问题能省一半时间。我重点关注四类指标。

延迟类:TTFT(首字延迟)、ITL(token 间隔)、端到端总耗时。这三个必须分 P50/P90/P99 看,只看平均值会被长尾掩盖。TTFT 的 P99 如果比 P50 高 10 倍以上,基本可以断定是缓存失效或者路由打偏了。

吞吐类:每秒输入 token 数、每秒输出 token 数、每秒完成请求数。输入和输出要分开统计,因为 prefill 和 decode 是两个完全不同性质的负载,混在一起看会误判。

缓存类:前缀缓存命中率、平均复用 token 数、radix tree 节点数。命中率下降通常是两个原因:流量模式变了(新业务进来,前缀完全不同),或者路由策略出了 bug。

错误类:按错误码分类、按后端 worker 分类。如果某个 worker 的 5xx 明显高于其他,先别急着下线,看看是不是它承载了更多的长请求。

日志这块,我强烈建议记录完整请求的哈希而不是完整内容。完整 prompt 动辄几千 token,全量落盘一天就是几百 GB,而且有隐私风险。记录哈希 + 前 64 个 token + token 总数,排查问题时足够定位到是哪个请求了。

4. 从零搭一套能跑的网关

4.1 环境准备与目录规划

先说明,下面的配置是我基于常见部署实践整理的一套最小可用方案,不是官方唯一标准,你可以按自己环境调整。

目录结构建议这样分:

model-gateway/ ├── conf/ │ ├── gateway.yaml # 主配置 │ ├── models.yaml # 模型注册表 │ └── limits.yaml # 限流规则 ├── scripts/ │ ├── start.sh │ └── smoke_test.sh └── logs/

依赖上,网关本身是轻量的,主要是 HTTP 框架和 Redis(用于分布式限流的计数器)。如果只有一个租户,Redis 可以省掉,用进程内计数器就行;一旦上多租户或者网关多副本,就必须用共享存储,否则每个副本各算各的,限流形同虚设。

后端 worker 那边,每台机器要暴露一个内部端口用于上报状态(队列长度、缓存摘要、显存占用),这个端口绝对不能对外,否则等于把你的调度信息全公开了。

4.2 配置文件逐字段拆解

主配置我一般这么写:

server: listen: 0.0.0.0:8080 max_body_size: 32MB read_timeout: 300s stream_flush_interval: 20ms routing: strategy: cache_aware weights: cache_hit: 0.55 queue_len: 0.30 ttft: 0.15 health_check_interval: 2s unhealthy_threshold: 3 upstream: connect_timeout: 2s request_timeout: 600s max_idle_conns: 256 retry_on_5xx: 1 observability: metrics_path: /metrics sample_ratio: 1.0

逐条解释几个容易写错的。

stream_flush_interval设为 20ms 是个折中值。设太小(比如 1ms),每个 token 都触发一次 flush,网关 CPU 会被系统调用吃满;设太大(比如 200ms),用户会觉得"字一个一个往外蹦"变成"一段一段往外蹦",体感变差。20-50ms 是流式场景比较舒服的区间。

retry_on_5xx只设 1 次,不要设 3 次以上。因为 5xx 往往意味着后端真的出问题了,重试太多次只会把压力放大,而且流式请求重试的成本极高(已经吐出去的字收不回来)。正确做法是重试到另一个 worker,且只在尚未输出第一个 token时重试。

request_timeout设 600s 看着很长,但长文本生成确实可能跑这么久。我的经验是:这个值要大于max_tokens / 最低生成速度。假设最低生成速度是 5 token/s,max_tokens=2048,那就是 410s,留点余量到 600s 合理。

4.3 启动与冒烟验证

启动之后,先做三步验证,缺一不可。

第一步,模型列表校验:

curl -s http://localhost:8080/v1/models | jq '.data[].id'

输出应该和models.yaml里的别名列表完全一致。如果不一致,说明配置加载顺序有问题。

第二步,非流式请求:

curl -s http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{ "model": "chat-prod", "messages": [{"role": "user", "content": "用一句话解释什么是前缀缓存"}], "max_tokens": 128, "stream": false }' | jq '.usage'

检查usage字段里的prompt_tokens、completion_tokens是否都大于 0。如果是 0,说明后端的 tokenizer 统计没接上。

第三步,流式请求 + 首字延迟实测:

curl -N -s -w "\nTTFT: %{time_starttransfer}s\n" \ http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"chat-prod","messages":[{"role":"user","content":"你好"}],"stream":true}'

time_starttransfer就是首字延迟。冷启动状态下,7B 模型在单卡上大概是 200-400ms;如果超过 1s,先查是不是路由打到了冷 worker 上。

4.4 容量测算与参数调优

这一步是很多人跳过、然后上线就被打爆的地方。我用一个具体例子走一遍。

假设模型是 7B、32 层、8 个 KV 头、每头维度 128、fp16 精度。单个 token 的 KV 缓存大小:

2 (K 和 V) × 32 (层) × 8 (KV头) × 128 (头维度) × 2 (字节) = 131072 字节 = 128 KiB

那么 8K 上下文的单条请求占 128 KiB × 8192 =1 GiB。

再看显存预算。一张 80GB 的卡,7B fp16 权重约 14GB,框架和其他开销留 6GB,剩 60GB 给 KV 缓存。理论上能同时容纳 60 条 8K 请求。但实际要留 10-15% 的碎片余量,所以配置上按50 条来设并发上限比较稳。

吞吐估算换个角度。decode 阶段是访存瓶颈,每生成一个 token 都要把全部权重读一遍。权重 14GB,显存带宽假设 3TB/s,单条序列的理论上限就是3000 / 14 ≈ 214 token/s。batch 增大后,权重读取被多条序列摊薄,总吞吐会明显上升,但单条序列的 ITL 也会上升。

所以在网关侧配置max_concurrent时,要和后端实际能扛的并发对齐。我一般的做法是先压测出后端的"拐点"——即吞吐不再随并发增长、而延迟开始陡增的那个点——然后把网关的并发上限设为拐点的80%。留 20% 是为了应对突发流量和慢请求堆积。

压测脚本我常这么写:

import asyncio, aiohttp, time, statistics async def one(session, sem, prompt, results): async with sem: t0 = time.perf_counter() async with session.post(URL, json=payload(prompt)) as resp: first = None async for line in resp.content: if first is None and line.startswith(b"data:"): first = time.perf_counter() - t0 results.append((first, time.perf_counter() - t0)) async def main(concurrency=50, total=500): sem = asyncio.Semaphore(concurrency) results = [] async with aiohttp.ClientSession() as s: await asyncio.gather(*[one(s, sem, gen_prompt(i), results) for i in range(total)]) ttfts = [r[0] for r in results if r[0]] print(f"P50 TTFT: {statistics.median(ttfts)*1000:.0f}ms") print(f"P99 TTFT: {sorted(ttfts)[int(len(ttfts)*0.99)]*1000:.0f}ms") asyncio.run(main())

压测时一定要用接近真实的 prompt 分布。全用一样的短 prompt,缓存命中率接近 100%,测出来的数字漂亮得毫无参考价值。我一般从线上日志里采样 200 条真实 prompt(脱敏后)作为压测语料。

5. 常见问题排查实录

5.1 首字延迟忽高忽低

这是最高频的问题。表现是同样长度的请求,TTFT 在 250ms 到 3000ms 之间随机跳。

排查顺序我是这么走的:先看是不是打到了不同 worker。在响应头里带上 worker id,然后统计不同 worker 的 TTFT 分布。如果某个 worker 明显偏高,问题就收敛到单机——通常是它的队列里有长请求占着,或者它的缓存树太大导致匹配变慢。

如果所有 worker 都高,看是不是缓存失效。统计命中率,如果从 85% 掉到 40%,很可能是路由策略改了,或者新上线的业务前缀和原有业务完全不重合。这种情况不用改代码,等缓存慢慢建立起来就行,但要在监控上标注一下,免得被误判成故障。

还有一种隐蔽情况:tokenizer 在不同 worker 上的行为不一致。如果不同 worker 加载的 tokenizer 版本有细微差异,同一个 prompt 切出来的 token 序列就不同,缓存永远对不上。这个问题极难发现,因为单看每台机器都正常。检查方法是拿同一个 prompt 分别打各 worker,看prompt_tokens是否完全一致。

5.2 长上下文请求中途断流

超过 16K 的请求,经常跑到一半连接就断了。原因通常有三类。

网关侧超时:read_timeout设了 300s,但长请求在 prefill 阶段就可能卡住超过 300s(尤其是并发高的时候)。解决办法是把 prefill 阶段的超时和 decode 阶段的超时分开设,或者干脆在 prefill 期间定期发心跳保持连接。

客户端侧超时:客户端 SDK 默认超时往往是 60s,长请求必挂。这个要在接入文档里写清楚,让业务方调整。

内核连接跟踪表溢出:高并发下 conntrack 表满了,新连接被丢弃。这个现象是"偶发、集中在高峰",dmesg里会有nf_conntrack: table full的记录。调整内核参数或者改用长连接池能缓解。

提示:长请求一定要在网关侧开启 TCP keepalive,间隔设 30s 左右。很多中间设备(负载均衡、NAT)会在空闲 60-120s 后悄悄掐掉连接,而流式请求在 prefill 阶段恰恰是"有连接无数据"的状态,最容易被误杀。

5.3 前缀缓存命中率上不去

命中率长期在 50% 以下,先别怀疑代码,按这个顺序查。

前缀是否真的可复用。如果业务是"每次用户问一个完全不同的问题",那本来就没多少公共前缀,命中率低是正常的。这时候该做的是在系统提示词上做统一,把共性的指令放到最前面,能显著提升复用率。

路由是否真的按缓存打分。打个日志,看每次路由的候选列表和打分结果。我遇到过配了cache_aware但实际上权重写反了(w2和w3用了负值),结果变成了"越忙的机器越优先",缓存命中率当然上不去。

缓存淘汰策略。radix tree 的节点数是有限的,如果配置的缓存容量太小,长对话场景下前缀会被频繁淘汰。检查 worker 侧缓存占用的显存比例,一般给到总显存的 10-20% 比较合适。

5.4 问题速查表

现象最可能的原因快速验证方法处理方式
TTFT P99 超过 P50 十倍路由打到冷 worker响应头带 worker id,分组统计检查缓存摘要上报是否正常
流式中途断连中间设备空闲超时抓包看 RST 来源开启 TCP keepalive
429 大量出现预扣额度泄漏对比预扣总额与结算总额检查 finally 分支
吞吐上不去但延迟不高并发上限设太低逐步提高并发看拐点设为拐点的 80%
结构化输出偶发失败schema 编译缓存污染对比相同 schema 的成功率按完整哈希做缓存 key
某租户拖垮全局缺少资源层限流按租户统计 token 消耗补三层限流

6. 生产环境里那些文档不会写的事

6.1 灰度与版本回滚

跑通网关之后,下一步一定是"怎么安全地换模型版本"。我的做法是三层隔离:别名层不动、权重层小步调、缓存层预热。

别名层不动是指业务方永远调chat-prod,不感知版本。权重层从 1% 开始,观察 30 分钟,重点看三个指标:新版本的 TTFT 是否劣化、结构化输出成功率是否下降、有没有新增错误码。这三个都正常,再提到 10%、50%、100%。

缓存层预热是最容易被忽略的。新版本 worker 上线时缓存是空的,如果直接切 50% 流量过去,那 50% 的请求全部命中不了缓存,整体延迟会瞬间抬升。正确做法是先在低峰期用一批典型 prompt 预热,或者让新 worker 先接 1% 流量跑半小时,缓存自然建立。

回滚要能做到秒级。这要求网关的模型注册表支持热更新,而不是重启进程。我一般用文件监听或者配置中心推送,改完 5 秒内全网生效。

6.2 我踩过的三个坑

第一个坑:把网关部署在同一批机器上。为了省资源,我把网关和后端 worker 混部,结果 worker 吃满显存时网关进程被 OOM Killer 干掉,整个服务不可用。教训是网关必须和推理进程物理隔离,网关本身资源需求很小(2 核 4G 足够撑几千 QPS),但它的可用性等级比 worker 更高。

第二个坑:健康检查太激进。健康检查间隔设了 1s,超时 500ms。推理服务在高负载下,一个健康检查请求排队 600ms 是很正常的,结果被判定为不健康,从路由池里摘掉,流量涌向剩下的机器,剩下的机器更慢,连锁摘除,最后全挂。后来改成间隔 5s、超时 3s、连续 3 次失败才摘除,稳定多了。推理服务的健康检查必须比普通微服务更宽容,因为它本来就有排队。

第三个坑:日志里打了完整响应。为了排查结构化输出的问题,我一时图省事把完整响应体打进了日志。结果日志量暴涨,磁盘两天写满,而且日志系统本身成了瓶颈。后来改成只记 schema 哈希、输出长度、校验结果三个字段,问题照样能定位,开销降了两个数量级。

6.3 一个容易被低估的优化点

最后分享一个我在多个项目里验证过、收益很稳定的优化:把系统提示词和 few-shot 示例做成"缓存锚点"。

具体做法是,把所有请求共用的那部分前缀(系统指令、格式说明、示例对话)固定下来,一个字都不要变,放在 prompt 最前面。然后网关在路由时,优先保证这批请求打到同一组 worker 上。因为这部分前缀是所有请求共享的,一旦被缓存,后续每个请求都能直接复用这几千 token 的 KV,prefill 成本几乎归零。

实测下来,在客服问答这类场景,做了这个优化之后,平均 TTFT 从 780ms 降到 210ms,整机吞吐提升接近 2.2 倍。代价只是要求业务方不要随意改系统提示词——这个约束一开始推行会有阻力,但把收益数据摆出来之后,大家都能接受。

后续如果想继续深挖,可以往"多级缓存"的方向走:网关侧做前缀的粗粒度匹配,worker 侧做细粒度的 radix 复用,两层配合能把长对话场景的成本再压一截。不过这部分我还在试,等有稳定结论再单独写一篇。

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

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

立即咨询