Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 人工智能 / Artificial Intelligence

大语言模型 / Large Language Models

1. LLM 前置知识学习路线 / A Prerequisite Learning Path for Large Language Models

2. 神经网络基础 - 从零理解 AI 的"计算单元" / Neural Network Fundamentals from Artificial Neurons

3. Token 与上下文窗口 - LLM 的计费与记忆单位 / Tokens and Context Windows as the Units of LLM Cost and Memory

4. 解码策略 - 控制 LLM 输出的艺术 / Decoding Strategies for Controlling LLM Output

5. 消息角色 - 构建 Agent 对话的基础 / Message Roles as the Foundation of Agent Conversations

6. 流式输出 - 实时交互的体验优化 / Streaming Output for Responsive Interaction

7. Prompt 工程基础 - 与 LLM 高效对话的技巧 / Prompt Engineering Fundamentals for Effective LLM Interaction

8. LLM 进化史 - 从词向量到 Transformer / The Evolution of LLMs from Word Embeddings to Transformers

9. Transformer 手动计算:从 Attention 到 Encoder-Decoder 完整数据流

10. Transformer 核心原理 - 现代 LLM 的基石 / Transformer Fundamentals Behind Modern LLMs

11. Decoder-Only LLM 深度解析:为什么扔掉 Encoder,以及 KV Cache 如何工作

12. 训练 vs 推理:同一个 Transformer,两条完全不同的执行路径

13. Transformer 训练阶段计算详解 - 手算每一行矩阵 / Transformer Training Computation Matrix by Matrix

14. Transformer 推理阶段详解 — 模型如何"思考"并生成回答 / Transformer Inference and Autoregressive Generation

15. 训练基础扫盲 - 理解 Fine-tune 在做什么 / A Training Primer for Understanding Fine-Tuning

16. LLM 预训练全景:数据管道、Scaling Laws 与训练稳定性

17. 训练基础设施 - 从单卡到千卡集群 / Training Infrastructure from One GPU to Thousand-GPU Clusters

18. Post-Training Pipeline - 从 Base Model 到可用助手 / The Post-Training Pipeline from Base Model to Assistant

19. SFT 深度解析:从 Base Model 到指令跟随——后训练第一步 / SFT Deep Dive: Teaching Base Models to Follow Instructions

20. RLHF 深度解析:从 Reward Model 到 PPO 的完整对齐流程

21. DPO 与对齐方法:从 RLHF 复杂度到直接偏好优化

22. 研究视角:DL/RL 理论到 LLM 训练的完整映射

本页目录

流式输出 - 实时交互的体验优化 / Streaming Output for Responsive Interaction ​

📅 创建时间:2026-04-28 🏷️ 标签:#Streaming #SSE #流式输出 #实时交互 📚 前置知识:[[04 - 消息角色]]


📋 本章目标 ​

  • 理解什么是流式输出(Streaming)
  • 理解 Server-Sent Events (SSE) 的原理
  • 对比流式与非流式响应的差异
  • 了解流式输出的应用场景
  • 理解前端如何处理流式响应

第1部分:什么是流式输出? ​

1.1 基本概念 ​

流式输出 = 数据不是一次性返回,而是"分段"返回

┌─────────────────────────────────────────────────────────────┐
│                    非流式 vs 流式对比                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  非流式(Traditional):                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 用户请求 → [等待 3秒] → [一次性返回全部内容]          │   │
│  └─────────────────────────────────────────────────────┘   │
│                        ↓                                    │
│                     3秒后:                                │
│  "人工智能是计算机科学的一个分支..."(完整内容)              │
│                                                             │
│  ─────────────────────────────────────────────────────────  │
│                                                             │
│  流式(Streaming):                                         │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 用户请求 → [立即开始] → [逐token返回]                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                        ↓                                    │
│  0秒:""                                                   │
│  0.5秒:"人"                                               │
│  1秒:"人工"                                               │
│  1.5秒:"人工智"                                           │
│  2秒:"人工智能"                                           │
│  ...                                                        │
│  3秒:"人工智能是计算机科学的一个分支..."                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

