Agent 上下文:Session、Checkpoint 与长期记忆

版本说明:本文保留为已发布快照。持续更新、经过源码核验和实践验证的版本,见 《Agent 工程实践》第 4 课。后续完整正文只在书籍仓库维护。

先做一个小实验:

1
2
3
4
5
6
你:记住,部署博客前必须运行 hexo g。
Agent:好的。

关闭 Python,重新启动。

你:部署前要做什么?

如果程序只把对话放在内存里,Agent 答不出来。旧进程退出后,messages 已经消失。即使程序把这句话写进 JSON,模型也不会自动知道;Harness 还要读取文件,并把相关内容放进本次请求。

模型不记事,程序递纸条。内存、JSON、数据库,只是纸条放在哪里。

1. 保存了,不等于模型看到了

这一课最重要的关系只有一条:

1
2
3
4
5
磁盘保存的数据
↓ Harness 读取、筛选
本次 Context
↓ API 请求
Model

持久化回答 “ 程序重启后数据还在不在 “;Context 回答 “ 这一次生成时模型实际看到了什么 “。JSON 里可以保存 100 条消息,Harness 本次只发送最后 2 条,模型就无法使用前面 98 条。

Provider 也可以通过状态 ID 在服务端续接对话,但原理没有变化:运行时负责保存和组装输入,模型不会跨请求主动回忆。OpenAI Conversation state

2. Context 是一张有限的桌子

假设模型窗口只能放 10 页:

1
2
3
4
5
System Prompt:1 页
当前问题:1 页
回答预留:2 页
推理预留:1 页
历史和 Tool Result:最多 5 页

输入预算不能直接等于模型标称窗口。工具定义、当前问题、输出和推理都要留位置。

此时 read_file 返回 8 页,不能先全部塞进 Prompt,再让模型总结。请求可能在到达模型前就超出预算,也可能把真正重要的问题和历史挤走。Tool 应先少返回:过滤、分页、限制长度,并明确说明内容被截断。

1
2
3
4
5
6
{
"content": "...相关片段...",
"truncated": true,
"next_offset": 4096,
"path": "logs/build.log"
}

完整文件继续留在磁盘。模型拿到证据、截断状态和回查位置即可。

3. 裁剪历史时,不能剪断一个 User Turn

一次工具任务可能包含四条 Message:

1
2
3
4
User:计算 248 × 15
Assistant:请求 multiply,tool_call_id=7
Tool:返回 3720,tool_call_id=7
Assistant:最终回答 3720

它们是 1 个 User Turn、2 次模型 API 调用、4 条 Message。User Turn 从用户问题开始,到面向用户的 Assistant Final 才结束。

如果只保留最后两条,Tool Result 就找不到原来的 Tool Call;协议配对和任务语义同时损坏。本课因此采用最保守的策略:历史只按完整 User Turn 保留或淘汰,当前尚未结束的 active_turn 优先完整保留。

1
2
3
Tool Call 与 Tool Result 不能分开

第一版实现直接把完整 User Turn 当作裁剪单位

这不是所有系统的唯一做法。若一个未完成 Turn 自己已经大到放不下,就要寻找协议安全切点:只能在完整 Tool Call/Result 对之后切开,把前缀压成 Turn Prefix Summary,保留后面的原文。这种做法叫 Split Turn Compaction,第 5 课再结合开源实现展开。

4. 旧历史留纪要,稳定规则单独记

会话进行 20 轮后,早期原文通常不能全部进入 Context。程序可以把较早轮次压成 Summary,最近两轮继续保留原文:

1
2
3
Summary:旧任务的目标、决定、已完成、待办
+ 最近完整轮次:保留原始 Message
+ active_turn:保留当前现场

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
2
3
4
5
1. 按 SESSION_ID 读取 Checkpoint
2. 读取 Summary、长期记忆和最近完整轮次
3. 与 active_turn 一起组装 Context
4. 执行 remember / forget,并把结果继续交给 Model
5. 出现 Assistant Final 后,归档完整 Turn 并写入 Checkpoint

只有第 5 步完成后,active_turn 才会进入 state["turns"]。如果 Tool 已执行、Assistant Final 尚未生成时进程崩溃,当前 Checkpoint 仍只有以前完成的轮次,不能据此断定 Tool 没执行。

这时要分清两类恢复:

  • Session / Checkpoint 保存消息和会话进度,负责恢复到哪里继续。
  • Execution Ledger 不恢复对话;它记录副作用工具实际尝试了什么,让恢复程序判断成功、失败还是状态未知。
  • 幂等约束保证同一个业务请求重试时不会再次产生相同副作用。

纯读取或整数乘法通常不需要 Ledger;邮件、付款和文件写入等动作才需要进一步处理 “ 可能已经执行 “ 的不确定性。

7. 运行最小实验

先运行不调用真实模型的自检:

1
2
3
git clone https://github.com/unix2dos/agent-engineering-book.git
cd agent-engineering-book
python examples/lesson_04_session_memory.py --self-check

预期结果:

1
self-check passed

在线运行需要配置支持 Tool Calling 的 OpenAI-compatible 凭据:

1
2
python -m pip install openai
python examples/lesson_04_session_memory.py

先明确要求 Agent 记住项目部署规则,批准 remember 后退出程序。重新启动并询问部署要求,可以验证项目长期记忆;继续旧任务进度则验证 Session Checkpoint。二者不要混为一次测试。

主动回忆自测

读完后合上文章,再口头回答:

  1. JSON 已保存规则,为什么 Model 仍可能看不到?
  2. “ 已经持久化 “ 和 “ 已经进入 Context” 有什么区别?
  3. 任务进度、项目规则、用户偏好和当前问题分别放在哪里?
  4. 四条工具消息为什么仍只算一个 User Turn?
  5. 为什么历史不能简单保留最后几条 Message?
  6. Summary 与项目长期记忆分别适合保存什么?
  7. Tool Result 太大时,为什么要在进入 Context 前限流?
  8. 项目记忆与用户记忆使用相同 key 时,冲突应在哪里解决?
  9. Checkpoint 没有未完成 Turn,为什么不能断定 Tool 没执行?
  10. Split Turn Compaction 最重要的安全条件是什么?
展开查看简答
  1. 存储不会自动进入模型输入;Harness 必须读取、筛选并放入本次请求。
  2. 持久化保证程序以后还能读取;Context 是本次生成时 Model 实际可用的信息。
  3. 任务进度进 Session Checkpoint,项目规则进项目长期记忆,用户偏好进用户长期记忆,当前问题直接进入 Context。
  4. User Turn 按一个用户问题的完整处理过程划分,其中可以包含多次模型调用和多条工具消息。
  5. 可能剪断 Tool Call 与 Tool Result,也可能丢掉用户目标和调用意图。本课按完整 User Turn 裁剪。
  6. Summary 保存当前 Session 的旧目标和进度;项目长期记忆保存跨 Session 仍然有效的稳定规则。
  7. 大结果可能在请求前就超过预算,也会挤掉问题、历史和回答空间。应过滤、分页并返回截断标记和回查位置。
  8. Harness 组装 Context 时按显式优先级合并,不能让两个文件或 Model 自己猜。
  9. 当前代码只在 Assistant Final 后保存完整 Turn;Tool 可能已经执行,只是执行事实尚未写入 Checkpoint。
  10. 只能在协议安全点切开,不能把 Tool Call 与对应 Tool Result 分到摘要和保留原文的两侧。

参考资料