在实际开发中,当我们调用像 MiMo 这样的大模型 API 时,经常会遇到等待时间过长的问题。模型需要“思考”并生成完整的回复后,才能将一大段文本返回给前端,用户只能盯着加载动画干着急。流式输出(Streaming) 正是解决这一痛点的关键技术。它能让模型“边想边说”,将生成的文本片段像水龙头流水一样实时推送给客户端,极大地改善了用户体验。
实现流式输出有多种方式,而 SSE(Server-Sent Events) 是其中一种非常经典且高效的协议。它的核心思想是:客户端通过一个普通的 HTTP 请求连接到服务器,服务器保持这个连接不中断,并持续地、单向地向客户端推送数据。与 WebSocket 这种全双工协议相比,SSE 更加轻量,且天然支持自动重连和事件ID等特性,非常适合像模型文本生成这种“服务端主导”的单向数据推送场景。
一、什么是流式输出与 SSE
流式输出,通俗来讲,就是“逐字逐句”地生成和返回数据。对比来看,传统的非流式响应如同“等写完一整封信再寄出”,而流式响应则是“每写好一句话,就通过电话告诉你一句”。这不仅让用户更快地看到初始反馈,也便于在客户端逐步渲染较长的生成内容。
SSE 协议则定义了这种通信的“规矩”。它基于 HTTP,服务器返回的 Content-Type 头为 text/event-stream。发送的数据有特定的文本格式,每一行数据以 data: 字段开始,并以两个换行符(\n\n)作为一个完整消息的结束。浏览器端可以通过原生的 EventSource API 或任意 HTTP 客户端库来解析和处理这些事件。
二、为什么流式输出对大模型至关重要
使用流式输出主要有三个核心优势。首先,提升用户感知速度。第一个 token 生成后就能立即返回,用户体验上的延迟感会大幅降低,这被称为“首字节时间(TTFB)”优化。其次,降低客户端内存压力。对于生成超长文本的任务(如写文章、代码),一次性加载到内存可能造成卡顿,而流式处理可以边接收边消费。最后,支持中断和部分结果利用。用户可以在模型生成过程中随时停止,已接收的内容仍然有效。
从技术实现角度看,流式输出也解耦了模型的长时间推理与网络传输。服务器可以维护一个生成队列或使用异步生成器,将推理步骤中产出的 token 实时转换为 SSE 事件流发回。这使得系统资源利用更高效。
三、SSE 协议的核心格式解析
SSE 的报文格式非常简单易读。一个典型的 SSE 事件流看起来像这样:
data: {"token": "你好", "index": 0}
data: {"token": ",", "index": 1}
data: {"token": "欢迎", "index": 2}
data: [DONE]
每一行 data: 后面跟随的就是具体的数据,通常是一个 JSON 字符串。JSON 的结构由你的 API 设计定义,可能包含生成的文本片段、元信息(如是否结束)等。最后一个 data: [DONE] 是一个约定的结束标志,告诉客户端本次流式传输已完成。
提示:SSE 是单向通信,数据只能从服务器发送到客户端。如果客户端需要发送消息(如新的提问),需要建立另一个普通的 HTTP 请求。这恰恰符合大模型“一问一答”的交互模式。
四、Python 实现:服务端生成 SSE 流
下面是一个使用 Flask 框架模拟 MiMo 流式输出的示例。我们假设 model_stream_generate 是一个返回生成器(generator)的函数,它会逐步产出 token。
from flask import Flask, Response
import json
import time
app = Flask(__name__)
def model_stream_generate(prompt):
# 这里模拟一个模型生成过程
response_tokens = ["你", "好", "!", " ", "这", "是", "一", "个", "流", "式", "输", "出", "的", "例", "子", "。"]
for i, token in enumerate(response_tokens):
# 模拟模型推理耗时
time.sleep(0.1)
yield json.dumps({"token": token, "index": i})
@app.route('/chat', methods=['POST'])
def chat_stream():
prompt = "请介绍一下你自己" # 实际中应从请求中获取
def generate_events():
# 生成 token 流
for token_json in model_stream_generate(prompt):
# 按照 SSE 格式发送:data: ...\n\n
yield f"data: {token_json}\n\n"
# 发送结束标志
yield "data: [DONE]\n\n"
# 返回 Response 对象,设置正确的 MIME 类型
return Response(generate_events(), mimetype='text/event-stream')
if __name__ == '__main__':
app.run(debug=True)
五、客户端如何处理 SSE 事件流
在客户端(如 JavaScript),我们可以使用原生的 EventSource API 来消费这个流。当收到数据时,解析 JSON 并更新页面内容。
// 前端 JavaScript 示例
const eventSource = new EventSource('/chat');
const outputElement = document.getElementById('output');
eventSource.onmessage = function(event) {
if (event.data === '[DONE]') {
eventSource.close();
console.log('生成完毕');
return;
}
// 解析服务器发来的 JSON 数据
const data = JSON.parse(event.data);
// 将 token 追加到页面显示区域
outputElement.textContent += data.token;
};
eventSource.onerror = function(error) {
console.error("EventSource failed:", error);
eventSource.close();
};
六、最佳实践与注意事项
在实际应用流式输出和 SSE 时,有几个关键点需要注意。第一,务必设置超时和心跳机制。长时间没有数据的连接可能会被中间网络设备(如代理、网关)断开。服务器应定期发送空注释(以 : 开头的行)作为心跳,例如 : heartbeat\n\n。第二,考虑客户端兼容性。虽然现代浏览器普遍支持 EventSource,但在某些环境(如微信小程序、旧版浏览器)中可能需要使用 fetch API 来手动解析流。
最后,合理设计数据格式。JSON 是传递结构化 token 信息(如是否包含引用、是否安全敏感)的理想选择。同时,要为流式传输的失败和重试做好准备,提供清晰的状态指示和错误处理逻辑,从而构建出健壮、用户友好的应用。