28 小时任务中断 3 次,最后只重做 5 个单元
独立研究顾问周遥用 AI 智能体准备一套市场进入资料包:48 份批准来源、36 个工作单元、160 条候选证据,最终生成报告、证据表、决策简报、附录和变更说明 5 个文件。任务持续 28 小时,期间发生 3 次中断:第 7 单元写完文件、尚未提交检查点时进程退出;第 21 单元调用模型前额度耗尽;第 30 单元完成后,客户又把受众从产品团队改成投资委员会。
这 3 次中断的难点并不相同。第一次要判断写出来但未提交的文件能不能用;第二次要保住前 20 个单元,不让追加额度变成从头再跑;第三次则要分清哪些事实仍然有效,哪些排序和表达必须随受众改变。如果恢复只靠“继续上次对话”,系统无法可靠回答任何一个问题:它不知道磁盘上的文件是否完整,也不知道外部查询是否已经发生。
周遥因此把任务改成 36 个有输入、输出、依赖和验收条件的工作单元。每次成功提交都同时写入版本化检查点与产物清单。恢复器不采信模型自述,而是读取已提交状态,核对产物哈希值,比较输入和计划版本,再计算下一项动作。
| 事件 | 观察到的状态 | 恢复决定 | 结果 |
|---|---|---|---|
| U07 产物已写、检查点未提交 | 无已提交完成证据 | 隔离孤儿,重跑 U07 | 重做 1 单元 |
| U21 额度耗尽、未产出产物 | U21 转为已阻塞并释放租约 | 人工追加预算后以新尝试重启 | 不重做 U01–20 |
| U30 后受众变化 | 计划 v4→v5 | 使 U27–30 失效并重做 | 重做 4 单元 |
| 最终交付 | 36 当前成功 | 人工批准后导出 | 重复产物0、外部动作0 |
用于统计产物版本基线的共有 41 次检查点提交:36 个当前单元完成、4 个后来失效的旧版本完成,以及 1 个计划变更提交。运行中、已阻塞、租约等运行状态另有事件记录,不混进这 41 次;另有 1 个未被检查点引用的孤儿。数字多不代表可靠,关键是每个状态都能解释为什么可以复用、为什么必须重做、为什么此时应该停止。
本文是虚构教学案例,不是某个智能体框架的配置教程。不同运行时对检查点、重放、中断、待处理写入和持久化存储的语义不同;涉及支付、客户发送、生产写入或受监管资料时,还要按具体系统做幂等、授权、审计、备份和事件响应设计。
检查点不是聊天摘要、自动保存或“我记得做到哪里”的一句话
聊天摘要保存语义线索,检查点保存可执行事实。摘要可以说“已分析大部分访谈”,却没有运行编号、具体完成单元、输入版本、输出位置和哈希值、验证结果及待执行动作。普通自动保存可能只是把某一刻的内存写下来,也不保证产物与状态同时提交。真正的恢复要回答“能否从这里安全继续”,而不只是“读起来像上次内容”。
| 载体 | 能回答 | 不能单独证明 |
|---|---|---|
| 对话历史 | 人与智能体说过什么 | 文件已落盘、工具副作用状态 |
| 滚动摘要 | 当前目标与重要决定 | 精确依赖、版本、完整错误 |
| 普通自动保存 | 某时刻内存/草稿 | 保存边界是否一致、是否可重放 |
| 审计日志 | 发生过哪些事件 | 当前有效状态与下一步行动 |
| 检查点 + 清单 | 已提交状态、产物、依赖、恢复入口 | 产物内容一定正确 |
检查点仍不是正确性证明。错误结论也能被完整持久化;所以提交前必须有结构、来源、主张或产物验收,提交后仍保留人工检查。持久性解决“进度不会无故消失”,质量解决“这个进度是否值得保留”。
先拆 36 个工作单元,再画清依赖
周遥把任务拆成六个阶段,而不是让智能体持有一个“完成全部资料包”的长指令。每个单元最长 45 分钟,有确定输入集、输出产物、验收器和依赖。不能独立命名、验收或重跑的步骤继续拆;只有几秒钟、又没有可复用结果的操作,才留在单元内部。
| 阶段 | 单元数 | 代表输入 | 代表输出/验收 |
|---|---|---|---|
| 请求受理 | 4 | 48 份来源 + 任务合同 | 清单、哈希值、可读性报告 |
| 提取 | 12 | 分组来源标识符 | 证据行、来源位置校验 |
| 验证 | 8 | 160条候选证据 | 支持/冲突/未知 |
| 综合 | 6 | 已验证证据 | 6个部分摘要 |
| 产物 | 4 | 部分摘要+模板 | 报告/表/简报/附录 |
| 最终 | 2 | 4文件+检查规则 | 跨文件审计、变更备注 |
| 合计 | 36 | 版本化输入 | 5个交付文件 |
拆得太粗,U07 失败会让数小时工作无法判定;拆得太细,每写一句话都做检查点,又会增加存储、协调和调试成本。边界优先放在昂贵调用之后、人工等待之前、外部副作用前后、任务分开或汇合处,以及可复用产物形成后。
工作单元不是模型的一轮消息。一次单元可包含检索、模型提取和规则验证;也可以完全不用模型。恢复层只依赖单元契约,不依赖某个模型恰好用了几轮思考。
用有向无环图表达依赖,而不是只记“做到第几步”
36 个单元形成一张有向无环图(DAG)。12 个提取单元可以按来源组并行;8 个验证单元等待相应提取完成;6 个综合单元各自使用一组已验证证据;跨文件审计只有在 4 个产物均为当前版本时才运行。恢复器寻找依赖已经满足的待处理单元,而不是简单执行“编号 + 1”。
| 单元 | 依赖 | 何时失效 | 可并行 |
|---|---|---|---|
| E03 | I01 清单、I03 解析器报告 | 来源组 C 哈希值变化 | E01–E12 |
| V03 | E03 | 证据结构或行变化 | V01–V08 |
| S02 | V01、V03、V04 | 受众或对应声明变化 | S01–S06 |
| A01 报告 | S01–S06、模板 v7 | 任一部分/模板变化 | A02–A04部分并行 |
| F01 跨文件审计 | A01–A04 | 任一产物哈希值变化 | 否 |
这张依赖图也使局部恢复成为可能。来源组 C 变化,只让 E03 及其真正下游失效,不重跑与 C 无关的全部提取。受众变化影响风险排序与摘要表达,案例中只传播到 U27–30;原始文件清单和证据提取仍可复用。
循环探索也要显式设成有界迭代。例如 S04 最多进行 3 轮“生成→主张检查→修正”,每轮都有尝试编号和停止条件。不能把无界循环伪装成一个图节点,否则检查点只能记录“还在想”,既无法预测成本,也无法判断任务是否已经卡死。
状态词必须有单一含义:运行中、成功、已阻塞、失败和已失效不能混用
| 状态 | 定义 | 恢复行为 |
|---|---|---|
| 待定 | 依赖未满足或尚未领取 | 等待/可调度 |
| 就绪 | 依赖满足且允许执行 | 执行器可主张 |
| 运行中 | 某尝试持有效租约 | 不重复领取 |
| 成功 | 当前版本输出已验证并提交 | 默认复用 |
| 已阻塞 | 缺人、权限、输入或批准 | 不自动重试 |
| 可重试失败 | 短暂失败且预算未耗尽 | 有界重试 |
| 终止失败 | 输入或规则不允许继续 | 人工处理或结束 |
| 已失效 | 曾成功但上游版本已变 | 回到待定生成新版本 |
| 取消 | 负责人明确终止 | 不恢复 |
“完成 80%”不属于状态,它只是进度估计。成功要求产物存在、哈希值匹配、验证器通过且检查点提交成功;文件写到一半仍然是运行中。已阻塞也不是失败:等待客户确认受众时反复重试不会增加成功概率,只会浪费调用。
状态转换由编排器执行,模型只能提交结果和原因。模型在摘要里写“已完成 U18”不会修改持久状态;必须由验证器确认结果,再通过带版本条件的更新,把预期旧状态一次性推进到新状态。
运行、输入、计划、单元、尝试和产物必须分别编号,恢复时才不会认错对象。 运行编号代表一次业务任务,例如 ME-042;输入快照 v3 固定 48 个来源标识、哈希值与任务合同;计划 v4 定义 36 个单元和依赖;单元编号表示逻辑工作;尝试编号表示一次具体执行;产物编号和版本表示输出。六者混成一个“会话编号”,就无法区分重试、重规划与新任务。
| 身份 | 示例 | 变化条件 |
|---|---|---|
| 运行编号 | ME-042 | 新业务任务 |
| 输入版本 | in-v3 | 来源或合同改变 |
| 计划版本 | 计划-v4→v5 | 依赖图、规则或受众改变 |
| 单元编号 | U27 | 逻辑职责稳定 |
| 尝试编号 | U27-a1、U27-a2 | 每次执行递增 |
| 产物版本 | 风险摘要@哈希值:… | 内容字节改变 |
| 检查点版本 | cp-0001…0041 | 每次有效提交 |
恢复请求必须带运行编号与预期检查点版本。新建运行不复用旧光标;重试保持单元编号但更换尝试编号;需求变更保持运行编号,提升计划版本并执行失效分析;真正的分支方案则创建分支编号,避免两个方向抢写同一当前产物。
哈希值用于发现内容是否变化并绑定清单,它不证明内容真实、作者可信,也不代表碰撞风险已经得到妥善管理。敏感产物还需要访问控制、加密和来源签名或审计;不要把一串哈希值写成万能安全印章。
检查点只保存恢复所需的最小事实
{
"运行编号": "ME-042",
"检查点版本": 41,
"输入版本": "in-v3",
"计划版本": "plan-v5",
"单元": { "U27": { "状态": "成功", "尝试": 2 } },
"产物": [{ "编号": "风险简报", "哈希值": "sha256:…", "结构版本": "1.2" }],
"待处理": ["U31", "U32"],
"已阻塞": [],
"批准": [{ "门槛": "受众", "决定": "已批准", "版本": "v5" }],
"预算": { "已用模型调用": 74, "上限": 110 },
"下一步动作": "依赖检查通过后领取 U31"
}
除了这些,还要保存最后错误类别、外部操作收据、活跃租约、策略/模型/工具版本、来源快照、创建/提交时间和检查点父项。模型的长思维链既不需要也不应该作为恢复依赖;保存的是决定、证据、约束和产物,不是不可复核的内部推理流水。
| 必存 | 原因 | 不应默认全存 |
|---|---|---|
| 状态与依赖版本 | 计算下一步 | 全部聊天令牌 |
| 产物位置、哈希值和结构版本 | 验证可复用结果 | 来源文件全文副本 |
| 决策与批准版本 | 防旧批准漂移 | 与任务无关个人数据 |
| 错误/尝试/预算 | 控制重试与成本 | 调试中的机密 |
| 外部收据和操作密钥 | 对账副作用 | 无必要的完整接口响应 |
| 下一步动作 + 停止原因 | 人工接管 | 模型自我评价 |
检查点结构要有版本和迁移策略。新代码读不了旧检查点时应停止或迁移副本,不猜字段;迁移前后保留校验与回滚证据。
检查点还要区分事实、派生视图和可重建缓存。单元状态、批准、外部收据与产物指针属于事实来源;“总进度83%”、供人阅读的摘要和就绪列表可由前者重算。把派生字段当权威,升级算法后可能出现状态说U19 成功、百分比却停在78%的双重真相。提交时只让一种规范状态驱动执行,面板数据标明计算版本。
敏感度也要按字段处理。输入快照可以只存来源标识与哈希值,不复制原文;错误只保留类别与安全消息,不保存完整的服务商响应;恢复包通过来源位置按需取资料。若检查点存储一旦泄露就会暴露全部客户文本,说明所谓“最小恢复状态”并没有真正做到最小化。
产物清单把“做过”变成“可核验”
每个单元都输出到不可变的尝试目录,例如“运行/ME-042/U07/a2/”。清单记录媒体类型、大小、哈希值、结构版本、来源标识、验证器结果、生产者版本和创建时间。所谓“当前版本”只是检查点中的指针,不会覆盖旧文件。这样既能追查某个结果为何失效,也能区分孤儿文件与正式产物。
| 产物状态 | 含义 | 是否供下游使用 |
|---|---|---|
| 暂存 | 文件写完、尚未验证 | 否 |
| 已验证 | 结构与内容检查通过 | 等检查点提交 |
| 当前 | 已提交检查点引用 | 是 |
| 已失效 | 上游/规则版本变化 | 否,保留审计 |
| 孤儿 | 文件存在但无提交引用 | 否,隔离后清理 |
| 隔离 | 恶意/损坏/来源异常 | 否,限制访问 |
U07 第一次尝试写出“证据-07.json”后进程退出。由于 cp-0007 不存在,恢复扫描发现该文件未被任何已提交清单引用,于是把它标记为孤儿,而不是根据修改时间猜测它已经完成。a2 重跑并通过验证后才成为当前版本。即使 a1 与 a2 的字节完全相同,也要保留“未提交→重试”的事实。
清单还能避免反复读取大产物。恢复上下文只加载摘要、结构版本和必要摘录;需要完整表格时再按来源位置读取。若哈希值不匹配,立即把产物视为损坏或已更改,并向下游传播失效。
原子提交的目标是:要么看见旧检查点,要么看见完整新检查点,不看见半完成状态
跨文件系统和数据库,很难凭一句“保存”就获得原子性。本案例采用“不可变产物 + 事务指针”:先写尝试目录,完成结构和哈希值验证;再使用预期检查点版本做条件更新,把单元状态、清单引用、预算和下一步动作一起从 cp-n 推进到 cp-n+1。只有事务成功,单元才对外宣布成功。
领取租约 → 执行计算 → 写入暂存产物 → 验证结构与哈希值
→ 以“预期检查点版本 = n”为条件提交新检查点
→ 成功:引用的产物成为当前版本,并释放租约
→ 冲突或崩溃:产物保留为孤儿,已提交状态不前进
| 故障时点 | 持久化观察 | 恢复 |
|---|---|---|
| 写产物前崩溃 | 只有运行中+过期租约 | 重跑单元 |
| 产物写后、提交前崩溃 | 暂存或孤儿,无新检查点 | 隔离并重跑,或按明确策略核验复用 |
| 提交成功、确认前断线 | 新检查点已存在 | 读取检查点,不重复执行 |
| 两个执行器同时提交 | 只有一个版本条件更新成功 | 失败方丢弃本次尝试并刷新 |
| 清单引用不存在 | 检查点校验失败 | 停止并告警,不能跳过 |
如果存储不支持跨记录事务,就采用预写意图、单一清单对象或其他有明确恢复语义的方案;不能让单元状态和产物指针分别“尽量写成功”。真正的边界要通过断电/终止测试验证,而不是仅从代码顺序推断。
“提交成功但执行器没收到确认”尤其容易制造重复。执行器不能在超时后直接创建 a3,而要先用运行编号和预期检查点查询最新状态:若检查点已从 22 变成 23,且清单包含 a2,就把 a2 视为成功并停止;仍为 22 时,才用同一单元的新尝试重试。查询本身失败则保持状态未知并等待或告警,不能把网络不确定解释为提交失败。
反过来,产物存储确认写入而检查点数据库失败,恢复器也不能自动把文件“补登记”为当前,因为当时验证器、预算或策略是否全部通过可能不可证。默认隔离孤儿;只有清单含完整签名验证结果、补登记流程经过人工/规则明确批准时才能采用。保守重算会增加一次成本,却避免半成品进入下游。
检查点频率由损失窗口和副作用边界决定
检查点间隔过长,崩溃后就要重做昂贵调用;间隔过短,又会带来存储膨胀、锁竞争、序列化延迟和隐私复制。周遥以单元结束为基本检查点,并在阶段完成、人工中断前、不可逆动作前后,以及输入或计划变更时额外提交。单元内部每 10 秒记录一次模型令牌用量,对恢复并没有实际价值。
| 触发点 | 是否检查点 | 理由 |
|---|---|---|
| 单元领取 | 是,运行中 + 租约 | 防并发重复 |
| 只读检索每一页 | 否,单元内可重做 | 状态噪声过大 |
| 昂贵批次提取完成 | 是 | 降低损失窗口 |
| 人工批准请求前 | 是 | 可安全等待数小时 |
| 外部写入意图/收据 | 是 | 支持对账,不盲重试 |
| 滚动摘要每次改字 | 否 | 不等于稳定里程碑 |
| 计划/输入改变 | 是 | 建立失效与新基线 |
可以估算恢复点目标:若一个单元最多45分钟,最坏重算窗口约一个单元,但不包括已发生且无法回滚的外部动作。高成本或有副作用的单元应更细,纯本地廉价转换可以更粗。
恢复上下文靠重建,不靠重读 28 小时对话
恢复包由任务合同、当前输入/计划、已完成单元摘要、必要产物摘录、待定决策、错误与下一步行动组成。历史对话只在解释尚未结构化的决策时按需读取。这样上下文大小与当前工作相关,不与运行时间线性增长。
| 恢复包层 | 内容 | 令牌策略 |
|---|---|---|
| 不变性 | 目标、禁止项、质量门 | 始终加载 |
| 当前状态 | 检查点、待处理/已阻塞、预算 | 结构化短文本 |
| 工作单元 | 当前输入、输出和结构版本 | 只加载一个或一组 |
| 证据 | 来源位置+相关摘录 | 按需读取 |
| 决策 | 仍有效的批准/冲突处理 | 去掉已失效版本 |
| 历史 | 事故/旧尝试 | 调试时加载 |
压缩摘要本身也要带源检查点版本和生成器版本;上游决策改变后,旧摘要可能失效。恢复器先验证恢复包引用的产物与检查点,再交给模型,避免把陈旧自然语言当当前真相。
Azure 架构中心的智能体编排指导也建议,长任务应把进度、中间结果和必要对话状态写入持久化存储,并限制为恢复所需的最小信息。本文进一步把“必要”落实为可验证字段,而不是保存整个上下文窗口。
恢复算法先审计,再决定下一步
| 顺序 | 检查 | 失败动作 |
|---|---|---|
| 1 | 读取运行的最新已提交检查点 | 无则新建/人工确认 |
| 2 | 校验结构、父链、哈希值 | 停止并从备份恢复 |
| 3 | 比较代码/工具/策略/输入/计划版本 | 迁移或失效分析 |
| 4 | 对账运行中租约与外部收据 | 等待、接管或对账 |
| 5 | 验证当前产物存在且哈希值匹配 | 已失效或隔离 |
| 6 | 按依赖图传播失效 | 只影响真实下游 |
| 7 | 计算就绪集合、重试预算和批准 | 选择一个/并行组 |
| 8 | 以版本条件领取单元、生成新尝试 | 冲突则刷新后重算 |
恢复不是无条件“从最后一步继续”。旧检查点由不同代码生成且无迁移时,继续可能比重跑危险;外部操作处于状态未知时先查询远端;输入快照已被用户替换时先做影响分析。没有安全下一步行动就是已阻塞,不由模型编一个。
Microsoft、LangGraph与其他持久化运行时可能自动完成部分步骤,但应用仍需正确划分活动、确定持久字段和副作用语义。框架的“恢复”按钮不能替代业务产物与下游对账。
恢复(运行编号):
检查点 = 读取最新已提交检查点(运行编号)
验证父链、结构与产物(检查点)
比较运行环境与输入版本(检查点)
对账过期租约与状态未知的外部操作(检查点)
受影响单元 = 从变更依赖传播失效(检查点)
就绪单元 = 筛选依赖当前且预算可用的单元(检查点,受影响单元)
如果没有就绪单元:返回“完成”或“带原因阻塞”
返回“以版本条件和新隔离令牌领取一个就绪单元”
这段伪代码刻意没有“让模型回忆下一步”。模型只在已经领取的单元内工作,调度选择来自持久化事实。只有所有必需的最终单元均为当前版本、且最终验证器通过,任务才算完成;就绪列表为空,但仍存在已阻塞或已失效单元,并不等于完成。恢复器的计算可以重复执行,遇到版本冲突就从最新检查点重新开始。
重试、恢复、重放和重启是四种不同动作
| 动作 | 起点 | 复用什么 | 适用 |
|---|---|---|---|
| 重试 | 同一单元新尝试 | 已提交上游 | 瞬时故障 |
| 恢复 | 同一运行最新检查点 | 所有仍当前结果 | 进程/等待后继续 |
| 重放 | 从历史重建/验证状态 | 既有事件/结果按运行时语义 | 调试、确定性恢复 |
| 重启 | 新运行或明确清空基线 | 通常不复用旧当前 | 任务定义重大改变 |
AWS 步骤函数的具体重驱功能会保留成功步骤的结果和执行历史,并从未成功步骤继续且使用同一输入/定义;这是某产品的明确行为,不是所有智能体“重试”天然具备的语义。LangGraph的持久性按步骤保存检查点,中断恢复可能重新执行所在节点,因此官方特别提醒中断前的副作用应幂等。
若团队说“再跑一次”,必须写清是哪一种。周遥对 U21 做重试,对进程重启做恢复,对故障测试样本做重放验证;受众彻底改变但核心任务不变时提升计划并局部失效,而不是全新重启。
外部动作必须单独保存意图、操作键与回执
本案例把最终发送留给人,运行期间只有本地不可变产物,所以恢复相对简单。但真实智能体可能创建工单、发邮件或更新客户系统;检查点写着“运行中”,不代表远端一定没有执行,写着“失败”,也可能只是回复丢失。外部动作要在独立账本中记录意图、幂等键、请求哈希值、远程编号、回执和对账状态。
| 本地/远端组合 | 不能直接做 | 正确下一步 |
|---|---|---|
| 意图无回执、远端有记录 | 重发 | 按操作键查远端并补回执 |
| 意图有回执、检查点旧 | 重做单元 | 复用回执并推进状态 |
| 超时、远端未知 | 标失败后再发 | 标状态未知,查询/人工对账 |
| 批准旧于产物 | 沿用批准 | 重新审批新哈希值 |
| 邮件已送达、检查点丢失 | 再发送 | 用服务商消息编号确认 |
检查点负责“我知道做到哪一步”,幂等与对账负责“外部世界实际发生了什么”。这也是 O27 的重点;本篇只要求长任务把操作回执纳入恢复输入,不能仅凭对话记忆判断。
任何不可逆动作前都建议提交操作前检查点,动作后尽快提交回执;两者之间仍存在崩溃窗口,所以必须能查询或人工接管。无法对账的高风险动作不应交给自动恢复。
并发接管和人工中断都要留下可恢复边界
租约和隔离令牌防止两个执行器同时接管
进程 A 失联不等于已经停止,它可能只是遇到网络分区。若进程 B 看到“运行中”超时就直接写同一产物,A 恢复后可能继续提交,形成两个执行器同时认为自己有效的冲突。编排器因此给每次尝试分配短期租约和递增的隔离令牌;每次提交都验证令牌仍是当前值,旧执行器即使继续计算,也不能推进检查点。
| 字段 | 示例 | 作用 |
|---|---|---|
| 负责人 | 执行器-12 | 可追踪执行者 |
| 租约截止时间 | 14:35:00Z | 何时可考虑接管 |
| 隔离令牌 | 87 | 拒绝旧负责人写入 |
| 最后心跳时间 | 14:34:20Z | 观察活性,不证明没有外部动作 |
| 尝试目录 | U21/a2 | 隔离输出 |
| 预期检查点 | 22 | 版本条件更新的提交基线 |
接管流程先等租约过期,再查询外部操作,分配隔离令牌 88 和新的尝试目录。恢复心跳的旧执行器读到令牌失效后停止,并隔离自己的输出。租约只协调执行权,不能证明工具调用或客户动作没有发生,外部状态仍然要看回执。
对个人本机智能体,也可能同时开两个窗口或自动任务与手工恢复碰撞。一个轻量锁文件若无负责人、过期和围栏,在崩溃后容易永久卡住或被强删导致并发;至少要有可审计的接管规则。
人工中断要同时保存问题、选项、影响和恢复令牌
在 U26 完成后,智能体发现受众定义模糊,于是提交检查点并进入已阻塞,向周遥展示:当前受众是“产品团队”,候选受众是“投资委员会”,会受影响的是 U27–30,不受影响的是 U01–26,并列出两种选择的预计重做成本。人类作出选择后,决策事件与计划 v5 一起提交,恢复从明确边界继续。
| 批准字段 | 本例 |
|---|---|
| 放行门槛 | 受众范围 |
| 检查点 | cp-0030 |
| 产物或输入哈希值 | 当前证据集 |
| 选项 | 保持/改为投资委员会/取消 |
| 影响 | 使 U27–30 失效 |
| 决策/负责人/时间 | 变更/周遥/18:42Z |
| 过期 | 输入或计划再变即失效 |
| 恢复令牌 | 不透明的一次性引用 |
不要只存“用户说可以”。批准要绑定对象、版本、范围和有效期;之后报告哈希值或外部受众发生变化,旧批准不能跟着漂移。恢复令牌不能包含秘密,也不能直接授权高风险动作;它应当一次性使用、到期失效,并绑定运行与放行门槛。
LangGraph 官方中断文档说明,检查点用于保存图状态,线程编号指向恢复位置;重新使用同一线程会继续运行,而新的标识会开启空状态。具体产品语义不能直接复制到其他框架,但它说明了一个通用原则:恢复入口必须稳定,而且必须能与新任务区分。
输入和需求变化后,只保留能证明仍有效的工作
变更不是错误。恢复时若发现输入 v4,不应把所有旧结果当当前,也不必一律从零开始。每个产物清单记录源标识符、规则/模型/工具/模板版本与决策依赖;用溯源图计算哪些结果真正受影响,再由策略决定自动失效还是人工评估。
| 变化 | 自动保留 | 失效 | 理由 |
|---|---|---|---|
| 新增无关附录文件 | 已验证核心证据 | 文件清单与附录相关单元 | 输入集合变化但影响局部 |
| 来源 C 的内容哈希值变化 | 其他来源组 | E03 及其下游 | 精确来源 |
| 受众改为投资委员会 | 摄入/提取/验证 | U27–30 | 事实保留,排序/表达重做 |
| 主张结构升级不兼容 | 原文件清单 | 全部证据下游 | 结构契约变化 |
| 拼写模板修订 | 证据与综合 | 4个产物 + 最终审计 | 只影响呈现 |
案例中的 4 个旧单元从成功变为已失效,但不删除;新尝试在计划 v5 下完成,当前指针随之更新。最终统计同时报告“36 个当前成功”和“4 个过时成功”,避免把重做隐藏成“总共只做了 36 步”。
人工可覆盖自动影响分析,但必须记录理由。若来源缺失或规则变化难以判断,宁可扩大失效范围,不让旧结果悄悄进入新交付。
传播规则还要防两种错误。第一是过度失效:改一个附录拼写却重做160条证据,浪费已验证工作;第二是不足失效:只重做报告文本,却复用旧受众下的风险排序。每条依赖边标注类型——内容、结构、策略、受众、模板或展示;变化事件也带同类标签,恢复器据此匹配,再由高风险规则扩大而不能缩小默认范围。
若 A01 报告与 A03 简报都引用 S04,而 S04 只有格式变化,可能只需重新渲染;若 S04 的声明集合发生变化,则两者与 F01 跨文件审计都要失效。影响图应输出“变化起点→经过的依赖→被失效单元”,让人能够复核,不能只给出一句无法解释的“需要重做 23 步”。
失败分类和预算决定何时重试、何时停下
| 失败 | 自动动作 | 上限/停止 |
|---|---|---|
| 模型速率限制 | 退避后重试 | 2次或预算截止 |
| 网络超时 | 若只读则有限重试 | 2次;写操作先对账 |
| 结构不合格的输出 | 依据结构错误反馈再生成 | 1 次,仍错则阻塞 |
| 源损坏 | 隔离 | 不换来源猜内容 |
| 权限被拒绝 | 已阻塞 | 不扩大范围 |
| 检查点冲突 | 刷新最新再算下一个 | 不盲目覆盖 |
| 预算耗尽 | 已阻塞 + 成本报告 | 人工追加或缩范围 |
| 不变量被违反 | 终止停止 | 隔离运行并审计 |
预算也要进入检查点,包括模型调用、工具调用、模型令牌用量、总历时、存储量和重试次数。恢复后不能把计数清零,否则多次崩溃就能绕过预算。U21 因额度耗尽进入已阻塞,周遥次日批准新额度后更新预算决策;系统没有把它当成瞬时故障无限等待。
连续没有新已提交产物、重复相同错误、就绪集合为空但非完成、租约长期续租无进展都触发卡滞告警。智能体不能用更长“正在处理”掩盖没有状态变化。
事件轨迹要能对上 41 次基线提交
| 时间/事件 | 检查点变化 | 产物/状态 |
|---|---|---|
| H0 启动 | cp-0000,in-v3/计划-v4 | 36 个待定 |
| U01–06 完成 | 6 个单元提交 | 6 个当前 |
| U07-a1 后崩溃 | 无新提交 | 1 个孤儿 |
| U07-a2、U08–20 | 14个单元提交 | U01–20 当前 |
| U21 额度阻塞 | 运行中→已阻塞写入运行事件,不计基线提交 | 无产物 |
| 额度批准、U21–26 | 6个完成提交 | U01–26 当前 |
| U27–30完成 | 4个完成提交 | 30 当前 |
| 受众变更 | 1个计划变更提交 | 4 已失效 |
| U27–30 a2 | 4个完成提交 | 再回30 当前 |
| U31–36 | 6个完成提交 | 36 当前、5 最终文件 |
这里的 41 次提交只统计 36 个当前完成、4 个后来失效的完成和 1 次计划变更。运行中、已阻塞、租约心跳等状态更新另在事件日志中计数,不混进“完成或计划基线提交”。U07-a1 没有完成提交,U21 首次运行也没有产物。先把统计口径写清楚,才能避免同一事件在不同图表里变成不同数字。
28 小时是总历时,包含夜间等待与人工修改。由于没有可靠的“无检查点”对照组,本文不虚构节省了多少小时。能够证明的结果只有三项:U01–06、U08–20 等已提交单元没有重做;每次恢复决定都有证据;最终没有重复当前产物或外部动作。
用 48 项故障测试证明恢复不是偶然成功
| 系列 | 案例数 | 代表故障 |
|---|---|---|
| 结构或身份 | 8 | 错运行、旧结构、断父级、哈希值错误 |
| 产物/原子性 | 10 | 写前/写中/写后、提交前/后终止 |
| 恢复/重试 | 10 | 过期租约、确认丢失、额度/超时 |
| 失效/版本 | 8 | 来源、受众、工具或结构变化 |
| 并发或隔离 | 6 | 双执行器、旧令牌、版本条件冲突 |
| 人工/外部 | 6 | 旧批准、过期令牌、未知收据 |
| 合计 | 48 | 允许、拒绝、已阻塞与恢复均留证据 |
每个案例固定检查点前态、注入点、磁盘/数据库/远端观察、预期状态、应重跑单元集合和禁止重复对象。测试不是杀掉进程后看“最后能完成”;必须核对U01–06没有再次调用、孤儿未进入下游、旧执行器不能提交、旧批准不复用、外部未知没有盲重试。
框架升级、检查点结构改变、单元边界改变、产物存储迁移或新增写入工具都触发相关套件全跑。LangGraph官方持久性文档提到按步骤保存检查点、失败超级步骤的待处理写入和从最后成功步骤恢复等具体机制;使用时应按实际运行时验证,不能把本文自建清单语义强套给所有框架。
测试还要区分实时安全与离线回放。可能发送邮件或写入客户系统的故障案例,只在沙箱或模拟服务商中运行,不能为了证明不会重复而对真实客户连发两次。真实环境只演练暂停、只读探测、授权撤销和人工回退。测试判断同时比较状态、产物、调用账本和外部收据,而不是只看最终页面是否显示“成功”。
运行面板、保留与恢复演练让检查点从“有数据”变成“真的可用”。 面板至少显示运行/计划/输入版本、最新已提交检查点、当前/已失效/孤儿数量、就绪/运行中/已阻塞、租约负责人、最后进度时间、预算、待批准、外部未知和可执行下一步行动。只显示百分比无法支撑接管。
| 告警 | 阈值(案例) | 行动 |
|---|---|---|
| 无提交进度 | 60分钟 | 检查执行器/已阻塞 |
| 检测到孤儿 | 任意1个 | 隔离、定位终止点 |
| 哈希值不匹配 | 任意 1 个 | 停止下游、重新验证 |
| 过期租约运行中 | 5分钟 | 对账后接管 |
| 失效扇出 | >12 单位 | 人工复核影响图 |
| 预算 >80% | 88/110 调用 | 缩范围/申请决定 |
| 外部未知 | 任意1个 | 禁止自动重复 |
检查点和产物可能含有客户资料,不应无限保留。当前交付按合同保存;已失效产物和孤儿文件在审计窗口结束后安全删除;事件日志尽量只存元数据;机密和长思维链不进入检查点。备份也要做恢复测试:能列出对象,不等于能恢复父链、权限和哈希值。
人工接管页面不只给一个“继续”按钮。它要能导出当前任务合同、最新基线、36个单元状态、4个已失效理由、孤儿清单、待批准、预算余量、外部状态未知、最近错误和建议下一步行动;接管人可选择恢复、缩小范围、重新执行某单元、分支、取消或标记需要专业处理。每个选择先显示会复用和会失效的对象,再产生新决策事件,不能直接手改数据库状态。
交接还需要一条人类可读叙述,但叙述从结构化状态生成:最后一个可靠基线是什么、为什么停、已经花了什么、哪些资料未验证、恢复后第一步是什么。它帮助周遥快速理解,不替代机器状态。若叙述与清单冲突,以可验证状态为准并触发告警,而不是让模型“协调”出一个看似顺畅的版本。
周遥每月做一次演练:复制去标识运行、随机终止两个边界、用另一执行器恢复、核对当前集合和调用账本,再测试人工取消。没有定期恢复演练,检查点很可能只在顺利运行时可写,真正事故时却读不回来。
备份恢复还要测试三个层次:检查点数据库能否恢复最新记录与父链;产物存储能否按清单取回具有相同哈希值的对象;凭证和访问控制列表能否让恢复执行器只看到本次运行所需的对象。只恢复数据库、但产物已经过期,得到的是一组断链指针;只恢复产物、却丢了检查点,则无法判断哪些版本是当前版本。恢复点不一致时,宁可停在双方都能验证的最后一个共同基线。
灾难恢复演练还要记录恢复点目标和恢复时间目标。本案例允许最多丢失一个未提交单元,目标是在 30 分钟内恢复到可人工接管的状态;这只是业务选择,不是通用推荐。若任务包含不可逆的外部动作,恢复点目标不能只用“一个单元”粗略表达,必须单独保证操作回执与远端对账。
先让一个两小时任务能停、能查、能接
最小控制包包括任务合同、依赖图和单元注册表、状态转换、身份与版本规则、检查点结构、产物清单、原子提交、恢复算法、失效图、租约与隔离、批准记录、操作回执、预算与告警、48 项故障测试和恢复运行手册。第一次实施不必上复杂平台,但不能省掉这些语义。
本案例参考了几类官方实现:LangGraph 的持久化与中断文档展示了步骤检查点、线程恢复、待处理写入,以及中断前外部动作的幂等要求;Microsoft 持久化任务与智能体文档展示了检查点状态、跨重启恢复和持久化执行,但产品保证取决于具体运行环境;Azure 持久化编排以事件历史和确定性重放来重建本地状态;AWS 步骤函数的重放功能在限定条件下保留成功步骤,并从失败步骤继续。本文提炼的是通用设计问题,不声称不同产品的检查点可以互换。
- LangGraph官方:Persistence
- LangGraph官方:Interrupts
- Microsoft:Durable Task for AI agents
- Microsoft:Durable orchestrations overview
- Microsoft:AI Agent Orchestration Patterns
- AWS 步骤函数:通过重放重新启动执行
- Temporal官方文档:Durable Execution
今天先选一个可重复的两小时任务,拆成 4—8 个单元。故意在“产物写完、检查点提交前”和“检查点提交后、执行器收到确认前”各终止一次。恢复时,如果你能用状态和哈希值解释为什么重做或为什么不重做,而不是靠聊天记录猜测,就已经建立了真正的恢复入口。