AI Infra 33 分钟阅读

AI 服务的并发瓶颈与架构设计:从第一性原理到两个真实场景(游戏 AI 并发 / 运营数据问答)

引子:两个真实系统,一套底层物理

这篇文章不谈"如何调 vLLM 参数",而是回答一个更根本的问题:当你要把一个 AI 模型服务从"能跑"推到"扛住高并发、多租户、低延迟"时,到底是什么在拦你?

我手上有两个正在生产运行的系统,它们的画像几乎处在光谱的两端:

  • 场景一:游戏 GUI Agent 并发。 一个 Qwen3-VL 8B 的 VLM,看一两张游戏截图,输出一个点击/滑动动作。当前单步 1000–2800ms,目标是让几十上百个云手机客户端共享一个推理集群,并把单步压到 500ms 以内。这是一个视觉输入重、输出极短、延迟敏感、天然高并发的负载。
  • 场景二:游戏运营数据问答(Text2SQL)。 一个 Qwen2.5-7B 的 LoRA 模型,把自然语言问题翻译成 SQL,打到两个 MySQL 库上,返回答案。当前单进程 FastAPI,49 条验收 97.9%。目标是扛住多用户/多租户并发。这是一个文本输入中等、输出中等、瓶颈在数据库而非 GPU、安全是头号风险的负载。

它们看起来毫无共同点。但两者都跑在同一台机器上——一台 8×RTX 5090 32GB、没有 NVLink 的服务器。而正是这套共同的底层物理(以及 LLM 推理本身的计算特性),决定了它们的架构必须遵守同一批第一性原理约束。

本文的实测数据来自这台机器的真实拓扑与服务状态;架构结论综合了 2025–2026 年 LLM/VLM serving 领域的公开研究(vLLM/SGLang 官方文档与 PR、DistServe/Mooncake/Sarathi-Serve/EPD/RTC/CHASE-SQL 等论文、Uber/LinkedIn/Databricks/Snowflake 的工程博客)。文末给出核心引用。凡属"厂商宣称"而非可复现测量的数字,我都会标注。

先把这台机器的真实样子摆出来,因为后面所有架构决策都从它长出来:

$ nvidia-smi topo -m
       GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7
GPU0    X   PIX  NODE NODE SYS  SYS  SYS  SYS
GPU1   PIX   X   NODE NODE SYS  SYS  SYS  SYS
GPU2   NODE NODE  X   PIX  SYS  SYS  SYS  SYS
GPU3   NODE NODE PIX   X   SYS  SYS  SYS  SYS
GPU4   SYS  SYS  SYS  SYS   X   PIX  NODE NODE
GPU5   SYS  SYS  SYS  SYS  PIX   X   NODE NODE
GPU6   SYS  SYS  SYS  SYS  NODE NODE  X   PIX
GPU7   SYS  SYS  SYS  SYS  NODE NODE PIX   X

三个事实刻进了骨头里:

  1. 没有 NVLink。 卡间最好的连接是 PIX(同一 PCIe switch 下成对),跨对是 NODE(过 CPU),跨 NUMA 是 SYS(过 socket 互联)。任何跨卡的高频张量通信都要走 PCIe,而不是 900GB/s 的 NVLink。
  2. 32GB 显存/卡。 比 A100 80G 小得多,KV cache 预算是硬约束。
  3. sm120 架构(Blackwell 消费级)。 很多为 Hopper 调优的 kernel 在这里要么没编译、要么没调优。

记住这三条,我们开始。


Part 1:AI 服务并发的第一性原理

1.1 一次推理请求,其实是两个物理特性相反的阶段

这是理解一切的起点。LLM/VLM 生成一次回复,分成两个阶段,它们的硬件瓶颈完全相反:

阶段 做什么 瓶颈 特征
Prefill 并行处理整个输入 prompt,算出第一个 token compute-bound(算力) 一次性、高并行、吃满 GPU 的 FLOPs
Decode 自回归地逐个生成 token memory-bandwidth-bound(显存带宽) 每步只算 1 个 token/请求,算力利用率极低,靠搬运 KV cache 和权重

这个区分是过去三年几乎所有推理架构创新的源头。为什么?因为:

  • Prefill 决定 TTFT(Time To First Token,首字延迟)。它是 compute-bound 的,所以一个超长 prompt 会把 GPU 算力占满。
  • Decode 决定 TPOT/ITL(Time Per Output Token / Inter-Token Latency,吐字速度)。它是 memory-bound 的,单请求时 GPU 算力大量闲置——这正是"batch 起来"能提升吞吐的根本原因:把多个请求的 decode 拼在一起,用同一次权重搬运服务多个请求,才能把闲置的算力填上。

一个直接的推论:batch 越大,吞吐越高,但每个请求的延迟越差(每次 iteration 要处理更多请求)。吞吐和延迟从物理上就是对立的。所有调度策略,本质上都是在这条曲线上选一个工作点。

1.2 KV Cache:真正的容量墙

