关键词: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 回答的是执行效果,不替代 risksubject 和最终 Authority。

3. idempotent 解决的是“同一动作能不能安全重复”

分布式系统里最危险的一句话之一是:

刚才好像超时了,再发一次试试。

客户端超时时,可能存在至少三种情况:

  1. 请求根本没有到达服务端;
  2. 请求到达但业务事务失败;
  3. 请求已经成功,只是响应没有返回客户端。

如果第三种情况下直接重试,可能产生:

  • 两笔退款;
  • 两次库存扣减;
  • 两条通知;
  • 两个重复工单;
  • 两次外部部署。

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 不继续定义重试、并发和事务

看到 idempotenttimeout_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 系统而言,这已经足以避免大量昂贵而且完全可以预防的错误。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