Agent 能调用业务接口之后,下一个问题往往是“什么情况下必须停下来等人”。真正困难的不是加一个审批按钮,而是划清边界:治理契约应该声明哪些可移植条件,运行时应该控制什么,业务系统又必须保留什么。
当团队用 Dify、MCP、函数调用或自研 Agent 接通一套业务系统后,审批需求通常很快出现。
最初的规则也许很简单:
退款金额超过 1000 元时,需要人工审批。接着,规则会逐渐变成:
退款金额超过 1000 元,并且客户不是 VIP 时,需要经理审批; 如果订单来自高风险地区,还要经过风控; 如果是全额退款,则由财务复核; 黑名单客户直接拒绝; 夜间发起的退款只能等到次日处理。于是一个很自然的想法出现了:
既然已经有一份 Agent 能力声明,为什么不把这些规则全部写进去?
如果沿着这条路继续走,能力声明很快就会长出嵌套的AND、OR、NOT,再加入变量引用、数据库查询、身份属性、时间窗口、审批人匹配、工作流跳转和自定义函数。
最后,它不再是一份不同运行时可以共同理解的能力契约,而会变成另一门尚未被完整定义的业务规则语言。
问题因此不是“条件越多越好”,而是:
条件审批应该表达多少,才能既让不同运行时得到一致的治理意图,又不假装自己能够取代企业的策略引擎、审批系统和最终业务授权?
1. 先把四个经常混在一起的问题拆开
一次 Agent 工具调用进入审批链路时,至少存在四个不同问题。
1.1 这次调用是否需要外部决策
例如:
amount > 1000这是调用发生之前,可以根据已声明参数判断的治理意图。
1.2 外部决策由谁作出
可能是当前用户、门店店长、财务人员、OA 流程、风控系统或另一个组织。
这是审批所有权和组织流程问题。
1.3 审批过程怎样暂停和恢复执行
有界面的 Agent 可以弹出确认页;无界面的 API Gateway 可以返回待处理状态;异步运行时可以创建审批任务并挂起调用;确定性工作流可以暂停节点。
这是运行时控制问题。
1.4 当前主体最终有没有权执行
审批通过之后,业务系统仍要检查:
- 当前员工是否有退款权限;
- 订单是否属于当前租户;
- 订单状态是否仍允许退款;
- 可退款余额是否充足;
- 同一笔退款是否已经执行。
这是最终业务授权,也就是 Authority。
这四件事相关,却不能被压缩成一个approved: true。
一份可移植的治理契约,最多只能稳定声明第一件事:这次调用是否产生了外部决策意图。
2. 条件审批不是业务授权
假设接口声明了:
approval:when:-param:amountop:">"value:1000它表达的是:
当本次调用参数
amount大于 1000 时,运行时应在执行前创建审批意图。
它不表达:
- 金额小于等于 1000 时业务系统必须放行;
- 审批通过后退款一定合法;
- 调用者具有处理该订单的权限;
- 当前订单还有足够的可退款余额;
- 这笔退款没有违反黑名单或监管规则。
条件未触发,只代表这条声明没有要求外部审批。
它不代表业务授权已经完成。
这一区分非常重要。否则,团队很容易把一个轻量的调用前治理判断,错误升级成业务系统必须信任的权限结论。
3. 什么样的条件才适合进入通用契约
一个条件要成为跨运行时可移植的声明语义,至少应该同时满足几项要求。
3.1 输入来自当前调用,而不是私有业务数据库
运行时可以直接得到:
{"order_id":"SO-1001","amount":1500}因此,它可以一致判断amount > 1000。
但“退款金额是否超过当前可退款余额”需要查询订单、历史退款和实时状态。不同运行时并不知道:
- 去哪里获取余额;
- 用谁的身份查询;
- 数据多久算新鲜;
- 查询失败时如何处理;
- 审批等待期间数据变化后是否重新计算。
这类规则属于业务操作或专门的预检 API,不应被伪装成静态契约条件。
3.2 比较值是明确字面量,而不是未定义的上下文变量
下面的声明容易理解:
-param:amountop:">"value:1000而下面的写法看似更强,实际上引入了新的协议问题:
-param:amountop:"=="value:"$order.refundable_balance"$order.refundable_balance从哪里来、谁能读取、何时失效、拿不到时怎么办,都没有答案。
一个没有数据来源、授权、时效和失败语义的“变量”,不会增强契约,只会制造可执行的假象。
3.3 条件必须具有确定的类型语义
如果调用参数是字符串:
{"amount":"1500"}运行时不能擅自把它转换成数字1500再进行比较。
否则,不同语言和框架可能采用不同的隐式转换规则:
- 某个 JavaScript 实现认为条件成立;
- 某个严格类型实现直接报错;
- 某个实现把非法值当成条件未命中,于是绕过审批。
对治理条件而言,“类型不兼容”不能被安静地解释为“不需要审批”。正确行为是:在调用到达业务操作之前拒绝这次无效调用。
3.4 独立实现必须得到同一个判断结果
如果一条声明在两个兼容运行时中可以分别被解释成“需要审批”和“不需要审批”,那就不是实现自由,而是规范歧义。
审批界面的样式可以不同,审批任务存在哪里也可以不同;但相同参数是否产生审批意图必须一致。
4. ACC v1 为什么使用有界的条件模型
ACC v1 的approval可以表达无条件审批意图:
x-agent-capability:version:1enabled:truescope:refund.createapproval:required:trueprompt:"Approve refund for order {order_id}?"approval.required: true表示每一次调用都需要创建审批意图。
条件规则不能降低这项要求。即使所有条件都未命中,无条件审批仍然成立。
它也可以表达参数级条件审批:
x-agent-capability:version:1enabled:truescope:refund.createapproval:when:-param:amountop:">"value:1000label:"退款金额超过 1000 元"-param:batch_countop:">"value:20label:"批量退款超过 20 笔"当approval.required为false或省略时,approval.when使用ANY语义:任意一条条件命中,就创建审批意图。
也就是说,上面的规则表达:
amount > 1000 或者 batch_count > 20任一成立,都需要外部决策。
如果approval.when不存在或为空,则不会产生条件审批意图。
ACC v1 把操作符限制为>、>=、<、<=、==、!=、in、contains和exists。其中:
- 大小比较只接受数值参数和有限数值字面量;
==、!=与in使用 JSON 类型感知的严格相等;contains只适用于字符串或数组,数组元素仍按严格 JSON 相等判断;exists只判断参数路径是否存在,不要求value;param必须解析到绑定操作已经声明的输入,不能凭空访问其他上下文。
这套模型不复杂,但它有一个重要性质:审批要求是单调增加的。
- 增加一条条件,只会增加可能触发审批的情况;
- 某条条件未命中,不能取消另一条已经命中的条件;
- 条件规则不能取消
approval.required: true; - 模型输出不能把治理元数据改成更宽松的值。
在安全边界中,这种可预测性往往比表达式的丰富程度更有价值。
5. 为什么 v1 不急着加入任意 AND / OR 嵌套
开发者很容易提出这样的需求:
金额超过 1000,并且商品属于电子产品时才审批。如果只看这一条规则,加入all和any好像很简单。
但一旦允许通用布尔组合,下一步通常会出现:
(A AND B) OR (C AND NOT D)再下一步会出现:
- 嵌套条件的优先级;
- 缺失字段在
NOT中的含义; - 数组中“任意元素”和“全部元素”的区别;
- 条件求值错误时是拒绝、审批还是跳过;
- 新旧运行时忽略未知操作符时是否会降低安全性;
- 不同语言对数字、空值和集合比较的差异。
这时,规范已经站在通用策略语言的门口。
通用策略语言当然有价值,但它需要完整定义:
- 表达式语法;
- 类型系统;
- 数据来源;
- 求值顺序;
- 错误和失败语义;
- 沙箱与资源限制;
- 版本与兼容策略。
如果这些内容没有被完整定义,只增加几个看起来灵活的字段,反而会让实现者产生“规则已经被可靠执行”的错觉。
因此,ACC v1 选择有界的 ANY 条件,不是在否认复杂策略的存在,而是在拒绝用半套表达式语言冒充可移植标准。
需要复杂布尔策略的部署,可以在明确的运行时策略命名空间中使用 OPA、Cedar、自有规则引擎或其他成熟方案;但它们的结果不能被伪装成所有 ACC 运行时都必须理解的核心语义。
6. 为什么这些规则不应该写进条件审批
下面几类常见要求都很重要,但不属于通用条件审批声明。
6.1 黑名单客户直接拒绝
这是业务拒绝规则,不是审批意图。
黑名单数据的来源、更新频率、申诉机制和最终效力都由业务系统掌握。即使 Agent 运行时提前发现风险,业务系统仍必须在所有调用路径上执行同一规则。
6.2 退款金额不能超过当前可退款余额
这是依赖实时业务状态的跨字段不变量。
它应由退款操作或业务预检 API 验证,而不能依赖运行时持有一份可能过期的订单快照。
6.3 超过 5 万元由总监审批
“超过 5 万元需要外部决策”可以是可移植声明;“由哪位总监审批”则依赖组织目录、岗位代理、休假委托和企业流程。
前者可以属于能力契约,后者属于审批所有者。
6.4 审批 30 分钟后过期
有效期涉及时钟来源、等待队列、重试、跨时区和失效后的恢复方式。它应由审批协议或部署策略完整定义。
简单增加一个时间字符串,却让旧运行时忽略它继续执行,会造成安全意图降级。
6.5 只有工作日 9:00 到 18:00 才允许执行
时间窗口需要时区、节假日日历、可靠时钟和失败语义。它是有价值的运行时策略,却不因此自动成为通用核心字段。
关键不是这些需求“高级”或“不重要”,而是它们分别依赖不同的权威数据和执行责任。
7. 审批意图为什么不等于审批工作流
同一份审批声明,可以被不同类型的运行时一致理解,却以不同方式兑现:
| 运行时形态 | 可能的实现 |
|---|---|
| 带 UI 的 Agent 应用 | 展示结构化确认页并等待决定 |
| 无 UI 的 API Gateway | 返回待审批状态和后续查询入口 |
| 异步任务系统 | 创建审批任务并挂起执行 |
| 确定性工作流 | 暂停当前节点,收到外部决定后恢复 |
| 企业控制面 | 接入现有 OA、工单或业务审批系统 |
这些实现不必拥有相同界面、数据库和任务状态机。
它们需要一致的是:
在相同声明和相同调用参数下,是否必须先取得外部决策,再进入业务执行。
ACC 因此声明 approval intent,而不定义:
- 谁来审批;
- 审批人如何匹配;
- 是否需要多人会签;
- 审批 UI 长什么样;
- 任务如何持久化;
- 超时后怎样升级;
- 批准后由哪个队列恢复执行。
这些是运行时和企业流程真正应该拥有的自由度。
8. 审批通过,也只能批准被看见的那一次调用
条件判断解决的是“是否需要外部决策”,还没有解决“批准的究竟是什么”。
如果审批页面展示的是:
{"order_id":"SO-1001","amount":1500}审批通过后,Agent 重新推理成:
{"order_id":"SO-1001","amount":15000}原审批不能授权这组新参数。
一个可靠运行时通常需要:
- 在创建审批意图前校验并冻结能力标识和调用参数;
- 将审批决定绑定到被审阅的精确调用;
- 防止批准被另一组参数复用;
- 默认把批准作为一次性消费;
- 区分拒绝、过期、执行失败和业务系统拒绝;
- 恢复执行时再次核对调用快照。
这些属于实现安全属性和配套审批协议,不是approval.when本身能够证明的事情。
同样,一条由运行时自己写入的“已审批”日志,也不自动构成第三方可验证证据。高保证场景还需要定义规范化序列化、签名者、密钥发现、时效、重放保护和失败关闭行为。
通用契约应该明确自己的非保证,而不是用一个字段暗示整条证据链已经成立。
9. 一条完整的条件审批链路应该怎样工作
把各层重新组合,一次调用可以经历下面的顺序:
Agent 选择能力并提出参数 -> 按绑定协议的输入 Schema 校验参数 -> 解析可信行动主体 -> 评估能力范围与运行时策略 -> 评估 risk 与 approval 意图 -> 条件命中时冻结本次调用并创建审批意图 -> 由外部审批所有者作出决定 -> 核对决定绑定的能力、主体和参数 -> 恢复被批准的精确调用 -> 业务系统按实时权限和业务状态作最终授权 -> 记录结果、拒绝或失败在这条链路中:
- 模型可以提出工具和参数,但不能批准自己;
- 契约可以声明审批意图,但不能任命企业审批人;
- 运行时可以暂停和恢复,但不能替代业务最终授权;
- 业务系统可以拒绝已经审批的调用;
- 审批系统可以作出决定,但不能悄悄改变被批准的参数。
责任边界越清楚,系统越不需要依赖某一个“万能组件”。
10. 六个常见误区
误区一:把审批要求写进系统提示词就够了
提示词可以改善模型行为,不能成为执行层的信任边界。
误区二:条件未命中就代表调用已获授权
条件只决定是否产生审批意图,最终 Authority 仍属于业务系统。
误区三:字符串"1500"可以自动当成数字比较
治理条件必须使用严格、类型感知的比较。类型不兼容应在执行前被拒绝。
误区四:条件越复杂,契约越专业
没有完整语法、数据协议和失败语义的复杂表达式,只会增加跨实现歧义。
误区五:批准“退款”就等于批准之后任意退款参数
审批必须绑定被审阅的精确能力和调用参数。
误区六:审批通过后业务系统必须执行
订单状态、租户隔离、余额和当前主体权限仍由业务系统实时判断。
11. 一份最小设计检查表
在为 Agent 工具增加条件审批时,可以先回答下面的问题:
- 条件是否只读取当前调用中已经声明、已经校验的参数?
- 比较值是否是明确字面量,而不是来源不明的上下文变量?
- 每个操作符是否具有严格、跨语言一致的类型语义?
- 类型不兼容时是否在业务执行前拒绝,而不是按“未命中”放行?
- 无条件审批是否不会被条件规则取消?
- 模型输出是否无法修改风险、主体和审批治理元数据?
- 审批意图是否与审批人、UI 和工作流实现保持分离?
- 审批决定是否绑定精确能力、主体和参数快照?
- 审批通过后,业务系统是否仍执行最终授权?
- 需要复杂业务规则时,是否交给有权威数据的策略或业务层?
如果这些问题没有被回答,“加了审批”往往只意味着系统多了一个页面,还没有建立可靠的行动边界。
12. 结语:好的条件审批不是表达一切,而是只承诺能够一致兑现的部分
Agent 进入真实业务系统后,条件审批确实是不可回避的治理需求。
但标准化的价值不在于把企业所有规则搬进同一份 YAML,而在于找到一组不同运行时都能一致理解、验证和执行的最小公共语义。
对于条件审批,这个最小语义可以非常克制:
- 条件只读取已声明的调用参数;
- 只与明确字面量进行类型严格的比较;
- 任一独立条件命中就产生审批意图;
- 无条件审批不能被条件取消;
- 审批意图不冒充审批流程;
- 审批通过不冒充最终业务授权。
复杂策略、组织流程和动态业务状态都是真问题。但尊重这些问题所属的层次,比把它们全部塞进契约更专业。
一份行业契约最难的地方,从来不是还能增加多少字段。
而是它是否有足够的纪律,只声明那些自己真的能够跨系统兑现的承诺。
延伸阅读
- ACC 规范:
SPEC.md中的approval语义 - ACC 设计边界:
DESIGN_RATIONALE.md - ACC 实现者指南:审批意图、调用冻结与恢复参考流程
- 前序拆解:风险分级应该描述操作,还是直接规定审批流程?
- 前序拆解:Agent 代表谁行动,可信主体为什么不能来自模型输出?