🧑💻 面试官:做一个 AI 聊天页面,模型回答要像打字一样不断显示。你用 SSE 还是 WebSocket?
🙋♂️ 我:聊天是双向的,用户要发问题,模型要回消息,所以我会用 WebSocket。
🧑💻 面试官:用户的问题用普通 HTTP 请求提交,模型生成的内容再由服务端推给页面,不也能完成吗?
🙋♂️ 我:可以。只看这种文本聊天,SSE 就够了。它负责把服务端生成的内容持续发给浏览器,用户发问题可以走另一条普通请求。
🧑💻 面试官:现在模型回答到一半,用户刷新了页面。SSE 不是会自动重连吗,刚才那半段和后面的内容是不是就回来了?
🙋♂️ 我:自动重连只能处理连接问题,不能凭空找回生成任务。刷新后,页面要先找到原来的任务和已经生成的内容,再接收后续输出。
🧑💻 面试官:如果页面重新订阅后,服务端又把刚才的几个字发了一遍?如果用户以为失败,再点一次发送呢?
🙋♂️ 我:这又是两个不同的重复:输出事件要有序号,前端才能去重;提交问题要有幂等标识,后端才能避免再启动一次模型调用。
🧑💻 面试官:那你说说,页面刷新之后,究竟靠什么接着显示同一次回答?
SSE 和 WebSocket 决定消息怎么传;能不能接回同一次回答,取决于服务端有没有保存任务、已生成内容和接收进度。
面试速答(60 秒版)
如果是普通的文字聊天,用户提交一次问题,服务端持续返回回答,我会先用普通 HTTP 请求提交问题,再用 SSE 把生成内容推给页面。用户能发问题,不代表这条连接必须是双向的。只有需要在同一连接里持续双向收发,比如实时语音或多人协作,我才会考虑 WebSocket。
但是,页面刷新后能不能恢复,跟选 SSE 还是 WebSocket 不是一回事。
后端要给这次生成一个 run_id,保存已经生成的文字、任务状态和输出顺序。页面刷新后,先按 run_id 取回已有内容,再从上次收到的位置接收后续事件。重复的事件按序号去掉,用户重复提交的问题则要用幂等标识拦住,不能重新调用一次模型。
如果生成任务本来就绑在浏览器连接上,断开时任务也跟着结束,那换成 WebSocket 也不能接着生成。想让用户刷新后继续看同一条回答,服务端就得先把这条任务管起来。

知识点详解:流式连接和生成任务,其实是两件事
先看谁需要持续给谁发消息
在普通文本聊天里,用户输入一句问题,然后等待模型一段一段地返回内容。用户的提问可以由普通 HTTP 请求发送;生成过程主要是服务端不断往浏览器推送结果。
SSE 就适合承担后半段工作:它是一条服务器到浏览器的事件流。浏览器收到一个事件,就把新增内容显示出来。用户如果点“停止生成”,也可以另外发一个取消请求,不必为了这个按钮改用 WebSocket。
WebSocket 的长处是同一条连接可以双向收发。假如产品需要用户持续发送音频、服务端同时返回识别结果和语音片段,或者多人在一个会话里实时协作,双向连接就更有理由使用。
所以选型时要问的是:这个功能是否真的需要一条持续双向通信的连接? 不能因为页面上既有“发送”又有“接收”,就直接把普通 AI 聊天判成 WebSocket 场景。
不过,这只解决“消息怎么传”。用户刷新页面以后,真正麻烦的地方才开始。
页面没了,任务未必应该跟着没
假设用户让 AI 总结一份需求文档。模型已经输出了半段,页面突然刷新。
旧页面消失了,原来的连接自然也断了。但产品可以选择让生成任务继续运行,等用户回来再接着看。如果要兑现这个体验,生成任务就不能只活在那条浏览器连接的处理函数里。
一种比较清楚的做法是:用户提交问题时,后端创建一次生成任务,返回 run_id。生成过程中,后端保存这条消息已经写出的内容,以及任务状态:还在等待、正在生成、已经完成、失败,或者被用户取消。前端的“正在打字”只是显示状态;真正的任务状态要以服务端记录为准。
这里最好再区分两个 ID:message_id 标记界面上的这条回答,run_id 标记一次具体的生成尝试。用户点“重新生成”时,可以有新的 run_id,但不能把新旧两次输出混在同一条事件序列里。
刷新以后,页面可以从会话消息记录里找回原来的 run_id,而不是指望组件内存还在。再按这个 ID 查询已经生成的文字和任务状态。如果任务还在跑,就订阅后续事件;如果任务已经失败,就展示失败和已有内容,而不是继续显示一个永远转动的光标。

