流式协议

共 57 题
📑 题目列表 57 题
#
★★★

1. SSE 的 event、data、id、retry 和空行边界如何解析,网络分块为何不等于事件边界

SSE 的 event、data、id、retry 字段和空行边界应如何解析?为什么网络分块(chunk)不等于事件边界?

  • SSE 协议格式(event/data/id/retry)
  • 空行作为事件分隔符
  • 网络分块与事件边界的区别

SSE 事件由多行组成,以空行分隔:event: 指定事件类型,data: 追加数据(可多条,按换行拼接),id: 提供事件 ID 用于断线重连,retry: 设置重连间隔。解析器按"空行"作为一个事件结束的边界,而非按网络包(chunk)边界。网络分块(HTTP chunked)只是传输层的分帧,一个 SSE 事件可能被拆成多个 chunk,也可能多个事件在同一 chunk 里,因此解析器必须做"累积缓冲 + 按空行切分":把收到的字节追加到缓冲区,完整地按空行切出事件,再交给事件解析。不能把一次 read 或一个 chunk 当作一个事件。

SSE 是"应用层事件流",网络分块是"传输层分帧",两者边界不同。解析器必须缓冲并按空行(事件分隔符)切分,才能正确处理跨 chunk 的事件。

#
★★★

2. 如何把 Provider 的文本、完成、usage、工具调用和错误增量转换为稳定的业务事件协议

如何把 Provider 的文本、完成、usage、工具调用和错误增量转换为稳定的业务事件协议?

  • Provider 增量事件归一化
  • 统一业务事件协议
  • 事件类型与可观测性

把各 Provider 的原始增量(文本 token、完成标志、usage 统计、工具调用增量、错误)归一化为"统一业务事件协议",屏蔽 Provider 差异。协议定义核心事件类型:text_delta(文本增量)、tool_call_delta(工具调用增量,按 call_id 聚合)、tool_result(工具结果)、usage(token 统计)、finish(完成原因)、error(错误,含可重试性分类)。归一化时保留"可观测 ID"(Provider 请求 ID、消息 ID、事件序列号),并映射各 Provider 的完成原因(stop/length/tool_calls 等)到统一枚举。业务下游只消费统一协议,适配层负责转换,从而支持多 Provider 切换。

统一协议的目的在于"下游只依赖稳定契约"。文本/完成/usage/工具/错误都归一化为业务事件,并保留可观测 ID,让上层不感知 Provider 差异,同时保持可追踪。

#
★★★

3. HTTP/2 与 HTTP/3 下的流式响应与 HTTP/1.1 chunked 编码差异如何在客户端正确处理,避免流被截断或合并

HTTP/2 与 HTTP/3 下的流式响应与 HTTP/1.1 chunked 编码差异如何在客户端正确处理,避免流被截断或合并?

  • 各 HTTP 版本下的流式传输差异
  • 客户端正确处理
  • 避免截断/合并

HTTP/1.1 用 chunked 编码分块传输,流以"0-size chunk"结束;HTTP/2 用 DATA frames 进行流式传输,无 chunked 概念,流的结束由 END_STREAM 标志表示;HTTP/3 基于 QUIC,用 STREAM frames,同样以流的结束标志收尾。客户端正确处理:不要依赖"连接关闭"判断流结束,而应依据协议层的结束标志(HTTP/1.1 的终止 chunk、HTTP/2 的 END_STREAM、HTTP/3 的流结束);不要在框架层按 chunk/frame 切分应用事件,仍要按应用层边界(如 SSE 空行)重组;对 HTTP/2 的流式响应,注意不要被代理/网关的缓冲破坏,配置 stream 模式。避免截断/合并的关键是"按应用层协议解析 + 用协议层结束标志判断完成"。

HTTP 版本只改变传输分帧与结束标志,不改变应用层事件边界。客户端必须按协议层结束标志判断完成、按应用层边界重组,才能避免截断或合并。

#
★★★

4. Provider 流事件(OpenAI delta、Anthropic message_start/delta/stop、Gemini chunk)如何归一化到内部统一事件协议,并保留可观测 ID

Provider 流事件(OpenAI delta、Anthropic message_start/delta/stop、Gemini chunk)如何归一化到内部统一事件协议,并保留可观测 ID?

  • 各 Provider 流事件形态差异
  • 归一化映射
  • 可观测 ID 保留

各 Provider 事件形态不同:OpenAI 用 choices[].delta 承载文本/工具增量,结束有 finish_reason;Anthropic 用 message_start/content_block_delta/message_stop 分阶段,含 content block 与 tool_use;Gemini 用 candidates[].content.part 的 chunk 增量。归一化映射:把"文本增量"统一为 text_delta,把"工具调用增量"按 call_id 聚合为 tool_call_delta,把"完成/停止"统一为 finish(含 reason 映射),把"usage"统一为一个统计事件。可观测 ID 保留:记录 Provider 的请求 ID、消息 ID、事件序列号,并关联到内部 request_id,写入日志与 Trace,使跨 Provider 的排查可追溯到原始 Provider 事件。

归一化本质是"建立从 Provider 事件到内部协议的一一映射"。文本/工具/完成/usage 四个维度统一后,下游稳定;可观测 ID 则保证归一化不丢失原始追踪信息。

#
★★★

5. 代理缓冲、空闲超时和连接保活会怎样破坏 SSE,心跳与反向代理应如何配置

代理缓冲、空闲超时和连接保活会怎样破坏 SSE?心跳与反向代理应如何配置?

  • 代理缓冲/空闲超时对 SSE 的破坏
  • 心跳机制
  • 反向代理配置

代理缓冲(如 nginx 的 proxy_buffering on)会缓存响应,导致 SSE 事件延迟到达或一次性到达,破坏流式体验;空闲超时会在一段时间无数据时关闭连接,导致 SSE 中断;代理连接保活设置不当可能使长连接被复用而引起串流。对策:关闭代理缓冲(proxy_buffering off)、配置长超时(proxy_read_timeout 设大)、关闭或正确配置 keep-alive。心跳:定期发送 SSE 注释行(: 开头)或空事件作为心跳,保持连接活跃、防止超时被关闭,同时让代理"刷新"空闲计时。心跳频率要权衡(太快浪费带宽、太慢被关闭)。

SSE 的维持依赖"连接活跃 + 响应即达"。代理缓冲/超时/保活是常见破坏源,心跳注释行保活 + 关闭缓冲/调大超时是标准配置,能稳定长连接。

#
★★★

6. 如何治理背压、慢消费者、并发连接、超时和资源释放,避免连接或线程耗尽

如何治理背压、慢消费者、并发连接、超时和资源释放,避免连接或线程耗尽?

  • 背压与慢消费者
  • 并发连接与超时
  • 资源释放

背压与慢消费者:当消费者消费速度慢于 Provider 生产速度时,缓冲会增长,需用有界缓冲 + 背压信号(如暂停读取、丢弃或降级),避免内存膨胀;慢消费者可被断开或降级。并发连接:限制每用户/全局的并发 SSE 连接数,超限拒绝或排队,防止连接耗尽。超时:为连接、空闲、读取、写超时设置合理上限,超时即清理。资源释放:确保连接关闭、缓冲清空、定时器/订阅取消在 finally/cancel 中执行,避免泄漏。工程上用"连接池/许可量 + 有界缓冲 + 超时兜底 + 资源回收"组合治理,配合监控(连接数、缓冲水位、超时率)预警。

连接/线程耗尽的根源是"无界资源 + 无界缓冲 + 无超时"。有界缓冲与背压、并发限流、超时兜底、确定性资源释放,四者合力才能让流式资源可回收、可收敛。

#
★★★

7. 如何处理流中穿插的非文本事件(tool_call、reasoning、citation、usage)

如何处理流中穿插的非文本事件(tool_call、reasoning、citation、usage)?

  • 非文本事件的识别与分类
  • 与文本流的协调
  • 前端展示策略

流中除文本外还穿插 tool_call(工具调用增量)、reasoning(思考过程)、citation(引用)、usage(用量统计)等非文本事件。处理:建立统一事件协议,按事件类型区分,非文本事件不进入文本渲染管线,而是进入各自的处理通道——tool_call 按 call_id 聚合、reasoning 单独展示或隐藏、citation 记录待最终校验后展示、usage 仅用于统计。前端按类型渲染:文本进正文、工具调用显示为进度/动作、引用显示为链接、usage 不进正文。同时要保证事件顺序与完成状态正确(如 citation 在最终阶段才显示可信标识)。关键是"类型化 + 分通道 + 顺序协调"。

