从上下文压缩到文件级持久化
🎯 学习目标
- 为什么不选向量数据库/SQL,而选文件系统?
- manifest / MEMORY.md / 记录文件 三者各是什么?谁是唯一权威?
- 四种记忆类型(user/feedback/project/reference)各记什么?
- 加载时为什么先选名称再读正文?选择失败如何降级?
- 整理(consolidation)为什么必须声明 source_names?提交顺序为什么不能先删后写?
- Memory 和 Session Memory(第8章压缩)解决的是同一个问题吗?
🧠 核心概念
1 · 为什么选文件系统 ▸
向量数据库:部署依赖重、调试不直观、小规模杀鸡用牛刀。结构化数据库:查询逻辑要自己写,LLM 读起来不自然,需额外序列化层。
文件系统:每个记忆是 .md 文件,人可直接打开检查;LLM 用自然语言读写;不需部署数据库;出问题可对照 manifest 和正文定位。格式选 Markdown + YAML frontmatter。
| 类型 | 记什么 | 举例 |
|---|---|---|
| user | 用户是谁、偏好 | 用 tab 不用空格 |
| feedback | 怎么做事、踩过坑 | 别 mock 数据库 |
| project | 当前在发生什么 | auth 重写是合规驱动 |
| reference | 东西在哪找 | pipeline bug 在 Linear INGEST |
2 · 一份集合,两份派生视图 ▸
| 文件 | 职责 |
|---|---|
| .memory/manifest.json | 当前记忆集合的唯一权威指针 |
| .memory/MEMORY.md | 由当前集合生成的有界目录,供选择器读取 |
| .memory/<name>-<id>.md | 单条记忆的 YAML frontmatter 与正文 |
文件名带唯一 ID,提交新集合不覆盖旧文件。MEMORY.md 可重建,不承担事务指针;读取集合只认 manifest。写操作还创建 .memory/.lock(proper-lockfile 跨进程互斥,stale 30 秒/update 10 秒)+ 进程内 Promise 队列。
3 · 加载:先选名称,再读正文 ▸
每轮 beginTurn(query) 读当前集合,把查询+有界目录交给选择器。模型返回记忆 name 数组(不是易漂移的文件下标)。ModelMemoryQueries 发起 side-query,请求明确 tools:[],响应必须:无工具调用、finishReason==="stop"、非空文本——finishReason==="length" 时即使文本是 "[]" 也是截断产物,不算有效响应。再 JSON.parse 整个响应(不从围栏截取子串)。
选择最多 5 条。失败/非JSON数组/名称重复/引用未知 → 降级为确定性关键词匹配(英文≥3字符 token,中文 bigram,得分相同按名稳定排序)。选中的正文包在 <relevant_memories> 注入当次请求,不追加到 canonical history。
4 · 写入:从 canonical history 提取 ▸
用户通常不说「请写入记忆」。Agent 给出最终回答后,complete() 调用 extractor 从完整 canonical history 提取(不是被压缩的 request history)。
提取器只返回完整 JSON 数组,每项必须且只能含 name/type/description/body 四字段。任意一项非法,整批都不提交。失败只记 lastError=Memory extraction failed,旧集合不变,不中断主任务。
5 · 整理:声明 source_names,一次提交 ▸
达阈值(默认10条)时整理器收到「当前集合+本轮提取」。不能只返回新数组,要明确指出替换哪些旧记录(source_names 必须非空、唯一、都属于整理候选;未列入的必须保留)。
提交顺序不能「先删旧再逐个写新」。applyConsolidation 在文件锁内:重读 manifest → 验证基础记录未变 → 合并未参与+并发新增+本轮新增替换 → fsync 新文件 → 写索引 → 最后原子替换 manifest → 成功后才清理被替换旧文件。任一步失败,旧 manifest 仍指向完整旧集合,不会「整理失败后记忆全空」。
6 · Memory vs Session Memory ▸
| Memory | Session Memory(第8章) | |
|---|---|---|
| 持久范围 | 跨实例、跨会话 | 当前会话 |
| 存储 | .memory/*.md + manifest | request history 摘要+transcript |
| 注入 | 第9章context消息;第10章Prompt section | 替换本次请求早期历史 |
| 解决 | 长期偏好/约束/项目知识 | 压缩后继续当前任务 |
同一轮时序:beginTurn 选择 → 组装context+请求级压缩历史 → 模型工具循环(canonical持续完整追加) → 最终回答 → complete 提取 → 达阈值整理一次提交。
▶️ 交互演示:记忆生命周期
🔑 一句话总结
Memory 不保存「曾经发生过的一切」,只保存下个会话仍会用到的内容。P09 的强项不在「记忆质量更高」,而在「边界更可审计」。