Chapter 09

Memory 与 FTS5 跨会话搜索

陈述性记忆 vs 程序性记忆;MemGPT 的分层架构; Generative Agents 的记忆流;Hermes 怎么用 SQLite FTS5 做本地全文搜索。

本章约 6,200 字 阅读 ~25 分钟 关键词:MemGPT · Generative Agents · FTS5 · prefetch · sync_turn

上一章我们看 Skill——程序性记忆。这一章看陈述性记忆:"上次和那家做 retrieval 的创业公司 聊过什么""用户名字叫什么、喜好是什么""两周前我帮他改的那个 bug 是哪个文件的"。这种"事情"型的记忆怎么存、怎么取、怎么不爆 context window。

9.1研究脉络:从 ChatGPT memory 到 Generative Agents

Agent 记忆的研究 2023–2024 年集中爆发。三篇里程碑论文:

Foundational Paper
Generative Agents: Interactive Simulacra of Human Behavior
Park et al. · arXiv:2304.03442 · UIST 2023

Stanford 在 ChatGPT 之后第一篇大规模玩 "LLM agent + memory" 的论文。25 个 LLM agent 在虚拟小镇里互相交往,每个 agent 有:

  • Memory Stream:所有经历的事件按时间序列存。
  • Retrieval:基于 recency + importance + relevance 三轴检索。
  • Reflection:定期把低级事件总结成高级 insight 存回 stream。
  • Planning:基于 reflection 决定下一步。

这是第一次把 "store everything + retrieve on demand + reflect to compress" 系统化提出来。Hermes 的 memory 设计直接受这篇影响。

Foundational Paper
MemGPT: Towards LLMs as Operating Systems
Packer et al. · arXiv:2310.08560 · 2023

Berkeley 的论文把"OS 中的 RAM 分层架构"类比到 LLM context 管理。 Context window 像 RAM,外部 vector store 像 disk。Agent 用工具 (page_inpage_out)显式管理"什么内容在 hot context"。

MemGPT 后来开源成 Letta(2024 改名),成为最广为引用的开源 memory 框架之一。 Hermes 的 memory_provider 接口直接对应它的"core memory" + "archival memory" 概念。

Industry Practice
ChatGPT Memory Feature
OpenAI · 2024

OpenAI 给 ChatGPT 加了一个简化版的 memory:用户可以让它"记住"某些事实 (我住在北京、我喜欢 Python)。本质是一份用户级 facts list, 每次 system prompt 注入。这是 declarative memory 在消费产品里的第一次大规模登陆。

这三个工作奠定了今天 Agent memory 的标准范式:

  1. 分层:hot (context window) + warm (recent sessions) + cold (vector/full-text store)。
  2. 显式 retrieval tool:让 LLM 主动决定取什么。
  3. 定期总结:从原始事件抽象出 insight。
  4. 多检索信号:recency + importance + relevance(向量相似度 / 全文匹配)。

9.2Hermes 的 Memory 架构

Hermes 的 memory 走pluggable路线——内置一个 SQLite 后端,但允许换成 Honcho、Mem0、Hindsight、Supermemory、Holographic 等 8 个外部 provider。

核心接口在 agent/memory_provider.py:

agent/memory_provider.py:42-90
class MemoryProvider(ABC):
    """Abstract base class for memory providers."""

    @property
    @abstractmethod
    def name(self) -> str:
        """Short identifier (e.g. 'builtin', 'honcho')."""

    @abstractmethod
    def is_available(self) -> bool:
        """Configured + ready? Called at init; no network calls."""

    @abstractmethod
    def initialize(self, session_id: str, **kwargs) -> None:
        """Called once at session start."""

    def system_prompt_block(self) -> str:
        """Static block to include in system prompt (provider-side info)."""
        return ""

    def prefetch(self, query: str, *, session_id="") -> str:
        """Recall relevant context for upcoming turn.

        Called before each API call. Return formatted text to inject,
        or empty string. Should be fast — heavy work via background threads.
        """
        return ""

    def sync_turn(self, turn_messages) -> None:
        """Write after each turn completes."""

    @abstractmethod
    def get_tool_schemas(self) -> List[Dict[str, Any]]:
        """Tool schemas to expose to the LLM (e.g. memory_write).

        Empty list = context-only provider (no user-facing tools).
        """
Listing 9.1 MemoryProvider 抽象类的核心 6 个方法

这 6 个方法定义 memory 的生命周期:

