HITL交互界面
核对日期:2026-08-26。
1. 定义与边界
HITL 交互界面是人类在环(Human-in-the-loop)的前端投影:当策略引擎把 run 停在 waiting_approval 时,界面必须让审批人看懂要做什么、为什么、影响是什么,并能批准、拒绝、改参数或升级。
策略、超时、双人复核见 ../01-Agent基础理论/Human-in-the-loop.md 和 ../12-安全与治理/人类审批.md。本文件只写界面信息架构、状态恢复和常见空确认框如何避免。
AI SDK 提供 addToolApprovalResponse({ approved: true }) 这类 API。它能接线,不能当产品设计:只有 Approve / Deny 的按钮,等于把责任推给看不懂上下文的人。
2. 为什么重要
审批失败通常不是模型绕过了按钮,而是人点了看不懂的按钮:
- 只显示「是否确认」时,审批人无法承担后果。
- 批准后不展示将执行的最终参数,编辑过的字段会静默丢失。
- 前端允许点击,但服务端不复检策略,等于把授权放在浏览器。
- 拒绝后 Agent 换一个同义工具继续跑,界面看起来「已拒绝」,系统仍在写。
3. 核心机制
界面是策略的展示,不是策略本身。浏览器里的「已批准」只是一次用户意图;执行前必须用同一套策略再算一遍。
4. 架构模式
4.1 审批卡片必含字段
| 字段 | 作用 | 缺失时发生什么 |
|---|---|---|
| 用户目标 | 判断动作是否服务于原任务 | 批准无关副作用 |
| 工具名与风险等级 | 识别写 / 资金 / 外发 | 把退款当成查询 |
| 最终参数 | 人看到的就是将执行的 | 模型参数与展示不一致 |
| 证据摘要 | 说明依据来自哪些只读工具 | 凭感觉批准 |
| 影响范围 | 条数、金额、外部收件人 | 低估爆炸半径 |
| 可回滚性 | 能否补偿 | 把不可逆当可逆 |
| 选项 | approve / reject / edit / escalate | 只有确认 / 取消 |
| 过期时间 | 超时策略可见 | 人离开后静默执行或静默丢弃 |
run_id / approval_id | 审计与回放 | 事故无法对齐 |
4.2 审批载荷
interface ApprovalCard {
approvalId: string;
runId: string;
traceId: string;
toolCallId: string;
toolName: string;
riskLevel: 'medium' | 'high';
goal: string;
argumentsPreview: Record<string, unknown>;
evidence: Array<{ toolName: string; summary: string }>;
impact: {
resourceType: string;
resourceCount: number;
reversible: boolean;
};
options: Array<'approve' | 'reject' | 'edit' | 'escalate'>;
expiresAt: string;
}
前端可以编辑 argumentsPreview 的白名单字段,提交时必须整包回传。服务端以回传包为准再 schema + policy,而不是以模型原始参数为准。
5. 工程实现
5.1 Vue 3 审批卡片
<script setup lang="ts">
interface ApprovalCardView {
approvalId: string;
runId: string;
toolName: string;
riskLevel: 'medium' | 'high';
goal: string;
argumentsPreview: Record<string, unknown>;
evidence: Array<{ toolName: string; summary: string }>;
impact: { resourceType: string; resourceCount: number; reversible: boolean };
expiresAt: string;
}
const props = defineProps<{ card: ApprovalCardView }>();
const emit = defineEmits<{
decide: [payload: { approvalId: string; action: 'approve' | 'reject' | 'escalate'; editedArgs?: Record<string, unknown> }];
}>();
function decide(action: 'approve' | 'reject' | 'escalate'): void {
emit('decide', {
approvalId: props.card.approvalId,
action,
editedArgs: action === 'approve' ? props.card.argumentsPreview : undefined,
});
}
</script>
<template>
<aside class="approval-card" role="dialog" aria-modal="true">
<p>目标:{{ card.goal }}</p>
<p>动作:{{ card.toolName }}({{ card.riskLevel }})</p>
<p>影响:{{ card.impact.resourceCount }} 个 {{ card.impact.resourceType }},可回滚:{{ card.impact.reversible ? '是' : '否' }}</p>
<p>过期:{{ card.expiresAt }}</p>
<ul>
<li v-for="item in card.evidence" :key="item.toolName">
{{ item.toolName }}:{{ item.summary }}
</li>
</ul>
<pre>{{ JSON.stringify(card.argumentsPreview, null, 2) }}</pre>
<footer>
<button type="button" @click="decide('approve')">批准并执行</button>
<button type="button" @click="decide('reject')">拒绝</button>
<button type="button" @click="decide('escalate')">升级人工</button>
</footer>
</aside>
</template>
批准按钮文案应包含动作,而不是「确认」。资金类应用「批准退款」而不是「OK」。
5.2 提交与恢复
interface ApprovalDecision {
approvalId: string;
runId: string;
action: 'approve' | 'reject' | 'edit' | 'escalate';
editedArgs?: Record<string, unknown>;
reason: string;
}
async function submitApproval(decision: ApprovalDecision): Promise<void> {
const response = await fetch(`/api/agent/runs/${decision.runId}/approvals/${decision.approvalId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(decision),
});
if (!response.ok) {
throw new Error(`approval_http_${response.status}`);
}
}
服务端顺序:
- 校验审批人身份与
approval_id未过期、未被消费。 - 若
edit,用editedArgs替换待执行参数。 - schema 校验。
- policy 复检。
- 执行工具或写入拒绝状态,禁止同类写工具在本 run 自动改道。
- 恢复 Agent Loop,并从
waiting_approval继续。
LangGraph interrupt、OpenAI Agents SDK HITL 都是这个状态机的实现,不是另一种产品逻辑。
5.3 和 AI SDK 审批 API 的关系
useChat 的 addToolApprovalResponse 适合把「工具执行前停住」接到 React / Vue。生产上仍要:
- 自己渲染目标、证据、影响,而不是只渲染 SDK 示例里的 Approve / Deny。
- 拒绝原因、编辑字段写入审计和评测集。
- 自动批准(
isAutomatic)必须在界面上标明「策略自动通过」,避免人以为自己点过。
6. 生产实践
| 实践 | 说明 |
|---|---|
| 阻塞式模态 | 高风险审批出现时禁止继续发新消息改同一 run |
| 参数 diff | 人编辑后展示与模型原稿的字段差 |
| 脱敏 | 卡片展示打码值,执行仍用服务端持有的原文 token |
| 移动端 | 小程序里用全屏页而不是 toast 确认 |
| 超时可见 | 倒计时;超时结果与策略一致(升级而非默认批准) |
| 多审批人 | 队列里显示当前处理人,避免两人同时批同一 approval_id |
7. 常见反模式
| 反模式 | 表现 | 后果 | 修正 |
|---|---|---|---|
| 空确认框 | 「是否让助手继续」 | 无人能负责 | 必含目标 / 参数 / 证据 / 影响 |
| 前端授权 | 点批准就直接调业务 API | 绕过策略 | 只提交意图,服务端执行 |
| 批准后不复检 | 参数在等待期间被篡改 | 越权执行 | approve 后 policy + schema |
| 拒绝后改道 | 换 send_mail_v2 | 治理失效 | 拒绝写入状态,禁止同风险类工具 |
| 全部动作弹窗 | 读操作也打断 | 审批疲劳,人开始盲点 | 按风险分级 |
| 审批不进 trace | 没有 approval_id | 无法回放 | 审批 span 绑定 run |
8. 评测方法
| 指标 | 说明 |
|---|---|
| Approval Context Completeness | 卡片是否含必含字段 |
| Blind Confirm Rate | 打开后 3 秒内批准且无滚动 / 无展开参数的比例 |
| Edit Rate | 人修改参数的比例;过低可能是不敢看或看不懂 |
| Post-Approval Incident Rate | 批准后仍出事故 |
| Dual Display Mismatch | 界面参数与实际执行参数不一致,目标为 0 |
| Mobile Completion Rate | 小程序 / H5 审批完成率 |
评测集应收录真实拒绝理由,见 HITL 主文的 label_for_eval 示例。
9. 安全与治理
- 审批卡片中的证据文本来自工具返回,按不可信内容渲染。
- 人类批准不能扩大权限:数据 ACL 仍由身份系统执行。
- 展示层脱敏,执行层用保险库 / 服务端秘密,不把明文 API Key 放进卡片。
- 高风险可要求二次认证(已登录不等于已批准资金)。
- 审批日志不可被 Agent 工具改写。
- 浏览器 / Computer Use 的审批必须在每个动作 block 之前,并展示当前 URL 与截图或控件树,见 ../20-Computer-Use与浏览器Agent/凭证与HITL.md。
10. 权威资料
- OpenAI Agents SDK HITL: https://openai.github.io/openai-agents-python/human_in_the_loop/ (核对日期:2026-08-26)
- LangGraph interrupts: https://docs.langchain.com/oss/python/langgraph/interrupts (核对日期:2026-08-26)
- Vercel AI SDK tool approvals: https://ai-sdk.dev/docs/agents/tool-approvals (核对日期:2026-08-26)
- NIST AI RMF: https://www.nist.gov/itl/ai-risk-management-framework (核对日期:2026-08-26)
- OWASP Top 10 for LLM Applications: https://owasp.org/www-project-top-10-for-large-language-model-applications/ (核对日期:2026-08-26)