Skip to content

L01-04 流式输出 SSE ​

全局中心内容:流式不改变总时长,它改变的是首字节时间和可中断性。 全局讲解主线:同步有多难受 → 流式怎么流出来 → 写出接口 → 证明中断真的省了钱。


P1 · 产出页 ​

中心内容:打字机效果 + 断开连接立刻停止烧钱。

  • 页面内容

    • GET /api/chat/stream 返回 SSE
    • 浏览器逐字显示
    • 客户端断开 → 服务端 cancel
  • 讲解技巧

    • 先放最终效果:播 5 秒打字机录屏,一句话不说。视觉冲击比文字描述强十倍。
    • 明确说「这一节只要 30 行代码」——降低心理门槛。
  • 时长:20s


P2 · 痛点页:12 秒的空白 ​

中心内容:同步模式的真正损失是钱,不是体验。

  • 页面内容

    • 首 Token 0.8s / 完整生成 12s
    • 用户以为卡死 → 刷新 → 又烧一次
    • 同步下中途放弃,钱照扣
  • 讲解技巧

    • 讲一个具体的钱的故事:「用户点了 5 次刷新,每次服务端都完整生成了一遍,5 倍费用,用户什么都没看到」。
    • 这页不要讲技术,只讲损失。技术留到 P3。
  • 时长:2min


P3 · 原理图:分片是怎么来的 ​

中心内容:模型吐的是分片,Spring AI 转成 Flux。

  • 页面内容

    • 模型 chunk「你」→ Flux<String> → SSE: data: 你
    • [DONE] → onComplete → EventSource 关闭
  • 讲解技巧

    • 必须纠正一个误解:「流式不会让模型变快」。总时长几乎一样。讲清楚这点,否则学员后面做性能优化时会找错方向。
    • 类比迁移:「就像下载大文件时的进度条——文件不会变小,但你不用干等」。
  • 时长:2min 30s


P4 · 选型表:SSE vs WebSocket ​

中心内容:95% 的场景 SSE 就够,别上 WebSocket。

  • 页面内容

    • SSE:单向、HTTP 穿透代理好、自动重连、极简
    • WebSocket:双向、需握手、要自己做心跳重连
    • 判据:需要「客户端在生成中途反向控制」才用 WS
  • 讲解技巧

    • 主动劝退:明说「大多数声称要 WebSocket 的项目,其实只是要打字机效果」。这种反向建议在课程里很加分。
    • 埋下一节课的钩子:真需要打断的场景下一节讲。
  • 时长:2min


P5 · 编码:Controller 三处必须写对 ​

中心内容:produces / .stream() / doFinally,缺一不可。

  • 页面内容

    • produces = TEXT_EVENT_STREAM_VALUE
    • .stream() 而非 .call()
    • .doFinally(signal -> ...)
  • 讲解技巧

    • 先翻车:去掉 produces 跑一次,浏览器会一次性等到结束。让学员看到「为什么必须加」。
    • 强调 doFinally 是唯一能观测中断的钩子——这是后面证明「真的省了钱」的关键。
  • 时长:5min


P6 · 编码:需要 usage 时用 chatResponse ​

中心内容:流式下多数厂商不返回 usage,计费要另想办法。

  • 页面内容

    • .chatResponse() → Flux<ChatResponse>
    • concatWith 补一条 usage 事件
    • 流式 usage 常缺失 → 估算兜底或事后查
  • 讲解技巧

    • 这页是专业度的体现:绝大多数教程不讲流式下拿不到 usage。讲出来学员会觉得「他真的做过生产」。
    • 给明确结论:预算用估算,对账用厂商接口或事后统计。
  • 时长:3min


P7 · 前端:beforeunload 必须 close ​

中心内容:EventSource 会自动重连,不关就是重复烧钱。

  • 页面内容

    • new EventSource(url)
    • onmessage 拼接、onerror 关闭
    • window.addEventListener('beforeunload', () => es.close())
  • 讲解技巧

    • 先翻车:不写 beforeunload,演示「关掉页面再打开」触发了一次新请求。用日志里的 usage 证明钱被重复花了。
    • 这是新手最常踩、也最难自己发现的坑——因为本地测的时候感觉不出来。
  • 时长:4min


P8 · 运行与中断验证 ​

中心内容:日志里看到 signal=cancel 才算真的成功。

  • 页面内容

    • curl -N 看原始 SSE 逐行输出
    • timeout 2 curl -N ... 制造中断
    • 期望日志:stream finished: signal=cancel
  • 讲解技巧

    • 中断验证是本节核心,一定要现场做。看到 cancel 那一刻,学员才真正理解「中断生效了」。
    • 对比 onComplete 与 cancel 两种 signal,说明它们分别意味着「正常结束」和「被放弃」。
  • 时长:4min


P9 · 避坑与小结 ​

中心内容:Nginx 会缓冲 SSE,这是上线必踩的坑。

  • 页面内容

    • proxy_buffering off + proxy_read_timeout 300s
    • 监控 cancel 占比,异常高说明响应太慢
    • HTTP/1.1 下同域名并发连接上限 6 个
  • 讲解技巧

    • 第一条要用「内网好好的,一上生产就没了」的口吻讲。这个坑的杀伤力在于它只在生产出现。
    • 结尾悬念:「SSE 已经能打字机了,什么场景还需要 WebSocket?」
  • 时长:2min


讲师备忘 ​

项内容
课前必做准备一段 curl -N 的终端录像;准备中断后 signal=cancel 的日志截图
最容易超时处P6 的 usage 缺失,学员会追问计费细节——答「02 篇和 04 篇有完整方案」并收住
学员最常问「流式比同步慢吗?」答:略慢(多 SSE 帧开销),但首字节快得多
现场备用网络卡导致打字机不流畅 → 切预录视频,不要现场等