方法调用时机典型实现
is_available()Agent 启动检查 env vars、依赖包
initialize()Session 开始建立 DB 连接、启动后台线程
system_prompt_block()System prompt 构建注入静态提示如"Available memories:"
prefetch(query)每次 API call 前异步检索相关记忆
sync_turn()每 turn 结束写入本轮内容
get_tool_schemas()Schema 收集暴露 memory_write 等工具

9.3MemoryManager:编排者

MemoryManager 是单一集成点,agent/memory_manager.py:

agent/memory_manager.py:1-36
"""MemoryManager — orchestrates memory providers for the agent.

Single integration point in run_agent.py. Replaces scattered
per-backend code with one manager that delegates to providers.

Only ONE external plugin provider is allowed at a time — attempting
to register a second is rejected with a warning.  This prevents
tool schema bloat and conflicting memory backends.

Usage in run_agent.py:
    self._memory_manager = MemoryManager()
    self._memory_manager.add_provider(plugin_provider)

    # System prompt
    prompt_parts.append(self._memory_manager.build_system_prompt())

    # Pre-turn
    context = self._memory_manager.prefetch_all(user_message)

    # Post-turn
    self._memory_manager.sync_all(user_msg, assistant_response)
    self._memory_manager.queue_prefetch_all(user_msg)
"""

关键设计决策:

① 一次只允许一个外部 provider

为什么?两个 provider 各自暴露 memory_write / memory_search 工具, LLM 不知道用哪个。schema 也膨胀。Hermes 强制单选——你想用 honcho 还是 mem0, 二选一。

但内置 provider(FTS5 SQLite)可以和外部 provider 并存—— FTS5 是 context-only(不暴露工具),不和外部 provider 抢工具名。

② prefetch 是异步的

prefetch_all() 在每次 API call 前调用——但 不能阻塞。 Memory 检索可能要 100ms、可能 1s。

Hermes 的做法:

def queue_prefetch_all(self, query):
    """Kick off prefetch in background thread for *next* turn."""
    for provider in self.providers:
        thread = threading.Thread(target=provider.prefetch, args=(query,))
        thread.start()
        provider._pending_prefetch = thread

def prefetch_all(self, query) -> str:
    """Wait for already-running prefetch, return concatenated context."""
    parts = []
    for provider in self.providers:
        if hasattr(provider, '_pending_prefetch'):
            provider._pending_prefetch.join(timeout=2.0)   # 上限 2s
        result = provider.prefetch(query)
        if result:
            parts.append(result)
    return "\n\n".join(parts)

Turn 结束时 queue_prefetch_all(user_msg) 把检索提前起在后台。 下次 API call 前调 prefetch_all,大概率已经跑完了,立即拿结果。 这个模式叫 speculative prefetch

设计模式 Speculative prefetch 在 OS/DB 里普遍使用(pre-page、read-ahead)。 应用到 Agent 上:用户在打字的几秒里把检索做完,下次 API call 时 0 延迟拿结果。 这是好的 UX 工程。

9.4Volatile Layer 注入

prefetch 拿到的内容怎么进 prompt?回顾第 5 章——它进 volatile 层

agent/system_prompt.py:272-280
# Volatile 层(每轮重算)
memory_block = agent.memory_manager.build_system_prompt()
if memory_block:
    volatile_parts.append(memory_block)

user_profile = _r.load_user_profile()
if user_profile:
    volatile_parts.append(user_profile)

volatile_parts.append(f"Current time: {now_iso()}\nSession: {sid}\nModel: {agent.model}")

memory_block 在 system prompt 最末尾。stable + context 部分不动,cache 命中。 memory_block 每轮可能不同(不同的 prefetch 结果),它之前的部分仍然 cache 命中。

memory_block 长什么样

# Relevant Memories

[Recent — 5 mins ago]
User mentioned working on a Hermes plugin called "redact_secrets".
The plugin uses the transform_tool_result hook.

[Long-term — 2 weeks ago]
User's preferred Python style: type hints required, line length 100.
Avoid mock-heavy tests.

[User profile]
Name: zjw
Timezone: Asia/Shanghai
Working on: agent tooling, multi-modal pipelines.

注意结构化时间标注。这是 Generative Agents 的 recency 维度的实现—— 让 LLM 知道哪些信息"新鲜"。

9.5FTS5:本地全文搜索

Hermes 内置的 memory provider 不用向量库——它用 SQLite FTS5。理由:

看 schema,hermes_state.py:

hermes_state.py (schema)
CREATE TABLE IF NOT EXISTS sessions (
    id TEXT PRIMARY KEY,
    source TEXT NOT NULL,             -- 'cli' | 'telegram' | ...
    user_id TEXT,
    model TEXT,
    model_config TEXT,                -- JSON
    system_prompt TEXT,
    parent_session_id TEXT,           -- 压缩链
    started_at REAL NOT NULL,
    ended_at REAL,
    ...
);

CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES sessions(id),
    role TEXT NOT NULL,
    content TEXT,
    tool_call_id TEXT,
    tool_calls TEXT,                  -- JSON
    tool_name TEXT,
    timestamp REAL NOT NULL,
    ...
);

-- FTS5 虚拟表:全文索引
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
    content,
    tokenize='trigram'        -- CJK-friendly
);

-- 触发器:每条 message INSERT 时同步 FTS 索引
CREATE TRIGGER IF NOT EXISTS messages_fts_insert AFTER INSERT ON messages BEGIN
    INSERT INTO messages_fts(rowid, content) VALUES (
        new.id,
        COALESCE(new.content, '') || ' ' ||
        COALESCE(new.tool_name, '') || ' ' ||
        COALESCE(new.tool_calls, '')
    );
END;

-- 类似的 UPDATE / DELETE 触发器维护一致性

FTS5 怎么用

查询 "上次和谁聊过 memory startup"

SELECT session_id, role, content, timestamp
FROM messages_fts JOIN messages USING (rowid)
WHERE messages_fts MATCH 'memory startup'
ORDER BY bm25(messages_fts)        -- 内置 BM25 相关性
LIMIT 20;

FTS5 优点:

缺点:

所以 Hermes 的策略:FTS5 是 fallback,需要语义检索时用外部 provider。 两个并存。FTS5 给 session_search 工具用(让 LLM 显式查"过去说过什么")。

9.6session_search:让 LLM 显式查历史

用户问"上次我们讨论 redis 缓存策略时你说什么"——LLM 调 session_search

session_search(
    query="redis 缓存策略",
    days_back=30,
    limit=10,
)

实际查询:

SELECT session_id, role, content, timestamp
FROM messages_fts JOIN messages USING (rowid)
WHERE messages_fts MATCH 'redis 缓存 策略'
  AND timestamp > :ts_30_days_ago
ORDER BY bm25(messages_fts), timestamp DESC
LIMIT 10;

返回的不只是 message,还有 session 上下文。Hermes 可能把整个 session 的前后几条 一起返回,让 LLM 有完整 context。

LLM 总结再注入

原始 raw match 可能是 200KB。直接塞回 messages 会爆 context。所以 Hermes 在 session_search 工具里加了一步:用 auxiliary LLM 把搜索结果概括成一段

def session_search_tool(query, days_back=30, **kw):
    raw_results = _fts5_query(query, days_back)
    if len(raw_results) <= 3:
        return _format_raw(raw_results)

    # 多于 3 条 → 用便宜小模型概括
    summary = aux_llm.chat(
        f"User searched for: {query}. Here are matching messages "
        f"from past {days_back} days:\n\n{raw_results}\n\n"
        "Summarize the relevant facts in 5 bullet points or fewer."
    )
    return json.dumps({"summary": summary, "match_count": len(raw_results)})

这是 Anthropic 2025 "context engineering" 里说的 just-in-time + summarization 的具体实现。

9.7外部 Provider:Honcho 与 Mem0

FTS5 不够智能时,外部 provider 上场。Hermes 仓库里默认带 8 个:

Provider特点定位
Honcho (plastic-labs)Dialectic 用户建模"理解用户性格" — Anthropic-style user modeling
Mem0结构化事实抽取 + 向量检索类似 ChatGPT memory,更结构化
Hindsight事件回放 + reflection类似 Generative Agents 的 reflection
SupermemorySaaS 服务(云端)跨设备同步
Holographic基于 holographic reduced representation实验性
RetainDB / OpenViking / Byterover其他社区项目各有专长

每个都是 plugins/memory/<name>/ 下的 Python 包,实现 MemoryProvider ABC。

Honcho 简介

Honcho 来自 Plastic Labs,做的是 "Theory of Mind for AI agents"—— 让 Agent 长期维护一个对每个用户的"心理模型"。它存的不是 raw messages, 而是推理出来的事实:

