跳到主要内容

Tool Approval、Interrupt 与 Resume

做 Agent 系统时,审批流很容易停在概念层:

  • 哪些动作要审批
  • 哪些动作可以自动执行
  • 哪些步骤需要人工接手

真正开始实现之后,问题会立刻变具体:

  • 工具调用是在什么地方被拦住的
  • 系统暂停之后,状态存在哪里
  • 审批结果怎样回到原来的执行链里
  • reject、edit、timeout 之后流程怎么继续

这里专门讨论这一层。

参考资料主要来自:

1. 先把 3 个词分开

1.1 Tool Approval

Tool approval 讨论的是:

某个工具调用在真正执行前,是否要先经过人工确认。

它关注的是工具级策略。

1.2 Interrupt

interrupt 讨论的是:

系统在执行过程中,能不能安全地停下来,把控制权暂时交出去。

它关注的是暂停点和状态保存。

1.3 Resume

resume 讨论的是:

暂停之后,系统怎样从原来的位置继续往下跑。

它关注的是恢复执行。

把这三件事连起来看,会更容易理解实现链路:

  • 先决定某个工具是否需要 approval
  • 如果需要,就触发 interrupt
  • 人工决定之后,再用 resume 继续执行

2. 最小执行链路

一个最小实现通常会长这样:

这里先看 4 个实现要点:

  • 暂停前状态已经保存
  • 审批数据结构是明确的
  • resume 时知道恢复到哪个执行点
  • reject 不是报错,而是流程分支

3. Tool approval 放在哪一层

常见有 3 种做法。

3.1 放在 tool policy 层

也就是先定义:

  • 哪些工具需要审批
  • 哪些参数组合需要审批
  • 哪些风险级别需要审批

这种做法最适合:

  • 工具边界清楚
  • 风险主要跟工具种类有关
  • 主流程本身不复杂

例如:

  • send_email
  • delete_file
  • run_sql
  • create_payment

这类工具通常适合先挂 tool policy。

3.2 放在 workflow 节点层

也就是把审批看成流程中的一个显式节点。

这种做法更适合:

  • 审批本身就是业务流程的一部分
  • 审批可能跨时间、跨人、跨角色
  • 需要 checkpoint / resume
  • 需要审计和追踪

3.3 放在 guardrail / middleware 层

也就是让工具调用先经过一层统一检查,再决定是否打断。

这种做法更适合:

  • 工具很多
  • 想集中做风险控制
  • 希望把策略和业务节点分开

4. interrupt 到底做了什么

从实现角度看,interrupt 不是“弹个框”,而是 4 件事:

  1. 停止当前执行链
  2. 暴露一个可供人审的 payload
  3. 保存当前状态
  4. 等待外部输入后继续

在 LangGraph 里,这个模式很明确:

  • 调用 interrupt(...)
  • graph 停在这里
  • checkpointer 保存状态
  • 后面通过 Command({ resume: ... }) 恢复

这一点需要单独记住。

如果没有可靠的状态保存,所谓 interrupt 往往只是:

  • 前端拦一下
  • 后端重新跑一遍
  • 或者重新生成一次 tool call

这种实现很容易出问题,尤其在下面几类场景里:

  • 有副作用的工具
  • 长链路任务
  • 多步推理后才出现的写操作

5. resume 不是“重新开始”

resume 的重点不是重跑,而是:

从暂停点继续。

这意味着实现上至少要满足两件事:

5.1 能定位到原来的执行上下文

也就是要知道:

  • 当前是哪条线程 / run
  • 中断发生在哪个节点
  • 当前 state 是什么
  • 当前还挂着哪些未处理 interrupt

5.2 能把审批结果送回暂停点

例如:

  • approve
  • reject
  • edit 后的新参数
  • 额外人工说明

如果 resume 只是“重新把用户问题发一遍”,那就不算真正意义上的 resume。

6. approve / edit / reject 的实现分支

6.1 approve

最简单。

  • 原参数执行
  • trace 记一条 approval 记录
  • 后续流程继续

6.2 edit

这一支经常被低估。

实际业务里很常见:

  • 邮件内容要改
  • SQL 条件要缩小
  • 金额要修正
  • 收件人要替换

所以 interrupt payload 最好不要只支持布尔值,还要支持:

  • 参数修改
  • 批注
  • 人工补充字段

6.3 reject

reject 之后一般有 3 种处理:

  • 直接结束
  • 回到规划步骤
  • 转人工完全接管

如果 reject 后只是抛异常,链路通常不够完整。

7. 一个最小 payload 该包含什么

审批 payload 至少应该有这些字段:

{
toolName: string,
reason: string,
args: Record<string, unknown>,
riskLevel: "low" | "medium" | "high",
reversible: boolean,
traceId: string,
runId?: string,
suggestedAction?: "approve" | "edit" | "reject"
}

如果还要更实用,可以再加:

  • 目标对象
  • 影响范围
  • 参数 diff
  • 上一步来源
  • 是否命中某条策略

8. 超时、断线和重复提交怎么处理

这一层如果不处理,审批流上线后会很不稳。

8.1 超时

常见策略:

  • 超时自动 reject
  • 超时转人工队列
  • 超时后把 run 标成 pending review

8.2 断线

关键是中断状态要落盘。

只要 state 和 interrupt id 还在,前端掉线后也能重新接上。

8.3 重复提交

审批动作要尽量幂等。

例如:

  • 同一个 interrupt 只能被最终处理一次
  • 第二次 approve 不应再次执行 tool
  • edit 后再 approve,要能区分版本

9. 多 interrupt 怎么处理

当系统开始并行执行时,会遇到一个更实际的问题:

多个分支同时停下来怎么办?

LangGraph 官方也专门提到过这一点:

  • 并行分支可能同时触发多个 interrupt
  • resume 时需要按 interrupt id 对应各自的 resume value

这一层最好尽早定清:

  • 是串行处理
  • 还是支持批量审批
  • 还是由不同审批人分别处理

10. 哪些动作最适合先做 interrupt

第一批通常先选这类:

  • send_email
  • run_sql
  • delete_*
  • write_*
  • publish_*
  • payment_*
  • browser / computer use 中带外部副作用的动作

理由很简单:

  • 副作用明确
  • 风险清楚
  • 容易定义 policy
  • 最容易做出第一版闭环

11. 和现有专题里的关系

这篇文档最适合和下面几篇一起看:

12. 小结

这条实现链路可以压缩成 3 步:

  • tool approval 决定哪些动作要停
  • interrupt 负责把链路安全停下
  • resume 负责把链路从原位置接起来

如果这三层分清了,审批流就不再只是一个概念,而会开始变成可实现、可调试、可审计的系统能力。