一、为何需要“流式输出”?

在传统的 HTTP 请求/响应模型中,客户端发送一个请求,服务器则完整地处理后,一次性返回所有数据。这对于返回一个页面或一份 JSON 数据来说没问题,但当应用场景变为大语言模型(LLM)生成文本实时数据更新时,这种模式就显得笨拙了。用户需要等待模型生成完整回答,期间可能面临长达数秒甚至数十秒的空白,体验很差。

流式输出(Streaming) 的核心思想就是:边生成,边发送。服务器不再等待所有内容就绪,而是一旦生成了一部分数据(比如模型生成了一个词或一个句子),就立即将其推送给客户端。客户端可以实时接收并渲染这些“碎片”,用户几乎瞬间就能看到内容开始出现,极大地改善了交互的流畅度和感知速度。

提示: 流式输出不仅是优化用户体验的利器,它还能降低服务器的内存峰值压力,因为服务器无需在内存中缓存完整的巨大响应体,而是处理完一小块就发出一小块。

二、SSE:专为流式而生的协议

要在 Web 上实现流式输出,我们需要一个机制让服务器能够持续、单向地向客户端推送数据。服务器发送事件(Server-Sent Events, SSE) 就是这样一个专为此设计的 W3C 标准。它基于 HTTP 协议,但与传统的轮询(Polling)有本质区别。

SSE 的优势非常明显:

三、如何将 LLM 的流式输出通过 SSE 传递?

当我们调用如 MiMo 这类大模型的 API 并希望获得流式响应时,后端服务所扮演的角色就是一个“中转站”和“格式转换器”。整个流程是:客户端 -> 你的后端服务 -> 模型 API -> 你的后端服务 -> 客户端

后端服务的关键职责是:

  1. 以流式方式调用模型 API(例如,请求参数中设置 stream=True)。
  2. 实时接收模型返回的数据块(这些块通常是独立的 JSON 对象,包含生成的新 token)。
  3. 将模型的数据块转换为 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 简单高效,但在生产环境中集成时,你需要关注以下几点:

最终建议: 对于 AI 对话类应用,SSE + 流式输出是目前的主流最佳实践。它在实现复杂度、浏览器兼容性和开发体验上取得了很好的平衡。理解其背后的协议细节,能让你在调试连接问题时游刃有余。