拆开任务生命周期和连接生命周期,处理断线、重连、取消和事件缓冲。
本文于 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.started、progress、answer.delta、approval.required、task.completed 和 task.failed。工具参数的半截 JSON 不能被前端当作可执行命令,最终业务结果应通过独立契约发送。
SSE 重连不等于重新执行
客户端携带最后收到的事件 ID,服务器从事件存储继续发送。若事件保留期已过,应返回任务快照与“无法完整重放”的明确信号。不能为了恢复显示,再创建一个相同任务。
创建任务本身也需要客户端请求 ID 或幂等键,以处理页面重试。事件订阅接口必须鉴权,知道 taskId 不应自动获得任务内容。
如果使用浏览器原生 EventSource,还要匹配它实际支持的认证方式。不要为了方便把长期访问令牌写进 URL,造成日志和历史记录泄露。
断线和慢客户端都要释放资源
Spring MVC 异步请求文档说明,Servlet 场景下通常需要通过周期性写入发现断线。心跳间隔需要与代理空闲超时协调,并在写失败、完成和超时回调中清理订阅与缓冲。
SSE 并没有自动解决浏览器消费缓慢的问题。应用要限制每个连接的缓冲字节数和事件数。超过上限,可以断开并要求从持久化事件重连;不能无限把 token 累积在堆内存里。
即时问答可以采用“断线后短暂等待,仍无订阅则请求取消”的策略;长任务则继续执行并保留状态。无论哪一种,都要在产品契约中说清楚。
取消是一条传播链
用户取消后,任务进入 CANCEL_REQUESTED,编排器不再发起新步骤,并尝试取消当前模型或工具调用。实际依赖不一定立即停止,最终状态需要反映仍在对账的写操作。
数据库查询超时、HTTP 请求取消和供应商生成停止不是同一个事件。没有拿到停止确认时,不能承诺不再产生任何费用,也不能直接把可能已成功的写操作标为未执行。
一组有意义的联调
用慢客户端限制读取速度,观察缓冲是否有上限;在答案生成中途断网,再重连,检查事件是否重复显示但任务没有重复执行;在写工具提交后取消,检查系统是否进入核对结果的状态。
还应在真实代理链上检查首个业务事件到达时间和最终完成时间。只在本机控制台看到 token 持续输出,并不能证明浏览器端的流式体验成立。
