位置:首页 > 进阶教程 > 退款接口成功但Agent未收到响应时是否可以安全重试

退款接口成功但Agent未收到响应时是否可以安全重试

时间:2026-08-18  |  作者:冻月看渠  |  阅读:0

一个 Agent 调用退款接口:

退款接口可能已经成功,但 Agent 没收到响应:这时候能不能重试?

POST /refunds

业务系统已经完成扣账、生成退款单,并提交到支付渠道。

但就在响应返回前,网络连接中断了。

Agent 运行时看到的只有:

timeout

这时候最自然的动作似乎是:

调用失败 -> 重试

可第一次请求也许根本没有失败。

它只是“响应没有被调用方看到”。

如果系统直接再发一次请求,可能产生:

  • 两笔退款;
  • 两条库存调整;
  • 两次优惠券发放;
  • 两个账号;
  • 两次部署;
  • 两份不可逆的外部指令。

这说明 Agent 执行真实业务动作时,必须区分三个完全不同的问题:

传输有没有成功?业务后果有没有发生?调用方有没有拿到足够证据?

三者不能被一个 success / failed 布尔值代替。

为什么“失败就重试”在 Agent 场景更危险

在只读查询中,重试通常问题不大:

GET /orders/SO-1001

多查一次,最多增加延迟和负载。

但 Agent 的目标是完成任务,它可能连续调用:

创建退款修改库存关闭账号发送合同触发部署提交采购

这些操作会改变真实世界。

模型和运行时还可能把错误文本理解成新的行动提示:

第一次失败了,请再试一次

如果系统缺乏稳定的任务身份、业务幂等键以及结果查询机制,Agent 在执行过程中越是“积极”地追求目标,就越容易引发重复的副作用。

所以,重试策略不能只围绕 HTTP 状态码设计,而必须围绕业务后果设计。

先区分三种成功与失败

一次 Agent 业务调用至少存在三层状态:

层次 成功意味着什么 失败意味着什么
传输层 请求和响应完成交换 超时、断连、网关错误
业务层 业务系统产生或拒绝业务后果 业务校验失败、权限不足、事务回滚
证据层 调用方获得可验证的结果与审计记录 结果丢失、审计写入失败、回执不完整

下面这个状态完全可能发生:

传输层:失败业务层:成功证据层:失败

也就是:

退款已经发生但 Agent 没收到响应审计系统也暂时不可用

如果运行时把它压缩成:

{ "status": "failed"}

上层很可能自动重试,从而制造第二次业务效果。

任务身份、调用尝试和业务幂等键不是一回事

为了正确处理重试,系统至少需要区分三种标识。

持久任务 ID

task_id = task-7f3...

它代表用户希望完成的一个业务目标:

为订单 SO-1001 申请退款 199 元

无论任务经历多少次暂停、恢复、重启或查询,这个身份都不应改变。

传输尝试 ID

attempt_id = attempt-03

它代表某一次向下游发出的网络请求。

同一任务可能有多个传输尝试,但这不意味着可以产生多个业务后果。

业务幂等键

idempotency_key = refund:SO-1001:199.00:duplicate-payment

它由业务系统识别,用来保证同一业务意图重复到达时,只兑现一次结果。

正确关系是:

一个持久任务-> 可以有多个传输尝试-> 但必须绑定同一个业务幂等身份-> 最终只允许一个业务后果

如果每次重试都生成新的幂等键,幂等机制等于不存在。

execution.idempotent: true 不是运行时的魔法

能力声明可以表达:

execution:idempotent: true

但这个字段不能凭空让接口变得幂等。

它只能表示:

真正的幂等必须由业务系统兑现。

例如:

UNIQUE (tenant_id, idempotency_key)

或者:

收到相同幂等键-> 返回第一次执行的结果-> 不重复创建退款

运行时负责:

  • 生成或传递稳定幂等键;
  • 确保重试不改变业务身份;
  • 保存任务和尝试之间的关系;
  • 在结果未知时优先查询而不是盲目重放。

业务系统负责:

  • 原子地记录幂等键和业务结果;
  • 对重复请求返回同一业务结果;
  • 保证并发请求不会产生多个副作用。

二者缺一不可。

结果未知必须是一种显式状态

很多系统只有:

pendingsuccessfailed

这对于真实业务执行不够。

至少还需要:

outcome_unknown

它表示:

请求已经发出但当前无法证明业务效果发生或未发生

进入这个状态后,系统不应直接自动重试高风险写操作,而应:

  • 使用幂等键查询业务系统;
  • 查询业务对象当前状态;
  • 检索下游回执或支付流水;
  • 等待异步回调;
  • 必要时进入人工对账。

只有确认第一次请求没有产生业务效果,或者业务系统能够保证相同幂等键不会重复执行时,才可以继续传输重试。

一个更准确的任务状态机

可以把执行过程建模为:

approved-> dispatching-> acknowledged-> committed-> evidence_recorded

故障可能发生在任何两步之间。

因此,外部可见状态可以包括:

状态 含义 是否允许自动重试
not_dispatched 尚未向业务系统发送 可以
dispatching 已开始发送,结果尚不明确 取决于幂等保证
outcome_unknown 无法确认业务后果 默认不允许
succeeded 已确认成功并获得结果 不需要
failed_before_commit 已确认提交前失败 可以按策略重试
committed_evidence_degraded 业务已成功,但证据链不完整 不允许重复执行
reconciliation_required 需要查询或人工对账 不允许盲目重试

这比“接口报错了”更接近真实世界。

审计存储故障,不能抹掉已经发生的业务事实

考虑下面的顺序:

1. 业务系统完成退款2. 返回成功3. 运行时写审计日志4. 审计存储不可用

如果系统因为第 4 步失败,向上层返回:

执行失败,请重试

就会产生严重误导。

真实状态不是“失败”,而是:

业务已提交证据记录降级需要补写或对账

因此应当显式表示:

{ "status": "committed_evidence_degraded","retryable": false,"reconciliation_required": true}

治理系统可以选择在执行前要求审计存储可用,以实现 fail closed。

但如果业务提交已经发生,就不能假装它没有发生。

此时重点应从“是否执行”切换到“如何补全证据与恢复一致性”。

不可变执行信封让重试可以被验证

为了证明多次尝试属于同一业务行动,可以构造一个不可变执行信封:

{ "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_idbusiness_idempotency_key 以及 execution_hash

这样,审计人员可以区分:

同一业务行动的传输重试

与:

参数或主体已经变化的第二次业务请求

但要注意:

不可变信封!= 最终授权!= 业务幂等实现!= 审批永久有效

它只是让行动身份和证据关系可验证。

九个关键故障点

一条受治理执行链路至少应测试以下故障:

  • 审批持久化后、下游发送前崩溃;
  • 下游已提交、响应返回前断连;
  • 响应收到后、任务状态持久化前重启;
  • 业务成功后审计存储不可用;
  • 任务被接受后协调器重启;
  • 审批暂停期间策略版本变化;
  • 审批过期后任务恢复;
  • 同一幂等键并发到达业务系统;
  • 结果查询接口暂时不可用。

每个测试都不应只验证“最终状态看起来正确”,而应证明:

同一个持久任务-> 最终收敛到明确状态-> 不产生第二次业务效果-> 能解释每次传输尝试

一个退款故障示例

第一次调用:

task_id: task-001idempotency_key: refund-SO-1001attempt_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

比“再试一次看看”更诚实,也更安全。

ACC 在这里负责什么,不负责什么

ACC 可以声明:

  • operation 是否为只读;
  • 是否声称具备幂等语义;
  • 建议超时;
  • 速率限制;
  • 风险等级;
  • 是否需要可信主体和审批。

这些语义帮助不同运行时选择保守执行策略。

但 ACC 不替代:

  • 业务系统的幂等账本;
  • 分布式事务;
  • 下游状态查询接口;
  • 任务队列持久化;
  • 审批证据存储;
  • 审计补偿流程;
  • 最终业务授权。

因此,声明:

execution:idempotent: true

不是安全证明,而是一项需要实现方兑现和测试的契约。

八个常见误区

误区一:HTTP 超时就是业务失败

超时只说明调用方没按时获得响应。

误区二:重试时生成新 request ID 更安全

新的传输 ID 可以生成,但业务幂等身份必须保持稳定。

误区三:运行时去重就足够

并发、重启和旁路调用仍可能绕过运行时,最终幂等必须由业务系统兑现。

误区四:审计写失败就返回通用失败

如果业务已经提交,通用失败会诱发重复执行。

误区五:幂等只适用于支付

创建账号、发券、发货、部署、发消息、修改库存都可能需要业务幂等。

误区六:相同参数一定是同一行动

还要比较主体、租户、能力、目的和业务对象上下文。

误区七:有幂等键就不用查结果

幂等键防止重复副作用,结果查询帮助任务收敛到可解释终态。

误区八:模型可以根据错误信息决定是否重试

高风险重试必须由确定性运行时策略控制,不能交给模型临场猜测。

一份实现检查表

  • [ ] 是否区分持久任务 ID、传输尝试 ID 和业务幂等键?
  • [ ] 同一业务意图重试时,幂等键是否保持不变?
  • [ ] 业务系统是否原子地兑现幂等语义?
  • [ ] 是否存在 outcome_unknown 或等价状态?
  • [ ] 高风险写操作结果未知时,是否默认禁止盲目重试?
  • [ ] 是否提供按幂等键或业务对象查询结果的路径?
  • [ ] 业务已提交但审计失败时,是否返回非重试的降级状态?
  • [ ] 运行时重启后,任务和审批证据是否仍可恢复?
  • [ ] 是否能区分一次业务行动的多次传输尝试与第二次业务请求?
  • [ ] 故障测试是否验证“没有第二次业务效果”?
  • [ ] 业务系统是否仍然执行最终权限和状态校验?

结语:可靠执行的目标不是“尽量成功”,而是“只产生一次正确后果”

Agent 很擅长持续尝试。

这在搜索信息、生成文本和调用只读工具时通常是一种优点。

但当它开始退款、改库存、创建账号或触发部署时,“再试一次”可能不是韧性,而是事故。

因此,真实业务执行需要一种更严格的可靠性目标:

同一个用户意图-> 一个持久任务身份-> 一组不可漂移的行动参数-> 一个业务幂等身份-> 最多一次业务后果-> 一个可解释的终态

接口是否返回 200,只是这条链路中的一个瞬间。

真正重要的是:

Agent 从“会调用工具”走向“可以承担真实业务执行”,前提不是更激进地重试,而是确保同一业务意图只产生一次正确后果。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多