第 05 步:Guard、HITL 与执行¶
这一节学什么¶
- Human-in-the-loop 不只是同步
y/n确认 pendingapproval 如何让运行时挂起- host / UI / 外部审批系统如何用
request_id恢复执行 - guard、approval、execution 在新 HITL 主路径里的位置
你会运行什么¶
示例代码:
examples/getting_started/step_05_add_guard_and_execution.py
运行:
这个示例不依赖 LLM。它直接给 ToolExecutor 一个 tool call,让你只关注 HITL runtime 本身。
主心智¶
- Agent、Node 或 Tool 触发一个需要人工介入的动作
ApprovalManager返回pending- runtime 生成一个带
request_id的 suspension - host、UI、IM bot 或审批系统把这个 suspension 展示给人
- 人做出决定后,host 用同一个
request_idresume - 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会把控制权交给外部 hostExecutionConstraints- 决定审批通过之后,工具实际在哪里执行
- 比如本地、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 理解成这条链路的最前面一层开关:
allow- 直接放行,不进入 approval
confirm- 允许进入 approval 流程
- approval 可能返回 approved / rejected / pending
deny- 直接拒绝,不进入执行阶段
旧版 Step 05 里强调的那层心智其实还在,只是现在要把它接到 HITL 主线上看:
confirm不是“弹一个框”这么简单- 它的真正含义是“这次调用需要经过 approval”
- 如果 approval 返回
pending,它就会升级成一个可恢复的 runtime suspension
也就是说:
- guard / checker 负责决定“这次是不是要受控”
- approval 负责决定“这次受控动作的结论是什么”
- HITL runtime 负责决定“如果结论暂时拿不到,系统怎么停下来并在之后恢复”
运行时控制面的最小心智¶
你可以把一次受控 tool call 粗略理解成下面这条链路:
- Agent 或节点决定要调用哪个 tool
- guard checker 先检查策略、次数、预算等限制
- 如果 checker 结果要求确认,approval 决定批不批准,或者返回
pending - 如果 approval 是
pending,runtime 发布 suspension,交给 host - host 恢复之后,execution 决定用什么执行环境真正跑这个 tool
- 结果再回到 state / message observation
这也是为什么这一节示例没有强调 prompt,而是直接用 ToolExecutor:
- 它把注意力集中在“工具调用之后的运行时控制”
- 不把 LLM 规划、prompt 设计和 HITL 机制混在一个例子里
pending 和 suspended 的区别¶
这两个词很容易混在一起:
pending是 approval 领域的状态- 表示这个审批还没有被外部人类或系统决定
suspended是 runtime 领域的状态- 表示 runner 已经停在一个可恢复的交互点
一个 approval 返回 pending 后,runner 会把它发布成 require_approval suspension。
host 看到的关键字段是:
request_idreasoncategorymessagepayload
其中 request_id 最重要。resume 时必须带回同一个 id,否则 runtime 会拒绝恢复。
为什么需要 checkpoint¶
HITL 经常跨越一次进程生命周期:
- Agent 今天发起审批
- 用户半小时后才批准
- 服务可能已经重启
- runner 需要从挂起点恢复,而不是重新跑一遍前面的步骤
所以示例里会显式使用 InMemoryCheckpointer。
真实应用里,你通常会换成文件、数据库或应用自己的持久化层。关键原则是一样的:suspend 时保存 checkpoint,resume 时按同一个 thread_id 和 request_id 恢复。
另外,如果你的 state schema 是 Pydantic model,HITL 示例需要允许 runtime 写入交互元数据:
这是因为 suspension、resume payload 和 approval progress 会作为运行时元数据挂到 state manager 里。
示例在演示什么¶
示例流程现在分成两段:
- guard / checker 基础
calculator配成confirm,由 approval callback 自动放行- 演示
max_tool_calls=1如何拦下一批超限的 tool call - 演示
python_repl如何被deny策略直接拒绝 - HITL suspend / resume
- 定义一个
charge_card工具 - 配置
tool_policy={"charge_card": "confirm"} ApprovalManager对这类请求返回ApprovalState.PENDING- 调用
agent.run_until_suspend(...) - 打印 runtime 产生的 suspension
- 模拟外部审批系统批准
- 调用
agent.resume_interaction(...) - 工具真正执行,并写回 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"},工具不会执行,工作流会以失败或拒绝观察结束。