基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
简历
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-11-01 11:58

代理停止按钮如何真正终止后台任务

dev.to · AI#Agent#任务取消#可靠性
Editor brief · 编辑速览

文章将取消操作定义为跨层状态转换,区分浏览器断流、服务端接收取消、工作器响应和模型供应商取消等不同事实。重点是阻止后续任务启动、记录最终状态,并明确仍在执行的操作。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

用户点了 Stop,聊天窗口停止出字,后台 Agent 却可能还在生成、点击页面,甚至准备发送邮件。原文讨论的就是这个落差:怎样让一次取消请求穿过浏览器、模型、工具和任务队列,最终得到可信的执行结果。

作者的结论是:取消应该是持久化的状态变化,不能只靠一个布尔值或断开的连接。这篇没有性能对照或实测数据,但给出了一套具体的工程约束。对正在做 AI 对话、工具调用和后台任务的前端、全栈开发者,它最有用的地方是把「停止显示」和「停止执行」之间的账理清了。

Stop 按钮应该承诺到哪一步

一次取消至少涉及几件不同的事:用户提出停止、系统收到请求、后台开始收尾、任务确认终止,以及外部操作是否已经发生。把这些事实塞进一个 cancelled 字段,界面和后台很容易各说各话。

作者建议使用这样的主路径:

running → cancel_requested → cancelling → cancelled

此外还要有 completed、failed 和 needs_reconciliation 等终态。最后一个可以理解为「结果待核实」:外部写入已经发出,却还不知道有没有成功,此时不能直接宣布取消完成。

产品应当先约定:有权限的用户可以取消任务;执行方观察到持久化的取消请求后,不再启动新的模型轮次、工具调用或排队步骤;正在运行的工作接收尽力而为的取消信号,并有确认期限。只有编排器记录了最终决定,API 才能返回 cancelled。

这里有个边界:请求已经落库,与所有执行方都已观察到它,并非同一时刻。传播期间仍可能有工作在途,需要靠后续确认处理。

停止按钮的可信度,取决于后台能否解释最后发生了什么。

取消也不能触发静默重试。用户已经改变意图,重新运行应该创建新的 attempt,或者经过明确的恢复操作。

为什么需要数据库记录,不能只传 AbortSignal

AbortController 适合控制当前进程里的请求,但进程重启、队列转交、多个 worker 并行时,它无法独自承担最终依据。作者建议把权威状态放进数据库或工作流引擎。

下面这段类型定义,把取消原因、时间和版本一起存下来:

type RunStatus =
  | "queued" | "running" | "cancel_requested" | "cancelling"
  | "cancelled" | "completed" | "failed" | "needs_reconciliation";

type AgentRun = {
  id: string;
  tenantId: string;
  status: RunStatus;
  cancelRequestedAt?: string;
  cancelReason?: "user" | "deadline" | "policy" | "operator";
  activeStepId?: string;
  version: number;
  terminalAt?: string;
};

要留意 version:作者希望借它实现比较并更新(compare-and-set),防止迟到的 worker 把 cancel_requested 覆盖成 running。不过,字段本身不会提供并发保护,写入时必须真正检查版本。

取消入口是幂等的 POST /runs/:id/cancel:检查权限,记录请求,返回当前状态,再通知 worker。重复点击、重连和网络重试,应当指向同一次取消过程。

作者使用事务性 outbox:在同一个事务里更新任务并写入 run.cancel_requested 事件,之后再投递通知。它解决的是「通知发出但记录没存下」或「记录存下但进程来不及通知就崩了」的问题。原文认为不必所有技术栈都使用 outbox,但必须先保存决定,再通知执行方。

浏览器端则先结束本地流,再请求服务端取消:

const controller = new AbortController();

stopButton.onclick = async () => {
  controller.abort("user_pressed_stop"); // 立即结束本地流
  await fetch(`/api/runs/${runId}/cancel`, { method: "POST" });
  showStatus("Stopping…");
};

关键是最后一行只显示 Stopping…,不能显示 Stopped;终态需要等服务端事件确认。这个简化示例也没有处理取消接口失败,实际接入还需要补上请求失败后的状态查询和提示。

页面断线不能自动等同于取消意图。移动网络切换、标签页休眠,都可能造成连接中断。SSE 重连应该从单调递增的事件序号继续回放,而不是重新启动任务。

模型、并行工具和队列,要分别处理什么

编排器应在每个步骤边界读取持久化状态;长模型流还要传递取消信号,关闭网络请求,或使用供应商提供的取消 API。但关闭 HTTP 连接只能证明客户端停止等待,不能证明上游推理已经停止,也不能证明没有计费。需要核实时,应保存 provider request ID,再确认最终状态。

