从零手写 Agent:Learn Claude Code s08--s09 学习笔记与薄弱点复盘(记忆管理篇)
学习对象:learnclaudecode 教程「记忆管理」模块(s08 上下文压缩 / s09 长期记忆)
学习方式:跑通 → 改坏 → 回看 三步法(延续 s01-s04 笔记)
配套可运行 demo:
s08_compaction.py、s09_memory.py
目录
- 零、全局心智模型:记忆三层金字塔
- [一、s08 上下文压缩:账本太厚,先归档再记](#一、s08 上下文压缩:账本太厚,先归档再记)
- [二、s09 长期记忆:会话会死,记忆不死](#二、s09 长期记忆:会话会死,记忆不死)
- 三、我的薄弱点清单(错题本)
- [四、s08 vs s09 横向对比速查表](#四、s08 vs s09 横向对比速查表)
- 五、自测题(附答案)
零、全局心智模型:记忆三层金字塔
s05-s09 五课其实都在解决同一个矛盾:上下文有限,任务无限。记忆问题分三层,每层一个载体:
| 层 | 载体 | 活多久 | 对应课 | 淘汰条件 |
|---|---|---|---|---|
| 工作记忆 | messages(当轮上下文) | 一次 API 调用 | s01 | 调用结束即忘 |
| 会话记忆 | 压缩摘要 + todos 计划表 | 一次会话 | s05 / s08 | 会话结束销毁 |
| 长期记忆 | 磁盘文件 | 跨会话,理论上永久 | s09 | 几乎不淘汰(但要定期整理) |
两条贯穿本模块的铁律:
- 模型无状态 :LLM 没有记忆模块,每次调用进去一串文字、出来一串文字,结束即忘。它的一切"知道"都来自这次调用塞给它的文字。所谓记忆,全是程序在搬运文字。
- 压缩和记忆是同一笔交易的两面:用「信息变少」换「上下文变轻」。s06 子代理隔离如此,s08 压缩如此,s09 的"只记结论不记流水账"也如此。
一、s08 上下文压缩:账本太厚,先归档再记
1.1 要解决的问题
messages 每轮 +2(assistant 的 tool_use + user 的 tool_result),无限增长。s04 里那个 context_watch 钩子打印的 1 → 3 → 5 → 7 就是无人管理的加法------迟早撞上模型的上下文窗口上限。
1.2 核心代码
循环开头的"水位检查"(s08 对主循环的全部改动):
python
for turn in range(1, max_turns + 1):
if len(messages) > THRESHOLD: # 水位超了(demo 里 THRESHOLD=9)
messages = compact(messages) # 压缩
...
压缩函数的三段式保留:
python
new_messages = (
[messages[0]], # ① 原始任务 ------ 永远第一位,碰都不碰
[摘要消息], # ② 历史摘要(一段话概括干过什么)
messages[cut:], # ③ 最近 KEEP_RECENT 条原文
)
成对删除(真实开发中最高频的压缩 bug 防线):
python
cut = n - KEEP_RECENT
while cut > 1 and messages[cut].get("role") == "assistant":
cut -= 1 # 切点落在 tool_use 上就往前挪,保住 tool_use/tool_result 成对
1.3 知识点
| # | 知识点 | 要点 |
|---|---|---|
| 1 | 水位检查只能放循环里 | 控制"循环自身节奏"的东西(Nag、压缩、重试)只能改主循环;注册表救不了这类需求(s02/s04 的开闭原则管的是"被调用者") |
| 2 | 三段式保留 | 原始任务 + 摘要 + 最近原文。丢了原始任务 = 目标漂移,后面全白干 |
| 3 | tool_use / tool_result 必须成对删 | 拆散一半,API 报 tool_use without corresponding tool_result。测试通过 ≠ 逻辑正确,可能只是没踩中 |
| 4 | 压缩是笔交易 | 细节永久丢失换上下文变轻。摘要质量决定一切 |
| 5 | 摘要要 LLM 写,不是规则拼 | 程序判断不了什么信息重要、什么能丢;模型能读懂语义做取舍------又是"用模型智能补程序能力" |
1.4 摘要四要素(设计简答题的答案)
| 要素 | 为什么必须有 |
|---|---|
| ① 目标复述 | 不写目标 → 模型漂移(Q2 排障场景) |
| ② 已完成进度 | 不写进度 → 模型重复劳动 |
| ③ 当前状态 / 下一步 | 压缩发生在任务中途,模型醒来第一件事是"我到哪了" |
| ④ 关键发现与约束 | "用户要用中文""X 文件是坏的"------最易漏,只活在摘要里 |
自检方法:压缩后问模型三个问题------"你要干嘛?干到哪了?有什么坑要注意?"三个都答得上,摘要才合格。
进阶认识 :s05 的 todos 本质是"结构化的进度摘要"------它放在循环外,压缩碰不到,所以比摘要更可靠。真实产品的做法:能结构化的就结构化,剩下才压成摘要。
1.5 动手实验
THRESHOLD = 9改成5→ 观察压缩频率变化;太频繁 = 丢更多细节 + 更多总结开销- 删掉 while 循环 +
KEEP_RECENT = 1→ 尝试踩中"拆散配对"的事故 - 把
fake_summarize改成只写"已完成若干步"不写具体内容 → 问模型"第 3 步跑了什么",答不上 = 亲眼看丢失
二、s09 长期记忆:会话会死,记忆不死
2.1 要解决的问题
会话一结束,messages 整个消失;压缩还会丢细节。下次会话,Agent 是个失忆的人。
2.2 核心代码
全部核心 = 两个文件操作 + 一个注入点:
python
def memory_save(text): # 追加写(append,不覆盖)
with open(MEMORY_FILE, "a", encoding="utf-8") as f:
f.write(text.strip() + "\n")
def memory_load(): # 启动时读全文
with open(MEMORY_FILE, "r", encoding="utf-8") as f:
return f.read().strip()
新会话的注入点:
python
memory_text = memory_load() # ① 程序读磁盘
query = f"[长期记忆-启动时自动注入]\n{memory_text}\n\n你好" # ② 缝进第一条消息
messages = [{"role": "user", "content": query}] # ③ 发给模型
2.3 知识点
| # | 知识点 | 要点 |
|---|---|---|
| 1 | "记得"的本质是注入 | 会话 B 模型什么都没"记得"------它只是在新会话第一条消息里看到了一段文字 。记忆在磁盘,搬运靠程序,模型只负责读到。铁证:demo 里判断"有没有记忆"用的是 "用户约定" in str(messages[0]) ------ 字符串查找,不是记忆检索 |
| 2 | 无状态是模型的本性 | LLM 没有记忆模块、没有存储区。它的一切"知道"(记忆、Python 语法、用户名字)机制完全相同:上下文里的文字 |
| 3 | 该记什么:约定/偏好、踩坑结论、项目约定 | 不该记:流水账、原始对话全文、临时报错。判断标准:这条信息下次会话还用得上吗? 记忆文件本身也占上下文,垃圾记忆 = 每个新会话背废料 |
| 4 | 当场写,不等结束 | 三个致命场景:①压缩随时触发,晚写细节先丢,模型自己都忘了谁去写?②会话可能崩/被杀,"结束"可能永远不来;③长会话结束时的"回忆"本身已被压缩污染,不可靠。笔记是当场记的,没人散会后再凭记忆补全场 |
| 5 | 记忆要可更新 | 用户改主意时旧约定必须能被替换(更新/覆盖/带时间戳定期整理)。永远 append = 矛盾记忆越攒越多。完整生命周期 = 当场写 + 定期整理 |
2.5 动手实验
- 会话 B 启动改成不注入 → 看回答变成"没找到任何记忆"------记忆的价值 = 有/没有的行为差
- 连跑两次脚本 → 第二次读到的是叠加记忆(append 不覆盖)→ 体会"记忆失效"问题
三、我的薄弱点清单(错题本)
| # | 薄弱点 | 我的错误答案 | 正确理解 | 状态 |
|---|---|---|---|---|
| 1 | 分不清"原件"和"复印件" | 把"摘要"当成最不能压掉的信息 | 摘要是压缩的产物(复印件),丢了可重生成;messages[0] 原始任务是原件,丢了永久漂移。备份原则:先分清原件和复印件 |
✅ 已理解 |
| 2 | 给模型安"记忆模块" | "会话 B 记得是因为模型有专属记忆模块" | LLM 无状态,没有记忆模块。"记得" = 程序把磁盘文件缝进上下文,模型只是读到文字。模型 = 每次都失忆的天才 | ✅ 已理解 |
| 3 | 记忆写入时机 | "会话结束时统一整理再写" | 当场写。压缩随机触发 + 会话可能崩 + 结束时的回忆已被污染。当场写 + 定期整理 才是完整生命周期 | ✅ 已理解 |
| 4 | 混淆"保留哪些消息"和"摘要里写什么" | 设计摘要内容时答成了三段式结构 | 结构问题(原始任务+摘要+最近原文)≠ 内容问题(摘要四要素:目标/进度/状态/约束) | ✅ 已理解 |
历次复发提醒 :本模块之前,"程序/模型会自己想办法"的误区已复发 5 次(s01 权限换方案、s02 未知工具查询、s03 重试降级、s05 收尾改状态、s06 整理重派)。s08/s09 测验中该误区未再出现于程序侧,但转移到了模型侧("模型有记忆模块")。统一判据不变:
先找代码。程序做的,代码里一定有那几行;找不到,就是模型"读到文字后的生成行为"。而模型的"能力",也必须能指出来自上下文里的哪段文字。
四、s08 vs s09 横向对比速查表
| 维度 | s08 压缩 | s09 记忆 |
|---|---|---|
| 解决什么 | 会话内上下文装不下 | 跨会话全部失忆 |
| 作用范围 | 一次会话内 | 跨会话 |
| 机制 | 水位检查 + 三段式替换 | 追加写文件 + 启动注入 |
| 信息代价 | 细节永久丢失(换空间) | 只留结论(放弃过程) |
| 谁写摘要/记忆 | LLM(程序判断不了重要性) | LLM(同左) |
| 什么时候触发 | 循环开头水位检查 | 当场写(不能等) |
| 与 s05 的关系 | 摘要是"临时进度" | todos 是结构化摘要,压缩碰不到 |
| 真实对应 | WorkBuddy/Claude Code 的会话压缩 | MEMORY.md / 每日学习日志 |
一次长任务里两者的配合 :干活 → 发现重要约定 → 当场 memory_save (s09)→ 上下文满了 → compact 压缩(s08,细节丢但约定已在磁盘)→ 会话结束 messages 销毁 → 新会话 memory_load 注入 → 无缝续命。
五、自测题(附答案)
1. messages 每轮 +2,第 20 轮时多少条?压缩的触发条件写在哪里?
答案 1 + 19×2 = 39 条。触发条件在 agent_loop 的循环开头:`if len(messages) > THRESHOLD`。水位检查只能放循环里------控制循环节奏的东西改不了注册表。
2. 压缩时为什么必须保留 messages0?为什么摘要反而可以丢?
答案 messages0 是原始任务(原件),丢了 = 目标漂移,且无法重生成。摘要是压缩的产物(复印件),旧消息还在时随时可重写。先分清原件和复印件。
3. API 报 tool_use without corresponding tool_result,最可能的原因?
答案 压缩切点落在 assistant(tool_use) 上,把它的 tool_result 切走了。修复:切点回退 while 循环,保证成对删除。
4. 摘要里必须写清哪四样东西?
答案 ① 目标复述(防漂移)② 已完成进度(防重复劳动)③ 当前状态/下一步 ④ 关键发现与约束(最易漏)。自检:压缩后模型能答"干嘛/到哪了/什么坑"三问。
5. 为什么摘要用 LLM 写而不是程序规则拼?
答案 程序判断不了哪些信息重要、哪些能丢;模型能读懂语义做取舍。又是"用模型智能补程序能力"。
6. 会话 B 全新 messages,它"记得"约定的机制是什么?
答案 程序启动时 memory_load() 读磁盘文件,缝进 messages0 发给模型。模型无状态、无记忆模块,它只是"读到了一段文字"。验证:demo 里用 `"用户约定" in str(messages0)` 字符串查找判断。
7. 哪些信息该进长期记忆?哪些不该?判断标准一句话?
答案 该记:用户约定/偏好、踩坑结论、项目约定。不该记:流水账、原始对话全文、临时报错。标准:这条信息下次会话还用得上吗?(记忆文件本身占上下文)
8. 为什么重要约定必须当场写进记忆,不能等会话结束?
答案 ① 压缩随时触发,晚写细节先丢,到结束时模型自己都不记得了,谁去写?② 会话可能崩溃/被杀,"结束"可能永远不来。③ 长会话结束时的回忆已被压缩污染。当场写 + 定期整理才是完整生命周期。
9. 用户改主意了(简短→详细),旧记忆还在文件里,怎么办?
答案 memory_save 要支持更新/覆盖旧条目(或带时间戳定期整理)。永远 append = 矛盾记忆越攒越多,每个新会话都读到自相矛盾的指令。
10. s05 的 todos 和 s08 的摘要是什么关系?哪个更可靠?为什么?
答案 todos 本质是"结构化的进度摘要"。它更可靠:放在循环外(模块级变量),压缩碰不到、会话内不丢;摘要则是 LLM 总结的产物,可能漏。真实产品原则:能结构化的就结构化,剩下才压摘要。
本篇收束句 :会话是会死的,但账本可以不死。模型是每次都失忆的天才,程序是那个勤快记笔记的秘书------天才负责判断,秘书负责让天才"以为"自己没失忆。