主题
React + AI 应用开发
React AI 聊天界面如何消费流式响应?
重点不是把 token 逐个 append 到页面,而是设计一条可取消、可恢复、可观测的流式消息链路。
面试官想考什么
- 你会选 SSE、Fetch Stream 还是 WebSocket? 考察协议边界与业务约束。
- 服务端分片如何转换成一条完整消息? 考察流解析、编码和结束语义。
- React 状态如何承载生成中的消息? 考察不可变更新和渲染控制。
- 用户刷新或网络断开后怎么办? 考察恢复、幂等与可观测性。
一句话回答
text
我会让服务端返回带事件类型和 messageId 的流,前端用 ReadableStream 逐帧解析,按 messageId 更新一条 draft 消息,完成或出错时提交最终状态,并用 AbortController、幂等键和服务端持久化保证可取消和可恢复。面试回答详解
核心是协议适配层与 UI 状态层分离。组件不应该直接理解字节分片,也不应该把每个 token 都当作一次独立业务事件。
1. 典型数据流
text
用户提交 -> 创建 assistant message -> 请求流
-> 解码字节 -> 解析事件 -> 校验 messageId
-> 更新 draft -> done/error -> 持久化最终状态可以使用 Fetch 的 response.body.getReader(),配合 TextDecoder 处理跨 chunk 的 UTF-8 字符边界。若采用 SSE,则按 SSE 的 event、data、id 规则解析,而不是简单按换行切字符串。
2. 事件协议建议
json
{"type":"message.started","messageId":"m1"}
{"type":"message.delta","messageId":"m1","text":"你好"}
{"type":"tool.started","messageId":"m1","toolCallId":"t1"}
{"type":"message.completed","messageId":"m1","usage":{"outputTokens":42}}- 事件类型:区分文本、工具、引用、状态和错误。
- 消息标识:防止多会话或重试时串写。
- 序号或 cursor:断线恢复时检测丢片或重复。
- 版本与 requestId:方便灰度和链路排查。
3. React 层的职责
把 messages 视为业务状态,把 streamBuffer 视为传输状态。可以在 hook 中维护 reader 和解析器,组件只接收 messages、status、stop 等稳定接口。生成中消息使用 status: streaming,完成后才允许进入可编辑、可复制和可持久化的最终态。
4. 工程取舍
- Fetch Stream:适合 POST 对话请求、携带请求体和自定义鉴权。
- SSE:适合服务端单向推送,事件语义和重连约定更清晰。
- WebSocket:适合双向实时协作,但连接、鉴权、代理和扩缩容更复杂。
- 逐 token 更新:延迟低但渲染和 Markdown 解析成本高,通常应按时间或字符窗口批量提交。
5. 生产边界
不要把模型密钥放在浏览器。服务端负责权限、配额、模型调用和审计,前端只接收最小必要字段。流结束必须有明确的 completed/error/cancelled 状态;网络层还要记录首 token 延迟、总耗时、断开原因和输出 token 数。
可直接背诵的 30 秒回答
text
我会把流式响应分成传输层、协议层和 React 状态层。传输层用 Fetch Stream 或 SSE,协议层解析带 type、messageId 和序号的事件,状态层只更新对应会话中的 streaming 消息。更新不能每个 token 都触发重渲染,而应做节流或批处理。完成、取消、错误和断线都要有明确状态,并由服务端保存最终消息和幂等信息。扩展知识
流式响应不是“字符串拼接”
UTF-8 字符可能跨网络 chunk;SSE 的 data 也可能多行。解析器需要保留未完成 buffer,结束时还要处理最后一个未带分隔符的事件。
前端的最小状态模型
text
idle -> submitted -> streaming -> completed
|-> cancelled
|-> failed面试官追问链
追问一:为什么不能直接 setText(text + delta)?
- 考察点:状态更新和闭包竞态。
- 回答方向:连续异步回调可能拿到旧值,应使用函数式更新、reducer 或按 messageId 的结构化更新,并控制提交频率。
追问二:SSE 断线后能否直接重连?
- 考察点:是否理解重复和丢失。
- 回答方向:必须有事件 id/cursor、服务端可重放或查询最终状态,并用幂等逻辑去重;不能盲目重新调用模型。
追问三:Markdown 代码块在流式过程中如何处理?
- 考察点:用户体验与解析成本。
- 回答方向:增量阶段允许临时纯文本或轻量解析,完成后再做完整 Markdown、代码高亮和链接安全处理。