1.2 为什么需要流式输出? ​

用户体验的革命:

┌─────────────────────────────────────────────────────────────┐
│                    用户感知对比                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  非流式:                                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 你:发消息                                             │   │
│  │ AI:[加载中...][加载中...][加载中...]  ← 等了3秒      │   │
│  │ AI:人工智能是...(突然显示完整内容)                   │   │
│  └─────────────────────────────────────────────────────┘   │
│  用户感受:焦虑等待                                          │
│                                                             │
│  流式:                                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 你:发消息                                             │   │
│  │ AI:人... ← 立刻开始显示                               │   │
│  │ AI:人工...                                           │   │
│  │ AI:人工智... ← 持续更新,用户感觉在"打字"              │   │
│  │ AI:人工智能是...                                      │   │
│  └─────────────────────────────────────────────────────┘   │
│  用户感受:流畅、响应快                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

1.3 感知响应时间 ​

关键数据:

响应方式感知延迟用户体验
非流式2-5秒焦虑等待
流式<100ms即时响应

第2部分:Server-Sent Events (SSE) 原理 ​

2.1 什么是 SSE? ​

Server-Sent Events (SSE) = 服务器向浏览器推送数据的技术

┌─────────────────────────────────────────────────────────────┐
│                      SSE 工作原理                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  浏览器                              服务器                  │
│    │                                   │                    │
│    │ ──────── 建立连接 ─────────────→ │                    │
│    │                                   │                    │
│    │ ←──────── 数据流 ◄────────────── │                    │
│    │    event: chunk                   │                    │
│    │    data: "人"                    │                    │
│    │ ←──────── 数据流 ◄────────────── │                    │
│    │    event: chunk                   │                    │
│    │    data: "工"                    │                    │
│    │ ←──────── 数据流 ◄────────────── │                    │
│    │    event: chunk                   │                    │
│    │    data: "智"                    │                    │
│    │                                   │                    │
│    │ ←───────── 完成 ◄────────────── │                    │
│    │    [DONE]                        │                    │
│    │                                   │                    │
│    │ ──────── 关闭连接 ─────────────→ │                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

2.2 SSE 数据格式 ​

标准 SSE 格式:

event: chunk
data: "今天"

event: chunk
data: "天气"

event: chunk
data: "很好"

event: done
data: "[DONE]"
1
2
3
4
5
6
7
8
9
10
11

2.3 SSE vs WebSocket ​

特性SSEWebSocket
方向单向(服务器→客户端)双向
复杂度简单复杂
兼容性几乎所有浏览器现代浏览器
重连自动重连需要手动处理
使用场景实时推送双向通信
LLM 输出完美支持也支持

为什么 LLM 适合用 SSE:

LLM 的场景是"服务器推送到客户端"
客户端(浏览器)只需要接收,不需要发送
SSE 足够用,而且更简单
1
2
3

第3部分:流式响应的数据格式 ​

3.1 OpenAI 的流式响应格式 ​

API 调用示例:

python
from openai import OpenAI
client = OpenAI()

stream = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[{"role": "user", "content": "什么是人工智能?"}],
    stream=True  # 开启流式输出
)
1
2
3
4
5
6
7
8

返回的数据格式:

python
# 每个 chunk 是一个 Server-Sent Event
for chunk in stream:
    print(chunk)
1
2
3

典型 chunk 结构:

python
# Chunk 1
ChatCompletionChunk(
    id="chatcmpl-xxx",
    choices=[
        Choice(
            delta=ChoiceDelta(content="人"),  # 第一个 token
            index=0,
            finish_reason=None
        )
    ]
)

