使用 AI Agent 时,经常会遇到这样的情况:

你让 Agent 分析一个项目,它先读取文件,再分析代码,然后准备修改几个文件。等了半天,你突然发现需求说错了,于是点击「停止」。

普通聊天中,停止生成文字通常就够了。但 Agent 不一样,它可能正在调用模型、执行工具、读写文件,甚至请求外部 API。

这时,一个问题就出现了:用户点击停止之后,Agent 真的停下来了吗?

如果只是让前端不再显示回答,后台任务可能还在运行。如果只停止当前模型请求,Agent 也可能在下一步继续调用工具。

因此,任务取消不是简单地增加一个停止按钮,而是需要让 Agent 的整个执行过程都能响应取消操作。

一、为什么 Agent 不能简单地停止生成?

先看一个常见的 Agent 执行过程:

用户提出需求
    ↓
AI 分析任务
    ↓
调用搜索工具
    ↓
分析搜索结果
    ↓
调用文件处理工具
    ↓
整理结果
    ↓
返回回答

Agent 并不是一次性生成答案,而是根据每一步的执行结果,决定接下来做什么。

例如,用户让 Agent「搜索资料并生成一份报告」,它可能先搜索网页,再读取内容,最后生成报告。每一步都可能涉及新的模型请求或工具调用。

如果用户在搜索过程中点击停止,系统至少要解决三个问题:

  • 不再发起新的模型请求。

  • 不再启动新的工具调用。

  • 尽可能停止已经运行的模型请求和工具任务。

前两个相对容易,第三个则取决于具体操作是否支持取消。

例如,正在执行的本地任务可能可以主动检查取消状态;已经提交给第三方服务的任务,则未必能被立即终止。

所以,停止 Agent,不只是停止它说话,还要阻止它继续做事。

二、给 Agent 一个「停止信号」

可以把 Agent 想象成一个正在执行任务的员工。

用户点击「停止」,就相当于通知这个员工:「先别继续做了。」

但光发出通知还不够。员工需要在工作过程中留意这条通知,收到后停止接下来的工作。

在程序中,这种通知可以通过「取消信号」实现。

以 JavaScript 为例,浏览器和 Node.js 都提供了 AbortController,可以用它发出取消信号。

const controller = new AbortController();

// 把信号传给支持取消的任务
const task = fetch("/api/agent/run", {
    method: "POST",
    signal: controller.signal
});

// 用户点击停止时
controller.abort();

这段代码演示的是取消 HTTP 请求,并不是完整的 Agent 取消实现。

它的关键在于:controller.abort() 发出取消信号后,接收这个信号的操作必须能够响应它。

如果某个工具根本不检查取消信号,或者底层操作不支持取消,那么发出信号也无法保证它立即停止。

因此,Agent 运行时需要把同一个任务的取消信号传递给模型调用和支持取消的工具,而不是只在前端处理。

三、Agent 应该在什么时候检查停止信号?

Agent 不一定能在任意时刻立即停止。更实际的方式是让它在执行过程中的关键位置检查取消状态。

例如:

准备调用模型
    ↓
检查是否已取消
    ↓
调用模型
    ↓
检查是否已取消
    ↓
准备执行工具
    ↓
检查是否已取消
    ↓
执行工具
    ↓
检查是否已取消
    ↓
进入下一轮

假设 Agent 正在分析代码,用户此时点击停止。

如果 Agent 已经完成当前步骤,那么它可以在下一次检查时发现取消信号,直接退出,不再继续分析或修改文件。

如果 Agent 正在执行一个耗时很长的工具,则要看这个工具有没有配合取消机制。

例如,一个工具需要处理 1000 个文件,可以每处理一批就检查一次取消信号。用户点击停止后,工具就不再处理下一批。

但如果工具正在执行一段无法中断的长时间计算,就可能需要等到这段计算结束才能停止。

这种方式称为「协作式取消」:系统发出停止通知,正在运行的任务在能够检查和响应的位置主动停止。

它比强行终止整个进程更容易控制,也更适合需要清理资源的任务。

四、模型调用和工具调用,停止方式有什么不同?

Agent 的工作主要涉及两类操作:请求模型,以及执行工具。两者的取消方式并不完全一样。

1. 正在请求模型

例如,Agent 正在调用大模型生成回答。

如果使用的 SDK 支持取消请求,就可以把取消信号传递给 SDK,尝试终止当前请求。

如果模型以流式方式返回内容,还需要停止继续读取和处理后续内容。

但需要注意:客户端停止等待响应,并不一定代表模型服务端已经停止计算,也不代表已经产生的费用一定会退回。

具体效果取决于模型服务和 SDK 的实现。

2. 正在执行工具

工具的情况更加复杂。

例如:

工具操作取消时可能发生的情况
搜索网页可以尝试取消正在进行的 HTTP 请求
读取大量文件可以在文件处理过程中检查取消信号
批量处理数据可以停止后续批次
调用第三方 API取决于对方是否支持取消
发送邮件已经发送成功的邮件无法靠取消请求收回
修改数据库已经提交的修改不会自动撤销

这也是为什么工具在设计时就应该考虑取消机制。

如果工具只提供一个执行函数,却没有超时控制、取消检查和错误处理,那么 Agent 很难可靠地控制它。

五、用户点击停止后,后端应该怎么做?

如果 Agent 任务只运行几秒,并且完全依赖当前 HTTP 请求,后端可以在请求执行过程中管理取消信号。

