在实际开发中,当我们调用像 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 信息(如是否包含引用、是否安全敏感)的理想选择。同时,要为流式传输的失败和重试做好准备,提供清晰的状态指示和错误处理逻辑,从而构建出健壮、用户友好的应用。