# Chunk 2
ChatCompletionChunk(
    id="chatcmpl-xxx",
    choices=[
        Choice(
            delta=ChoiceDelta(content="工"),  # 第二个 token
            index=0,
            finish_reason=None
        )
    ]
)

# 最后一个 Chunk
ChatCompletionChunk(
    id="chatcmpl-xxx",
    choices=[
        Choice(
            delta=ChoiceDelta(content=""),  # 空内容
            index=0,
            finish_reason="stop"  # 表示生成结束
        )
    ]
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

3.2 数据流解析过程 ​

┌─────────────────────────────────────────────────────────────┐
│                    数据流解析过程                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  服务器返回的原始数据:                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ event: chunk                                        │   │
│  │ data: {"id":"...","choices":[{"delta":{"content":"人"}}]}│   │
│  │                                                      │   │
│  │ event: chunk                                        │   │
│  │ data: {"id":"...","choices":[{"delta":{"content":"工"}}]}│   │
│  │                                                      │   │
│  │ event: chunk                                        │   │
│  │ data: {"id":"...","choices":[{"delta":{"content":"智"}}]}│   │
│  └─────────────────────────────────────────────────────┘   │
│                          ↓                                   │
│  OpenAI SDK 解析后:                                         │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ chunk.choices[0].delta.content = "人"               │   │
│  │ chunk.choices[0].delta.content = "工"               │   │
│  │ chunk.choices[0].delta.content = "智"               │   │
│  └─────────────────────────────────────────────────────┘   │
│                          ↓                                   │
│  拼接成完整内容:                                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ full_content = "人" + "工" + "智" + ...            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

3.3 完整代码示例 ​

Python 后端:

python
from openai import OpenAI
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import json

app = FastAPI()
client = OpenAI()

@app.get("/stream-chat")
async def stream_chat(message: str):
    def generate():
        stream = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": message}],
            stream=True
        )

        for chunk in stream:
            # 提取 delta.content
            content = chunk.choices[0].delta.content
            if content:
                # 发送 SSE 格式
                yield f"data: {json.dumps({'token': content})}\n\n"

        # 发送完成信号
        yield f"data: {json.dumps({'done': True})}\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

前端 JavaScript:

