SSE协议与流式响应面试题
2026/4/21大约 6 分钟
SSE协议与流式响应面试题
SSE 协议
什么是 SSE 协议?它的完整消息格式是怎样的?
SSE (Server-Sent Events) 是一种基于 HTTP 的单向服务端推送协议。客户端发起一个普通的 HTTP 请求后,服务端保持连接不关闭,持续向客户端推送事件流。它的响应头中包含 Content-Type: text/event-stream。
完整的 SSE 规范包含四个字段和一个注释机制:
data::核心字段,包含事件的数据内容。一个事件可以有多行data:,并最终以两个连续换行(空行)作为结束标志。event::自定义事件类型。如果不写,默认类型为message。客户端可根据此字段执行不同的回调。id::事件标识 ID。主要用于支持断线重连,客户端重连时会通过 HTTP 头Last-Event-ID携带此标识,以便服务端进行断点续传。retry::服务端告知客户端断线后重连的间隔时间(毫秒)。:(冒号开头的行):注释行,客户端会直接忽略。在生产环境中通常用作心跳保活(如: keepalive),防止 Nginx、负载均衡器等中间件因长时间无数据传输而主动断开连接。
轮询、SSE 和 WebSocket 有什么区别?为什么大模型 API 普遍采用 SSE?
三种实时通信方案的区别:
- 长轮询 (Long Polling):基于 HTTP。客户端发请求,服务端无新数据时阻塞等待。有数据返回后连接关闭,客户端需立即发起新请求。本质仍是一问一答。
- SSE:基于 HTTP。建立连接后,服务端单向向客户端持续推送数据。规范内置断线重连机制,浏览器自带 API 支持。
- WebSocket:独立的双向协议(
ws://)。建立连接后双方可随时双全工互发消息。实现较复杂,需自行实现重连和心跳机制。
大模型 API 普遍采用 SSE 的原因:
- 通信方向匹配:大模型生成内容是典型的“服务端向客户端单向推送”场景(一问多答),无需客户端在流式输出期间发送数据,WebSocket 的双向能力显得冗余。
- HTTP 原生兼容:SSE 本质上就是普通的 HTTP 请求,对 Nginx、CDN、API 网关等基础设施天然友好,零额外配置。而 WebSocket 需要配置专门的升级代理(Upgrade)。
- 实现简单:服务端只需按规范格式输出文本流,客户端逐行解析即可,无需像 WebSocket 那样处理复杂的帧(Frame)编码与解码。
大模型的流式响应在数据结构上与非流式有什么区别?
大模型的流式响应具有以下几个显著的特征和细节:
- 内容位置与增量输出:非流式返回完整 JSON 对象,内容在
choices[0].message.content;而流式返回的是多个data:开头的 JSON 块 (chunk),属于增量输出,内容在choices[0].delta.content。 - 特殊字段的生命周期:
role字段通常只出现在第一个 chunk 的delta中。finish_reason字段只在最后一个 chunk 有值(如"stop","length"等),前面 chunk 中的该字段均为null。
- 流结束标记:整个流的最后通常以一个特殊的文本行
data: [DONE]结束,这并非 JSON 格式,解析时需要单独判断。 - Token 统计 (
usage) 差异:非流式会直接在响应体返回usage;流式模式下不同平台差异很大。有的平台会在最后一个 chunk 返回,有的需要显式传递stream_options: {"include_usage": true}参数,有的甚至不返回,需要客户端自行估算。 - 容错处理:在流式传输中,中间可能出现空
delta(即{}),或者没有content字段。解析时必须做好空值检查,避免抛出异常。
在 Java 中实现一个生产级的 SSE 客户端,需要注意哪些坑?
在生产环境中,仅使用 BufferedReader.readLine() 做简单的 System.out.print 是不够的,手写健壮的 SSE 客户端需要注意以下几点:
- 合理的超时控制:必须区分 OkHttp 的
readTimeout和connectTimeout。在流式场景下,readTimeout代表“两个数据块之间的最大等待时间”,而不是整个请求的完成时间。由于大模型深度思考可能耗时较长,建议将readTimeout适当延长至 30~60 秒。 - 网络异常与状态保留:由于大模型生成的注意力缓存(KV Cache)在服务端,一旦连接异常断开,通常无法利用 SSE 的
id字段进行断点续传。因此,发生异常时应保留已接收的partialContent回调给调用方,并视业务需求将其作为全新的请求重新发起重试。 - 强大的解析容错:
- 必须主动跳过空行和以
:开头的注释行(心跳包)。 - 如果个别 chunk 的 JSON 解析失败,应该捕获异常并跳过(
continue),绝不能中断整个连接的读取。 - 必须兼容部分平台在结束时
choices为空数组的情况。
- 必须主动跳过空行和以
- 设计回调机制:不能阻塞主线程。应设计如
onToken(收到增量内容)、onComplete(正常结束)、onError(异常中断)的回调接口,以便于业务逻辑的解耦(如将数据进一步转发给前端浏览器)。
流式与非流式调用
非流式调用和流式调用有什么区别?分别适用于哪些场景?
非流式调用 (stream: false):
- 工作机制:发送请求后,必须等模型把所有内容都生成完毕,服务端才一次性返回完整的 JSON 响应。
- 体验与场景:首字等待时间长(可能达到几秒至几十秒),用户可能会觉得“卡顿”。但实现简单,适合后台批量数据处理、不需要实时展示结果的场景(如文本向量化 Embedding、重排序 Reranker 等)。
流式调用 (stream: true):
- 工作机制:模型每生成一小段内容(哪怕是一个字),就立刻通过网络推送给客户端,客户端收到一段显示一段。
- 体验与场景:首字响应延迟极低,用户能看到类似 ChatGPT 的“打字机”效果。需要处理数据流,实现稍复杂。适合直接面向用户的实时对话、需要即时交互反馈的场景。
流式调用底层使用的是什么协议?请简述其工作机制。
流式调用底层通常基于 SSE(Server-Sent Events,服务端推送事件) 协议。
- 机制区别:传统的 HTTP 请求是一问一答、完成后关闭连接。而 SSE 是在客户端发起请求后,服务端保持连接打开。
- 数据推送:服务端会持续地往客户端推送数据块,每个数据块是一行以
data:开头的文本。 - 结束标志:当模型生成完所有内容后,服务端会发送一个特殊的结束标记
data: [DONE],此时才可以关闭连接。
在代码中解析“非流式响应”和“流式响应”的数据格式有什么不同?
- 非流式:直接解析整个 JSON 响应体,从
choices[0].message.content中提取模型的完整回答。 - 流式:不能一次性读取响应体,必须通过
BufferedReader等方式逐行读取持续推送的数据。每读取到一个以data:开头的 JSON 块,需要从中提取增量内容。流式的增量内容存放在choices[0].delta.content中,而不是message里。最后需将所有delta.content拼接起来得到完整内容。