非文本事件与文本事件是"不同类型的并行信息"。统一协议按类型分流,避免把工具增量或 citation 误当正文,同时保证各通道在最终状态协调一致。

#
★★★

8. 流式错误恢复([DONE] 缺失、连接提前关闭、Provider 5xx)应如何被服务端捕获、记录并通知客户端

流式错误恢复([DONE] 缺失、连接提前关闭、Provider 5xx)应如何被服务端捕获、记录并通知客户端?

  • 流式错误类型
  • 服务端捕获与记录
  • 客户端通知与恢复

流式错误类型:[DONE] 缺失(流被截断但未给出正常结束)、连接提前关闭(中途断开)、Provider 5xx(服务端错误)。服务端处理:捕获流的不完整结束(超时未收到结束标志、连接异常关闭、Provider 返回错误),把这些情况记录为结构化错误日志(含 request_id、阶段、已收到的部分、错误码);对可重试的错误(如瞬时的 5xx)可重试,对不可重试的(如已产生部分输出)则记录并通知。客户端通知:通过 error 事件或 finish(reason=error) 通知客户端"流已结束但因错误终止",并给出可恢复的提示(重试/降级);保证已收到部分内容的处理(回滚或保留)。

流式错误恢复的关键是"识别非正常结束 + 记录 + 通知"。服务端检测不完整结束并分类记录,客户端收到 error 事件后决定重试或降级,避免把错误当正常完成。

#
★★★

9. SSE、WebSocket、WebTransport 与非流式响应应如何按单向性、双向性和部署环境选型

SSE、WebSocket、WebTransport 与非流式响应应如何按单向性、双向性和部署环境选型?

  • 各协议的单向/双向性
  • 部署环境兼容性
  • 选型依据

SSE 是单向的(服务端→客户端),基于 HTTP,简单、自动重连、兼容性好,适合"服务端推送"场景(如文本流、通知),但每个连接单向、同源并发连接数有限(浏览器约 6 个)。WebSocket 是双向的,适合"客户端与服务端双向实时交互"(如实时语音、游戏、双向消息),但需自行实现心跳、重连、协议。WebTransport 基于 QUIC,支持双向、多路复用、低延迟,但浏览器与部署环境支持有限,适合前端稀疏的实时场景。非流式响应适合一次性请求、无需实时推送的场景。选型按"单向 vs 双向"、"部署环境(浏览器/服务端/代理)"与"延迟/复杂度要求"综合决定。

选型的核心是"通信方向 + 部署环境"。单向下推用 SSE,双向实时用 WebSocket,前沿低延迟场景可考虑 WebTransport,能一次性返回就别用流式。复杂度与兼容性也要权衡。

#
★★★

10. Serverless 或 Edge 环境存在执行时长和连接限制时,流式接口如何降级或续传

Serverless 或 Edge 环境存在执行时长和连接限制时,流式接口应如何降级或续传?

  • Serverless/Edge 的执行时长与连接限制
  • 流式降级策略
  • 续传/异步化

Serverless/Edge 常有限制:函数执行时长上限(如几秒到几十秒)、连接保持限制、无状态。因此长流式无法直接跑在受限函数里。降级策略:把长流式改为"异步任务"——函数快速接收请求并返回一个任务 ID,后台由常驻服务或队列执行生成,客户端轮询或通过 SSE 订阅任务结果;或把"短流式"(前几秒)在函数内执行,超过限制则切换为异步续传。Edge 上可用"分片/续传"——把长输出分段,每段由一次函数调用生成,客户端用游标/偏移续传下一段。降级时前端要能区分"同步流"与"异步任务",并平滑过渡。总之把"长连接"变成"短请求 + 异步回传"。

Serverless 的时长/连接限制决定了"长流式"不可行。降级思路是"切短、异步化、续传":短流在函数内、长任务转异步任务 + 轮询/订阅,客户端用任务 ID 续接。

#
★★★

11. text/event-stream 的 BOM、字符编码、压缩(gzip/brotli)对事件边界的影响如何测试

text/event-stream 的 BOM、字符编码、压缩(gzip/brotli)对事件边界的影响如何测试?

  • BOM 与字符编码的影响
  • 压缩对事件边界的影响
  • 测试方法

BOM(字节序标记)若出现在流开头,可能被当作事件内容的一部分,需解析器跳过 BOM;字符编码(UTF-8 多字节字符)可能跨数据块,需按解码单元处理,避免把字节流当字符切分;压缩(gzip/brotli)在传输层解压后才是事件流,解压是流式的,需保证解压后按事件边界重组,且压缩可能改变缓冲区边界。测试方法:构造"含 BOM 的流"、"跨块的 UTF-8 字符"、"分块压缩/解压"等用例,验证解析器是否能正确切分事件、不丢不串;用固定的 SSE 事件集,分别以不同 chunk 大小、带/不带压缩、带/不带 BOM 发送,断言解析结果一致。

BOM、编码、压缩都会影响"字节→事件"的边界。测试要从"字节级"构造各种边界情况(BOM、跨块字符、压缩分块),验证解析器在字节重组后仍能正确切分事件。

#
★★★

12. 多 SSE 连接同时推送时,浏览器同源 6 连接限制和代理 keep-alive 限制如何绕过

多 SSE 连接同时推送时,浏览器同源 6 连接限制和代理 keep-alive 限制如何绕过?

  • 浏览器同源连接限制
  • 代理 keep-alive 限制
  • 绕过/缓解策略

浏览器对同一源(origin)的 HTTP/1.1 并发连接约 6 个,多 SSE 连接容易占满。缓解:用 HTTP/2 多路复用(一个连接承载多个流,打破 6 连接限制);或把 SSE 连接合并——多个会话用一个"多路复用流"(事件内带会话 ID 区分);或减少常驻连接,用"短连接 + 轮询";或使用 WebSocket(突破 HTTP 连接数限制)。代理 keep-alive 限制指代理对空闲 keep-alive 连接数量的限制,SSE 长连接会占用;对策:配置代理 keep-alive 上限、关闭代理级缓冲、用 WebSocket 或 HTTP/2 避免逐连接占用。也要考虑把多个订阅合并到共享连接并在服务端按会话 ID 分发。

绕过限制的本质是"减少独立连接数"。HTTP/2 或 WebSocket 可在单连接上多路复用,或服务端合并订阅、按会话 ID 分发,从而绕开浏览器与代理的连接数瓶颈。

#
★★★

13. 如何区分“流已结束”和“流暂时没新数据”,避免误判超时关闭

如何区分"流已结束"和"流暂时没新数据",避免误判超时关闭?

  • 流结束与无新数据的区分
  • 超时策略
  • 心跳与结束标志

区分"流已结束"与"暂时没新数据"依赖"明确的结束标志"与"心跳机制":结束由协议层标志(如 [DONE]finish 事件、连接正常关闭)表示;暂时没新数据则表现为"连接还活着但无事件"。为避免误判超时关闭,超时逻辑要区分"空闲超时"(无数据无心跳)与"整体超时"(超过总时长):服务端定期发心跳(注释行)表示"连接仍活跃",客户端收到心跳即重置空闲计时;只有"既无数据也无心跳超过阈值"才判定连接失效。同时设置合理的整体超时上限,避免无限等待。因此"结束看标志、存活看心跳、超时看策略"。

误判超时的根源是"把空闲当结束"。用结束标志判断完成、用心跳判断存活、用空闲+整体双重超时兜底,才能正确区分流结束与暂时无数据。

#
★★★

14. 为什么不应在 SSE 事件中夹杂大体积二进制(如 base64 图片)

为什么不应在 SSE 事件中夹杂大体积二进制(如 base64 图片)?

  • 大体积 base64 在 SSE 中的问题
  • 文本流中的二进制
  • 替代方案

不应在 SSE 事件中夹杂大体积二进制,原因:base64 会把二进制膨胀约 33%,large payload 会阻塞事件流、增加延迟、占满内存缓冲,还可能超过代理/浏览器对单事件或单条数据长度的限制;且 SSE 是文本协议,大二进制会破坏流式体验(事件迟迟不结束)。替代方案:SSE 事件只携带"引用 + 元数据"(如图片 URL、资源 ID、长度),二进制通过独立的 GET 或分块上传/下载通道获取,或用专门的二进制传输(WebSocket 的二进制帧、HTTP 分块下载)。这样流保持轻量、事件边界清晰。