{
  "user_id": "alice",
  "facts": [
    "Has a 7-year-old daughter named Emma",
    "Works as a backend engineer at Stripe",
    "Prefers terse responses, dislikes hedging",
    "Has cited burnout once in past month"
  ],
  "last_interaction": "2026-05-23T18:42:00Z"
}

这些事实由 Honcho 在后台用一个 LLM 从对话里抽出来。下次 user prompt 时, 相关事实通过 prefetch() 注入到 volatile 层。

9.82026 Agent Memory 框架全景

2024–2026 Agent memory 这个领域出现了若干竞争方案。 Hermes 通过 MemoryProvider ABC 支持所有主流——但你应该理解每家的设计哲学, 选合适的接入。

Letta (formerly MemGPT)

Framework
Letta — MemGPT 的产品化继承者
letta.com · open source · 2024 改名

Letta 是 MemGPT 论文作者团队(UC Berkeley → 创业)把研究成果产品化。架构沿用 MemGPT 的 OS-like memory 思想:核心记忆(core memory)始终在 context、归档记忆(archival memory)按需 page in。

2025 年起 Letta 加入:

  • Agent persistence:每个 agent 自己有 ID、自己的内存数据库,跨重启保留。
  • Memory blocks:把 memory 切成命名块("human"、"persona"、"scratch"),agent 用工具显式编辑。
  • 多 agent 共享内存:通过共享 block 实现 agent 间状态同步。

适合:长跑 agent、需要自我管理 memory 的场景。Letta 比 Hermes 内置 memory 更结构化、更"自主"。

Mem0 — 商业向

Framework
Mem0 — Hosted Memory Layer
mem0.ai · SaaS + 开源 SDK

Mem0 走 SaaS 路线:你调它的 API,它在云端维护一份"用户 fact graph"。后台 LLM 自动抽 facts、 判断旧 fact 是否需要更新、做去重和冲突解决。

设计取舍:

  • ✓ Zero-config — 调 API 就完事,没有 schema 设计负担。
  • ✓ 自动冲突解决 — 用户说"我现在用 3"会自动覆盖旧的"用 2"。
  • ✗ 数据在 Mem0 服务器(企业敏感)。
  • ✗ 黑盒——你不知道 LLM 抽出了什么 fact,需要审计时痛苦。

Hermes 通过 plugins/memory/mem0/ 支持。适合消费者产品、不适合企业内部。

A-Mem — 学术新方向

Research
A-Mem: Agentic Memory for LLM Agents
arXiv:2502.12110 · 2025

把 memory 本身当作一个"动态知识图谱"——每条新 memory 进来时让 LLM 决定: 新建节点?合并到已有节点?建立新链接?

创新点:每条 memory 不是孤立写入,而是触发"局部图更新"。让 memory 之间形成关联, 检索时能 walking 上下游事实。

适合:研究、个人 assistant。生产级稳定性还在验证。

Anthropic Memory Tool (2026)

2026 年初 Anthropic 把 memory 也做成 API 一级 feature—— 把 ChatGPT memory 那种"记住用户偏好"做进 Claude API。 你 enable 后,Claude 自动维护 user memory,跨 conversation 持久。

对 Hermes 的影响:Anthropic 这个 memory tool 是客户端不可见的(Claude 自己管), 这意味着 Hermes 在 Claude 上跑时,会"同时有两层 memory"——Hermes 自己的 + Anthropic 服务端的。 目前 Hermes 默认禁用 Anthropic 服务端 memory,避免双写冲突。

四家对比矩阵

Hermes FTS5LettaMem0A-MemAnthropic Memory
存储位置本地本地/云SaaS本地Anthropic 云端
抽取机制FTS5 索引Agent 工具自管后台 LLM 抽 factLLM 维护图Claude 内部
语义检索BM25EmbeddingEmbedding图遍历未公开
对用户透明度完全中(黑盒)
适合个人 / 离线长跑 agent消费产品研究纯 Claude 用户
选哪个 90% 用户用 Hermes 内置 FTS5 + USER.md 就够。
数据敏感企业:内置 FTS5,禁用一切外部 memory provider。
长跑 agent / 多 agent 共享状态:Letta。
消费产品要"懂用户":Mem0。
研究项目想做创新:A-Mem 或自己实现 MemoryProvider。

未解决问题:Memory 一致性

所有上面这些方案,2026 年仍未解决"memory inconsistency"问题。看一个真实情景:

