前端承接 Agent 长任务时,我怎样把任务面板做成可恢复工作流
最近在做一个带 Agent 工作流的前端台子,最早的实现很直接:用户点一次“开始执行”,页面里起轮询,拿到结果就更新列表,拿不到就等下次刷新。这个方案在 Demo 阶段看起来很顺,但一到真实工作流就暴露问题了。任务会跑十几分钟,中间还会插入审批、重试、外部知识源刷新和结果回写。只要用户刷新页面、切标签页太久,或者本地网络闪断,任务面板就会和后端执行状态脱节。

原创示意图:任务快照、轮询恢复、审批队列与结果同步需要统一收敛到一条前端工作流链路中。 来源:Codex image generation
问题背景
这类 Agent 任务和普通表单提交差别很大。OpenAI 的 Background mode 文档明确提到,长任务应该异步启动,再通过响应对象持续轮询状态,常见状态会经历 queued 和 in_progress,还支持显式取消。LangGraph 的 Durable Execution 文档也把另一件事说得很透:长流程真正难的地方,不在单次调用成功率,而在中断之后能否从上一次已完成的位置继续推进。前端如果只把任务看成一个转圈中的 Promise,很快就会在刷新、重连、审批插入这些场景里失去控制。
我踩到的三个坑
第一个坑是状态源太散。列表卡片、详情抽屉、顶部通知、审批弹窗各自维护一份状态,任何一个轮询响应晚到半拍,界面就会同时出现“已完成”和“等待审批”两种展示。第二个坑是恢复时机不稳定。TanStack Query 的持久化文档提到,恢复缓存是异步的,如果应用在 restore 过程中就开始新一轮 fetch,很容易出现竞争条件。我最早就是页面一挂载就直接请求,结果本地缓存刚恢复,新的请求又把任务状态覆盖回旧值。第三个坑是把“恢复”理解得太轻。TanStack 还专门提醒,持久化 mutation 后如果没有默认 mutation function,刷新页面后无法继续 resume paused mutations。这类提醒背后反映的是同一个工程事实:恢复从来不是把界面渲染回来,而是要把可以继续执行的动作也一起还原。
解决思路
后来我把前端任务面板拆成了四层。第一层是任务快照仓,负责保存 taskId、阶段、最近一次事件游标、审批节点、结果摘要和用户可见错误。第二层是执行控制器,专门处理 start、resume、cancel、approve 这些动作。第三层是轮询与重连模块,只做两件事:根据状态决定轮询频率,以及在页面重新可见、网络恢复、应用重启后补一次同步。第四层是展示层,所有卡片和详情组件只消费统一快照,不直接拼装状态。
关键步骤
我最后落地时抓住了五个动作。其一,启动任务时立刻把本地任务记录写入持久层,并把服务端返回的任务 ID 和事件游标一起存下。其二,轮询不再盯着“有没有结果”,而是盯着“状态有没有推进”,只有状态推进才更新 UI。其三,审批节点进入等待态后暂停高频轮询,把界面切成显式待处理状态,避免空转。其四,应用恢复时先 hydrate 本地任务仓,再恢复暂停中的请求或继续轮询,顺序不能反。其五,所有取消和重试动作都做成幂等命令,这样刷新后补发一次也不会把流程弄乱。
可复用经验
这次重构之后,我对 Agent 前端有了一个更稳的判断:任务面板本质上是一个轻量工作流壳层,职责是承接后端执行现实,而不是只负责把结果渲染出来。只要任务包含长时运行、人工介入和跨页面恢复,前端就应该优先设计“可恢复状态机”,再去设计卡片样式、动画和提示语。把任务快照、恢复顺序、命令幂等和轮询策略先定清楚,后面无论接 RAG 刷新、桌面端 IPC 任务,还是多 Agent 协作,整套面板都能继续复用。