# 记忆 · 跨会话记住东西

> grok 的记忆系统把「这个项目的经验」跨会话持久化：markdown 文件存事实，SQLite 的 FTS5 + 向量做混合检索，/flush 把当前会话总结进盘，/dream 把多次会话合并进长期记忆。这一章拆它的存储、检索与两个反思动作。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/memory
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 记忆 · 跨会话记住东西

[压缩](https://daiw.org/manual/grok-build/compaction)解决的是「一盘对话内」的上下文，记忆解决的是「跨盘对话」的经验积累——关掉再开，grok 还记得这个项目的约定。这套系统在 `crates/codegen/xai-grok-memory`(实验特性，默认关闭：用 `GROK_MEMORY=1` 或配置 `[memory] enabled = true` 开启，会话里也能 `/memory on`；早先的 `--experimental-memory` 参数还留着，但已隐藏)。

## 存在哪：文件 + 索引

记忆以 **markdown 文件**为真身(人可读、可 git)，旁边配一个 SQLite 索引供检索：

```text
~/.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`)，把两种召回融合：

1. **FTS5 BM25** 关键词搜(永远跑)，外加一条对全局/工作区精选记忆的补充查询，防会话日志把精选挤掉；
2. **向量 KNN**(sqlite-vec，配了 embedding 模型才有；默认不配，只走全文检索)；
3. 按 chunk 合并、归一到 [0,1]：两路都命中时按 0.3 × BM25 + 0.7 × 向量加权，且不低于单看关键词的分数；
4. **时间衰减**：精选记忆免疫，会话 chunk 按半衰期(默认 30 天)指数衰减 `e^(-λ·age)`——越老越不容易被召回；
5. 源权重 + 访问频次加成 + `min_score`(默认 0.7)过滤；
6. **MMR 多样性重排**(可选)，避免召回一堆近乎重复的片段。

<Callout type="info">
  一个 Rust 工程细节：`rusqlite::Connection` 不是 `Sync`，而 embedding 要 `await`。代码严格把「借用 `&index` 的同步阶段(FTS/向量/写库)」和「不借用的异步阶段(embed 查询)」**切开**，保证 `&index` 借用绝不跨 `.await`——否则 future 就不是 `Send` 了(后端索性每次查询都新开一个 `MemoryIndex`)。这是异步 Rust 里非常典型的一类约束与解法。
</Callout>

## 两个反思动作：`/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」这件事的成熟做法高度一致：结构化存、混合检索、定期反思去噪。

---

上一篇：[上下文压缩](https://daiw.org/manual/grok-build/compaction) · 下一篇：[打断与防失控](https://daiw.org/manual/grok-build/interjection-doom-loop)
