数据截至 (上游 commit 6692a1b5195b)
记忆与知识:三层记忆 + 混合检索 + Deep Dream
30 秒导读: 一个 Agent 每轮都从零重建 prompt(第 2 章),那它凭什么"记得"你上周说过的话?CowAgent 的答案是把记忆分成三层——对话上下文(短期)、每日记忆
memory/YYYY-MM-DD.md(中期)、MEMORY.md长期索引——再配一套 SQLite 里的向量 + 关键词混合检索按需读回,最后靠 Deep Dream 在夜里把当天的碎片蒸馏成精炼的常青记忆。本章讲清这三层怎么协作。
1. 先建直觉:为什么要"分层"
Agent 的上下文窗口是有限的。你不可能把用户三个月来的所有对话都塞进每一轮的 prompt——既装不下,也会淹没真正重要的信息。
这和操作系统的内存 vs 磁盘是同 一个问题:快而小的空间放当前要用的,大而慢的空间放全部历史,中间用一套机制按需搬运。
CowAgent 就是这么设计的。它把记忆分成三层,每层的"新鲜度"和"容量"正好相反:
| 层 | 存在哪 | 角色类比 | 特点 |
|---|---|---|---|
| 对话上下文 | 内存 + SQLite messages 表 | CPU 寄存器 / 内存 | 最新、最全、最贵,装不下就要"下沉" |
| 每日记忆 | memory/YYYY-MM-DD.md | 当天的工作日志 | 按天累积,是"事件流水",会随时间衰减 |
| 长期索引 | MEMORY.md(工作区根) | 精炼过的备忘录 | 常青、精简(目标 ≤50 条)、每轮自动注入 prompt |
再加一个和记忆正交的东西:知识库 knowledge/,按主题(概念、实体……)组织的结构化文档,也一起进了同一个向量索引供检索。
一句话直觉:对话是"刚刚发生的",每日记忆是"今天发生的",MEMORY.md 是"值得永远记住的",知识库是"我学到的一整块领域知识"。
2. 顶层全景:一条记忆的一生
先看四层数据怎么流动。从上往下是"信息下沉"(越往下越浓缩),从下往上是"检索读回"。
┌──────────────────────────────┐
用户消息 ───────────▶ │ ① 对话上下文(短期) │
│ 内存 self.messages │
│ + SQLite messages 表 │
└───────────────┬──────────────┘
│ 上下文超长 / 被裁剪
│ (flush_memory,LLM 蒸馏)
▼
┌──────────────────────────────┐
│ ② 每日记忆(中期) │
│ memory/2026-07-14.md │
│ "按事件"记的当天流水 │
└───────────────┬──────────────┘
│ 夜里 23:5x
│ Deep Dream(LLM 蒸馏 + 去重)
▼
┌──────────────────────────────┐
│ ③ 长期索引(常青) │
│ MEMORY.md (≤50 条) │
│ + memory/dreams/ 叙事日记 │
└──────────────────────────────┘
④ 知识库 knowledge/<主题>/<页>.md ── 和 ①②③ 正交,人工/工 具维护
①②③④ 中的 .md 文件 ──▶ 全部切块 + 向量化 ──▶ SQLite 索引(chunks 表)+ 可插拔向量后端
│
memory_search / memory_get 工具 ◀┘ 按需读回(第 5 章)
MEMORY.md 每轮自动注入 prompt ◀── (第 2 章)
各部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
MemoryManager | 记忆总入口:检索、同步、下沉的调度中枢 | agent/memory/manager.py:20 |
MemoryStorage | SQLite 存储层:FTS5 关键词检索 + 委托可插拔 VectorBackend 做向量检索 | agent/memory/storage.py:111 |
TextChunker | 把 .md 按行切成带行号的块 | agent/memory/chunker.py:20 |
EmbeddingProvider | 把文本变成向量(多厂商 OpenAI 兼容) | agent/memory/embedding/provider.py:35 |
MemoryFlushManager | 下沉 + Deep Dream:对话→每日记忆→MEMORY.md | agent/memory/summarizer.py:194 |
ConversationStore | 对话历史的 SQLite 持久化(第①层的落盘) | agent/memory/conversation_store.py:364 |
KnowledgeService | 知识库文件管理 + 索引同步 | agent/knowledge/service.py:28 |
主线走一遍(高层):
- 用户发消息,进入对话上下文;每轮结束由
ConversationStore.append_messages落盘。 - 上下文快撑爆时,老的对话被
flush_memory蒸馏成一两句,追加进当天的每日记忆。 - 每天夜里,Deep Dream 读当天(或最近几天)的每日记忆,和现有
MEMORY.md一起交给 LLM,产出一份更新后的精炼MEMORY.md+ 一篇叙事式梦境日记。 - 所有这些
.md文件(加知识库)被sync切块、向量化,写进 SQLite;下一轮对话里,MEMORY.md自动进 prompt,其余的靠memory_search按需捞。
3. 核心机制一:混合检索(向量 + 关键词,失败静默降级)
它要解决的小问题
用户问"我上次说的那个 Python 教程链接呢?"——记忆库里可能有几百个块。怎么找到最相关的那几个?
两种找法各有短板:向量检索懂语义(能把"教程"和"tutorial"关联起来)但要调用嵌入 API,可能超时/失败;关键词检索(FTS5 全文索引)精确、离线、快,但只认字面。
CowAgent 的策略:两个都跑,加权融合;向量那一路一旦出问题,静默退回纯关键词,绝不让检索整个失败。
思路图示
query ──┬──▶ 向量检索 search_vector ──┐
│ (embed 失败?→ 记日志,当作空结果)
│ ├──▶ _merge_results ──▶ 时间衰减 ──▶ TopK
└──▶ 关键词检索 search_keyword ┘ (加权融合) (对 dated 文件)
(FTS5 → trigram → LIKE 三级兜底)
关键在于:关键词那一路永远会跑(manager.py:155),所以哪怕向量路彻底挂掉,keyword_results 还在,检索仍有结果。