Files

4.0 KiB
Raw Permalink Blame History

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
react
typescript
fastapi
任务状态
状态机
useReducer
SSE
幂等
乐观更新
异常恢复
advanced
frontend.react.hooks-rendering
2.5
frontend.react.interview
ai.langgraph.scenario.refund-approval
frontend.vue.scenario.ai-chat-stream
7
12

长任务界面:Agent 任务的状态机、事件去重、乐观更新和恢复

业务背景与问题

报表 Agent 生成一份报表要 1~3 分钟,中间可能需要用户补充信息(“要按大区还是按城市?”)。前端第一版的问题:

  1. 用户刷新页面后,看不到正在运行的任务,以为丢了,又提交了一次;
  2. 网络重连后收到重复的进度事件,步骤列表出现重复项;
  3. 任务失败只显示“出错了”,用户不知道能不能重试;
  4. 点击“取消”后界面立即显示已取消,但后端其实还在跑;
  5. 同时打开两个标签页,状态不一致。

约束

  • 后端是 LangGraph + FastAPI,任务状态存在数据库中,事件通过 SSE 推送。

方案设计

  1. 任务以服务端为准:提交后后端返回任务 ID;页面加载时先查询“我的进行中任务”并恢复显示;提交接口带幂等键(防止重复提交)。
  2. 前端状态机:useReducer 集中管理状态转换(排队 / 运行 / 等待输入 / 成功 / 失败 / 取消),事件带递增序号,旧事件和终态后的事件丢弃。
  3. 重连:断开后先调用查询接口拿到最新快照(包含当前序号),再从该序号之后订阅事件。
  4. 失败分类:后端返回 retryable,前端据此显示“重试”按钮或“联系管理员”。
  5. 取消:点击后进入“取消中”,后端确认(事件或接口返回)后再显示“已取消”;不做乐观的终态显示。
  6. 等待输入:显示问题和输入框,提交后通过接口恢复任务(对应后端的 interrupt / Command(resume=...))。
  7. 多标签页:都以服务端事件为准;必要时用 BroadcastChannel 同步。

关键代码

状态机和组件见 examples/task-panel。重连时先取快照再订阅(示意):

async function connect(taskId: string, dispatch: (e: TaskEvent) => void, signal: AbortSignal) {
  const snap = await fetchJSON<TaskSnapshot>(`/api/tasks/${taskId}`, { signal });
  dispatch({ type: "snapshot", snapshot: snap });                 // 用快照覆盖本地状态
  await streamEvents(`/api/tasks/${taskId}/events?after=${snap.seq}`, dispatch, signal);
}

取舍

选择 优点 缺点
服务端为准 + 前端状态机(本方案) 刷新、重连、多标签页都一致 需要快照接口和事件序号
只靠前端内存状态 简单 刷新即丢失
轮询任务状态 实现简单、天然可恢复 延迟和请求量;可作为 SSE 不可用时的降级
乐观更新所有操作 体验快 对“取消”这类不一定成功的操作会误导用户

易错点

  • 状态转换分散在多个 useState 和事件处理函数中,难以保证一致。
  • 不区分“已取消”和“取消中”。
  • 重连后不取快照,直接订阅,错过断线期间的事件。
  • useEffect 中订阅没有清理,切换任务后旧任务的事件串进来。

面试怎么讲

“长任务界面我以服务端为准:提交带幂等键,页面加载时恢复进行中的任务。前端用 useReducer 做状态机,事件带序号,重复、乱序、终态后的事件都丢弃;断线重连先取快照再从序号之后订阅。失败按 retryable 显示重试,取消要等后端确认,不做乐观的终态。等待用户输入时,提交后调用接口恢复后端的 LangGraph 任务。reducer 是纯函数,状态转换都有单元测试。”