人工审批 (HITL × Card)
让 AI 在执行高危操作(删数据库、转账、发邮件等)前先问人——以企业微信 / 飞书 / 微信卡片的形式弹出,用户点"批准 / 拒绝 / 编辑"决定继续与否。
是什么 / 不是什么
是:
- AI 调用某个工具前,平台拦截一下,把"想做的事"渲染成卡片或文字推给指定用户
- 用户点击决策(批准 / 拒绝 / 编辑参数),平台把决策回传给 AI 让它继续跑
- 不论隔了多久(几分钟或第二天再点)都能从中断处恢复
不是:
- 不是给 AI 输出加审核(输出已生成才看是不卡的事)
- 不是限流 / 配额(按调用次数限制)
- 不是基于 args 内容动态判断("只对 prod 环境的 delete 审批"是 V2 范围)
三层配置模型
任何一层都能开启 HITL,下层覆盖上层(应用 > 编排器 > 工具自身)。
┌───────────────────────────────────────────┐
│ 第 1 层:MCP 工具自身(兜底默认) │
│ "delete_db 这个工具天生危险,所有人用都拦" │
└───────────────────────────────────────────┘
↓ 合并
┌───────────────────────────────────────────┐
│ 第 2 层:编排器(业务场景) │
│ "客服编排器里 send_email 都要审" │
│ 或"安全编排器整个静默" │
└───────────────────────────────────────────┘
↓ 合并
┌───────────────────────────────────────────┐
│ 第 3 层:应用(部署环境覆盖) │
│ "生产 corp 必须审,开发 corp 关掉" │
└───────────────────────────────────────────┘
↓
最终生效的 HITL 规则优先级:后者覆盖前者。但有两个特殊"快速开关"——
- 静默开关
hitl_disabled(编排器层 + 应用层都有):开启后整层无视所有规则,OR 合并——任一层勾了就静默。 - per-tool false 在哪一层都能"显式禁用"上层声明。
第 1 层:MCP 工具
谁配? 工具作者 / 管理员 配什么? "这个工具默认是否危险"
在 Web 控制台配
打开 MCP 工具 → 选中工具 → 编辑:
- 需要人工审批 开关 → 勾上代表此工具默认要审批
- 允许的决策 多选:
- ✅ 批准(必选)
- ❌ 拒绝
- ✏️ 编辑参数(高级,让用户改 args 后再执行)
- 卡片描述模板(可选):卡面给用户看的文案,支持占位符
{tool_name}→ 工具名{tool_args}→ 完整 args JSON{<arg_key>}→ 单个 args 字段,例如{command}- 例:
AI 想执行 shell:{command}
用 CLI 配
# 创建时直接配
siracli mcp create -n delete_db -t service \
--protocol streamable-http --service-url https://internal/mcp \
--requires-approval \
--approval-decisions approve,reject \
--approval-description "AI 想清空数据库 {target}"
# 后期改
siracli mcp update <tool-id> --requires-approval
siracli mcp update <tool-id> --no-requires-approval
siracli mcp update <tool-id> --approval-decisions approve,reject,edit
siracli mcp update <tool-id> --approval-description "确认执行 {tool_name}"
# 查看
siracli mcp info <tool-id> # 末尾会显示 HITL Approval 区块Service 类型工具的精细控制(白/黑名单)
一个 MCP service 通常暴露 N 个工具(如 tavily-mcp 含 tavily_search / tavily_extract / tavily_crawl ...)。如果对整个 service 勾"需要人工审批",那 N 个工具都会被拦——多数场景太粗。
打开 service 类型 MCP 的编辑对话框 → 测试连接拿到工具列表后 → "审批策略" 区块选模式:
| 模式 | 含义 | 典型用法 |
|---|---|---|
全部审批 (all,默认) | service 内所有工具都拦 | 服务整体危险(如内部数据库 service) |
白名单 (whitelist) | 只对勾选的工具拦,其它跳过 | 大多数搜索/查询工具安全,只有 tavily_crawl(爬整站)需要审 |
黑名单 (blacklist) | 勾选的工具跳过,其它都拦 | 默认全审,但 tavily_search 调用频繁不想被打扰 |
白名单 vs 黑名单怎么选
- 白名单更安全:默认 deny,显式 allow——新工具加进 service 不会意外漏审
- 黑名单更省事:默认 audit,显式 skip——已经知道哪些是噪音
该功能仅对 service 类型 MCP 有意义;stdio 单工具 MCP 直接用上面"需要人工审批"开关就够了。
设计意图
工具危险性是内禀属性——delete_db 不论谁用、用在哪,都该被拦一下。在工具层标记是兜底防线,避免每个编排器作者"忘记加配置"导致漏拦。Service 白/黑名单则是这条防线的"工具级粒度",让一个 service 里的工具能区别对待。
第 2 层:编排器
谁配? 编排器作者 配什么? "这个业务场景下,哪些工具要拦 / 哪些不拦"
静默开关(推荐先看)
如果你的编排器整个不需要 HITL(开发调试、批量 cron、单测主路径),直接打开静默开关:
Web 控制台:编排器编辑页 → "人工审批 (HITL)" 卡片顶部 → "禁用人工审批 (静默模式)" 琥珀色 toggle
Single / DeepAgents / Supervisor / Collaboration 等基于 ReAct 的编排器都暴露同一张 HITL 卡片,配置入口一致。
CLI:
siracli orchestrator update <id> --hitl-disabled # 整编排器静默
siracli orchestrator update <id> --no-hitl-disabled # 取消静默开启静默后,下方 per-tool 规则区域会灰化提示"已禁用"——所有 HITL 规则不生效。
为什么要这个开关?
MCP 工具是动态加载的——编排器作者不可能完整列出所有 requires_approval=true 的工具名再逐个写 false。这个开关是"批量关掉"的快捷方式。
Per-tool 规则(覆盖工具层)
需要精细控制时,逐个工具配:
Web 控制台:编排器编辑页 → "人工审批 (HITL)" 卡片下方"添加工具规则":
| 选项 | 含义 |
|---|---|
| 启用(用工具默认) | 沿用工具自己 requires_approval 的设置 |
| 显式禁用 | 即使工具默认要审,本编排器关掉它 |
| 自定义 | 单独指定 allowed_decisions / description |
CLI:
# 加规则(创建时)
siracli orchestrator create -n my_orch -D 客服 -t deep_agents \
--ai-model-id <id> -s "You are…" \
--interrupt-on send_email \
--interrupt-on execute \
--interrupt-off ls # 显式关掉某个 tool 默认
# 加规则(更新;与既有 interrupt_on dict 合并)
siracli orchestrator update <id> --interrupt-on transfer_money
siracli orchestrator update <id> --interrupt-off send_email
# 整 dict 替换(脚本化场景)
siracli orchestrator update <id> --interrupt-on-json '{
"delete_db": true,
"execute": {"allowed_decisions": ["approve","reject"]},
"send_email": false
}'
# 清空编排器层规则
siracli orchestrator update <id> --clear-interrupt-on设计意图
工具层定的是"通用危险性",但业务场景会改变:客服编排器的 send_email 是合规要求要审;DevOps 编排器的 send_email 可能就是日常运维不需要审。编排器层让作者按业务调。
第 3 层:应用
谁配? 部署者 / 管理员 配什么? "同一个编排器在不同 corp / 不同部署环境,HITL 策略不一样"
静默开关(OR 合并)
Web 控制台:应用编辑页 → 步骤 3 "平台配置" → 高级区块 → "禁用人工审批(应用级静默)" toggle
CLI:
siracli application update <id> --hitl-disabled # 整应用静默
siracli application update <id> --no-hitl-disabled关键语义:OR 合并
| 编排器 | 应用 | 实际行为 |
|---|---|---|
| 静默 | 不管 | 静默 |
| 不管 | 静默 | 静默 |
| 都静默 | 静默 | |
| 静默 | 显式开 | 仍然静默(应用层不能 override 编排器静默) |
| 都不静默 | 走完整 per-tool 合并 |
应用层不能"强制开启"被编排器静默的 HITL
当前 hitl_disabled 是"自愿降级"语义——任一层勾了就静默。"管理员强制升级开"是另一个独立字段(hitl_required,V2 计划),目前没实现。
Per-tool 覆盖(最高优先级)
应用层的 per-tool 配置叫 interrupt_overrides,优先级最高——可以强制开启编排器层关掉的工具,也可以反过来。
Web 控制台:应用编辑页 → 步骤 3 → 高级 → "应用级 per-tool 覆盖 (interrupt_overrides)" 区域
CLI:
# 创建时
siracli application create -n my_app -D Bot -p api_service \
-o <orchestrator-id> \
--hitl-disabled \ # 整应用静默
--interrupt-override delete_db \ # 强制开某个工具
--interrupt-override-off send_email # 强制关某个工具
# 后期增量
siracli application update <id> --interrupt-override transfer_money
siracli application update <id> --interrupt-override-off ls
# 整 dict 替换
siracli application update <id> --interrupt-overrides-json \
'{"delete_db": {"allowed_decisions": ["approve"]}}'
# 清空应用层规则
siracli application update <id> --clear-interrupt-overrides设计意图
同一个客服编排器可能同时部署到 5 个公司的企业微信——每家公司合规要求不同。应用层让部署者不修改编排器也能调每家的 HITL 策略,这是 V1 多租户的核心机制。
典型场景速查
| 想做什么 | 推荐配置 |
|---|---|
| 某工具天生危险,所有用户都拦 | MCP 工具层:requires_approval=true |
| 本编排器调试期间不要 HITL | 编排器:hitl_disabled=true |
| 本编排器整体审批 + 个别工具例外 | 工具层标 + 编排器 per-tool false |
| 同编排器多应用,部分应用静默 | 应用层 hitl_disabled=true |
| 同编排器多应用,部分应用更严 | 应用层 interrupt_overrides 加新规则 |
| 紧急关停某编排器 HITL | 编排器层 hitl_disabled 开关一秒生效 |
| 单测 / e2e 跑主路径 | 测试用 application 加 hitl_disabled=true |
卡片体验
WeCom(企业微信)
收到 HITL 触发时用户看到:
- 标题:"AI 想执行:execute"
- 描述:工具的 args 内容(或自定义 description 模板渲染后的文本)
- 按钮:✅ 批准 / ❌ 拒绝 / ✏️ 编辑(按 allowed_decisions 配置显示)
点击批准 / 拒绝后 5 秒内,卡面会更新为终态("AI 想执行:execute" + ✅ 已批准 按钮),变成永久决策记录。AI 续跑后的工具执行结果通过单独的跟随消息发出。
微信(绑到企业微信的 iLink 用户)
如果你用个微绑定到企业微信账号 (/wxbind),HITL 自动降级为文字 fallback:
🔔 等待你的决策
📋 操作:执行 SQL 删除
内容:DROP TABLE users WHERE id < 1000
回复以下任一选项:
1 ✅ 批准
2 ❌ 拒绝
⚠️ 24 小时内有效回复 1/批准/approve/y/yes 任意一个就算批准,回复 2/拒绝/reject/n/no 算拒绝。
飞书 / WebClient
V2 计划——目前飞书 / WebClient 暴露 HITL 时也会自动走文字 fallback(不会卡死,只是没原生卡片)。
故障排查
"我配了 HITL 怎么没卡片?"
按这个顺序检查:
检查应用是否静默:管理员可能开了应用层静默
bashsiracli application info <app-id> # 看输出里是否有 hitl_disabled = true检查编排器是否静默:
bashsiracli orchestrator get <orch-id> # 看 HITL 区块检查工具层是否标了:
bashsiracli mcp info <tool-id> # 看 HITL Approval 区块三层都开但还是没卡片 → 工具调用名跟你配的
interrupt_onkey 是否一致?比如 SubAgent 暴露的工具是myagent.execute不是execute。后端日志会有:orchestrator hitl_disabled=true → 跳过 HumanInTheLoopMiddleware 或 approval dispatched: thread=... handle=... platform=wecom tool=execute
"卡片出现了但点了没反应"
- 检查后端日志找
handle_decision:用户的点击事件是否到达 dispatcher - 检查 PostgreSQL
langgraph_checkpoints表是否有该thread_id的记录(HITL 续跑依赖 PG checkpoint) - 卡片5 秒窗口已过:仍然能续跑,但卡面不再变化(实际工具执行的结果会以跟随消息送达)
"iLink 用户没收到卡片,也没收到文字 fallback"
/wxstatus检查个微绑定是否正常- 检查
wx_bindings表routing字段是否指向了正确的source_platform
"重启 backend 后用户再点,没反应"
- 确认 PG checkpointer 已启用:
LANGGRAPH_CHECKPOINT_BACKEND=postgres(或配置项) - 看
checkpoints表是否有该 thread 的数据
Cron 与 HITL
定时任务(Cron Jobs)是无人值守场景——没有"target user"也没有 channel 上下文,HITL 卡片派发所有面向 channel 的字段都缺失。
默认行为:cron 调用编排器命中 HITL → 自动按"批准"续跑,工具该执行就执行。语义上等价于 cron 跑的瞬间整个编排器自动 hitl_disabled=true。
为什么这样设计:
- cron 是周期性自动化:没有"等人"的概念
- 没人能审批:cron context 没有 target_user / platform,卡片派发会写脏 fallback key 且 thread 卡死在 PG checkpointer
- 业务安全靠工具自身:高危工具该不该让 cron 调用,工具作者 / 编排器作者应该在配置层面管控(比如把
delete_db工具不放进 cron 编排器的 tool_ids)
自动续跑机制:
- 第一次执行命中 HITL → cron_runner 拿到
result["interrupted"]=True和pending_action_count=N - 自动用
Command(resume={"decisions": [{"type":"approve"}]*N})续跑 - agent 该跑工具的跑、再触发新 HITL 的话再续,最多
SIRA_CRON_HITL_MAX_AUTO_RESUME轮(默认 10) - 超过 N 轮还在中断态 → 当 ERROR 处理(job 报失败 + error_msg "HITL auto-resume exceeded N attempts")
调参:
# 后端启动环境变量
SIRA_CRON_HITL_MAX_AUTO_RESUME=10 # 默认 10,调小可以更早 fail-fast如果你想 cron 触发 HITL 时拒绝而不是批准:
当前没有 per-job 策略(V2 计划),唯一办法是给该编排器配 per-tool 规则,比如对所有可能命中 HITL 的工具加 false——但如果你能列出来那些工具,就直接不放进 tools 列表更干净。
安全契约
鉴权:用户点卡片时,平台校验
raw_user_id == 原 target_user_id,防止 A 用户去批 B 用户的卡片(群聊场景)防重复点击:dispatcher 维护
approval:thread:{tid}.stateRedis 状态,已 PROCESSING / APPROVED / REJECTED 的 thread 收到二次点击直接忽略过期:默认 24h
expires_at,超期由后台 cron 清理 + 通知用户"请求已过期"Key 格式(运维用得上):
Redis Key 内容 TTL approval:thread:{thread_id}路由元数据 7d approval:handle:{handle}反查 thread_id 7d approval:fallback:{user}:{platform}文字 fallback 反查 24h
相关文档
- 编排器配置 — Single / DeepAgents / Supervisor / Collaboration 等所有 ReAct 类编排器都生效
- MCP 工具配置 — 第 1 层的工具自身配置入口
- 应用接入 — 第 3 层的部署级覆盖
- Skill 包管理 — 跟 HITL 互补的 Markdown 剧本机制
设计文档
本页是面向用户的使用指南。完整架构 / 协议 / 实现细节请看仓库内 devdocs/HITL_INTERACTIVE_CARD_DESIGN.md。
