只读、幂等、超时和限流为什么属于能力声明?
关键词:Agent 工具幂等、AI 工具调用重试、Agent 超时、Agent 限流、业务 API 执行约束
假设一套企业系统向 Agent 提供四项能力:
order.read 查询订单
refund.request.create 创建退款申请
notification.batch.send 批量发送通知
report.generate 生成经营报表
它们的参数都可以由 OpenAPI 或其他输入 Schema 描述,运行时也知道应该向哪个地址发送请求。
但只知道“怎么调用”,仍然不足以安全执行。
运行时还需要知道:
- 查询订单是否真的不改变业务状态?
- 创建退款申请失败后能不能用同一组参数重试?
- 报表生成等待多久应该停止?
- 批量通知在一分钟内最多允许调用多少次?
这些问题看起来像运行时配置,实际上首先描述的是 operation 本身的稳定执行属性。
如果每个 Agent 平台都靠 HTTP 方法、接口名称或私有经验重新猜一次,就会出现完全不同的行为:
运行时 A 看到 GET,默认无限重试
运行时 B 看到 POST,永不重试
运行时 C 等待模型接口的默认 30 秒
运行时 D 没有限流,短时间发起数百次批量通知
因此,能力声明除了说明参数结构和治理意图,还需要一组最小、可移植的执行提示。
执行提示不负责替运行时编排整个工作流,但它应该让不同运行时对“这项操作能否安全读取、重复、等待和限速”形成同一基本理解。
1. 输入 Schema 描述形状,执行语义描述行为
下面这段 Schema 可以告诉运行时退款接口需要哪些参数:
type: object
required:
- order_id
- amount
properties:
order_id:
type: string
amount:
type: number
它不能回答:
调用会不会改变业务状态?
网络断开后重发会不会创建两笔退款?
等待 10 秒后超时,服务端是否仍可能继续执行?
这个能力能承受多高调用频率?
参数结构与执行行为是两个维度。
前者保证调用格式正确,后者帮助运行时避免用错误方式调用一项本来合法的能力。
对于普通人工页面,这些知识可能被写死在前端代码、SDK 或开发者经验里。进入 Agent 场景后,调用者可能来自不同模型、平台、工作流和网关,隐含知识就会迅速失效。
这正是把最小执行语义放进能力声明的原因。
2. readonly 描述的是业务状态,不只是 HTTP 方法
ACC v1 可以这样声明:
execution:
readonly: true
它表示:
该 operation 不应改变业务状态。
这里强调的是业务效果,不是网络协议表面。
一个 GET 请求通常只读,但并不天然安全:
GET /export-and-mark-as-downloaded
GET /track-email-open
GET /legacy/trigger-sync
这些历史接口可能产生状态变化或外部副作用。
反过来,一些只读查询因为参数复杂,可能使用 POST:
POST /reports/query
POST /search/advanced
如果运行时只根据 HTTP 方法推断,就可能同时犯两类错误:
- 把会改变状态的
GET当作只读能力; - 把使用
POST的纯查询当作写操作。
显式 readonly 为不同绑定和运行时提供一项稳定信号。
但它仍然只是一项声明,不是最终保证。
- API 作者必须如实标注;
- 实现本身必须真的不改变业务状态;
- 读取敏感数据仍然需要主体和业务授权;
- “只读”不等于“无风险”;
- 大规模导出即使不修改状态,也可能具有高后果。
所以 readonly 回答的是执行效果,不替代 risk、subject 和最终 Authority。
3. idempotent 解决的是“同一动作能不能安全重复”
分布式系统里最危险的一句话之一是:
刚才好像超时了,再发一次试试。
客户端超时时,可能存在至少三种情况:
- 请求根本没有到达服务端;
- 请求到达但业务事务失败;
- 请求已经成功,只是响应没有返回客户端。
如果第三种情况下直接重试,可能产生:
- 两笔退款;
- 两次库存扣减;
- 两条通知;
- 两个重复工单;
- 两次外部部署。
ACC v1 的声明是:
execution:
idempotent: true
它表示:
使用相同参数重复调用该 operation,可以被安全地视为同一逻辑动作。
这项语义让运行时知道重试是否可能成立,但它不会凭空让接口获得幂等性。
真正的幂等通常需要业务实现提供:
- 稳定的幂等键;
- 唯一约束;
- 请求指纹;
- 重复结果回放;
- 明确的幂等窗口;
- 对并发请求的原子处理。
如果 API 没有这些保证,却把 idempotent: true 写进声明,运行时会基于错误信号采取危险行动。
因此:
幂等声明描述已经存在的能力属性,不是要求运行时替业务系统发明幂等。
同样,idempotent: true 也不等于“应该无限自动重试”。是否重试还需要考虑:
- 错误类型;
- 最大次数;
- 退避策略;
- 剩余任务期限;
- 服务端是否返回明确终态;
- 当前调用是否仍绑定原始主体和参数;
- 外部系统是否具有同样的幂等保证。
幂等是重试的必要信号之一,不是完整重试策略。
4. timeout_ms 是等待边界提示,不是取消保证
长时间没有结果时,Agent 运行时必须决定继续等待、转为异步、返回待处理,还是终止本次尝试。
ACC v1 支持:
execution:
timeout_ms: 10000
它表示 invocation 的超时提示,以毫秒为单位。
这项信息有几个现实价值:
- 避免运行时无限等待;
- 帮助工作流设置有界阻塞时间;
- 让用户界面区分同步操作和长任务;
- 为模型提供明确的“暂未完成”状态,而不是让它自行猜测;
- 让不同运行时不必为同一 operation 各自发明默认值。
但“客户端停止等待”和“服务端停止执行”不是一回事。
下面这条链路完全可能发生:
运行时等待 10 秒后超时
-> 客户端关闭连接
-> 业务服务继续处理
-> 第 15 秒完成退款
-> Agent 因为超时又发起一次调用
所以 timeout_ms 不能被解释为:
- 服务端已经取消;
- 业务事务已经回滚;
- 可以立即安全重试;
- 超时后一定没有产生业务后果;
- 运行时必须采用某一种 UI 或错误文本。
如果系统需要真正的取消语义,还必须定义取消令牌、服务端确认、可取消阶段、补偿动作和最终状态查询。这已经超出一个简单执行提示能够承担的范围。
5. rate_limit 描述调用节奏,不替代企业配额系统
一项能力单次调用可能完全合法,但短时间高频调用仍会产生风险。
例如:
批量发送通知:单次参数正确,连续调用会骚扰大量客户
报表导出:单次只读,高频执行会拖垮数据库
库存查询:单次低风险,循环调用会放大成本
外部供应商 API:超过频率会触发封禁或额外费用
ACC v1 可以声明:
execution:
rate_limit:
count: 30
window: 1m
它表达一项运行时限流提示:
在给定窗口内,调用次数不应超过声明上限。
这让兼容运行时能够在调用到达业务系统之前建立基本节奏控制。
但一条通用声明无法替整个企业回答所有配额问题:
- 是按 route、Agent、用户、主体还是租户计数?
- 多个运行时如何共享计数?
- 窗口是固定、滑动还是令牌桶?
- 超限后拒绝、排队还是降级?
- 不同客户套餐是否有不同额度?
- 内部重试是否计入配额?
- 审批等待后的恢复是否重新计数?
这些问题依赖部署策略、计费体系和组织边界。
因此,通用契约适合提供可移植的调用频率提示;部署方负责决定计数键、算法、共享状态和失败行为。
6. 四项执行提示彼此正交,不能相互推导
一项能力可以同时具有不同组合:
| 能力 | readonly | idempotent | timeout | rate limit |
|---|---|---|---|---|
| 查询单个订单 | true | true | 短 | 较宽松 |
| 生成复杂报表 | true | true | 长 | 较严格 |
| 创建退款申请 | false | 可能为 true | 中 | 严格 |
| 批量发送通知 | false | 取决于业务键 | 长 | 很严格 |
| 触发一次性部署 | false | 通常为 false | 长 | 严格 |
从 readonly: true 不能自动推出:
- 数据不敏感;
- 风险一定是 low;
- 可以无限并发;
- 可以无限重试;
- 不需要可信主体。
从 idempotent: true 也不能自动推出:
- 操作是只读;
- 没有业务副作用;
- 任何错误都值得重试;
- 相同参数在任何时间都代表同一业务意图。
执行提示的价值就在于它们分别描述不同事实,而不是被压缩成一个模糊的“safe”字段。
7. 为什么这些信息不能只留在运行时私有配置里
团队当然可以在某个平台里配置:
refund.create timeout=10s
refund.create retry=false
refund.create limit=10/min
问题是,同一业务能力可能同时被:
- Dify;
- n8n;
- MCP Client;
- 自研 Agent;
- API Gateway;
- 本地执行器;
- 云端工作流
调用。
如果执行语义只存在某一个运行时里,其他调用方仍然需要重新猜测和配置。随着平台增加,配置会分叉:
平台 A 认为可重试
平台 B 认为不可重试
平台 C 等待 30 秒
平台 D 没有限流
把 operation 的稳定属性放在能力声明里,可以建立一份共同事实源。
部署方仍可采用更保守的本地策略:
声明上限 30/min,当前组织限制为 10/min
声明超时 10s,当前路由只允许等待 5s
声明可幂等重试,当前高风险场景仍选择不自动重试
但本地策略不应该悄悄放宽声明中的安全边界。
8. 为什么 ACC 不继续定义重试、并发和事务
看到 idempotent 和 timeout_ms 后,一个自然问题是:
为什么不顺便把重试次数、退避算法、并发数、回滚和事务都写进 ACC?
因为这些概念已经从稳定 operation 属性进入具体执行策略和工作流语义。
重试策略
需要区分网络错误、业务拒绝、超时、限流和未知终态,还要定义退避、抖动、预算和截止时间。
并发控制
需要确定计数维度、共享状态、一致性模型和租户边界。
事务与回滚
跨多个业务 API 的原子性不能由一个 atomic: true 字段创造。它需要事务协调、Saga、补偿授权、状态恢复和失败语义。
取消
需要服务端协议确认,而不是客户端停止等待。
这些都是真实需求,但它们属于运行时、工作流、业务系统或独立协议。把它们没有边界地塞进能力声明,会让一个薄契约逐渐变成无法跨实现兑现的编排语言。
9. 声明、运行时与业务系统怎样分工
一条可靠链路可以这样理解:
能力声明:
告诉调用方 operation 是否只读、是否幂等、建议等待多久、调用频率上限
运行时:
结合本地策略决定等待、限流、是否尝试重试以及怎样报告状态
业务系统:
真正保证状态变化、幂等唯一性、最终授权和业务结果
更完整地说:
| 层次 | 责任 |
|---|---|
| ACC Core | 定义可移植执行提示及其稳定含义 |
| 协议 Binding | 映射承载协议的原生信号、优先级和保守回退 |
| Agent 运行时 | 执行超时与限流,结合错误和本地政策作保守决策 |
| 工作流系统 | 管理重试预算、异步等待、并发、编排和补偿 |
| 业务系统 | 保证真实只读效果、幂等实现、最终权限和状态一致性 |
这仍然遵循 Reach 与 Authority 的分离。
执行提示告诉运行时“怎样更安全地尝试调用”,并不授予主体操作业务对象的最终权限。
10. 一段完整声明应该怎样理解
例如:
x-agent-capability:
version: 1
enabled: true
scope: order.read
risk:
level: low
subject:
required: true
audit:
sensitive: true
execution:
readonly: true
idempotent: true
timeout_ms: 5000
rate_limit:
count: 60
window: 1m
它表达的是:
- 该 operation 显式允许进入 Agent-facing 候选范围;
- 当前场景可通过稳定 scope 引用它;
- 最坏合理后果被声明为 low;
- 每次调用都需要可信行动主体;
- 参数或响应可能含敏感数据,需要保守日志处理;
- 操作不应改变业务状态;
- 相同参数可以安全重复;
- 运行时建议在 5 秒处建立等待边界;
- 运行时应尊重每分钟 60 次的调用频率提示。
它没有表达:
- 当前用户一定可以读取这个订单;
- 所有 low 风险能力都不需要治理;
- 运行时可以无限重试;
- 5 秒后服务端一定取消;
- 任何租户都拥有相同配额;
- 审计和限流已经由某个产品自动完成。
契约的专业性,不在于字段看起来多,而在于每项声明都清楚说明自己保证什么、不保证什么。
11. 八个常见误区
误区一:GET 一定只读
历史接口和不规范实现可能在 GET 中产生副作用,显式声明与实现审查仍然必要。
误区二:POST 一定不可幂等
带稳定业务键、唯一约束和结果回放的 POST 可以具备幂等语义。
误区三:幂等就应该自动重试
重试仍要看错误类型、次数、截止时间和未知终态。
误区四:客户端超时代表业务没有执行
停止等待不等于服务端取消,更不等于事务回滚。
误区五:限流只为保护服务器性能
限流也控制外部通知、资金动作、第三方成本和业务影响范围。
误区六:只读操作天然低风险
大规模导出、敏感查询和跨租户读取即使不改状态,也可能具有严重后果。
误区七:声明了幂等,运行时就能替业务系统保证幂等
幂等必须由真正持有业务状态的一侧实现。
误区八:执行提示等于最终授权
调用方式正确,不代表当前主体有权操作当前资源。
12. 一份最小执行语义检查表
在把 operation 暴露给 Agent 前,可以先回答:
-
readonly是否按真实业务效果标注,而不是只看 HTTP 方法? - 只读能力是否仍按数据敏感性和影响范围评估风险?
-
idempotent: true是否有服务端唯一约束或等价机制支撑? - 相同参数在幂等窗口内是否真的代表同一逻辑动作?
- 超时后是否可以查询最终状态,而不是立即盲目重试?
- 客户端停止等待与服务端取消是否被明确区分?
- 限流的本地计数维度和失败行为是否已经定义?
- 部署策略是否只会收紧,而不会悄悄放宽声明边界?
- 重试、并发、补偿和事务是否留在正确的运行时或工作流层?
- 业务系统是否仍执行最终主体权限与业务状态校验?
- 日志和审计能否区分首次调用、重试、重复消费和幂等命中?
如果这些事实只能藏在某一个 SDK 或某位开发者脑中,多 Agent、多运行时接入后迟早会产生不一致。
13. 结语:执行语义是能力的一部分,但不是整个执行系统
Agent 调用业务能力时,风险不只来自“选错工具”或“参数写错”。
即使工具和参数完全正确,错误的执行方式仍然可能造成真实后果:
- 把有副作用的操作当成查询;
- 把未知终态当成失败并重复执行;
- 无限等待一个已经失联的任务;
- 在短时间内放大一项原本有限的业务动作。
因此,只读、幂等、超时和限流不是某个产品的界面偏好,而是不同运行时理解一项能力时需要共享的最小执行语义。
它们应该进入能力声明,因为它们描述 operation 的稳定属性。
它们又必须保持克制,因为完整的重试、取消、并发、事务和补偿需要更丰富的运行时与业务协议。
好的契约不会试图执行一切。
它只会在不同系统即将采取行动之前,把那些不能继续依赖猜测的事实明确说出来。
对 Agent 系统而言,这已经足以避免大量昂贵而且完全可以预防的错误。
更多推荐


所有评论(0)