HTTP

SSE 流式聊天的端到端检查

聊天接口“返回了完整答案”和“用户能逐字看到答案”是两种不同的验收结果。SSE 链路经过模型服务、应用服务、反向代理和浏览器,任何一层缓冲都可能让本应逐块到达的内容在最后一次性出现。

聊天接口“返回了完整答案”和“用户能逐字看到答案”是两种不同的验收结果。SSE 链路经过模型服务、应用服务、反向代理和浏览器,任何一层缓冲都可能让本应逐块到达的内容在最后一次性出现。

先约定事件语义

SSE 使用 text/event-stream,每条事件以空行结束。与其让前端猜测某个特殊字符串是否表示结束,不如明确事件类型:

event: delta
data: {"text":"你好"}

event: done
data: {}

delta 只追加新内容,done 表示正常结束,error 表示流开始后的失败。鉴权和明显无效的参数应在开始发送流之前检查,此时仍可以返回正常的 HTTP 错误;一旦响应头发出,后续错误就应通过流内事件表达。

原生 EventSource 适合 GET 订阅。如果聊天请求需要 POST 请求体、特定鉴权头或上传内容,通常使用 fetch 读取响应流,并自行解析 SSE 事件。不要把一次网络读取当作一条完整事件:UTF-8 字符、data: 行甚至事件之间的空行都可能被拆到不同数据块中。

服务端与代理逐层检查

服务端应在取得模型增量后尽快转发,并在客户端断开时取消上游工作。不要先把所有增量收集成一个字符串,最后再包装成流式响应。长时间没有内容时,可按基础设施要求发送心跳,避免空闲超时。

反向代理可能缓存响应。以 Nginx 为例,流式路由需要检查 proxy_buffering、压缩、读超时和上游传输方式;也可以通过应用响应头 X-Accel-Buffering: no 告知 Nginx 不缓冲该响应。具体配置要以实际代理链为准,不能只看应用本地直连结果。

浏览器端维护消息状态

客户端至少区分“连接中、生成中、正常完成、失败、用户取消”。流中收到 delta 时更新当前消息,而不是为每个数据块创建一条新消息;收到 done 后关闭读取,提交最终内容。用户主动取消时中止请求,并保留已经收到的文本,便于重新提问或继续操作。

展示 Markdown、代码和公式时,增量文本可能处于未闭合状态。渲染器需要容忍暂时不完整的语法,或者降低渲染频率;不能因为某一帧内容还缺结束标记就抹掉先前文本。长回复还要检查滚动位置,避免新内容把用户正在阅读的开头推走。

验收真正的渐进到达

用能够显示接收时间的客户端记录至少三次增量的到达时刻,再分别测试应用直连和经过代理的地址。如果直连有间隔、经过代理却同时到达,优先查代理缓冲;如果两者都一次性到达,检查模型 SDK、服务端迭代和响应包装。

测试还应覆盖模型中途报错、网络断开、用户取消、超时和空响应。最终文本相同并不能证明流式体验正确,关键是事件顺序、到达间隔,以及异常后界面是否能恢复操作。