记忆 · 跨会话记住东西
grok 的记忆系统把「这个项目的经验」跨会话持久化:markdown 文件存事实,SQLite 的 FTS5 + 向量做混合检索,/flush 把当前会话总结进盘,/dream 把多次会话合并进长期记忆。这一章拆它的存储、检索与两个反思动作。
记忆 · 跨会话记住东西
压缩解决的是「一盘对话内」的上下文,记忆解决的是「跨盘对话」的经验积累——关掉再开,grok 还记得这个项目的约定。这套系统在 crates/codegen/xai-grok-memory(实验特性,默认关闭:用 GROK_MEMORY=1 或配置 [memory] enabled = true 开启,会话里也能 /memory on;早先的 --experimental-memory 参数还留着,但已隐藏)。
存在哪:文件 + 索引
记忆以 markdown 文件为真身(人可读、可 git),旁边配一个 SQLite 索引供检索:
~/.grok/memory/
MEMORY.md # 全局精选记忆
{slug}-{hash8}/ # 每个工作区一个目录
MEMORY.md # 该工作区的精选记忆
index.sqlite # FTS5 + 向量索引
sessions/YYYY-MM-DD-{slug}-{sid8}.md # 会话日志(会随时间衰减)hash8 取 blake3 哈希的前 8 位十六进制。哈希输入优先用 git origin 远端规范化后的 org/repo,不在 git 仓库里或没有 origin 时才退回规范化后的路径——所以同一个仓库的多个 clone、worktree 共用一份记忆。
「精选记忆」是长期的、人工/dream 沉淀的;「会话日志」是每次会话 flush 出来的、会衰减的。两者分开,是这套系统的核心区分。
怎么检索:混合搜索
检索用 hybrid_search(search.rs),把两种召回融合:
- FTS5 BM25 关键词搜(永远跑),外加一条对全局/工作区精选记忆的补充查询,防会话日志把精选挤掉;
- 向量 KNN(sqlite-vec,配了 embedding 模型才有;默认不配,只走全文检索);
- 按 chunk 合并、归一到 [0,1]:两路都命中时按 0.3 × BM25 + 0.7 × 向量加权,且不低于单看关键词的分数;
- 时间衰减:精选记忆免疫,会话 chunk 按半衰期(默认 30 天)指数衰减
e^(-λ·age)——越老越不容易被召回; - 源权重 + 访问频次加成 +
min_score(默认 0.7)过滤; - MMR 多样性重排(可选),避免召回一堆近乎重复的片段。
一个 Rust 工程细节:rusqlite::Connection 不是 Sync,而 embedding 要 await。代码严格把「借用 &index 的同步阶段(FTS/向量/写库)」和「不借用的异步阶段(embed 查询)」切开,保证 &index 借用绝不跨 .await——否则 future 就不是 Send 了(后端索性每次查询都新开一个 MemoryIndex)。这是异步 Rust 里非常典型的一类约束与解法。
两个反思动作:/flush 与 /dream
记忆不是自动堆积,而是靠两个「反思」动作沉淀:
/flush —— 会话 → 盘。它是一次单独的模型 turn:取最近 20 条消息(再往前扩到最近一条用户消息,保证从 turn 边界开始),让模型写一份「这次会话值得记的东西」,写进当天的会话日志。配了 embedding 时,写之前还做语义去重——把候选 embed 一下,和已有 chunk 的余弦相似度超过 0.92 就跳过。除了手动触发,压缩前(用量到压缩阈值再留出 4000 token 余量时)会自动 flush 一次,会话空闲一段时间(默认 300 秒)也会在后台 flush;会话结束时另有一份不经 LLM 的元数据摘要落盘。
/dream —— 多会话 → 长期记忆。这是一次跨会话的整合:把若干会话日志合并进工作区的 MEMORY.md。有门槛(默认距上次至少 24 小时、攒够 5 个会话才自动跑;手动 /dream 绕过这两道门槛)。它的提示 DREAM_SYSTEM_PROMPT 很讲究:合并、解决矛盾(以当前为准)、相对日期转绝对、丢掉寒暄和「当前状态」「下一步」这类易腐信息、保留决策与理由与架构。整合完删掉已处理的日志(但跳过 5 分钟内改过的,保护并发会话)。
模型这一侧则有 memory_search、memory_get 两个工具可用,用户也能用 /remember 手动记一条。
后端抽象
值得一提:MemoryBackend 这个 trait 定义在工具 crate里(不在记忆 crate),以 Arc<dyn MemoryBackend> 挂在 SessionContext 上。这样工具、首轮注入、压缩恢复三处共用同一套配置,而记忆实现可替换(真实现走混合搜索,测试用 blake3 造确定性向量的 mock)。
另一条路:v2 记忆
新近还多了一条独立的 v2 管线:配置 [memory] mode = "v2" 后(默认仍是上面这套 legacy),记忆改放在单独的 memory-v2/ 目录树里,每个作用域分成 topics/(持续维护的主题笔记)、observations/_inbox/(新观察的收件箱)和自动生成、只读的 MEMORY.md 索引;模型直接用普通文件工具读写这些 markdown,system prompt 里也会多出一段 <memory> 说明。空闲 flush、自动 dream 这些后台流程只在 legacy 模式下跑。
和《从 LLM 到 Coding Agent》对照
那本没有专门的「跨会话记忆」章——因为它是产品级的关注点。grok 这套(文件为真身 + SQLite 混合检索 + flush/dream 两级沉淀)是一个完整的、可独立学习的「给 agent 加长期记忆」参考实现。你会发现它和「给 AI 助手配 memory」这件事的成熟做法高度一致:结构化存、混合检索、定期反思去噪。