# AI Agent 的停止按钮为什么不好做：取消信号、工具进程、会话历史与执行状态

**URL:** <https://www.sunai.net/t/topic/1517>\
**Category:** IT\
**Tags:** ai, 编程\
**Created:** [2026 年10 月 7 日 00:25 UTC](https://www.sunai.net/t/topic/1517 "2026-10-07T00:25:35Z")\
**Posts on this page:** 1\
**Showing post:** 2

<div class="post-metadata">

作者： ![Logos](https://www.sunai.net/user_avatar/www.sunai.net/logos/32/2077_2.png) [@Logos](https://www.sunai.net/u/Logos)\
发布日期： [2026 年10 月 7 日 00:26 UTC](https://www.sunai.net/t/topic/1517/2 "2026-10-07T00:26:55Z")

</div>

## 补充：术语、执行记录与取消测试清单

下面补几项实现细节。数据字段与测试标准是设计示例，可按自己的系统调整；API 行为另附官方来源。

### 1. 几个容易混淆的术语

| 术语 | 在本文中的含义 |
| --- | --- |
| 协作式取消 | 发出停止请求，由正在执行的代码观察信号并退出；不会自动强制终止任意代码 |
| 强制终止 | 使用操作系统机制结束进程，不能依赖程序执行完自己的业务清理 |
| 超时 | 时间预算耗尽，通常触发取消；仍需处理已启动工作和结果未知的情况 |
| 竞态 | 取消、完成、派发等事件时间上重叠，最终结果取决于实际发生顺序和同步设计 |
| 幂等 | 对同一操作重复执行，预期业务效果不被重复叠加；具体接口的保证范围需要查文档 |
| 补偿 | 用新的业务操作处理先前操作的影响，不等于原操作从未发生 |
| 安全边界 | 系统选定的可检查取消、接收新要求或持久化进度的位置；需要明确它能保护哪些步骤 |

AbortController 与 AbortSignal 是一组控制器和信号：前者发出取消，后者供下层观察。Go 的 Context 还携带截止时间等信息。.NET 则使用 CancellationTokenSource 发出通知，由 CancellationToken 的接收方协作处理。它们都是机制，不应直接等同于某个 Agent 产品的完整停止实现。[1][2][3]

### 2. Go 的 CommandContext 不是完整的进程树管理器

`exec.CommandContext` 的默认取消行为是调用直接启动进程的 Kill；它不会自动提供“先 SIGTERM、再 SIGKILL、覆盖所有后代”的完整流程。官方允许在启动前定制 `Cmd.Cancel` 和 `Cmd.WaitDelay`。[4]

`WaitDelay` 可以限制文档列出的进程退出和管道关闭等待，但不会把脱离管理的后代自动归入自己的终止范围。它也不是所有阻塞的通用超时器：官方文档提醒，阻塞的输入 Reader 或输出 Writer 仍可能让 Wait 等待，因此日志接收端与 I/O 生命周期也要单独设计。[4]

建议命令工具先明确三个问题：直接启动的是业务程序还是 shell？谁负责管理后代？输出的读取和关闭由谁负责？解决这些问题之后，再封装跨平台执行器。

### 3. 执行记录最好同时保存“结果”和“确定程度”

考虑一个示例：Agent 调用部署工具，远端返回前本地取消。建议保存的内部记录可以包括：

| 字段 | 示例 |
| --- | --- |
| run\_id | 本轮任务 ID |
| tool\_call\_id | 模型工具调用 ID |
| operation\_id | 远端业务操作 ID |
| execution\_state | 结果未知 |
| cancel\_requested | 是 |
| remote\_cancel\_state | 已请求，未确认 |
| side\_effect\_state | 待核实 |
| last\_observation | 请求已提交，未获得最终状态 |
| recovery\_action | 查询原部署任务，不直接再次部署 |

这不是任何模型 API 的请求格式。内部记录应先保存事实，再由适配层转换成目标 API 接受的工具结果。

建议保留 run\_id、工具调用 ID 和业务操作 ID 的映射，不要把三者混为一个 ID：一次模型调用可以触发多项工具执行；同一业务操作也可能跨越重试和恢复。

对输出还应记录是否截断、最后观测时间和来源。只拿到一段日志时，可以记录“已知输出”，不能据此声称掌握完整执行结果。

### 4. “检查一下取消状态”还有一个时间缝隙

下面是一种假设时序：

1. 调度器检查：任务还未取消。
2. 用户点击停止，任务进入 cancelling。
3. 调度器按照刚才的检查结果启动工具。

建议让“关闭本轮入口”和“为工具取得启动许可”通过同一个受控调度机制协调，比如串行调度器、锁或具备条件更新的状态存储。启动许可要能对应执行记录，不能只是读一个布尔值。

已经获得许可、正在启动的工具，则应归入运行中工作，由取消流程继续管理。

这里的设计目标是明确工作归属、减少竞态，并不意味着可以把本地状态更新与任意远端副作用变成一个原子操作。外部请求已经发出而记录尚未完成时，仍可能需要状态查询和幂等保护。

### 5. 保存了执行日志，也不代表解决了所有恢复问题

建议在执行前写下操作意图，执行后保存结果。这样发生崩溃时至少能知道哪些操作需要核实。

但要区分两种假设情况：

- 记录了“准备发送”，随后崩溃，实际还没有发送。
- 已经发送成功，保存成功结果之前崩溃。

如果本地只留下相同的“准备发送”记录，恢复程序就无法单靠这条记录判定实际结果。不能把所有未结束记录都自动重试，也不能全部改成成功。

建议给有副作用的工具定义恢复策略：能否查询原操作、能否复用幂等键、是否需要人工确认、是否有补偿动作。工具接口如果缺少这些能力，应如实暴露限制。

### 6. 取消机制的测试不要只测 sleep

下面是一份建议的故障注入清单。测试应观察进程、队列、执行记录、模型历史与业务结果，不能只看界面是否停止输出。

| 场景 | 建议验收点 |
| --- | --- |
| 工具尚在排队时取消 | 没有启动；历史描述为未执行 |
| 重试退避时取消 | 等待及时结束，没有下一次普通重试 |
| 模型流只返回半截工具参数 | 不执行不完整调用，不生成虚假的完成结果 |
| shell 派生长时间运行的子进程 | 确认管理范围内的工作全部退出 |
| 后代建立独立会话 | 能发现残留或明确报告管理边界 |
| 主进程退出，后代保持输出管道 | 读取不无限等待，保留输出截断情况 |
| 工具忽略优雅退出请求 | 在设定时限后升级终止，并确认实际状态 |
| 并行工具中有的成功、有的取消 | 每个调用单独保存结果与对应 ID |
| 工具成功与取消同时发生 | 不覆盖可核实的成功结果，也不继续派发后续工作 |
| 远端完成，但响应丢失 | 状态保留为未知或经查询核实，不直接重复写操作 |
| 连点停止按钮 | 不重复补结果、不重复执行补偿 |
| Agent 在保存结果前崩溃 | 重启后能识别未确认操作并按恢复策略处理 |
| 取消后立即开始新一轮 | 新旧运行隔离，旧回调不会串改新一轮状态 |
| Steering 后紧接着取消 | 不把待应用的新要求作为继续执行的理由 |

最后一项可以再扩展：应用侧 Steering 队列中的消息如果未被模型使用就发生取消，建议保留“未应用”标记，由用户决定是否用于下一轮，而不是默默丢掉或自动执行。

### 参考资料

[1] [WHATWG DOM：Aborting ongoing activities](https://dom.spec.whatwg.org/#aborting-ongoing-activities)

[2] [Go：context](https://pkg.go.dev/context)

[3] [Microsoft：Cancellation in Managed Threads](https://learn.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads)

[4] [Go：os/exec](https://pkg.go.dev/os/exec)

---

_[View the full topic](https://www.sunai.net/t/topic/1517)._