接回后续内容,还要防止“少几个字”和“多几个字”
只存一段当前文本还不够。页面取回这段文本时,服务端可能正在生成新的内容。如果前端先取快照、再去订阅,中间产生的内容就可能漏掉;如果服务端把旧内容又发一遍,页面又会重复显示。
因此,同一次任务的输出需要有顺序。可以给每个增量事件一个递增的序号 seq,并保存可回放的事件;快照同时返回“当前文本”和“最后保存到哪个序号”。页面拿到快照后,再请求这个序号之后的事件;服务端先补发已经产生但页面没拿到的事件,再继续推送新的。前端收到相同序号时直接忽略。快照内容和序号也要对应得上,否则看似有了游标,仍然可能漏字。
SSE 本身也有事件 id 和自动重连机制。短暂断网后,同一个 EventSource 对象可以带着最后的事件 ID 重新连接。但整页刷新会建立新的页面和对象,它不会替应用记住 run_id,也不会替后端保存刚才的回答。我们仍然需要上面那套快照和续传机制。任务完成后,页面也要关闭订阅,别让正常结束变成反复重连。
还有一个容易混淆的重复:用户没看到回复,以为发送失败,又点了一次“发送”。事件序号只能防止文字重复显示,阻止不了后端创建两次任务。创建任务的请求还需要一个 client_request_id;同一个用户重复提交同一次请求时,后端应返回已有的 run_id,而不是再调用一次模型。
这几件事放在一起,刷新恢复才算完整:任务还在、内容能取回、后续事件不漏不重,也不会因为重试多跑一次模型。

面试官继续追问
用户点“停止生成”,SSE 不能往服务端发消息,怎么办?
发送一个普通的取消请求就行。后端收到以后,把对应 run_id 的状态改为“已取消”,并尽可能停止上游生成。页面可以从事件流或者任务查询结果里看到最终状态。
只有取消、编辑这些指令需要持续低延迟地和服务端双向交互时,才有必要重新评估通信方式。一个“停止”按钮本身,不足以证明必须使用 WebSocket。
如果断线时模型任务也停止了,还能叫“恢复”吗?
只能恢复已经保存的那部分内容,不能承诺继续同一次生成。
这不是 SSE 的缺陷,而是应用把生成任务和连接绑在一起的结果。如果产品希望刷新后继续输出,就要让服务端在页面断开后仍管理该任务,并记录生成状态。若业务决定断开即取消,也可以这样设计,但界面应该明确告诉用户,不能刷新后又假装之前那次任务还在运行。
怎样检查这套恢复机制真的可靠?
可以在生成中途分别模拟短暂断网、整页刷新和重复点击发送,再核对刷新前后是不是同一个 run_id,最终文字是否与服务端保存的一致,是否出现漏字、重复字或多次模型调用。
还要测一次无权限访问:知道别人的 run_id,不应该就能订阅他的输出、读取快照或者取消他的任务。run_id 是任务编号,不是通行证。
面试速记卡
- 选型:普通文本聊天可以用 HTTP 提交问题、SSE 推送回答;持续双向收发再考虑 WebSocket。
- 边界:SSE 自动重连解决连接问题,不自动保存生成任务和历史内容。
- 恢复:服务端保存
run_id、消息内容和状态;刷新后先取快照,再从游标之后接收事件。- 去重:事件序号防重复文字,提交幂等标识防重复模型调用。
- 验证:核对同一任务的完整文本、失败状态和权限,不能只看页面能否重新连上。
