一、为何需要“流式输出”?
在传统的 HTTP 请求/响应模型中,客户端发送一个请求,服务器则完整地处理后,一次性返回所有数据。这对于返回一个页面或一份 JSON 数据来说没问题,但当应用场景变为大语言模型(LLM)生成文本或实时数据更新时,这种模式就显得笨拙了。用户需要等待模型生成完整回答,期间可能面临长达数秒甚至数十秒的空白,体验很差。
流式输出(Streaming) 的核心思想就是:边生成,边发送。服务器不再等待所有内容就绪,而是一旦生成了一部分数据(比如模型生成了一个词或一个句子),就立即将其推送给客户端。客户端可以实时接收并渲染这些“碎片”,用户几乎瞬间就能看到内容开始出现,极大地改善了交互的流畅度和感知速度。
提示: 流式输出不仅是优化用户体验的利器,它还能降低服务器的内存峰值压力,因为服务器无需在内存中缓存完整的巨大响应体,而是处理完一小块就发出一小块。
二、SSE:专为流式而生的协议
要在 Web 上实现流式输出,我们需要一个机制让服务器能够持续、单向地向客户端推送数据。服务器发送事件(Server-Sent Events, SSE) 就是这样一个专为此设计的 W3C 标准。它基于 HTTP 协议,但与传统的轮询(Polling)有本质区别。
SSE 的优势非常明显:
- 简单:它本质上就是一个长连接的 HTTP 响应,
Content-Type被设置为text/event-stream。客户端使用浏览器内置的EventSourceAPI 即可轻松处理。 - 轻量:协议本身非常简单,数据格式是纯文本,每条消息以
data:字段开头,以两个换行符 (\n\n) 结尾。 - 自动重连:
EventSource内置了自动重连机制,当连接意外断开时,它会默认尝试重新建立连接,这省去了客户端自己处理重连的复杂逻辑。 - 原生支持事件类型:可以通过
event:字段定义事件类型,客户端可以对不同类型的消息进行不同的监听处理。
三、如何将 LLM 的流式输出通过 SSE 传递?
当我们调用如 MiMo 这类大模型的 API 并希望获得流式响应时,后端服务所扮演的角色就是一个“中转站”和“格式转换器”。整个流程是:客户端 -> 你的后端服务 -> 模型 API -> 你的后端服务 -> 客户端。
后端服务的关键职责是:
- 以流式方式调用模型 API(例如,请求参数中设置
stream=True)。 - 实时接收模型返回的数据块(这些块通常是独立的 JSON 对象,包含生成的新 token)。
- 将模型的数据块转换为 SSE 协议要求的格式,然后通过一个永不关闭的 HTTP 响应,将这些 SSE 事件流推送回前端客户端。
提示: 你的后端服务不能直接将模型 API 的流式响应“原样透传”给客户端。模型 API 返回的格式是其自定义的(如 OpenAI 的data: {"choices": [...]}),而你需要将其转换为标准的text/event-stream格式,并可能附加一些元信息。
四、实战:使用 Python 和 Flask 实现 SSE 服务端
下面是一个简化的代码示例,展示了如何使用 Python 的 Flask 框架和 requests 库来搭建一个支持流式输出的 SSE 服务。
from flask import Flask, Response, request
import requests
import json
app = Flask(__name__)
@app.route('/chat/stream', methods=['POST'])
def chat_stream():
# 假设前端传来的消息
user_message = request.json.get('message', '')
# 1. 模拟调用一个支持流式输出的模型API(这里用伪代码表示)
# 实际中你需要替换成真实的模型调用逻辑
def generate_model_stream():
# 假设我们模拟模型逐词生成
full_response = "这是一个流式输出的示例,MiMo会逐字生成回复。"
for char in full_response:
yield json.dumps({"token": char}) # 模型API返回的原始数据块
time.sleep(0.1) # 模拟生成延迟
# 2. 定义SSE事件生成器,转换数据格式
def event_stream():
for model_data in generate_model_stream():
# 解析模型返回的JSON
data = json.loads(model_data)
token = data.get('token', '')
# 3. 格式化为SSE协议格式并发送
# “data:”后跟内容,最后必须有两个换行符
yield f"data: {json.dumps({'content': token})}\n\n"
# 4. 返回一个流式响应,MIME类型为text/event-stream
return Response(event_stream(), mimetype='text/event-stream')
if __name__ == '__main__':
app.run(debug=True)
五、客户端的简单处理
在前端,处理 SSE 变得异常简单。浏览器原生支持 EventSource API。你可以像这样订阅我们的 /chat/stream 接口:
const eventSource = new EventSource('/chat/stream');
// 监听默认的消息事件(当服务器发送没有指定event类型的data时触发)
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
// 在页面上追加显示新收到的token
document.getElementById('response').textContent += data.content;
};
// 监听连接错误
eventSource.onerror = function(event) {
console.error("EventSource failed:", event);
// 客户端会自动尝试重连,但你可以在这里做一些额外处理,比如提示用户
};
注意,EventSource 默认使用 GET 方法。如果你的接口是 POST(如上例),客户端需要借助第三方库(如 event-source-polyfill)或自行封装 fetch 来实现,因为 EventSource 本身不支持 POST。
六、实际应用中的考量与陷阱
虽然 SSE 简单高效,但在生产环境中集成时,你需要关注以下几点:
- 连接状态管理:一定要妥善处理连接的
open、error和close事件。在客户端组件销毁(如页面跳转)时,务必调用eventSource.close()来关闭连接,避免资源泄漏。 - 跨域问题(CORS):如果你的前端和后端 API 不在同一个源(域名、协议、端口),后端服务必须在响应头中正确设置
Access-Control-Allow-Origin等 CORS 头,否则浏览器会拦截响应。 - 缓冲与代理:很多反向代理(如 Nginx)或云服务负载均衡器可能会缓冲你的 SSE 响应,直到收集到一定量数据才发送给客户端,这会破坏流式体验。你需要为 SSE 端点配置特殊的代理规则(例如,在 Nginx 中设置
X-Accel-Buffering: no)来禁用缓冲。 - 与 WebSocket 的选择:SSE 是服务器到客户端的单向通道,非常适合“通知”和“数据推送”场景(如 LLM 输出、股票行情、新闻流)。如果你的需求是需要客户端和服务器之间实时双向通信(如在线聊天、协同编辑),那么 WebSocket 通常是更合适的选择。
最终建议: 对于 AI 对话类应用,SSE + 流式输出是目前的主流最佳实践。它在实现复杂度、浏览器兼容性和开发体验上取得了很好的平衡。理解其背后的协议细节,能让你在调试连接问题时游刃有余。