跳转至

第 05 步:Guard、HITL 与执行

这一节学什么

  • Human-in-the-loop 不只是同步 y/n 确认
  • pending approval 如何让运行时挂起
  • host / UI / 外部审批系统如何用 request_id 恢复执行
  • guard、approval、execution 在新 HITL 主路径里的位置

你会运行什么

示例代码:

  • examples/getting_started/step_05_add_guard_and_execution.py

运行:

python examples/getting_started/step_05_add_guard_and_execution.py

这个示例不依赖 LLM。它直接给 ToolExecutor 一个 tool call,让你只关注 HITL runtime 本身。

主心智

  1. Agent、Node 或 Tool 触发一个需要人工介入的动作
  2. ApprovalManager 返回 pending
  3. runtime 生成一个带 request_id 的 suspension
  4. host、UI、IM bot 或审批系统把这个 suspension 展示给人
  5. 人做出决定后,host 用同一个 request_id resume
  6. runtime 从 checkpoint 恢复并继续执行原来的工作流

也就是说,HITL 是一种运行时控制流,而不只是一个同步确认函数。

这一节和 Step 06 的边界

这一节很容易和下一节混在一起,但它们关注的问题不同:

  • Step 05 关心“工具调用时,运行时允许什么、不允许什么,以及卡住之后怎么恢复”
  • Step 06 关心“工具本身该怎么定义,怎样暴露给模型”

可以这样区分:

  • Tool 决定“这个能力是什么”
  • ToolSet 决定“当前这组能力有哪些”
  • GuardConstraints 决定“哪些能力允许被调用”
  • ApprovalManager 决定“调用前是直接批准、直接拒绝,还是进入 pending”
  • ExecutionConstraints 决定“调用发生在哪个执行环境”

也就是说:

  • Tool / ToolSet 更偏能力建模
  • Guard / Approval / Execution 更偏运行时治理

如果你把这两层混在一起,后面会很难排查问题。比如:

  • tool 调不到,不一定是 tool 写错了,也可能是 tool_policy="deny"
  • tool schema 没问题,不代表它一定能执行,还可能在 approval 阶段被挂起
  • prompt 写得再好,也不能替代 budget、confirm、sandbox 这种运行时约束

guard、checker、approval、execution 各自管什么

这三个概念仍然重要,但它们的位置要换一种看法:

  • GuardConstraints
  • 决定什么动作需要管控
  • 比如 tool_policy={"charge_card": "confirm"}
  • 启用 Permission V2 后,也可按 action/resource/effect 表达规则
  • checker
  • 是 guard 真正执行检查时的一组规则
  • 比如 ToolPolicyChecker、RateLimitChecker、budget 相关 checker
  • 解决“这次调用在运行时具体过不过”
  • ApprovalManager
  • 决定这次管控动作是批准、拒绝,还是进入 pending
  • pending 会把控制权交给外部 host
  • ExecutionConstraints
  • 决定审批通过之后,工具实际在哪里执行
  • 比如本地、Docker、受限 runner

所以 Step 05 的主线不是“怎么写 tool”,也不是“怎么手工拼 checker”,而是:

  • checker 决定当前调用的治理结果
  • approval 决定是否需要人参与
  • suspension / resume 决定人参与之后怎么继续往下跑

主线是:

tool call
  -> guard says confirm
  -> approval says pending
  -> runtime suspends
  -> host resolves approval
  -> runtime resumes
  -> tool executes

Permission V2:按能力和资源治理

tool_policy 仍完整保留;未配置 guard.permission 时行为不变。启用 Permission V2 后,GuardEnforcer 会选择 PermissionRuleChecker 作为本次 调用唯一的权限 checker。同一个 guard 配置中,guard.permission 不能和 tool_policy 或 tool_rules 混用:

guard:
  permission:
    default_effect: allow
    rules:
      - action: edit
        resource: "*"
        effect: ask
      - action: edit
        resource: "docs/**"
        effect: allow
      - action: edit
        resource: ".env*"
        effect: deny

规则按书写顺序匹配,最后一条命中规则覆盖前面的规则。一个调用影响多个 资源时,整体按 deny > ask > allow 聚合。工具在 ToolExecutor 执行前 根据真实参数生成资源;例如 write_file、str_replace、apply_patch 和 move_path 都归为 edit。ask 继续复用现有 ApprovalManager 和 suspend/resume,不会产生第二套审批运行时。

ApprovalManager.reply_request() 支持 once、always、reject。 always 需要 host 注入 grant store 与 opaque scope_key;saved allow 不能覆盖 configured deny。Permission 只是策略层,不替代 filesystem、 network 或 container sandbox。

tool_policy、checker 和 HITL 是什么关系

可以先把 tool_policy 理解成这条链路的最前面一层开关:

  1. allow
  2. 直接放行,不进入 approval
  3. confirm
  4. 允许进入 approval 流程
  5. approval 可能返回 approved / rejected / pending
  6. deny
  7. 直接拒绝,不进入执行阶段

旧版 Step 05 里强调的那层心智其实还在,只是现在要把它接到 HITL 主线上看:

  • confirm 不是“弹一个框”这么简单
  • 它的真正含义是“这次调用需要经过 approval”
  • 如果 approval 返回 pending,它就会升级成一个可恢复的 runtime suspension

