Skip to content

流式输出与 SSE

为什么流式、SSE 原理、后端实现、前端对接。

Updated View as Markdown
For humans

流式输出与 SSE

模型生成是秒级,不流式用户就对着白屏等。SSE 是 LLM 应用的标配。面试主线:为什么、原理、实现、踩坑。

为什么必须流式

  • 首 token 延迟(TTFT)通常在 0.5-2 秒,全量生成要 5-30 秒
  • 流式让用户边生成边看,体感快一个量级
  • 对话型产品(ChatGPT 打字机效果)没有流式没法用

SSE 原理

SSE(Server-Sent Events):HTTP 长连接上的服务端单向推送

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache

data: {"delta": "你"}

data: {"delta": "好"}

data: [DONE]
  • 文本格式:data: 内容\n\n,事件间空行分隔
  • 基于普通 HTTP(不用 WebSocket),浏览器 EventSource 原生支持
  • 自动重连(浏览器内置)、单向(服务端推)

对比:SSE 单向推送适合 LLM 输出;WebSocket 双向适合交互(见 WebSocket 篇)。

后端实现

from fastapi.responses import StreamingResponse

@app.post("/chat")
async def chat(request: ChatRequest):
    async def gen():
        stream = client.chat.completions.create(
            model="gpt-4o", messages=request.messages, stream=True)
        for chunk in stream:
            delta = chunk.choices[0].delta.content
            if delta:
                yield f"data: {json.dumps({'delta': delta})}\n\n"
        yield "data: [DONE]\n\n"
    return StreamingResponse(gen(), media_type="text/event-stream")

要点:

  • 流式响应不能套全局压缩中间件(内容长度未知,见 FastAPI 生产篇)
  • 代理层(Nginx)要关缓冲(X-Accel-Buffering: no),否则攒一批才发
  • 错误处理:生成中断要在流里发错误事件,客户端能感知

前端对接

const es = new EventSource("/chat?q=你好");
es.onmessage = (e) => {
    if (e.data === "[DONE]") return es.close();
    append(e.data);   // 增量渲染
};
  • EventSource 只支持 GET;POST 场景用 fetch + ReadableStream 解析
  • 心跳:代理层空闲断开时,服务端定期发注释行 : keepalive\n\n 保活
  • 中断/取消:AbortController 断开,服务端感知连接断开停止生成

踩坑清单

解法
代理缓冲(攒批) X-Accel-Buffering: no / proxy_buffering off
空闲断连 心跳注释行
中文乱码 UTF-8 + 字符边界(分块可能切半个字符,要缓冲拼接)
流中断无提示 流尾发 error 事件
压缩中间件 流式响应跳过压缩

面试追问

  1. SSE 和 WebSocket? SSE 单向 HTTP 推送(自动重连),WebSocket 双向。LLM 输出用 SSE 够
  2. 为什么流式? 首 token 快,边生成边看。体感从等待变对话
  3. 代理层怎么配? 关缓冲(X-Accel-Buffering: no),否则 SSE 攒批失去意义
  4. 流断了怎么感知? 心跳保活 + 流尾事件 + 客户端超时兜底
  5. 中文乱码? 分块可能切开多字节字符,缓冲拼接完整字符再发
Navigation

Type to search…

↑↓ navigate↵ selectEsc close