流式对话前后端分别如何实现?

考查重点:增量事件与消息生命周期。后端收模型事件并归一化转发,前端解析片段更新同一消息;处理结束、错误、取消和断线,禁代理缓冲

本题目录

参考答案

核心回答

流式对话需要打通模型生成 → 后端接收并转发事件 → 前端解析并更新当前消息这条链路。只把模型参数设成 stream=true,并不能保证用户逐步看到内容。

后端接收对话请求,调用模型的流式接口,将文本增量、工具状态、结束或错误转换为应用自己的事件,通过 SSE 等方式持续写出。前端读取响应流,正确处理 UTF-8 和 SSE 消息边界,把增量追加到对应消息,直到收到明确的完成或失败事件。

对“前端提交问题、后端持续推送答案”的文本问答,HTTP 加 SSE 通常足够。需要持续双向交互时可以评估 WebSocket;两者都还要处理取消、异常和会话状态,流式本身不会让生成更准确,也不保证总耗时更短。

后端怎样组织这次生成?

可以把一次回答作为独立的生成请求:

  1. 接收会话标识、当前输入和必要参数,验证访问范围,生成本次 requestId,并组装历史上下文。模型服务密钥由后端使用。
  2. 调用模型流式接口,边接收边处理,不能先把完整结果收集完再转发。
  3. 适配不同提供方的事件。正文增量、工具参数、状态和用量不能都当成文本;还要区分“本次新增文字”和“截至当前的完整文字”,避免重复追加。
  4. 持续写出完整应用事件,维护消息状态与已生成内容;按上游结束状态记录成功、截断、取消或失败,并释放连接等资源。

例如 Chat Completions 风格的接口会提供正文 delta,Responses 风格则可能通过不同事件表示文本增量、工具参数完成和整个响应完成。工具参数的 JSON 还没收齐时,不能仅因收到一段内容就执行工具。应用统一适配之后,前端就不必绑定每个厂商的原始字段。

后端返回 Content-Type: text/event-stream,并检查框架、反向代理和网关是否缓冲响应。必要时配置禁用相应缓冲和合适的空闲超时;长时间没有业务事件时可发送 SSE 注释心跳。心跳只表示连接仍在,不表示模型已经生成正文或任务完成。

应用事件可以长什么样?

下面是一种设计示例,事件名和字段由应用约定,不是所有模型接口都原样返回这些字段。每条事件后都需要空行:

event: delta
data: {"requestId":"r-17","seq":1,"text":"你好"}

event: delta
data: {"requestId":"r-17","seq":2,"text":"!"}

event: done
data: {"requestId":"r-17","seq":3,"finishReason":"stop"}

requestId 帮助把事件更新到正确的那次回答,seq 可用于顺序检查和重复过滤。若发生业务失败,可以另发 app_error 事件,包含可识别的错误类型和是否允许重试等信息;它与浏览器连接层的错误要区分。

完成消息还可以携带最终消息 ID、用量或其他必要信息。若上游因为输出上限结束,应保留截断原因,不能把“连接结束”统一包装成完整回答成功。

前端怎样读取和展示?

若接口需要 POST 问题和历史消息,可以使用 fetch 发起请求,检查 HTTP 状态、响应类型及 response.body,再读取响应流。原生 EventSource 不提供 POST 请求体和任意自定义请求头的配置;也可设计先创建任务、再订阅结果的两步接口,按实际交互选择。

读取过程可以分成三层:

  • 字节解码:使用 TextDecoderStream,或使用带流式状态的 TextDecoder。中文字符的 UTF-8 字节可能跨多次读取,不能每块独立解码。
  • SSE 解析:用符合格式的解析器缓冲半行、半事件,处理换行和多行 data,等完整事件后再解析其 JSON。一次读取可能只有半条事件,也可能同时包含多条。
  • 消息更新:按请求和消息标识处理事件。delta 追加正文,done 更新终态,app_error 展示失败或重试入口。累积全文事件应替换对应内容,不能再次追加全文。