也就是说:

  • guard / checker 负责决定“这次是不是要受控”
  • approval 负责决定“这次受控动作的结论是什么”
  • HITL runtime 负责决定“如果结论暂时拿不到,系统怎么停下来并在之后恢复”

运行时控制面的最小心智

你可以把一次受控 tool call 粗略理解成下面这条链路:

  1. Agent 或节点决定要调用哪个 tool
  2. guard checker 先检查策略、次数、预算等限制
  3. 如果 checker 结果要求确认,approval 决定批不批准,或者返回 pending
  4. 如果 approval 是 pending,runtime 发布 suspension,交给 host
  5. host 恢复之后,execution 决定用什么执行环境真正跑这个 tool
  6. 结果再回到 state / message observation

这也是为什么这一节示例没有强调 prompt,而是直接用 ToolExecutor:

  • 它把注意力集中在“工具调用之后的运行时控制”
  • 不把 LLM 规划、prompt 设计和 HITL 机制混在一个例子里

pending 和 suspended 的区别

这两个词很容易混在一起:

  • pending 是 approval 领域的状态
  • 表示这个审批还没有被外部人类或系统决定
  • suspended 是 runtime 领域的状态
  • 表示 runner 已经停在一个可恢复的交互点

一个 approval 返回 pending 后,runner 会把它发布成 require_approval suspension。

host 看到的关键字段是:

  • request_id
  • reason
  • category
  • message
  • payload

其中 request_id 最重要。resume 时必须带回同一个 id,否则 runtime 会拒绝恢复。

为什么需要 checkpoint

HITL 经常跨越一次进程生命周期:

  • Agent 今天发起审批
  • 用户半小时后才批准
  • 服务可能已经重启
  • runner 需要从挂起点恢复,而不是重新跑一遍前面的步骤

所以示例里会显式使用 InMemoryCheckpointer。

真实应用里,你通常会换成文件、数据库或应用自己的持久化层。关键原则是一样的:suspend 时保存 checkpoint,resume 时按同一个 thread_id 和 request_id 恢复。

另外,如果你的 state schema 是 Pydantic model,HITL 示例需要允许 runtime 写入交互元数据:

class HITLState(BaseModel):
    model_config = ConfigDict(extra="allow")

这是因为 suspension、resume payload 和 approval progress 会作为运行时元数据挂到 state manager 里。

示例在演示什么

示例流程现在分成两段:

  1. guard / checker 基础
  2. calculator 配成 confirm,由 approval callback 自动放行
  3. 演示 max_tool_calls=1 如何拦下一批超限的 tool call
  4. 演示 python_repl 如何被 deny 策略直接拒绝
  5. HITL suspend / resume
  6. 定义一个 charge_card 工具
  7. 配置 tool_policy={"charge_card": "confirm"}
  8. ApprovalManager 对这类请求返回 ApprovalState.PENDING
  9. 调用 agent.run_until_suspend(...)
  10. 打印 runtime 产生的 suspension
  11. 模拟外部审批系统批准
  12. 调用 agent.resume_interaction(...)
  13. 工具真正执行,并写回 observation

也就是说,这个例子不是把旧版 guard/checker 思路扔掉,而是先把它跑一遍,再把同一个 confirm 入口接到新的 HITL runtime 主线上。

最小代码形状

核心形状大概是这样:

approval_manager = ApprovalManager(callback=request_external_approval)

agent = Agent(
    ToolExecutor(name="PaymentTools", tools=[ChargeCardTool()]),
    state_schema=HITLState,
    context=RunContext(
        constraints=Constraints(
            guard=GuardConstraints(tool_policy={"charge_card": "confirm"})
        ),
        approval_manager=approval_manager,
    ),
    checkpointer=checkpointer,
    thread_id="getting-started-hitl",
)

first = await agent.run_until_suspend(
    input_data={
        "actions": [
            ToolCall(
                name="charge_card",
                arguments={"customer_id": "cus_123", "amount": 42.5},
            )
        ]
    }
)

resume = await agent.resume_interaction(
    request_id=first.suspension.request_id,
    payload={"approved": True, "reason": "approved in external UI"},
)

注意这里不是直接调用 approval_manager.resolve_request(...)。

Getting Started 里推荐先通过 resume_interaction(...) 理解 host-facing 主路径。更底层的 manager resolve / subscription 适合应用框架、后台服务或长驻 runtime 集成。

旧版内容哪些还有效

旧版 Step 05 里讲的基础内容并没有过时,它们只是变成了 HITL 主线下面的子层:

  • 同步 approval 仍然可用
  • TTY 里的 y/n 仍然可用
  • tool_policy="deny" 仍然可以直接拒绝高风险工具
  • max_tool_calls、budget、execution sandbox 仍然是运行时治理的一部分
  • 手工拼 GuardEnforcer 仍然是高级定制入口

如果你读完这一节只能记住一句话,应该是:

  • Step 05 既在教你“怎么约束 tool 的运行”,也在教你“约束之后如果需要人参与,runtime 怎么停下来并继续执行”

运行后观察什么

你应该能看到:

  • 第一次运行返回 outcome="suspended"
  • suspension 的 category 是 require_approval
  • suspension payload 里包含 tool 名称、tool call id、审批阶段等信息
  • resume payload 里只需要给出 approved: true/false
  • 审批通过后,工具才会真正执行
  • final state 里出现 tool observation

如果把 resume payload 改成 {"approved": False, "reason": "not allowed"},工具不会执行,工作流会以失败或拒绝观察结束。

下一步