超时也要沿调用链传递。作者举的例子是:检索已经花了 50 秒,后续模型调用不该因为重新计时,又获得完整的 60 秒。应设置绝对 deadline,每层只拿剩余时间。

并行任务尤其容易漏取消。搜索、内部查询、写入工具同时启动时,每个子任务都要收到信号,但处理策略不同:

  • 只读工具通常可以取消或放弃等待。
  • 尚未越过提交点的写入,可以尝试取消。
  • 已经发出的写入,需要回执或目标系统的权威查询。
  • 清理任务可能需要一个短暂的受保护窗口,用来记录最终状态。

Python 的 asyncio 是协作式取消,在 await 位置抛出 CancelledError。作者建议通过 try/finally 清理,或捕获后记录中断,再继续抛出;不能吞掉异常后继续执行 Agent 工作。

队列同样需要检查。worker 应先获取租约,在每次对外可见的动作前重新确认状态,退出时释放租约。消息已经进队列,不代表仍有执行权限。Temporal 可以把取消建模成显式工作流命令;使用 Postgres 和 worker,也需要实现相应语义。

已经发出去的写入,为什么不能直接算取消

假设 Agent 已经发出创建发票的请求,还没保存返回值,用户就点了停止。此时发票可能已经存在,只是本地不知道。

直接标记 cancelled,容易让后续重试再创建一张;直接认定成功,同样没有证据。作者建议进入 needs_reconciliation,通过稳定的 operation key 查询目标系统:存在就展示已创建,需要撤销时再走独立且获得授权的补偿操作;确认不存在,才能把步骤记为取消。

取消负责阻止未来工作,回滚负责补偿已经发生的工作。

因此,写入工具仍需要幂等键和操作账本。网络超时也不能当作写入失败的证据。

验证时,要测的是竞态窗口。作者列出至少八类场景:排队时取消、生成 token 时取消、模型输出后工具派发前取消、两个并行工具运行时取消、写入派发后取消、进程崩溃时取消、重复取消,以及取消后的恢复尝试。

可以在模型调用前、工具派发前、派发后但回执存储前、worker 持有租约时设置确定性的测试钩子,并保存 trace,在运行时变化后回放。监控则关注请求到终态的耗时、取消后仍启动的工作、孤儿工具调用、对账比例,以及部分输出被误当最终答案的比例。

我的判断

我认同作者把取消放进任务生命周期。只要 Agent 能调用外部工具,前端流停止就不足以代表工作结束。相比只使用 AbortController,持久化记录能让重启后的 worker 和重新打开页面的用户看到同一份事实。

不过,完整方案有适用前提。只有单次文本生成、没有后台队列和外部写入的功能,可以从较小的实现开始;引入事务性 outbox、租约和操作账本,会增加实现与运维成本。原文没有提供不同规模团队的成本或可靠性对照,这些选择仍属于工程经验,不能据此认定 Temporal 一定优于自建 worker。

原文的取消接口示例还有一处需要补齐:它只排除了 completed、failed、cancelled,重复请求仍可能更新取消时间、增加版本并再次插入事件,甚至把 cancelling 改回 cancel_requested;needs_reconciliation 也没有被保护。因此,「接口必须幂等」是正确要求,但示例还没有完整满足它。

此外,动作前检查状态与真正派发之间仍存在竞态窗口。原文提出了检查点,却没有给出彻底关闭这个窗口的协议。落地时必须结合目标系统的幂等能力与回执机制,明确哪些结果可以确认,哪些仍要对账。

能拿来做什么

  • 做流式聊天时,把「本地停止出字」和「服务端取消任务」拆成两个动作;界面显示停止中,收到终态后再确认,并把截断文本标记为中断内容,避免自动送入下一轮 Agent。
  • 给 Node 编排服务增加持久化 run 状态、取消原因和绝对 deadline,在每次模型调用及工具派发前检查状态,并向下传递剩余时间。
  • 给发送邮件、创建记录等写入工具配置稳定的幂等键和操作账本;取消撞上在途写入时先核实结果,避免自动重发。
  • 为取消流程补上派发前、派发后回执落库前和进程崩溃后的测试,同时检查重复取消是否产生重复清理,以及恢复是否创建有来源记录的新 attempt。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
DeepSeek开源以减少人工打扰为目标的代理原型
下一篇
用静态扫描拦截 AI 生成的 Rust 隐患

资讯速览

dev.to · AI · 2026-11-01 11:58

AI 评分 8.0 / 10

相关标签

#Agent#任务取消#可靠性

继续阅读

  • 让编程代理记住代码库,先写清隐性约束
  • DeepSeek开源以减少人工打扰为目标的代理原型
  • 用静态扫描拦截 AI 生成的 Rust 隐患
  • 让 AI 网页测试同时核对后端结果
  • 千级工具路由为何不能只靠向量检索