第 12 章:跨会话记忆
Compaction 让一个 session 继续,Memory 让未来 session 复用过去知识。Grok Build 的记忆系统刻意以 Markdown 为人类可读事实层,再用 SQLite/FTS/向量索引提供检索视图。
12.1 功能边界与门控
Memory 是实验性/配置门控能力,默认可用性需结合 build feature、GROK_MEMORY 和运行配置判断。不能因为 crate 存在就假设每个安装都启用。
其目标是:
- 跨 session 保存项目知识;
- 人可以直接查看和编辑;
- 搜索能在没有向量服务时降级;
- session 日志可被后台 Dream 整理;
- endpoint credentials 不越过信任范围。
12.2 Markdown 是事实层
记忆位于 ~/.grok/memory/ 下,按项目身份划分。项目身份优先由规范化 git remote(如 org/repo)推导,失败时回退到路径,并加入短 hash 防冲突。
语义布局包括:
memory/
MEMORY.md # 全局记忆
<project-identity>/
MEMORY.md # 项目长期记忆
sessions/
YYYY-MM-...md # 会话日志/候选素材2
3
4
5
6
具体命名细节可能随版本迁移,设计重点是:
- Markdown 可读、可编辑、可备份;
- SQLite 是可重建索引;
- session log 与整理后的长期 memory 分层。
12.3 为什么不只用向量数据库
纯向量库的问题:
- 人难以审计“Agent 记住了什么”;
- embedding 模型变化会影响可访问性;
- 索引损坏或服务离线时难恢复;
- 删除/修订事实不直观。
Markdown-first 结构把语义所有权留给文件,索引只负责加速发现。
12.4 索引结构
SQLite index 包含:
- chunk 表与 source metadata;
- contentless FTS5;
- index metadata/version;
- 可选 sqlite-vec
vec0向量表。
Watcher 监听 Markdown create/modify/delete,通过 ArcSwap 等结构维护 dirty set。搜索前同步 dirty 文件,避免每次全量重建。
SQLite 使用 WAL 支持读写并发。由于 rusqlite::Connection 不是 Send + Sync,MemoryBackend 为每次查询打开新 index/connection,而不是跨 async task 共享裸 connection。
12.5 Chunking
Markdown 不能任意按固定字符切割,否则标题与正文语义分离。Chunker 应尽量保留:
- 标题路径;
- 段落;
- source;
- 日期/时间范围;
- 全局、项目、session 类型;
- 可用于 temporal decay 的 metadata。
空标题、模板脚手架或缺少实质内容的 chunk 会在检索中被过滤。
12.6 检索流水线
search.rs 的整体顺序:
12.7 FTS 与 Vector 如何互补
FTS5/BM25
擅长:
- 文件名、函数名、错误码;
- 精确技术词;
- 用户已知关键词。
Vector KNN
擅长:
- 语义近似;
- 不同措辞表达同一概念;
- 用户记不住精确术语。
两路结果归一后合并。若 embedding 不可用,系统退化为 FTS-only,而不是让 Memory 整体失败。
12.8 时间衰减不是一刀切
Session 日志中的临时信息会过时,因此应用指数时间衰减;全局/项目 MEMORY.md 被视为人工或 Dream 整理的 evergreen 内容,不同等衰减。
这体现了 source-aware ranking:
final_score =
merged_relevance
× source_weight
× access_weight
× temporal_decay_if_ephemeral2
3
4
5
不是源码公式的逐字符摘录,而是排序语义。
12.9 MMR 多样性
Top-k 纯按相似度可能返回五段几乎相同的 session 日志。可选 MMR(Maximal Marginal Relevance)在相关性与结果间差异之间折中:
choose hit maximizing:
relevance_to_query - lambda * similarity_to_already_selected2
对 Agent 来说,三条互补事实通常比三条重复描述更有价值。
12.10 Embedding 边界
Embedding 请求按批次处理(源码批量大小为 32),并严格限制 credentials:
- 只有 trusted endpoint 才携带当前 session credential;
- endpoint 不匹配时丢弃 credential;
- 网络/模型失败可降级 FTS;
- embedding 向量是索引,不是持久事实。
这避免把模型 API credential 意外发送到用户配置的任意向量 endpoint。
12.11 三个注入入口
记忆进入 Agent 的主要方式:
- Memory tool:模型显式搜索;
- first-turn injection:初始查询自动召回相关内容;
- compaction recovery:压缩后重新注入,以免此前记忆被 summary 丢失。
不同入口必须去重和限量。若每轮自动注入同一大段记忆,它会反过来造成上下文压力。
12.12 Session log
会话结束或 idle flush 时,系统可把有价值的 session 内容写入按月/时间命名的 Markdown 日志。日志是 Dream 的原料,不一定代表稳定事实。
flush 由 SessionActor 的 timer/生命周期协调,确保:
- 不阻塞每个模型 token;
- shutdown 前可请求 durable flush;
- 多次 idle flush 可去重;
- feature/配置禁用时不启动无用任务。
12.13 Dream:把经历整理成长期记忆
Dream 是一个后台整理流程:
- 检查开关;
- 检查距上次 dream 的时间;
- 检查可处理 session 数量;
- 获取 dream lock;
- 读取当前项目 MEMORY 与候选 session logs;
- 在大小上限内构建模型输入;
- 让模型生成更新后的结构化 Markdown;
- 验证输出;
- 原子写回项目 MEMORY;
- 仅删除已经处理且足够旧的 session logs;
- 释放锁。
12.14 Dream 的失败策略
模型输出必须:
- 有 Markdown headings;
- 不是
NO_REPLY; - 不超过大小上限;
- 有实质内容。
在验证并成功写回前,不删除 source logs。清理还有 recency guard,避免删掉仍可能被写入的最新日志。失败时回滚/释放 lock,下一次可以安全重试。
这是典型的“先构建新快照,原子提交,再垃圾回收旧输入”。
12.15 为什么 Dream 的模型调用在 Session 层
Memory crate 负责存储、索引、搜索与 dream 数据结构;SessionActor 拥有认证、model、sampler 和运行时生命周期,因此实际模型调用由 shell 的 memory_dream.rs 协调。
这遵守了第 2 章的边界:
- Memory 定义能力;
- Session 决定何时、用哪个模型执行。
12.16 Archive
Memory 可打包为 memory.tar.gz,用于上传、诊断或重建。Archive 仍应遵守:
- 路径 traversal 防护;
- 大小上限;
- 不包含无关 credentials;
- 解包目标受控;
- Markdown 是可恢复主数据,index 可重建。
12.17 与 Prompt/Compaction 的关系
形成一个长周期闭环:
- Memory 提供跨 session 先验;
- 当前会话产生新经验;
- Compaction 维护本 session 连续性;
- session log 记录候选经验;
- Dream 把候选提炼回长期记忆。
12.18 源码阅读卡
| 目的 | 文件 |
|---|---|
| Memory 总览 | xai-grok-memory/src/lib.rs |
| 目录与项目身份 | storage.rs |
| 索引/schema | index.rs、schema.rs |
| 搜索排序 | search.rs |
| MMR | mmr.rs |
| Watcher | watcher.rs |
| Dream 数据流程 | dream.rs |
| Session Dream 调度 | memory_dream.rs |
12.19 本章结论
Grok Build 的记忆系统采用“可读事实 + 可重建索引 + 受控整理”:
Markdown 保持可审计,FTS 提供可靠基线,向量补充语义召回,source/time/MMR 控制排序,Dream 只在原子写回后清理素材。
下一章研究三种长任务机制:子代理、Goal 和 Workflow。它们看似都能“自动继续”,但控制模型完全不同。