跳转到内容

记忆系统设计

记忆系统设计

学习目标

读完本章后,你将能够:

  • 区分短期记忆、长期记忆和状态持久化的不同用途
  • 设计安全的文件系统记忆存储方案
  • 实现基于 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:父线程 → 子线程的边
  • ThreadSpawnEdgeStatusOpen(活跃)或 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 是一种后台整合机制,用于将短期信号提升为长期记忆:

阶段行为

  1. 信号收集:从会话、工具结果、用户反馈中提取潜在记忆信号
  2. 候选评分:基于频率、多样性、时效性等维度打分
  3. 阈值过滤:仅当分数超过阈值时才晋升
  4. 人类审查:阶段摘要写入 DREAMS.md,供人类审查和调整

关键设计

  • Opt-in:默认关闭,用户显式启用
  • 调度:由记忆插件自动管理一个周期性 cron 任务
  • 双车道
    • Live dreaming:从短期 dreaming 存储 (memory/.dreams/) 晋升
    • Grounded backfill:回放历史 memory/YYYY-MM-DD.md 笔记,审查但不直接晋升
  • 可回滚: staged 的候选可独立回滚,不影响正常日记条目或召回状态

为什么需要 Dreaming?

  • 防止 MEMORY.md 被低质量、冗余信息污染
  • 将”保存所有事情”转变为”只保存真正重要的事情”
  • 提供人类审查窗口,避免 Agent 自主写入不可控

混合记忆搜索

当配置了嵌入提供商时,记忆搜索可结合两种检索方式:

  • 向量相似度(语义):找到概念相关但措辞不同的记忆
  • 关键词匹配(精确):找到包含特定术语(如 ID、代码符号)的记忆

实现要点

  • 自动检测嵌入提供商(OpenAI、Gemini、Voyage、Mistral 等)
  • 无需额外配置,检测到 API Key 后自动启用
  • 可配置 reranking 和 query expansion 进一步提升质量

Hook 驱动的持续学习

除了 Dreaming 这种后台整合机制外,另一种持续学习模式是 Hook 驱动的会话观察,其核心思路是通过工具生命周期的确定性捕获来提取可复用模式。

与 Dreaming 的差异:

维度DreamingHook 驱动持续学习
触发方式定期 cron 整合每次工具调用都捕获
触发率定时执行(如每 5 分钟)100%(确定性)
分析方式扫描短期信号文件后台 Observer Agent
输出单位MEMORY.md 条目原子化 Instinct(带置信度)
人类审查DREAMS.md 摘要审查/evolve 命令聚类后审查
作用域当前项目项目隔离 + 全局晋升

这种模式的关键优势在于全面性:Hook 100% 触发保证了不遗漏任何会话模式。关于 Instinct 模型的完整设计,详见 连续学习与 Instinct 模型


设计取舍

文件系统记忆 vs 向量数据库

维度文件系统向量数据库
语义检索差(仅全文搜索)优(语义相似度)
实现复杂度
依赖需要额外服务
可解释性高(人类可读)低(嵌入向量)
容量受限于磁盘可扩展

推荐策略:简单场景使用文件系统记忆 + 全文搜索;需要语义检索时引入向量数据库(如 Chroma、Qdrant)。

本地 SQLite vs 远程数据库

维度SQLitePostgreSQL/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.pyhermes_state.pytools/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 += 1
if 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_starton_session_endon_session_switchon_pre_compresson_memory_writeon_delegation

当前支持的插件后端:honcho、mem0、supermemory、byterover、hindsight、holographic、openviking、retaindb。同一时间只允许一个外部 provider 活跃。


补充:CrewAI 的统一记忆与 LLM 分析管线

来源:CrewAI(crewAIInc/crewAI)lib/crewai/src/crewai/memory/unified_memory.pyencoding_flow.pyrecall_flow.py,commit 77a6127

层次化 Scope 系统

CrewAI 的记忆系统使用路径式的层次化 Scope

MemoryScope("/company/team/user") # 层级:company → team → user
MemorySlice([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:查询蒸馏 + 并行多路搜索

召回管线比简单的”查询 → 返回结果”更复杂:

  1. LLM 查询蒸馏:将用户的自然语言查询蒸馏为更适合向量搜索的形式
  2. 并行多查询多 Scope 搜索:生成多个变体查询,并行搜索多个 scope
  3. 基于置信度的路由决策decide_depth):
    • synthesize:结果足够好,直接综合返回
    • explore_deeper:结果不足,使用不同的搜索策略迭代
  4. 迭代探索预算:限制最大探索次数,防止无限搜索

存储后端

  • LanceDB:基于文件的向量存储,跨进程锁(store_lock),乐观并发重试(5 次重试,0.2s 基础延迟),自动压缩,scope BTREE 索引
  • Qdrant Edge:写本地/同步中心模式,每进程独立分片,孤儿分片清理,payload 索引

后台写入队列Memory 类使用后台线程池处理写入(_submit_savedrain_writes),避免阻塞主流程。陷阱:进程退出时未 flush 的记忆可能丢失。


补充:LlamaIndex 的记忆分层架构

来源:LlamaIndex llama-index-core v0.14.22,commit f027669

LlamaIndex 的记忆系统分为两层:对话缓冲(短期)和 Memory Blocks(长期)。

对话缓冲层

类型策略适用场景
ChatMemoryBufferToken 窗口管理(max_tokens 限制)简单对话,固定上下文窗口
ChatSummaryMemoryBuffer自动摘要压缩长对话,自动压缩早期历史
VectorMemory向量检索式记忆按语义相似度检索历史对话

Memory + MemoryBlocks 架构(新版)

新版记忆系统引入 Memory 类和可插拔的 MemoryBlock

  • StaticMemoryBlock:固定不变的上下文(如用户偏好、系统指令)
  • VectorMemoryBlock:通过向量检索动态检索相关记忆
  • FactExtractionMemoryBlock:从对话中提取事实并存储

InsertMethod 模式:每个 MemoryBlock 定义了如何插入新记忆(追加、替换、向量索引),允许用户根据场景选择最合适的插入策略。

与 Agent 的集成:Agent 的 Context.store 中存储 memory 键,Agent 在每个推理步骤中自动将记忆注入到 LLM 输入中。

陷阱ChatMemoryBuffermax_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.mjsscripts/persistent-mode.mjsdocs/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清理/归属生命周期提示

边界规则

  1. 小负载内联(当调用方阈值允许时)
  2. 大负载使用 Descriptor + 简短人类可读摘要
  3. 保留归属/清理元数据,确保后续清理可审计

陷阱

  • Notepad 膨胀:如果不定期清理,Notepad 会积累过时信息,浪费上下文窗口
  • 关键信息遗漏:PreCompact Hook 只有 10 秒超时,可能来不及收集所有关键信息
  • 恢复不完整:压缩后恢复的信息可能丢失细节,导致 Agent 做出错误决策
  • Descriptor 过期:Descriptor 引用的实际文件可能被删除或修改,导致引用失效