---
title: "AI 服务的并发瓶颈与架构设计:从第一性原理到两个真实场景(游戏 AI 并发 / 运营数据问答)"
description: "拆解 LLM/VLM 在线服务高并发的本质瓶颈,并落到 8×RTX5090 无 NVLink 集群上的两套真实架构:游戏 GUI Agent 实时并发,与多租户 Text2SQL 运营问答。"
pubDate: 2026-07-29
tags: ["推理优化","高并发","多租户","VLM","Text2SQL","RTX5090","KV Cache","PD分离","架构设计"]
category: "AI Infra"
lang: "zh"
math: false
---
# 引子:两个真实系统,一套底层物理

这篇文章不谈"如何调 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)。

## 1.3 消费级 GPU 无 NVLink:一个反直觉的架构结论

大多数人的直觉是: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 | 1× |
| 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](https://arxiv.org/abs/2309.06180))、SGLang/RadixAttention([2312.07104](https://arxiv.org/abs/2312.07104))、Sarathi-Serve/chunked prefill([2403.02310](https://arxiv.org/abs/2403.02310))、DistServe/goodput([2401.09670](https://arxiv.org/abs/2401.09670))、Mooncake([2407.00079](https://arxiv.org/abs/2407.00079))、VTC 公平调度([2401.00588](https://arxiv.org/abs/2401.00588))、Autellix agent 调度([2502.13965](https://arxiv.org/abs/2502.13965))。

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

**Text2SQL:** 语义层 +17~23pp([2604.25149](https://arxiv.org/abs/2604.25149))、小模型 CoT 微调([2603.22942](https://arxiv.org/abs/2603.22942))、P2SQL 注入/ICSE2025([2308.01990](https://arxiv.org/abs/2308.01990))、CHASE-SQL([2410.01943](https://arxiv.org/abs/2410.01943))、XiYan-SQL([2411.08599](https://arxiv.org/abs/2411.08599))、BIRD-CRITIC 修 SQL([2506.18951](https://arxiv.org/abs/2506.18951))、XGrammar([2411.15100](https://arxiv.org/abs/2411.15100))、GPTCache 实测([2602.18922](https://arxiv.org/abs/2602.18922))。工程博客:[Uber QueryGPT](https://www.uber.com/en-US/blog/query-gpt/)、[LinkedIn SQL Bot](https://www.linkedin.com/blog/engineering/ai/practical-text-to-sql-for-data-analytics)、[Databricks Text2SQL 微调](https://www.databricks.com/blog/improving-text2sql-performance-ease-databricks)、[Snowflake Cortex Analyst](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-analyst)。

**消费级 GPU:** Consumer Blackwell 实测([2601.09527](https://arxiv.org/abs/2601.09527))、前作《在 8×RTX4090 上部署 Qwen3-32B》(本站)。
