反向代理默认启用缓冲
Nginx 默认开启代理缓冲(proxy buffering),会攒够一段再转发。流式传输必须显式禁用:proxy_buffering off、proxy_cache off,并将读超时放宽到 120 秒。服务端同时返回 X-Accel-Buffering: no 头,构成两层防护。否则浏览器仍要等整段生成结束才收到首字节。
第 4 课的问答接口要等整段回答生成完毕才一次性返回。本课改为服务端逐段推送:学生提交后显著降低首字延迟(time to first token),无需等待整段生成完毕。推送采用 SSE(Server-Sent Events,服务器推送事件)。
| 维度 | SSE | WebSocket |
|---|---|---|
| 方向 | 服务端到浏览器的单向推送 | 全双工,双方随时互发 |
| 承载 | 普通 HTTP 响应,长连接不关闭 | 先握手升级协议,之后不再是 HTTP |
| 鉴权 | 沿用会话 Cookie 与既有中间件 | 握手阶段可带 Cookie,之后需自行维护会话 |
| 断线恢复 | 浏览器自动重连 | 需自行实现重连与续传 |
| 实现成本 | 浏览器只收不发,提交提问需另发一条普通 POST | 多一条长连接与一套心跳机制 |
每一帧由 event: 与 data: 两行组成,以一个空行结束。data 是 JSON。服务端按用途分四类事件。
| 事件 | 何时推送 | 载荷 |
|---|---|---|
meta | 检索完成之后、生成之前,最先到达 | 状态、本次命中的技能、引用列表 |
delta | 生成过程中多次推送 | 本次新增的文本片段,不是累计全文 |
done | 助手消息落库之后 | 该条消息的编号 |
error | 流已开始但后续失败 | 通用错误文案 |
Nginx 默认开启代理缓冲(proxy buffering),会攒够一段再转发。流式传输必须显式禁用:proxy_buffering off、proxy_cache off,并将读超时放宽到 120 秒。服务端同时返回 X-Accel-Buffering: no 头,构成两层防护。否则浏览器仍要等整段生成结束才收到首字节。
Go 的 ResponseWriter 同样有缓冲。每写入一帧必须调用 http.Flusher 的 Flush 把缓冲区刷到连接,否则片段滞留在服务端。响应头另需 Content-Type: text/event-stream、Cache-Control: no-cache 与 Connection: keep-alive。
服务端在第一个事件产生时才写响应头并返回 200——因为检索是否命中要等检索阶段完成才能确定。此后再出错已无法返回 4xx 或 5xx,只能以 error 事件收尾。检索前即可判定的失败(会话不存在、检索服务不可用)才返回真正的 404 与 503。
EventSource 只能发起 GET,而提交提问要带 JSON 正文。因此本课用 fetch 发起 POST,再读 response.body 的流,自己按 event: 与 data: 行解帧。