Abort Generation
Abort Generation 是 AI 流式输出中“停止生成”的端到端交互:前端停止本地 transport,同时通过显式 CancelRun 让服务端 durable Run 进入 cancelled 并传播取消。
[!info] related notes
- 所属 MOC: AI Agent Application MOC
- 相关: Cancellation, SSE Client, Chat UI
- 后端: Cancellation Propagation
- 浏览器原语: AbortController 与异步取消
- Durable recovery: Durable SSE Recovery with after_seq
Abort Generation
一句话定义
“停止生成”是一个端到端业务动作,而 AbortController.abort() 只是其中可能使用的浏览器 transport 取消原语。
可靠实现要同时处理:
1. explicit server-side CancelRun
2. durable cancelled state/event
3. cancellation propagation to live execution
4. client transport/UI cleanup
为什么不能把 Stop 按钮等价成 abort()
controller.abort();
只能直接保证调用方停止等待/读取支持 AbortSignal 的本地异步操作。
它不自动证明:
server Run cancelled
LLM/provider stopped
Tool stopped
partial output was durably reconciled
other clients know the final state
特别是对于 detached/durable Agent Run,浏览器网络断开后服务端本来就应该能够继续执行。因此不能让同一个“连接关闭”信号既代表普通网络故障,又代表用户明确取消。
正确的停止生成流程
User clicks Stop
↓
UI enters cancelling
↓
POST/command CancelRun(run_id)
↓
server authorizes + atomically resolves current state
↓
run becomes cancelled
↓
propagate cancellation to active execution where possible
↓
persist/emit run.cancelled
↓
frontend observes terminal state
↓
abort/close local SSE reader as cleanup
具体顺序可以根据 API 设计调整,但必须保证业务取消由服务端权威状态决定,而不是由浏览器 socket 状态决定。
Disconnect 与 Stop 是两条路径
Network/Wi-Fi/proxy disconnect
→ recover / reconnect / replay
Explicit Stop button
→ CancelRun
这是 production streaming 最重要的分界之一。
如果把 disconnect 自动映射成 cancel,会误杀仍可继续的 Run;如果把 Stop 只做本地 abort,则后台仍可能继续消耗模型/工具资源并生成用户已经不想要的结果。
前端状态机
可以显式建模:
streaming
├─ transport lost → reconnecting/recovering
├─ explicit stop → cancelling → cancelled
├─ run.completed → completed
└─ run.failed → failed
注意 reconnecting 与 cancelling 完全不同:
reconnecting = 我不知道/暂时看不到 Run,但没有要求它停止
cancelling = 我已经明确要求服务端停止 Run,等待权威确认
Partial Output
是否保留已生成的部分内容取决于产品/领域契约。常见做法是:
partial text remains visible
+
message/run marked cancelled or partial
但不要让前端在没有服务端事实时自行伪造最终状态。更稳的是用 durable message/run/read-model 做最后 reconciliation。
BodySense 当前实践
当前 BodySense 已有显式 API:
POST /consultation-runs/:id/cancel
服务端路径会对 Run 做授权/状态裁决,触发 active execution cancellation,并产生 durable run.cancelled。普通 transport disconnect 则保留 Run 并由客户端通过 Runtime Event Log / after_seq 恢复。
主要代码边界:
apps/web/src/features/consultation/services/consultationService.ts
apps/api/internal/handler/consultation_handler.go
apps/api/internal/consultation/runtime.go
apps/api/internal/service/run_service.go
apps/api/internal/repository/run_repository.go
常见错误
- Stop button 只执行
AbortController.abort()。 - 网络断线被误判为用户取消。
- 服务端已 cancelled,但 LLM/tool execution 没收到 propagation。
- 前端先显示
cancelled,服务端实际上已经完成,造成终态分叉。 - Cancel/Complete race 没有 atomic server state transition。
- 把 cancellation 当 rollback,假设已经提交的外部副作用能自动撤销。
最短记忆
AbortController = stop local transport/work
CancelRun = request business cancellation
run.cancelled = authoritative terminal fact
Disconnect = recovery problem, not cancellation intent