decode 阶段为什么是 memory-bound?因为每生成一个 token,都要读一遍之前所有 token 的 Key/Value 向量——这就是 KV cache。它必须常驻显存,而且随序列长度线性增长

KV cache 的大小可以估算:

每 token 的 KV 字节数 ≈ 2 (K和V) × num_layers × num_kv_heads × head_dim × dtype_bytes

对一个 7B/8B 模型,FP16 下大约是每 token 数十 KB 量级。这意味着:

在 32GB 的 5090 上,扣掉模型权重(7B FP16 约 14GB,或 4-bit 约 4.5GB),剩下的显存决定了你能同时挂多少并发序列 × 多长的上下文。 这是一个硬预算:

可容纳的总 KV token 数 = (显存 - 权重 - 激活开销) / 每token KV字节
并发序列数 × 平均上下文长度 ≤ 总 KV token 预算

一旦 KV cache 占满,新请求要么排队,要么触发抢占(preemption)——把某个请求的 KV 换出到 CPU 或直接重算。抢占会以 ITL 尖刺的形式泄漏给用户。所以 kv_cache_usage_perc 接近 1 是一个一线告警信号:你即将开始抢占,延迟即将崩。

在消费级 GPU 上,几乎必开 FP8 KV cache——它把 KV 容量直接翻倍,让你在同样显存下要么支持 2× 并发,要么支持 2× 上下文。代价是极小的精度损失(但 KV 量化对某些模型敏感,Qwen 系尤其要实测,不能盲开 INT4 KV)。

大多数人的直觉是:8 张卡,那就 tensor parallel(TP)把它们当一张大卡用嘛。在这台机器上,这几乎总是错的。

TP 需要在每一层做 all-reduce 通信。有 NVLink 时这是 900GB/s 的片内操作;没有 NVLink 时,它要走 PCIe。研究和实测反复证明:

  • 跨节点/跨 PCIe 的 TP,decode 阶段的 median TBT(token 间延迟)可能恶化 2 倍以上(Sarathi-Serve 等)。
  • 消费级双卡实测,短上下文多任务负载下,2×5090 的 TP 配置吞吐反而比单卡低 11%、延迟高 3.8 倍(《Consumer Blackwell》类实测)。
  • vLLM 在 TP>2 且检测不到 P2P 时会禁用 custom all-reduce,回退到更慢的 NCCL/PCIe 路径。

正确形态是:每张卡跑一个完整的模型副本(DP,data parallel),前面放一个 cache-aware router 做请求分发。 这与我之前那篇《在 8×RTX 4090 上部署 Qwen3-32B》里的核心教训一致——那篇里 32B 装不进单卡,不得不用 SGLang EdgeAcc 的单卡 P/D 分离 + DP Attention 硬扛;而 7B/8B 能装进单卡,就更没有理由去跨卡 TP。

双卡的价值不是"提吞吐",而是"当单卡显存装不下你要的上下文长度时,用两张卡把 context 从 32k 推到 64k+"。扩容量,不是提速度。

一句话记住:消费级无 NVLink 集群上,是"N 个单卡独立引擎 + 智能路由",不是"把 N 张卡焊成一张大卡"。

1.4 多租户的 noisy neighbor:LLM 的特化形态

传统 Web 服务里,一个用户的请求再重,也就占一个线程。LLM 不一样。一个租户提交一个 100k token 的超长 prompt,会发生什么?

  • 这个 prefill 是 compute-bound 的,它会占满 GPU 一个或多个 iteration。同一个 batch 里所有其他租户的 decode 全部暂停——表现为其他人的吐字突然卡顿(ITL 尖刺)。
  • 它的 KV cache 可能吃掉数十 GB,把别人的请求挤到被抢占。
  • 而这个租户,只用了 1 个 request。任何基于"请求数"的限流(RPM)对它完全无感。

这就是为什么 对 LLM 用请求数限流是错的。单请求的成本方差跨越 4–5 个数量级(OpenTelemetry GenAI 规范给 token 用量的直方图桶一直排到 6.7×10⁷)。正确的计量单位是 token,而且要区分:

  • ITPM(input tokens/min):对应 prefill,compute 瓶颈。
  • OTPM(output tokens/min):对应 decode,带宽瓶颈。

Anthropic 的 API 就是把 ITPM 和 OTPM 分开限的——因为它们打在不同的硬件资源上。这是可以直接抄的设计。

缓解 noisy neighbor 有两条正交的路径,必须同时上:

  1. 引擎层:chunked prefill。 把长 prefill 切成小块,让新请求加入 batch 时不必等一整个巨型 prefill 跑完。vLLM/SGLang 默认已开,确认别被关掉即可,零成本。
  2. 调度层:token 计量的公平调度(VTC 类)。 给每个租户维护一个虚拟 token 计数器,优先服务"累计消费最少"的租户,保证任意两个持续活跃租户之间的服务量差距有界。

