给 Agent 开了数据库写权限,第二天它批量更新了三万行——不是它"想作恶",而是它在完成一次"清理脏数据"的任务时,把 WHERE 条件理解宽了,并且没有任何一步拦住它。
传统服务账号的权限设计假设三件事:调用者固定、行为可预演、错误有界。Agent 三条都不满足。所以它需要一套分层的、带审批点与影响上限的权限模型,而不是一把更大的 key。
1. 与传统服务账号的三个差别
| 维度 | 服务账号 | Agent | 直接后果 |
|---|---|---|---|
| 调用者 | 确定(某个服务) | 不确定(同一次会话内意图会漂移) | 无法按调用方静态授权 |
| 行为 | 代码固定,可预演 | 由计划与上下文生成,不可预演 | 白名单要按"能力"而非"语句"划 |
| 错误 | 失败即停,影响面有界 | 会连续放大(循环调用、批量重放) | 必须设影响范围与熔断 |
结论:服务账号靠最小权限就够;Agent 要在最小权限之上再加影响上限、时间窗、审批三层。
2. 权限的四层结构
| 层 | 控制什么 | 典型实现位置 | 缺这一层会怎样 |
|---|---|---|---|
| L1 工具白名单 | 能用哪些能力 | Agent 注册表 / 工具清单 | 它能调用未授权的工具 |
| L2 参数约束 | 参数取值与形状 | 工具 schema + 服务端校验 | 能拼出越界查询 |
| L3 影响范围 | 一次操作能碰多少数据 | 服务端配额与事务边界 | 一次误判改三万行 |
| L4 时间窗与批次 | 什么时候、分几次做 | 调度器 / 审批网关 | 深夜批量写无人察觉 |
// 工具声明:四层写在同一处,避免散落在提示词里
tool: db_update
capabilities: [write]
params:
table: { enum: ["orders", "tickets"] } // L2 参数约束
filter: { required: true, min_length: 8 } // 禁止无条件全表
limits:
max_rows_per_call: 200 // L3 影响范围
max_calls_per_task: 5
window:
allowed_hours: "09:00-19:00" // L4 时间窗
requires_approval_above_rows: 50
关键点:这些约束必须在服务端可执行。写在提示词里的"请勿全表更新"是建议,不是权限。
3. 读、写、删必须分级
| 级别 | 语义 | 默认策略 | 典型例子 |
|---|---|---|---|
| R0 公开读 | 无副作用、无隐私 | 免审批 | 查文档、读配置 |
| R1 受限读 | 含个人数据或商业秘密 | 记录审计,必要时脱敏 | 查用户资料、读工单 |
| W1 幂等写 | 重复执行结果一致 | 配额限制 | 打标签、写缓存 |
| W2 不可逆写 | 覆盖、批量更新 | 需审批 + 影响预估 | 批量改状态、发通知 |
| D 删除 | 不可恢复或难恢复 | 单独一级,强制人工确认 | 删记录、删索引、删文件 |
"删"不能和"写"共用一级,因为两者的恢复成本差一个量级:写错了可以再写一次覆盖回去,删错了只能靠备份。恢复成本决定审批成本。
4. 审批点:哪些必须人确认,确认时展示什么
审批的设计目标不是"多一道门",而是让人在 30 秒内判断该不该放行。所以确认界面必须带四样东西:
| 展示项 | 作用 | 缺失后果 |
|---|---|---|
| 意图摘要(一句话) | 判断是否符合任务 | 只能凭参数猜目的 |
| 精确参数与目标对象 | 判断影响面 | 无法发现范围过宽 |
| 影响预估(行数/金额/对象数) | 判断代价 | 只能盲批 |
| 可撤销性(能否回滚、多久内) | 判断风险 | 一律拒绝或一律通过 |
// 审批请求:机器生成,人只需看 impact 与 reversible
{
"intent": "把超时未支付的订单标记为关闭",
"tool": "db_update",
"params": { "table": "orders", "set": {"status": "CLOSED"},
"where": "created_at < NOW() - INTERVAL 7 DAY AND status = 'PENDING'" },
"impact": { "estimated_rows": 1280, "tables": ["orders"], "irreversible": false },
"reversible": { "window": "30m", "method": "backup_snapshot" },
"requester": "agent:session-7f2a", "approver_policy": "oncall"
}
必须人确认的判据可以写死成三条:不可逆、影响超过阈值、触碰生产数据。其余走自动放行 + 事后审计,否则审批会变成噪音。
5. 确认疲劳会让审批形同虚设
| 症状 | 机制 | 修正 |
|---|---|---|
| 通过率极高且审批耗时极短 | 人在机械点击 | 提高阈值,减少低价值审批 |
| 审批请求集中在同一类操作 | 阈值定得过低 | 该类改自动放行 + 抽样复核 |
| 出现"批量批准"入口 | 审批量超过人的处理能力 | 按影响降级为异步审计 |
| 审批人长期是非相关角色 | 责任错配 | 换成能判断影响面的人 |
6. 幂等与重试:同一条指令被重放
Agent 的重试比服务间重试更危险:它可能重新生成参数再发一次。
| 重试形态 | 风险 | 处理 |
|---|---|---|
| 同参数重发(网络失败) | 重复副作用 | 幂等键 = 任务 id + 工具 + 参数哈希 |
| 重新生成参数再发 | 两次不同操作 | 只允许原样重试,禁止重新规划后自动重发 |
| 超时后不确定是否成功 | 状态未知 | 先查询再决定,不做"再写一次" |
| 批量任务部分失败 | 半完成状态 | 记录进度点,支持从断点续做 |
// 幂等键由意图内容决定,不由调用次数决定
key = sha256(f"{task_id}:{tool}:{canonical(params)}")
if seen(key) and seen(key).status == "DONE":
return seen(key).result // 重放直接返回原结果
execute_once(key, lambda: call(tool, params))
参数需键序归一后再哈希。
7. 沙箱与真实环境的边界
| 边界 | 沙箱允许 | 生产允许 | 常见错误 |
|---|---|---|---|
| 数据范围 | 合成或脱敏数据集 | 真实数据 | 沙箱脚本直接指向生产库 |
| 写操作 | 可自由写 | 需审批与配额 | 把演练用的宽权限复制过去 |
| 删除 | 常开放 | 单独审批 | 沙箱养成"随手删"的习惯 |
| 凭据 | 独立测试账号 | 单独签发、可即时吊销 | 一套凭据两处用 |
**"演练环境的权限不能顺手复制"**是这条边界最贵的一课:权限一旦复制,约束也随之失效,而习惯已经养成。
8. 审计要记录什么
| 字段 | 为什么必须有 |
|---|---|
| 意图(自然语言摘要) | 事后判断"它当时想干什么" |
| 参数(归一化后的完整值) | 复现与举证 |
| 结果(成功/失败/影响行数) | 判断实际发生了什么 |
| 发起人(人 + Agent 会话 id) | 责任落点 |
| 审批人与其看到的视图版本 | 判断批准是否知情 |
| 时间与工具版本 | 对齐发布与配置变更 |
// 审计事件:只追加,不允许 Agent 自己改写
{ "ts": "2026-11-09T10:00:00Z", "actor": "agent:session-7f2a",
"human": "USER_ID", "tool": "db_update", "params_hash": "HASH",
"impact_rows": 1280, "approved_by": "ROLE_A", "result": "OK" }
审计的价值不在合规,而在归因:没有意图字段,日志只能告诉你"改了什么",不能告诉你"为什么"。
9. 提权路径必须显式,不能由推理产生
Agent 遇到权限不足时,自然的反应是"找个有权限的路径"。这条路径要么被显式设计,要么它自己发明一条:
// 唯一的提权入口:申请、审批、限时、记名
escalation:
request: { reason: required, scope: [tool, params, rows], ttl: "2h" }
approve: { roles: ["ROLE_A"], max_ttl: "8h" }
effect: { grant: temporary, revoke_on: [task_end, ttl_expiry] }
audit: [requester, approver, grant_scope, expiry]
禁掉的形态要明确写出来:不允许 Agent 修改自身工具清单、不允许通过写配置文件扩展权限、不允许复用他人凭据。
10. 人机责任划分
| 情形 | 责任落点 | 依据 |
|---|---|---|
| Agent 按批准的范围执行 | 批准者 | 批准时可见影响预估 |
| Agent 超出批准范围 | 权限模型设计者 | 约束未在服务端生效 |
| Agent 理解错意图 | 指令下达者 | 意图未被写成可验证的判据 |
| 未经审批的自动操作出事 | 策略配置者 | 阈值与放行规则由其设定 |
| 审计缺失导致无法归因 | 平台方 | 审计字段不完整 |
"Agent 做了什么不该由 Agent 负责"——它没有资产、没有判断力、也没有拒绝的能力。把它拟人化成责任主体,等于让真正的责任方隐身。
11. 度量
| 指标 | 口径 | 异常信号 |
|---|---|---|
| 高权限操作数 | W2 + D 级调用次数 | 突增或集中在单一会话 |
| 审批通过率 | 通过 / 请求 | 接近 100% 且耗时刻度极短 |
| 越权尝试数 | 被 L1–L3 拦下的调用 | 上升说明工具描述误导 |
| 回滚次数 | 执行后回退的操作数 | 上升说明影响预估不准 |
| 平均影响行数 | 单次 W2 操作的影响量 | 超出阈值却未触发审批 |
12. 排错:五种"权限配了还是出事"
| 现象 | 原因 | 处理 |
|---|---|---|
| 一次改了三万行 | 只有白名单,没有影响上限 | 补 L3 配额与服务端事务边界 |
| 审批全部通过、无人细看 | 阈值过低导致疲劳 | 提高阈值,只留不可逆与大影响 |
| 重试后数据被改两次 | 无幂等键或键含调用序号 | 幂等键用参数哈希,重放返回原结果 |
| 出事后无法归因 | 审计无意图与审批视图 | 补意图摘要与审批快照 |
| 权限不足时它自己绕过 | 未定义提权入口 | 显式提权通道 + 禁用自改权限 |
13. 边界:什么时候可以只做工具白名单
| 情形 | 可压缩到 | 理由 |
|---|---|---|
| 纯只读场景 | L1 白名单 + 审计 | 无副作用,影响面为零 |
| 输出由人复核且不落库 | L1 + L2 | 人有最终拦截点 |
| 影响可即时撤销(草稿、临时文件) | L1 + 幂等键 | 恢复成本接近零 |
| 单用户本地工具 | L1 | 调用者、被执行者、受害者同一人 |
| 高影响且不可逆(资金、删除、外发) | 四层全开 + 强制审批 | 恢复成本决定审批成本 |
判据始终是同一条:这次操作的恢复成本有多高。恢复成本低,权限可以宽;恢复不了,就必须有人先看一眼。
14. 自测
- Agent 与服务账号的三个差别中,哪一条直接决定了白名单要按能力划分?
- 四层权限里,哪一层缺了会导致"一次误判改三万行"?
- 为什么"删"不能与"写"共用同一权限级?
- 审批界面必须展示的四项信息,缺哪一项会让人只能盲批?
- 幂等键为什么不能包含调用序号或重试次数?
- 什么情形下权限模型可以只保留工具白名单一层?