你要为一个 Web 应用接入“流式 AI 对话”能力。用户发送问题后,前端需要像 ChatGPT 一样边生成边展示,并且支持:
请从前端工程视角回答:
我会这样设计我的交互流程。用户可以在输入框进行输入,敲击回车后发送上下文给大模型。大模型收到输入以后,开始生成返回,使用流式传输把信息分批传给前端,直到最后完成整体输入以后,告知前端消息已发送完成。如果用户主动终止了生成,就立刻告诉前端生成停止。生成过程当中,如果用户提交了新的问题,则先加到队列当中,在当前问题生成完成以后再处理。
前端需要记录返回的结果,同时要记录返回结果的状态,是传输中、传输完成还是错误终止。如果收到了新的流式数据,把结果进行拼接,同时更新结果状态。
当服务端收到用户的输入之后,便开始生成结果。如果没有被打断,则在生成完成以后,把这一轮对话存进数据库。如果用户打断了对话,则由前端告诉服务端用户在什么时刻停止了对话,把已经渲染的内容回传给服务端,用于保存为历史对话。这样的设计是在用户哪怕断网或者切换 APP 以后依旧可以由服务端完成对话的落库。
这次回答已经抓住了几个关键方向,说明你并不是停留在“能把流式文本显示出来”的层面。
优点:
还需要补强的点:
我会把这套能力当成一个 带状态机的对话系统 来设计,而不是一个简单的“发请求 + 拼字符串”功能。
先说整体交互流程。
用户发送消息后,前端先立即写入一条 user message,同时创建一条空的 assistant message 作为占位,并为这次生成分配 requestId。整个会话用 conversationId 标识,每条消息有自己的 messageId。随后前端发起流式请求,把本轮上下文和这些标识传给服务端。前端边接收流式 chunk,边把内容 append 到当前 assistant message 上,UI 持续增量渲染。
在协议选择上,如果这是典型的 服务端单向推送模型输出,我会优先考虑 SSE 或 fetch + ReadableStream。原因是大模型文本生成天然是单向流输出,这两种方式更贴合 HTTP 语义,实现也更轻。WebSocket 更适合双向高频实时交互,例如协作编辑、语音、多人在线状态同步;如果只是做文本流式回答,WebSocket 往往会显得更重。
前端状态我会至少拆成三层:
streaming / completed / stopped / failedidle / requesting / aborting / reconnectingnot_started / persisting / persisted / persist_failed这样可以避免“回答已经显示完了,但数据库还没写成功”这种状态混淆。
“停止生成”是最容易翻车的地方之一。我会这样处理:
AbortController 立即中止本地流读取,让 UI 立刻停下来abort(requestId) 事件或接口调用stopped,以及最终持久化结果这里我会明确:服务端是最终事实源,前端不是。因为前端可能丢 chunk、页面可能刷新、网络可能中断,如果让前端把“我看到的内容”回传后决定最终入库,容易造成用户看到的内容和数据库保存内容不一致。
关于“落盘一致性”,我会要求服务端基于 requestId 或 messageId 做幂等写入。也就是说,同一轮生成不管由于重试、断线重连还是前端重复提交触发了多少次保存请求,最终都只更新同一条 assistant message,而不是新插入多条记录。这样可以避免页面刷新后出现重复消息。
页面刷新和网络异常的恢复我会分两种情况处理:
第一种,已完成的历史消息恢复。页面重新加载时,前端先根据 conversationId 拉取服务端历史消息,直接恢复完整会话。
第二种,进行中的生成恢复。如果服务端记录这条消息仍处于 streaming 或 aborting 中间态,前端需要根据能力选择:
requestId 重新订阅流,则继续接续展示最后,我认为这题里最容易翻车的地方有三个:
所以这道题的关键不是“把字流出来”,而是 设计一套前后端状态一致、控制权明确、异常可恢复的流式对话系统。