⚠️ 一个容易踩的坑:如果你的租户各自有独立的长 system prompt,纯公平调度会摧毁 prefix cache 命中率(它强制在不同租户间轮转,而它们前缀不同)。这时要用 locality-aware 的变体(DLPM 类),否则可能损失 2.87× 吞吐。

1.5 通用武器库(一句话速查)

在进入两个具体场景前,把通用手段列成一张速查表。这些是"默认应该知道"的:

技术 解决什么 何时用
Continuous batching 迭代级调度,请求随时进出 batch 永远开(现代引擎默认)
PagedAttention KV cache 分页,消除碎片 永远开
Chunked prefill 长 prompt 不阻塞 decode 永远开,noisy neighbor 第一道防线
Prefix caching (RadixAttention/APC) 复用共享前缀的 KV 有共享 system prompt / 多轮 / agent 时收益巨大
PD 分离 prefill 与 decode 分池,各自优化 仅当 TPOT 严格、TTFT 宽松,且有高速 KV 传输通道。无 NVLink 时慎用
单卡多副本 DP + router 消费级无 NVLink 的正解 模型能装进单卡时
FP8 KV cache KV 容量翻倍 消费级几乎必开
Multi-LoRA 一个 base 服务多个微调 adapter 多租户微调模型,单卡可挂几十到上百 adapter,惩罚 <5%
约束/结构化解码 保证输出语法合法 输出有固定结构(JSON/SQL)时

关于 PD 分离要多说一句,因为它是最被过度神话的技术。vLLM 官方文档明说:PD 分离不提升吞吐,它买的是 SLO 的可控性。在公平的双卡对照实验里,colocated(不分离)在所有 batch size 上 TTFT 都更好,PD 分离只在 batch 极大(KV 溢出触发重算)时才在 TPOT 上赢。而且它依赖高速 KV 传输——无 NVLink 时,跨卡传 KV 走 PCIe,TTFT 会明显恶化。所以在这台机器上,我们两个场景都不用 PD 分离。


Part 2:场景一 —— 游戏 GUI Agent 的实时并发

2.1 先定位瓶颈:你的 1–2.8 秒,几乎全是 output token

这是整个场景最重要的一个结论,也是最反直觉的一个。整个 VLM 效率文献都在优化"视觉编码"和"visual token 剪枝",但对这个场景,那些几乎都不是重点。

用 vLLM 官方在单张 A100 上 Qwen2.5-VL-7B 的实测数据(并发 50):TTFT(含 ViT 编码 + prefill)只有 58ms,TPOT 是 16.36ms/token。据此外推端到端单步延迟:

output tokens 端到端延迟 decode 占比
8 ~189 ms 69%
16 ~320 ms 82%
32 ~582 ms 90%
64 ~1105 ms 95%
128 ~2152 ms 97%
256 ~4246 ms 99%

看到没有?你报告的 1000–2800ms,精确对应 60–170 个 output token——也就是一个 think 字段 + 一个 action JSON 的典型长度。视觉编码在并发下只占 2–6%。

这个发现直接重排了优化优先级。sub-500ms 的可行预算(单卡 A100/H100 量级,并发 30–60):

ViT 编码(720p 单图):10–20 ms
prefill(~1200 visual token + 文本):20–60 ms
decode:output_tokens × 13–17 ms   ← 这一项是主导
⇒ 要进 500ms,output 必须 ≤ 28 token(A100)/ ≤ 45 token(H100)

所以第一优先级不是任何系统优化,而是砍 output token: 极简化或去掉 think 字段,action 用最紧凑的格式(甚至纯坐标),用结构化解码硬约束输出长度。这一条的收益是决定性的——64 token → 16 token,在并发 50 下就是 1105ms → 320ms。

这也解释了我记忆里的一个旧结论:S2 消融时"单帧+动作文本 ≈ 双帧精度,推理 1666→1077ms(-35%),但 500ms 需砍输出 token"——当时的直觉是对的,现在有了屋顶线数据支撑:500ms 门槛的唯一钥匙就是输出长度。

2.2 一个幸运的事实:"很多游戏客户端"是天然优势,不是负担

单个 Agent 回合是严格串行的(必须看到上一步结果才能决定下一步),单 episode 内无法并行。但——N 个云手机跑 N 个独立 episode,天然构成 batch = N 的负载,没有任何依赖、没有同步点。

这直接把你从"batch=1 的 memory-bound 灾难区"搬到"decode 高效区"。还是那份 A100 实测:

并发 Req/s TPOT 相对吞吐
10 6.08 13.69 ms
50 20.89 16.36 ms 3.4×

从并发 10 到 50,吞吐涨 3.4 倍,而 TPOT 只涨 20%,TTFT 涨到 58ms。 这是极好的 scaling。

推论,而且是很多人会搞反的推论:不要为了降延迟而降并发。 恰恰相反,应该把并发推高到 TPOT 开始明显恶化的拐点(多图负载通常在某个 QPS 后 P99 TPOT 暴涨),然后在那个点前设限。GPU 利用率上去了,单步延迟几乎没变差。

