一、为什么我们需要“流式输出”?

在与大模型(如 MiMo)交互时,如果等待它“一口气”生成完整回复,用户会面临明显的延迟感。想象一下,你提出一个复杂问题后,屏幕空白了十几秒才瞬间弹出一大段文字,这种体验非常糟糕。流式输出(Streaming) 的核心价值正在于此:它允许模型边生成边将 token 流式地传送到客户端,用户几乎立刻就能看到第一个词,后续内容像打字一样逐字出现,极大地提升了交互的实时感和流畅度。

提示:流式输出的本质是将一个大型的、耗时的计算任务(文本生成)的结果,拆分成多个小的数据块进行异步传输,从而优化用户体验,而不是优化总生成时间。

这种模式不仅改善了体验,也为前端设计提供了更多可能性。例如,可以在收到第一个 token 时就开始播放“思考中”的动画,或在内容较长时实现滚动跟随,让用户感觉模型“一直在努力工作”。

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

Server-Sent Events (SSE) 是一种用于实现服务端向客户端单向、持续推送事件的 Web 标准协议。它是实现大模型流式输出的理想技术选择,因为它比 WebSocket 更轻量、更简单,并且天生支持基于 HTTP 的流式文本传输。SSE 的连接基于普通的 HTTP 请求,服务端设置特定的 MIME 类型 (text/event-stream),然后就可以持续向客户端发送事件。

SSE 的数据格式非常清晰,每一则“事件”由字段构成:

一个典型的 SSE 消息流看起来是这样的:

data: {"token": "你", "id": 0}
data: {"token": "好", "id": 1}
data: {"token": "!", "id": 2}
data: [DONE]

三、实现原理:从前端到后端

实现流式输出需要前后端协同工作。前端负责发起请求并监听 SSE 事件流;后端(集成 MiMo 模型)负责接收请求,调用模型生成流式响应,并将其格式化为 SSE 事件流发送出去。

核心流程如下:

  1. 前端通过 EventSource API 或 fetch + ReadableStream 发起一个 GET 或 POST 请求。
  2. 后端 API 端点收到请求,立即调用 MiMo 模型的流式生成接口(如 .stream())。
  3. 后端将模型生成的每一个(或每一批) token,包装成 SSE data 事件。
  4. 前端 EventSourceonmessage 事件处理器被触发,解析 data 并将新 token 追加到页面上。
  5. 模型生成结束,后端发送一个特殊的 data: [DONE] 事件,前端据此关闭连接。

四、代码示例:一个简单的 FastAPI + SSE 服务端

下面是一个使用 Python 和 FastAPI 实现流式输出服务端的精简示例,展示了如何将模型的流式响应转换为 SSE 流。

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

async def stream_generator(prompt: str):
    """模拟 MiMo 模型流式生成"""
    # 假设这是调用 MiMo SDK 的流式方法
    # from mimo_sdk import MimoClient
    # client = MimoClient(api_key="your_key")
    # response = client.chat.completions.create(..., stream=True)
    # for chunk in response:
    #     token = chunk.choices[0].delta.content
    #     if token:
    #         yield f"data: {json.dumps({'token': token})}\n\n"
    
    # 这里用简单字符串模拟流式输出
    simulated_response = f"你好!你刚才对我说了:“{prompt}”。很高兴为你服务!"
    for char in simulated_response:
        yield f"data: {json.dumps({'token': char})}\n\n"
        await asyncio.sleep(0.05)  # 模拟生成延迟
    yield "data: [DONE]\n\n"  # 流结束标志

@app.post("/chat")
async def chat_stream(request: Request):
    body = await request.json()
    prompt = body.get("prompt", "")
    return StreamingResponse(
        stream_generator(prompt),
        media_type="text/event-stream"
    )

五、前端如何接收与消费流?

前端通常使用浏览器原生的 EventSource API 来建立与服务端的 SSE 连接,并监听消息。以下是一个简单的 JavaScript 示例:

const eventSource = new EventSource('/chat?prompt=你好');
const outputElement = document.getElementById('chat-output');

eventSource.onmessage = function(event) {
    if (event.data === '[DONE]') {
        eventSource.close(); // 流结束,关闭连接
        return;
    }
    // 解析数据,这里假设是简单的字符串
    const token = event.data;
    outputElement.textContent += token; // 追加到页面
};

eventSource.onerror = function(error) {
    console.error("SSE Error:", error);
    eventSource.close();
};
重要提示:上述 EventSource 默认只支持 GET 请求。如果你的接口设计为 POST(传递较长的上下文),你需要使用 fetch API 手动读取 ReadableStream 来实现类似功能,这是现代前端开发中更灵活的做法。

六、关键考量与最佳实践

在生产环境中应用 SSE 流式输出,有几个关键点不容忽视:

七、超越基础:流式输出的进阶思考

流式输出不仅仅是“逐字显示”。它开启了更丰富的交互设计空间:

  1. 流式函数调用:当模型决定调用工具(Function Calling)时,可以先将工具名称和参数以流式方式返回给前端,前端可以提前准备相应的 UI(如显示“正在调用天气查询接口...”),而不是等到整个决策过程结束。
  2. 结构化流data 字段中传输的不只是纯文本,可以是 JSON 对象。例如,将 token、该 token 是否属于代码块、是否需要特别强调等元信息一并流式传输,让前端能实时进行语法高亮或样式渲染。
  3. 混合内容流:理论上,SSE 可以推送不同类型事件。你可以定义 event: codeevent: text 来区分代码段和普通文本段,前端根据事件类型进行差异化渲染。

理解并掌握了 SSE 流式输出,你就获得了构建实时、互动性强的大模型应用的关键技能。它是连接“智能”与“用户体验”之间的一座重要桥梁。