AI 大模型评测平台前端要实时看到多家模型的流式输出。这里用 SSE(Server-Sent Events)而不是 WebSocket,因为评测场景是纯「服务端推、客户端收」的单向流,SSE 基于标准 HTTP、自带重连、实现简单,WebSocket 的双向通信能力在这里用不上,反而增加复杂度。选 SSE 不是因为它功能多,而是因为它刚好够用且省心。
一、SSE 和 WebSocket 的取舍
| 对比项 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 服务端→客户端(单向) | 双向 |
| 底层协议 | HTTP | 独立握手 + TCP 帧 |
| 自动重连 | 浏览器内置 | 需自己实现 |
| 代理/防火墙 | 普通 HTTP,友好 | 部分代理不支持 |
| 实现复杂度 | Spring MVC 一个 SseEmitter | 需握手、心跳、状态机 |
| 适用场景 | 推送结果、日志、流 | 实时聊天、协作 |
| 浏览器限制 | 同域 6 连接(HTTP/1.1) | 无此限制 |
评测平台要的是「后端把模型输出推给前端看」,没有前端反推后端的需求。SSE 的单向模型足够,且省掉 WebSocket 的握手维护和心跳逻辑。WebSocket 的双向能力在这里是多余功能,引入它只会增加复杂度——要写心跳保活、要处理握手失败、要管连接状态机,全是评测场景不需要的负担。
二、Spring 里怎么实现 SSE
- Controller 返回
SseEmitter,设置超时; - 把多个模型的并行流订阅到 emitter,每出一段就
send; - 完成后调用
complete,异常时completeWithError。
@RestController
@RequestMapping("/api/eval")
public class EvalController {
private final ParallelEvalService evalService;
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@RequestParam String prompt) {
SseEmitter emitter = new SseEmitter(120_000L); // 2分钟超时
evalService.runParallel(prompt, Duration.ofMinutes(2))
.doOnNext(chunk -> {
try { emitter.send(SseEmitter.event().name("chunk").data(chunk)); }
catch (IOException e) { emitter.completeWithError(e); }
})
.doOnComplete(emitter::complete)
.doOnError(emitter::completeWithError)
.subscribe();
return emitter;
}
}
SseEmitter 是 Spring MVC 对 SSE 的封装。Controller 返回它后,Spring 会保持 HTTP 连接打开,后续每次 send 都往这个连接写一个事件。前端用 EventSource 订阅,收到事件就更新对应模型的回答区域。超时设 2 分钟是因为模型流式推理可能要几十秒,设短了会提前断。
三、为什么 SSE 比 WebSocket 更省心
- 浏览器自动重连:断线后浏览器原生 EventSource 会自动重连,不用前端写重连逻辑;WebSocket 断了得自己检测、自己重连、自己恢复状态;
- 走标准 HTTP:中间的 WAF、负载均衡、CDN 都按普通请求处理,不用单独配 WebSocket 升级;WebSocket 的握手升级在部分代理和 WAF 下会被拦截,排查成本高;
- 不用维护心跳:WebSocket 长连接要定期发心跳保活,不然会被中间网络设备断开;SSE 靠 HTTP 连接本身,前端 EventSource 收到断开会自动重连。
四、SSE 的事件格式
SSE 推送的数据格式是 event: 类型\ndata: 内容\n\n。Spring 的 SseEmitter.event().name("chunk").data(obj) 会自动生成这个格式。前端 EventSource 监听对应事件名,拿到 data 后解析。可以发不同 name 的事件区分语义,比如 chunk 是推理片段、done 是完成、error 是失败,前端按 name 分别处理。
五、什么场景该换回 WebSocket
如果评测平台要加「前端实时干预」(比如前端发「停掉某个模型的推理」指令),那 SSE 单向就不够。两种解法:一是换 WebSocket,支持双向;二是 SSE + 一个额外的 POST 接口——前端发 POST 告诉后端停哪个模型,后端收到后取消对应订阅。纯推结果用 SSE 就行,要双向交互再评估是否值得引入 WebSocket 的复杂度。
六、连接数限制要注意
HTTP/1.1 下浏览器对同域 SSE 默认只有 6 个连接,如果评测平台同时开多个评测任务的 SSE 流,可能撞上限。解法是启用 HTTP/2(无此限制),或者把多个评测任务复用到同一个 SSE 流上用事件名区分。HTTP/2 是更干净的做法,现代浏览器和服务器都支持。
常见问题
Q1:SSE 有连接数限制吗?
有。浏览器对同域 SSE 默认 6 个连接限制,用 HTTP/2 可规避。
Q2:SseEmitter 超时怎么定?
按模型推理时间留余量。设短了会提前断,设长了占线程。
Q3:SSE 能传 JSON 吗?
能。SseEmitter.event().data(obj) 会序列化,前端按 JSON 解析即可。
Q4:前端怎么收 SSE?
用 EventSource 对象。监听 message 事件或自定义事件名,收到就更新界面。