2.3 藏住延迟:异步推理 + Action Chunking

机器人/具身智能社区比 GUI Agent 早两年撞上"大模型延迟 vs 实时控制"这个墙,方法论已经成熟,直接可抄。

核心范式叫 异步推理(asynchronous inference):在执行当前动作的同时,并行计算下一个动作。这样推理延迟被藏进动作执行的时间里,控制回路不再"停顿-前进-停顿"。

配套的是 action chunking:让模型一次预测未来 k 步动作,开环执行前几步,同时后台已经在算下一批。Physical Intelligence 的 Real-Time Chunking(RTC) 把 chunk 衔接建模成一个 inpainting 问题,实测能容忍 >300ms 的推理延迟(超过预测 horizon 的 30%)仍完成高动态任务,比同步执行还快 20%。

对游戏 Agent 的映射:

  • 如果模型能改造成一次输出 3–5 步的战术动作序列,就能用异步推理把 300ms 的单步推理完全藏进执行时间。这是把"单步 500ms"这个硬指标绕过去的结构性手段——与其把单步压到 500ms,不如让 500ms 的推理不阻塞控制回路。
  • 高频反应事件(血量低了吃药、按钮出现就点)不该走 VLM,应该走一个 dual-system 里的"快速反应层"(规则 / 小 grounding 模型),VLM 只做 3–5 步的战术规划。这是 GR00T N1 那套 System1/System2 的思路。

