【Agent工程】(6)—— 危险写操作与二次确认

【Agent工程】(6)------ 危险写操作与二次确认

文章目录

  • [【Agent工程】(6)------ 危险写操作与二次确认](#【Agent工程】(6)—— 危险写操作与二次确认)
    • [1. 写工具通过门禁不等于可以立即执行](#1. 写工具通过门禁不等于可以立即执行)
      • [1.1 三类需要二次确认的典型写操作](#1.1 三类需要二次确认的典型写操作)
    • [2. 注册表里的风险等级](#2. 注册表里的风险等级)
      • [2.1 与卡片约束的关系](#2.1 与卡片约束的关系)
      • [2.2 风险等级变更要走评审](#2.2 风险等级变更要走评审)
    • [3. 两阶段执行与待确认队列](#3. 两阶段执行与待确认队列)
      • [3.1 Agent 侧语义](#3.1 Agent 侧语义)
      • [3.2 确认接口的最小约定](#3.2 确认接口的最小约定)
    • [4. 贯穿示例:通知值班与删除工单](#4. 贯穿示例:通知值班与删除工单)
      • [4.1 拒绝与降级](#4.1 拒绝与降级)
      • [4.2 确认页应展示的最小信息](#4.2 确认页应展示的最小信息)
    • [5. 可运行示意:pending 队列与确认执行](#5. 可运行示意:pending 队列与确认执行)
      • [5.1 注册表与入队](#5.1 注册表与入队)
      • [5.2 确认后再执行](#5.2 确认后再执行)
    • [6. 与验收和审计对齐](#6. 与验收和审计对齐)
      • [6.1 与 scope 门禁的顺序](#6.1 与 scope 门禁的顺序)
      • [6.2 验收项扩展示例](#6.2 验收项扩展示例)
    • [7. 四个常见误区](#7. 四个常见误区)
      • [7.1 与模型提示词的边界](#7.1 与模型提示词的边界)
    • [8. 适用边界](#8. 适用边界)
      • [8.1 存量系统如何补二次确认](#8.1 存量系统如何补二次确认)
    • [9. 术语速查](#9. 术语速查)
    • [10. 小结与下一篇](#10. 小结与下一篇)

摘要 :注册表、作用域与调用前门禁可以拦住越权,但对外通知、删除、批量变更等写操作仍可能在一次模型提议后直接落地。本篇在工具注册表上增加风险等级,把高危写操作改成两阶段:提议入待确认队列,人工或策略确认后再执行。贯穿示例继续用工单助手。适合读完 【Agent工程】(5)------ 工具作用域与对象级授权、准备给不可逆副作用加确认层的工程师。读完可以独立完成:为高危工具配置 risk_level,并实现一版 pending 队列与确认接口。


1. 写工具通过门禁不等于可以立即执行

前几篇工具面已经做了三层约束:

  1. 注册表与允许清单 :能不能用这个 tool_id
  2. 读写分离:写操作单独计数、单独策略
  3. 作用域 :参数里的 ticket_id 是否在卡片范围内

工单升级场景里,notify_oncall 通常可以通过以上三道检查。工具合法、对象也合法,但通知值班 会把内容推到外部通道:措辞错误、工单号抄错、重复轰炸值班手机,后果往往比改一个 priority 字段更严重。delete_ticket 则属于不可逆操作,即便 scope 命中,也不应让模型一次 function call 直接删库。

工程上要再分一层:

阶段 回答的问题
调用前门禁 工具、对象、参数是否合法
二次确认 合法的高危写是否现在就要执行

普通写(改备注、改优先级)在卡片允许且 scope 命中后可以立即执行。高危写(对外通知、删除、批量变更)应挂起 ,等确认后再调用实现层。前文见 【Agent工程】(4)------ 工具注册表与读写分离【Agent工程】(5)------ 工具作用域与对象级授权

常见故障模式是:演示时值班同学坐在旁边口头说「可以发」,上线后同一句话写进模型回复就被当成确认。口头确认没有独立记录,验收与审计都无法核对。

1.1 三类需要二次确认的典型写操作

不必把所有 write 都做成 pending。工程上优先盯住三类:

类型 特征 工单示例
对外通知 内容离开系统边界 notify_oncall、发短信
不可逆 删除或无法自动回滚 delete_ticket
批量变更 一次调用影响多对象 batch_close_tickets

update_ticket_priority 通常仍属 medium:在 scope 内、可回滚、不直接对外。若业务规定「升紧急必须值班知晓」,可以把 medium 在卡片里 override 成 human_required,但那是收紧而不是放松。


2. 注册表里的风险等级

在注册表条目上增加 risk_level (风险等级),与 side_effectenabled 并列。建议先用四档,名称可以随团队调整,但语义要固定:

risk_level 含义 典型工具 默认策略
low 可逆、影响面小 添加内部备注 门禁通过即执行
medium 业务写、可回滚 改优先级 卡片允许即执行
high 对外副作用 通知值班 待确认后执行
critical 不可逆或极高风险 删除工单 默认禁用 + 必须人工

字段还可以带上 confirm_policy(确认策略),例如:

  • none:无需确认
  • card_override:卡片显式声明 auto_confirm: true 时才跳过(仅适用于 medium)
  • human_required:必须人工 approve
  • blocked:与 enabled=False 配合,永不执行

底线规则critical 不允许被卡片放宽为自动执行。卡片可以收紧(例如 medium 也要求确认),不能比注册表更松。

2.1 与卡片约束的关系

任务卡片除了 allowed_toolsscope,还可以写:

json 复制代码
{
  "confirm_policy_overrides": {
    "notify_oncall": "human_required"
  },
  "auto_confirm": false
}

auto_confirm: false 应作为工单类 Agent 的默认值:宁可多一道人工,也不要误通知。只有明确标注的低风险批量任务,才在卡片级允许特定工具跳过确认。

2.2 风险等级变更要走评审

risk_level 不是文档标签,而是运行时分支。把 notify_oncall 从 high 改成 medium,等于默认不再 pending。变更应:

  1. 在注册表 PR 里说明业务理由
  2. 同步更新卡片模板与验收项
  3. 在评测集里加一条「误通知」回归用例

反过来,线上若 pending 队列长期无人处理,优先加值班排班或超时降级,而不是把 high 一律改成 medium 省事。


3. 两阶段执行与待确认队列

高危写的运行时流程在门禁之后多两步:入队确认执行

  1. 模型提议工具调用
  2. 调用前门禁(注册、启用、允许清单、schema、scope)
  3. risk_level 需要确认:写入 pending 队列,返回 pending_id 给 Agent,调用 SDK
  4. 人工或策略审批:approve / reject
  5. approve 后,用快照参数执行实现层

队列条目建议包含:

字段 用途
pending_id 唯一编号,对外展示给值班确认
tool_id, args 拟执行调用快照,确认后不可被模型改写
card_id 关联任务卡片,便于核对目标
requested_at, expires_at 创建与超时时间
status pending / approved / rejected / expired
decided_by 审批人或服务账号

超时 :pending 超过 expires_at 自动变为 expired,Agent 应走降级路径(例如标记「通知待人工补发」),而不是反复入队。

3.1 Agent 侧语义

门禁返回三类结果之一:

结果 Agent 行为
直接执行成功 继续推理,读工具结果
拒绝(越权等) 换方案或结束
pending_created 告知「已提交确认」,停止重复提议同一调用

pending_created 不是失败,但也不是完成。Agent 不应在同一轮里用略改参数再发一次 notify_oncall 来「碰运气」,否则会堆多条 pending。应在卡片预算里限制同一工具 pending 次数。

3.2 确认接口的最小约定

对外暴露 approve API 时,建议固定请求体:

json 复制代码
{
  "pending_id": "a1b2c3d4e5f6",
  "action": "approve",
  "decided_by": "oncall_zhang",
  "comment": "措辞已核对"
}

拒绝时使用 "action": "reject" 并填写 reason_code。接口只接受 pending_id,不接受客户端重新上传 tool args,避免确认页被篡改参数。


4. 贯穿示例:通知值班与删除工单

继续工单升级。目标:将 T-1024 升为紧急并通知值班。

步骤 动作
1 Agent 提议 update_ticket_priority(T-1024, urgent),medium,直接执行
2 Agent 提议 notify_oncall(T-1024, message=...),high,生成 pending
3 值班在确认页核对工单与措辞,点击 approve
4 运行时按快照执行通知,回写 notified=True
5 验收(第 3 篇)断言 priority 与 notify 状态

delete_ticket 在注册表里应是 critical + enabled=False。即便有人把它写进允许清单,门禁仍应在启用检查处拒绝;若将来启用,也必须 human_required 且双人复核,不在本篇最小示例里展开。

4.1 拒绝与降级

值班 reject 时,应记录原因码(例如 wording_unsafewrong_ticket)。Agent 收到 rejected 后可以:

  • 修改 message 重新提议(生成新 pending,旧条目保持 rejected)
  • 或标记任务降级:priority 已改,通知转人工电话

无论哪条路径,轨迹里都要保留 pending 与审批记录,验收时才能解释「为什么 priority 变了但没有 notified」。

4.2 确认页应展示的最小信息

值班同学不是审代码,确认页应一眼能判断该不该点 approve:

展示项 来源
工单号 args 快照
拟执行工具 tool_id + 展示名
通知正文 message 快照
任务目标 卡片 goal 摘要
过期时间 expires_at

缺少卡片 goal 时,值班很难判断通知是否与任务一致。确认 UI 是安全链路的一部分,不是可有可无的前端装饰。


5. 可运行示意:pending 队列与确认执行

下面代码演示:门禁通过后,高危写入队;确认前不执行;approve 后用快照参数调用。

5.1 注册表与入队

python 复制代码
from __future__ import annotations

import uuid
from datetime import datetime, timedelta, timezone
from typing import Any, Callable


REGISTRY: dict[str, dict[str, Any]] = {
    "update_ticket_priority": {
        "side_effect": "write",
        "risk_level": "medium",
        "confirm_policy": "none",
        "enabled": True,
        "input_schema": {"required": ["ticket_id", "priority"]},
    },
    "notify_oncall": {
        "side_effect": "write",
        "risk_level": "high",
        "confirm_policy": "human_required",
        "enabled": True,
        "input_schema": {"required": ["ticket_id", "message"]},
    },
    "delete_ticket": {
        "side_effect": "write",
        "risk_level": "critical",
        "confirm_policy": "blocked",
        "enabled": False,
        "input_schema": {"required": ["ticket_id"]},
    },
}

PENDING: dict[str, dict[str, Any]] = {}
EXEC_LOG: list[dict[str, Any]] = []


def needs_confirmation(meta: dict[str, Any], card: dict[str, Any]) -> bool:
    policy = (card.get("confirm_policy_overrides") or {}).get(
        meta.get("tool_id", ""), meta.get("confirm_policy", "none")
    )
    if meta["risk_level"] == "critical":
        return True
    if policy == "human_required":
        return True
    if policy == "none":
        return card.get("auto_confirm", False) is False and meta["risk_level"] == "high"
    return False


def dispatch_tool(
    tool_id: str,
    args: dict[str, Any],
    card: dict[str, Any],
    executor: Callable[[str, dict[str, Any]], Any],
) -> dict[str, Any]:
    meta = {**REGISTRY[tool_id], "tool_id": tool_id}
    if not meta.get("enabled", False):
        return {"ok": False, "reason": "disabled_tool", "tool_id": tool_id}

    if needs_confirmation(meta, card):
        pending_id = uuid.uuid4().hex[:12]
        PENDING[pending_id] = {
            "pending_id": pending_id,
            "tool_id": tool_id,
            "args": dict(args),
            "card_id": card.get("card_id"),
            "status": "pending",
            "expires_at": datetime.now(timezone.utc) + timedelta(minutes=15),
        }
        return {"ok": True, "status": "pending_created", "pending_id": pending_id}

    result = executor(tool_id, args)
    EXEC_LOG.append({"tool_id": tool_id, "args": args, "result": result})
    return {"ok": True, "status": "executed", "result": result}


def approve_pending(
    pending_id: str,
    decided_by: str,
    executor: Callable[[str, dict[str, Any]], Any],
) -> dict[str, Any]:
    item = PENDING.get(pending_id)
    if not item or item["status"] != "pending":
        return {"ok": False, "reason": "pending_not_found_or_done"}
    if datetime.now(timezone.utc) > item["expires_at"]:
        item["status"] = "expired"
        return {"ok": False, "reason": "pending_expired"}

    item["status"] = "approved"
    item["decided_by"] = decided_by
    result = executor(item["tool_id"], item["args"])
    EXEC_LOG.append(
        {
            "tool_id": item["tool_id"],
            "args": item["args"],
            "result": result,
            "pending_id": pending_id,
            "decided_by": decided_by,
        }
    )
    return {"ok": True, "status": "executed", "result": result}


if __name__ == "__main__":
    card = {"card_id": "card-100", "auto_confirm": False}

    def fake_exec(tool_id: str, args: dict[str, Any]) -> dict[str, Any]:
        return {"tool_id": tool_id, "args": args, "ok": True}

    r1 = dispatch_tool(
        "update_ticket_priority",
        {"ticket_id": "T-1024", "priority": "urgent"},
        card,
        fake_exec,
    )
    r2 = dispatch_tool(
        "notify_oncall",
        {"ticket_id": "T-1024", "message": "升级紧急,请接手"},
        card,
        fake_exec,
    )
    print("priority:", r1)
    print("notify:", r2)
    assert r1["status"] == "executed"
    assert r2["status"] == "pending_created"

5.2 确认后再执行

python 复制代码
if __name__ == "__main__":
    card = {"card_id": "card-100", "auto_confirm": False}

    def fake_exec(tool_id: str, args: dict[str, Any]) -> dict[str, Any]:
        return {"tool_id": tool_id, "args": args, "ok": True}

    pending = dispatch_tool(
        "notify_oncall",
        {"ticket_id": "T-1024", "message": "升级紧急,请接手"},
        card,
        fake_exec,
    )
    pid = pending["pending_id"]
    assert len(EXEC_LOG) == 0

    approved = approve_pending(pid, "oncall_zhang", fake_exec)
    assert approved["ok"] is True
    assert len(EXEC_LOG) == 1
    assert EXEC_LOG[0]["decided_by"] == "oncall_zhang"
    print("确认后执行完成,EXEC_LOG:", EXEC_LOG)

生产环境里 executor 应走统一 SDK 入口,仍然经过注册表门禁,不允许 confirm 页面直连后端绕过审计。


6. 与验收和审计对齐

第 3 篇验收区分自动项与人工项。二次确认不是替代验收,而是把高危写从自动链路里拆出来:

类型 本篇落地
自动 priority 字段、pending 条数、EXEC_LOG 与 pending 状态一致
人工 通知措辞是否适合对外(可在 approve 页完成)

轨迹建议记录:pending_idstatus、审批人、耗时。验收脚本可以检查:

  • 存在 notify_oncall 时,必须有 approved pending 或合法降级标记
  • critical 工具 EXEC_LOG 必须为空

审计查询应能回答:「谁在什么时间批准了对 T-1024 的通知,用的哪条 message 快照」。

6.1 与 scope 门禁的顺序

推荐顺序:注册与启用 → 允许清单 → schema → scope → 风险确认。scope 不通过时不应创建 pending,否则队列里堆满越权提议,增加值班负担。

6.2 验收项扩展示例

在第 3 篇与第 5 篇验收清单上,可追加:

检查点 期望 类型
notify_pending notify 对应 approved pending 或降级 自动
no_orphan_pending 任务结束无遗留 pending 自动
critical_never_executed delete 等无 EXEC_LOG 自动
notify_wording 对外措辞 人工(approve 页)

这样自动项管状态一致性,人工项管对外表达是否合适,职责与第 3 篇三类验收项一致。


7. 四个常见误区

误区 典型表现 更稳妥的做法
模型自确认 回复「用户已同意」就执行 独立 approve 接口与记录
口头确认不落库 复盘无法核对 pending 快照 + decided_by
pending 无超时 队列堆积、重复通知 expires_at + expired 状态
卡片放宽 critical 删除被自动执行 critical 不可 auto_confirm

还有一种隐蔽做法:把高危工具拆成「预览 + 确认」两个 tool_id,但确认工具不做参数绑定校验。正确做法是:确认操作必须引用 pending_id,执行层只认队列快照,不认模型新参数。

7.1 与模型提示词的边界

系统提示里写「删除前必须询问用户」不能替代 pending。模型询问用户后仍可能在同一轮 function call 里直接 delete。二次确认是运行时硬约束,提示词只是辅助表达。


8. 适用边界

本篇方法适合:

  • 写工具已走注册表与 scope,仍有关键对外副作用
  • 有值班或 On-call 界面可以点 approve
  • 需要可审计的审批记录

本篇不覆盖:

  • 基于业务角色的动态授权(只有工单负责人能批)------需接权限系统
  • 跨 Agent 委托时的确认链------后续编排篇目展开
  • 合规场景下的双人复核与电子签章------需单独流程设计

若团队规模很小、只有单用户本地 Agent,仍建议保留 pending 结构与日志接口,避免将来上多人值班时重写运行时。

8.1 存量系统如何补二次确认

已有 Agent 若高危写仍是「门禁通过即执行」,可以按三步迁移:

  1. 标 risk_level:先把 notify、delete 标 high / critical
  2. 只读旁路:notify 先写 pending 但不阻塞读路径,观察队列长度
  3. 接 approve 页:确认后再写 EXEC_LOG,验收改为查 pending 状态

迁移期允许人工在旧界面操作,但新调用必须走 pending_id,避免双轨执行。


9. 术语速查

术语 含义
风险等级 risk_level 注册表中对写操作严重程度的分类
确认策略 confirm_policy 是否需要人工或策略确认后再执行
待确认 pending 已通过门禁但尚未执行的高危写提议
pending_id 待确认条目的唯一编号
两阶段执行 提议入队 → 确认 → 执行实现
approve / reject 人工或策略对 pending 的通过 / 拒绝

10. 小结与下一篇

工具面四篇递进关系可以概括为:

  1. 注册表:工具目录与读写属性
  2. 作用域:对象级授权
  3. 本篇:高危写的二次确认与 pending 队列

工程上请固定三条规则:

  • risk_level 写在注册表,critical 不可被卡片自动执行
  • 高危写先 pending,确认后用快照参数执行
  • 轨迹与验收能核对 pending 与审批人

下一篇进入编排单元:单 Agent 如何把任务卡片、工具门禁与确认队列串成闭环,并在失败时重试或降级。

系列导航

相关推荐
具身AGI17 分钟前
基座模型架构之争,物理AI 人类学习路线 的双脑答案
人工智能·学习·架构
DeepIntelli18 分钟前
GEO服务、传统网页优化与AI问答引流的区别:从工程视角看三者的目标、链路与验收方式
人工智能
SmartBrain18 分钟前
以史为鉴,对AI时代企业数字化转型的启示
人工智能·架构·创业创新
阿宁学科技术库23 分钟前
关于“库库AI”在股票投研场景实用性的技术观察
大数据·人工智能·职场和发展
Patrick_Wilson30 分钟前
Superpowers 与 Codex Harness:冲突分析与治理提示词
agent·ai编程·cursor
AR-26710-37 分钟前
机器学习复习Day8——异常检测
人工智能·python·机器学习·scikit-learn
TAN-90°-42 分钟前
Deep Learning for Computer Vision——Attention and Transformers
人工智能·深度学习·神经网络·机器学习·计算机视觉·cnn·机器翻译
Profile排查笔记43 分钟前
指纹浏览器手机版怎么选?从本地 App 到云端 Android 的实现方式解析
前端·人工智能·后端·自动化