任务状态与 Checkpoint
长任务如何记录进度、中间结果与已执行的动作。
一句话理解:任务状态是长任务的“施工日志”:计划了什么、每步做到哪、产物放在哪、对外做过哪些动作、在等谁;它必须是结构化的、每个关键节点都落盘,这样任何时候中断都能准确续上,而不是重做或漏做。
回到本模块的实践目标:让 Agent 支持“继续昨天那个任务”。上一课的会话能帮 Agent 想起“昨天聊过什么”,但对一个要诊断 200 个页面的批量 SEO 分析任务来说,这远远不够。Agent 需要准确知道:
- 200 个页面里哪些处理完了、哪些失败了、哪些还没开始?
- 第 87 个页面创建了工单,但通知是否发出去了?
- 哪 3 个页面的改动在等主管审批?审批结果回来了吗?
- 已经花了多少预算,还剩多少?
这些问题的答案如果只能从几百轮对话里“读出来”,就既慢又不可靠:历史可能已被压缩,模型也可能读错。这一课讲如何设计任务状态和检查点。
对话历史为什么不能当进度
| 维度 | 对话历史 | 结构化任务状态 |
|---|---|---|
| 可读者 | 主要给模型读 | 程序、模型、人都能读 |
| 查询“第 87 个页面是否已通知” | 需要模型在长历史里找 | 直接查字段 |
| 被压缩后 | 细节可能丢失 | 不受影响 |
| 用于界面展示进度 | 困难 | 直接渲染 |
| 用于防止重复执行写操作 | 不可靠 | 可以做程序级校验 |
Anthropic 在关于长时间运行 Agent 的实践中给出了一个很好的例子:让 Agent 先生成一份 JSON 格式的功能清单,每项初始标为“未通过”,每完成并验证一项才修改状态,同时配合 Git 提交和进度文件。这本质上就是把任务状态从对话中剥离出来,存成结构化、可验证的外部记录。
任务状态里应该有什么
以「全站 SEO 批量诊断任务」为例,一份任务状态可以是这样(示例结构,不对应任何特定产品):
{
"task_id": "task_20260917_0042",
"goal": "诊断 9 月 16 日抓取的 200 个页面的 SEO 问题",
"status": "running",
"created_by": "user_ops_zhang",
"created_at": "2026-09-17T09:00:00+08:00",
"updated_at": "2026-09-17T10:42:13+08:00",
"plan": [
"逐页诊断 SEO 问题",
"为需要修复的页面创建工单",
"通知页面负责人",
"影响范围超过阈值的改动提交主管审批"
],
"progress": { "total": 200, "done": 86, "failed": 2, "waiting_approval": 3, "pending": 109 },
"items": {
"page_087": {
"stage": "notify",
"steps": {
"diagnose": { "status": "done", "result": "缺失 meta 描述", "checked_at": "2026-09-17T10:40:02+08:00" },
"create_ticket": { "status": "done", "external_id": "T-8812", "idempotency_key": "task_20260917_0042:page_087:create_ticket" },
"notify": { "status": "started", "idempotency_key": "task_20260917_0042:page_087:notify" }
}
}
},
"approvals": [
{ "item": "page_033", "action": "批量改写 37 组重复标题", "requested_at": "2026-09-17T09:31:00+08:00", "decision": null }
],
"artifacts": { "report_draft": "s3://reports/task_20260917_0042/draft.md" },
"budget": { "limit_usd": 20, "spent_usd": 7.35 },
"last_checkpoint": "page_087.notify.started"
}
逐项说明它们各自的作用:
| 字段 | 作用 | 缺了会怎样 |
|---|---|---|
goal、plan |
恢复时让模型迅速进入状态 | 恢复后模型重新理解任务,结论可能偏移 |
progress |
给人看进度,给程序判断是否完成 | 用户只能问“做完了吗” |
items[].steps |
精确到每个对象、每个步骤的状态 | 不知道从哪里续跑 |
external_id |
记录外部系统返回的 ID | 无法核对“工单到底建没建” |
idempotency_key |
保证重试不重复执行 | 重复工单、重复通知 |
status: started |
标记“开始执行但结果未知” | 崩溃后无法区分“没做”和“做了一半” |
approvals |
记录等待中的人工决定 | 审批回来后找不到对应的动作 |
artifacts |
中间产物的位置(而非内容本身) | 产物塞进状态,导致状态臃肿 |
budget |
已花费与上限 | 续跑后预算重新计算,失去约束 |
| 时间戳 | 判断数据是否过期 | 用几小时前的诊断结论执行动作 |
两个原则:状态里存引用而不是大块内容(报告正文放对象存储,状态里只存路径);状态要能被人读懂(运营同学打开它,应该能看明白任务进展)。
Checkpoint:什么时候落盘
Checkpoint(检查点)就是把任务状态持久化保存的时刻。粒度的选择是一种权衡:
| 粒度 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 太粗 | 整个任务结束才保存 | 实现简单 | 中断就全部重来 |
| 适中 | 每个业务步骤、每个写操作前后保存 | 恢复精确,开销可控 | 需要设计步骤边界 |
| 很细 | 每次模型调用都保存完整上下文 | 可以精确回放 | 存储与性能开销大 |
把三种粒度放在同一条时间线上:
对含有写操作的任务,推荐的节奏是“意图 → 执行 → 结果”三次落盘:
框架里也有现成的检查点机制,但语义各不相同:
- LangGraph:Checkpointer 在图执行过程中保存线程状态的检查点,支持对话延续、人工介入、时间回溯和容错。需要注意,用
interrupt()暂停的节点在恢复时会从头重新执行,暂停点之前的写操作必须幂等。 - Claude Agent SDK:会话保存对话,另有文件检查点功能用于快照和回滚 Agent 对文件的修改。两者分别管“说了什么”和“改了什么文件”,业务系统里的写操作仍需你自己记录。
- Claude API 的记忆工具:启用后,API 会自动在系统提示中加入一段协议,要求模型先查看记忆目录中的既有进度,并在工作中记录状态与进展,因为上下文窗口随时可能被重置。这是一种“由模型维护进度笔记”的轻量方案,适合开放式任务;对批量业务任务,仍建议由程序维护结构化状态。
恢复流程
“继续昨天那个任务”的标准恢复流程:
第 7 步是关键:恢复时给模型的是“一份精炼的简报”,而不是把昨天几百轮对话全部塞回去。这既省成本,也避免模型被旧对话中过时的推断误导。
常见误区
- “有会话恢复就够了。” 会话恢复的是对话,长任务需要的是可查询、可校验的进度。
- “状态里把所有中间结果都存进去,方便恢复。” 状态会迅速膨胀。大块内容存外部,状态只存引用和摘要。
- “步骤标记为 done,就说明做完了。” 标记应该在验证之后,而不是调用之后。发送成功要看回执,创建成功要有外部 ID(见 各类动作的验证方式)。
设计要点
- 任务状态结构化,包含目标、计划、逐项步骤状态、外部 ID、幂等键、审批、产物引用、预算与时间戳。
- 写操作遵循“意图 → 执行 → 结果”三次落盘。
started状态是恢复时的重点检查对象,必须能查询真实结果。- 恢复时先刷新业务状态,再给模型一份精炼简报。
- 任务状态同时服务于界面进度展示,字段设计要考虑“人能看懂”。
- 明确任务状态的保留期:任务结束后保留多久用于审计,何时清理。
练习
为「可以断点续跑的长任务 Agent」(第 8 周实践目标)完成设计,用时约 60 分钟:
- 选一个场景:全站 SEO 诊断,或批量处理客服工单。
- 写出任务状态的 JSON 结构,至少覆盖本课表格中的 8 个字段。
- 画出一个写操作的三次落盘流程。
- 模拟三个中断时刻(刚写完意图、外部调用超时、审批等待中),分别写出恢复时 Harness 应该执行的步骤。
延伸阅读
- Effective harnesses for long-running agents:功能清单、Git 提交与进度文件的组合实践。
- Memory tool:记忆工具的使用协议与跨会话开发模式。
- LangGraph Persistence:检查点与线程状态。
下一步
任务状态随任务结束而结束。下一课「长期记忆」讨论跨越任务和会话仍然需要记住的东西:写入什么、如何召回、如何纠错与遗忘。