必须实现的最小 staleness 机制: 请求带上帧时间戳;推理返回时,如果当前帧和请求帧差了超过阈值(比如 2 帧或 200ms),丢弃这个 action 而不是执行它——避免点到已经变化的 UI。这与我之前记的"界面无响应兜底逻辑"一致,现在有了文献支撑(Don't Act Blindly 的 action-effect verification)。

2.4 榨干这台机器:prefix cache + session affinity + 帧序

同一个 episode 的连续 step 共享一大段前缀:固定的 system prompt + 累积的文本动作历史 + 上一帧图像。这里有两个高杠杆的免费优化:

(1) Session affinity 路由——必做。 必须把同一个 episode 的连续请求路由到同一个引擎实例,否则每一步都是 cache miss。SGLang 的 cache-aware router 在长共享前缀下能拿到 2× 吞吐;它还有一个专门为 agent/RL rollout 场景写的 session-aware radix cache(PR #29173),防止高并发下活跃 session 的前缀被互相踢掉。没有它,并发一高,你的 episode 前缀会互相驱逐,命中率崩塌。

(2) 帧序必须是"旧帧在前"。 把 prompt 组织成 [固定system][文本动作历史][上一帧图][当前帧图][query]。这样前三段跨 step 全部前缀命中,只有当前帧需要新算 ViT + prefill。关键是:上一帧图像其实就是上一步的"当前帧",用同一个 multi_modal_uuids 就能让它的 ViT embedding 和 KV 都命中缓存。 但前提是它在 prompt 里的位置不变——如果你把当前帧放前面,前缀就断了,全部失效。

这也和我记忆里 v5.1 数据格式的血泪教训对上了:"上一帧在前、当前帧在后……我 07-03 一度改成'当前帧在前'是错的,已回退"。当时是为了训练分布一致性回退的,现在从推理 prefix cache 的角度看,旧帧在前不仅训练对,推理上也是唯一能命中缓存的顺序。 两个理由指向同一个格式。

(3) 文本历史替代图像历史——这是 SOTA 的公认做法。 UI-TARS 的显式设计就是:图像只保留最近 N 帧,动作+thought 全量以文本形式保留。算术很暴力:1080×1920 一帧 = 2691 token,保留 5 帧 = 13455 token;而一条文本动作记录("click(520,1180) 点击开始战斗")约 20 token,5 步才 100 token。图像历史比文本历史贵 130 倍。

2.5 这台机器上的部署形态

综合以上,游戏 AI 服务在 8×5090 上的形态:

                    ┌─────────────────────────────────────┐
   N 个云手机        │   Async 请求队列 + Cache-aware Router  │
   (独立协程)  ───▶  │   - session affinity(episode 粘同实例) │
   带帧时间戳         │   - staleness 检查(超时帧丢弃)         │
                    └───────┬──────────┬──────────┬─────────┘
                            ▼          ▼          ▼
                      ┌─────────┐ ┌─────────┐ ┌─────────┐
                      │ 5090 #0 │ │ 5090 #1 │ │ 5090 #2 │ ...每卡一个完整
                      │ SGLang  │ │ SGLang  │ │ SGLang  │   Qwen3-VL-8B 引擎
                      │ FP8 KV  │ │ FP8 KV  │ │ FP8 KV  │   (DP,不是 TP)
                      │ ViT CG  │ │ ViT CG  │ │ ViT CG  │
                      └─────────┘ └─────────┘ └─────────┘

关键配置(全部零/低成本,按 ROI 排序):

  1. 每卡一个独立引擎(DP),不做 TP。 无 NVLink 的铁律(见 1.3)。8 卡就是 8 个副本,吞吐线性叠加。
  2. 降分辨率。 1080p→720p 直接砍 56% visual token,一行 max_pixels 配置,无需改模型。手游按钮通常 ≥80px,720p 下仍占 3×3 token,安全。
  3. ViT CUDA graph + FlashInfer 后端。 固定分辨率的游戏截图意味着 shape bucket 极少,graph 命中率接近 100%。实测能把 encoder 的 P99 从 172ms 砍到 26ms——这是游戏 Agent 相对通用 VLM 服务的结构性优势,一定要吃到。
  4. ViT DP + LM TP 模式(--mm-enable-dp-encoder):ViT 走 DP、LM 走 TP,一致降低 TTFT,无精度损失。
  5. FP8 KV cache: 32GB 显存下把并发能力翻倍。

必须自己先测的三件事(文献里没有答案):

  • adb screencap / scrcpy 的截图捕获延迟——可能是隐藏的最大项,且完全在推理优化射程之外。这与我之前排查云手机投屏的经验相关。
  • inference 节点的 CPU 利用率——vLLM 实测过"CPU 100%、GPU 20%"的情形,因为 N 个云手机的 PNG 解码 + resize 全压在推理节点 CPU 上。解法:在设备侧就降分辨率,或把预处理挪出去。
  • 5090 上 Qwen3-VL 8B 的 TTFT/TPOT/QPS 曲线——sm120 无公开基准,必须自测。

2.6 优先级清单

# 动作 收益 成本
1 砍 output token(去 think、紧凑 action、结构化解码限长) 决定性(64→16 token = 1105→320ms) 低(改格式/prompt)
2 把并发推高到 TPOT 拐点前 吞吐 3.4×,TPOT 只 +20% 极低
3 降分辨率 1080p→720p visual token −56% 极低
4 ViT CUDA graph + FlashInfer P99 encoder −85%
5 ViT DP + LM TP TTFT↓ 吞吐↑ 极低
6 Session affinity + session-aware radix cache 长前缀 2× 吞吐
7 帧序"旧帧在前" + 稳定 mm_uuid 上一帧 ViT+KV 跨步命中,encoder 负载 −50%
8 异步推理 + action chunking + staleness 丢弃 把推理延迟藏进执行时间,可容忍 300ms+ 中高(需改输出为 chunk)
9 测基座替换(InternVL3.5-2B/4B,TPOT 6.57/11.57ms vs 7B 的 16ms) 可能是单项最大收益 中(需重训评估)

Part 3:场景二 —— 多租户运营数据问答(Text2SQL)

这个场景的画像和游戏 AI 完全相反,所以架构结论也几乎全反过来。

3.1 先纠正认知:97.9% / 49 条,统计上没有信号

先泼一盆冷水,因为它直接影响你对"系统已经很好了"的判断。

48/49 的 Wilson 95% 置信区间是 [89.3%, 99.6%]。 要在统计上区分"真实 95%"和"真实 90%",需要约 431 条样本。叠加两个工业界测到的噪声源:Uber 明说同一评测集重跑有 ~5% 的波动;LinkedIn 发现其基准里 ~60% 的问题有多个正确答案,不补全会低估 recall 10–15%。

更要命的一件事:如果你的 49 条验收题同时也在 RAG 的 few-shot 索引里,那 97.9% 是自我验证,不是泛化。 Snowflake 的评测机制专门为此设计——把用作评测的 verified query 从语义视图里临时移除再生成 SQL,确保"不是靠记住答案"。

立刻要做的第一件事:把验收集从检索索引里剔除后重测。 我预期这个数字会明显下降,而那才是你真实的起点。然后把验收集扩到 150–300 题,覆盖多个运营业务域,每题准备 2–3 种措辞变体,并允许多个正确答案。

3.2 生产 NL2SQL 的六段流水线

把 Uber、LinkedIn、Databricks、Snowflake、Google 五家的公开架构叠在一起,2026 年的生产 NL2SQL 已经收敛成一条六段流水线,而不是"一次 LLM 调用":

用户问题
 → ① 意图/复杂度路由(小模型分类:查表?取指标?多步归因?闲聊?)
 → ② Scope 缩窄(workspace / 主题域 / 语义视图 —— 人工策展的边界)
 → ③ 检索(schema + 已验证SQL样例 + 业务术语,向量+重排)
 → ④ 生成(约束/结构化解码;优先走 metric query 而非 raw SQL)
 → ⑤ 校验(AST + 存在性 + EXPLAIN,不执行)→ 有界自纠
 → ⑥ 执行(受治理的只读账号)→ NL 总结 + 可视化

最重要的一条洞察:所有成熟产品都在第 ② 步放了人工策展的边界,而不是让模型直面全库。这是准确率的最大单一来源,超过模型能力本身。 好消息是,你的 schema 规模很小(93 篇文档 vs LinkedIn 的百万级表),所以工业界一半的复杂度(为了在万级表里做检索)你根本不需要。schema linking 不是你的瓶颈——甚至有研究(《The Death of Schema Linking?》)指出,当 schema 能塞进上下文时,过度检索反而有害(RAG 漏掉必需的表,模型无论如何都对不了)。建议做个消融:全 schema 直塞 vs 当前 RAG。

3.3 最高 ROI 的一步:写一份业务语义文档

这是本场景给你的最强建议,有可复现的实测支撑。

2026 年 4 月的一篇论文(arXiv:2604.25149)做了严格的配对实验:3 个前沿模型 × 100 个问题 × 跑在 ClickHouse 上的零售数据集:

条件 准确率
只给 warehouse schema 45.5% – 50.5%
schema + 一份 4KB 手写 markdown(描述指标、约定、消歧规则) 67.7% – 68.7%
增益 +17 到 +23 个百分点(p < 0.01)

论文的原话极重要:"语义层文档解释了几乎全部的显著方差,而同一档位内的模型选择不能。"

翻译成人话:与其继续调 LoRA 或换基座,不如写一份 4–8KB 的业务语义文档(游戏运营指标定义、口径约定、维度层级、字段消歧规则、术语缩写表、同义词)。这个增益对模型无关——换模型也不会丢。对游戏运营域尤其关键,因为它是典型的"文档不足的专家领域":服务器/大区口径、活动周期、付费档位、留存定义,这些不写下来模型永远猜。

但也别神话它:即使给了语义文档也只有 ~68%。语义层不是万能药,它把错误率从 ~54% 降到 ~32%。而且 Snowflake 诚实地披露:即使把语义层做到 DDL 级别,也只有约 10% 的真实提问能走 metric query,90% 仍要 fallback 到 raw SQL。所以正确形态是双通道 + 自动降级:命中已验证指标走 metric query(确定、可缓存、可预聚合),未命中走受保护的 raw SQL,并持续把高频 fallback 回填进语义层。

3.4 拆掉错误重试环——它是你最贵、最脆弱的部件

你当前的"SQL 报错 → 喂回模型 → 重新生成"这个环,是我建议优先重构的对象。理由是一个硬数据:

专门评测"LLM 修 SQL"能力的基准(BIRD-CRITIC)显示,领先的推理模型 O3-Mini 成功率只有 33–39%。

也就是说,你的重试环假设"把报错喂回去模型能修",而这个假设只有约 1/3 的时候成立。更糟的是,失败的重试不是免费的:

  • 成本: 每次重试都是一次完整 prompt(含 schema + few-shot)重放。3 次重试 = 4 倍生成成本 + 4 倍输入 token。
  • 尾延迟(真正的杀手): 一条会全表扫描的坏 SQL,可能失败前先跑 30 秒。重试环把"慢查询"和"重试次数"乘起来了:一个正常 3s 的问答,最坏路径变成 3×(3s + 30s) = 100s,而这 100s 里一直占着一个 worker、一个 DB 连接。

正确的替代顺序是:预防 > 校验 > 有界重试。

  1. 语法错误用约束解码消灭(见 3.5)。 语法非法不该靠重试,应该在解码阶段就不可能发生。
  2. 语义错误用"不执行的校验"消灭。 LinkedIn 的原则值得抄成规约:"校验器最有用的时候,是它能访问查询生成器看不到的新信息。" 具体:AST 解析 → 表/列存在性检查(对齐 information_schema)→ EXPLAIN。EXPLAIN 顺手给你执行计划,可以在执行前就拒掉 type=ALL(全表扫描)、估算行数超阈、缺分区过滤的 SQL——Uber 就反复吃过"忘加分区过滤"的亏。
  3. 重试上限 = 1,且必须携带新信息(EXPLAIN 输出 + 真实列清单 + top-K 取值),而不是干巴巴回灌 stderr。第 2 次失败 → defer 给用户澄清(多选题消歧),而不是继续烧 token。
  4. 缓存已验证的 SQL。 最高 ROI:把 {问题, SQL, 验证时间, 验证人} 存成一个活的 Verified Query Repository。Databricks 的"Request Review"让用户一键送审 → 人工核验/改正 → 回写,把生产流量变成免费的标注机。你的 49 条验收集应该演化成这样一个活的仓库。

3.5 约束解码:一次消灭语法错误 + 表列幻觉 + 非 SELECT

这是给你的最高杠杆的单点建议之一。

约束解码(XGrammar / Outlines,vLLM/SGLang 已内置)能保证输出符合一个上下文无关文法(CFG)。它能做什么、不能做什么要分清:

不能
保证输出是语法合法的 SQL 保证 join/口径/过滤语义正确
保证只生成 SELECT(把文法限制成 SELECT 子集)——这是极强的安全原语 保证结果语义正确
保证 tool-call JSON 结构正确

强化版:把 schema 编进文法。 既然你只有 93 篇文档规模的库,动态生成一个只包含合法表名/列名作为终结符的 CFG 是完全可行的。这一步同时消灭三类问题:语法错误、表/列幻觉(编造不存在的字段——这是 Uber 至今"没完全解决"的问题)、非 SELECT 语句。对小 schema 系统,这是杀手锏。

3.6 真正会把你打挂的:数据库瓶颈 + 安全

GPU 通常不是这个场景的瓶颈,MySQL 和安全才是。

数据库侧的三个结构性问题: LLM 生成的分析型 SQL 有三个和 OLTP MySQL 严重不匹配的习性——无索引的宽条件聚合(全表扫描)、大 GROUP BY/ORDER BY(临时表落盘)、忘记分区过滤。行存 + 单查询单线程意味着一条坏 SQL 就能长时间占住连接和 IO,并污染 buffer pool 里其他租户的热数据。

可承载 QPS ≈ 分析型连接池大小 / 平均 SQL 执行时间。 池=16、平均执行 1s → 上限约 16 QPS;若退化到 5s(全表扫)→ 3 QPS。这就是为什么预聚合是并发的第一杠杆: 把头部 20–50 个运营指标(DAU/留存/付费/关卡漏斗)做成物化视图(按天×服×渠道),让 LLM 只对这些窄表写 SQL——同时解决准确率(表窄语义清晰)、延迟(毫秒级)、并发(不打原始大表)三个问题。把平均执行从 2s 降到 0.15s,等于 DB 容量放大 13 倍,不用加机器。

三层超时防御(因为 MySQL 的 max_statement_time 官方明说不能作为防资源耗尽的唯一手段,它"不是立即中止,是定期检查计时器"):

  1. 执行前:EXPLAIN 门禁拒绝坏计划;
  2. 执行中:max_statement_time + 应用层 asyncio.wait_for 双超时;
  3. 执行后:独立 watchdog 轮询 information_schema.processlist,对超阈会话 KILL QUERY

安全是这个场景最可怕的部分。 P2SQL 注入(通过自然语言诱导生成恶意 SQL)在 ICSE 2025 的论文里被证明对 LangChain 类应用"高度易感"。纵深防御,按优先级:

  • 第 0 层(最重要):架构隔离。 LLM 永远不持有数据库凭证。生成的 SQL 是"待审的数据",由一个独立的、无 LLM 参与的执行服务审核后才执行。账号 GRANT SELECT ONLY,禁 LOAD_FILE/INTO OUTFILE/LOAD DATA。表/视图白名单——不是"过滤 SQL 里的表名",而是权限上就不存在,把"AST 漏判"的后果从"数据泄露"降级成"报错"。
  • 第 1 层:AST 校验 + 改写。 用 sqlglot,但必须 parse_one(sql, dialect="mysql")(不传 dialect 会在"所有方言超集"上判断,比 MySQL 实际接受的更宽,白名单会被绕过)。allowlist 式(只允许我认识的节点),强制注入 LIMIT,拒绝 exp.Command(sqlglot 对无法解析结构的兜底节点,是最危险的逃逸口),canonicalize 后只执行重新生成的 SQL(消灭注释注入、多语句、Unicode 同形一大类攻击)。
  • 第 2 层:租户过滤不能靠 prompt。 绝不能在 system prompt 里"请加上 WHERE tenant_id=X"。MySQL 没有原生 RLS,替代是 per-tenant 安全视图(视图定义里写死 WHERE tenant_id=<固定值>)+ 只给视图 SELECT 权限。这是唯一在"模型完全失控"时仍成立的机制。
  • 第 4 层:审计。 记录提问用户/问题原文/生成 SQL/AST 判定/改写 diff/EXPLAIN 计划/扫描行数/耗时/重试次数/租户 ID。注意审计日志本身含敏感数据,需同等保护。

3.7 让 3–10 秒感觉不慢 + 分层缓存

流式暴露中间产物,顺序按"最先可见、最有信息量"优先:意图回显(~200ms)→ 选中的表+为什么(~1s)→ 生成中的 SQL(token 流)→ 校验勾选项 → 执行中(行数计数器)→ 结果 → NL 总结。LinkedIn 的经验:内嵌进现有数据平台 vs 独立 chatbot,采用率差 5–10 倍;而那个"Fix with AI"按钮占了 80% 的会话——最高 ROI 的功能其实是"帮用户修他自己写坏的 SQL",不是从零生成。

分层缓存,从便宜到贵,注意误命中比未命中危险得多(返回一个"看着对但口径是上个月"的答案就是一次错误决策):

  1. 精确匹配(归一化问题文本)→ 直接返回;
  2. VQR 命中(问题相似 → 复用 SQL,重新执行拿最新数据) ← 这层最有价值,SQL 复用安全,结果复用才有陈旧风险;
  3. 结果缓存(同 SQL + 短 TTL);
  4. 语义缓存(向量相似)→ 最后一层,最严阈值,且必须按租户分区(否则跨租户串数据),多轮场景必须做 context-aware 匹配。

顺带戳破一个营销:GPTCache 宣传"降本 10×、提速 100×",但独立实测它的准确率只有 37.9%。语义缓存收益真实存在,但你必须自己测误命中率

3.8 优先级清单

优先级 动作 依据
第 0 周 验收集从 RAG 索引剔除后重测;扩到 150–300 题、多措辞、允许多答案 防自我验证;48/49 置信区间 [89.3%,99.6%]
第 1 优先 写 4–8KB 业务语义文档 +17~23pp,与模型无关
第 1 优先 AST 安全加固(dialect=mysql、白名单、canonicalize)+ 只读账号 + 白名单视图 P2SQL 是 ICSE 2025 实锤,当前最大未缓解风险
第 1 优先 三层超时 + processlist watchdog max_statement_time 不能单用
第 2 优先 头部 20–50 指标做预聚合表 准确率+延迟+并发三收
第 2 优先 EXPLAIN 执行前门禁;重试上限降到 1 且带新信息;第 2 次失败转澄清 LLM 修 SQL 仅 ~38%
第 2 优先 per-tenant 并发信号量 + 独立小连接池 + 只读副本 DB 才是瓶颈
第 3 优先 Schema-aware 约束解码(schema 编进 CFG) 一次灭语法错误+表列幻觉+非SELECT
第 3 优先 LoRA 训练数据补 reasoning trace + auto-thinking +9.5pp;简单问题跳过 CoT 省 25% token
第 3 优先 VQR + Request Review 闭环;分层缓存 把生产流量变免费标注机

Part 4:两个场景的架构对偶性

写到这里,最有意思的是把两个场景并排看——它们几乎在每一个维度上都是对偶的,而这种对偶恰恰印证了 Part 1 的第一性原理:架构不是抄模板,是从负载的物理特性推导出来的。

维度 游戏 AI 并发 运营数据问答
模型 Qwen3-VL 8B(视觉) Qwen2.5-7B LoRA(文本)
输入 重(截图,~1200 visual token) 中(schema + 问题)
输出 极短(几十 token) 中(SQL 几百 token)
延迟瓶颈 output token 数(decode) SQL 执行 + 重试(DB,非 GPU)
吞吐瓶颈 visual token(prefill/encoder) 数据库连接池
并发性质 天然高 batch(N 独立 episode) 独立请求,DB 争抢
首要优化 砍输出长度 写语义文档 + 拆重试环
头号风险 单步延迟 / 帧陈旧 SQL 注入 / 全表扫描
缓存关键 prefix cache(session 前缀 + 上一帧) VQR(已验证 SQL)+ 预聚合
GPU 角色 核心瓶颈 通常不是瓶颈
这台机器的用法 8 卡 = 8 个 DP 副本 + cache router 1–2 卡 SGLang + 约束解码

而它们共享的、不可违背的底层约束只有几条,全部来自 Part 1:

  1. 无 NVLink → 单卡多副本 DP,不做跨卡 TP。
  2. 32GB 显存 → KV cache 是硬预算,FP8 KV 几乎必开。
  3. 延迟与吞吐物理对立 → 按负载特性选工作点(游戏推高并发换吞吐,问答控并发保 DB)。
  4. 计量用 token 不用请求数 → 多租户公平和限流的唯一正确基础。
  5. 能在解码/校验阶段消灭的错误,绝不留给重试。

这就是为什么我说,理解并发瓶颈不是背一堆技术名词,而是先搞清楚你的负载到底卡在 compute、bandwidth、KV capacity、还是下游的数据库上——定位对了,架构几乎是自明的。


核心引用

推理引擎与调度: vLLM/PagedAttention(arXiv:2309.06180)、SGLang/RadixAttention(2312.07104)、Sarathi-Serve/chunked prefill(2403.02310)、DistServe/goodput(2401.09670)、Mooncake(2407.00079)、VTC 公平调度(2401.00588)、Autellix agent 调度(2502.13965)。

VLM / 实时控制: vLLM EPD 博文(blog.vllm.ai/2025/12/15/vllm-epd.html)、Real-Time Chunking(2506.07339)、OpenVLA(2406.09246)、GR00T N1(2503.14734)、UI-TARS(2501.12326)、FastV(2403.06764)、ShowUI(2411.17465)。

Text2SQL: 语义层 +17~23pp(2604.25149)、小模型 CoT 微调(2603.22942)、P2SQL 注入/ICSE2025(2308.01990)、CHASE-SQL(2410.01943)、XiYan-SQL(2411.08599)、BIRD-CRITIC 修 SQL(2506.18951)、XGrammar(2411.15100)、GPTCache 实测(2602.18922)。工程博客:Uber QueryGPTLinkedIn SQL BotDatabricks Text2SQL 微调Snowflake Cortex Analyst

消费级 GPU: Consumer Blackwell 实测(2601.09527)、前作《在 8×RTX4090 上部署 Qwen3-32B》(本站)。