21|可靠执行:重试、幂等、崩溃与状态恢复
本章目录
本章预备知识
请求重试解决网络不稳定,但外部动作可能已经发生。幂等键识别同一业务动作;事务保证一组修改的某些原子属性;两者都不能凭空覆盖所有外部系统。
本章各题列出所需基础。可先完成相应基础课,也可通过自检确认掌握程度。
进入深读前的小检查:列出“还没执行”“已执行且收到回执”“可能执行但没收到回执”三种状态,再决定各自可否重试。
术语带虚线时可点击速查。公式中的字母先看定义;不要用猜缩写替代推导。
LLM越能自主执行,越需要区分“建议再试一次”和“再次执行是否安全”。本章沿一次写操作的故障窗口展开,不把模型自我反省当事务机制。
沿着对象与状态理解本章
超时证明没收到响应,不证明远端没做事。下图将副作用与回执分开,解释unknown状态、幂等标识与对账。仅给本地数据库加事务,不能自动消除远端重复执行风险。
下方图解的固定输入在“输入与适用条件”中声明。它用于解释本章机制,不与本章其他独立例子的数值暗中接续。
超时告诉你没收到,不告诉你没发生
为什么自动重试有时会把一次工作做两遍?
外部服务可能已经完成动作,只是成功消息没回到调用方。此时“未知”是精确状态,不是系统不够聪明。对于会产生新记录、发送消息等副作用的动作,恢复需要稳定操作标识、结果核对或服务支持的幂等语义。只读查询的重试通常不需要同样复杂的设计。
输入与适用条件:构造的外部写入场景;不承诺跨数据库、文件系统和远程API天然具备“恰好一次”事务。
1 / 6 · 请求 op-1
窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。
创建一条记录;客户端等待回执
教学状态默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。
为什么需要
在不可靠网络下维持正确副作用次数。
怎样实现
先判断操作是否可重复,再决定重试、查状态、对账或人工确认。
可能的代价与误解
只在本地记一个成功标志,不能消除“远端已提交、本地未记录”的窗口。
如何判断是否做对
故意丢回执并重试,核对最终仅存在一条逻辑记录。
Q081|工具超时后为什么必须有unknown状态?不能直接标记failed吗?
核心回答
超时只表示调用方在期限内未得到确定结果,不能证明副作用没有发生。unknown表示执行结果尚无法确认;它阻止系统把“没看到成功”错误等同于“可以安全重做”。[IDEMPOTENT]
一次创建工单的五个时刻
T1:客户端记录准备创建,业务动作ID为a17。T2:请求发到服务端。T3:服务端把工单写入数据库。T4:成功响应在网络中丢失。T5:客户端超时。
此时客户端若标记failed并用新ID重试,服务端会创建第二张工单。模型看到两个结果后再删除一个也不一定等价:通知、审计或外部同步可能已经发生。正确恢复要使用相同幂等身份查询/重试,或按业务唯一条件核对现有结果;无法确定则保持unknown并要求适当人工处理。
相比之下,如果请求在本地参数验证阶段失败,明确没有执行副作用,通常可调整参数再试。故障位置决定恢复方式,因此错误结构应包含已知执行阶段,而不是只有一个字符串“error”。
请求超时,但计数器已经加1,应该叫什么状态
输入与条件。外部计数器初值10。操作op7请求加1,服务器执行到11,成功响应在网络中丢失。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1客户端观察 | 等待超时 | 只证明没有及时收到可用响应。 |
| 2外部事实 | 计数器已11 | 不能因此标记“没有执行”。 |
| 3安全状态 | 记录op7=unknown | 保留操作身份、请求内容与核查方式。 |
| 4查询恢复 | 按op7查外部操作记录 | 若查到成功11,转succeeded;若明确未提交,可按规则重试。 |
| 5危险重试 | 改用新ID op8再加1 | 计数器到12,重复副作用出现。 |
把过程连起来。成功、失败、处理中和结果未知要区分;超时是通信观察,不是执行事实。
边界与反例。外部系统未提供操作查询或幂等能力时,可能只能人工核查,不能凭内部日志保证恰好一次。
依据与适用范围:[DERIVE]。示例输入和场景由本书明确设定。
追问:所有只读查询都能无脑重试吗?
只读通常风险低,但仍有计费、速率限制和时效变化;某些名义查询会触发缓存刷新或后台任务。重试应有截止时间和退避,并尽可能保持同一逻辑查询的上下文。不要让基础设施失败无限消耗LLM轮次。
追问:模型说“我检查过没有创建”,能消除unknown吗?
只有它确实调用了可信查询,并且查询范围足以确认,才有证据。自然语言承诺不能替代远端状态核验。还要注意最终一致性:刚创建的对象可能暂时查不到,不能立即据此再次创建。
追问:unknown状态会不会让系统过于保守?
它只应用于真实不确定的副作用边界。低风险可重复计算无需复杂处理;不可逆或昂贵写操作值得谨慎。明确unknown比一律禁止重试更精确,也比盲目重试更安全。
Q082|幂等键怎样设计?为什么本地去重表不能保证跨系统Exactly Once?
核心回答
幂等键标识同一个业务意图,使重复请求返回同一次结果或拒绝冲突。关键在于去重记录与实际副作用是否在同一可保证一致性的边界内。单独本地写一行“执行过”不能消除远程副作用与记录之间的崩溃窗口。[IDEMPOTENT]
看两种错误顺序
先写本地done再调用远端:若写done后崩溃,远端根本没执行,恢复却跳过,造成丢操作。先调用远端再写done:若远端成功后崩溃,本地没有done,恢复重试,造成重复操作。无论顺序怎样,跨两个独立系统都存在间隙。
若远端原生支持幂等键,客户端用稳定key发请求,远端在其事务边界内保存key、请求语义和结果,可安全处理重复。相同key但不同参数应被识别为冲突,不能悄悄返回旧结果。key还需要作用域、保留时间和过期策略,避免不同用户或不同业务意图碰撞。
本地事务可原子地写业务数据和outbox事件,再由工作进程发送;发送本身仍可能重复,因此接收方常需去重或幂等处理。所谓exactly-once通常只在明确定义的处理边界、故障模型和保留策略内成立,不是整个互联网端到端的免费保证。
幂等键为什么还要绑定参数和执行结果
输入与条件。服务支持请求键k7,第一次参数为账户u7加10,结果余额110。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1首请求 | 事务内记录键、参数摘要和结果 | 成功后k7对应110。 |
| 2同键同参数重试 | 再次提交k7和加10 | 返回已有结果110,不再加到120。 |
| 3同键异参数 | k7却改成加20 | 必须拒绝冲突,不能把旧结果冒充新操作成功。 |
| 4键过期 | 去重记录清除后又提交k7 | 若没有更长业务约束,重复风险重新出现。 |
| 5外部边界 | 本地数据库记了键,但远端支付不认 | 本地去重不能覆盖跨系统失败窗口。 |
把过程连起来。幂等性是操作身份、参数一致性、结果持久化和保留策略共同构成的契约。
边界与反例。余额为教学状态,不执行真实金融操作;exactly-once效果需说明事务与外部系统边界。
依据与适用范围:[SQLITE]。示例输入和场景由本书明确设定。
追问:幂等键可以用所有参数的hash吗?
有时能作为内容身份,但相同参数可能代表两个合法独立意图,例如同金额的两次付款;重试时某些无关字段变化又可能导致不同hash。更稳妥的是应用生成逻辑操作ID,并把关键参数绑定到该ID。hash不是业务意图的替代品。
追问:模型每次生成一个新UUID可以吗?
如果这是同一操作重试,新UUID会绕过去重。动作身份应由Harness在首次确定意图时生成并持久保留,后续重试复用;模型可以建议动作,但不应随意重建幂等身份。
追问:补偿操作是否等于回滚?
不完全等价。退款、撤销发布、删除重复工单可能无法消除已发送通知或外部观察。补偿是新的业务动作,有自己的权限、失败与审计需求。不要把“出了问题再补偿”当作可以无约束重复执行的理由。
Q083|本地文件修改怎样用CAS和原子替换防止覆盖?它与数据库事务如何配合?
核心回答
CAS式更新要求当前版本仍等于预期版本,才应用变更;原子替换避免读者看到半个文件。它们分别解决并发覆盖与可见性问题,但不能自动让文件系统和另一个数据库成为一个原子事务。[SQLITE]
一个编辑器的安全写链
读取文件内容old与版本v;根据old生成new;写到同目录临时文件;必要时flush/fsync;在受控并发策略下再次确认当前版本仍为v;原子替换目标;记录新版本和动作结果。不同操作系统/文件系统的rename与持久化语义需查实现,不能把一个Python函数当作跨平台断电保证。
注意“先检查版本、再替换”之间仍可能有TOCTOU窗口。若多个写者同时工作,需要锁、受控单写者或真正支持条件写入的存储。单靠在应用里比较一次hash并不能阻止检查后他人修改。
文件已替换但任务数据库未记录成功时,恢复可比较目标内容是否等于该动作预期新内容,再补记结果;若文件又被他人修改,就不能简单回放。保持旧版本与预期新版本有助于区分已执行、未执行和冲突。无需把所有文件操作提升成复杂事务,但共享关键文件应有明确策略。
原子写文件与“检查版本后写入”为什么还差一步
输入与条件。进程A检查文件版本v7,准备写v8;在它检查之后、替换之前,进程B已把文件改成v9。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1无保护检查 | A看到v7符合预期 | 这个观察只在当时成立。 |
| 2并发插入 | B写入v9 | A的检查结果已过期。 |
| 3原子替换 | A用os.replace把新文件替过去 | 读者不会看到半份文件,却把B的v9覆盖了。 |
| 4正确协调 | 同一锁内完成校验与替换,或使用支持版本条件更新的存储 | 保证比较和更新之间不被其他写入穿插。 |
| 5持久化边界 | 崩溃后的落盘还可能需fsync等措施 | 可见原子性不等于断电持久性。 |
把过程连起来。原子替换解决半写,CAS解决丢失更新,事务解决多项一致性,持久化解决崩溃后是否还在。
边界与反例。文件系统、平台和锁作用域会影响保证;只在进程内加锁不能阻止其他不遵守锁的写入者。
追问:为什么测试通过后再格式化也要谨慎?
格式化或代码生成仍会改变被验证对象,可能引入语义变化、环境差异或不完整写入。简单安全的流程是把所有预期修改完成,再针对最终状态验证;若验证后又改动,至少重跑受影响检查。
追问:数据库事务能保护Shell命令吗?
数据库事务只保护其内部读写。Shell修改文件、调用网络或发通知不在事务内;数据库回滚不能撤销这些副作用。不要把事务包住一段外部命令就宣称端到端原子性。
追问:CAS冲突后能否让模型自动合并?
可以作为新任务步骤,但必须先读取最新内容,重新理解双方修改并验证。不能把冲突当暂时错误原样重试。自动合并结果也需要新的版本条件,防止再次覆盖。
Q084|长任务重启后怎么继续?Retry、Resume、Replay、Cancel分别是什么?
核心回答
Retry重新尝试某个操作;Resume从已知状态继续任务;Replay重放记录以重建或诊断;Cancel请求停止后续工作。它们不能互换,尤其不能把重放日志自动变成再次执行副作用。
恢复流程按事实走
启动时读取任务目标、当前Checkpoint和未决调用;核对实际产物是否存在、版本是否匹配;对unknown副作用进行查询;重建最小充分模型上下文;只从下一未完成步骤继续。已经完成的只读结果可按时效复用,已经完成的写操作不应因聊天历史缺失重做。
重放用于调试时,可以用录制工具结果替代真实执行,检查模型或Harness如何处理。若重放要真实访问外部系统,必须明确隔离环境和权限,避免“测试恢复能力”时再次发邮件或部署。
取消要停止新动作、向运行中工具传播取消信号,并等待或记录最终状态。模型生成停止、UI关闭、进程退出和远端任务停止是不同事件。用户看不到进度不意味着任务已回滚。
重试、恢复、重放和取消四个动作怎样区分
输入与条件。一个任务执行到事件20,工具c7已成功,但界面断线;另一个c8仍在外部运行。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1重试 | 再次发送某个请求 | 必须判断c7是否幂等,不能因界面断线直接再执行。 |
| 2恢复 | 从检查点和后续事件重建当前任务 | 目标是接着做,已成功动作不重复。 |
| 3重放 | 把事件重新作用到本地状态机 | 理想设计中只重建状态,不再次发外部写入。 |
| 4取消 | 停止未来调度并尽力取消c8 | 远端若已执行,仍需记录结果或核查。 |
| 5结束 | 已取消任务也可能存在已发生副作用 | 最终状态说明必须区分停止工作和撤销结果。 |
把过程连起来。同一个“再来一次”按钮若混合四种语义,就容易重复修改或丢失事实。
边界与反例。取消通常是协作式,不保证撤回已提交外部操作;补偿也不一定能还原所有副作用。
依据与适用范围:[DERIVE]。示例输入和场景由本书明确设定。
追问:指数退避为什么还需要随机抖动?
大量客户端在同一时刻失败后,若按相同时间表重试,会再次同步冲击服务。随机抖动分散重试时间。还应有最大尝试次数、总体deadline和错误分类,不对权限或参数错误反复退避。
追问:Checkpoint应该多频繁?
取决于重做成本和副作用风险。每个Token持久化通常不必要;关键阶段、昂贵计算、外部写操作前后更值得保存。频率过高增加I/O和复杂度,过低增加恢复损失。
追问:恢复成功如何验证?
检查没有重复副作用、没有跳过必要步骤、关键约束仍在、产物版本一致、最终验证真实运行。仅仅“程序能重新启动”不是任务恢复成功。