流式响应输出
用户发送问题后,AI 回答非一次性全部输出,而是生成一段输出一段;后端调用大模型时需设置stream=true,模型服务边生成边推送数据块,后端持续读取并实时转发给前端,前端实时渲染。
- 实现技术
主流实现流式输出有 3 种方式:HTTP 分块传输编码、SSE、WebSocket。项目选用SSE实现流式输出,SSE 提供了一种简单的、实时的服务器推送数据给客户端的方法。SSE建立在HTTP协议之上,无需复杂协议升级,无专门服务器支持,实现与维护成本低;专为服务器向客户端单向数据流设计,适配 AI 平台回答输出场景;相比 WebSocket 开销更小,无需处理双向消息逻辑;连接意外中断时,内置自动重连机制,提供更稳定的体验。
- 实现方案
前端主流方案:fetch + ReadableStream,优势在于:首先他是完全可控的,可精细控制 POST 请求、headers 与数据处理,灵活性很高;然后他的与各大模型官方 API 的标准完全对齐;可分块接收数据,实现打字机效果。
原生EventSource也可以实现:简单、自带重连、浏览器原生支持,但是存在局限,它仅支持 GET 方法且不支持自定义 Request Headers(这是由 HTML5 规范定义的标准行为),因此无法直接携带 Token 进行鉴权,也不适合提交复杂的 POST 业务参数。并且readablestream提供了对字节级流的精细化控制,适合做文本增量解析。同时,底层网络传输和streams api返回的数据是原始的二进制(uint8array),必须解码成字符串之后才能进行后续处理,fetch readablestream 返回的是二进制块,这里就要用textdecoder将uint8array解码为js字符串,才能用jsonprase或者是data:前缀,流式输出的过程中,用textdecoder(utf-8,{stream:true})对字节流进行增量解码,保证跨chunk的字符能够被正确还原。所以项目流式输出的方案是更灵活的Fetch API + ReadableStream+textdecoder。
核心流程:用 fetch 发送 POST 请求到后端 API;ReadableStream 接收服务端返回的流式数据;分块读取和处理数据,解码拼接实现实时展示;渲染优化:使用buffer 缓冲配合 RAFrequest Animation Frame批量刷新,减少冗余 DOM 操作。这里为什么要进行一个渲染优化呢,如果每次收到数据块chunk直接操作DOM.innerHTML会引发频繁重排(reflow)和重绘(repaint),导致页面卡顿。
优化方案:我这里是设置了一个缓冲区,累积若干 chunk 或等待短时间(16ms),再更新 DOM;利用requestAnimationFrame控制渲染频率(让他和浏览器60fps屏幕刷新频率一致),与浏览器渲染周期同步,避免掉帧,无内容时自动停止渲染,页面不可见时暂停执行。
- 流式中断处理与异常重试机制
在流式渲染中,网络波动或者后端异常,会导致readablestream/SSE中断,不处理的话,用户会看到对话突然停止,用户体验差。
流式中断处理通常分为四步:
捕获异常和超时,判断流是不是中断了;缓存已经接收的文本,记录最后渲染的位置;根据最大重试次数尝试重连或者续流;恢复之后按照原有分批渲染逻辑输出,同时保证滚动和用户感知。
- 中断检测
判断异常是人为还是异常中断
异常捕获,在fetch、readerread时捕获异常
超时检测,使用心跳保活机制,每 30 秒向服务器发送一次心跳请求,防止长连接超时;心跳请求设置 5 秒超时,不阻塞主流程。服务端每隔几秒向流连接发送空数据,在ReadableStream中加入心跳逻辑,每 5s 推送一个空心跳 chunk,前端过滤心跳 chunk,有数据即代表连接活跃,避免长时间静默断开。
- 缓存已经接收的数据-断点续传
通过receiveChunks数组存储已接收消息片段;前端重试时,保证同一点的重试请求服务端只处理一次,避免重复生成内容(幂等性保证)。
- 重连、恢复机制
实现了带重试的fetch请求,支持网络错误和特定的http状态码429、500、502、504。
429:用户发送请求太多、被限流(并发请求太多、被服务商暂时拒绝,单位时间调用次数超了);500:AI 服务器内部出错(临时服务故障、AI 服务 / 网关负载过高挂掉、后端服务重启中);502:网管坏了、代理服务器挂了(ai服务商网管或负载均衡挂了、后端服务重启中、网络链路抖动);503:服务暂不可用(ai模型正在扩容、重启、维护,服务其过载);504:网关超时(AI 服务未返回、服务负载高、处理慢、网络链路延迟大)。遇到以上几个状态码时会自动重试,重试的过程是采用指数退避策略:每次重试延迟时间指数增长,添加随机抖动因子(0.8-1.2)避免多个请求同时重试,达最大重试次数后向用户返回错误。