← [ BACK_TO_NOTES ]

SSE 流式输出的暂停与中断

AI 流式对话中的暂停/中断与取消:区分 SSE transport 断开、可恢复的 Agent interrupt,以及显式 CancelRun 业务终止。

#ai #concept #growing

[!info] related notes

SSE 流式输出的暂停与中断

一句话定义

在 production Agent streaming 中,“SSE 断了”“Agent 暂停等待用户”“用户明确停止生成”是三种不同状态:

Transport disconnect
Agent interrupt / waiting_user
Business cancellation

不能只靠一个 AbortController 或 socket close 把它们混成同一个 cancelled。

三种情况先分清

1. Transport disconnect

Wi-Fi lost / proxy timeout / reader abort / tab reload
→ SSE transport unavailable

它只说明客户端暂时收不到 live event。若服务端 Run 是 durable/detached 的,它可以继续运行;客户端后续应该恢复/catch up,而不是自动取消。

2. Agent interrupt / waiting_user

例如 ask_user:

Run reaches interaction.required
→ durable pending interaction
→ execution pauses
→ user answers later
→ resume same logical Agent thread/checkpoint

这是正常业务流程,不是错误,也不是取消。

3. Explicit cancellation

用户点击真正的“停止生成”:

CancelRun(run_id)
→ server validates current state
→ durable run becomes cancelled
→ propagate cancellation to live execution
→ run.cancelled terminal event/state

这是不可恢复继续的终态语义,除非产品显式定义另一种重新开始机制。

前端状态机

更合理的 UI 状态可以表达为:

streaming
  ├─ transport lost
  │    → recovering/reconnecting
  │    → streaming | terminal
  │
  ├─ interaction.required
  │    → interrupted/waiting_user
  │    → run.resumed
  │    → streaming
  │
  ├─ explicit stop
  │    → cancelling
  │    → cancelled
  │
  ├─ run.completed → completed
  └─ run.failed    → failed

这样 UI 不会把“网络暂时断了”错误显示成“用户已取消”。

AbortController 的正确边界

const controller = new AbortController();
fetch(url, { signal: controller.signal });

controller.abort();

它负责停止浏览器这一侧的 fetch/ReadableStream 工作。

它不自动等于:

CancelRun accepted
run.cancelled persisted
LLM/tool actually stopped

因此:

AbortController
= transport/client async cancellation primitive

CancelRun
= business command

见 AbortController 与异步取消 与 Cancellation。

为什么不能“后端检测连接关闭就自动取消 Run”

网络关闭的原因不包含可靠的用户意图:

user clicked Stop
和
Wi-Fi dropped

在服务器看来都可能表现为 transport close/request cancellation。

如果把它直接映射成 Run cancel:

临时网络故障
→ Agent 被误杀
→ durable result 无法继续产生

所以 durable Agent architecture 通常让 execution lifetime 独立于单条 SSE request lifetime,再通过显式 CancelRun 处理真正的用户取消。

断线恢复

如果客户端已经消费:

seq 1..5

网络断开,而服务端继续产生:

6..10

客户端恢复时应从 durable event source 请求:

after_seq=5
→ 6..10

并按 (run_id, seq) 做 ordering/dedup。详见 Durable SSE Recovery with after_seq。

HITL Resume 与 SSE Reconnect 不是一回事

SSE reconnect
= 换一条 transport,追赶同一 Run 的事件

Agent resume
= 用户给 interrupted execution 新业务输入,让未完成的 logical thread 继续计算

BodySense 当前 ask_user Resume 会保持同一个 Conversation / logical checkpoint thread,但创建新的 public Run,因此新 Run 拥有新的 run-local sequence domain,可以从 seq=1 开始。

Partial Message

取消、断线和 interrupt 对 partial content 的策略也不同:

Disconnect
→ partial UI projection 可以保留,等待 replay 继续补齐

Interrupt
→ partial state + pending interaction 都应可恢复

Cancel
→ 是否保留 partial result 由 durable product/domain contract 决定

不要仅因为 reader 结束就清空或固化最终消息。

BodySense 当前实现对应

Live SSE
→ fetch + ReadableStream
→ StreamEvent validation
→ ActiveTurn reducer

Transport disconnect
→ durable Runtime Event Log / after_seq recovery

Explicit Stop
→ POST /consultation-runs/:id/cancel
→ durable run.cancelled

ask_user
→ interaction.required
→ user answer persisted
→ new public Run resumes same logical Agent thread

主要代码边界:

apps/web/src/features/consultation/hooks/useSSEProcessor.ts
apps/web/src/features/consultation/runtime/durableRunRecovery.ts
apps/web/src/features/consultation/runtime/activeTurnReducer.ts
apps/api/internal/consultation/runtime.go

常见误解

  • EventSource.close() / AbortController.abort() = 服务端 Run 已取消。
  • SSE EOF = run.completed。
  • interaction.required = error/cancel。
  • reconnect 和 resume 是同一种操作。
  • cancel 后能自动回滚已经发生的外部 tool side effect。

最短记忆

SSE断了      → recover transport/events
Agent暂停     → wait for input, then resume execution
用户点停止    → explicit CancelRun → durable cancelled
创建于 2026/5/29 更新于 2026/9/16
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本