一、 什么是流式输出?
在传统的 HTTP 请求-响应模型中,客户端发送一个请求,服务器需要完整地处理这个请求后,才能将一个完整的结果返回给客户端。对于大模型(LLM)这类需要较长生成时间的应用来说,用户必须等待整个回答生成完毕才能看到内容,这会导致用户体验上的“卡顿感”,用户不知道系统是在正常工作还是已经卡死。
流式输出(Streaming) 正是为了解决这个问题而诞生的核心技术。它的核心思想是:服务器在生成回答的过程中,逐步、分块地将内容推送给客户端。就像打字机一样,每生成一个词、一个句子,就立刻发送给前端显示。这为用户提供了实时的反馈,极大提升了交互的流畅感和“智能体正在思考”的拟人感。
提示:流式输出不是一种全新的协议,而是一种数据传输模式。它通常基于HTTP长连接或类似的技术实现,而 SSE(Server-Sent Events) 就是实现这种模式最经典、最标准的协议之一。
二、 为什么需要 SSE 协议?
既然流式输出这么好,如何实现它呢?你可能会想到几种方案:传统的 AJAX 长轮询(Low Polling)、WebSocket 以及 SSE。长轮询效率低、资源消耗大,基本已被淘汰。WebSocket 虽然强大(支持双向通信),但对于服务器单向、持续地推送事件这个场景来说,它显得有些“重”了。
SSE (Server-Sent Events) 是一项在 HTML5 规范中提出的技术,它正是为“服务器到客户端”的单向实时数据流而设计的。它的优势在于:
- 协议简单:基于标准的 HTTP 协议,无需像 WebSocket 那样进行协议升级握手。
- 轻量易用:浏览器内置
EventSourceAPI 支持,自动处理断线重连。 - 格式标准:数据以简单的纯文本格式传输,易于调试和实现。
对于 MiMo 这类大模型的应用场景,模型只需将生成的 token 一个个“吐”出来,客户端只需“倾听”并显示,这完美契合了 SSE 的设计哲学。因此,SSE 成为了实现大模型流式输出的事实标准。
三、 SSE 协议的核心数据格式
SSE 有其非常易懂的文本协议。服务器端推送的 text/event-stream 类型响应,其内容由一系列的事件组成,每个事件由多个字段构成,字段间以换行符(\n)分隔。
一个最简单的事件流数据如下所示:
data: 第一个词
data: 第二个词
data: 第三个词,这句话结束了。
更规范地,一个完整的事件可能包含:
event:- 事件类型(可选)。data:- 事件数据载荷,必须字段。可以有多行,但每行都需以data:开头。id:- 事件 ID(可选)。retry:- 建议客户端重连的间隔时间(可选)。
在 MiMo 或其他大模型的流式输出中,你通常会看到服务器返回类似这样的数据流:
data: {"content": "你"}
data: {"content": "好"}
data: {"content": ","}
data: {"content": "有什么"}
data: {"content": "可以帮你"}
data: {"content": "的吗?"}
data: [DONE]
关键提示:data: [DONE] 是一个常见的约定,用于明确指示流式响应的结束。客户端收到此消息后,应断开连接并完成本次对话。
四、 MiMo 如何处理流式请求?
当你在 MiMo 的 API 中启用流式输出时(通常通过设置 stream=True),整个调用流程与普通请求有显著不同:
- 请求阶段:客户端发起一个普通的 HTTP POST 请求,但会在请求头中期望接收
text/event-stream类型的响应。 - 响应阶段:服务器收到请求后,立即开始生成回答,并持续地以 SSE 事件流的形式返回数据,而不是等到生成完毕。HTTP 连接会保持打开状态,直到流传输完成。
- 客户端处理:客户端需要使用支持 SSE 的方法(如浏览器的
EventSource或后端语言的特定库)来监听这个事件流,逐步接收并处理每一个data事件。
这个过程的关键在于服务器的响应不再是单个完整的 JSON 对象,而是一个持续的数据流。这要求客户端的代码逻辑必须从“一次性获取结果”转变为“逐步订阅事件”。
五、 客户端代码实践(Python)
在 Python 中,我们可以使用 requests 库来演示如何消费一个 SSE 流。虽然 requests 本身不直接支持 SSE,但我们可以通过它的流式响应功能手动解析。
import requests
import json
def stream_chat(query):
url = "https://api.mimo.example.com/v1/chat/completions"
headers = {
"Content-Type": "application/json",
# 假设我们需要一个API Key
"Authorization": "Bearer YOUR_API_KEY"
}
data = {
"model": "mimo-model",
"messages": [{"role": "user", "content": query}],
"stream": True # 关键:启用流式输出
}
# 使用 stream=True 参数发起请求,保持连接打开
response = requests.post(url, headers=headers, json=data, stream=True)
full_response = ""
for line in response.iter_lines():
if line:
# 每行数据都以 b'data: ' 开头
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data: '):
json_str = decoded_line[6:] # 去掉 'data: ' 前缀
if json_str.strip() == '[DONE]':
print("\n\n[流式传输结束]")
break
try:
chunk = json.loads(json_str)
content = chunk.get('choices', [{}])[0].get('delta', {}).get('content', '')
if content:
print(content, end='', flush=True) # 实时打印
full_response += content
except json.JSONDecodeError:
# 处理可能存在的非JSON行,如注释
continue
return full_response
# 调用示例
user_input = "用流式输出给我讲个故事。"
final_answer = stream_chat(user_input)
六、 总结与思考
MiMo 采用 SSE 实现流式输出,本质上是选择了最契合其交互场景的技术方案。 对于生成式 AI 而言,用户感知的流畅度直接决定了产品的体验优劣。SSE 以其简单、高效、标准化的特点,成为了连接大模型“思维流”与用户屏幕之间的绝佳管道。
在实际开发中,你需要:
- 后端:确保你的 API 网关和服务端逻辑能够正确设置
Content-Type: text/event-stream响应头,并以 SSE 格式组织数据。 - 前端:使用
EventSourceAPI(浏览器端)或相应的 HTTP 流式客户端库来优雅地消费事件流,并处理好断线重连、错误处理等边缘情况。 - 理解协议:虽然可以直接使用封装好的 SDK,但了解底层 SSE 协议格式(
data: ...\n\n)对于调试和排查问题至关重要。
进阶思考:除了 SSE,另一种流行的方式是使用 WebSocket 实现双向流式通信,这更适用于需要高频、低延迟交互的场景(如实时游戏)。但对于大多数对话式 AI 应用,SSE 的“单向流”已经足够,并且架构更简单、更容易扩展。MiMo 的选择反映了其注重实用性和用户体验的产品定位。