关键词:Agent 幂等重试、接口超时、退款重复执行、业务结果不确定、AI 调用业务系统
一个 Agent 调用退款接口:
POST /refunds
业务系统已经完成扣账、生成退款单,并提交到支付渠道。
但就在响应返回前,网络连接中断了。
Agent 运行时看到的只有:
timeout
这时候最自然的动作似乎是:
调用失败 -> 重试
可第一次请求也许根本没有失败。
它只是“响应没有被调用方看到”。
如果系统直接再发一次请求,可能产生:
- 两笔退款;
- 两条库存调整;
- 两次优惠券发放;
- 两个账号;
- 两次部署;
- 两份不可逆的外部指令。
这说明 Agent 执行真实业务动作时,必须区分三个完全不同的问题:
传输有没有成功?
业务后果有没有发生?
调用方有没有拿到足够证据?
三者不能被一个 success / failed 布尔值代替。
请求超时不等于业务失败。结果未知时,盲目重试不是可靠性策略,而是在拿真实业务后果做概率实验。
1. 为什么传统的“失败就重试”在 Agent 场景更危险
在只读查询中,重试通常问题不大:
GET /orders/SO-1001
多查一次,最多增加延迟和负载。
但 Agent 的目标是完成任务,它可能连续调用:
创建退款
修改库存
关闭账号
发送合同
触发部署
提交采购
这些操作会改变真实世界。
模型和运行时还可能把错误文本理解成新的行动提示:
第一次失败了,请再试一次
如果系统没有稳定的任务身份、业务幂等键和结果查询机制,Agent 越“积极完成目标”,越容易造成重复副作用。
所以,重试策略不能只围绕 HTTP 状态码设计,而必须围绕业务后果设计。
2. 先区分三种成功与失败
一次 Agent 业务调用至少存在三层状态:
| 层次 | 成功意味着什么 | 失败意味着什么 |
|---|---|---|
| 传输层 | 请求和响应完成交换 | 超时、断连、网关错误 |
| 业务层 | 业务系统产生或拒绝业务后果 | 业务校验失败、权限不足、事务回滚 |
| 证据层 | 调用方获得可验证的结果与审计记录 | 结果丢失、审计写入失败、回执不完整 |
下面这个状态完全可能发生:
传输层:失败
业务层:成功
证据层:失败
也就是:
退款已经发生
但 Agent 没收到响应
审计系统也暂时不可用
如果运行时把它压缩成:
{
"status": "failed"}
上层很可能自动重试,从而制造第二次业务效果。
3. 任务身份、调用尝试和业务幂等键不是一回事
为了正确处理重试,系统至少需要区分三种标识。
3.1 持久任务 ID
task_id = task-7f3...
它代表用户希望完成的一个业务目标:
为订单 SO-1001 申请退款 199 元
无论任务经历多少次暂停、恢复、重启或查询,这个身份都不应改变。
3.2 传输尝试 ID
attempt_id = attempt-03
它代表某一次向下游发出的网络请求。
同一任务可能有多个传输尝试,但这不意味着可以产生多个业务后果。
3.3 业务幂等键
idempotency_key = refund:SO-1001:199.00:duplicate-payment
它由业务系统识别,用来保证同一业务意图重复到达时,只兑现一次结果。
正确关系是:
一个持久任务
-> 可以有多个传输尝试
-> 但必须绑定同一个业务幂等身份
-> 最终只允许一个业务后果
如果每次重试都生成新的幂等键,幂等机制等于不存在。
4. execution.idempotent: true 不是运行时的魔法
能力声明可以表达:
execution:
idempotent: true
但这个字段不能凭空让接口变得幂等。
它只能表示:
该 operation 声称支持在相同业务幂等身份下安全重试,运行时可以据此采用对应的执行策略。
真正的幂等必须由业务系统兑现。
例如:
UNIQUE (tenant_id, idempotency_key)
或者:
收到相同幂等键
-> 返回第一次执行的结果
-> 不重复创建退款
运行时负责:
- 生成或传递稳定幂等键;
- 确保重试不改变业务身份;
- 保存任务和尝试之间的关系;
- 在结果未知时优先查询而不是盲目重放。
业务系统负责:
- 原子地记录幂等键和业务结果;
- 对重复请求返回同一业务结果;
- 保证并发请求不会产生多个副作用。
二者缺一不可。
5. 结果未知必须是一种显式状态
很多系统只有:
pending
success
failed
这对于真实业务执行不够。
至少还需要:
outcome_unknown
它表示:
请求已经发出
但当前无法证明业务效果发生或未发生
进入这个状态后,系统不应直接自动重试高风险写操作,而应:
- 使用幂等键查询业务系统;
- 查询业务对象当前状态;
- 检索下游回执或支付流水;
- 等待异步回调;
- 必要时进入人工对账。
只有确认第一次请求没有产生业务效果,或者业务系统能够保证相同幂等键不会重复执行时,才可以继续传输重试。
6. 一个更准确的任务状态机
可以把执行过程建模为:
approved
-> dispatching
-> acknowledged
-> committed
-> evidence_recorded
故障可能发生在任何两步之间。
因此,外部可见状态可以包括:
| 状态 | 含义 | 是否允许自动重试 |
|---|---|---|
not_dispatched |
尚未向业务系统发送 | 可以 |
dispatching |
已开始发送,结果尚不明确 | 取决于幂等保证 |
outcome_unknown |
无法确认业务后果 | 默认不允许 |
succeeded |
已确认成功并获得结果 | 不需要 |
failed_before_commit |
已确认提交前失败 | 可以按策略重试 |
committed_evidence_degraded |
业务已成功,但证据链不完整 | 不允许重复执行 |
reconciliation_required |
需要查询或人工对账 | 不允许盲目重试 |
这比“接口报错了”更接近真实世界。
7. 审计存储故障,不能抹掉已经发生的业务事实
考虑下面的顺序:
1. 业务系统完成退款
2. 返回成功
3. 运行时写审计日志
4. 审计存储不可用
如果系统因为第 4 步失败,向上层返回:
执行失败,请重试
就会产生严重误导。
真实状态不是“失败”,而是:
业务已提交
证据记录降级
需要补写或对账
因此应当显式表示:
{
"status": "committed_evidence_degraded",
"retryable": false,
"reconciliation_required": true
}
治理系统可以选择在执行前要求审计存储可用,以实现 fail closed。
但如果业务提交已经发生,就不能假装它没有发生。此时重点应从“是否执行”切换到“如何补全证据与恢复一致性”。
8. 不可变执行信封让重试可以被验证
为了证明多次尝试属于同一业务行动,可以构造一个不可变执行信封:
{
"task_id": "task-7f3...",
"trusted_subject": "user-1008",
"capability": "refund.request.create",
"canonical_arguments": {
"order_id": "SO-1001",
"amount": "199.00",
"currency": "CNY",
"reason": "duplicate-payment"
},
"policy_version": "refund-policy-2026-07-30",
"approval_evidence": "approval-...",
"business_idempotency_key": "refund:SO-1001:199.00:duplicate-payment",
"downstream_result_reference": null
}
对该信封进行规范化和哈希:
execution_hash = SHA256(canonical_execution_envelope)
后续每次传输尝试都引用同一个 task_id、business_idempotency_key 和 execution_hash。
这样,审计人员可以区分:
同一业务行动的传输重试
与:
参数或主体已经变化的第二次业务请求
但要注意:
不可变信封
!= 最终授权
!= 业务幂等实现
!= 审批永久有效
它只是让行动身份和证据关系可验证。
9. 九个关键故障点
一条受治理执行链路至少应测试以下故障:
- 审批持久化后、下游发送前崩溃;
- 下游已提交、响应返回前断连;
- 响应收到后、任务状态持久化前重启;
- 业务成功后审计存储不可用;
- 任务被接受后协调器重启;
- 审批暂停期间策略版本变化;
- 审批过期后任务恢复;
- 同一幂等键并发到达业务系统;
- 结果查询接口暂时不可用。
每个测试都不应只验证“最终状态看起来正确”,而应证明:
同一个持久任务
-> 最终收敛到明确状态
-> 不产生第二次业务效果
-> 能解释每次传输尝试
10. 一个退款故障示例
第一次调用:
task_id: task-001
idempotency_key: refund-SO-1001
attempt_id: attempt-001
业务系统完成退款,但响应超时。
运行时进入:
outcome_unknown
这时不应创建:
idempotency_key: refund-SO-1001-retry-2
而应使用原键查询:
GET /refunds/by-idempotency-key/refund-SO-1001
如果查询返回:
{
"status": "succeeded",
"refund_id": "RF-9001"
}
运行时应把原任务收敛到成功,而不是再次调用退款接口。
如果查询仍无法确认:
reconciliation_required
比“再试一次看看”更诚实,也更安全。
11. ACC 在这里负责什么,不负责什么
ACC 可以声明:
- operation 是否为只读;
- 是否声称具备幂等语义;
- 建议超时;
- 速率限制;
- 风险等级;
- 是否需要可信主体和审批。
这些语义帮助不同运行时选择保守执行策略。
但 ACC 不替代:
- 业务系统的幂等账本;
- 分布式事务;
- 下游状态查询接口;
- 任务队列持久化;
- 审批证据存储;
- 审计补偿流程;
- 最终业务授权。
因此,声明:
execution:
idempotent: true
不是安全证明,而是一项需要实现方兑现和测试的契约。
12. 八个常见误区
误区一:HTTP 超时就是业务失败
超时只说明调用方没按时获得响应。
误区二:重试时生成新 request ID 更安全
新的传输 ID 可以生成,但业务幂等身份必须保持稳定。
误区三:运行时去重就足够
并发、重启和旁路调用仍可能绕过运行时,最终幂等必须由业务系统兑现。
误区四:审计写失败就返回通用失败
如果业务已经提交,通用失败会诱发重复执行。
误区五:幂等只适用于支付
创建账号、发券、发货、部署、发消息、修改库存都可能需要业务幂等。
误区六:相同参数一定是同一行动
还要比较主体、租户、能力、目的和业务对象上下文。
误区七:有幂等键就不用查结果
幂等键防止重复副作用,结果查询帮助任务收敛到可解释终态。
误区八:模型可以根据错误信息决定是否重试
高风险重试必须由确定性运行时策略控制,不能交给模型临场猜测。
13. 一份实现检查表
- [ ] 是否区分持久任务 ID、传输尝试 ID 和业务幂等键?
- [ ] 同一业务意图重试时,幂等键是否保持不变?
- [ ] 业务系统是否原子地兑现幂等语义?
- [ ] 是否存在
outcome_unknown或等价状态? - [ ] 高风险写操作结果未知时,是否默认禁止盲目重试?
- [ ] 是否提供按幂等键或业务对象查询结果的路径?
- [ ] 业务已提交但审计失败时,是否返回非重试的降级状态?
- [ ] 运行时重启后,任务和审批证据是否仍可恢复?
- [ ] 是否能区分一次业务行动的多次传输尝试与第二次业务请求?
- [ ] 故障测试是否验证“没有第二次业务效果”?
- [ ] 业务系统是否仍然执行最终权限和状态校验?
14. 结语:可靠执行的目标不是“尽量成功”,而是“只产生一次正确后果”
Agent 很擅长持续尝试。
这在搜索信息、生成文本和调用只读工具时通常是一种优点。
但当它开始退款、改库存、创建账号或触发部署时,“再试一次”可能不是韧性,而是事故。
因此,真实业务执行需要一种更严格的可靠性目标:
同一个用户意图
-> 一个持久任务身份
-> 一组不可漂移的行动参数
-> 一个业务幂等身份
-> 最多一次业务后果
-> 一个可解释的终态
接口是否返回 200,只是这条链路中的一个瞬间。
真正重要的是:
当网络、运行时、审计系统和业务系统在任何位置发生故障时,我们仍然能说清楚业务后果是否发生,并确保不会因为一次不确定而制造第二次后果。
这才是 Agent 从“会调用工具”走向“可以承担真实业务执行”的基础。