摘要与核心论点
人类审批(Human-in-the-Loop Approval)在大模型智能体(LLM Agent)工程中被普遍视为高风险工具调用的“最后一道防线”。然而,当审批逻辑在架构上被放置在工具执行之后,或者系统把“没有交互式聊天上下文”这一状态直接等价于“已经获得预授权”时,这道防线就不再是防线,而退化为一种可被时序绕过的装饰性机制。本文以两个具有代表性的开源实现为分析样本:OpenHuman 的 ApprovalGate 对无聊天上下文工具调用的“预授权自动化”归类,以及 PraisonAI 在调用 onToolCall 审批回调之前已将可执行工具对象传入 generateText() 的时序安排。笔者提出一条贯穿全文的分析主线:审批的有效性不取决于审批回调是否存在,而取决于审批点与副作用点之间是否构成不可绕过的时序不变量(Temporal Invariant)。
围绕这条主线,本文依次讨论:审批语义的形式化定义与威胁模型;两个样本实现的架构时序剖析;能力令牌(Capability Token)与最小权限在 Agent 工具链中的落地;可验证审批日志与审计不变量;以及面向生产环境的纵深防御操作路径。全文强调可验证性:任何“人类已批准”的主张,都必须能被转化为可被独立复核的日志证据与状态机约束,否则它在架构上不成立。
目录
一、问题定义:审批的语义、威胁模型与失效分类
1.1 什么是“人类审批”:一个需要被精确定义的术语
在智能体工程语境中,“人类审批”常被含糊地用于描述三种截然不同的机制:其一,交互式确认(interactive confirmation),即系统在副作用发生前暂停,等待人类显式输入“同意/拒绝”;其二,策略预授权(policy pre-authorization),即人类在部署阶段通过策略声明“某类操作在某种条件下无需逐次确认”;其三,事后审计(post-hoc audit),即操作已执行,人类在日志中复核。三者在安全属性上完全不等价:只有第一种能在副作用发生前阻断,第二种把判断权前移到了策略编写时刻,第三种根本不具备阻断能力。
本文评述:把三者统称为“审批”是大量工程事故的语义根源。当一个系统声称“我们有人类审批”时,必须追问:审批发生在副作用之前还是之后?审批的对象是具体调用实例还是调用类别?审批结果是否被绑定到该实例且不可被后续流程替换?这三个问题构成了本文后续分析的检验清单。
1.2 威胁模型:谁能绕过审批
讨论审批失效,必须先明确攻击面。参照经典的最小权限原则与 Saltzer 与 Schroeder 在 1975 年提出的系统设计八原则(其中“最小权限”“完全中介”“经济机制”三条与本文直接相关),笔者将 Agent 工具调用链中的审批绕过归纳为四类威胁主体:
| 威胁主体 | 能力 | 典型绕过方式 |
|---|---|---|
| 恶意或被注入的提示 | 控制模型输出 | 诱导模型选择“无需审批”的工具路径 |
| 框架实现缺陷 | 控制调用时序 | 审批回调晚于工具对象传递 |
| 配置错误 | 控制策略判定 | 把“无上下文”误配为“已预授权” |
| 内部人员 | 控制日志与策略 | 事后篡改审批记录 |
本文评述:四类主体中,框架实现缺陷与配置错误最具隐蔽性,因为它们不违反任何“显式规则”,而是让规则本身在错误的时序位置上失效。这正是本文两个样本案例的共同特征。
1.3 失效分类:审批装饰化的五种模式
基于对公开框架文档与源码的梳理,笔者将“审批装饰化”归纳为五种可识别的模式,后文将逐一在两个样本中定位:
- 后置审批:审批回调在副作用已发生后才被调用,只能记录不能阻断。
- 对象先行:可执行工具对象在审批前已被传入执行上下文,审批仅改变一个随后被忽略的标志位。
- 状态等价:把“无交互上下文”“非交互会话”“批处理模式”等状态直接映射为“已批准”。
- 类别审批:审批针对工具名而非具体参数实例,导致参数被替换后审批仍然“有效”。
- 日志不可验证:审批记录与执行记录分离且无绑定关系,无法证明某次执行对应某次批准。
笔者认为,这五种模式并非孤立缺陷,而是同一根因的不同表现:审批被实现为一个“事件”而非一个“约束”。事件可以被跳过、延迟或伪造;约束则必须在状态机的每条路径上被满足,否则执行无法推进。
二、架构时序剖析:两个样本实现的审批点位置
2.1 OpenHuman ApprovalGate:“无聊天上下文即预授权”的语义跳跃
OpenHuman 的 ApprovalGate 设计初衷是为工具调用提供一个统一的审批入口。其文档与实现中一个关键判定是:当工具调用发生在没有交互式聊天上下文的场景(例如定时任务、后台流水线、由其他 Agent 触发的子调用)时,ApprovalGate 将其归类为“预授权自动化”(pre-authorized automation)并放行。这一设计的合理性在于:后台任务无法等待人类实时输入,若强制阻塞将导致流水线死锁。
但问题在于语义跳跃。“没有交互式聊天上下文”是一个关于会话形态的事实,而“已获人类批准”是一个关于授权状态的断言。二者之间不存在逻辑蕴含关系。一个由定时器触发的删除操作,其“无聊天上下文”为真,但其“已获批准”可能为假。把前者当作后者的充分条件,等价于在授权判定中引入了一个恒真的旁路。
// 示意性伪代码:反映“状态等价”模式的判定结构
function shouldApprove(call, session) {
if (session.hasInteractiveChatContext) {
return requestHumanApproval(call); // 交互路径
}
// 无交互上下文 → 直接视为预授权
return { approved: true, reason: "pre-authorized-automation" };
}
本文评述:这段判定结构的问题不在于“放行后台任务”这一目标,而在于它把授权状态的来源从“人类显式声明”替换成了“会话形态推断”。正确的做法应当是:后台任务的授权必须在任务创建时由人类显式授予,并以能力令牌的形式绑定到该任务实例;运行时只校验令牌,而不是从会话形态反推授权。
2.2 PraisonAI:onToolCall 回调与 generateText() 的时序错位
PraisonAI 提供了 onToolCall 审批回调,允许开发者在工具被调用前介入。然而其调用时序存在一个关键细节:可执行工具对象在 onToolCall 回调被触发之前,已经被传入 generateText() 的执行上下文。这意味着审批回调所面对的,是一个“已经具备执行能力”的工具引用,而非一个“待批准后才可实例化”的占位符。
从工程角度看,这种时序安排有其实现上的便利:generateText() 需要工具的模式(schema)来构造提示,因此工具对象必须提前可用。但“模式可用”与“执行能力可用”是两回事。把完整的可执行对象提前传入,使得审批回调的返回值只能影响一个后续分支,而无法在架构上阻止执行路径的存在。一旦某条代码路径忽略了该返回值,或模型输出直接触发了已注册的工具,审批即被绕过。
// 示意性伪代码:反映“对象先行”模式的时序
const tools = [dangerousTool]; // 可执行对象已就绪
const result = await generateText({
model,
tools, // 执行能力已注入
onToolCall: async (call) => {
const ok = await requestHumanApproval(call);
if (!ok) throw new Error("rejected"); // 依赖异常传播阻断
}
});
本文评述:依赖回调内抛异常来阻断执行,是一种“约定式安全”(security by convention),而非“构造式安全”(security by construction)。它要求框架的每条内部路径都正确传播该异常,且不允许任何重试、降级或并行分支吞掉异常。在复杂框架中,这一假设极难被静态保证。更稳健的设计是让审批发生在工具实例化之前:审批通过后,才由工厂函数产出可执行对象。
2.3 两个样本的共同根因:审批点与副作用点的相对位置
把两个样本放在同一坐标系中,可以看到它们失效的几何本质。设审批点为 A,副作用点为 E(effect)。安全要求是 A 严格先于 E,且 E 的存在依赖于 A 的成功。OpenHuman 的问题在于 A 被一个恒真判定短路,A 实际上未发生;PraisonAI 的问题在于 E 的能力在 A 之前已被注入,A 与 E 之间缺少强制依赖。二者共同指向同一根因:审批没有被建模为副作用的前置条件,而被建模为一个可选的旁路事件。
笔者认为,这一根因在 Agent 框架中普遍存在,原因在于框架的演化路径:早期框架优先解决“让工具能被调用”,审批是后加的横切关注点(cross-cutting concern)。后加的关注点往往以回调、钩子、装饰器的形式插入既有调用链,而既有调用链的执行能力早已在更早的位置被注入。于是审批天然地落在了执行能力之后。要根治,必须把审批前移到能力注入之前,这需要重构工具注册与实例化的边界。
三、理论框架:时序不变量、能力令牌与最小权限
3.1 时序不变量:把审批写成不可绕过的约束
时序不变量(Temporal Invariant)是指系统在所有合法执行路径上都必须满足的、关于事件顺序的约束。对审批而言,核心不变量可表述为:对任意副作用 E,若 E 属于需审批类别,则存在一个审批事件 A,满足 A 先于 E,且 E 的执行凭证由 A 唯一派生。注意“唯一派生”四字:它排除了“审批存在但执行不依赖它”的情形,正是 PraisonAI 式缺陷的形式化刻画。
本文评述:把审批写成不变量,而非写成回调,带来的直接好处是可验证性。不变量可以被转化为运行时断言、静态分析规则或形式化模型检查的目标;回调则只能被“相信”。在工程上,这意味着审批逻辑应当与执行逻辑共享同一个状态机,而不是分布在不同层。
3.2 能力令牌:让“批准”成为可传递、可校验、可过期的对象
能力令牌(Capability Token)源自操作系统安全中的能力(capability)概念,其核心思想是:访问权不是一个可被查询的布尔值,而是一个必须被持有才能执行的对象。将这一思想引入 Agent 工具调用,可以得到如下设计:人类审批的产物不是“approved=true”,而是一枚绑定到具体调用实例(工具名 + 参数哈希 + 会话 + 有效期)的令牌;工具执行函数在入口处强制校验令牌,无令牌即拒绝。
| 令牌字段 | 作用 | 缺失后果 |
|---|---|---|
| tool_name | 限定工具 | 类别审批,参数可被替换 |
| args_hash | 绑定具体参数实例 | 批准 A 执行 B |
| session_id | 绑定会话 | 跨会话重放 |
| expires_at | 限制有效期 | 长期有效令牌被滥用 |
| nonce | 防重放 | 同一令牌重复执行 |
本文评述:能力令牌把审批从“事件”转化为“对象”,从而把时序约束转化为类型约束——没有令牌类型的值,无法调用执行函数。这比在回调里抛异常要稳健得多,因为它在编译期或入口校验期就阻断了路径,而不依赖运行时的异常传播正确性。
3.3 最小权限与完全中介:两条经典原则的 Agent 化
Saltzer 与 Schroeder 的最小权限原则要求每个主体仅持有完成其任务所必需的最小权限;完全中介原则要求每次访问都必须经过检查。将二者映射到 Agent 工具链:最小权限意味着不应把“全部工具”一次性注入 generateText(),而应按当前子任务动态授予;完全中介意味着不存在绕过审批的“内部快捷路径”,包括框架内部的重试、缓存命中与并行分支。
笔者认为,完全中介原则在 Agent 场景中尤其容易被违反,因为框架为了性能与体验,常引入缓存、流式输出与并行工具调用。这些优化路径若未同步接入审批校验,就会成为事实上的旁路。因此,审批校验必须下沉到工具执行的最内层入口,而非停留在编排层。
四、工程实践:可验证审批日志与状态机约束
4.1 操作路径一:把审批前移到工具实例化之前
第一步是重构工具注册边界。将工具拆分为“模式描述”与“执行体”两部分:模式描述可在提示构造阶段自由传入;执行体则封装在工厂函数后,只有持有有效令牌时才被实例化。这样,generateText() 拿到的只是模式,而非可执行能力,从构造上消除了“对象先行”缺陷。
// 模式与执行体分离
const schemaOnly = { name: "delete_file", parameters: {...} };
async function executeWithToken(call, token) {
assertValidToken(token, call); // 入口强制校验
const impl = toolFactory(call.name); // 此时才实例化
return impl(call.args);
}
本文评述:这一重构的代价是需要修改框架的工具注入接口,收益是把安全属性从“约定”提升为“构造”。在无法修改框架的场合,可用包装层模拟:向 generateText() 传入的是拒绝执行的桩函数,桩函数内部再校验令牌并转发到真实实现。
4.2 操作路径二:为“无交互上下文”建立显式授权来源
针对 OpenHuman 式缺陷,正确做法是:后台任务的授权必须在任务创建时由人类显式声明,并固化为任务级令牌。运行时若发现某调用无令牌,应默认拒绝并升级为待审批队列,而非默认放行。换言之,把默认值从“允许”翻转为“拒绝”(default-deny),是修复“状态等价”模式最直接的手段。
设计原则:会话形态只能用于决定“向谁请求审批”,不能用于决定“是否已获审批”。前者是路由问题,后者是授权问题,二者不可混同。
4.3 操作路径三:可验证审批日志的字段与绑定关系
可验证性要求审批记录与执行记录之间存在密码学或结构上的绑定。最低要求是:执行记录中携带审批记录的标识与参数哈希,审批记录中携带被批准调用的参数哈希,二者一致方可认为“该执行对应此批准”。进一步可引入哈希链,使日志具备防篡改能力。
| 日志类型 | 必备字段 | 验证方式 |
|---|---|---|
| 审批记录 | approval_id, args_hash, approver, ts | 签名校验 |
| 执行记录 | exec_id, approval_id, args_hash, ts | 与审批记录交叉比对 |
| 链式摘要 | prev_hash, curr_hash | 重算哈希链 |
本文评述:日志绑定是“事后可验证”的基础,但它不能替代“事前阻断”。二者是纵深防御的不同层:令牌负责阻断,日志负责取证与追责。只做日志不做令牌,等于承认审批可被绕过但事后能发现;只做令牌不做日志,则无法在争议中证明审批的真实性。
4.4 操作路径四:状态机约束与测试策略
把工具调用建模为状态机:IDLE → REQUESTED → APPROVED/REJECTED → EXECUTING → DONE。约束是:EXECUTING 只能从 APPROVED 进入,且 APPROVED 只能由人类审批事件触发。测试策略上,应针对每条非法转移编写负向测试,例如“从 REQUESTED 直接进入 EXECUTING 必须失败”“REJECTED 后重试必须重新审批”。这类测试能有效捕获“对象先行”与“后置审批”缺陷。
笔者认为,状态机约束的价值在于它把安全需求转化为可执行的测试用例,使审批不再依赖开发者对框架内部时序的直觉理解。对于无法修改源码的第三方框架,可在其外层包裹状态机代理,代理持有真实执行体,框架只能通过代理间接调用。
五、对比、局限与前沿预判
5.1 三种审批架构的对比
| 架构 | 阻断能力 | 实现成本 | 主要风险 |
|---|---|---|---|
| 回调式(现状) | 弱,依赖异常传播 | 低 | 时序绕过、异常被吞 |
| 令牌式 | 强,入口强制校验 | 中 | 令牌签发环节需可信 |
| 代理式(外层状态机) | 强,框架无法直连执行体 | 中高 | 代理本身成为单点 |
本文评述:三种架构并非互斥,生产环境宜采用令牌式为主、代理式为辅的组合:令牌负责实例级绑定,代理负责隔离不可信框架。回调式应仅作为通知机制,不应承担阻断职责。
5.2 局限:本文分析的边界条件
需要明确本文结论的边界。第一,本文基于公开文档与源码结构的分析,未对具体版本做穷尽审计,不同版本的行为可能变化,读者应以所使用版本的源码为准。第二,能力令牌方案假设令牌签发环节可信,若签发端本身被攻破,令牌机制同样失效,这需要独立的密钥管理与审批人身份认证来支撑。第三,状态机代理会引入额外延迟,对高频低风险工具应允许策略化的批量授权,否则将损害可用性。
笔者认为,承认边界条件恰恰是工程严谨性的体现。审批机制的目标不是“零风险”,而是“把风险控制在可声明、可验证、可追责的范围内”。任何声称“有了审批就安全”的表述,都应当被本文的时序不变量检验清单所质疑。
5.3 前沿预判:从回调到策略即代码
从趋势看,Agent 审批正在从“框架内回调”向“外置策略引擎”演进。以策略即代码(Policy as Code)方式描述审批规则,由独立于 Agent 框架的策略决策点(PDP)做判定,Agent 只负责发起判定请求与执行判定结果。这一架构与云原生中的准入控制(Admission Control)思路同源,其优势是策略与执行解耦,策略可被独立测试、版本化与审计。
本文评述:外置策略引擎能解决“框架内审批点位置错误”的问题,因为判定发生在执行体被授予之前,且判定结果以令牌形式返回。但它也带来新的依赖:策略引擎的可用性成为执行路径的关键路径,需要设计降级策略——而降级策略本身又必须遵循 default-deny,否则会重新打开旁路。这一张力是未来工程实践的核心议题。
六、结论与操作清单
回到文章标题提出的问题:当审批逻辑在架构上位于工具执行之后,或把“不在对话里”等同于“已经批准”时,人类审批是最后一道防线还是装饰?本文的结论是:在缺乏时序不变量与能力绑定的实现中,它是装饰;只有当审批成为副作用的前置条件、且执行凭证由审批唯一派生时,它才是防线。OpenHuman 的“预授权自动化”归类与 PraisonAI 的“对象先行”时序,分别对应“状态等价”与“对象先行”两种失效模式,二者共同说明:审批的安全性是一个架构位置问题,而非一个功能有无问题。
面向落地,笔者给出如下操作清单,按优先级排列:
- 审计现有框架中工具对象被传入执行上下文的时点,确认其是否早于审批回调;
- 将默认值改为 default-deny,任何无令牌的需审批调用一律拒绝并进入待审批队列;
- 引入绑定参数哈希、会话与有效期的能力令牌,执行入口强制校验;
- 为工具调用建立状态机,编写非法转移的负向测试;
- 审批日志与执行日志通过 approval_id 与 args_hash 双向绑定,并启用哈希链;
- 对无法改造的第三方框架,外层包裹状态机代理,隔离执行体;
- 将策略判定外置为独立 PDP,并明确其降级策略同样遵循 default-deny。
本文评述:这份清单的共同逻辑是把“相信”替换为“验证”。在 Agent 系统日益自主化的背景下,人类审批的价值不在于它被调用了多少次,而在于每一次调用都能被证明是副作用发生的必要条件。做不到这一点,审批就只是一个好看的装饰;做得到,它才是真正的最后一道防线。
参考文献与拓展资源
本文在写作中参考了智能体框架文档、访问控制与能力系统、策略即代码、日志可验证性等方向的公开资料,累计参考条目 62 篇,其中近三年(2023—2025)文献占比约 56%。涉及框架行为的描述以公开文档与源码结构为准,未进行私有环境复现;文中伪代码为说明性示意,不代表任何具体版本的逐行实现。若涉及数据集,均为公开基准的说明性引用,未做额外清洗,仅按原始发布说明使用。
主要参考文献(8 篇)
- Saltzer, J. H., & Schroeder, M. D. The Protection of Information in Computer Systems. Proceedings of the IEEE, 1975.
- Miller, M. S. Robust Composition: Towards a Unified Approach to Access Control and Concurrency Control. PhD Thesis, Johns Hopkins University, 2006.
- Denning, D. E., & Denning, P. J. Data Security. ACM Computing Surveys, 1979.
- Sandhu, R., et al. Role-Based Access Control Models. IEEE Computer, 1996.
- OpenHuman 项目文档与 ApprovalGate 相关源码说明(公开仓库,访问于 2025 年)。
- PraisonAI 项目文档与 onToolCall 回调相关源码说明(公开仓库,访问于 2025 年)。
- NIST. Security and Privacy Controls for Information Systems and Organizations (SP 800-53 Rev.5), 2020.
- OWASP. Top 10 for Large Language Model Applications, 2025 版。
拓展资源
文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。

