Memory 与 FTS5 跨会话搜索
陈述性记忆 vs 程序性记忆;MemGPT 的分层架构; Generative Agents 的记忆流;Hermes 怎么用 SQLite FTS5 做本地全文搜索。
上一章我们看 Skill——程序性记忆。这一章看陈述性记忆:"上次和那家做 retrieval 的创业公司 聊过什么"、"用户名字叫什么、喜好是什么"、"两周前我帮他改的那个 bug 是哪个文件的"。这种"事情"型的记忆怎么存、怎么取、怎么不爆 context window。
9.1研究脉络:从 ChatGPT memory 到 Generative Agents
Agent 记忆的研究 2023–2024 年集中爆发。三篇里程碑论文:
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 设计直接受这篇影响。
Berkeley 的论文把"OS 中的 RAM 分层架构"类比到 LLM context 管理。
Context window 像 RAM,外部 vector store 像 disk。Agent 用工具
(page_in、page_out)显式管理"什么内容在 hot context"。
MemGPT 后来开源成 Letta(2024 改名),成为最广为引用的开源 memory 框架之一。 Hermes 的 memory_provider 接口直接对应它的"core memory" + "archival memory" 概念。
OpenAI 给 ChatGPT 加了一个简化版的 memory:用户可以让它"记住"某些事实 (我住在北京、我喜欢 Python)。本质是一份用户级 facts list, 每次 system prompt 注入。这是 declarative memory 在消费产品里的第一次大规模登陆。
这三个工作奠定了今天 Agent memory 的标准范式:
- 分层:hot (context window) + warm (recent sessions) + cold (vector/full-text store)。
- 显式 retrieval tool:让 LLM 主动决定取什么。
- 定期总结:从原始事件抽象出 insight。
- 多检索信号: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).
"""
这 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。
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。理由:
- 零依赖:FTS5 在 SQLite 3.20+ 自带。不用装 chromadb / pinecone / faiss。
- 无 embedding 费用:不要 embedding API。本地索引。
- CJK 友好:用 trigram tokenizer,中日韩文也能搜。
- 本地优先:所有 session 数据在
~/.hermes/sessions.db,完全离线可用。
看 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 优点:
- BM25:行业标准的全文相关性算法,无需训练。
- 布尔查询:
'memory AND (startup OR company)'。 - 短语:
'"prompt cache"'。 - NEAR:
'memory NEAR/5 startup'。
缺点:
- 不懂语义:搜 "memory" 找不到 "knowledge storage"——这是字面匹配的本质限制。
- 不会概括:搜不出"用户喜欢 Python",因为这是 inferred fact,原话没说。
所以 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 |
| Supermemory | SaaS 服务(云端) | 跨设备同步 |
| 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)
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 — 商业向
Mem0 走 SaaS 路线:你调它的 API,它在云端维护一份"用户 fact graph"。后台 LLM 自动抽 facts、 判断旧 fact 是否需要更新、做去重和冲突解决。
设计取舍:
- ✓ Zero-config — 调 API 就完事,没有 schema 设计负担。
- ✓ 自动冲突解决 — 用户说"我现在用 3"会自动覆盖旧的"用 2"。
- ✗ 数据在 Mem0 服务器(企业敏感)。
- ✗ 黑盒——你不知道 LLM 抽出了什么 fact,需要审计时痛苦。
Hermes 通过 plugins/memory/mem0/ 支持。适合消费者产品、不适合企业内部。
A-Mem — 学术新方向
把 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 FTS5 | Letta | Mem0 | A-Mem | Anthropic Memory | |
|---|---|---|---|---|---|
| 存储位置 | 本地 | 本地/云 | SaaS | 本地 | Anthropic 云端 |
| 抽取机制 | FTS5 索引 | Agent 工具自管 | 后台 LLM 抽 fact | LLM 维护图 | Claude 内部 |
| 语义检索 | BM25 | Embedding | Embedding | 图遍历 | 未公开 |
| 对用户透明度 | 完全 | 高 | 中(黑盒) | 中 | 低 |
| 适合 | 个人 / 离线 | 长跑 agent | 消费产品 | 研究 | 纯 Claude 用户 |
数据敏感企业:内置 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 系统给的答案不一样:
- FTS5:两条 fact 都返回,让 LLM 自己看时间戳决定。 LLM 通常能正确选最近的——但如果中间隔太久,老 fact 可能进不了 prefetch。
- Mem0:尝试自动合并/覆盖。但"取代关系"判断不一定准——用户可能两种都写。
- Letta:让 agent 主动更新 memory block,但需要 agent 注意到旧 fact 过期。
- A-Mem:图节点添加 "supersedes" 链接,理论上最优雅;实践依赖 LLM 判断准确性。
这是开放研究问题。本书第 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 灵活。
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 自己决定怎么处理:
- FTS5:把 turn 里的所有 message 写进 messages 表,触发器自动更新 FTS。
- Honcho:起后台任务用 LLM 抽事实。
- Mem0:扔进 vector store。
关键:sync_turn 不应该阻塞 Agent 主循环。重活都丢到后台线程。 Agent 接下来的 turn 不等 memory 写完。
9.11Compaction 链接 (parent_session_id)
第 3 章讲过 context compression——超过窗口时让小模型总结历史。但 压缩后原始历史保留在哪?
Hermes 的设计:
- 触发压缩时,开一个新 session,
parent_session_id指向旧 session。 - 新 session 的 messages 第一条是"前一个 session 的总结"。
- 旧 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本章带走的
- Memory 是陈述性记忆。研究脉络:Generative Agents → MemGPT → ChatGPT Memory。
- Hermes 用 MemoryProvider ABC 让 memory 后端可插拔。一次只允许一个外部 provider 防 schema 冲突。
- 6 个生命周期方法:
is_available/initialize/system_prompt_block/prefetch/sync_turn/get_tool_schemas。 - Speculative prefetch:在 turn 结束时为下一 turn 提前 background 检索。
- 检索结果进 volatile 层,不破 cache。
- 内置后端用 SQLite FTS5——零依赖、零费用、CJK 友好、BM25 排序。
- FTS5 不懂语义,所以
session_search工具在 raw match > 3 条时用小模型概括。 - 8 个内置外部 provider(Honcho 主推 user modeling、Mem0 主推结构化事实等)。
USER.md提供手动 declarative memory——五分钟搞定 personalization。- Compaction 用 parent_session_id 链:hot 短,archival 全。
章末练习
- Easy 为什么 Hermes 一次只允许一个外部 memory provider?两个会怎样?
-
Easy
写你自己的
USER.md。至少 5 条偏好。下次跑 Hermes 时观察它怎么用。 - Medium Speculative prefetch 在什么场景下会出错?设计一个 turn 序列让 prefetch 的内容"过时"。
-
Medium
自己实现一个最简 MemoryProvider:把每条 user message 用
echo写到一个 JSON 文件, prefetch 时返回最近 5 条。约 50 行代码。 - Hard FTS5 不懂语义。设计一个 hybrid 检索方案:先用 FTS5 取 top-50,再用 embedding 重排到 top-10。 讨论:什么场景下 hybrid 比纯向量检索强、什么时候反过来?
- Hard Generative Agents 论文里的 "Reflection" 机制是定期把低级事件总结成高级 insight。 Hermes 的 Curator(下一章)做的是 skill 维护——能不能把同一种机制扩展到 memory? 设计一个"Memory Curator"。