2366 字
约 7 分钟
12
S21-A29 · 检查点:为什么"跑了 40 分钟的任务"失败后只能从头再来

一个多步 Agent 任务跑了 40 分钟,第 12 步调外部接口超时,整个任务失败。用户点重试,它从第 1 步重新开始——已经调过的 11 次工具、已经付掉的 token、已经写了一半的中间产物,全部作废。第二次又在前面的某一步遇到限流,第三次才成功。用户等了两个多小时。

这类问题的修法不是"加大重试",而是在任务里放检查点:把"完成到哪一步、产出是什么"落成可恢复的状态。检查点的价值不是"更快",是让失败的成本从"全损"变成"损失最后一段"。


1. 检查点有三种,作用完全不同

类型 记录什么 失败后能做什么
进度检查点 走到第几步 从该步继续
数据检查点 中间产物(文件、结构化结果) 复用不必重算
决策检查点 已经做出的判断与依据 不重复询问/推理

第一行只解决"跳过",第二行才解决"省成本"。只有进度而没有产物时,重试仍然要重跑那些步骤——因为它们的输出没留下来。

// 一次 12 步任务的成本分布(无检查点 vs 有)
无检查点:第 12 步失败 → 重跑 1–12,累计成本 2 倍
有进度检查点:从第 12 步重跑,成本 1.08 倍(仍重算中间产物)
有数据检查点:只重跑第 12 步,成本 1.03 倍
// 差别不在"重试机制",在"上一次的产出有没有留下来"

2. 检查点放哪:三个判据

位置 该放吗 理由
每次工具调用之后 ✅ 一次调用就是一个成本单元
每次不可逆操作之前 ✅ 需要人工确认的天然断点
长耗时步骤中途 ❌(通常) 恢复语义复杂,收益低
纯推理步骤之间 ⚠️ 视情况 重算便宜时可跳过

第三行是最容易做过度的地方:把检查点放进步骤内部(比如一个 30 分钟的批处理的第 15 分钟),恢复时要处理"半批数据",语义复杂度远高于收益。除非那一步本身是可分片的幂等操作。