2025-09: User 跟 Agent 说 "I prefer Python."
2026-03: User 跟 Agent 说 "I'm now mostly writing Go."
2026-05: User: "Recommend a side project."

Agent 应该推荐 Go 项目还是 Python 项目?

不同 memory 系统给的答案不一样:

这是开放研究问题。本书第 13 章 13.15 节也提到这是 2026 还没解决的几大方向之一。

9.9USER.md:手动 declarative memory

除了自动 memory,Hermes 还支持手动声明~/.hermes/USER.md

# About Me

- Name: zjw
- Role: ML/Agent engineer
- Located: Beijing, China timezone
- Languages: Chinese (native), English (fluent)
- Coding preferences:
  - Python 3.11+; type hints required
  - Line length 100
  - Avoid mock-heavy tests, prefer integration tests
  - Comment density: low (let names speak)

# My Stack

- Daily editor: Cursor / Hermes Agent
- Primary language: Python, TypeScript, Go
- Cloud: GCP + Modal for ML workloads

# Things I Don't Want

- Don't preface answers with "Great question!"
- Don't end emails with corporate disclaimer
- Don't generate React class components (always functional)

这文件在 volatile 层每轮注入。LLM 立即知道你的偏好。 比 Honcho 简单,比 OpenAI 的 memory 灵活。

实践 你的 Agent 上线第一天就应该填 USER.md。 这是性价比最高的 personalization——五分钟写完,每次对话都被参考, 且你可以随时编辑、不需要"忘掉"操作。

9.10记忆的写时机:sync_turn

什么时候写记忆?每个 turn 结束时调 sync_turn():

run_agent.py (相关部分)
def run_conversation(self, ...):
    ...
    # Turn 结束
    self.memory_manager.sync_all(
        user_msg=user_message,
        assistant_response=final_response,
    )
    self.memory_manager.queue_prefetch_all(user_message)
    ...

每个 provider 自己决定怎么处理:

关键:sync_turn 不应该阻塞 Agent 主循环。重活都丢到后台线程。 Agent 接下来的 turn 不等 memory 写完。

9.11Compaction 链接 (parent_session_id)

第 3 章讲过 context compression——超过窗口时让小模型总结历史。但 压缩后原始历史保留在哪

Hermes 的设计:

  1. 触发压缩时,开一个新 sessionparent_session_id 指向旧 session。
  2. 新 session 的 messages 第一条是"前一个 session 的总结"。
  3. 旧 session 完整保留在 DB,可以 session_search 查。

这样形成压缩链

flowchart TB
  S1["session_001
4 hours"] -->|压缩| S2["session_002
4 hours, parent=001"] S2 -->|压缩| S3["session_003
4 hours, parent=002"] S3 -->|压缩| S4["session_004
now, parent=003"]:::current S4 -.->|当前 turn 只看这段| Hot["Hot context
(短, 高保真)"]:::hot S1 -.->|FTS5 搜索可跨链回溯| Arch["Archival memory
(完整历史)"]:::arch S2 -.-> Arch S3 -.-> Arch classDef current fill:#f5ede0,stroke:#8b1538,color:#8b1538 classDef hot fill:#ecf3eb,stroke:#2f5d3a,color:#2f5d3a classDef arch fill:#f0eaf1,stroke:#6b4488,color:#6b4488

这是 MemGPT 的 archival memory 思想的实现—— hot context 短而精,archival 完整但只能按需查。

9.12本章带走的

章末练习

  1. Easy 为什么 Hermes 一次只允许一个外部 memory provider?两个会怎样?
  2. Easy 写你自己的 USER.md。至少 5 条偏好。下次跑 Hermes 时观察它怎么用。
  3. Medium Speculative prefetch 在什么场景下会出错?设计一个 turn 序列让 prefetch 的内容"过时"。
  4. Medium 自己实现一个最简 MemoryProvider:把每条 user message 用 echo 写到一个 JSON 文件, prefetch 时返回最近 5 条。约 50 行代码。
  5. Hard FTS5 不懂语义。设计一个 hybrid 检索方案:先用 FTS5 取 top-50,再用 embedding 重排到 top-10。 讨论:什么场景下 hybrid 比纯向量检索强、什么时候反过来?
  6. Hard Generative Agents 论文里的 "Reflection" 机制是定期把低级事件总结成高级 insight。 Hermes 的 Curator(下一章)做的是 skill 维护——能不能把同一种机制扩展到 memory? 设计一个"Memory Curator"。