Java 流式 AI 接口:SSE、取消传播与慢客户端治理 @ Lin | 2026-06-24T10:00:00+08:00 | 3 分钟阅读 | 更新于 2026-09-12T10:00:00+08:00

拆开任务生命周期和连接生命周期,处理断线、重连、取消和事件缓冲。

本文于 2026-09-12 补充整理,按 2026-06 专题归档。示例为方案设计,参数用于说明方法,不代表已上线项目或实测成果。

任务完成与连接结束是两件事

浏览器断开 SSE 连接,可能只是用户刷新了页面;服务器上的模型请求却还在运行。反过来,模型生成结束,也可能因为代理缓冲而迟迟没有把最终结果送到浏览器。

因此建议先创建任务,再订阅任务事件。创建接口返回 taskId,SSE 负责展示进度;是否取消任务由独立的取消接口或明确的短任务策略决定。

定义事件契约

id: 18
event: answer.delta
data: {"taskId":"task-demo","text":"请先检查凭据有效期"}

id: 19
event: task.completed
data: {"taskId":"task-demo","resultId":"result-demo"}

事件 ID 在单任务内单调递增,并在需要重放时持久化。不同模型供应商的原始流事件先转换为应用事件,不直接暴露给前端,否则换模型就会改变界面协议。

建议区分 task.startedprogressanswer.deltaapproval.requiredtask.completedtask.failed。工具参数的半截 JSON 不能被前端当作可执行命令,最终业务结果应通过独立契约发送。

SSE 重连不等于重新执行

客户端携带最后收到的事件 ID,服务器从事件存储继续发送。若事件保留期已过,应返回任务快照与“无法完整重放”的明确信号。不能为了恢复显示,再创建一个相同任务。

创建任务本身也需要客户端请求 ID 或幂等键,以处理页面重试。事件订阅接口必须鉴权,知道 taskId 不应自动获得任务内容。

如果使用浏览器原生 EventSource,还要匹配它实际支持的认证方式。不要为了方便把长期访问令牌写进 URL,造成日志和历史记录泄露。

断线和慢客户端都要释放资源

Spring MVC 异步请求文档说明,Servlet 场景下通常需要通过周期性写入发现断线。心跳间隔需要与代理空闲超时协调,并在写失败、完成和超时回调中清理订阅与缓冲。

SSE 并没有自动解决浏览器消费缓慢的问题。应用要限制每个连接的缓冲字节数和事件数。超过上限,可以断开并要求从持久化事件重连;不能无限把 token 累积在堆内存里。

即时问答可以采用“断线后短暂等待,仍无订阅则请求取消”的策略;长任务则继续执行并保留状态。无论哪一种,都要在产品契约中说清楚。

取消是一条传播链

用户取消后,任务进入 CANCEL_REQUESTED,编排器不再发起新步骤,并尝试取消当前模型或工具调用。实际依赖不一定立即停止,最终状态需要反映仍在对账的写操作。

数据库查询超时、HTTP 请求取消和供应商生成停止不是同一个事件。没有拿到停止确认时,不能承诺不再产生任何费用,也不能直接把可能已成功的写操作标为未执行。

一组有意义的联调

用慢客户端限制读取速度,观察缓冲是否有上限;在答案生成中途断网,再重连,检查事件是否重复显示但任务没有重复执行;在写工具提交后取消,检查系统是否进入核对结果的状态。

还应在真实代理链上检查首个业务事件到达时间和最终完成时间。只在本机控制台看到 token 持续输出,并不能证明浏览器端的流式体验成立。


查看系列导读与阅读路线

© 2019 - 2026 Lin 的博客

Powered by Hugo with theme Dream.

avatar
关于我

Lin 的 ❤️ 博客

技术专题:Java Agent 工程化:从知识库问答到可靠业务执行。围绕 RAG、工具治理、任务恢复与 Java 平台架构整理设计思路和验证方法。

记录一些 🌈 生活上,技术上的事

  • 🛠️ 技术阵地: 深耕 Java 开发 ,目前向 AI 应用领域探索与拓展。这里主要记录我从后端架构到 AI 赋能的进阶之路、技术思考以及日常的 debug 血泪史。始终致力于写出优雅且没有 bug 的代码(尽量)。
  • 🌈 生活侧写: 偶尔脱下极客的外衣,我也会在这里分享生活中的灵光一现。不管是好用的效率工具,还是周末的一场骑行,都是构成我真实生活的一部分。