界面通常先创建一条“生成中”的消息,再逐步更新。可以合并高频片段,在动画帧或适当批次中渲染,减少每个小片段都更新复杂 Markdown 的成本;显示的代码块、表格也可能尚未闭合,应按当前生成状态处理。

同一会话出现多次请求时,还要防止旧回答的迟到事件写进新回答。只根据“列表最后一条消息”追加内容,容易在重试、切换会话或并发生成时串消息。

停止、断流和重试怎样处理?

用户点击停止时,前端可以通过 AbortController 终止 fetch;后端应感知断开或接收取消请求,并把取消向上游传递。若任务本来需要在后台继续,则要把任务生命周期与订阅连接分开设计,而不是让它无意中继续运行。

若还没收到正常结束事件就断开,保留已经显示的内容并标记“中断”,不要当作完整成功。后端已经开始发送 SSE 后,也不能再依赖改成一个普通 HTTP 错误响应来表达业务失败。

浏览器的自动重连不等于服务端自动续传。需要恢复原生成时,应用必须保存请求、事件序列或最终结果,并按最后确认位置补发;如果服务没有这项能力,应明确重新生成。重新生成产生的是新一次回答,不能把新模型输出无条件接到旧文本后面。

常见追问

流式返回的 JSON 片段能立即 JSON.parse 吗?

先区分两种情况。应用事件的数据如果约定为完整 JSON,可以在事件边界后解析;模型生成的结构化答案或工具参数则可能仍是半个 JSON,需要聚合到对应对象完成后,再做结构和业务校验。网络块、SSE 事件和模型输出对象没有一一对应关系。

用了 SSE,为什么仍是最后一下全出现?

检查模型上游是否真在增量返回、后端是否先收集全文、框架是否及时写出、代理是否缓冲,以及前端是否等到 response.text() 完成才渲染。分段记录时间,才能找到内容在哪一层被积累。

怎样衡量流式体验?

分别记录首字节、首个可见正文、片段间隔、完整回答耗时和中断率。收到响应头或心跳不等于用户已经看到答案;总耗时变长,也可能同时存在首个正文更快的体验改善。

对应教程

  • LangChain 快速上手:流式调用:流式示例逐块读取模型输出,适合理解上游增量从哪里来;浏览器事件解析、代理与取消则需要在应用链路继续实现。

参考资料

  • 国内:JavaGuide:LLM Gateway:借鉴取消、首 Token 超时、断连与重试的讨论,说明正文中流式请求终态与部分输出的区别,以及重试不能直接拼接旧生成。
  • 国内:阿里云百炼:流式输出:通过模型文本块、增量与累积输出的差别说明客户端怎样组装内容,也展示正文与用量可能出现在不同数据块。
  • 国内:腾讯云 TokenHub:Responses API 兼容接口:参考文本增量、输出条目、工具参数与整体完成事件的区分,解释正文中后端事件适配为何不能把所有事件都当文本。
  • 国外:MDN:Using server-sent events:依据命名事件、空行边界、心跳和重连机制,解释正文中服务端持续写出与浏览器接收的连接方式及自动重连边界。
  • 国外:WHATWG HTML:Server-sent events:明确 UTF-8、换行、多行 data 与事件分发规则,解释为什么网络读取块不能直接当成完整 SSE 消息或 JSON。

相关面经

5 篇

美团

AI 后端
校招秋招一面、二面
查看原面经 →
对于需要用户交互的 Agent 任务,系统是否支持流式返回?

广电数科

Java 开发工程师
实习
查看原面经 →
你们的大模型流式输出如何实现的

百度

Agent 应用开发工程师
秋招一面
查看原面经 →
有流式输出吗?具体的流式响应是怎么处理的?

金山办公

数据分析师
日常实习一面
查看原面经 →
使用AI的时候有使用流式对话吗?前后端怎么写的?

拼多多

Agent 应用开发工程师
社招一面
查看原面经 →
你平时调用模型用的是 stream 还是非 stream 方式?

相关题目

4 题