第十五章:失败与中断之后,怎样可靠继续
简报写完了,发送工具等待很久后返回超时。你准备再发一次,却发现手机已经收到第一条。第一次失败的是“收到发送结果”,不一定是“发送动作没有发生”。
再换一个情境:网关重启后,历史里最后一条是“开始投递”,没有后续记录。系统如果自动从这条输入重跑,可能再次生成并发送相同内容;如果一律跳过,又可能遗漏尚未开始的任务。
本章的产物是一张故障恢复表。重点是依据证据选择继续方式,而不是把所有异常都交给“多试几次”。
15.1 先定位哪一层失败
第五章讲了行动循环,模型可以利用工具错误选择后续动作;第十章又说明,连模型响应都未取得时,运行程序先处理请求失败。这两类情形不该混在一起。
| 发生在哪里 | 当前能知道什么 | 接下来先判断什么 |
|---|---|---|
| 模型服务连接或响应失败 | 没有取得本轮可用输出 | 错误是否临时、能否有限重试或切换 |
| 工具明确拒绝,执行未开始 | 相应动作尚未发生 | 参数、授权或前置条件能否修正 |
| 只读工具取材失败 | 资料未取得或只取得一部分 | 换入口、缩小范围、保留缺口 |
| 写入或发送后结果丢失 | 外部变化可能已发生 | 先核对状态,避免盲目重复 |
| 进程退出或运行中断 | 内存进度可能丢失 | 用持久记录确定已开始、已完成与未知 |
错误类型相同,也可能有不同副作用。网页读取超时与发送超时,都叫“超时”,恢复责任却不同。
15.2 重试预算与任务预算不是一回事
短暂断网、临时限流等情况,程序可以按具体策略等待后有限重试。认证无效或参数不被接受,则需要修复条件。重复同一个无效请求,不会因为重试次数更多就变成有效。
重试本身消耗时间和资源。若一次模型轮次内部重试三次,不能把它误记为三个根据新反馈作决定的行动轮次。工具内部重试、运行程序的模型请求重试、模型选择再调用一次工具,也要分开统计。
Hermes 的循环按错误类型、响应情况和相关恢复策略处理,可能进行重试、上下文处理或模型回退;这些都有具体条件。任务最终因预算、停止或不可恢复错误结束时,也应报告已取得的结果与缺口,而不是以一段流畅文字掩盖未完成。
一个合理的重试理由应能说清:这次条件为何可能与上次不同,最多还尝试多少,失败后如何交付。不能只说“也许下一次就好了”。
15.3 为什么需要“结果未知”
发送包含至少三件事:本地提出请求,外部系统执行动作,本地记录结果。任何两个环节之间都可能断开。外部已执行、本地未记账的窗口,是重复副作用的典型来源。
“未知”不是拒绝负责,而是准确说明当前证据。它允许系统暂停自动推进,再由有权限的人或可靠查询确认结果。若没有可靠查询,恢复决策就要在可能遗漏与可能重复之间明确取舍。
Hermes 的规范输入恢复也保留这种区分:从未开始的排队输入与拥有者退出时已经开始的输入,不采用一律自动重放的策略。已经开始而无法证明结果的情况可以进入未知状态,暂停相应队列,等待显式处理。
这避免将一次失联直接推成重复执行,但不会神奇地撤销已发生的动作。停止一个运行,也不等于回滚它此前发送的消息或写入的文件。
15.4 实际观察:未领取与已领取的等待超时
我们实际载入 Hermes v0.22.5 的原始投递队列和 SQLite 相关模块,用临时数据库与固定驱动执行下面步骤。这里没有模型调用、没有真实网关或消息平台,也没有外部发送。
| 实验动作 | 实际观察 | 可以解释什么 |
|---|---|---|
| 同一执行编号重复入队 | 仍为待发送,保留首次内容 | 编号约束防止同一请求重复插入或被重写 |
| 尚未领取时,把等待预算设为零 | 仍保留在待发送队列 | 没有人开始发送,与发送失败不同 |
| 子进程领取后正常退出,不写完成结果 | 状态停在投递中 | 领取记录与完成记录是两次转换 |
| 再用零等待预算检查已领取项 | 转成结果未知 | 队列不能从没有回执推出未执行 |
| 用相同编号再次入队 | 仍为未知,不能再次领取 | 同一项没有被自动变回可重发 |
| 新编号交给本地成功回调 | 记录完成并清理正文负载 | 对应执行完成自己的生命周期 |
| 新编号交给本地拒绝回调 | 记录失败及原因 | 明确失败与未知分别保留 |
零等待是故障注入,用来稳定进入相关分支,不是 Hermes 的默认等待配置。子进程在本实验中确实没有发送,但队列并不知道这个外部事实,因此仍按已领取而无回执处理。
这次没有验证死进程探测、完整调度账本回写或真实平台恢复。最后的“完成”只代表自编本地回调按约定返回成功,不能证明任何收件人收到消息。原始记录与复现入口保存在案例页。
15.5 同一编号为什么重要,又为什么不够
用一个稳定编号标记同一次操作,重复请求就有机会识别“这是先前那件事”。这种重复调用不改变既有结果的设计,常称为幂等。它需要落到存储和动作处理,而不是仅在提示词里写“别重复”。
但范围很重要。队列识别同一执行编号,不表示平台一定能识别两次发送是同一业务动作;如果上层把重试每次都改成新编号,还可能绕开原来的去重范围。
也不能把队列“最多领取一次”的选择当成绝不会遗漏。若领取后退出但其实从未发送,保守停止自动重发就可能需要人工补偿。这正是防重复与保交付之间的设计取舍。
对于草稿写入,可以采用可定位版本与结果回读;对于外部发送,要结合渠道回执或可查询标识。不同工具不能只套用同一句“支持重试”。
15.6 恢复以前,把记录与外部现实对齐
第七章已经说明保存记录与动作执行不是同一事务。恢复时可以按下面次序形成判断:先找对应任务、会话和执行编号;读最后确认的阶段;核对产物与外部状态;再决定继续、补偿、重新执行或报告无法确认。
例如草稿完整、发送尚未领取,就无需重新取材;发送已明确拒绝,可以在授权仍有效、内容仍适用时安排新的交付;发送结果未知,则先核对接收端,不能只因为系统重启就自动换编号再发。
并发时还有写入归属问题。旧运行恢复得慢,新运行可能已经接手;旧结果若无条件覆盖最新状态,就会把真实进度倒退。Hermes 的执行和投递路径使用相应执行身份限制回写。理解这个设计的关键是:结果必须证明属于当前有权完成记账的那次运行。
持久化、事务、归属与去重共同减少不一致窗口,各自也有范围。它们需要配合,不能由其中一个概念概括成“永不丢失、永不重复”。
15.7 失败后的交付也应可检查
把“失败了,请重试”换成有用的结果:本周材料已核对两件,草稿第 2 版已保存;投递进入未知状态,未自动重发;需要核对指定目标是否收到。这样用户知道哪些成果可保留,也知道下一步该解决哪个未知。
这是案例的恢复记录模板,不是本项目已经发送过真实简报。下一章会把它与正常结果一起纳入验收,检验系统是否既会完成任务,也会准确说明没完成的部分。
本章要点
- 先区分失败环节与副作用,再决定重试;请求超时不等于动作未发生。
- 重试、行动轮次与任务总预算分别计算,恢复需要明确停止条件。
- 结果未知是一种必要状态;停止和重启都不会自动回滚外部动作。
- 编号、持久记录与回写归属各有范围,防重复通常还伴随遗漏与补偿的取舍。
理解题
1. 等待超时但任务还在待发送队列,应当立刻新建另一条发送吗?
不应仅凭超时这样做。队列可能只是尚无可用发送方,原项仍会在之后被领取。新增另一项会扩大重复风险,先确认原项状态和交付策略。
2. 同一编号不能重复领取,是否证明用户一定只收到一次且一定收到?
都不能直接证明。约束有自己的范围;领取后未发送也可能停止自动重发,外部渠道还可能有独立行为。需要结合动作与回执,而非把队列约束扩大成端到端保证。
3. 停止按钮已使子任务退出,为什么还需要检查草稿和接收端?
停止控制后续执行,不能撤销已完成的写入或投递。应核对停止前的结果和未知状态,再决定保留、补偿或重新执行。
下一章把正常、失败和未知状态放到同一份验收方案中。