id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week
| id |
title |
type |
domain |
stack |
tags |
level |
prerequisites |
est_hours |
related |
week |
| frontend.react.scenario.long-task-ui |
长任务界面:Agent 任务的状态机、事件去重、乐观更新和恢复 |
scenario |
frontend |
|
| 任务状态 |
| 状态机 |
| useReducer |
| SSE |
| 幂等 |
| 乐观更新 |
| 异常恢复 |
|
advanced |
| frontend.react.hooks-rendering |
|
2.5 |
| frontend.react.interview |
| ai.langgraph.scenario.refund-approval |
| frontend.vue.scenario.ai-chat-stream |
|
|
长任务界面:Agent 任务的状态机、事件去重、乐观更新和恢复
业务背景与问题
报表 Agent 生成一份报表要 1~3 分钟,中间可能需要用户补充信息(“要按大区还是按城市?”)。前端第一版的问题:
- 用户刷新页面后,看不到正在运行的任务,以为丢了,又提交了一次;
- 网络重连后收到重复的进度事件,步骤列表出现重复项;
- 任务失败只显示“出错了”,用户不知道能不能重试;
- 点击“取消”后界面立即显示已取消,但后端其实还在跑;
- 同时打开两个标签页,状态不一致。
约束
- 后端是 LangGraph + FastAPI,任务状态存在数据库中,事件通过 SSE 推送。
方案设计
- 任务以服务端为准:提交后后端返回任务 ID;页面加载时先查询“我的进行中任务”并恢复显示;提交接口带幂等键(防止重复提交)。
- 前端状态机:
useReducer 集中管理状态转换(排队 / 运行 / 等待输入 / 成功 / 失败 / 取消),事件带递增序号,旧事件和终态后的事件丢弃。
- 重连:断开后先调用查询接口拿到最新快照(包含当前序号),再从该序号之后订阅事件。
- 失败分类:后端返回
retryable,前端据此显示“重试”按钮或“联系管理员”。
- 取消:点击后进入“取消中”,后端确认(事件或接口返回)后再显示“已取消”;不做乐观的终态显示。
- 等待输入:显示问题和输入框,提交后通过接口恢复任务(对应后端的
interrupt / Command(resume=...))。
- 多标签页:都以服务端事件为准;必要时用
BroadcastChannel 同步。
关键代码
状态机和组件见 examples/task-panel。重连时先取快照再订阅(示意):
取舍
| 选择 |
优点 |
缺点 |
| 服务端为准 + 前端状态机(本方案) |
刷新、重连、多标签页都一致 |
需要快照接口和事件序号 |
| 只靠前端内存状态 |
简单 |
刷新即丢失 |
| 轮询任务状态 |
实现简单、天然可恢复 |
延迟和请求量;可作为 SSE 不可用时的降级 |
| 乐观更新所有操作 |
体验快 |
对“取消”这类不一定成功的操作会误导用户 |
易错点
- 状态转换分散在多个
useState 和事件处理函数中,难以保证一致。
- 不区分“已取消”和“取消中”。
- 重连后不取快照,直接订阅,错过断线期间的事件。
useEffect 中订阅没有清理,切换任务后旧任务的事件串进来。
面试怎么讲
“长任务界面我以服务端为准:提交带幂等键,页面加载时恢复进行中的任务。前端用 useReducer 做状态机,事件带序号,重复、乱序、终态后的事件都丢弃;断线重连先取快照再从序号之后订阅。失败按 retryable 显示重试,取消要等后端确认,不做乐观的终态。等待用户输入时,提交后调用接口恢复后端的 LangGraph 任务。reducer 是纯函数,状态转换都有单元测试。”