为什么 AI 对话的流式响应要用 SseEmitter
做 AI 对话接口的人,大概率都纠结过一个问题:大模型返回内容要等好久,是一次性等它说完再吐给你,还是一边生成一边推给你?
答案是后者。用过 ChatGPT 的都知道,AI 回答是逐字蹦出来的,不是加载半天突然蹦一整段。这个体验背后,靠的就是流式响应(Streaming)。而在 Spring 生态里,实现它最顺手的方式就是 SseEmitter。
先看不用它是什么样
如果你用 Spring 写一个普通的接口,返回一个 String,前端会经历什么?
- 前端发请求,然后干等。
- 后端调大模型,等它把整段回答生成完(可能十几秒)。
- 生成完,一次性把整个字符串返回。
- 前端这才看到内容。
问题很明显:用户盯着一个转圈,十几秒一片空白。本质是"非流式"——数据攒齐了才发。
流式响应解决什么
流式响应是让后端一边拿大模型的输出,一边往连接里写,前端收到一点渲染一点。用户看到的是文字一行行/一字字冒出来,等待感大幅下降,甚至让人觉得"AI 边想边说",体验完全不一样。
而 SSE(Server-Sent Events)就是做这件事的协议:一条普通的 HTTP 连接保持不关,服务器不断往里面写数据,客户端逐块接收。Spring 把它的封装体叫做 SseEmitter。
为什么是它,而不是别的方案
选 SseEmitter 之前,你有三个选项,对比一下就清楚了:
1. 轮询。 前端隔几秒问一次"生成了没"。落后、浪费请求,而且你做不出"逐字输出"的效果,只能拿"整段"或"半段"。
2. WebSocket。 强大,但重。它主打双向通信,而 AI 对话只需要服务器单方向往下推,客户端几乎不说话。为单向需求上双向武器,配置和心智负担都白费。
3. SseEmitter。 单向推送,基于普通 HTTP,天然支持断线重连,走代理负载均衡都顺。AI 对话恰恰就是"服务器单向推、客户端只等"的典型场景。它够用,而且刚好够用。
代码长什么样
核心就一个:接口返回 SseEmitter,在拿到大模型输出的回调里不断 send,最后 complete。
@GetMapping("/chat")
public SseEmitter chat(@RequestParam String prompt) {
SseEmitter emitter = new SseEmitter();
// 模拟大模型逐字返回
new Thread(() -> {
try {
String reply = "你好,我是 AI";
for (char c : reply.toCharArray()) {
emitter.send(String.valueOf(c));
Thread.sleep(100);
}
emitter.complete(); // 流式结束
} catch (Exception e) {
emitter.completeWithError(e); // 出错也要收尾
}
}).start();
return emitter;
}
前端用浏览器自带的 EventSource 就能接,不用引任何库:
const es = new EventSource(`/chat?prompt=你好`);
es.onmessage = (event) => {
console.log(event.data); // 每收到一块就渲染一块
};
前端收到一块渲染一块,这就是"逐字输出"的真相。
两个必须注意的坑
1. 超时。 new SseEmitter() 默认 30 秒超时。大模型生成慢,很容易超时断连。长对话建议设长一点,或设 0 表示不过期:
SseEmitter emitter = new SseEmitter(0L); // 不超时
2. 代理缓冲。 开发环境好好的,线上却不推了,大概率是 Nginx 默认缓冲把流式数据攒住不往下发。记得关掉:
location /chat {
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
}
一句话总结
AI 对话要"边生成边返回",流式是刚需;而单向推送恰好是 SSE 的主场,Spring 又把它封装成了 SseEmitter——比轮询实时,比 WebSocket 轻量,用一个普通 HTTP 接口就搞定了。