javascript
async function streamChat(message) {
    const response = await fetch(`/stream-chat?message=${encodeURIComponent(message)}`);

    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    let fullText = '';

    while (true) {
        const { done, value } = await reader.read();

        if (done) break;

        // 解码数据
        const chunk = decoder.decode(value);

        // 解析 SSE 格式
        const lines = chunk.split('\n');
        for (const line of lines) {
            if (line.startsWith('data: ')) {
                const data = JSON.parse(line.slice(6));

                if (data.token) {
                    fullText += data.token;
                    // 更新 UI
                    document.getElementById('output').textContent = fullText;
                }

                if (data.done) {
                    console.log('生成完成');
                }
            }
        }
    }

    return fullText;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

第4部分:流式 vs 非流式时序对比 ​

4.1 时间线对比 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│                          时序对比图                                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  假设总响应时间:5秒,响应长度:100 tokens                                   │
│                                                                             │
│  时间    0s    1s    2s    3s    4s    5s                                │
│          │     │     │     │     │     │                                 │
│  ─────────────────────────────────────────────────────────────────────     │
│                                                                             │
│  非流式:  │███████████████████████│                                        │
│            [用户等待...]         [完整显示]                                  │
│                                                                             │
│  流式:    │█│█│█│█│█│█│█│█│█│█│                                        │
│            █ █ █ █ █ █ █ █ █ █  [逐字显示]                               │
│                                                                             │
│  ─────────────────────────────────────────────────────────────────────     │
│                                                                             │
│  用户感知:                                                                  │
│  非流式:   [..........][完整结果]  → 等了5秒才看到任何东西                    │
│  流式:     [█][█][█][█][█][█]...  → 立刻开始看到结果                        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

4.2 性能指标对比 ​

指标非流式流式
首次响应时间 (TTFB)等待完整生成<100ms
感知延迟5-10秒<1秒
用户流失率较高较低
网络传输量相同相同
服务器负载相同略高

4.3 流式输出的优势 ​

┌─────────────────────────────────────────────────────────────┐
│                    流式输出的四大优势                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 即时反馈                                               │
│     用户立刻看到"AI在响应",减少等待焦虑                        │
│                                                             │
│  2. 更自然的交互                                           │
│     模拟人类打字聊天的体验                                     │
│                                                             │
│  3. 早期终止                                               │
│     如果用户看到结果满意,可以提前终止请求                       │
│                                                             │
│  4. 长输出友好                                             │
│     对于长文本,不必等待完整生成才能开始阅读                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

第5部分:流式输出的应用场景 ​

5.1 ChatGPT 风格对话 ​

典型场景:

用户输入 → 流式显示 AI 回复 → 逐字出现,模拟打字
1

用户感知:

AI正在输入中...                    ← 马上显示
AI正在输入中...人工智能是...        ← 持续更新
AI正在输入中...人工智能是计算机科学...
AI正在输入中...人工智能是计算机科学的一个分支...
1
2
3
4

5.2 代码生成 ​

典型场景:

用户:写一个Python快排函数

AI:def quicksort(arr):
     │ 立刻显示函数定义
AI:def quicksort(arr):
    if len(arr) <= 1:
        │ 继续显示...
AI:def quicksort(arr):
    if len(arr) <= 1:
        return arr
    ...
1
2
3
4
5
6
7
8
9
10
11

优势:用户可以提前终止不符合预期的输出

5.3 长文档生成 ​

典型场景:

用户:写一篇关于AI的文章(5000字)

AI:[开始生成...]
    文章标题:人工智能的过去、现在与未来
     │
     ├── 第一章:人工智能的起源
     │   人工智能的概念最早可以追溯到...
     │   1950年,图灵提出了著名的...
     │
     ├── 第二章:...
     │
     └── ...

用户可以在生成过程中就开始阅读,而不是等待5分钟
1
2
3
4
5
6
7
8
9
10
11
12
13
14

5.4 实时翻译 ​

典型场景:

用户输入:I love artificial intelligence

AI流式输出:我...我爱...我热爱...我热爱人工...我热爱人工智能

用户立刻获得大致翻译方向,最终得到准确翻译
1
2
3
4
5

第6部分:流式输出的实现注意事项 ​

6.1 常见问题与解决方案 ​

问题1:SSE 被代理缓存

解决方案:
- 确保响应头设置正确
- Cache-Control: no-cache
- Content-Type: text/event-stream
1
2
3
4

问题2:中文乱码

原因:UTF-8 编码被截断
解决方案:
- 使用 TextDecoder 正确解码
- 确保服务器正确设置编码
1
2
3
4

问题3:连接超时

解决方案:
- 实现心跳机制
- 自动重连逻辑
1
2
3

6.2 完整的响应头设置 ​

Python (FastAPI):

python
from fastapi.responses import StreamingResponse

response = StreamingResponse(
    generate(),
    media_type="text/event-stream",
    headers={
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "X-Accel-Buffering": "no",  # Nginx 禁用缓冲
    }
)
1
2
3
4
5
6
7
8
9
10
11

Node.js (Express):

javascript
res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'X-Accel-Buffering': 'no'
});
1
2
3
4
5
6

6.3 前端兼容性处理 ​

javascript
// 检测浏览器支持
if (typeof ReadableStream !== 'undefined') {
    // 现代浏览器,支持流式处理
    processStream(response);
} else {
    // 降级为非流式
    response.text().then(displayResult);
}
1
2
3
4
5
6
7
8

核心总结 ​

总结1:流式输出的本质 ​

流式 = 不是一次性返回完整数据
      而是通过持久连接,分段发送数据块

