很多 AI agent 系统把“取消”理解成一个前端按钮。用户点了停止,请求断掉,似乎就结束了。但真实问题恰恰在这里才开始:模型可能还在生成、工具请求可能已经发出、外部写操作可能已进入 outbox、worker 可能还持有 lease。
所以取消、中断和安全停机解决的不是“怎么停”,而是“停下来以后,系统还能不能保持一致”。如果这个问题没有被建模,取消只是把混乱藏到后台。
建议先结合 AI agent Checkpoint 与断点恢复、AI agent Worker Lease 与心跳机制、AI agent 人工审批控制台设计 和 AI agent Run Ledger 审计模型 一起看。
先给结论:取消不是失败态,而是一种显式状态迁移
| 场景 | 触发源 | 正确收口 |
|---|---|---|
| 用户主动停止 | 前端操作 | 终止后续步骤,保留已生成 artifact |
| 系统预算耗尽 | orchestrator | 进入 safe stop 或 degrade |
| 人工接管 | review console | 冻结自动执行,转 handoff |
| 依赖异常中断 | worker / 下游服务 | 记录中断点,等待恢复或人工 |
如果取消只表现为“连接断开”,系统就无法知道自己现在是可恢复、可移交,还是应该直接关闭。
一、先区分取消、失败和挂起
这三种状态经常被混用,但意义完全不同:
- 取消:有人明确要求停止自动继续
- 失败:系统尝试继续,但没有成功
- 挂起:当前不继续执行,等待外部条件满足
如果把三者都写成 failed,后续恢复和审计就会全部混乱。
二、取消信号需要变成统一事件,而不是到处 if abort
更稳的方式是把取消写成结构化事件:
{
"runId": "run_123",
"signal": "cancel_requested",
"requestedBy": "user",
"reason": "user_clicked_stop",
"requestedAt": "2026-05-10T10:00:00Z"
}
这样 orchestrator、worker、tool gateway 和 review console 才能围绕同一事件工作,而不是各自定义“停止”的含义。
三、安全停机的核心不是终止线程,而是冻结新的副作用
一个最小 safe stop 协议通常至少包含这些动作:
- 不再接收新的工具调用
- 冻结外发与写入型动作
- 记录当前 step 与已产生的 artifact
- 更新 run 状态为
cancelling或interrupted - 释放或转移 lease
重点在于:系统必须知道哪些东西已经发生,哪些还不该继续发生。
很多团队做到这里还不够,因为“停止中”本身也需要状态流。一个更稳的 safe stop 状态机通常长这样:
| 状态 | 允许动作 | 不允许动作 |
|---|---|---|
cancel_requested | 标记停止原因、冻结新任务 | 再次派发新 tool call |
draining | 等待只读步骤结束、回收 lease | 发起新副作用 |
stopped_cleanly | 进入 handoff 或 resume 决策 | 自动恢复执行 |
stopped_dirty | 对账、补记 ledger、人工处理 | 直接假装未发生 |
有了这层状态以后,系统才能区分“停住了”和“看起来停住了”。
四、运行中断后要先判断“能不能干净停住”,再决定要不要恢复
真实中断通常发生在这些位置:
| 中断位置 | 风险 | 常见收口 |
|---|---|---|
| 模型生成中 | 结果不完整 | 丢弃临时片段,保留 step trace |
| 工具读请求中 | 无副作用 | 可直接重试或终止 |
| 外发动作前 | 可冻结 | 等待人工或 safe stop |
| 外发动作后 | 需查账本 | 不能假设没发生 |
也就是说,并不是所有中断都能“立刻停止后原样恢复”。有些中断只适合进入 AI agent Checkpoint 与断点恢复 的安全恢复流程。
五、人工接管不是简单改 owner,而是完整 handoff
当取消是为了转人工时,系统至少要移交这些东西:
- 当前 run 状态和停止原因
- 已完成步骤和未完成步骤
- 当前可复用的 artifact 与 evidence
- 已冻结的副作用和 outbox 项
否则人工接手以后仍然得从日志和聊天记录里猜系统停在了哪里。
更进一步,handoff 最好不是口头意义上的“有人接手”,而是一份显式 stop packet:
{
"runId": "run_123",
"stopEpoch": 5,
"stopReason": "manual_takeover",
"frozenOutboxIds": ["outbox_11"],
"leaseReleased": true,
"resumeFrom": "checkpoint_04"
}
stopEpoch 这类字段很关键,它能帮助下游拒绝旧 worker 在停止后继续写回,避免出现“系统已经停了,但旧执行者还在落地结果”的脏写问题。
六、取消逻辑要和 Review Console、Lease、Ledger 一起联动
一次健康的停止流程,至少会影响三个面:
- AI agent Worker Lease 与心跳机制:释放执行权,避免旧 worker 继续跑
- AI agent 人工审批控制台设计:显示停止原因和可接手动作
- AI agent Run Ledger 审计模型:记录是谁、在什么时候、因为什么触发了停止
如果这三层没有联动,取消只会停住前台,不会停住系统事实。
七、上线后要观察“停止质量”,而不是只看取消次数
建议持续记录:
| 指标 | 用途 |
|---|---|
| cancel requested to safe stop latency | 看系统能多快真正停住 |
| cancelled runs with side-effect leakage | 看停止后是否还有外发漏出 |
| late write after stop count | 看停止后是否还有旧 worker 写回 |
| interruption recovered cleanly ratio | 看中断后是否能稳定收口 |
| manual handoff completion rate | 看移交流程是否真的可用 |
如果取消次数不高,但 side-effect leakage 持续存在,说明真正的问题不是用户爱不爱点停止,而是系统根本停不干净。
八、失败案例:用户点了停止,但消息还是发出去了
某个通知 agent 在人工审批前允许用户点“停止生成”。某次用户点停后,前端立刻提示已取消,但后台 outbox dispatcher 仍按旧状态扫描到准备发送的消息,最终真实外发。
根因有三个:
- 取消只中断了前端请求,没有更新 run 状态
- outbox 没有读取 cancel 信号
- ledger 里也没有记录此次 stop request
修复后,团队把停止动作统一改成 cancel_requested -> safe_stop -> outbox_frozen 的状态流,前后台才真正一致。
九、取消与安全停机 Checklist
- 是否区分取消、失败和挂起三种状态
- cancel signal 是否被结构化记录,而不是只中断连接
- safe stop 是否先冻结新的副作用,再停止执行
- 是否有
cancel_requested -> draining -> stopped_*的显式状态流 - 中断位置不同,是否有不同收口动作
- 人工接管时是否有完整 handoff packet
- stopEpoch 或 fencing token 是否能阻断旧 worker 继续写回
- 是否与 lease、review console、ledger 联动
- 是否监控 side-effect leakage 和 safe stop latency
结语
AI agent 的取消、中断与安全停机,不是“给用户一个停止按钮”,而是把“何时停止、停到什么程度、停后交给谁”变成显式协议。只有这样,系统才不会在看似已经结束的时候,继续偷偷推进旧动作。
延伸阅读:


