包裹已经到了,寄件人却没收到回执。此时再寄一遍,不是“更加可靠”,而是可能送出第二份。
Agent调用写入工具时,也会遇到同样的缝隙:服务端已经创建了报告、工单或订单,响应却丢在网络里。模型只看见一条超时,便决定“再试一次”。如果运行系统没有保护,认真重试反而把事情做重了。
本文只回答一个问题:Agent写入后超时,怎样恢复,才不会把同一件事做两遍? 重点不是给模型增加一句“谨慎重试”,而是区分检查点、业务收据与幂等契约分别能证明什么。
AI原创概念插画。木刻与水粉质感的递送站、凭牌和断纸是艺术隐喻,不是真实物流现场、产品界面或网络拓扑。
一、先别把“超时”翻译成“没有执行”
假设资料助手已经获得授权,要在内部系统建立一份周报。它发送创建请求后,可能经历三种不同情况:
- 请求根本没有到达,远端没有创建。
- 远端明确拒绝,返回了可解释的失败,例如无权写入。
- 远端完成创建,但成功响应没有回到调用方。
只凭调用方的一次超时,通常不能区分第一种和第三种。第三种并不是“任务失败”,而是调用方不知道任务是否成功。工程上应单独保留“结果未知”,不要立刻改写成“待重试”。
AWS Builders’ Library用启动单实例工作负载的例子解释这类困境:收不到响应时,直接重试可能多启动一个实例;查回实际资源则需要对账,而且还得辨认资源究竟属于哪次请求。[1] 这不是AI专属问题,Agent只是多了一位会自行决定下一步的调用者。
因此,运行层应该把工具结果说清楚:是明确拒绝,还是请求已发出但结果未知?有无业务对象ID、调用意图ID及可用查询入口?模型可以协助选择下一步,却不能从一条网络错误里推断远端事实。
二、检查点记住进度,不代表它拿到了收据
检查点可以理解为工作流存下来的进度快照。它能帮系统在中断后恢复已保存的任务状态,而不必完全依赖聊天记忆。
但请把时间线拉开:远端提交写入;工具收到结果;工作流把结果保存到检查点。这几步不一定发生在同一个事务里。远端提交之后、本地保存结果之前,仍然存在一段空隙。 在这里中断,本地可能只记得“正要创建”,远端却已经创建好了。
LangGraph当前Functional API文档明确区分了两种情况:恢复时,已保存的task结果会从检查点取回;已经开始却没有完成的task,仍可能在恢复时再次运行。因此文档不仅要求把副作用封装成task,还要求为可能重执行的写入设计幂等,或核实已有结果。[2]
这也解释了为什么“我已经加了检查点”并不是完整答案。检查点解决的是工作流已知结果如何延续;幂等和对账解决的是外部动作可能重复、或者结果尚不确定时怎么办。两者互补,不能互相替代。
Anthropic的长任务Harness文章强调进度文件、Git历史与可接手的产物,帮助新会话理解工作状态。[3] 那是跨会话连续性的具体做法,不是外部API恰好只写一次的保证。把这些材料放在一起看,才不容易把“记住了工作”误读成“控制了全部副作用”。
三、幂等键不是每次尝试的新编号
在这里,幂等的实用含义是:对同一项操作重复提交,不产生额外的业务副作用。 响应的时间戳等字段可以变化,关键是不要多创建一份资源。
支持幂等的服务一般需要调用方提供一个稳定标识。第一次发送和后续恢复使用同一个标识,服务端据此识别“这还是那件事”,并返回原操作的结果。具体作用域、有效期、参数校验和并发行为必须以接口契约为准,不是随便加一个请求头就自动生效。[1]
设计时尤其要分开两个编号:
- 操作意图ID:用户授权的这一件事。例如“为本次资料任务创建这一份周报”。跨重试保持不变。
- 尝试ID:为了完成这件事,发出的某一次调用。每次可以不同,用于日志排查。
如果每次重试都生成新的幂等键,服务端看见的就是一件件新事,去重自然失效。反过来,用户明确要求再创建一份相同周报,这是新的业务意图,即使内容相同,也应使用新键。
相同参数不必然等于相同意图。 AWS原文用两个内容相同的EC2创建请求说明:用户可能确实需要两台相同实例,所以只对参数求摘要,不一定能推断重复。[1] 参数摘要适合检查同一键下的内容有没有被悄悄改掉,但不应该单独替用户决定“你只想要一份”。
同一个键配上不同内容怎么办?一个安全的策略是拒绝参数不匹配,要求调用方明确这是更新、纠错还是新操作。不要默默返回旧结果,也不要沿用旧确认去执行新内容。
四、做一个小实验:把成功响应故意丢掉
下面不是厂商性能评测,也没有调用真实周报API或额外模型。本次写作在本地实际运行了一个Python标准库与SQLite教学脚本:数据库扮演接收方,两次函数调用扮演工具请求;第一次先提交记录,再主动抛出TimeoutError,模拟“已提交,但调用方没有拿到成功结果”。
数据库保存在临时目录,每次请求重新建立连接;实验没有模拟真实网络、进程崩溃或磁盘断电。它验证的是一段明确的事务与恢复逻辑,不是某框架的端到端可靠率。
你可以按下面的步骤跟随这个案例:
第一步,做无保护的版本。 每次收到创建请求,都向naive表插入一条“weekly report”。第一次提交后丢弃响应,调用方再发送相同内容。数据库里留下两条记录。
第二步,做带稳定键的版本。 safe表有唯一的request_key字段。服务端在同一个写事务内先查询这个键:已存在就核对内容并返回原ID;不存在才插入。第一次仍然在提交后丢弃响应,第二次仍用intent-report-001。这次只留下一条,返回ID仍为1。

留下你的想法 0
欢迎提问、补充和不同观点。评论通过审核后会公开显示。