LLM 生成是顺序的(逐 token)
→ 天生适合流式输出
→ 每个 token 生成后立即发送
1
2
3
4
5
6

总结2:SSE 是 LLM 流式输出的标准 ​

SSE = Server-Sent Events
• 单向通信(服务器→客户端)
• 基于 HTTP 协议
• 简单易用
• 自动重连
1
2
3
4
5

总结3:用户感知优化 ​

方式用户感知延迟体验
非流式3-10秒等待焦虑
流式<100ms即时响应

总结4:实现要点 ​

1. 服务器:使用 StreamingResponse,正确的响应头
2. 数据格式:SSE event: chunk / data: token
3. 客户端:TextDecoder 正确解码,逐块更新 UI
4. 错误处理:心跳、自动重连
1
2
3
4

章节测试 ​

测试1:SSE 方向 ​

SSE(Server-Sent Events)是单向通信还是双向通信?

测试2:感知延迟 ​

非流式响应和流式响应,哪个能让用户更快感知到 AI 正在工作?

测试3:数据单位 ​

LLM 流式输出时,每个数据块通常包含什么?

测试4:应用场景 ​

对于一个 5000 字的长文生成,哪个方案用户体验更好:流式还是非流式?为什么?

测试5:响应头设置 ​

如果 SSE 响应被代理服务器缓存,应该在响应头中设置什么?


参考答案 ​

测试1答案 ​

答案:单向通信

解析:

SSE 的全称是 Server-Sent Events
意思是"服务器发送事件"
只有服务器可以向客户端推送数据
客户端不能通过 SSE 向服务器发送数据
1
2
3
4

测试2答案 ​

答案:流式

解析:

流式响应:
• 第一个 token 生成后立即返回(<100ms)
• 用户立刻看到 AI 开始输出

非流式响应:
• 需要等待所有 token 生成完毕
• 可能等待 3-10 秒才看到任何内容
1
2
3
4
5
6
7

测试3答案 ​

答案:通常包含一个或多个新生成的 token

解析:

OpenAI 流式响应中,每个 chunk 的结构:
{
    "choices": [{
        "delta": {
            "content": "新token"  // 新生成的 token
        }
    }]
}

例如:第一个 chunk 可能是 {"content": "人"}
第二个 chunk 可能是 {"content": "工"}
客户端需要把这些内容拼接起来
1
2
3
4
5
6
7
8
9
10
11
12

测试4答案 ​

答案:流式更好

解析:

1. 感知时间:
   • 流式:<100ms 就能看到开头
   • 非流式:可能需要等待 30 秒到 1 分钟

2. 用户体验:
   • 流式:用户可以边看边读
   • 非流式:必须等待完整生成

3. 早期终止:
   • 流式:用户看到满意内容可以停止
   • 非流式:必须等全部完成
1
2
3
4
5
6
7
8
9
10
11

测试5答案 ​

答案:Cache-Control: no-cache

解析:

问题原因:
代理服务器(如 Nginx)可能会缓存响应
导致客户端无法实时接收数据

解决方案:
在响应头中设置:
Cache-Control: no-cache      ← 禁用缓存
X-Accel-Buffering: no        ← Nginx 禁用缓冲
1
2
3
4
5
6
7
8

相关笔记 ​

  • [[04 - 消息角色]] - 流式输出也是 messages 结构
  • [[03 - 解码策略]] - token 生成过程
  • [[06 - Prompt 工程]] - 通过 Prompt 控制输出长度

下一步学习 ​

  • [ ] 阅读 06 - Prompt 工程基础

学习状态:✅ 已完成

最后更新于:

Pager
上一篇5. 消息角色 - 构建 Agent 对话的基础 / Message Roles as the Foundation of Agent Conversations
下一篇7. Prompt 工程基础 - 与 LLM 高效对话的技巧 / Prompt Engineering Fundamentals for Effective LLM Interaction

持续记录,持续成长

Copyright © Tidenflow