审批与确认体验

H2
实战手册 · 智能体体验与人机交互

从人不再阅读载荷的那一刻起,审批体验就失效了。

只有当人真正评估了所批准的内容,确认对话框才算得上监督。多数已上线的审批流都达不到这条标准:它们问得太频繁、问错了对象、附带的载荷无人阅读,并因此引发那个有据可查的失败模式——确认疲劳:用户为清掉通知而盲目点击"批准"。本文讨论如何设计这样的关卡:把罕见而重大的决策路由给一个真正会看的人,其余情况则让开路。

STEP 1

按后果与可逆性分级,而非按动作类型分级。

"任何写操作前都确认"正是制造疲劳的反模式。真正重要的维度不是动作属于哪一类,而是最坏情况有多糟以及撤销它有多廉价。一个实用的三级模型:

  • 自动——可逆、影响半径小、在分布内。直接执行并记录,不弹窗。(起草、只读查询、幂等重试。)
  • 通知——有后果但可逆。执行,但显著呈现并提供一键撤销。(发送内部消息、创建工单。)
  • 拦截——不可逆或影响半径大。阻塞,等待明确的人工批准。(资金划转、对外邮件、删除、生产环境部署。)

监督级别是决策的属性,由风险与上下文动态计算得出——而不是工具上的一个静态标志。

STEP 2

钉死被批准的载荷,否则你确认的是与实际执行不同的另一个动作。

生产环境中最常见的人在回路缺陷是:审批界面给用户看的是一组参数,但智能体在恢复执行时重新跑了一次大模型调用,参数在"批准"与"执行"之间发生了变异。用户批准的是发给 alice@ 的邮件;实际执行的调用却发到了 all-staff@。解法是结构性的——在中断时刻对确切载荷做哈希,给用户看的就是那一份载荷,一旦哈希漂移就拒绝执行。

# Approve the bytes that will run, not a regenerated paraphrase
payload = tool_call.freeze()
h = sha256(payload)
decision = await human.review(payload, digest=h)
if decision.approved and sha256(payload) == h:
    await execute(payload)
else:
    await abort("payload drifted or rejected")

如果你的智能体在恢复时会重新规划,那么没有载荷钉定的"已批准"毫无意义。要把哈希不匹配当作硬失败,绝不能当作重新弹窗的理由。

STEP 3

用批量与延迟来对抗决策疲劳。

连续十次审批会退化成十次条件反射式点击。要降低的是决策的数量,而不只是它的摩擦:把同质的低风险动作合并成一份可审阅的清单("一次性批准这 12 处标签变更"),并推迟中断,让智能体先并行完成可并行的安全工作,再呈现一个汇总的决策点,而不是断断续续地打断。要压低的指标是每完成一个任务的审批次数;要保护的指标是每份受控载荷上花费的时间——用户不到一秒就通过的关卡,根本不算关卡。

STEP 4

默认项才是真正的策略——要以对抗性的视角去选它。

对话框预选了什么,疲惫的用户就会选什么。所以无论文档怎么写,默认项就是你的安全策略。两条规则:(1) 默认必须是安全、可逆的那个选项,绝不能是破坏性的那个,即使这会让常见路径多花一次点击;(2) 绝不要把破坏性的主按钮放在键盘回车 / 肌肉记忆所在的位置。要为那个不读内容的用户去设计默认项,因为在高负荷下,每个用户都是那个用户。

STEP 5

不可逆的动作需要的是另一种交互,而不是更大声的对话框。

对于真正无法挽回的动作(电汇、删除生产数据、发给客户),一个是/否模态框在结构上太弱了——它和用户一整天反射式批准的,是同一个手势。要升级的是交互形态:要求重新键入目标对象("键入表名以确认")、强制一段让后果变得具体的冷却延迟,或为最高级别要求第二位审批人。更好的做法是先把可逆性工程化进来——一个带撤销的 30 秒发送延迟,能把多数"不可逆"动作变回可逆,从而完全免去那道沉重的关卡。

在添加一个更吓人的确认之前,先问问能不能改为让动作可逆。一个软删除加撤销,胜过仍会被反射式点掉的最强模态框。

STEP 6

什么时候不该加审批关卡。

如果动作可廉价撤销且影响半径小,关卡带不来安全,只会带来疲劳——而你将在那个真正重要的关卡上偿还这笔疲劳。