
构建 Agent 控制平面:让工具调用从「能执行」变成「被授权」
构建 Agent 控制平面:让工具调用从「能执行」变成「被授权」
Agent 能把模型输出变成动作,却常常答不上一个更关键的问题:这个动作到底被允许发生吗?本文拆解规划平面、控制平面、执行与观测平面三层结构,给出九步落地方法:收窄动作契约、分离身份与委派、把授权做成确定性决策、按后果分级、把审批绑定到规范化动作、防御提示注入、限定执行权限、独立验证结果、用账本记录决策轨迹。
大多数 Agent 技术栈已经非常擅长把模型输出变成一次动作:模型选对了工具、参数 JSON 语法合法、接口返回 HTTP 200、追踪面板上那个工具调用 span 是绿色的。但系统仍然可能取消了错误的账户。问题不在于「这个调用格式对不对」,而在于「这个动作到底有没有被授权发生」。
Schema 校验只告诉你输入是否成形;认证只告诉你请求是谁发来的;工具定义只告诉模型它可以请求什么。这些都不能证明:当前主体是否有权取消那个特定的订阅、那个特定的账户,也不能证明取消真的生效了。本文要讲的,就是如何为 Agent 系统搭一层控制平面,把「能力」和「权限」分开处理。这是一篇偏架构与工程实践的参考型教程,适合正在把 Agent 从演示推向生产、需要处理高风险动作的工程师与架构师阅读。
准备工作
控制平面不是一段提示词能解决的事,它需要一些前置条件先到位,否则后面九步都落不了地。
- 一份动作清单。把系统当前所有会对外产生副作用的调用列出来,标注它改的是什么资源、影响范围有多大、能不能撤销。没有这份清单,后面的分级无从谈起。
- 一个策略决策点(PDP)。可以是一个独立的策略服务,也可以是嵌在网关里的策略引擎,但必须是模型之外、模型改不动的组件。策略的输入是结构化的动作提案,输出是 allow / deny / approval_required 三态之一。
- 一个执行代理(broker)。它是唯一持有高权限凭据的组件,规划层拿不到这些凭据。所有对外的写操作都从它这里出去。
- 一套身份体系。至少要能区分三类身份:人类用户、Agent 自身、以及「人类委派给 Agent」的临时授权。三者不能混用同一个 token。
- 一个不可篡改的账本。用于记录决策与结果的迁移,而不是事后从聊天记录里反推。聊天记录是可变的,不能当审计日志。
- 一个验证通道。能够读取权威的后置条件或回执,用来判断动作到底有没有生效。
这些组件不一定一开始就齐全,但至少要清楚缺哪一块,以及缺的那块会让哪一类动作失去保护。
操作步骤
第一步:把动作契约收窄并且强类型化
核心原则是:不要因为模型会用,就暴露 http_request、run_shell、裸 SQL 或者不受限的浏览器这类通用工具。应该暴露的是有明确边界的业务级动词,例如 create_draft_invoice、queue_refund_review、send_approved_notice、revoke_session。
工具 schema 在这里的角色是接口契约:它减少自由形式的歧义,但它本身不授权任何动作。一个干净的 schema 应该同时声明以下字段:
| 字段 | 作用 |
|---|---|
action_id 与 schema_version | 唯一标识动作及其契约版本,便于策略按版本匹配 |
side_effect_class | 取值如 read_only、draft、reversible_write、external、irreversible |
required_scopes 与 allowed_environments | 声明所需权限范围与允许运行的环境 |
risk_tier 与 approval_mode | 风险等级与对应的审批方式 |
idempotency_requirement | 是否要求幂等键,以及幂等键的生成规则 |
verification_method | 动作完成后如何验证生效 |
data_classification | 涉及的数据分级与出站数据规则 |
主流模型厂商都支持结构化的工具输入与函数 schema,但它们的客户端执行模型也说明了同一件事:模型只是请求调用,真正执行工具的是应用侧。控制平面要利用的正是这个缝隙。
第二步:把认证、委派、审批拆成三件事
常见做法是用户登录一次,然后把用户的 token 直接交给 Agent,之后假定所有工具调用都已获授权。这会把一次临时请求变成环境权限(ambient authority),也让审计变得困难:到底是人做的、Agent 替人做的,还是某个共享系统身份做的?
正确做法是给 Agent 独立的、可归因的身份,而不是让它长期以通用服务账号运行,或者借用人类会话。需要把三个概念分开实现:
- Agent 身份:每个 Agent 有独立身份,动作可归因到具体实例。
- 人类身份:发起请求的真实用户。
- 委派关系:人类把有限权限授予 Agent 的这段关系,包含范围与有效期。
OAuth 2.0 的设计思路在这里有参考价值:它通过授权层提供有限访问,而不是让第三方持有资源所有者的密码;访问令牌本身就承载了范围、有效期等属性。但要注意,OAuth 解决的是委派词汇的问题,它并不等于 Agent 治理——策略执行、验证、监控、撤销仍然要由控制平面自己完成。
第三步:让授权成为一个确定性决策
任何模型输出都不应该直接抵达有副作用的对外接口。正确的链路是:
- 模型产出结构化的
ActionProposal。 - 动作网关校验这个提案。
- 策略决策点返回
allow、deny或approval_required。 - 由执行代理(而不是规划层)拿到受限的、短期的执行权限去执行被允许的动作。
这条链路里有几个不能妥协的点:
- 策略检查发生在 LLM 之外。
- 决策做出之前,执行代理不得执行。
- 如果需要审批,审批必须绑定到即将派发的那个规范化摘要上。
- 派发返回 200 不等于成功,输出不能自动判定为成功。
- 账本围绕「决策」与「结果」的状态迁移来写,而不是事后从聊天记录重建。
零信任架构的思路在这里同样适用:授权是动态的,策略决策可以综合考虑请求本身、身份与属性、资源要求以及上下文信号,而不是把一次登录当作永久信任。
第四步:按后果给动作分级
不应该把整个 Agent 笼统地归类为「全自主」或「人在回路」。这个分类属于单个动作和数据流,而不是整个 Agent。同一个 Agent 可以自动读取受限知识库、在预发环境创建草稿,同时在修改生产访问权限前要求双人复核。
分级模型可以按自己的场景调整,但升级规则值得固定下来:
- 目标身份是推断出来的而不是用户明确选择的,升一级。
- 动作受到不可信内容实质影响的,升一级。
- 没有幂等机制、也没有权威验证路径的,升一级。
- 范围变成跨租户、批量、对外、不可逆、涉及重大金额、涉及法律后果或生产关键的,升一级。
- 永远不要因为模型表达了高置信度就自动降级。
动作是否由模型发起并不重要,重要的是这个动作的后果是否具有实质性。
第五步:把审批绑定到规范化动作
这一步必须具体,不能含糊。举例来说,用户写「请处理一下这个账单问题」,Agent 随后自行选择了收款方、取消原因、订阅金额和退款选项——这不是对实际效果的授权。
审批人必须看到即将派发的效果的规范化表示,而不是模型的自然语言意图。并且,一旦动作发生变化,原审批必须失效。这意味着审批记录需要包含以下字段:
| 字段 | 说明 |
|---|---|
| 动作名称与 schema 版本 | 明确审批针对的是哪个契约版本 |
| 租户、用户/主体、Agent 运行实例、执行者身份 | 四类身份都要留痕 |
| 解析后的目标资源与关键参数 | 不是模型原话,而是解析后的实际值 |
| 风险等级与要求审批的策略版本 | 说明为什么需要这次审批 |
| 规范化动作与资源版本的摘要/哈希 | 审批与派发必须比对同一个摘要 |
| 审批人身份、角色、决定、时间、有效期、理由 | 完整的审批元数据 |
| 一次性 nonce/state | 防止审批被静默重放 |
这对界面也有要求:任何变更都要用业务语言描述清楚;界面需要突出收款方、目标、金额、范围、数据类型、环境和不可逆性;审批人必须能在有意义的决策边界上拒绝或修改,而不是在提供方已经接受请求之后才介入。
第六步:防御提示注入
不可信的网页、邮件、检索到的文档以及工具输出,不会因为模型能读它们就自动升级为指令。它们应被显式归类为不可信数据;必要时,先在一个无工具、无密钥的隔离步骤里处理,提取出事实之后,再交给有权限的 Agent 使用。
提示注入、用户意图歧义、上下文过期、凭据范围过宽,这四类问题都会把一个「合法的工具调用」变成错误的现实效果。OWASP 在 Agent 系统风险清单里明确列出了过度自主、高影响动作滥用、审批操纵、工具滥用、数据外泄和级联失败等条目。工程上的应对不能是「在提示词里再加一条指令」,而必须是当后果足够重要时仍然保持确定性的控制平面。
第七步:只发放窄范围、短时效的执行权限
执行代理不应该持有长期的高权限凭据。它应该按次换取窄范围、短时效的执行权限:只覆盖这一次动作、这一个资源、这一段时间。动作完成或超时后,权限立即失效。
这样做的直接好处是:即使某次决策被绕过,攻击者拿到的也只是一次性的、范围极窄的凭据,而不是可以横向移动的长期密钥。
第八步:独立验证结果
执行代理调用受限适配器或隔离 worker,资源服务或提供方执行动作,可能返回失败,也可能返回不确定的结果。此时必须由验证器读取权威的后置条件或回执,并把状态明确区分为:
pending:已派发,尚未确认。unknown:结果不确定,需要人工或后续任务介入。failed:明确失败。verified:已通过权威来源验证生效。
把这四种状态混为一谈,是很多「看起来成功」的事故的根源。
第九步:用账本记录决策与效果轨迹
审计账本与遥测系统要保留的是决策—效果的完整轨迹,而不是把可变的聊天记录当作审计日志。账本应围绕状态迁移来写:提案产生、策略判定、审批发生、权限发放、派发、验证结果。每一步都有时间戳和主体身份,事后可以完整还原「为什么这个动作被允许」。
一个完整示例
把上面的步骤串起来,看一个取消订阅的场景。用户消息是:「请把那个不再使用的账户的订阅取消掉。」
规划平面:模型选择 cancel_subscription,从更早的对话里提取出一个账户 ID,产出结构化的动作提案:
{
"action_id": "cancel_subscription",
"schema_version": "3",
"side_effect_class": "irreversible",
"risk_tier": "high",
"approval_mode": "explicit",
"idempotency_requirement": "required",
"verification_method": "provider_receipt",
"target": { "account_id": "acct_8812" },
"reason": "user_requested_unused_account"
}
控制平面:动作网关校验提案,策略决策点检查发起主体是否有权取消该租户下的这个账户。这里有一个关键判断——目标账户 ID 是从历史消息里推断出来的,而不是用户明确选择的。按照第四步的升级规则,这会把风险等级再抬一级,策略返回 approval_required。
系统生成规范化摘要,把目标账户、取消原因、生效时间等关键参数固定下来,交给审批人。审批人看到的是业务语言描述的实际效果,而不是模型的原话。审批通过后,记录里带上审批人身份、时间、有效期、一次性 nonce,以及被审批的那个摘要哈希。
执行与观测平面:执行代理按次换取窄范围、短时效的执行权限,调用受限适配器。提供方返回回执。验证器读取回执,确认该账户的订阅状态确实变为已取消,把状态从 pending 迁移到 verified。整个过程写入账本。
对比一下没有控制平面的版本:模型选对了工具、JSON 合法、接口返回 200、追踪面板全绿——但取消的可能是另一个账户。差别不在于模型能力,而在于有没有一层独立于模型的授权与验证。
注意事项
- 审批不是普遍适用的。控制平面可以对低风险、只读、范围窄的动作直接授权;对后果重大但可逆的动作要求用户确认;对关键动作直接拒绝,让 Agent 去准备材料,交给人工操作手册执行。不要把所有动作都塞进同一个审批流程。
- 审批必须绑定到同一个摘要。如果审批时看到的参数和派发时的参数不是同一个规范化摘要,审批就形同虚设。动作一旦变化,原审批立即失效。
- 不要用模型置信度做降级依据。模型说「我很确定」不构成降低管控级别的理由。
- 聊天记录不是审计日志。它是可变的,不能用来重建决策过程。账本要围绕决策与结果的状态迁移来写。
- 200 不等于成功。派发返回成功只说明请求被接受,真正的生效与否要靠独立的验证通道确认。
- 不可信内容不因可读而可信。网页、邮件、检索文档、工具输出都应显式标记为不可信数据,必要时先在无工具、无密钥的隔离环境里处理。
- 身份不能混用。Agent 身份、人类身份、委派关系要分开实现,避免把临时请求变成环境权限。
- 本文涉及的风险分级、审批模式、字段设计属于工程实践建议,具体实现方式与各平台的能力、配额、版本相关,请以相关产品官网的当前信息为准。本文不构成安全合规或法律层面的专业意见。