给 Agent 高风险工具加确认闸时,我会先把 dry-run 和审计账本拆清楚
最近给桌面端 Agent 工作台接一组高风险工具时,我先卡在确认链路上。工具本身很普通:写入文件、提交表单、触发自动化任务、调用内部接口。问题在于模型给出的动作经常看起来合理,但用户真正关心的是它会改哪些东西、能不能撤回、失败后有没有证据。后来我把这类能力收敛成一条确认闸:先 dry-run 生成计划,再做风险分级,经过用户确认后才进入执行器,最后把每一步写进审计账本。

原创示意图:把高风险工具调用拆成 dry-run 计划、风险分级、用户确认、受控执行和审计账本。 来源:Codex image generation
问题背景
MCP Tools 文档把工具描述为可以被模型发现和调用的能力,同时在安全建议里强调,敏感操作前应展示工具输入、请求用户确认,并记录工具使用以便审计。这个提醒很直接:工具调用的边界不能藏在提示词里,也不能只靠一次 UI 弹窗表达。
我遇到的真实场景是,Agent 要帮用户批量整理项目文件。模型计划里包含新建目录、移动文件、重命名配置和触发构建。用户愿意授权整理,但希望先看到影响范围。早期实现只有一个 confirm(),确认后执行器直接跑。MDN 对 window.confirm() 的说明里提到,在某些情况下浏览器可能不会显示对话框,或者不会等待用户确认;它最终也只返回一个布尔值。这个粒度不足以承载高风险工具的工程语义。
关键难点
第一个难点是计划和执行混在一起。模型一边解释一边调用工具,用户看到的是自然语言摘要,执行器拿到的是参数。两者稍微不一致,排障时就很难判断用户批准的到底是哪一版动作。
第二个难点是拒绝和取消的含义不同。MCP Elicitation 文档把 accept、decline、cancel 分成三种动作,我觉得很适合拿来做确认闸。接受代表继续,拒绝代表用户明确不同意,取消代表当前上下文中断,后续 UI 和调度器应该区别处理。
第三个难点是桌面端体验。Electron 的 dialog.showMessageBox() 能返回点击按钮索引,也能返回 checkbox 状态,并支持把对话框挂到父窗口上。对桌面端来说,这比 Web 页里的临时确认框更可控,但它仍然只能解决交互入口,风险解释、幂等键、回滚提示和审计字段还要由业务协议承担。
解决思路
我现在会让每一次高风险工具先进入 plan 阶段。执行器只做静态展开:列出目标文件、请求方法、外部服务、预计副作用、是否可回滚、幂等键和风险等级。这个阶段不产生真实副作用,只输出一份带 planId 的计划。
确认阶段只读取这份计划,不允许模型临场改参数。用户确认时记录 consentId、planHash、用户选择、时间和客户端来源。执行阶段必须带上同一个 planHash,如果计划内容被改过,执行器直接拒绝。这样一来,批准对象和执行对象能对上。
关键步骤
落地时我先给工具注册表补 riskLevel、requiresConsent、dryRunOnly 和 rollbackHint。低风险只做日志,高风险必须先 dry-run。随后把执行器拆成两个入口:previewToolCall 只返回计划,executeToolCall 只接受已确认的计划引用。UI 上把影响范围放在确认框主体,把可撤回程度、目标路径和外部请求放在详情区。
审计账本不只记成功和失败。我会记录 planned、shown_to_user、accepted、declined、cancelled、executed、rolled_back 这些状态,并把错误分成协议错误和执行错误。协议错误说明调用本身不合法,执行错误说明工具运行时遇到问题。这个区分和 MCP Tools 的错误处理思路一致,后续复盘会清楚很多。
可复用经验
确认闸的价值不在于多弹一次窗,而在于让高风险动作拥有可核对的生命周期。dry-run 让用户看见影响范围,planHash 让执行器拒绝偷换参数,三态确认让调度器知道下一步该停、该改,还是稍后再问,审计账本让复盘有证据。
我现在判断一个 Agent 工具能否上线,会先问四件事:能不能无副作用预览,用户批准的是不是结构化计划,执行前能不能校验计划未变,失败后能不能按同一条账本复盘。只要这四件事闭环,高风险工具就不再依赖运气和口头约定。