Agent 上下文:Session、Checkpoint 与长期记忆
版本说明:本文保留为已发布快照。持续更新、经过源码核验和实践验证的版本,见 《Agent 工程实践》第 4 课。后续完整正文只在书籍仓库维护。
先做一个小实验:
1 | 你:记住,部署博客前必须运行 hexo g。 |
如果程序只把对话放在内存里,Agent 答不出来。旧进程退出后,messages 已经消失。即使程序把这句话写进 JSON,模型也不会自动知道;Harness 还要读取文件,并把相关内容放进本次请求。
模型不记事,程序递纸条。内存、JSON、数据库,只是纸条放在哪里。
1. 保存了,不等于模型看到了
这一课最重要的关系只有一条:
1 | 磁盘保存的数据 |
持久化回答 “ 程序重启后数据还在不在 “;Context 回答 “ 这一次生成时模型实际看到了什么 “。JSON 里可以保存 100 条消息,Harness 本次只发送最后 2 条,模型就无法使用前面 98 条。
Provider 也可以通过状态 ID 在服务端续接对话,但原理没有变化:运行时负责保存和组装输入,模型不会跨请求主动回忆。OpenAI Conversation state
2. Context 是一张有限的桌子
假设模型窗口只能放 10 页:
1 | System Prompt:1 页 |
输入预算不能直接等于模型标称窗口。工具定义、当前问题、输出和推理都要留位置。
此时 read_file 返回 8 页,不能先全部塞进 Prompt,再让模型总结。请求可能在到达模型前就超出预算,也可能把真正重要的问题和历史挤走。Tool 应先少返回:过滤、分页、限制长度,并明确说明内容被截断。
1 | { |
完整文件继续留在磁盘。模型拿到证据、截断状态和回查位置即可。
3. 裁剪历史时,不能剪断一个 User Turn
一次工具任务可能包含四条 Message:
1 | User:计算 248 × 15 |
它们是 1 个 User Turn、2 次模型 API 调用、4 条 Message。User Turn 从用户问题开始,到面向用户的 Assistant Final 才结束。
如果只保留最后两条,Tool Result 就找不到原来的 Tool Call;协议配对和任务语义同时损坏。本课因此采用最保守的策略:历史只按完整 User Turn 保留或淘汰,当前尚未结束的 active_turn 优先完整保留。
1 | Tool Call 与 Tool Result 不能分开 |
这不是所有系统的唯一做法。若一个未完成 Turn 自己已经大到放不下,就要寻找协议安全切点:只能在完整 Tool Call/Result 对之后切开,把前缀压成 Turn Prefix Summary,保留后面的原文。这种做法叫 Split Turn Compaction,第 5 课再结合开源实现展开。
4. 旧历史留纪要,稳定规则单独记
会话进行 20 轮后,早期原文通常不能全部进入 Context。程序可以把较早轮次压成 Summary,最近两轮继续保留原文:
1 | Summary:旧任务的目标、决定、已完成、待办 |
Summary 是有损的。若摘要漏掉 “ 部署前运行 hexo g“,以后无法从摘要恢复原文。跨 Session 仍然有效的项目规则必须另存为项目长期记忆。
所以两者分工不同:
- Summary 帮当前 Session 继续任务。
- 长期记忆保存跨 Session 仍然成立的规则或偏好。
本课在估算占用约 70% 时尝试压缩,只是给输出和意外增长留余量的教学启发式。代码用 UTF-8 字节数粗略估算,并不把字节当成精确 Token;真正依赖窗口上限时,应使用模型对应的 Token Counting 能力。
5. Session、Checkpoint 与长期记忆放在哪里
存储介质不是概念本身。JSON、SQLite 或数据库都可以承载下面这些职责:
| 概念 | 回答的问题 | 本课文件 |
|---|---|---|
| Context | 这次模型实际看到什么? | 每次组装的 messages |
| Session | 哪些交互属于同一条会话? | SESSION_ID |
| Checkpoint | 这条会话恢复到哪里? | .agent_state/session-<id>.json |
| 项目长期记忆 | 这个项目跨 Session 仍遵守什么? | .agent_state/project-memory.json |
| 用户长期记忆 | 这个用户跨项目仍偏好什么? | ~/.agent-memory/user-memory.json |
不同 Session 不共享摘要和完成轮次,但同一项目的 Session 可以读取同一份项目记忆。用户记忆放在用户目录,才有机会跨项目复用。
分开文件只避免物理覆盖,不会自动解决语义冲突。两条记录在磁盘上都能保留;但 Harness 读取它们并一起放进 Context 后,Model 同时看到 language=中文 和 language=English,冲突才真正出现。Harness 必须在组装 Context 时明确合并顺序,例如:
1 | 当前用户明确要求 > 项目规则 > 用户默认偏好 |
这不是行业唯一顺序,但应用必须选定一套策略,不能把冲突原样丢给模型猜。
6. 代码怎样把这些数据接回 Agent Loop
完整实现共 439 行,不适合塞进正文。可运行版本固定在 GitHub Commit:
一次用户请求只经过五步:
1 | 1. 按 SESSION_ID 读取 Checkpoint |
只有第 5 步完成后,active_turn 才会进入 state["turns"]。如果 Tool 已执行、Assistant Final 尚未生成时进程崩溃,当前 Checkpoint 仍只有以前完成的轮次,不能据此断定 Tool 没执行。
这时要分清两类恢复:
- Session / Checkpoint 保存消息和会话进度,负责恢复到哪里继续。
- Execution Ledger 不恢复对话;它记录副作用工具实际尝试了什么,让恢复程序判断成功、失败还是状态未知。
- 幂等约束保证同一个业务请求重试时不会再次产生相同副作用。
纯读取或整数乘法通常不需要 Ledger;邮件、付款和文件写入等动作才需要进一步处理 “ 可能已经执行 “ 的不确定性。
7. 运行最小实验
先运行不调用真实模型的自检:
1 | git clone https://github.com/unix2dos/agent-engineering-book.git |
预期结果:
1 | self-check passed |
在线运行需要配置支持 Tool Calling 的 OpenAI-compatible 凭据:
1 | python -m pip install openai |
先明确要求 Agent 记住项目部署规则,批准 remember 后退出程序。重新启动并询问部署要求,可以验证项目长期记忆;继续旧任务进度则验证 Session Checkpoint。二者不要混为一次测试。
主动回忆自测
读完后合上文章,再口头回答:
- JSON 已保存规则,为什么 Model 仍可能看不到?
- “ 已经持久化 “ 和 “ 已经进入 Context” 有什么区别?
- 任务进度、项目规则、用户偏好和当前问题分别放在哪里?
- 四条工具消息为什么仍只算一个 User Turn?
- 为什么历史不能简单保留最后几条 Message?
- Summary 与项目长期记忆分别适合保存什么?
- Tool Result 太大时,为什么要在进入 Context 前限流?
- 项目记忆与用户记忆使用相同 key 时,冲突应在哪里解决?
- Checkpoint 没有未完成 Turn,为什么不能断定 Tool 没执行?
- Split Turn Compaction 最重要的安全条件是什么?
展开查看简答
- 存储不会自动进入模型输入;Harness 必须读取、筛选并放入本次请求。
- 持久化保证程序以后还能读取;Context 是本次生成时 Model 实际可用的信息。
- 任务进度进 Session Checkpoint,项目规则进项目长期记忆,用户偏好进用户长期记忆,当前问题直接进入 Context。
- User Turn 按一个用户问题的完整处理过程划分,其中可以包含多次模型调用和多条工具消息。
- 可能剪断 Tool Call 与 Tool Result,也可能丢掉用户目标和调用意图。本课按完整 User Turn 裁剪。
- Summary 保存当前 Session 的旧目标和进度;项目长期记忆保存跨 Session 仍然有效的稳定规则。
- 大结果可能在请求前就超过预算,也会挤掉问题、历史和回答空间。应过滤、分页并返回截断标记和回查位置。
- Harness 组装 Context 时按显式优先级合并,不能让两个文件或 Model 自己猜。
- 当前代码只在 Assistant Final 后保存完整 Turn;Tool 可能已经执行,只是执行事实尚未写入 Checkpoint。
- 只能在协议安全点切开,不能把 Tool Call 与对应 Tool Result 分到摘要和保留原文的两侧。