SSE 是"小而快"的文本事件流,大二进制会让它变"重而慢"。事件只带引用,二进制走独立通道,是保持流式体验与边界的标准做法。

#
★★★

15. EventSource 与 Fetch 流在请求方法、请求头、取消、错误处理和认证方面有何差异

EventSource 与 Fetch 流在请求方法、请求头、取消、错误处理和认证方面有何差异?

  • EventSource 与 Fetch 流的差异
  • 请求方法/请求头/取消/错误/认证
  • 封装选择

EventSource:仅支持 GET,不能自定义请求头(如 Authorization 需通过 query 或 cookie 传递),自动重连,取消用 close(),错误处理自动重连(不能精细控制),认证依赖 cookie 或查询参数。Fetch 流:支持任意方法(GET/POST)与自定义请求头(可直接带 Authorization),用 AbortController 精细取消,错误处理需手动处理重连与解析,认证可灵活设置。差异决定了:需要自定义头/认证/POST 时用 Fetch 流;需要简单帮助自动重连且只读时可用 EventSource。工程上常封装统一客户端,用 Fetch 流实现更灵活的控制。

EventSource 简单但受限(GET、无自定义头、自动重连不可精细控制),Fetch 流灵活但需自行处理重连与错误。按认证与请求需求选择,通常封装为统一客户端。

#
★★★

16. UTF-8 字符与 SSE 事件都可能跨数据块,前端应怎样分层解码、分帧和拼接

UTF-8 字符与 SSE 事件都可能跨数据块,前端应怎样分层解码、分帧和拼接?

  • 分层解码(字节→字符)
  • 分帧(按事件边界)
  • 拼接与缓冲

前端应分层处理:先做"字节缓冲 + 解码"——把网络字节累积到缓冲区,用 TextDecoder(流式解码,处理跨块的多字节 UTF-8 字符,不把半个字符当完整字符)解码;再在解码后的文本上做"分行/分帧"——按 SSE 事件的空行边界切分事件,把跨块的事件行缓冲拼接;最后按事件结构解析字段。即"字节→字符(解码)→行(分帧)→事件(解析)"三级。关键是要用流式解码器(TextDecoder 的 stream:true)处理跨块字符,用缓冲累积处理跨块事件,避免把半个字符或半个事件误判。

UTF-8 字符与事件都可能跨块,因此必须"字节缓冲 + 流式解码 + 事件缓冲"三层。每一层处理一个边界问题,组合起来才能正确还原完整事件流。

#
★★★

17. 增量 Markdown 尚未闭合代码块、表格或链接时,如何避免 DOM 抖动和不安全 HTML

增量 Markdown 尚未闭合代码块、表格或链接时,如何避免 DOM 抖动和不安全 HTML?

  • 未闭合 Markdown 的增量渲染
  • DOM 抖动
  • XSS 防护

