Skip to content

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 编码

python
# 用 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 / 请求
    → 这限制了并发请求数量

推理框架对比

自己部署模型时,推理框架的选择直接影响流式生成速度:

框架特点流式吞吐量适合场景
vLLMPagedAttention + 连续批处理⭐⭐⭐⭐⭐生产部署首选
TGI (HuggingFace)优化的推理服务器⭐⭐⭐⭐HuggingFace 生态用户
Ollama一键本地运行⭐⭐⭐个人开发、快速测试
llama.cppCPU/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 DecodePrefill 并行处理输入、Decode 串行生成输出
TTFT 来源主要来自 Prefill 阶段(与输入长度正相关)
BPE 分词英文 ~4 字符/Token,中文 ~1.5 字符/Token
中文影响同等 tokens/s 下,中文"看起来"比英文慢
KV Cache避免重复计算,Decode 阶段每步耗时恒定
vLLMPagedAttention + 连续批处理,生产部署首选
云 vs 本地云 API 能力强但有网络延迟,本地 TTFT 极低

下一章预告:流式传输协议 —— SSE、WebSocket、gRPC 三种方案的原理和代码实现,以及 AI 应用场景下的选型指南。

坚持是一种品格