AI 应用的流式输出全链路
从 LLM 的第一个 Token 到用户屏幕上的逐字渲染——拆解流式输出的每一层。
2. LLM 的 Token 生成机制——流式的起点
流式输出之所以可行,是因为 LLM 天然就是一个一个 Token 生成的。不是工程优化的结果,而是模型架构本身就这么工作。
这一章,我们深入 LLM 的"引擎盖",理解流式输出的物理基础。
2.1 自回归解码:一次只生成一个 Token
什么是自回归
所有主流 LLM(GPT、Claude、Llama、Gemini)都基于 Transformer Decoder 架构,使用**自回归解码(Autoregressive Decoding)**生成文本。
"自回归"这个词听起来很学术,但原理极其直觉:
自回归解码 = 每次只预测下一个 Token,然后把这个 Token 加到输入里,再预测下一个
示例:生成"今天天气真好"
第 1 步:
输入:[用户问题]
模型预测 → "今"
输出缓冲:["今"]
第 2 步:
输入:[用户问题] + "今"
模型预测 → "天"
输出缓冲:["今", "天"]
第 3 步:
输入:[用户问题] + "今天"
模型预测 → "天"
输出缓冲:["今", "天", "天"]
第 4 步:
输入:[用户问题] + "今天天"
模型预测 → "气"
输出缓冲:["今", "天", "天", "气"]
... 以此类推,直到模型输出 [EOS](结束标记)每一步都是一次完整推理
这是理解流式输出的关键——每生成一个 Token,模型都做了一次完整的前向传播(Forward Pass):
单次 Token 生成的计算过程:
已有 Token 序列
│
▼
┌──────────────────────┐
│ Embedding Layer │ ← 把 Token 转成向量
├──────────────────────┤
│ Transformer Block 1 │ ← 自注意力 + FFN
│ Transformer Block 2 │
│ ... │
│ Transformer Block N │ ← GPT-4 可能有 120 层
├──────────────────────┤
│ Output Head │ ← 预测下一个 Token 的概率分布
└──────────────────────┘
│
▼
概率分布 → 采样 → 输出一个 Token
(词表中每个 Token 的概率)
GPT-4 词表大小:~100,000 个 Token
→ 每一步输出的是 100,000 维的概率向量
→ 从中选一个 Token(贪心 / Top-K / Top-P 采样)两个阶段:Prefill vs Decode
LLM 生成分为两个阶段:
┌────────────────────────────────────────────────┐
│ Prefill(预填充)阶段 │
│ │
│ 处理所有输入 Token(用户消息 + System Prompt) │
│ → 并行处理,速度快 │
│ → 但输入越长,耗时越大 │
│ → 这个阶段不产生输出 │
│ → 这就是 TTFT 的主要来源 │
└────────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────────┐
│ Decode(解码)阶段 │
│ │
│ 逐个生成输出 Token │
│ → 串行处理,一次一个 │
│ → 每个 Token 生成后可以立即发送给用户 │
│ → 这就是流式输出的数据源 │
│ → 利用 KV Cache 加速(不用重新计算已有 Token) │
└────────────────────────────────────────────────┘
时间分配举例(GPT-4,输入 2000 tokens,输出 500 tokens):
Prefill 阶段:~500ms(处理输入,不产生输出)
Decode 阶段:~5-8s(逐个生成 500 个 Token)
→ TTFT ≈ 500ms
→ 流式输出在 Prefill 结束后立刻开始核心理解:流式输出不需要任何额外的工程"魔法"。LLM 本身就是一个一个 Token 生成的——你只需要在每个 Token 生成后立即推送给客户端,而不是等全部生成完再一次性返回。流式输出是 LLM 的自然模式,非流式才是"人为等待"。
2.2 Token 编码:一个汉字可能是 1-3 个 Token
流式输出是"逐 Token"推送的,但用户看到的是"逐字"。问题来了:Token ≠ 字。
BPE 分词算法
主流 LLM 使用 BPE(Byte Pair Encoding) 将文本拆分成 Token。BPE 的核心思路是:高频子串合并成一个 Token,低频字符拆成多个 Token。
BPE 分词示例(GPT-4 的 cl100k_base 词表):
英文(Token 效率高):
"Hello, world!" → ["Hello", ",", " world", "!"]
4 个 Token 表示 13 个字符
→ 平均 1 Token ≈ 3.25 个字符
中文(Token 效率低):
"你好世界" → ["你好", "世界"] 或 ["你", "好", "世", "界"]
2-4 个 Token 表示 4 个汉字
→ 平均 1 Token ≈ 1-2 个汉字
代码(高度可预测,效率最高):
"def hello():" → ["def", " hello", "():"]
3 个 Token 表示 13 个字符
→ 平均 1 Token ≈ 4.3 个字符这对流式输出意味着什么
流式输出时 Token 和文字的对应关系:
英文流式输出:
Token 1: "The" → 用户看到 "The" ← 一个完整单词
Token 2: " quick" → 用户看到 " quick" ← 一个完整单词
Token 3: " brown" → 用户看到 " brown" ← 流畅!每次都是完整词
中文流式输出:
Token 1: "在" → 用户看到 "在" ← 一个汉字
Token 2: "当今" → 用户看到 "当今" ← 两个汉字一起出现
Token 3: "快速" → 用户看到 "快速" ← 两个汉字一起出现
→ 中文流式可能看起来"一跳一跳"的
Emoji 和特殊字符:
"👨👩👧👦" → 可能被拆成 7+ 个 Token
→ 流式输出时可能看到乱码碎片
→ 前端需要做 UTF-8 解码缓冲用 Python 实测 Token 编码
# 用 tiktoken 库查看 Token 编码(GPT-4 使用的词表)
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4")
# 英文
text_en = "Hello, how are you doing today?"
tokens_en = enc.encode(text_en)
print(f"英文:{len(text_en)} 字符 → {len(tokens_en)} tokens")
# 输出:英文:31 字符 → 8 tokens
# 中文
text_zh = "你好,今天天气怎么样?"
tokens_zh = enc.encode(text_zh)
print(f"中文:{len(text_zh)} 字符 → {len(tokens_zh)} tokens")
# 输出:中文:11 字符 → 9 tokens
# 查看每个 Token 对应的文字
for token in tokens_zh:
print(f" Token {token} → '{enc.decode([token])}'")
# 输出:
# Token 57668 → '你好'
# Token 3922 → ','
# Token 98432 → '今天'
# Token 99519 → '天气'
# Token 11883 → '怎么'
# Token 26582 → '样'
# Token 11571 → '?'Token 效率对比(同样的内容):
语言 示例文本 字符数 Token 数 效率
─────────────────────────────────────────────────────
英文 "Hello world" 11 2 5.5 字符/Token
中文 "你好世界" 4 2-3 1.5 字符/Token
日文 "こんにちは世界" 7 4-5 1.5 字符/Token
代码 "print('hello')" 14 4 3.5 字符/Token
关键结论:
→ 中文消耗的 Token 数比英文多 2-3 倍
→ 同样的流式生成速度(tokens/s),中文显示的字数更少
→ 这会影响中文用户的流式体验(看起来更"慢")实用提示:如果你的应用主要面向中文用户,流式渲染时可以考虑做一个小缓冲——攒几个 Token 再一起显示,避免"一个字一个字蹦"的卡顿感。后续第 6 章前端渲染部分会详细讲。
2.3 生成速度的影响因素
理解了 Token 逐个生成的机制,下一个问题是:什么决定了每秒能生成多少个 Token?
影响因素全景
Token 生成速度的 5 大影响因素:
┌─────────────────────────────────────────────┐
│ │
│ 1. 模型大小 │
│ 参数量越大 → 每步计算量越大 → 速度越慢 │
│ 7B: ~100 tok/s │
│ 70B: ~30-50 tok/s │
│ 175B+: ~15-30 tok/s │
│ │
│ 2. 硬件配置 │
│ GPU 型号和数量直接决定天花板 │
│ 消费级 RTX 4090: ~80 tok/s (7B) │
│ 数据中心 A100: ~100 tok/s (70B) │
│ H100: ~150 tok/s (70B) │
│ │
│ 3. 量化精度 │
│ FP16 → INT8 → INT4:精度换速度 │
│ INT4 量化可提速 2-3x,质量损失很小 │
│ │
│ 4. 推理框架 │
│ 不同框架的优化程度差异巨大 │
│ vLLM > TGI > 原生 transformers │
│ │
│ 5. 并发请求数 │
│ 多人同时用 → 共享 GPU → 单人速度下降 │
│ 这就是云 API 高峰期变慢的原因 │
│ │
└─────────────────────────────────────────────┘KV Cache:流式输出的加速关键
Decode 阶段的一个关键优化——KV Cache。没有它,每生成一个 Token 都要重新计算所有之前的 Token,速度会慢到不可用。
没有 KV Cache(朴素实现):
生成第 1 个 Token:计算 [输入] 的注意力 → 100ms
生成第 2 个 Token:计算 [输入 + Token1] 的注意力 → 101ms
生成第 3 个 Token:计算 [输入 + Token1 + Token2] → 102ms
...
生成第 500 个 Token:计算 [输入 + 499 Tokens] → 600ms
→ 越来越慢!总时间 = O(n²)
有 KV Cache(标准实现):
Prefill 阶段:计算所有输入 Token 的 K/V 向量 → 缓存到 GPU 显存
生成第 1 个 Token:只计算新 Token 的注意力 + 查缓存 → 20ms
生成第 2 个 Token:只计算新 Token 的注意力 + 查缓存 → 20ms
...
生成第 500 个 Token:同上 → 20ms
→ 每步耗时恒定!总时间 = O(n)
代价:KV Cache 占用大量 GPU 显存
→ Llama 70B 的 KV Cache 可达 2-4 GB / 请求
→ 这限制了并发请求数量推理框架对比
自己部署模型时,推理框架的选择直接影响流式生成速度:
| 框架 | 特点 | 流式吞吐量 | 适合场景 |
|---|---|---|---|
| vLLM | PagedAttention + 连续批处理 | ⭐⭐⭐⭐⭐ | 生产部署首选 |
| TGI (HuggingFace) | 优化的推理服务器 | ⭐⭐⭐⭐ | HuggingFace 生态用户 |
| Ollama | 一键本地运行 | ⭐⭐⭐ | 个人开发、快速测试 |
| llama.cpp | CPU/Metal 推理 | ⭐⭐⭐ | 无 GPU 环境 |
| transformers | 原生 HuggingFace | ⭐⭐ | 研究实验、不推荐生产 |
vLLM 为什么这么快?
核心优化 1:PagedAttention
→ KV Cache 不再要求连续显存
→ 像操作系统的虚拟内存一样分页管理
→ 显存利用率提升 50-90%
→ 同样的 GPU 能服务更多并发请求
核心优化 2:Continuous Batching(连续批处理)
→ 传统方案:一批请求必须等最慢的那个完成
→ vLLM:某个请求完成后立刻换入新请求
→ 吞吐量提升 2-5x
对流式输出的影响:
→ 每个请求的 Token 生成速度基本不变
→ 但同时能服务的请求数大幅增加
→ 用户感知到的流畅度在高并发下也能保持云 API vs 本地部署的速度体验
两种模式的流式体验差异:
云 API(OpenAI / Anthropic):
✅ 模型大、能力强
✅ 不需要 GPU
❌ TTFT 受网络延迟影响(+50-200ms)
❌ 高峰期排队,速度波动大
❌ Token 按量计费
本地部署(Ollama / vLLM):
✅ TTFT 极低(无网络延迟)
✅ 速度稳定,不受他人影响
✅ 无 Token 费用
❌ 需要 GPU 硬件
❌ 模型能力有限(受硬件约束)
❌ 维护成本高
混合方案(推荐):
→ 个人开发/测试:Ollama + 小模型(即时反馈)
→ 生产环境:云 API + 流式中转(能力优先)
→ 降级方案:云 API 超时时切换到本地小模型本章关键结论:LLM 天生就是"逐 Token 生成"的——这是自回归架构决定的。流式输出不是额外的功能,而是直接暴露了模型的原始工作方式。剩下的工作,就是把每个生成的 Token 尽快送到用户屏幕上。
本章小结
| 知识点 | 要点 |
|---|---|
| 自回归解码 | 每次只生成 1 个 Token,再拼到输入中继续 |
| Prefill vs Decode | Prefill 并行处理输入、Decode 串行生成输出 |
| TTFT 来源 | 主要来自 Prefill 阶段(与输入长度正相关) |
| BPE 分词 | 英文 ~4 字符/Token,中文 ~1.5 字符/Token |
| 中文影响 | 同等 tokens/s 下,中文"看起来"比英文慢 |
| KV Cache | 避免重复计算,Decode 阶段每步耗时恒定 |
| vLLM | PagedAttention + 连续批处理,生产部署首选 |
| 云 vs 本地 | 云 API 能力强但有网络延迟,本地 TTFT 极低 |
下一章预告:流式传输协议 —— SSE、WebSocket、gRPC 三种方案的原理和代码实现,以及 AI 应用场景下的选型指南。