Agent 的记忆和状态 · Lesson 4 of 6

第 4 课:结构化状态:让 Agent 记住任务进行到哪

学习目标:

  • 解释为什么把任务进度埋在对话文字里不可靠,必须变成结构化状态
  • 说出待办事项从创建到删除要经历的完整生命周期
  • 区分"从头重来"和"从中断处恢复"分别付出的代价
  • 判断一份任务状态该留在会话历史里,还是该额外写进检查点

前置要求:完成第 3 课,理解外部记忆的两种模式 | 上一课 第 3 课 << | 下一课 第 5 课 >>

进程重启之后,Agent 还知道自己做到哪一步了吗

一个 Agent 正在执行一个多步骤任务:重构一个模块,涉及"改类型定义""改调用点""跑测试""更新文档"四个步骤。它刚做完第二步,进程因为一次意外重启中断了。重新拉起来之后,它面对的是同一个任务描述——这时候它是该从第一步重新做一遍,还是知道自己已经做完了前两步,直接从第三步继续?

答案取决于一件事:任务进行到哪一步,有没有被记录成一份结构化状态,而不只是散落在一堆对话文字里。如果"已经改完类型定义了"这句话只是模型某一轮回复里的一句自然语言描述,淹没在几十条消息中间,宿主代码没办法可靠地从里面提取出"当前处于第几步"这个明确的事实。但如果这个进度被表达成一份有固定字段的任务清单——每一项任务都有明确的状态标记——宿主代码就能直接读出:第一步、第二步是 completed,第三步还没开始。

这也是这一课要解决的核心问题:第 1、2、3 课讲的都是"内容"该怎么管理——对话历史、记忆文件;这一课讲的是"进度"该怎么表达,才能让 Agent 或者宿主应用在中断之后,准确地知道任务做到哪了。

任务清单的生命周期:创建、激活、完成、删除

以 Claude Code 的任务追踪工具为例,官方文档给出了待办事项在执行过程中要经历的完整生命周期,一共四步:1

  1. 创建(Created):识别出一项任务时,把它作为待办事项加入清单,状态是 pending(待处理)。
  2. 激活(Activated):真正开始执行这项工作时,把状态改成 in_progress(进行中)。
  3. 完成(Completed):任务顺利完成时,把状态标记为 completed(已完成)。
  4. 删除(Removed):不再需要某项待办事项时,通过一次 TaskUpdate 调用把它的状态设成 deleted(已删除)来移除它。1

这四步不是自然语言里含糊的"我做完了""我在做了",而是明确的四个状态值:pending、in_progress、completed,以及表示删除的 deleted。每一次状态变化,都是通过一次明确的工具调用完成的,而不是靠模型在回复文字里随口一提。

官方文档还说明了这套机制具体是怎么体现在对话里的:在一个拥有任务追踪工具的会话里,Claude 会维护一份写下来的待办事项清单,随着工作推进更新每一项的状态,你能在消息流里看到每一次变化——它是以一次结构化的工具调用的形式出现的。1 这句话点出了关键区别:进度不是被动地"体现"在对话里,而是主动地被写成一次可以被单独识别、单独解析的工具调用,这正是"结构化状态"和"散落在文字里的进度描述"的本质差别。

检查点:让恢复不等于从头重来

有了结构化的任务清单,下一个问题是:这份清单本身存在哪里?如果它只存在于这一次会话的对话历史里,一旦这次会话真正结束(不是短暂中断,而是彻底关闭,比如第 3 课讲过的"会话一结束,窗口里的一切就没了"),这份进度记录也会跟着消失——归根到底,API 是无状态的,服务器不替你的会话在请求之间保留任何东西2

这就是检查点要解决的问题:把任务在某个时间点的状态——已完成哪些步骤、当前在哪一步、还剩哪些步骤——写成一份数据,持久保存到会话生命周期之外的地方。检查点和第 3 课讲的外部记忆用的是同一套底层手段(写文件、之后再读回来),区别在于检查点里存的不是"值得记住的知识",而是"任务进行到哪一步"这类可以直接用来恢复执行的状态。

