第 9 章 文件级持久化彻底解决健忘症 · Agent架构实操九 开始测验

从上下文压缩到文件级持久化

「用 tab 不用空格」压缩后变成「用户有代码风格偏好」,换会话连这句话都没了。LLM 没有持久状态。需要一层不参与压缩、跨会话保留的存储。
⏱ 约 15 分钟 🧠 .memory/ + manifest 🔒 跨进程锁 + 原子提交 +memory capability

🎯 学习目标

学完能答
  • 为什么不选向量数据库/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 队列。

不盲信目录扫描 放进目录但未登记进 manifest 的文件、手工改坏的文件、链接逃逸的文件,都不会被当有效记忆。每次加载重新校验 manifest schema、文件名唯一/安全、realpath 仍在 .memory 内、frontmatter。
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)。

区分两份历史 第8章 snip/micro/摘要只改 request history,canonical history 不被改写。提取读后者,用户消息保持完整。但大 ToolResult 在进 canonical 前已被替换成 artifact 路径+预览,关键事实在中段必须先读 artifact。

提取器只返回完整 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
MemorySession Memory(第8章)
持久范围跨实例、跨会话当前会话
存储.memory/*.md + manifestrequest history 摘要+transcript
注入第9章context消息;第10章Prompt section替换本次请求早期历史
解决长期偏好/约束/项目知识压缩后继续当前任务

同一轮时序:beginTurn 选择 → 组装context+请求级压缩历史 → 模型工具循环(canonical持续完整追加) → 最终回答 → complete 提取 → 达阈值整理一次提交。

▶️ 交互演示:记忆生命周期

点按钮走一遍 beginTurn → 选择 → 注入 → 循环 → complete → 提取 → 整理。注意 manifest 是唯一权威,整理失败不会清空。
从 beginTurn 开始

🔑 一句话总结

manifest 唯一权威 + 跨进程锁 + fsync 原子替换 + 选择失败确定性降级 = 可审计的长期记忆

Memory 不保存「曾经发生过的一切」,只保存下个会话仍会用到的内容。P09 的强项不在「记忆质量更高」,而在「边界更可审计」。

QA 测验