但如果任务被放到队列中,由后台 Worker 执行,情况就不同了。

例如,PHP 后端收到用户请求后,将任务投递到 RabbitMQ,随后由 Worker 执行 Agent。

这时,即使用户关闭页面,RabbitMQ 中的任务和 Worker 也不会因此自动停止。

比较合理的做法是给每个任务分配一个唯一的 task_id,并提供取消接口。

用户提交任务
    ↓
后端创建任务
    ↓
返回 task_id
    ↓
Worker 执行 Agent
    ↓
用户点击停止
    ↓
调用取消接口
    ↓
后端记录取消请求
    ↓
通知 Worker
    ↓
Worker 停止后续执行

例如,接口可以设计为:

POST /agent/tasks/{task_id}/cancel

后端收到请求后,先检查当前用户是否有权取消这个任务,再通知对应的 Worker。

如果使用 Redis 保存任务状态,可以先把状态改为 cancel_requested,表示「已经收到取消请求,正在等待任务停止」。

需要注意:修改数据库或 Redis 中的状态,并不等于 Worker 已经停止。

Worker 仍然需要收到通知,并在模型调用或工具执行中响应取消信号。

因此,任务状态、取消通知和实际执行必须配合起来设计。

六、任务状态应该怎么设计?

如果任务只有 running 和 cancelled 两种状态,很容易让用户误以为任务已经彻底停止。

例如,用户点击停止后,后端立即把任务标记为 cancelled,但 Worker 仍在执行一个耗时工具。此时前端显示任务已取消,后台却还在工作。

可以将任务状态设计为:

状态含义
pending等待执行
running正在执行
cancel_requested已收到取消请求,等待执行端停止
cancelled执行已经停止
completed正常完成
failed执行失败

这里最重要的是区分 cancel_requested 和 cancelled。

前者表示用户已经提出取消请求,后者表示任务确实已经停止。

还要考虑一种特殊情况:用户点击停止时,任务可能恰好已经完成。

例如,Worker 刚生成最终答案,取消请求才到达。这时系统需要根据任务状态和实际执行结果,决定最终记录为 completed 还是 cancelled,不能简单地让最后一次状态更新覆盖之前的结果。

七、任务取消后,已经执行的操作怎么办?

这是最容易被忽略的问题。

假设 Agent 正在处理订单:

1. 查询订单
2. 创建退款请求
3. 调用支付服务
4. 发送退款通知

用户在第 3 步之后点击停止。

此时,Agent 可以不再执行第 4 步,但如果退款请求已经被支付服务接受,取消 Agent 并不会自动撤销退款。

类似的情况还有:

  • 邮件已经发送。

  • 数据库修改已经提交。

  • 消息已经投递到队列。

  • 文件已经被删除。

  • 第三方服务已经创建了任务。

这些操作产生的结果,不能仅靠停止 Agent 来恢复。

因此,涉及外部操作时,需要提前设计好处理方式:

  • 避免重复执行: 使用幂等键,防止重试造成重复提交。

  • 记录执行结果: 即使 Agent 被取消,也能判断操作究竟成功还是失败。

  • 提供撤销能力: 如果业务支持撤销,调用对应的撤销接口。

  • 设计补偿流程: 如果操作无法直接撤销,就通过新的业务操作修正结果。

  • 必要时要求确认: 对支付、删除等重要操作,可以在执行前要求用户确认。

要记住,取消任务只能阻止后续工作,不能保证已经发生的操作全部恢复原状。

八、生产环境还要注意什么?

在真实项目中,除了基本的取消机制,还需要考虑几个常见问题。

任务超时。 用户不取消,任务也可能一直运行。因此应该设置模型请求超时、工具执行超时和任务总时长。超时可以复用取消机制,但应保留独立的终止原因。

重试。 如果工具执行失败后准备重试,重试之前必须再次检查取消状态,避免用户已经取消任务,系统却继续发起新请求。

并行任务。 如果 Agent 同时执行多个工具,取消时需要通知所有相关任务,并等待它们停止或达到清理超时,不能只取消其中一个。

页面断开。 用户关闭浏览器不一定代表想取消任务。对于后台执行的任务,应当将「客户端断开」和「用户明确取消」分开处理。

取消结果。 系统应该记录任务是被用户取消、超时、管理员终止还是执行失败。出现问题时,才能查清任务为什么停止。

这些功能不一定全部写在 Agent Loop 中,但应该由任务调度器、Agent Runtime 和工具执行层共同承担。

九、总结

用户中途取消 Agent 任务,看起来只是一个按钮,实际上涉及模型请求、工具执行、后台任务和任务状态等多个环节。

一个基本的实现流程是:

用户点击停止
    ↓
后端接收取消请求
    ↓
通知正在执行的任务
    ↓
Agent 不再启动新的操作
    ↓
正在运行的操作尽可能停止
    ↓
记录最终状态
    ↓
处理已经产生的副作用

设计时记住三点就够了:

  1. 停止输出,不等于停止任务。 后台执行也必须能够收到取消信号。

  2. 发出取消信号,不等于所有操作都会立即停止。 模型 SDK、工具和第三方服务都需要具备相应的取消能力。

  3. 任务取消,不等于撤销已经完成的操作。 已经发送的邮件、提交的支付和完成的数据库写入,需要单独处理。

对于 Agent 来说,取消机制和超时、重试、权限控制一样,都是运行时需要考虑的基础能力。只有把这些问题处理好,Agent 才不仅能执行任务,也能在不需要继续执行时及时停下来。