有了检查点,可恢复性才成立:进程重启之后,Agent 不需要凭空猜测"我做到哪了",而是直接读取最近一份检查点,看到"步骤一、步骤二状态是 completed,步骤三是 in_progress",从步骤三继续,而不是把步骤一、步骤二重新做一遍。

这里的代价对比是具体的:重构任务里"改类型定义"这一步,如果本身是幂等的(重复执行结果一样),从头重来只是浪费时间;但如果某一步是"往数据库里插入一条迁移记录"这种非幂等操作,从头重来可能会插入两条重复记录,甚至污染数据。检查点省下来的不只是时间,还包括避免这类非幂等操作被意外重复执行的风险。

结构化状态,是为了让中断变得可以承受

回到本课开头的场景:进程重启之后,Agent 该从头重来,还是从中断处恢复,答案现在可以说清楚了——取决于两件事有没有做到位:任务进度有没有被表达成结构化状态(而不是散落在对话文字里),这份状态有没有被写进检查点(而不是只活在这一次会话的历史里)。两者缺一不可:只有结构化状态、没有检查点,状态照样在会话结束后消失;只有检查点、没有结构化状态,写进检查点的内容本身就是含糊的自然语言,下次读出来照样没法可靠地知道进行到哪一步。

这一课讲的是"进度怎么表达、怎么留存",下一课要讲的是另一个同样关键的问题:这些被留存下来的状态和记忆,会不会反过来成为攻击者的目标——如果攻击者能往检查点或者记忆文件里写入内容,会发生什么。

小结

  • 任务进度只有变成结构化状态,才能被宿主代码可靠地读取;散落在自然语言回复里的进度描述,无法稳定解析出"当前进行到哪一步"
  • 待办事项的完整生命周期是创建(pending)、激活(in_progress)、完成(completed)、删除(deleted)四步,每一步都通过一次明确的结构化工具调用完成,能在消息流里被观察到
  • 结构化和持久化是两件不同的事:结构化解决"能不能被程序读懂",检查点解决"进程重启或会话结束后状态还在不在"——两者缺一不可
  • 检查点把任务在某个时间点的状态写到会话生命周期之外,让恢复变成"从中断处继续",而不是"从头重来",尤其能避免非幂等操作被意外重复执行
  • 结构化状态和外部记忆一样,一旦被持久化下来,自身也会成为下一课要讨论的攻击目标——留存下来的东西越多,需要守住的边界也越多

>> 第 5 课:记忆的边界与安全

Footnotes

  1. Track todos — https://code.claude.com/docs/en/agent-sdk/todo-tracking 2 3

  2. Using the Messages API — https://platform.claude.com/docs/en/build-with-claude/working-with-messages

Exercises

01

一个 Agent 收到任务"给项目加上单元测试",拆成三个待办事项:「补充测试用例」「跑测试套件」「修复失败的用例」。按下面的时间顺序,写出每个待办事项在每个时间点应该处于生命周期的哪个状态(pending / in_progress / completed / deleted)。

Level 1:给一次任务执行标注生命周期状态
  1. 任务刚被拆解出来,三项都还没开始。
  2. Agent 开始写测试用例。
  3. 测试用例写完了,Agent 开始跑测试套件。
  4. 跑测试发现有两个用例失败(用例 A、用例 B),Agent 把「修复失败的用例」这一项细化成两个新待办事项「修复用例 A」「修复用例 B」,并开始修复用例 A。
  5. 修复用例 A 的过程中发现,用例 B 其实是测试本身写错了、根本不需要改,于是「修复用例 B」这一项被移除。
Done criteria · checked locally
02

一个团队设计了这样的执行逻辑:Agent 的任务进度只体现在它每一轮回复里的自然语言总结,比如"目前已经完成了前两步,正在处理第三步"。这份总结只存在于当前会话的消息历史里,团队没有额外写任何检查点文件。系统偶尔会因为资源限制重启进程,重启后同一个任务会被重新拉起。

Level 2:诊断一个不可恢复的任务设计

指出这个设计里的两个问题(一个跟"状态是否结构化"有关,一个跟"状态是否持久化"有关),并给出对应的修正方向。

Done criteria · checked locally

My note

Jot down thoughts, sticking points, things you didn't get. Written to this course's appendix only — the lesson file is never touched.