一、什么是流式输出?为什么大模型都在用它
在日常使用 ChatGPT 或智谱 GLM 等大语言模型时,你会发现答案并不是瞬间全部弹出来的,而是像打字机一样一个字一个字地蹦出来。这种机制在技术上被称为流式输出。
如果不用流式输出,采用传统的“一问一答”模式,由于大模型生成几百上千字的内容往往需要十几秒甚至更久,用户在等待期间只能对着一个空白屏幕发呆,体验极其糟糕。而流式输出的核心逻辑是“边生成边返回”:模型每计算出一个 Token(最小语义单元),就立刻通过网络推送给前端。这样极大地降低了用户的首字响应时间(TTFT),有效缓解了等待焦虑。
提示:从底层原理来看,大模型本质上是一个“下一个词预测器”。它本来就是在循环推测下一个 Token,所以流式输出不仅优化了体验,也完美契合了模型底层的生成逻辑。
二、背后的功臣:深入理解 SSE 协议
大模型是怎么做到“边想边说”的?这就不得不提背后的通信协议了。虽然 WebSocket 也能实现双向实时通信,但大模型 API 普遍选择了更轻量的 SSE(Server-Sent Events,服务器发送事件) 协议。
SSE 是基于 HTTP 协议的一种轻量级推送技术。与普通的 HTTP 请求一次性断开不同,在使用 SSE 时,服务器会在响应头中设置 Content-Type: text/event-stream。这意味着这条 HTTP 连接建立后不会被立刻关闭,服务器可以持续地通过这条长连接向客户端发送数据。
在数据格式上,SSE 有着严格的纯文本规范。每个推送的消息一般由若干个字段组成,最常见的是 data:。服务器发送的每条消息必须以两个换行符 \n\n 结尾,这样浏览器或客户端解析器就知道一个完整的文本块已经结束了。
三、GLM 模型的流式返回机制解析
智谱 GLM 系列模型(如 GLM-3-Turbo 或 GLM-4)的 API 设计非常贴近开发者习惯。当你把请求参数中的 stream 设置为 true 时,GLM 服务器就会开启流式响应模式。
此时,接口返回的不再是单个完整的 JSON 对象,而是一连串的 Chunk(数据块)。每一个 Chunk 都包含着模型刚刚生成的增量内容。你会观察到这类特殊的响应头信息,标志着数据流的开启:
Content-Type: text/event-streamCache-Control: no-cacheConnection: keep-alive
在 GLM 的流式数据流中,除了包含每次吐出的文本片段,还会在最后几个 Chunk 里带上本次对话消耗的 Token 统计信息。当模型完全生成完毕后,服务器会发送一个特殊的结束标志(通常是 data: [DONE]),随后关闭这次 HTTP 连接。
四、实战演练:用 Python 处理 GLM 流数据
理论说完了,我们来看看在实际代码中怎么消费 GLM 的流式接口。由于大模型厂商基本都兼容 OpenAI 的 SDK 格式,智谱也提供了非常便捷的调用方式。核心在于使用循环去不断读取数据块,并捕获结束信号。
下面是一段使用 Python 和 SSE 机制解析大模型流式输出的通用伪代码/示例:
import requests
import json
# GLM 流式请求示例 (伪接口地址,请替换为实际的智谱 API 地址)
url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
headers = {
"Authorization": "Bearer YOUR_ZHIPU_API_KEY",
"Content-Type": "application/json"
}
payload = {
"model": "glm-4",
"messages": [{"role": "user", "content": "请用100字解释什么是量子纠缠"}],
"stream": True # 关键:开启流式输出
}
# 发送流式请求
response = requests.post(url, headers=headers, json=payload, stream=True)
print("GLM 正在回复:", end="", flush=True)
# 逐行读取 SSE 数据流
for line in response.iter_lines():
if line:
# GLM 返回的格式通常是 b'data: {...}'
decoded_line = line.decode('utf-8')
# 确保是 SSE 的 data 字段
if decoded_line.startswith('data:'):
data_str = decoded_line.split('data:', 1)[1].strip()
# 捕获结束信号
if data_str == "[DONE]":
print("\n[流式输出结束]")
break
# 解析 JSON 并提取增量文本
chunk = json.loads(data_str)
delta_content = chunk['choices'][0]['delta'].get('content', '')
# 实时打印到控制台,并强制刷新缓冲区
print(delta_content, end="", flush=True)
这段代码的核心逻辑有三步:首先在请求体中设置 stream=True;其次利用 response.iter_lines() 按行迭代读取 TCP 连接中的数据流;最后过滤出 data: 字段并解析 JSON,遇到 [DONE] 则主动跳出循环。
五、避坑指南:流式开发的细节与心得
在把玩 GLM 流式输出时,有一些细节如果没处理好,很容易导致程序崩溃或体验下降。结合我个人的踩坑经验,总结了以下几点注意事项:
提示:在处理增量数据时,大模型流式返回的字段往往和全量返回不同,一定要仔细阅读官方文档中的字段差异!
- 提取字段不要搞混:在非流式的完整响应中,文本通常在
message.content里;而在流式响应中,增量文本往往在delta.content里。如果用解析完整响应的逻辑去硬套流式 Chunk,肯定会报KeyError。 - 处理好空字符串:在模型刚接收到请求还没开始吐字,或者在处理某些特殊函数调用(Tool Calling)时,有些 Chunk 的
content可能是空字符串""或者不存在。在提取前一定要使用.get('content', '')进行容错处理。 - 前端渲染的缓冲控制:如果你是在做 Web 前端开发(比如基于 Vue 或 React),在把后端透传过来的 SSE 片段拼接到 DOM 时,建议使用类似 Markdown 增量解析的库。因为流式返回的半个表格或半段代码块如果直接渲染,会导致页面闪烁抖动。
大模型的流式输出虽然让前后端的处理逻辑稍微复杂了一些,但它带来的是交互体验质的飞跃。真正理解 SSE 协议和流式数据的拼接处理,是每个 AI 应用开发者走向进阶的必经之路。