记忆系统设计
记忆系统设计
学习目标
读完本章后,你将能够:
- 区分短期记忆、长期记忆和状态持久化的不同用途
- 设计安全的文件系统记忆存储方案
- 实现基于 SQLite 的状态持久化架构
- 评估不同记忆存储后端的适用场景
前置知识
- SQLite 基础
- 文件系统权限模型
核心概念
1. 记忆的分层模型
Agent 的记忆系统通常分为三个层次:
2. 短期记忆:消息历史
消息历史管理用户的所有输入,与对话上下文(rollout)分离:
特点:
- 仅记录用户输入,不包含模型回复和工具结果
- 用于命令行历史补全(上下箭头)、输入建议
- 独立于 Session,跨 Session 可用
存储格式:
- JSONL:
{"session_id": "uuid", "ts": 1234567890, "text": "..."} - 每行一个记录,便于追加和顺序读取
截断策略:
- 硬上限:配置
max_bytes - 软上限:
0.8 * max_bytes,减少频繁裁剪 - 从头部(最旧)删除,保留尾部(最新)
3. 长期记忆:文件系统记忆
长期记忆通常以文件形式存储在用户目录中:
存储结构:
~/.agent/memories/- project-a/ - context.md - preferences.json- project-b/ - notes.md- global/ - user-profile.md安全控制:
- 路径沙箱:拒绝
..、根目录、隐藏文件、符号链接 - 作用域隔离:每个项目/线程只能访问自己的记忆目录
- Token 限制:读取时限制最大 token 数(如 20K),防止超限
访问接口:
memories/list:列出目录内容,支持分页memories/read:按行读取文件,支持偏移和限制memories/search:全文搜索,支持多种匹配模式
4. 状态持久化
状态持久化存储系统级数据,通常对模型不可见:
三库拆分模式:
| 数据库 | 用途 | 访问模式 |
|---|---|---|
| State DB | 线程元数据、Agent 图、backfill 状态 | 读写频繁 |
| Logs DB | 结构化日志(tracing 事件) | 写多读少 |
| Goals DB | 线程目标管理 | 读写中等 |
为什么拆分?
- 减少锁竞争:不同数据类型的访问模式不同,拆分后互不阻塞
- 独立备份和清理:日志可以单独轮转,不影响元数据
- 故障隔离:一个库损坏不影响其他库
SQLite 配置:
- WAL 模式(Write-Ahead Logging):读写不冲突
synchronous = NORMAL:平衡安全与性能- busy timeout:避免瞬时锁竞争导致失败
设计模式详解
Agent Graph Store
多 Agent 协作场景下,需要记录线程之间的派生关系:
数据结构:
ThreadSpawnEdge:父线程 → 子线程的边ThreadSpawnEdgeStatus:Open(活跃)或Closed(已关闭)
查询语义:
list_thread_spawn_children:直接子线程list_thread_spawn_descendants:所有后代(BFS + 深度排序)- 状态过滤:若父边为 Closed,其下所有 Open 后代也不返回
用途:
- 会话恢复:恢复父线程时同时恢复所有活跃子线程
- 资源清理:关闭父线程时级联关闭子线程
- 权限传播:父线程的权限配置可传递给子线程
日志批量写入
将 tracing 事件批量写入 SQLite,避免逐条插入的性能瓶颈:
参数:
- 批次大小:如 128 条
- 刷新间隔:如 2 秒
- 分区限制:每线程最多保留 10 MiB / 1000 条
优势:
- 减少 SQLite 写入次数,降低锁竞争
- 后台任务不阻塞主线程
- 分区限制防止日志无限增长
问题与规避
记忆文件的路径遍历攻击
问题:Agent 可能被诱导读取/写入超出允许范围的路径(如 ../../../etc/passwd)。
对策:
- 严格的路径解析:拒绝包含
..的路径 - 根目录限制:禁止访问系统根目录
- 符号链接检查:拒绝跟随符号链接
- 隐藏文件过滤:禁止访问以
.开头的文件
SQLite 并发锁竞争
问题:多线程同时读写 SQLite,导致 busy 错误或性能下降。
对策:
- WAL 模式:读写互不阻塞
- 连接池:复用数据库连接,减少连接开销
- 短事务:尽快提交,减少持有锁的时间
- busy timeout:设置合理的超时时间(如 5 秒),自动重试
日志数据无限增长
问题:长期运行的 Agent 会产生大量日志,占用磁盘空间。
对策:
- 分区限制:每线程最多保留固定条数/大小
- 自动轮转:超过限制时删除最旧的记录
- 异步清理:后台任务定期清理过期日志
设计取舍
文件系统记忆 vs 向量数据库
| 维度 | 文件系统 | 向量数据库 |
|---|---|---|
| 语义检索 | 差(仅全文搜索) | 优(语义相似度) |
| 实现复杂度 | 低 | 高 |
| 依赖 | 无 | 需要额外服务 |
| 可解释性 | 高(人类可读) | 低(嵌入向量) |
| 容量 | 受限于磁盘 | 可扩展 |
推荐策略:简单场景使用文件系统记忆 + 全文搜索;需要语义检索时引入向量数据库(如 Chroma、Qdrant)。
Markdown 文件记忆(补充模式)
一种将长期记忆以纯 Markdown 文件存储在用户工作区的设计模式:
文件结构:
| 文件 | 用途 | 加载策略 |
|---|---|---|
MEMORY.md | 长期记忆(事实、偏好、决策) | 每次 DM 会话开始时注入系统提示 |
memory/YYYY-MM-DD.md | 每日笔记(运行上下文和观察) | 今天和昨天的笔记自动加载 |
DREAMS.md | 梦境日记( dreaming 审查摘要) | 人类可读,不自动注入 |
特点:
- 人类可读:用户可直接编辑记忆文件,无需专用工具
- 版本可控:Markdown 文件天然适合 Git 版本控制
- 按需加载:daily notes 不自动注入,由模型通过工具按需读取
- 截断保护:大文件自动截断并添加标记,防止上下文膨胀
与数据库记忆的对比:
| 维度 | Markdown 文件 | 数据库/向量存储 |
|---|---|---|
| 可读性 | 极高(纯文本) | 低(二进制/嵌入向量) |
| 结构化查询 | 差(仅全文) | 优(SQL/向量相似度) |
| 语义检索 | 需配合嵌入服务 | 原生支持 |
| 用户控制 | 直接编辑文件 | 通过 API/工具 |
推荐策略:将 Markdown 文件作为人类可编辑的记忆源,配合数据库/向量存储实现语义检索。记忆写入由 Agent 自动完成,人类审查和修正。
Dreaming(背景记忆整合)
Dreaming 是一种后台整合机制,用于将短期信号提升为长期记忆:
阶段行为:
- 信号收集:从会话、工具结果、用户反馈中提取潜在记忆信号
- 候选评分:基于频率、多样性、时效性等维度打分
- 阈值过滤:仅当分数超过阈值时才晋升
- 人类审查:阶段摘要写入
DREAMS.md,供人类审查和调整
关键设计:
- Opt-in:默认关闭,用户显式启用
- 调度:由记忆插件自动管理一个周期性 cron 任务
- 双车道:
- Live dreaming:从短期 dreaming 存储 (
memory/.dreams/) 晋升 - Grounded backfill:回放历史
memory/YYYY-MM-DD.md笔记,审查但不直接晋升
- Live dreaming:从短期 dreaming 存储 (
- 可回滚: staged 的候选可独立回滚,不影响正常日记条目或召回状态
为什么需要 Dreaming?
- 防止
MEMORY.md被低质量、冗余信息污染 - 将”保存所有事情”转变为”只保存真正重要的事情”
- 提供人类审查窗口,避免 Agent 自主写入不可控
混合记忆搜索
当配置了嵌入提供商时,记忆搜索可结合两种检索方式:
- 向量相似度(语义):找到概念相关但措辞不同的记忆
- 关键词匹配(精确):找到包含特定术语(如 ID、代码符号)的记忆
实现要点:
- 自动检测嵌入提供商(OpenAI、Gemini、Voyage、Mistral 等)
- 无需额外配置,检测到 API Key 后自动启用
- 可配置 reranking 和 query expansion 进一步提升质量
Hook 驱动的持续学习
除了 Dreaming 这种后台整合机制外,另一种持续学习模式是 Hook 驱动的会话观察,其核心思路是通过工具生命周期的确定性捕获来提取可复用模式。
与 Dreaming 的差异:
| 维度 | Dreaming | Hook 驱动持续学习 |
|---|---|---|
| 触发方式 | 定期 cron 整合 | 每次工具调用都捕获 |
| 触发率 | 定时执行(如每 5 分钟) | 100%(确定性) |
| 分析方式 | 扫描短期信号文件 | 后台 Observer Agent |
| 输出单位 | MEMORY.md 条目 | 原子化 Instinct(带置信度) |
| 人类审查 | DREAMS.md 摘要审查 | /evolve 命令聚类后审查 |
| 作用域 | 当前项目 | 项目隔离 + 全局晋升 |
这种模式的关键优势在于全面性:Hook 100% 触发保证了不遗漏任何会话模式。关于 Instinct 模型的完整设计,详见 连续学习与 Instinct 模型。
设计取舍
文件系统记忆 vs 向量数据库
| 维度 | 文件系统 | 向量数据库 |
|---|---|---|
| 语义检索 | 差(仅全文搜索) | 优(语义相似度) |
| 实现复杂度 | 低 | 高 |
| 依赖 | 无 | 需要额外服务 |
| 可解释性 | 高(人类可读) | 低(嵌入向量) |
| 容量 | 受限于磁盘 | 可扩展 |
推荐策略:简单场景使用文件系统记忆 + 全文搜索;需要语义检索时引入向量数据库(如 Chroma、Qdrant)。
本地 SQLite vs 远程数据库
| 维度 | SQLite | PostgreSQL/MySQL |
|---|---|---|
| 部署 | 零配置 | 需要服务器 |
| 并发 | 中等(WAL 模式) | 高 |
| 容量 | 适合 GB 级别 | TB 级别 |
| 查询能力 | 基础 | 丰富 |
| 备份 | 文件复制 | 专业工具 |
推荐策略:单机 Agent 使用 SQLite;多用户、分布式场景使用 PostgreSQL。
AutoGen 补充:状态序列化与持久化
AutoGen 使用 Pydantic BaseModel 作为所有 Agent、Team、TerminationCondition 的状态容器,将运行时状态与创建配置分离。详见 Agent 状态序列化与持久化。
参考来源
补充:Hermes Agent 的记忆搜索与用户建模
来源:Hermes Agent(NousResearch/hermes-agent)agent/memory_manager.py、hermes_state.py、tools/memory_tool.py,commit 2517917
FTS5 全文搜索 + LLM 摘要
Hermes 使用 SQLite 的 FTS5 扩展实现跨会话记忆搜索:
- 所有历史会话的对话内容索引到 FTS5 虚拟表
- 用户查询时:FTS5 检索匹配的消息 → LLM 对结果进行摘要
- 摘要结果注入到当前 turn 的上下文,作为”跨会话记忆”
设计优势:无需引入外部向量数据库,仅用 SQLite 即可实现”按关键词找到历史,按 LLM 理解提炼”的两阶段检索。适合 Agent 场景中的精确引用(文件路径、函数名、错误信息)。
Honcho 方言用户建模
Hermes 集成了 Honcho 作为用户建模后端:
- Honcho 使用方言建模(dialectic modeling)方法:通过与用户的辩证交互构建用户画像
- 不直接存储用户的原始陈述,而是提取”触发条件 → 偏好行为”的原子化规则
- 支持跨会话持久化:即使用户换了新对话,Honcho 仍能召回历史画像
与通用记忆的区别:通用记忆存储”发生了什么”,Honcho 建模”用户是谁/偏好什么”。两者的数据结构和查询模式完全不同。
记忆 Nudge 机制
Hermes 实现了定期回顾触发器(nudge):
agent._turns_since_memory += 1if agent._turns_since_memory >= agent._memory_nudge_interval: _should_review_memory = True # 触发记忆回顾 agent._turns_since_memory = 0工作原理:
- 每 N 轮对话(可配置),Agent 自动触发一次记忆回顾
- 回顾的内容:检查现有记忆是否仍然准确,是否有新的偏好需要记录
- 计数器跨 turn 持久化:gateway 模式下每次创建新 AIAgent 实例时,从历史消息中重建计数器(
_user_turn_count+_turns_since_memory = prior_turns % interval)
为什么要 nudge? 如果不主动触发,Agent 可能在数十轮对话后才想起更新记忆,或者永远不更新。Nudge 提供了确定性的回顾节奏,确保记忆不会落后于对话。
插件化记忆后端 ABC
agent/memory_provider.py 定义了 MemoryProvider 抽象基类,生命周期为:
initialize → system_prompt_block → prefetch → sync_turn → get_tool_schemas → handle_tool_call → shutdown可选钩子:on_turn_start、on_session_end、on_session_switch、on_pre_compress、on_memory_write、on_delegation
当前支持的插件后端:honcho、mem0、supermemory、byterover、hindsight、holographic、openviking、retaindb。同一时间只允许一个外部 provider 活跃。
补充:CrewAI 的统一记忆与 LLM 分析管线
来源:CrewAI(crewAIInc/crewAI)lib/crewai/src/crewai/memory/unified_memory.py、encoding_flow.py、recall_flow.py,commit 77a6127
层次化 Scope 系统
CrewAI 的记忆系统使用路径式的层次化 Scope:
MemoryScope("/company/team/user") # 层级:company → team → userMemorySlice([scope1, scope2]) # 组合多个 scope 的视图MemoryScope 支持 .bind() 方法用于检查点恢复,MemorySlice 允许 Agent 同时查询多个作用域的记忆。
复合评分机制
每条记忆通过三维加权复合评分决定重要性:
score = semantic_weight × 语义相关性 + recency_weight × 时间衰减因子(指数) + importance_weight × LLM 评估的重要性默认权重:semantic=0.5, recency=0.3, importance=0.2。时间衰减使用指数衰减函数,确保近期记忆优先被召回。
EncodingFlow:5 步批量编码管线
CrewAI 的记忆编码不是一条一条处理,而是批量流水线:
batch_embed → intra_batch_dedup → parallel_find_similar → parallel_analyze → execute_plans关键优化——A/B/C/D 分组:
- 将记忆项分为四组,根据相似度、类型等维度分类
- 相似的项批量处理,减少 LLM 调用次数
- 去重在批量内部进行,而非逐条比较
RecallFlow:查询蒸馏 + 并行多路搜索
召回管线比简单的”查询 → 返回结果”更复杂:
- LLM 查询蒸馏:将用户的自然语言查询蒸馏为更适合向量搜索的形式
- 并行多查询多 Scope 搜索:生成多个变体查询,并行搜索多个 scope
- 基于置信度的路由决策(
decide_depth):synthesize:结果足够好,直接综合返回explore_deeper:结果不足,使用不同的搜索策略迭代
- 迭代探索预算:限制最大探索次数,防止无限搜索
存储后端
- LanceDB:基于文件的向量存储,跨进程锁(
store_lock),乐观并发重试(5 次重试,0.2s 基础延迟),自动压缩,scope BTREE 索引 - Qdrant Edge:写本地/同步中心模式,每进程独立分片,孤儿分片清理,payload 索引
后台写入队列:Memory 类使用后台线程池处理写入(_submit_save、drain_writes),避免阻塞主流程。陷阱:进程退出时未 flush 的记忆可能丢失。
补充:LlamaIndex 的记忆分层架构
来源:LlamaIndex llama-index-core v0.14.22,commit f027669
LlamaIndex 的记忆系统分为两层:对话缓冲(短期)和 Memory Blocks(长期)。
对话缓冲层
| 类型 | 策略 | 适用场景 |
|---|---|---|
ChatMemoryBuffer | Token 窗口管理(max_tokens 限制) | 简单对话,固定上下文窗口 |
ChatSummaryMemoryBuffer | 自动摘要压缩 | 长对话,自动压缩早期历史 |
VectorMemory | 向量检索式记忆 | 按语义相似度检索历史对话 |
Memory + MemoryBlocks 架构(新版)
新版记忆系统引入 Memory 类和可插拔的 MemoryBlock:
StaticMemoryBlock:固定不变的上下文(如用户偏好、系统指令)VectorMemoryBlock:通过向量检索动态检索相关记忆FactExtractionMemoryBlock:从对话中提取事实并存储
InsertMethod 模式:每个 MemoryBlock 定义了如何插入新记忆(追加、替换、向量索引),允许用户根据场景选择最合适的插入策略。
与 Agent 的集成:Agent 的 Context.store 中存储 memory 键,Agent 在每个推理步骤中自动将记忆注入到 LLM 输入中。
陷阱:ChatMemoryBuffer 的 max_tokens 是硬限制,当对话超出限制时直接截断最早的消息,可能导致正在进行的推理链断裂。ChatSummaryMemoryBuffer 通过自动摘要避免了这个问题,但增加了额外的 LLM 调用成本。
补充:Goose 的 SQLite 会话持久化架构
来源:Goose(aaif-goose/goose)crates/goose/src/session/session_manager.rs,commit 1cb5cb0
Schema v13 三表模型
Goose 使用 SQLite 存储会话与消息,Schema 版本为 13:
| 表 | 用途 | 关键字段 |
|---|---|---|
| sessions | 会话元数据 | id, name, working_dir, session_type, created_at, extension_data(JSON), token_counts, provider_name, model_config(JSON), goose_mode |
| messages | 对话消息 | message_id, session_id(FK), role, content_json, created_timestamp, metadata_json |
| threads / thread_messages | 线程分组 | 迁移 10 引入,用于将多个 session 组织为 thread |
session_type 枚举:User | Scheduled | SubAgent | Hidden | Terminal | Gateway | Acp,用于区分不同来源的会话。
游标分页与全文搜索
list_sessions(cursor, limit) -> Page<Session>search_chat_history(query) -> Vec<Match>search_chat_sessions(query, cursor, limit) -> Page<SessionMatch>- 使用 SQLite 的
ROWID和索引实现游标分页,避免OFFSET在大数据量时的性能退化 - 全文搜索利用 SQLite 的 FTS 扩展,跨会话匹配用户查询
操作模式
| 操作 | 实现方式 |
|---|---|
| 创建会话 | INSERT INTO sessions + 日期格式 ID(20250529_1) |
| 添加消息 | INSERT INTO messages |
| 压缩后替换 | DELETE FROM messages WHERE session_id = ? + 批量 INSERT(replace_conversation) |
| 截断对话 | DELETE FROM messages WHERE session_id = ? AND created_timestamp < ? |
| 导出/导入 | JSON 序列化 → 文件 round-trip |
| 克隆会话 | copy_session():复制 session 行 + 所有 messages |
压缩后替换的陷阱:压缩后需要删除旧消息并重新插入所有消息。这是 DELETE + 批量 INSERT,不是逐条 UPDATE。这样做的原因是压缩可能改变消息的结构(合并、删除),逐条更新不如整体替换简单。
补充:Oh-My-ClaudeCode 的抗压缩持久化模式
来源:Oh-My-ClaudeCode(Yeachan-Heo/oh-my-claudecode)scripts/pre-compact.mjs、scripts/persistent-mode.mjs、docs/ARCHITECTURE.md,commit ed7800dd,版本 v4.14.4
上下文压缩(Context Compaction)的本质问题
LLM 上下文窗口有限,当对话过长时系统会自动压缩上下文——删除早期消息以腾出空间。这导致 Agent 丢失:
- 关键决策记录
- 正在进行的工作状态
- 用户偏好和约束
- 多 Agent 编队的进度
抗压缩持久化的核心思想:在压缩发生前,主动将关键信息写入持久化存储,压缩后自动恢复。
三层抗压缩存储
| 层级 | 存储位置 | 保留策略 | 内容 |
|---|---|---|---|
| Notepad | .omc/notepad.md | 永久保留(手动清理) | 高优先级备忘录、工作记忆、临时笔记 |
| Project Memory | .omc/project-memory.json | 跨会话持久化 | 项目级知识、架构决策、团队规范 |
| Plan Notepad | .omc/notepads/{plan-name}/ | 按 Plan 隔离 | 每个执行计划的 learnings、decisions、issues |
PreCompact Hook 的工作流程
关键设计:
notepad_write_working:写入工作记忆条目,压缩后可恢复notepad_write_priority:写入高优先级笔记,永久保留(不受自动清理影响)- Project Memory 在
PostToolUse时增量更新,在PreCompact时完整快照
控制面与数据面的 Artifact Descriptor 模式
OMC 将状态分为两类:
- 控制面(
.omc/state/):队列状态、Worker 分配、会话状态——小且频繁读写 - 数据面(
.omc/plans/、.omc/notepads/、.omc/prompts/):大持久化产物——通过 Descriptor 引用
Descriptor 结构:
| 字段 | 用途 |
|---|---|
kind | 产物类别(plan、prompt、result、trace) |
path | 持久化路径 |
contentHash? | 完整性校验 |
sizeBytes? | 负载大小(用于阈值决策) |
retention | 清理/归属生命周期提示 |
边界规则:
- 小负载内联(当调用方阈值允许时)
- 大负载使用 Descriptor + 简短人类可读摘要
- 保留归属/清理元数据,确保后续清理可审计
陷阱
- Notepad 膨胀:如果不定期清理,Notepad 会积累过时信息,浪费上下文窗口
- 关键信息遗漏:PreCompact Hook 只有 10 秒超时,可能来不及收集所有关键信息
- 恢复不完整:压缩后恢复的信息可能丢失细节,导致 Agent 做出错误决策
- Descriptor 过期:Descriptor 引用的实际文件可能被删除或修改,导致引用失效