高风险动作的最终参数审批与一次性执行链

用户说“可以发”,Agent 随后又重新生成了收件人、正文或附件。系统继续沿用刚才的确认,把变化后的内容直接发出——这类事故表面上有人工审批,实际上批准人与最终执行内容并没有绑定。

据 Google Cloud 官方文档,2026 年 8 月 24 日发布的 Agent 安全治理文章把 human-in-the-loop 描述为:用明确规则识别关键动作,并在动作继续之前要求人工批准。工程上还要再补一层:批准的必须是即将执行的最终参数,而不是一个模糊目标。

把“我要做什么”和“马上执行什么”分开

一次高风险动作通常经历四个对象:

  1. intent:用户想达到的目标,例如“把周报发给项目组”;
  2. plan:Agent 计划调用哪些工具;
  3. payload:工具执行前的最终收件人、正文、附件和权限范围;
  4. receipt:外部系统返回的业务结果。

很多系统在 intent 或 plan 阶段弹一次确认框,然后允许 Agent 继续补参数。这只能证明用户同意了方向,不能证明用户看过最终内容。

适合审批的对象应当是已经归一化的 payload:排序稳定、默认值已展开、附件已确定、敏感字段已遮盖但仍能被批准人识别。

type SendPayload = {
  action: "send_email";
  recipients: string[];
  subject: string;
  body: string;
  attachmentHashes: string[];
};

function normalizePayload(p: SendPayload): SendPayload {
  return {
    ...p,
    recipients: [...new Set(p.recipients.map(x => x.trim().toLowerCase()))].sort(),
    attachmentHashes: [...p.attachmentHashes].sort(),
  };
}

归一化规则本身也要固定版本。比如 undefined 是否删除、时间是否统一成 UTC、金额是否转成最小货币单位、对象键如何排序,都必须在审批服务和执行器中得到完全相同的结果。否则两边即使处理同一份业务参数,也可能算出不同哈希;更糟的是为了绕过误报而关闭校验。应使用同一份 canonical JSON 实现,并用包含中文、空值、数组顺序和小数的测试向量锁定行为。

审批凭证至少绑定五样东西

一个可执行的审批凭证至少要回答:谁批准、批准了什么、允许哪个动作、在什么时间范围有效、是否已经使用。

{
  "approval_id": "apr_01...",
  "actor_id": "operator_ref",
  "action": "send_email",
  "payload_sha256": "<normalized-payload-hash>",
  "policy_version": "send-policy-17",
  "expires_at": "<iso-time>",
  "status": "approved",
  "used_at": null
}

payload_sha256 绑定最终参数;policy_version 防止审批后权限规则被替换;expires_at 限制旧批准长期有效;used_at 防止同一凭证被重复执行。

如果团队正在梳理哪些动作可以自动做、哪些必须停下来,可以先按Agent 上线前的分级放权检查项核对只读、建议、需确认和禁止四个层级,再把审批凭证放到真正有副作用的路径上。

执行器必须重新计算哈希

审批服务不能把 approved=true 直接交给工具。执行器拿到凭证后,应根据当前参数重新计算哈希,并逐项验证:

async function executeApproved(
  payload: SendPayload,
  approval: Approval,
) {
  const normalized = normalizePayload(payload);
  const digest = sha256(canonicalJson(normalized));

  if (approval.status !== "approved") throw new Error("not approved");
  if (approval.action !== normalized.action) throw new Error("action mismatch");
  if (approval.payloadSha256 !== digest) throw new Error("payload changed");
  if (approval.usedAt) throw new Error("approval already used");
  if (Date.parse(approval.expiresAt) <= Date.now()) throw new Error("approval expired");

  const claimed = await approvalStore.claimOnce(approval.id, digest);
  if (!claimed) throw new Error("approval replayed");

  return mailTool.send(normalized);
}

claimOnce 必须是原子操作。否则两个 worker 同时拿到同一审批凭证,都可能在 used_at 写入前执行。

回显页面不要只显示一句摘要

批准人需要看到能改变后果的字段。发邮件应显示收件人、抄送人、主题、附件名与哈希;改数据库应显示对象、字段、旧值、新值和影响范围;发布内容应显示账号、标题、正文摘要、图片数量、可见范围和 AI 内容声明。

对敏感数据可以脱敏,但不能脱敏到失去判断能力。例如手机号可以显示末四位,收件人域名应保留;附件不能只显示“文件 1”,至少要有文件名、大小和摘要哈希。

一个实用的回显结构如下:

action: publish_article
account: csdn_account_ref
visibility: public
title: "AI Agent 高风险动作审批"
body_sha256: "..."
assets:
  - name: approval-flow.png
    sha256: "..."
ai_content_disclosure: true
changed_after_last_review: false

如果 changed_after_last_review 为真,页面应让旧批准失效,而不是提示一个黄色小字后继续。

计划变化后必须重新批准

以下变化都应触发重新审批:

  • 新增、删除或替换收件人;
  • 正文或关键参数变化;
  • 附件内容变化,即使文件名没变;
  • 目标账号、可见范围或发布时间变化;
  • 工具从只读操作切换为写操作;
  • 权限策略或执行器版本发生不兼容变化。

而纯展示字段的变化,例如界面排序或本地化文案,可以由策略明确声明不影响批准。关键是规则必须可执行,不能靠每个调用方自己判断“变化大不大”。

六个反向用例比成功截图更有价值

  1. 批准后替换附件内容,执行必须被 payload changed 阻断;
  2. 两个 worker 同时使用同一凭证,只能有一个成功领取;
  3. 凭证过期后即使哈希相同也不能执行;
  4. 从测试账号切到生产账号,旧批准必须失效;
  5. 工具超时导致结果未知时,不允许拿同一凭证盲目重试;
  6. 执行失败后,审计记录能同时找到批准参数和外部回执。

这些用例通过后,“有人点过确认”才升级为“最终动作经过可核对批准”。人类审批不是流程里的一个按钮,而是一份有对象、有期限、不可重放、能与业务结果对账的执行授权。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