// 一个够用的检查点结构(存在会话状态里,不进 prompt)
checkpoint:
  run_id: r-8821
  step_index: 12
  status: failed               # running | failed | done
  artifacts:                   # 数据检查点:产出落在哪
    - { step: 3,  kind: file,      ref: s3://tmp/r-8821/extract.json }
    - { step: 7,  kind: structured, ref: state/r-8821/analysis.json }
  decisions:                   # 决策检查点:别再问第二遍
    - { step: 5, question: "时间范围", answer: "2025 全年" }
  next_action: retry_step_12

3. 恢复语义:三件事必须先定清楚

问题 不定清楚的后果
从哪一步恢复 重复执行已完成的副作用
中间产物是否可靠 用半截数据继续算,结果错但看起来像对
外部副作用是否已发生 邮件发两次、订单下两单

第三行是恢复语义的核心:检查点必须记录"哪些不可逆动作已经执行"。这与 S21 中断那篇的 irreversible_done 是同一条纪律——能安全重跑的前提是"知道什么不能再跑"。

// 恢复时的判定表
step 已完成 + 无副作用     → 跳过,复用产物
step 已完成 + 有副作用     → 绝不重跑,标记为 done
step 未完成 + 幂等         → 重跑
step 未完成 + 非幂等且可能已发出 → 先去查状态(查单、查消息),再决定
// 最后一行是唯一安全做法:不能靠"大概没发出去"

4. 检查点的存储:别塞进上下文

存法 优点 问题
写进 prompt / 对话历史 实现简单 占 token、且会被截断
存会话状态(KV / DB) 不占上下文、可跨会话 需要一次读写
存工作流引擎(Temporal 等) 自动重试、可观测 引入依赖

第二种是多数场景的平衡点:检查点是"系统的记忆",不是"模型的记忆"。把它塞进 prompt 有两个坏处——白花钱,且长任务里它一定会在某个时刻被截掉,于是"记忆"在最需要的时候消失。


5. 什么时候该重来而不是恢复

场景 选择 理由
输入数据变了 重来 旧产物与新输入不一致
中间某步逻辑改了(发版) 重来 新旧产物混用不可解释
失败了 3 次同一步 重来或改策略 说明该步本身有问题
产物已不可信(校验失败) 重来 宁可全损也不要用错数据

第二行是很容易踩的:发版后恢复旧检查点,会把旧逻辑的产物和新逻辑的步骤拼在一起。所以检查点要么带版本号、要么发版时作废。

// 恢复前的三步校验(任何一步不过就丢弃检查点)
$ sha256sum input/*.json > /tmp/fp && diff -q /tmp/fp state/r-8821/fingerprint
$ jq -r .logic_version state/r-8821/cp.json   // 与当前发版号比对
$ python3 verify.py state/r-8821/artifacts/    // schema + 行数 + 关键字段
// 三条都过 → 恢复;任一不过 → 从头开始,别硬凑
// 检查点的有效性判定(三条全过才允许恢复)
1 输入指纹未变(输入内容 hash 相同)
2 逻辑版本未变(checkpoint.logic_version == 当前版本)
3 产物通过校验(schema / 行数 / 关键字段)
// 任一不满足 → 丢弃检查点,从头开始

6. 排错:五种"有检查点还是重跑"

现象 八成原因 处理方向
恢复后仍从头跑 只存了进度没存产物 加数据检查点
恢复后结果错乱 产物是半截的 产物落盘要原子写
恢复后副作用重复 未记录不可逆动作 加 irreversible_done
检查点越来越大 把原始数据也存了 只存引用,不存内容
发版后恢复报错 无版本号 检查点带 logic_version
// 原子写产物的顺序(避免读到半截文件)
1 写 artifacts/step-7.json.tmp
2 fsync(确保落盘而不是在页缓存里)
3 rename step-7.json.tmp → step-7.json
// 第 3 步在同文件系统内是原子操作;跨设备 rename 会失败,不要跨挂载点

第二行的修法是"先写临时文件再 rename"——原子替换保证读到的产物要么是完整的旧版、要么是完整的新版,绝不会是写了一半的。


7. 边界:什么任务不需要检查点

场景 为什么 说明
单步任务(一次调用) 没有中间态 重试就是恢复
总耗时 < 30 秒 重跑成本可忽略 不值得
完全幂等且便宜 重跑无损失 直接重试
中间态无法序列化 强行做会出错 接受重跑

判断口径一句话:检查点的收益 = (已完成步骤的成本)×(失败概率),成本 = 序列化与恢复的复杂度——所以只在"任务够长 + 失败不罕见 + 中间态可序列化"三者同时成立时才值得做。反过来,短任务加检查点只是把复杂度提前引入。


8. 自测

  1. 三种检查点分别记录什么?为什么只有"进度"而没有"产物"时重试仍要重算?
  2. 检查点该放在哪三个位置?为什么放进"长耗时步骤中途"通常不划算?
  3. 恢复语义必须先定清楚哪三件事?哪一件关乎重复副作用?
  4. 为什么检查点不该塞进 prompt?除了费钱还有什么更严重的后果?
  5. 什么情况下该重来而不是恢复?发版后恢复旧检查点会发生什么?
  6. 检查点收益的公式是什么?三者中缺哪一个就不值得做?

9. 一句话总结

检查点的价值不是"更快",是让失败成本从"全损"变成"损失最后一段"——所以它必须同时记"进度"与"产物",只记进度等于没记。三件必须先定的事:从哪步恢复、产物是否原子、哪些不可逆动作已经发生(后者与中断那篇的 irreversible_done 同源)。检查点属于系统的记忆、不属于模型的记忆,塞进 prompt 只会在最需要时被截断。判据:任务够长 + 失败不罕见 + 中间态可序列化,三者缺一就别做。

S21-A29 · 检查点:为什么"跑了 40 分钟的任务"失败后只能从头再来
https://www.clxhxhhr.top/posts/6762/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。