流式 Markdown 可能处于"未闭合"状态(代码块无结束 ```、表格未对齐、链接未闭合),直接渲染会导致 DOM 抖动(结构反复重建)。处理方法:用"流式 Markdown 解析器"(如 Streamdown/micromark 流式模式)做增量解析,维护"未闭合状态"——未闭合的代码块/表格暂作为"进行中"渲染(如代码块边框、表格占位),不强制闭合;前端用"可编辑的临时节点"而非每次重建 DOM;对表格等需对齐的结构,用"末尾缓冲"——积攒到表格可能闭合时再渲染,避免抖动。XSS 防护:所有输出(含用户内容、链接 URL)必须经过净化(sanitize),链接 URL 做白名单/协议校验,禁止执行 HTML/JS,用 DOMPurify 等净化后再插入 DOM。

未闭合状态是流式 Markdown 的常态。处理诀窍是"流式解析维护未闭合状态 + 缓冲可能闭合的结构 + 净化输出",既避免 DOM 抖动又防 XSS。

#
★★★

18. 打字机渲染(typewriter effect)的人为延迟与实际流延迟叠加后,用户感知 TTFT 如何更准确

打字机渲染(typewriter effect)的人为延迟与实际流延迟叠加后,用户感知 TTFT 如何更准确?

  • 打字机人为延迟与网络流延迟
  • 感知 TTFT 的度量
  • 渲染与度量的解耦

打字机渲染会人为拉长"可见时间",而"首 Token 延迟(TTFT)"是网络层在收到首个 token 前的时间。若把打字机渲染混入 TTFT 度量,会导致感知 TTFT 虚高。更准确的做法是"分层度量":TTFT 在"收到首个 token 的时刻"即刻记录(网络层),与渲染无关;用户感知的"首字可见时间"= 网络 TTFT + 渲染调度延迟,单独度量。渲染层用 requestAnimationFrame 或时间片节流,人为延迟只影响"逐字显现"节奏,不影响 TTFT 统计。两者解耦:TTFT 反映网络/服务端性能,字速反映前端体验,分别监控才能定位"慢是慢在网络还是渲染"。

人为延迟与网络延迟是不同的"延迟来源"。把 TTFT(网络首 token)与首字可见时间(网络+渲染)分开度量,才能准确判断延迟瓶颈,避免打字机渲染污染性能指标。

#
★★★

19. 大回答(>10k token)的虚拟滚动、虚拟 DOM 与分块渲染策略如何避免内存爆炸

大回答(>10k token)的虚拟滚动、虚拟 DOM 与分块渲染策略如何避免内存爆炸?

  • 大内容的内存问题
  • 虚拟滚动/虚拟 DOM/分块渲染
  • 性能优化

大回答(>10k token)若全量渲染到 DOM,会因大量 DOM 节点而内存爆炸、卡顿。策略:虚拟滚动——只渲染可视区域内的行/块,滚动时替换,避免全量 DOM;虚拟 DOM/分块渲染——把长内容切成块(如按段落/代码块),增量渲染,已渲染的块无需重建;对超长内容做"分页/懒加载",只渲染当前需要部分;对结构化大输出(如长表格、长列表)用虚拟列表。内存管理:及时释放已不可见块、用轻量字符串缓存而非大量 DOM,限制单块大小。选择"可视窗口渲染 + 分块 + 懒加载"的组合,而不是一次性渲染全部 token。

内存爆炸源于"全量 DOM"。虚拟滚动限制可视节点数,分块渲染限制一次性构建量,懒加载延迟非可视内容,三者组合让超大内容在有限内存下流畅渲染。

#
★★★

20. 如何用 requestAnimationFrame 或时间片节流打字机渲染,同时度量首 Token 延迟和可见完成时间

如何用 requestAnimationFrame 或时间片节流打字机渲染,同时度量首 Token 延迟和可见完成时间?

  • rAF/时间片节流渲染
  • 首 Token 延迟与可见完成时间度量
  • 渲染与度量分离

打字机渲染用 requestAnimationFrame 或时间片节流:rAF 在每帧(约 16ms)渲染固定数量的字符,避免每 token 触发一次 DOM 更新造成卡顿;或用时间片(如每 50ms 渲染一批)控制渲染节奏。度量:首 Token 延迟(TTFT,网络层收到首个 token 的时刻)在收到首个 token 时立刻记录,与渲染解耦;可见完成时间在"最后一个字符渲染完成"时记录。两者都通过时间戳标记,分别反映"网络/服务端首 token 性能"与"前端渲染完成性能"。渲染层用 rAF 节流,不影响 TTFT 计时的准确性。

rAF/时间片解决"渲染频率"问题,TTFT 与可见完成时间解决"测量"问题。二者解耦:TTFT 在接收时刻记录,可见完成时间在渲染完毕记录,既流畅又可测。

#
★★★

21. 停止、重试、编辑后重发、分支对话和晚到事件应如何通过会话 ID、消息 ID 隔离

停止、重试、编辑后重发、分支对话和晚到事件应如何通过会话 ID、消息 ID 隔离?

  • 会话 ID 与消息 ID
  • 停止/重试/编辑/分支/晚到事件的隔离
  • 幂等与去重

用"会话 ID + 消息 ID"建立隔离:每个会话有唯一 session_id,每次生成有 message_id(或 run_id)。停止——取消当前 run,用 run_id 标记终止,晚到的事件若不匹配当前 run_id 则丢弃;重试——重新发起新 run,用新 run_id,旧的晚到事件不污染新 run;编辑后重发——编辑产生新消息与分支,用 message_id 区分,分支用 parent_id 关联;分支对话——同会话中多个分支各用独立消息链,靠 parent_id 隔离;晚到事件——每个事件带 run_id + 序号,前端只接受"当前 run_id 且序号合理"的事件,旧 run 的晚到事件丢弃。整体用"会话/消息/run 三级 ID + 序号"实现隔离与幂等。

停止、重试、编辑、分支、晚到都源于"同一会话内多个并发或先后 run"。用 session_id 作用域、message_id/run_id 标识生成、序号定序,前端只认当前 run 的合法事件,即可隔离。

#
★★★

22. 断线重连时如何用事件 ID 去重和恢复;服务端不支持游标时应怎样向用户降级

断线重连时如何用事件 ID 去重和恢复?服务端不支持游标时怎样向用户降级?

  • 事件 ID 去重与恢复
  • 游标支持
  • 无游标时的降级

SSE 的 id 字段支持断线重连:客户端记录最后处理的事件 ID,重连时用 Last-Event-ID 头告知服务端,服务端从该 ID 之后重发,实现恢复;客户端对重复收到的事件按 ID 去重。若服务端不支持游标(无法按 ID 恢复),则无法精确续传,此时降级:告知用户"连接中断,已展示部分内容",对已展示文本保留(不重复渲染),对未完成部分用"重新生成"或"重新请求"的方式恢复,但要避免重复执行副作用工具(只重放文本,不重放副作用)。降级的核心是"保证不重复、不破坏、不重复副作用"。

事件 ID 是"断点恢复"的机制。服务端支持游标则用 Last-Event-ID 续传并去重;不支持则降级为保留已展示内容 + 重新生成,且只重放文本不重放副作用。

#
★★★

23. AbortController 取消后,服务端继续推送的“晚到事件”如何在前端安全丢弃并避免计费

AbortController 取消后,服务端继续推送的"晚到事件"如何在前端安全丢弃并避免计费?

  • 取消后的晚到事件
  • 前端安全丢弃
  • 计费避免

调用 AbortController.abort() 会关闭前端连接,但服务端可能已生成并继续推送的事件(晚到事件)会到达被关闭的流或被异步缓冲。前端安全丢弃:用 run_id/消息 ID 校验事件归属,取消后前端把状态置为"已取消",后续到达的事件因不匹配当前 run 或已取消状态而被立即丢弃;不要对已取消 run 的事件做任何渲染或副作用。计费避免:取消后前端应把"取消信号"传回服务端(通过独立的取消请求或连接关闭后的通知),让服务端停止继续生成并上报"已用 token";计费按"实际被服务端处理的 token"而非"前端收到了多少"——取消后服务端应停止并记录已消耗量,避免继续计费。前端用"已经消费/已取消"状态与事件校验共同保证丢弃。

晚到事件的安全丢弃依赖"归属校验 + 取消状态";计费避免依赖"取消信号回传服务端,服务端停止生成并按实际消耗计费"。前端丢弃 + 后端停止才是完整闭环。

#
★★★

24. React Server Components、Next.js Streaming 与前端 SSE 客户端在 AI 流场景下如何结合使用

React Server Components、Next.js Streaming 与前端 SSE 客户端在 AI 流场景下如何结合使用?

  • RSC 与 Next.js Streaming
  • 前端 SSE 客户端
  • 结合方式

在 AI 流场景中,Streaming 能力可分层使用:Next.js Streaming 支持服务端渲染时流式输出(如骨架屏、Per-Component Streaming),适合"页面级内容"流式渲染;RSC 允许服务端把组件作为流式响应下发,适合需要服务端渲染的结构化 UI。前端 SSE 客户端用于"对话级"的实时 AI 流(逐 token 文本、工具调用)。结合方式:页面级用 Next.js Streaming 流式渲染 UI 骨架,对话内容用 SSE 客户端逐字流式更新;RSC 用于服务端渲染的静态/半静态组件,与 SSE 更新的动态对话区互补。注意避免重复渲染与状态冲突——SSE 更新的内容应独立于 RSC 渲染的静态部分,用明确的状态边界管理。

三种技术服务于不同粒度:Next.js Streaming 管页面级流式渲染,RSC 管服务端组件下发,SSE 管对话级实时流。分层使用、明确状态边界,避免重复渲染与冲突。

#
★★★

25. Markdown 中的数学公式(KaTeX/MathJax)在流式增量渲染时如何处理未闭合的分隔符与跨块公式,避免抖动与错位?

Markdown 中的数学公式(KaTeX/MathJax)在流式增量渲染时如何处理未闭合的分隔符与跨块公式,避免抖动与错位?

  • 数学公式的流式渲染
  • 未闭合分隔符处理
  • 避免抖动与错位

数学公式($...$$$...$$)在流式过程中可能处于"未闭合"状态(双 $$ 只来了一个,或公式字符跨多个块)。处理:用流式解析器识别公式分隔符,未闭合时把"疑似公式"暂存为"进行中"状态(不渲染或渲染为占位),等闭合后再用 KaTeX/MathJax 渲染;对跨块公式,用"缓冲累积"——把未闭合的公式内容累积到完整后再渲染,避免每块都重新渲染导致抖动与错位。为避免逐个字符重排,可对已渲染公式做"一次渲染 + 缓存",未闭合时显示原始文本或占位,闭合时一次性替换为渲染结果。同时限定公式渲染在"干净边界"(完整公式)执行,减少抖动。

公式的未闭合与跨块是流式渲染的难点。缓冲未闭合公式到完整、在干净边界一次性渲染并缓存,避免逐块重渲染造成的抖动与错位。

#
★★★

26. aria-live、焦点管理与 prefers-reduced-motion 如何让流式聊天对辅助技术可用

aria-live、焦点管理与 prefers-reduced-motion 如何让流式聊天对辅助技术可用?

  • aria-live 的播报策略
  • 焦点管理
  • prefers-reduced-motion 降级

流式聊天对辅助技术的可用性:aria-live——用 aria-live="polite" 声明动态更新区域,让屏幕阅读器在内容更新时播报,但流式逐字更新会频繁播报,需用"聚合/节流"(如每若干字符或每个完整段落播报一次)避免噪音;焦点管理——生成过程中保持输入框焦点、不打断用户输入,停止按钮可聚焦,动态出现的元素(如工具状态)适时聚焦或留给阅读器;prefers-reduced-motion——用户偏好减少动效时,关闭打字机/闪烁等动画,改为直接显示或低速更新,尊重系统偏好。三者结合让流式聊天对键盘、屏幕阅读器与动效敏感用户同样可用。

可用性设计要"动态内容可感知、焦点可预期、动效可降级"。aria-live 聚合播报、焦点保持与合理迁移、prefers-reduced-motion 关闭动画,是流式聊天可访问性的基础。

#
★★★

27. 流式内容中混入可执行 HTML(图片 onerror、SVG script)时,前端应如何净化渲染并防止 XSS?

流式内容中混入可执行 HTML(图片 onerror、SVG script)时,前端应如何净化渲染并防止 XSS?

  • 流式内容中的 XSS 风险
  • 净化渲染
  • 防止恶意 HTML

流式内容可能混入可执行 HTML(如 <img onerror>、SVG 内嵌 <script>),模型输出或用户数据都可能携带。前端必须净化:把渲染前的 Markdown/HTML 通过净化库(如 DOMPurify)过滤,移除可执行脚本、事件处理器(onerror/onclick)、危险的 URL 协议(javascript:)、SVG 中的可执行元素;对链接/图片 URL 做协议白名单。净化要"在插入 DOM 前"且"对最终渲染内容"执行,流式输出的每个部分也要经过净化(不能因分块而跳过)。渲染用文本节点而非 innerHTML 拼接,即使用户内容也视为不可信。此外服务端也应做净化兜底,前端净化为主。

流式内容必须视为不可信。前提是净化发生在插入 DOM 之前,且对每个渲染片段执行;用净化库 + 协议白名单 + 文本节点渲染,服务端兜底,构成防 XSS 的多层防线。

#
★★

28. 流式聊天中输入框、停止按钮、自动滚动、快捷键(Cmd+Enter)的交互状态应如何设计,才能在生成过程中保持输入可用且不丢失焦点?

流式聊天中输入框、停止按钮、自动滚动、快捷键(Cmd+Enter)的交互状态应如何设计,才能在生成过程中保持输入可用且不丢失焦点?

  • 生成中的交互状态
  • 输入可用与焦点保持
  • 自动滚动与快捷键

生成过程中交互设计:输入框保持可用(不因生成而禁用),用户可随时输入下一句或编辑;焦点始终保持在输入框,避免生成把焦点抢占;停止按钮在生成中显示,点击后停止当前生成并恢复可发送状态;自动滚动要"智能"——用户滚动到历史时暂停自动滚动,回到底部时恢复,避免强制滚动打断阅读;快捷键(Cmd+Enter 发送)在生成中仍可用,但若正在生成可先停止或排队。状态机:idle → generating → idle,生成中锁定发送"新消息"但输入框可编辑,用状态控制按钮与滚动的行为。

核心是"生成中不剥夺输入权"。输入框持续可用、焦点保持、停止按钮可触达、自动滚动尊重用户意图、快捷键不冲突,用清晰的状态机管理这些交互。

#
★★

29. 多标签页同时打开同一会话时,事件流如何在标签间共享与同步,避免重复消费

多标签页同时打开同一会话时,事件流如何在标签间共享与同步,避免重复消费?

  • 多标签页会话同步
  • 事件流共享
  • 避免重复消费

多标签页打开同一会话时,若每个标签页各自发起 SSE 连接,会重复消费/重复计费。方案:用 BroadcastChannel 或 Web Worker 做"单连接共享"——设计一个标签页(或 Worker 中)作为"唯一消费者"建立 SSE 连接,再把事件通过 BroadcastChannel 广播给其他标签页;或让服务端按"会话 + 客户端组"去重,同一会话只允许一个活跃流。同步用"最后事件序号/版本"做幂等:各标签页记录已处理的事件序号,广播时携带序号,重复事件跳过。关闭某标签页不中断共享流(由 Worker 或存活标签页维持)。如此避免重复发起连接与重复处理。

多标签页重复消费的根源是"多个连接各消费一份"。用单连接 + 广播分发(BroadcastChannel/Worker)+ 事件序号幂等,实现共享与去重。

#
★★

30. EventSource 与 Fetch 流在取消语义上为何不同,如何在 React 中统一封装

EventSource 与 Fetch 流在取消语义上为何不同?如何在 React 中统一封装?

  • EventSource 与 Fetch 流的取消语义
  • React 统一封装
  • 取消清理

EventSource 的取消是 close()——关闭连接,但没有"请求级取消"的概念,且自动重连;Fetch 流的取消用 AbortController——精确 abort 一个请求,关闭流并触发 abort 事件,无自动重连。二者取消语义不同:EventSource 是"关闭并停止",Fetch 是"abort 请求"。在 React 中统一封装:封装一个 useStream hook,内部用 AbortController 管理 Fetch 流,或对 EventSource 封装 close 逻辑;在组件卸载或依赖变化时调用取消(abort/close),并清理监听器与定时器;统一暴露 cancel() 与连接状态,把"取消"与"清理"绑定到组件生命周期,避免内存泄漏与重复流。

取消语义差异源于协议:EventSource 用 close、Fetch 用 abort。统一封装即在 hook 中抽象 cancel(),并绑定组件生命周期做清理,屏蔽底层差异。

#
★★

31. 如何统一建模文本 delta、对象 patch、工具参数 delta、工具结果、完成原因和 usage 事件

如何统一建模文本 delta、对象 patch、工具参数 delta、工具结果、完成原因和 usage 事件?

  • 统一事件模型
  • 各类增量的事件化
  • 类型与状态

统一建模为一个"事件 + 类型 + 载荷"的模型:{ type, id, seq, data }。类型包括:text_delta(文本增量,载荷为字符串片段)、object_patch(结构化对象的部分更新,载荷为 JSON patch / 字段路径值)、tool_call_delta(工具参数增量,按 call_id 聚合,载荷为参数片段)、tool_result(工具执行结果,载荷为结果 + 状态)、finish(完成原因,载荷为 reason:stop/length/tool_calls/error)、usage(token 统计,载荷为 prompt/completion/total)。所有事件带统一 id 与 seq 用于排序去重。文本用增量、对象用 patch、工具用 call_id 聚合、完成/用量用统一枚举,构成稳定的统一事件模型。

统一建模的关键是"把异构增量收敛到类型化事件"。文本/对象/工具/完成/用量各自映射到统一事件类型,带 id 与 seq 保证顺序与去重,下游只消费统一模型。

#
★★

32. 模型只返回半个 JSON 或转义字符跨包时,为何不能逐块 JSON.parse,应在何种边界校验

模型只返回半个 JSON 或转义字符跨包时,为何不能逐块 JSON.parse?应在何种边界校验?

  • 逐块 JSON.parse 的问题
  • 流式结构化输出的边界
  • 校验时机

流式结构化输出中,JSON 可能以"半个"状态出现(字段未闭合、转义字符跨多个数据块),若逐块 JSON.parse,会因不完整而失败,产生大量无效解析。因此不能逐块 parse,而应在"完整边界"校验:等到收到 finish/[DONE] 或检测到 JSON 结构闭合(如括号/引号匹配)时,才做整体 JSON.parse 与 Schema 校验。转义字符跨包时,需先完成字节/字符拼接(解码)再解析,避免在中间态解析。前端可对"可预览字段"做部分解析(尽力而为),但"正式校验"必须在完整 JSON 上执行。

逐块 parse 会把"进行中"误判为"已损坏"。正确做法是"先拼完整再校验",正式校验在完整边界(finish/结构闭合)执行,中间态只做预览。

#
★★

33. 并行工具调用的参数片段交错到达时,如何按 call ID 聚合并维持每个调用的状态机

并行工具调用的参数片段交错到达时,如何按 call ID 聚合并维持每个调用的状态机?

  • 工具调用参数片段交错
  • 按 call ID 聚合
  • 调用状态机

多个工具调用的参数片段可能交错到达(call A 的片段、call B 的片段、再 call A 的片段)。处理:用 call ID 作为聚合键,为每个调用维护独立缓冲与状态机(如 started → accumulating → complete)。收到片段时,按 call ID 路由到对应缓冲,追加参数片段;当某调用收到"完成"标记(工具参数结束)时,将其状态置为 complete,再整体解析参数并执行。状态机保证每个调用"参数完整后才执行",避免因交错而把 A 的参数混入 B。并发调用各自独立推进,互不干扰。

交错的核心问题在于"多个调用共享同一流的顺序"被打乱。按 call ID 分缓冲 + 状态机,每个调用独立累积、完整后再执行,是正确处理交错的手段。

#
★★

34. 流式结构化输出在“半 JSON”状态时应如何向 UI 暴露

流式结构化输出在"半 JSON"状态时应如何向 UI 暴露?

  • 半 JSON 状态的 UI 暴露
  • 部分可见 vs 最终一致
  • 渲染策略

半 JSON 状态(字段未闭合、结构不完整)时,向 UI 暴露的方式是"尽力而为的部分渲染 + 状态标记":解析出已完成且可用的字段,渲染为临时组件(如已生成的标题、已确定的字段),未完成的字段显示为加载/占位;同时用"partial(进行中)"或"final(已定稿)"状态标记,让 UI 知道当前是临时内容。最终校验失败时,把 partial 渲染回滚为错误/占位。要向 UI 暴露"部分可见"但要让 UI 明确"这是部分、最终可能变化",避免把半 JSON 当最终结果。结构化半 JSON 的状态机:partial → finalpartial → error

半 JSON 的 UI 暴露原则是"部分可见 + 状态标记 + 可回滚"。渲染已完成字段、对未完成占位、用 partial/final 标记,保证用户看到进展且最终一致。

#
★★

35. 多个工具调用并行流式到达时,参数片段可能交叉到达(call_id A 的部分、call_id B 的部分),应如何按 call ID 缓冲重组与校验?

多个工具调用并行流式到达时,参数片段可能交叉到达(call_id A 的部分、call_id B 的部分),应如何按 call ID 缓冲重组与校验?

  • 并行工具调用的交叉参数
  • 按 call ID 缓冲重组
  • 重组后校验

并行工具调用时,各调用的参数片段交错到达。处理:建立一个"按 call ID 索引的缓冲表",每个 call 维护独立参数累积;收到片段时按 call_id 路由到对应缓冲追加;当某 call 收到完成标记后,把该缓冲的完整参数做 JSON 解析与 Schema 校验,通过后再执行。校验要针对"重组后的完整参数"而非零散片段。对每个 call 维护状态机(accumulating → complete → validated → executed),并保证交错不串位。缓冲要有上限与超时,防止参数无限累积。

交叉参数的正确重组依赖"按 call ID 隔离缓冲"。每个调用独立累积、完成后再整体解析校验执行,并用状态机与超时兜底,避免串位与无限累积。

#
★★

36. 工具结果回传到模型再生成回答时,中间空白(思考时间)应如何处理才能让用户感知是“思考”而非“卡死”

工具结果回传到模型再生成回答时,中间空白(思考时间)应如何处理才能让用户感知是"思考"而非"卡死"?

  • 工具调用后的思考空白
  • 用户感知的反馈
  • 思考状态展示

工具执行后、模型重新生成回答前,有一段"思考/等待"时间(无文本输出),用户可能误以为卡死。处理:前端在工具调用完成后立即显示"思考中/正在处理"状态(如打字指示器、工具结果摘要、进度动画),让用户知道系统在继续工作;不断更新"已调用工具、正在进行下一步"的提示;设定超时,若超过阈值仍无输出则提示"仍在处理"或提供重试。关键是把"工具结果 → 模型思考 → 生成回答"的中间态可见化,不让用户面对空白。UI 用状态机区分"等待工具"与"等待模型回复"。

思考空白是真实存在的延迟,处理关键在于"让用户看到状态"。用思考指示器、工具进度与超时提示把空白变成可理解的"进行中",避免"卡死"误判。

#
★★

37. 流式 Structured Outputs 如何向客户端暴露“部分可见、最终已校验”两种状态

流式 Structured Outputs 如何向客户端暴露"部分可见、最终已校验"两种状态?

  • 部分可见与最终已校验
  • 状态暴露机制
  • 前端处理

流式 Structured Outputs 向客户端暴露两种状态:一是"部分可见"(partial)——边生成边暴露已形成的字段,让前端增量渲染;二是"最终已校验"(final)——生成结束后,服务端完成 Schema 校验,把完整、合法、已校验的结果作为 final 事件下发。协议上可区分:partial 事件携带增量/未定稿字段,final 事件携带完整已校验结果,带版本号保证前端只认最新。前端渲染 partial 作为临时预览,收到 final 后替换为可信结果;若 final 校验失败,则发送 error 并回滚 partial。两种状态明确区分,让前端"预览可滚动、最终可依赖"。

两种状态的分界是"是否完成校验"。partial 用于流畅预览,final 用于可信交付,用事件类型区分并在 final 失败时回滚,是"部分可见 + 最终一致"的标准模型。

#
★★

38. 工具开始、等待审批、执行中、成功和失败应展示哪些信息,哪些参数和内部错误必须脱敏

工具开始、等待审批、执行中、成功和失败应展示哪些信息?哪些参数和内部错误必须脱敏?

  • 工具状态各阶段的信息展示
  • 参数与内部错误的脱敏
  • 展示透明度与安全

工具各阶段展示:开始(显示工具名与用途)、等待审批(显示谁在审批、可取消)、执行中(显示进度/操作摘要)、成功(显示结果摘要与可验证信息)、失败(显示错误分类与重试/降级选项)。必须脱敏的信息:参数中的敏感值(密码、Token、API Key、个人身份信息)、内部错误详情(堆栈、内部路径、数据库信息、Provider 原始错误)——这些对用户不可见,只记录在日志。展示用"摘要 + 脱敏":参数用 *** 掩码或仅显示非敏感字段,错误用"可重试性/原因分类"而非原始堆栈。透明展示状态,但敏感信息只进审计日志。

工具状态展示要"透明可理解"又不泄露敏感信息。阶段信息给用户可理解的进度,参数与内部错误脱敏,摘要式展示 + 审计日志分离,是安全与透明度的平衡。

#
★★

39. 连接中途失败后,如何区分可安全回放的文本事件与不可重复执行的副作用工具

连接中途失败后,如何区分可安全回放的文本事件与不可重复执行的副作用工具?

  • 可回放事件 vs 副作用工具
  • 幂等性
  • 回放策略

连接失败后重连时,需区分"可安全回放"与"不可重复执行"的事件:文本事件(text_delta)是幂等的,重放只会重复展示文本,安全;副作用工具(写库、发邮件、支付)不可重复执行,重复执行会造成重复副作用。区分方法:事件类型标注"是否幂等/是否有副作用"——纯文本、只读查询、工具结果可重放;有副作用的工具调用需幂等键(idempotency key),执行前检查是否已执行过,重放时只返回已记结果而不重新执行。服务端记录"已执行副作用"的清单,重连/重放时跳过已执行副作用、只重放文本与查询结果。前端对该类事件不做盲目重放。

回放安全性的分界是"副作用"。文本幂等可重放,副作用工具需幂等键 + 服务端去重,重放时跳过已执行副作用,避免重复副作用。

#
★★

40. 流式事件中的错误(tool_error、rate_limit、timeout)应如何按可重试性分类,前端展示与自动重试策略如何与之对齐?

流式事件中的错误(tool_error、rate_limit、timeout)应如何按可重试性分类?前端展示与自动重试策略如何与之对齐?

  • 错误的可重试性分类
  • 前端展示与重试对齐
  • 重试策略

错误按可重试性分类:可重试——rate_limit(限流,等待后重试)、timeout(超时,重试)、瞬时的 5xx;不可重试——tool_error(业务参数错误,重试无意义)、权限拒绝、认证失败、4xx 业务错误。分类贯穿前端展示与自动重试:可重试错误展示"重试中/稍后自动重试",并有退避策略(指数退避 + 抖动);不可重试错误展示"无法完成 + 原因 + 修正建议",不自动重试。自动重试策略与分类对齐:只有可重试类才自动重试,且设置最大重试次数与退避;不可重试类直接降级或提示用户。前端要区分"可重试"与"不可重试"的展示,避免误导用户持续等待。

错误分类是"重试决策"的基础。可重试(限流/超时/瞬时)自动退避重试,不可重试(业务/权限/4xx)直接展示原因并降级,前端展示与重试策略按分类对齐。

#
★★

41. 前端在流式过程中如何区分“模型在思考”和“模型已放弃并降级”,提示文案如何随状态变化

前端在流式过程中如何区分"模型在思考"和"模型已放弃并降级"?提示文案如何随状态变化?

  • 思考与放弃的区分
  • 状态提示文案
  • 状态机

前端区分"思考中"与"已放弃降级":思考中表现为"连接活跃、可能有工具调用/中间事件、等待最终输出";已放弃/降级表现为"收到 finish(reason=error) 或 error 事件、连接关闭、明确告知无法完成"。前端用状态机(thinking → generating → done/error/fallback)管理,根据事件类型与 reason 切换状态。提示文案随状态变化:思考中显示"正在思考…/正在处理…";降级时显示"未能完成,已降级为…"并说明原因与补救(重试/简化)。关键是不把"思考中"误标为"已放弃",也不把"已放弃"当成"仍在思考"——用明确事件信号区分。

区分靠"事件信号"而非猜测。思考中依赖活跃流与中间事件,放弃依赖 error/finish 事件;文案按状态机切换,让用户认知与真实状态一致。

#
★★

42. 流式结束时如何确认所有工具都已回收,避免悬挂任务或未关闭的数据库连接

流式结束时如何确认所有工具都已回收,避免悬挂任务或未关闭的数据库连接?

  • 流结束时的资源回收
  • 悬挂任务与未关闭连接
  • 回收确认

流结束时需确认所有工具/资源已回收:流事件(finish/error/超时)触发后,服务端执行"回收检查"——遍历本次运行创建的工具调用,确认每个都已结束(成功/失败/已取消),未完成的调用强制终止或标记;对资源(数据库连接、文件句柄、外部请求、定时器)在执行 finally 中确定性关闭;对异步悬挂任务(如后台线程、长事务)设置超时与取消,超时即终止。用"跟踪 + 清理"机制:运行时维护"活跃资源/任务"清单,流结束时统一清理,并记录谁未回收(告警)。避免"流结束但任务仍在跑"的泄漏。

流结束不等于资源自然回收。用"活跃任务/资源清单 + 统一清理 + 超时兜底"确保结束时全部回收,悬挂任务与未关闭连接才能被确定性清理。

#
★★

43. SSE 心跳(comment 注释行)应多久发一次,频率过低会被代理关闭,频率过高浪费带宽如何权衡

SSE 心跳(comment 注释行)应多久发一次?频率过低会被代理关闭、频率过高浪费带宽,如何权衡?

  • 心跳频率的权衡
  • 代理超时阈值
  • 带宽与稳定性

心跳频率要在"代理超时阈值"与"带宽开销"之间权衡:频率过低,若超过代理空闲超时(如 nginx 默认 60s)会被关闭;频率过高则浪费带宽。实践做法:心跳间隔设为"略小于代理/网关空闲超时"(如代理超时 60s,则心跳 30s-45s 一次),并留出余量以应对网络抖动;空闲时才有心跳(有数据时无需额外心跳),繁忙时自然通过数据刷新。也可用"自适应":根据代理配置调整心跳间隔。整体权衡是"在不被关闭的前提下,尽量降低心跳频率"。

心跳是"保活信号",其频率的下限由代理超时决定,上限由带宽决定。设为代理超时的一半左右并仅在空闲时发送,是兼顾稳定性与开销的平衡点。

#
★★

44. 流式响应中插入的引用(citation)链接如何在最终已校验阶段才显示可信标识

流式响应中插入的引用(citation)链接如何在最终已校验阶段才显示可信标识?

  • 引用的流式呈现
  • 最终校验阶段
  • 可信标识时机

流式过程中 citation 可能不断出现或变化,若立即显示"可信链接"可能过早暴露未定稿内容。处理:流式阶段只显示"占位/待定"的引用标记(如编号、灰显),不显示可点击的可信链接;等到最终校验阶段(finish 事件、服务端完成引用校验——来源可达、权威性、URL 合法)后,才把引用替换为可信标识(可点击、带来源分级、可信度标记)。这样引用在"已确认"后才呈现为可信,避免流式中未验证链接误导用户。若最终校验失败,引用降级为"不可信"或移除。

引用的可信性与"是否已校验"绑定。流式阶段用占位,最终校验通过后才显示可信链接,校验失败则降级,避免把未验证引用当可信。

#
★★

45. 为何流式事件协议不应包含大字段(如完整 base64),应改成“引用 + 后续 GET”

为何流式事件协议不应包含大字段(如完整 base64)?应改成"引用 + 后续 GET"?

  • 大字段对流式协议的影响
  • 引用 + 后续 GET 模式
  • 协议轻量化

流式事件协议不应包含大字段(如完整 base64 图片、大文本),原因:大字段会使单个事件膨胀、阻塞事件流、占用缓冲、增加延迟与带宽,还可能超过事件长度限制;且大字段无法"增量"。应改为"引用 + 后续 GET":事件只携带一个小引用(资源 ID、URL、大小),前端需要时再通过独立 GET 请求获取完整内容。这样事件流保持轻量、事件边界清晰、延迟稳定,大内容按需加载。适用于图片、大文件、长文档等。

大字段是"事件流的毒药"。事件只放引用,内容按需 GET,让流保持轻快,是流式协议的设计原则。

#
★★

46. 流式与工具调用同时存在时,UI 进度条、计时器、可用性指示如何让用户清楚当前阶段

流式与工具调用同时存在时,UI 进度条、计时器、可用性指示如何让用户清楚当前阶段?

  • 流式与工具并存的阶段指示
  • 进度条/计时器/可用性指示
  • 阶段状态机

流式与工具调用并存时,UI 需清晰展示当前阶段:用阶段状态机(生成中 → 工具调用 → 生成中 → 完成)区分;进度条反映"当前阶段"而非整体百分比(工具阶段显示工具执行进度,文本阶段显示生成进度);计时器显示"本阶段已耗时"或"总耗时",让用户感知进展;可用性指示(如"正在调用查询工具""正在生成回答")让用户知道系统在做什么。工具调用时显示工具名与结果摘要,回到文本生成时显示打字指示。让用户随时知道"现在在哪个阶段、进行到哪、还要多久"。

并存时的关键是"阶段可见"。用阶段状态机 + 本阶段进度 + 计时 + 可用性文案,让用户理解当前是工具调用还是文本生成,避免困惑。

#
★★

47. 如何把流式事件流转换为统一日志格式(JSONL)以便回放和审计

如何把流式事件流转换为统一日志格式(JSONL)以便回放和审计?

  • 流事件转 JSONL 日志
  • 回放与审计
  • 结构化日志

把流式事件流转换为统一 JSONL 日志:为每个事件生成一行 JSON,包含统一字段——request_idsession_idmessage_idseq(序号)、ts(时间戳)、type(事件类型)、payload(事件载荷)、trace_id。按事件的 seq 顺序写入,保证可回放。审计用途:日志保存完整事件序列(含工具调用、中间推理、错误、用量),可回放时按 request_id 重放事件流,还原当时的输出过程;审计时查询敏感操作(工具调用、错误)的完整记录。JSONL 便于按行解析、分布式采集、与 Trace 系统关联。要注意敏感字段脱敏后再写日志。

JSONL 是"流事件的统一日志载体"。每行一个带统一字段的事件,按 seq 排序,可回放、可审计、可关联 Trace,并脱敏敏感信息。

#
★★

48. 流式与异步任务切换的边界判断(同步流结束 vs 异步任务 ID 返回)应依据什么信号,切换后前端如何平滑过渡?

流式与异步任务切换的边界判断(同步流结束 vs 异步任务 ID 返回)应依据什么信号?切换后前端如何平滑过渡?

  • 同步流与异步任务的边界信号
  • 切换信号
  • 前端平滑过渡

判断"同步流结束"与"转为异步任务"的信号:同步流以 finish/[DONE]/连接正常关闭表示结束;转异步任务时服务端返回 task_id(或 continue 事件)表示"剩余部分转为异步任务"。切换信号要明确:收到 task_id 事件即表示"流式结束,后续走异步轮询/订阅"。前端平滑过渡:收到 task_id 后,关掉当前流、保留已展示内容,切换到"任务状态"组件(轮询进度或订阅任务事件),用统一的消息 ID 衔接,避免内容断裂或重复;切换时显示"剩余内容正在生成"过渡提示。前端状态机支持 streaming → task 迁移。

切换的边界是"明确信号"(finish 或 task_id)。前端收到 task_id 后平滑迁移到异步模式,保留已展示内容并衔接,避免断裂与重复。

#
★★

49. 前端如何安全地做幂等合并并防止字段重复触发副作用

前端如何安全地做幂等合并并防止字段重复触发副作用?

  • 幂等合并
  • 防止重复副作用
  • 合并策略

前端幂等合并:用事件序号/字段版本做"去重合并"——同一字段的多次更新,只保留最新版本,重复事件(同 seq)跳过;对结构化对象用"按字段 patch + 版本覆盖"而非简单追加,避免重复字段。防止字段重复触发副作用:副作用(如发起请求、提交)与"渲染"解耦——只有"最终确认"状态才触发副作用,幂等合并过程不触发;对幂等键(idempotency key)做去重,重复提交返回同一结果。合并时用"状态机 + 版本号":前端只对"已确认最终版"执行副作用,中途的 partial 合并不触发。这样保证合并幂等、副作用不重复。

幂等合并依赖"序号/版本去重 + 字段覆盖",副作用防重复依赖"只有最终确认才触发 + 幂等键"。二者分离,避免合并过程误触发副作用。

#
★★

50. 流式结构化结果与工具事件面临哪些安全风险与攻击面,应通过哪些防护手段(鉴权、签名、限流、内容审计)

流式结构化结果与工具事件面临哪些安全风险与攻击面?应通过哪些防护手段(鉴权、签名、限流、内容审计)?

  • 流式结果与工具事件的安全风险
  • 攻击面
  • 防护手段

安全风险与攻击面:流式结果可能被注入恶意内容(提示注入、XSS)、被篡改(伪造事件);工具事件可能被伪造(恶意请求工具调用)、被越权(未授权调用敏感工具)、被滥用(限流绕过);流本身可能被劫持(窃听、中间人)。防护手段:鉴权——对每个 SSE/WS 连接与工具调用做鉴权(用户身份 + 权限校验);签名——对事件或工具调用做签名/校验,防止伪造与篡改;限流——对连接数、工具调用频率、token 消耗做限流,防止滥用与资源耗尽;内容审计——对事件流、工具调用、结果做审计日志,记录谁做了什么,用于事后追溯。此外对输出做净化防注入、对敏感操作做二次确认。

流式与工具事件是"新的攻击面"。鉴权、签名、限流、内容审计四层 + 输出净化 + 敏感操作确认,构成对流式安全性的纵深防护。

#

51. 客户端停止生成时,取消信号怎样传播到 Java 服务、Provider 请求和计费链路

客户端停止生成时,取消信号怎样传播到 Java 服务、Provider 请求和计费链路?

  • 取消信号的传播链路
  • Java 服务取消
  • Provider 请求与计费

客户端停止后取消信号传播链路:前端调用 AbortController/关闭连接 → 通知 Java 服务(通过取消请求或连接断开回调)→ Java 服务把取消传播到 Provider 请求(调用 Provider 的取消/中止 API,或关闭流连接)→ Provider 停止生成并返回已用 token → 计费按"实际消耗"(Provider 确认的已生成 token)而非计划量。Java 侧用异步取消(如 Future cancel、可取消的响应式订阅、connection close 回调)触发;传播时传递 request_id 与任务 ID,确保取消到正确的运行。计费链路要记录"取消时已消耗的 token",避免取消后仍按全额计费。

取消的传播是"前端→服务→Provider→计费"的端到端链路。关键是把取消信号可靠传递到 Provider 停止生成,并按实际消耗计费。

#

52. Java SseEmitter 与 Servlet 3.1 异步支持的超时、注册、心跳和异常回调生命周期有何陷阱

Java SseEmitter 与 Servlet 3.1 异步支持的超时、注册、心跳和异常回调生命周期有何陷阱?

  • SseEmitter 生命周期
  • 超时/心跳/异常回调
  • 陷阱与规避

SseEmitter 的陷阱:超时——默认有超时(可通过 setTimeout 设置),超时后连接关闭,需用心跳或定期刷新避免超时;注册——需在请求线程内尽早注册(AsyncContext 的异步启用),晚了会报错;心跳——需主动发送注释/空事件保持连接,否则代理/服务端超时关闭;异常回调——onCompletion/onTimeout/onError 回调需正确清理资源(关闭 emitter、撤销注册),避免泄漏;complete() 后不能再 send,否则抛异常。Servlet 3.1 异步支持要求容器开启异步、设置超时、处理异步异常。陷阱在于生命周期管理不当(未清理、超时未处理、过早 complete)导致连接泄漏或异常。

SseEmitter 的陷阱集中在"生命周期管理"。超时设置与心跳、及时注册、异常回调清理、complete 后不 send,都是避免连接泄漏与异常的关键。

#

53. 流式多模态(图像、音频)如何在文本流中插入并保持时间线对齐(先文本描述、再图像、再继续文本)

流式多模态(图像、音频)如何在文本流中插入并保持时间线对齐(先文本描述、再图像、再继续文本)?

  • 多模态内容的文本流插入
  • 时间线对齐
  • 播放顺序

流式多模态内容需要在文本流中插入而保持时间线对齐。处理:把图像/音频作为"独立事件"插入文本流,带统一的"位置序号(seq)"与"时间戳",保证与文本的事件顺序一致;模型在"先文本描述、再图像、再继续文本"时,事件序列为:文本 delta → 图像事件 → 文本 delta,前端按 seq 顺序渲染/播放,保证时间线对齐。图像/音频用"引用 + 独立加载"(避免大字段阻塞),但事件位置要按 seq 与文本严格有序。前端用"时间线渲染器"统一消费文本与媒体事件,按序播放,保证不错位。

多模态对齐的核心是"统一事件流 + 统一序号"。文本与媒体都作为带 seq 的事件按序下发,前端按 seq 渲染,才能保证"先文本、再图像、再文本"的顺序正确。

#

54. 如何把流式事件与 Trace ID、时间戳和序号关联,用于端到端延迟与缺包排查

如何把流式事件与 Trace ID、时间戳和序号关联,用于端到端延迟与缺包排查?

  • 流事件与 Trace 关联
  • 时间戳与序号
  • 延迟与缺包排查

把每个流事件与 Trace ID、时间戳、序号关联:事件携带 trace_id(端到端链路标识)、ts(服务端时间戳)、seq(事件序号)。延迟排查:对比"服务端生成时间戳"与"客户端接收时间戳",计算网络延迟与端到端延迟;缺包排查:用 seq 检测缺口——客户端发现 seq 跳跃即判定缺包,触发重连或补拉。Trace ID 关联到日志、Provider 调用、工具调用,全链路可追踪。前端记录接收时间戳,服务端记录生成时间戳,二者关联即可定位延迟在哪个环节、缺包发生在哪段。

关联的三要素是"trace_id(链路)、ts(时延)、seq(缺包)"。三者绑定到每事件,才能做端到端延迟分析与缺包检测。

#

55. 流式 Markdown 解析器(Streamdown、micromark、markdown-it 流式模式)如何保持状态增量解析,避免全量重解析导致抖动?

流式 Markdown 解析器(Streamdown、micromark、markdown-it 流式模式)如何保持状态增量解析,避免全量重解析导致抖动?

  • 流式 Markdown 解析器
  • 状态增量解析
  • 避免全量重解析

流式 Markdown 解析器通过"维护解析状态 + 增量处理"避免全量重解析:解析器维护当前块级状态(是否在代码块、表格、列表等),每次新 token 到达时,只在其基础上增量解析,而不是重新解析全量文本。Streamdown、micromark 的流式模式、markdown-it 的流式扩展都支持"token 流式输入",维护未闭合状态。未闭合的块"暂存",闭合后产生完整节点。这样只更新受影响的部分,避免每次新 token 触发全量 Markdown→DOM 重渲染带来的抖动与性能开销。适合长文本流式渲染。

增量解析的要点是"状态持久化 + 局部更新"。解析器记住未闭合块状态,新 token 只增量推进,避免全量重解析,从根源消除抖动。

#

56. Vercel AI SDK、Assistant UI 和 Streamdown 应按哪些协议、定制与安全需求选型

Vercel AI SDK、Assistant UI 和 Streamdown 应按哪些协议、定制与安全需求选型?

  • 各库的定位与协议
  • 定制需求
  • 安全需求

选型需按协议、定制与安全需求:Vercel AI SDK——提供统一的流式协议(data 流、tool call、usage 事件)、前后端封装、多 Provider 抽象,适合快速搭建完整 AI 流栈,但定制底层协议受其约束;Assistant UI——提供对话 UI 组件(消息、工具调用展示),适合快速构建聊天界面,但样式与交互定制受限;Streamdown——专注流式 Markdown 解析,适合需要精细控制流式 Markdown 渲染的场景,但只是解析层面。选型依据:协议——是否需要统一协议/Provider 抽象(用 AI SDK);定制——是否需要深度定制 UI/协议(则自建或选可定制库);安全——流式输出净化、鉴权、工具安全是否需自行掌控(选型应考虑可扩展安全层)。通常组合使用:AI SDK 管协议与流,Streamdown 管 Markdown 渲染,UI 组件自建或选 Assistant UI。

选型是"协议抽象、定制自由度、安全可控"的权衡。需要完整栈用 AI SDK,需要精细 Markdown 用 Streamdown,UI 用组件库,而安全层要能扩展。

#

57. 流式 Markdown 解析器(Streamdown)应如何处理未闭合表格避免错位

流式 Markdown 解析器(Streamdown)应如何处理未闭合表格避免错位?

  • 未闭合表格的流式处理
  • 避免错位
  • 缓冲策略

流式 Markdown 中表格可能未闭合(分隔行未完成、列数在变化、表格行还在输入)。Streamdown 处理未闭合表格:维护"表格进行中"状态,把未闭合的表格行暂存缓冲,不立即渲染为最终表格;等表格结构趋于闭合(分隔行出现、列数稳定)后再渲染为表格。内联单元格可能跨块,需缓冲累积。为避免错位,对"可能闭合"的表格用"尾缓冲+延迟渲染"——积攒几行直到确认表格边界,再一次性渲染,避免每行都重排导致列错位。未确定时渲染为占位/普通文本。

表格错位的根源是"列结构未定就渲染"。未闭合表格用缓冲暂存、延迟到边界稳定再渲染,避免逐行重排造成列错位。