给前端 Agent 流式输出加取消和重试时,我会先把事件账本建起来
最近给一个 Agent 工作台补流式输出能力时,我遇到一个典型问题:演示环境里回答很顺,真实使用时却会出现取消后仍继续追加内容、重试后旧 token 混进新回答、网络断开后说不清失败来源。后来我把这类问题从渲染组件里拆出来,单独做了一本事件账本。每次用户提交、服务端分片、取消、失败、重试和落库,都写成带 requestId 的事件,再由 UI reducer 消化。这个账本后来也成了测试和排障的共同语言,大家能围着同一条时间线讨论问题。

原创示意图:把用户操作、请求信号、流读取、重试路径和审计事件放在同一条工作流里观察。 来源:Codex image generation
问题背景
Agent 前端的中间态比普通聊天框多。一次任务可能先生成计划,再调用工具,随后流式返回摘要,还可能等待用户确认。只要某个环节允许取消或重试,页面就需要知道当前内容来自哪次请求。MDN 的 AbortController 文档说明,它可以用来中止一个或多个 Web 请求;Fetch 文档也说明,响应体可以作为流读取。把这两点合在一起,前端就不能只保存一个 loading 布尔值。
关键难点
第一个难点是取消不等于回滚。用户点停止后,已经展示的内容通常要保留,后续分片要丢弃,输入框、重试按钮、工具卡片也要进入可恢复状态。如果只在网络层 abort,组件层仍可能收到残留事件。
第二个难点是重试要有边界。重试一条 Agent 消息时,我会生成新的 attemptId,但保留原来的 messageId。用户看到同一条业务消息,工程上又能区分第几次尝试。旧尝试的 stream chunk 到达时,reducer 会先检查 attemptId,不匹配就记录为过期事件,不再写入展示区。
第三个难点是审计信息要低成本保留。SSE 或 fetch stream 出问题时,线上复盘不能只靠一句网络错误。我会记录开始时间、取消来源、最后一个 chunk 序号、错误类型和用户操作入口。MDN 的 Server Sent Events 文档提到,EventSource 可以接收服务端推送事件;在不需要自定义请求体的场景里,这种模型也适合做事件流对齐。
解决思路
我把实现拆成四层。请求层负责创建 AbortController,把 signal 传进 fetch,并分配 requestId。流读取层负责用 reader 取出分片,进入 token buffer 前先做归属校验。状态层只接收标准事件,例如 started、chunk、cancelled、failed、retrying、done。审计层把关键事件写成可查看的时间线。
这个结构让 UI 不直接理解网络细节。按钮只派发用户意图,网络层只负责传输,reducer 决定哪些事件能改变当前状态。取消时先标记当前 attempt 为 closing,再调用 abort。后续分片到达也只进入审计日志,不再污染用户正在看的回答。
关键步骤
第一步,为每次发送生成 messageId、attemptId 和 requestId。第二步,把 AbortController 放进请求注册表,组件卸载、用户取消、路由切换时都走同一个 cancelRequest。第三步,流读取循环里每处理一个 chunk 就检查 attempt 是否仍然有效。第四步,重试时创建新 attempt,把旧 attempt 的最终状态冻结。第五步,把审计面板接到事件账本,展示取消来源、耗时、chunk 数和错误摘要。
我还会补一组浏览器级回归:开始流式输出后立即取消,取消后快速重试,旧请求延迟返回,网络中断后再次发送,页面切走再回来。每个用例只断言用户可见行为,例如旧 token 不再追加、重试按钮可点击、错误说明可读、当前 attempt 能被审计面板定位。
可复用经验
流式 Agent 前端要先管住事件归属,再谈渲染体验。AbortController 解决传输中止,ReadableStream 解决分片读取,业务状态仍需要一套事件账本承接。只要 requestId、attemptId、buffer 和审计日志分清楚,取消和重试就会从偶发问题变成可测试、可复盘、可维护的工作流。
主要来源
MDN AbortController: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
MDN Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
MDN Using readable streams: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
MDN Using server sent events: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events