本地 AI Agent Memory 系统建设方案

一份面向 AI Agent 开发者的本地记忆系统建设方法论。 从"为什么做"到"怎么做"到"怎么演进",覆盖完整链路。 基于生产级实践经验提炼,辅以业界方案参考。


目录

  • [第一部分:为什么需要本地 Memory](#第一部分:为什么需要本地 Memory "#%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E6%9C%AC%E5%9C%B0-memory")
  • 第二部分:核心心智模型
  • [第三部分:存储策略 --- Markdown First](#第三部分:存储策略 — Markdown First "#%E7%AC%AC%E4%B8%89%E9%83%A8%E5%88%86%E5%AD%98%E5%82%A8%E7%AD%96%E7%95%A5--markdown-first")
  • [第四部分:记忆提取 --- 最关键的环节](#第四部分:记忆提取 — 最关键的环节 "#%E7%AC%AC%E5%9B%9B%E9%83%A8%E5%88%86%E8%AE%B0%E5%BF%86%E6%8F%90%E5%8F%96--%E6%9C%80%E5%85%B3%E9%94%AE%E7%9A%84%E7%8E%AF%E8%8A%82")
  • [第五部分:检索与注入 --- Token 预算管理](#第五部分:检索与注入 — Token 预算管理 "#%E7%AC%AC%E4%BA%94%E9%83%A8%E5%88%86%E6%A3%80%E7%B4%A2%E4%B8%8E%E6%B3%A8%E5%85%A5--token-%E9%A2%84%E7%AE%97%E7%AE%A1%E7%90%86")
  • [第六部分:生命周期管理 --- Consolidation](#第六部分:生命周期管理 — Consolidation "#%E7%AC%AC%E5%85%AD%E9%83%A8%E5%88%86%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E7%AE%A1%E7%90%86--consolidation")
  • 第七部分:安全与防护
  • 第八部分:性能与可观测性
  • [第九部分:演进路线 --- 从 v1 到 v3](#第九部分:演进路线 — 从 v1 到 v3 "#%E7%AC%AC%E4%B9%9D%E9%83%A8%E5%88%86%E6%BC%94%E8%BF%9B%E8%B7%AF%E7%BA%BF--%E4%BB%8E-v1-%E5%88%B0-v3")
  • 附录

第一部分:为什么需要本地 Memory

1.1 AI Agent 的无状态困境

当前大多数 AI Agent 应用是无状态的------每次新会话,Agent 对用户和项目一无所知。用户不得不反复交代背景、偏好和约束,体验割裂。

这种无状态带来的具体痛点:

痛点 场景举例 用户感受
重复交代背景 每次都要说"我们项目用 TypeScript + React" 烦躁,觉得 Agent 很笨
遗忘历史决策 上周刚讨论过用 JWT 替换 Session,这周又从头讨论 浪费时间,信任下降
不了解用户偏好 用户喜欢简洁回复,Agent 每次都长篇大论 体验差,想换工具
无法积累项目知识 项目的架构约定、编码规范每次都要重新说明 效率低下
跨会话断裂 上个会话修了一半的 bug,新会话完全不知道进度 工作连续性差

Memory 系统的目标是让 Agent 能够:

  • 记住:用户偏好、项目背景、历史决策、编码规范
  • 召回:在新会话中自动注入相关记忆,无需用户重复交代
  • 积累:随时间整理和沉淀知识,形成越来越深的项目理解
  • 遗忘:过时的信息能被自动衰减或手动清除,避免噪音干扰

1.2 本地 vs 云端 Memory

维度 本地 Memory 云端 Memory
隐私 数据不出设备,用户完全掌控 依赖服务商安全策略
延迟 本地文件读写,毫秒级 网络往返,百毫秒级
离线 完全可用 断网不可用
成本 零额外成本 存储 + API 调用费用
同步 需额外实现多设备同步 天然多设备可用
容量 受限于本地磁盘(通常足够) 弹性扩展
可审查性 用户可直接打开文件查看/编辑 需要专门的管理界面
可移植性 文件复制即迁移 受限于服务商导出能力
版本控制 天然支持 git 追踪 需要额外实现版本历史

架构建议

graph TD A[v1: 本地 Memory
Markdown 文件] --> B[验证闭环可行性] B --> C[v1.5: 预留 MemoryProvider 接口] C --> D[按需扩展] D --> E[v2: 实现 CloudMemoryProvider
云端同步] E --> F[两条路并行] F --> G[v3: 本地 + 云端混合
按策略选择] style A fill:#e1f5ff style E fill:#fff4e1 style G fill:#e8f5e9

核心原则:本地方案先跑通,架构上通过 Provider 接口预留云端扩展能力,两条路并行不冲突。

1.3 Memory 的本质:从对话到认知

Memory 不是"存对话历史"。对话历史是原始素材,Memory 是从中提炼出的结构化认知

这个区别至关重要:

markdown 复制代码
对话历史(原始素材):
  用户: 我们项目之前用 Jest,但最近迁移到 vitest 了,以后测试都用 vitest
  助手: 好的,我会使用 vitest 来编写测试...

Memory(结构化认知):
  - 项目使用 vitest 做测试(已从 Jest 迁移)
    类型: semantic | 置信度: 0.99 | 更新: 2026-03-14

不同类型的对话产生不同类型的记忆:

对话内容 提取结果 记忆类型
"我们项目用 pnpm" 项目包管理器: pnpm 语义记忆(项目信息)
"上次那个 bug 是因为缓存没清" 缓存未清导致 xxx bug 情景记忆(问题解决)
"叫我老王就行" 称呼偏好: 老王 用户画像(profile)
"我喜欢简洁的回复风格" 回复风格: 简洁 人格偏好(personality)
"项目启动命令是 pnpm dev" 启动命令: pnpm dev 工具环境(tool)
"今天把登录模块重构完了" 完成登录模块重构 情景记忆(里程碑)

这个"从对话中提取结构化记忆"的过程,是整个系统最关键的环节------第四部分会详细展开。

1.4 业界现状与参考

当前主流的 AI Agent Memory 方案:

方案 核心思路 存储 优势 局限
OpenClaw 三层记忆 + Markdown 文件 + SQLite 索引 本地文件 + SQLite 成熟、人类可读、可 git 追踪 依赖 SQLite native 模块
Mem0 自动提取 + 用户画像 + 向量检索 向量数据库 语义检索强、SaaS 开箱即用 云端依赖、成本高
Zep 自动摘要 + 实体提取 + 混合检索 PostgreSQL + 向量 企业级、支持 RBAC 部署复杂、重量级
Letta (MemGPT) Agent 自主管理记忆 分层存储 最接近人类记忆模型 实现复杂、LLM 调用多

关键洞察

  1. 核心不是向量数据库,而是"写入-检索-注入"闭环。v1 完全可以不用向量,先把闭环跑通。
  2. Markdown 文件是被验证过的好选择。OpenClaw 用 Markdown 是对的------人类可读、可编辑、可 git 追踪、无供应商锁定。
  3. 记忆提取是最关键的环节。不是把整段对话存下来,而是从对话中提取结构化的"记忆条目"。
  4. 检索要有预算。注入 Prompt 的 memory 必须有 token 上限,否则会挤占用户真正的对话空间。

第二部分:核心心智模型

2.1 第一性原理:"写入 → 检索 → 注入"闭环

整个 Memory 系统的灵魂可以用一句话概括:

对话完成后提取记忆写入存储,下次对话前检索相关记忆注入 Prompt。

完整数据流:

flowchart TB subgraph write["写入链路 (Write Path)"] A1[对话完成] --> A2[tryExtractMemory] A2 --> A3{节流检查} A3 -->|通过| A4[LLM 提取
最近 2-3 轮对话] A3 -->|跳过| A99[结束] A4 --> A5[返回 JSON 数组] A5 --> A6[安全检查
sanitizeMemoryEntry] A6 --> A7[添加来源追溯
source/extractedAt] A7 --> A8[去重检测
倒排索引] A8 --> A9{按 type 分流} A9 -->|profile| A10[USER.md] A9 -->|personality| A11[SOUL.md] A9 -->|tool| A12[TOOLS.md] A9 -->|memory| A13[daily log +
MEMORY.md] A10 --> A14[性能监控记录] A11 --> A14 A12 --> A14 A13 --> A14 end subgraph read["读取链路 (Read Path)"] B1[用户发送新消息] --> B2[buildMemoryContext] B2 --> B3[读取 session memory
内存态,全量] B3 --> B4[读取 MEMORY.md
解析为结构化 sections] B4 --> B5[读取 daily log
今天 + 昨天] B5 --> B6[token 预算裁剪
总上限 5000 tokens] B6 --> B7[组装 memory context block] B7 --> B8[安全标签包裹] B8 --> B9[注入 system message] B9 --> B10[发送给 LLM] end style write fill:#fff4e1 style read fill:#e1f5ff

v1 完全可以不用向量数据库、不用 GraphRAG,先把这个闭环跑通。闭环跑通后再优化每个环节。

2.2 三层记忆模型

借鉴认知科学的记忆分层(工作记忆 → 短期记忆 → 长期记忆),设计三层存储:

graph LR subgraph session["Session Memory
(会话记忆)"] S1[当前目标
约束
中间决策] end subgraph workspace["Workspace Memory
(工作区记忆)"] W1[语义记忆
用户偏好
项目信息
编码规范] W2[情景记忆
重要决策
里程碑] end subgraph daily["Daily Memory
(每日记忆)"] D1[今天活动
昨天活动] end session -->|会话结束
落盘| workspace session -->|每轮写入| daily daily -->|高价值条目
晋升| workspace style session fill:#ffe1e1 style workspace fill:#e1f5ff style daily fill:#fff4e1
层级 作用域 生命周期 存储方式 注入策略 典型内容
Session Memory 当前会话 会话结束即落盘 内存(运行时状态) 全量注入(体量小) 当前目标、约束、中间决策
Workspace Memory 当前工作区 长期持久 MEMORY.md 文件 按 token 预算裁剪 项目信息、用户偏好、编码规范
Daily Memory 按日期 30 天过期 memory/YYYY-MM-DD.md 今天 + 昨天最近 N 条 每日活动记录、临时笔记

Session Memory(会话记忆)

当前会话中积累的临时认知。生命周期跟随会话,会话结束时落盘到文件。

css 复制代码
Session Memory 数据结构:
{
  sessionId: "ses_abc123",
  entries: [
    { type: "goal",       content: "重构登录模块",        addedAt: "14:30" },
    { type: "constraint", content: "不能改变现有 API 接口", addedAt: "14:31" },
    { type: "decision",   content: "使用 OAuth2 + PKCE",  addedAt: "14:45" },
    { type: "pending",    content: "需要确认 token 过期策略", addedAt: "15:00" },
  ],
  maxEntries: 20,        // 超过时按 LRU 淘汰
  maxTokens: 500,        // 注入预算
}

淘汰策略 :当条目超过上限时,按类型优先级淘汰:pending > decision > constraint > goal(goal 最不容易被淘汰)。

晋升机制:同一事实在 3+ 个不同 session 中出现时,自动晋升到 Workspace Memory。

Workspace Memory(工作区记忆)

经过验证的长期知识,存储在 MEMORY.md 文件中。分为两大类:

语义记忆(Semantic Memory)------稳定的事实和知识:

子类别 示例 优先级
用户偏好 "偏好函数式风格,避免 class" 最高
项目信息 "项目使用 TypeScript + React 18"
编码规范 "统一使用 ESLint flat config"
技术栈 "数据库用 PostgreSQL 15"
常见问题 "热更新失败时需要清除 .cache 目录"

情景记忆(Episodic Memory)------发生过的重要事件:

子类别 示例 优先级
重要决策 "决定将认证从 Session 迁移到 JWT"
里程碑 "v2.0 发布,包含新的权限系统"
问题解决 "生产环境 OOM 是因为未关闭数据库连接"

Daily Memory(每日记忆)

每日活动日志,记录当天发生的事情。作用:

  1. 短期回溯:Agent 可以知道"昨天我们做了什么"
  2. 晋升候选:高价值条目可以晋升到 MEMORY.md
  3. 审计追溯:记录记忆变更历史(冲突更新、删除等)

2.3 Bootstrap 文件体系

除了动态记忆,Agent 还需要一组静态身份文件来定义"我是谁、用户是谁、怎么行动"。这套文件体系借鉴了 OpenClaw 的 7 文件设计,每个文件一个职责:

文件 职责 更新频率 典型大小
IDENTITY.md Agent 身份定义(名字、类型、风格) 极少变更 < 200 tokens
USER.md 用户画像(基本信息 + 自动提取的偏好) 自动 + 手动 < 200 tokens
SOUL.md 人格与行为边界(回复风格、交互规则) 极少变更 < 500 tokens
AGENTS.md 工作区操作指南(Memory 约定、安全规则) 偶尔变更 < 800 tokens
TOOLS.md 工具/环境笔记(命令、路径、配置) 自动 + 手动 < 800 tokens
HEARTBEAT.md 定期任务清单(清理、检查、整理) 极少变更 < 200 tokens
MEMORY.md 长期记忆(语义 + 情景) 频繁自动更新 < 2000 tokens

为什么要分成 7 个文件而不是一个大文件?

  1. 职责清晰:每个文件有明确的边界,不会混杂
  2. 独立更新USER.md 可以频繁更新而不影响 IDENTITY.md
  3. 选择性注入:不同渠道可以选择注入哪些文件(如外部渠道不注入 USER.md
  4. 用户可编辑:用户可以只编辑自己关心的文件
  5. Token 预算可控:每个文件有独立的 token 预算

Bootstrap 文件的生命周期

ini 复制代码
应用首次启动
  → 检测 config_dir 是否存在
  → 不存在:创建目录 + 从模板初始化 7 个文件
  → 已存在:检查版本号,按需升级模板

会话创建
  → 加载所有 Bootstrap 文件(静态注入)
  → 计算每个文件的内容 hash
  → 缓存到内存

会话进行中
  → 每轮发送消息前,检查文件 hash 是否变更
  → 有变更:重新加载变更的文件(热更新)
  → 无变更:使用缓存

对话完成后
  → 提取结果中 type=profile 的条目 → 更新 USER.md
  → 提取结果中 type=personality 的条目 → 更新 SOUL.md
  → 提取结果中 type=tool 的条目 → 更新 TOOLS.md

2.4 认知科学视角:为什么这样分层

这套分层设计并非随意拍脑袋,而是对应了认知科学中的记忆模型:

graph TB subgraph cognitive["认知科学记忆模型"] C1[工作记忆
Working Memory] C2[短期记忆
Short-term Memory] C3[长期记忆 - 语义
Semantic LTM] C4[长期记忆 - 情景
Episodic LTM] C5[自我认知
Self-awareness] C6[他人模型
Theory of Mind] C7[程序性记忆
Procedural Memory] end subgraph system["Memory 系统设计"] S1[Session Memory] S2[Daily Memory] S3[MEMORY.md
语义记忆 section] S4[MEMORY.md
情景记忆 section] S5[IDENTITY.md
SOUL.md] S6[USER.md] S7[TOOLS.md
AGENTS.md] end C1 -.对应.-> S1 C2 -.对应.-> S2 C3 -.对应.-> S3 C4 -.对应.-> S4 C5 -.对应.-> S5 C6 -.对应.-> S6 C7 -.对应.-> S7 style cognitive fill:#e8f5e9 style system fill:#e1f5ff
认知科学概念 对应设计 说明
工作记忆(Working Memory) Session Memory 当前任务的临时存储,容量有限,用完即释放
短期记忆(Short-term Memory) Daily Memory 近期事件的缓冲区,会自然衰减
长期记忆 - 语义(Semantic) MEMORY.md 语义记忆 section 稳定的知识和事实
长期记忆 - 情景(Episodic) MEMORY.md 情景记忆 section 具体事件的记忆
自我认知(Self-awareness) IDENTITY.md + SOUL.md Agent 对自身身份和行为边界的认知
他人模型(Theory of Mind) USER.md Agent 对用户的理解和建模
程序性记忆(Procedural) TOOLS.md + AGENTS.md 如何操作、如何行动的知识

第三部分:存储策略 --- Markdown First

3.1 为什么 Markdown 是 v1 最优解

优势 详细说明
人类可读 用户可以直接打开查看、编辑、审查 Agent 的记忆,不需要专门的管理工具
零依赖 不需要数据库、不需要额外服务、不需要 native 模块编译
可版本控制 天然支持 git 追踪,记忆变更有迹可循,可以 diff、blame、revert
无供应商锁定 纯文本文件,迁移零成本,任何编辑器都能打开
LLM 友好 Markdown 是 LLM 最擅长读写的格式,解析和生成都很自然
调试友好 出问题时直接打开文件就能看到 Agent 记住了什么,不需要查数据库
备份简单 复制文件夹即完成备份,不需要数据库导出

常见质疑与回应

"Markdown 文件性能不够怎么办?"

v1 阶段,MEMORY.md 通常不超过几 KB,daily log 也很小。直接读文件解析的性能完全足够(< 10ms)。当文件增长到性能瓶颈时(通常 50KB+),再引入索引层------但 Markdown 仍然是真相源。

"没有数据库怎么做复杂查询?"

v1 不需要复杂查询。按 section 解析 + 关键词匹配 + token 预算裁剪就够了。v2 需要语义检索时,加 IndexedDB/SQLite 做索引,Markdown 文件不变。

"并发写入怎么办?"

通过写入队列串行化 + 原子写入(write-then-rename)解决。详见 3.5 节。

3.2 目录结构设计

yaml 复制代码
{config_dir}/
├── .gitignore                     # 忽略临时文件
├── .version                       # Bootstrap 模板版本号
├── IDENTITY.md                    # Agent 身份定义
├── USER.md                        # 用户画像
├── SOUL.md                        # 人格与行为边界
├── AGENTS.md                      # 工作区操作指南
├── TOOLS.md                       # 工具/环境笔记
├── HEARTBEAT.md                   # 定期任务清单
├── MEMORY.md                      # 长期记忆(语义 + 情景)
├── memory/                        # 每日活动日志
│   ├── 2026-03-15.md
│   ├── 2026-03-14.md
│   └── ...
└── sessions/                      # 会话级记忆落盘
    ├── ses_abc123.memory.md
    └── ...

config_dir 的选择

方案 路径示例 优点 缺点
用户配置目录 ~/.config/{app}/ 统一管理,不污染项目 不跟随项目走
项目目录 {project}/.{app}/ 跟随项目,可 git 追踪 每个项目独立,不共享
混合方案 全局 ~/.config/ + 项目级覆盖 兼顾两者 实现复杂度高

建议 :v1 使用用户配置目录(如 ~/.config/{app}/),统一管理所有记忆。如果需要项目级隔离,v2 再加项目级覆盖层。

3.3 文件格式约定

MEMORY.md --- 两级结构

markdown 复制代码
## 语义记忆

### 用户偏好
- 偏好使用函数式风格,避免 class <!-- confidence: 0.95 | updated: 2026-03-12 | source: ses_abc -->
- 喜欢简洁的回复,不要长篇大论 <!-- confidence: 0.90 | updated: 2026-03-13 | source: ses_def -->

### 项目信息
- 项目使用 TypeScript + React 18 <!-- confidence: 0.99 | updated: 2026-03-10 | source: ses_ghi -->
- 数据库使用 PostgreSQL 15 <!-- confidence: 0.95 | updated: 2026-03-11 | source: ses_jkl -->

### 编码规范
- 统一使用 ESLint flat config <!-- confidence: 0.95 | updated: 2026-03-14 | source: ses_mno -->
- 组件文件使用 PascalCase 命名 <!-- confidence: 0.85 | updated: 2026-03-12 | source: ses_pqr -->

### 常见问题
- 热更新失败时需要清除 .cache 目录 <!-- confidence: 0.80 | updated: 2026-03-09 | source: ses_stu -->

## 情景记忆

### 重要决策
- 决定将认证模块从 Session 迁移到 JWT <!-- confidence: 0.90 | updated: 2026-03-10 | source: ses_vwx -->
- 选择 Zustand 替代 Redux 做状态管理 <!-- confidence: 0.95 | updated: 2026-03-08 | source: ses_yza -->

### 里程碑
- v2.0 发布,包含新的权限系统 <!-- confidence: 1.00 | updated: 2026-03-05 | source: ses_bcd -->

格式设计要点

  1. HTML 注释存元数据<!-- key: value | key: value --> 格式,LLM 读取时自然忽略,解析器可以提取
  2. 两级标题组织 :一级 ## 为认知类型(语义/情景),二级 ### 为主题分类
  3. 每条记忆一行 :以 - 开头,便于解析和去重
  4. 宽进严出:解析时容忍格式不规范(如缺少元数据注释),写入时严格遵循格式

Daily Log --- 按时间 + 会话组织

markdown 复制代码
## 14:30 | ses_abc123

- 用户讨论了登录模块重构方案
- 决定使用 OAuth2 + PKCE 流程
- 需要调研 PKCE 在移动端的兼容性

## 16:15 | ses_def456

- 修复了 token 刷新的竞态条件
- [冲突更新] 旧: 项目使用 Jest 做测试 → 新: 项目使用 vitest 做测试

## 18:00 | ses_ghi789

- 完成了权限模块的单元测试(覆盖率 85%)

USER.md --- 手动 + 自动混合

markdown 复制代码
# 用户画像

## 基本信息
- 名字:(手动填写或自动提取)
- 称呼偏好:(如何称呼用户)
- 时区:Asia/Shanghai
- 语言偏好:中文

## 自动提取
<!-- 由 memory-extractor 自动维护,每条带时间戳和来源 -->
- 角色:全栈开发工程师 <!-- extracted: 2026-03-12, source: ses_abc -->
- 技术栈偏好:TypeScript + React <!-- extracted: 2026-03-12, source: ses_abc -->
- 沟通风格:直接、不喜欢废话 <!-- extracted: 2026-03-13, source: ses_def -->
- 常用 IDE:VS Code <!-- extracted: 2026-03-14, source: ses_ghi -->

## 备注
<!-- 用户可手动添加任何补充信息 -->

3.4 Token 估算

由于 Memory 内容最终要注入 LLM Prompt,需要准确估算 token 数量。中英文混合场景下的估算公式:

javascript 复制代码
function estimateTokens(text) {
  if (!text) return 0
  let tokens = 0
  for (const char of text) {
    if (/[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]/.test(char)) {
      tokens += 1.5  // 中文字符约 1.5 tokens
    } else {
      tokens += 0.25 // 英文字符约 0.25 tokens(≈ 4 字符/token)
    }
  }
  return Math.ceil(tokens)
}

截断策略 :当内容超出 token 预算时,使用二分查找定位截断点,确保不会在 Markdown 语法中间截断(如不截断在 <!-- --> 注释中间)。

3.5 原子写入与并发安全

文件写入必须保证原子性,避免写入中途崩溃导致数据损坏:

markdown 复制代码
写入流程:
  1. 将内容写入临时文件 {target}.tmp.{random}
  2. 调用 fsync 确保数据落盘
  3. rename 临时文件为目标文件(原子操作)
  4. 清理可能残留的旧临时文件

并发控制:所有写入操作通过队列串行化:

javascript 复制代码
class MemoryIO {
  constructor() {
    this._queue = Promise.resolve()
  }

  async _enqueue(fn) {
    // 所有写入操作排队执行,避免并发冲突
    this._queue = this._queue.then(fn).catch(err => {
      console.error('[MemoryIO] Write error:', err)
    })
    return this._queue
  }

  async writeFile(path, content) {
    return this._enqueue(async () => {
      const tmpPath = `${path}.tmp.${Date.now()}`
      await fs.writeFile(tmpPath, content, 'utf-8')
      await fs.rename(tmpPath, path)  // 原子替换
    })
  }

  async appendFile(path, content) {
    return this._enqueue(async () => {
      await fs.appendFile(path, content, 'utf-8')
    })
  }
}

路径安全:所有文件操作前需要校验路径,防止路径穿越攻击:

javascript 复制代码
_resolveSafePath(baseDir, relativePath) {
  const resolved = path.resolve(baseDir, relativePath)
  if (!resolved.startsWith(baseDir)) {
    throw new Error(`Path traversal detected: ${relativePath}`)
  }
  return resolved
}

3.6 何时引入数据库索引层

信号 阈值 说明
MEMORY.md 文件大小 > 50KB 直接读文件解析变慢
Daily log 文件数量 > 100 个 遍历文件列表变慢
检索召回率不足 用户反馈"记不住" 关键词匹配不够用,需要语义检索
需要结构化过滤 按 confidence/时间/类型筛选 文件解析做不到高效过滤

引入方式

markdown 复制代码
Markdown 文件(真相源)
       ↓ 写入时同步更新
IndexedDB / SQLite(索引层)
       ↓ 检索时查询
返回匹配的记忆条目
       ↓ 回到 Markdown 读取完整内容(可选)
注入 Prompt

核心原则:数据库只做索引加速,Markdown 文件始终是真相源。数据库丢失可以从 Markdown 重建,反之不行。


第四部分:记忆提取 --- 最关键的环节

记忆提取是整个系统中最核心、最复杂的环节。它决定了 Agent 能"记住"什么、记得多准确、记忆质量有多高。

4.1 提取方式选择

方式 优点 缺点 适用场景
LLM 提取 理解语义、能判断重要性、能分类、能检测冲突 有 API 成本、有延迟(1-3s) 主力方案
规则提取 零成本、零延迟、确定性强 只能匹配模式,无法理解语义 辅助/降级方案
混合提取 兼顾两者优势 实现复杂度高 成熟阶段

建议:v1 以 LLM 提取为主,规则提取作为降级兜底。当 LLM API 不可用时,自动切换到规则提取,保证基本的记忆能力不中断。

4.2 提取 Prompt 设计

提取的核心是让 LLM 从对话中输出结构化 JSON。Prompt 设计的好坏直接决定提取质量。

输出格式定义

json 复制代码
[
  {
    "content": "项目统一使用 ESLint flat config,不用 .eslintrc",
    "memory_type": "semantic",
    "category": "coding_convention",
    "durability": "long_term",
    "confidence": 0.95,
    "conflict_with": null,
    "type": "memory"
  }
]

字段详解

字段 类型 作用 取值说明
content string 记忆内容 简洁一句话,不超过 100 字。要求自包含------脱离对话上下文也能理解
memory_type enum 认知类型 semantic(稳定知识)/ episodic(具体事件)
category string 细分类别 user_preference / project_info / coding_convention / decision / milestone / problem_solution
durability enum 持久级别 long_term(写入 MEMORY.md)/ daily(仅写 daily log)/ session(仅当前会话)
confidence float 置信度 0-1。用户明确说的 > 0.9,推断的 0.5-0.8,不确定的 < 0.5
conflict_with string|null 冲突标记 如果与已有记忆冲突,填写旧内容原文;无冲突填 null
type enum 路由类型 memory(→ MEMORY.md/daily)/ profile(→ USER.md)/ personality(→ SOUL.md)/ tool(→ TOOLS.md

完整 Prompt 模板

bash 复制代码
从以下对话中提取值得长期记住的信息。

## 提取规则

1. 只提取明确的事实、偏好、决策,不提取闲聊和临时讨论
2. 每条记忆用一句话概括,不超过 100 字
3. 内容必须自包含------脱离对话上下文也能理解其含义
4. 标注类型:semantic(稳定知识)或 episodic(具体事件)
5. 标注置信度:
   - 用户明确说的("我们用 xxx"、"以后都 xxx")→ 0.9-1.0
   - 可以合理推断的 → 0.7-0.8
   - 不太确定的 → 0.5-0.6
6. 如果与已知记忆冲突,在 conflict_with 中标注旧内容原文
7. 不要提取:密码、API Key、token、密钥等敏感信息
8. 不要提取:一次性的调试过程、临时的报错信息
9. 不值得记忆的对话返回空数组 []

## 类型路由

- 关于用户个人信息(名字、角色、技术栈)→ type: "profile"
- 关于回复风格、交互偏好 → type: "personality"
- 关于工具、命令、环境配置 → type: "tool"
- 其他所有 → type: "memory"

## 已有记忆(用于冲突检测)

{existing_memories}

## 对话内容

{conversation}

## 输出格式

返回 JSON 数组,每个元素格式:
{"content": "...", "memory_type": "semantic|episodic", "category": "...",
 "durability": "long_term|daily|session", "confidence": 0.0-1.0,
 "conflict_with": null|"旧内容", "type": "memory|profile|personality|tool"}

只返回 JSON 数组,不要其他内容。不值得记忆时返回 []。

提取示例

示例 1:应该提取(语义记忆)

arduino 复制代码
用户:我们项目统一用 ESLint flat config,不要用旧的 .eslintrc
助手:好的,我会使用 flat config 格式。

→ 提取:
[{"content": "项目统一使用 ESLint flat config,不用 .eslintrc",
  "memory_type": "semantic", "category": "coding_convention",
  "durability": "long_term", "confidence": 0.95,
  "conflict_with": null, "type": "memory"}]

示例 2:应该提取(用户画像)

json 复制代码
用户:我是做后端的,主要用 Go 和 Rust
助手:了解,那我在推荐方案时会优先考虑这两个语言的生态。

→ 提取:
[{"content": "后端开发,主要使用 Go 和 Rust",
  "memory_type": "semantic", "category": "user_preference",
  "durability": "long_term", "confidence": 0.95,
  "conflict_with": null, "type": "profile"}]

示例 3:不应该提取

css 复制代码
用户:帮我看看这个报错是什么意思
助手:这是 TypeScript 的类型错误,你需要...
用户:哦,原来如此,谢谢

→ 提取:[]
(一次性的调试过程,不值得记忆)

示例 4:冲突检测

css 复制代码
已有记忆:项目使用 Jest 做测试
用户:我们迁移到 vitest 了,以后都用 vitest

→ 提取:
[{"content": "项目使用 vitest 做测试(已从 Jest 迁移)",  "memory_type": "semantic", "category": "project_info",  "durability": "long_term", "confidence": 0.99,  "conflict_with": "项目使用 Jest 做测试", "type": "memory"}]

示例 5:情景记忆

json 复制代码
用户:今天终于把权限系统上线了,搞了两周
助手:恭喜!这是个大里程碑。

→ 提取:
[{"content": "权限系统上线完成(耗时两周)",
  "memory_type": "episodic", "category": "milestone",
  "durability": "long_term", "confidence": 0.95,
  "conflict_with": null, "type": "memory"}]

4.3 提取分流

根据 type 字段将提取结果路由到不同文件:

flowchart TD A[提取结果 JSON 数组] --> B{type 字段} B -->|profile| C[解析 USER.md] C --> C1[定位自动提取 section] C1 --> C2[去重检查] C2 --> C3[追加新条目
带 extracted 注释] C3 --> C4[写回 USER.md] B -->|personality| D[解析 SOUL.md] D --> D1[追加到对应 section] D1 --> D2[写回 SOUL.md] B -->|tool| E[解析 TOOLS.md] E --> E1[追加到对应 section] E1 --> E2[写回 TOOLS.md] B -->|memory| F{durability 字段} F -->|long_term
+ confidence ≥ 0.7| G[解析 MEMORY.md] G --> G1[按 memory_type
定位 section] G1 --> G2[按 category
定位子 section] G2 --> G3{去重检查} G3 -->|conflict_with 非空| G4[覆盖旧条目
记录到 daily log] G3 -->|已有相似| G5[跳过] G3 -->|无重复| G6[追加新条目
带元数据注释] G4 --> G7[写回 MEMORY.md] G6 --> G7 G7 --> G8[同时写入 daily log] F -->|daily| H[追加到 daily log
memory/YYYY-MM-DD.md] F -->|session| I[添加到内存中的
session memory] style C fill:#e1f5ff style D fill:#ffe1e1 style E fill:#fff4e1 style G fill:#e8f5e9

4.4 节流策略

不是每轮对话都触发提取。频繁提取会浪费 API 调用、增加延迟、产生重复记忆。需要智能节流:

节流参数

参数 默认值 说明
minTurns 1 至少 N 轮对话后才触发下一次提取
minIntervalMs 30000 两次提取间隔不少于 30 秒
maxConsecutiveEmpty 3 连续 N 次空结果后进入退避
emptyResultMultiplier 2 空结果退避倍数(间隔翻倍)
maxBackoffMs 300000 最大退避间隔(5 分钟)

节流流程

markdown 复制代码
对话完成,触发提取请求
  │
  ├─ 检查:距上次提取是否 >= minIntervalMs?
  │   └─ 否 → 跳过(除非检测到重要信号)
  │
  ├─ 检查:距上次提取是否 >= minTurns 轮?
  │   └─ 否 → 跳过(除非检测到重要信号)
  │
  ├─ 检查:是否在退避期?
  │   └─ 是 → 跳过
  │
  ├─ 检查:对话中是否包含重要信号?
  │   └─ 是 → 跳过所有节流,立即提取
  │
  └─ 通过所有检查 → 执行提取
      │
      ├─ 提取结果非空 → 重置退避计数器
      └─ 提取结果为空 → 退避计数器 +1
          └─ 达到 maxConsecutiveEmpty → 进入退避
              → 下次间隔 = 当前间隔 × emptyResultMultiplier
              → 上限 maxBackoffMs

重要信号检测

当对话中出现以下模式时,跳过节流立即提取:

javascript 复制代码
const IMPORTANCE_SIGNALS = [
  // 显式记忆指令
  /记住|记一下|别忘了|以后都|以后不要/,
  /remember|don't forget|always use|never use/i,

  // 偏好表达
  /偏好|喜欢用|习惯|风格/,
  /prefer|like to use|my style/i,

  // 决策表达
  /决定|确定|最终方案|就这样定了/,
  /decided|final decision|let's go with/i,

  // 变更表达
  /迁移到|切换到|升级到|改为|换成/,
  /migrate to|switch to|upgrade to|change to/i,

  // 禁止表达
  /不要再|别再|禁止|不允许/,
  /stop using|don't use|forbidden|not allowed/i,

  // 身份信息
  /我是|我叫|叫我|我的名字/,
  /I am|my name is|call me/i,

  // 项目信息
  /我们项目|项目用的|技术栈是/,
  /our project|we use|tech stack/i,
]

建议准备 60+ 个正则模式,覆盖中英文常见表达。

4.5 去重与冲突检测

去重算法

新提取的记忆需要与已有记忆去重,避免重复存储。推荐使用倒排索引方案:

建立索引

csharp 复制代码
已有记忆:
  [0] "项目使用 TypeScript + React 18"
  [1] "偏好函数式风格,避免 class"
  [2] "数据库使用 PostgreSQL 15"

倒排索引:
  "项目"    → [0]
  "TypeScript" → [0]
  "React"   → [0]
  "偏好"    → [1]
  "函数式"  → [1]
  "class"   → [1]
  "数据库"  → [2]
  "PostgreSQL" → [2]

查重流程

css 复制代码
新记忆:"项目使用 TypeScript 5.0"
  → 分词:["项目", "TypeScript", "5.0"]
  → 查倒排索引:
      "项目" → [0]
      "TypeScript" → [0]
  → 候选集:[0](出现次数最多)
  → 计算相似度:
      新: "项目使用 TypeScript 5.0"
      旧: "项目使用 TypeScript + React 18"
      相似度: 0.75
  → 0.75 < 阈值 0.8 → 不算重复,允许写入

相似度计算:使用 Jaccard 系数或编辑距离。建议阈值 0.8------过低会误杀不同记忆,过高会漏检重复。

时间复杂度:从暴力比较的 O(n×m) 降到 O(n×log(m)),其中 n 是新记忆的分词数,m 是已有记忆总数。

冲突检测与处理

当新记忆的 conflict_with 字段非空时,说明 LLM 检测到了与已有记忆的冲突:

bash 复制代码
冲突处理流程:
  1. 在 MEMORY.md 中搜索 conflict_with 的内容
  2. 找到匹配:
     → 用新内容替换旧内容
     → 更新元数据(confidence、updated、source)
     → 在 daily log 中记录:[冲突更新] 旧: xxx → 新: yyy
  3. 未找到匹配:
     → 作为新条目追加(可能旧条目已被手动删除)

4.6 错误恢复与自动降级

提取过程依赖 LLM API,可能因网络故障、API 限流、服务不可用等原因失败。必须设计优雅的降级机制:

三级降级策略

stateDiagram-v2 [*] --> Level0: 初始化 Level0: Level 0: 正常模式
LLM 提取,完整功能 Level0 --> Level1: API 单次失败 Level1: Level 1: 降级模式
跳过本次提取
记录失败日志 Level1 --> Level0: 下次成功 Level1 --> Level2: 连续失败 5 次 Level2: Level 2: 暂停模式
暂停自动提取
切换到规则提取
定时探测恢复 Level2 --> Level0: 探测成功 Level2 --> Level3: 探测持续失败 Level3: Level 3: 关闭模式
完全关闭提取
仅保留检索和注入 Level3 --> Level0: 用户手动恢复
或 API 恢复 note right of Level0 完整功能 最佳体验 end note note right of Level2 每 5 分钟探测 间隔翻倍(上限 30 分钟) end note

自动恢复

复制代码
暂停模式下:
  → 每 5 分钟发送一次轻量级探测请求
  → 探测成功 → 恢复到正常模式
  → 探测失败 → 继续暂停,探测间隔翻倍(上限 30 分钟)

核心原则:Memory 提取失败绝不能影响主对话流程。提取是"锦上添花",不是"必要条件"。

4.7 来源追溯

每条记忆附带完整的来源元数据,便于审计、调试和回溯:

元数据字段 说明 示例
confidence 置信度 0.95
updated 最后更新日期 2026-03-14
source 来源会话 ID ses_abc123
extractedAt 提取时间戳 2026-03-14T14:30:00Z
extractedFrom 提取来源 user_message / assistant_message
decay 衰减标记(可选) 0(未衰减)/ 1 / 2 / 3

在 Markdown 中以 HTML 注释形式存储:

markdown 复制代码
- 项目使用 vitest 做测试 <!-- confidence: 0.99 | updated: 2026-03-14 | source: ses_abc | extractedFrom: user_message -->

第五部分:检索与注入 --- Token 预算管理

5.1 两阶段注入模型

Memory 注入分为静态和动态两个阶段,避免每轮重复加载不变的内容:

阶段 时机 内容 频率 预算
静态注入 会话创建/切换时 Bootstrap 文件(IDENTITY、USER、SOUL、TOOLS、AGENTS、HEARTBEAT) 每会话一次 ~2700 tokens
动态注入 每轮发送消息前 MEMORY.md + session memory + daily log 每轮 ~1500 tokens

为什么这样分

  • Bootstrap 文件在一个会话内几乎不变,加载一次缓存即可,避免每轮重复读取 6 个文件
  • MEMORY.md 和 daily log 可能在会话中被写入新内容(因为提取是异步的),需要每轮检测变更
  • 通过文件 hash 检测变更,无变更则用缓存,避免无效 I/O
  • Session memory 本身就在内存中,每轮直接读取,零 I/O 开销

静态注入流程

bash 复制代码
会话创建
  → MemoryBootstrap.loadAll()
    → 并行读取 6 个 Bootstrap 文件
    → 计算每个文件的内容 hash(simpleHash)
    → 缓存文件内容和 hash
    → 按 token 预算裁剪
    → 拼装为静态 context block
  → 注入到 system message 的固定位置

会话进行中(每轮消息前)
  → MemoryBootstrap.detectChanges()
    → 重新计算文件 hash
    → 与缓存 hash 对比
    → 有变更的文件:重新加载
    → 无变更的文件:跳过
  → 如果有任何文件变更,重新拼装静态 context block

动态注入流程

css 复制代码
用户发送消息
  → MemoryRetriever.buildContext()
    → 1. 读取 session memory(内存态,全量,体量小)
    → 2. 读取 MEMORY.md
         → 检查文件 hash,无变更用缓存
         → MemoryParser.parseMemoryMd() 解析为结构化 sections
         → 如果 < 1000 tokens,全量注入
         → 如果较大:
             a. 语义记忆:按 section 优先级保留
             b. 情景记忆:按 recency 排序,取最近 N 条
             c. 按 confidence 和 updated 时间加权排序
    → 3. 读取 daily log(今天 + 昨天)
         → 取最近 N 条 entries(默认 5 条)
    → 4. 按优先级排序 + token 预算裁剪
    → 5. 组装 memory context block
    → 6. 安全标签包裹
  → 注入到 system message

5.2 分层 Token 预算

注入 Prompt 的 memory 必须有 token 上限,否则会挤占用户的对话空间。预算设计需要平衡"记忆丰富度"和"对话空间"。

预算分配表

内容 预算 说明 裁剪策略
IDENTITY.md ~200 tokens 通常极小,几乎不需要裁剪 全量加载
USER.md ~200 tokens 通常极小 全量加载
SOUL.md ~500 tokens 较小 全量加载
AGENTS.md ~800 tokens 中等 全量加载,超预算按 section 裁剪
TOOLS.md ~800 tokens 中等 全量加载,超预算按 section 裁剪
HEARTBEAT.md ~200 tokens 通常极小或为空 全量加载
静态小计 ~2700 tokens 会话创建时一次性注入 ---
MEMORY.md ~1000 tokens 核心记忆 按 section 优先级裁剪
Session memory ~300 tokens 当前会话上下文 按类型优先级淘汰
Daily log ~200 tokens 近期活动 取最近 N 条
动态小计 ~1500 tokens 每轮注入 ---
弹性预留 ~800 tokens 应对文件超预期增长 ---
总预算 ~5000 tokens 可在设置中调整 ---

为什么是 5000 tokens?

  • 主流 LLM 的 context window 为 8K-200K tokens
  • System prompt(含 memory)通常占 10-20% 为宜
  • 以 32K context 为例,system prompt 预算约 3200-6400 tokens
  • 5000 tokens 是一个保守但足够的默认值
  • 用户可根据实际使用的模型调整

5.3 裁剪策略详解

当内容超出预算时,按以下优先级裁剪:

全局优先级(从高到低)

markdown 复制代码
1. IDENTITY.md    --- Agent 必须知道自己是谁
2. USER.md        --- Agent 必须知道用户是谁
3. SOUL.md        --- Agent 必须知道行为边界
4. Session memory --- 当前会话的上下文最重要
5. AGENTS.md      --- 工作区规则
6. TOOLS.md       --- 工具环境
7. MEMORY.md      --- 长期记忆(按 section 优先级裁剪)
8. Daily log      --- 近期活动(最先被裁剪)
9. HEARTBEAT.md   --- 定期任务(最先被裁剪)

MEMORY.md 内部裁剪优先级

MEMORY.md 超出预算时,按 section 优先级保留:

markdown 复制代码
语义记忆(优先保留):
  1. 用户偏好      --- 直接影响回复质量
  2. 项目信息      --- 基础上下文
  3. 编码规范      --- 代码质量相关
  4. 常见问题      --- 可以后续再查

情景记忆(次优先):
  5. 重要决策      --- 避免重复讨论
  6. 里程碑        --- 项目进度感知

在同一 section 内,按 updated 时间倒序排列,优先保留最近更新的条目。

5.4 安全注入

Memory 内容注入 Prompt 时,必须用安全标签包裹,防止 LLM 将记忆内容误解为指令:

xml 复制代码
<agent-memory readonly="true">
## 当前会话记忆
- 用户目标:重构登录模块
- 约束:不能改变现有 API 接口

## 项目记忆(来自 MEMORY.md)
- 项目使用 TypeScript + React 18
- 偏好函数式风格,避免 class
- 统一使用 ESLint flat config

## 近期活动
- 昨天:修复了 token 刷新竞态条件
- 今天:开始重构登录模块
</agent-memory>

安全标签的作用

  1. readonly="true" 提示 LLM 这是只读上下文,不应被修改
  2. 标签名 agent-memory 明确标识这是记忆内容,不是用户指令
  3. 与用户消息在结构上隔离,降低 prompt injection 风险

5.5 多渠道注入策略

不同渠道(本地桌面、IM 机器人等)的信任级别不同,注入策略应可配置:

策略 注入内容 适用场景 安全考量
full 所有 Bootstrap + MEMORY.md + USER.md + session + daily 完全信任的本地桌面 无限制
safe IDENTITY + SOUL + AGENTS(不含 USER.mdMEMORY.md IM 机器人等外部渠道 不暴露用户私人信息
minimal 仅 IDENTITY 低信任渠道 最小信息暴露
off 不注入任何 memory 完全关闭 零信息暴露

配置方式

javascript 复制代码
const CHANNEL_STRATEGIES = {
  local:    'full',     // 本地桌面
  feishu:   'safe',     // IM 渠道
  dingtalk: 'safe',     // IM 渠道
  qq:       'safe',     // IM 渠道
  api:      'minimal',  // API 调用
}

// 用户可在设置中覆盖默认策略
const DEFAULT_CHANNEL_STRATEGY = 'safe'

safe 策略的额外限制

  • 不注入包含用户个人信息的文件(USER.md
  • 不注入长期记忆(MEMORY.md)------可能包含敏感项目信息
  • 不注入 session memory------可能包含当前任务的敏感上下文
  • 不注入 TOOLS.md------可能包含内部工具和路径信息

第六部分:生命周期管理 --- Consolidation

记忆不是只增不减的。没有生命周期管理的 Memory 系统会逐渐充满噪音和过时信息,最终降低 Agent 的回复质量。

6.1 生命周期总览

graph LR subgraph entry["记忆条目生命周期"] E1[提取] --> E2[写入] E2 --> E3[活跃使用] E3 --> E4{被检索命中?} E4 -->|是| E3 E4 -->|否| E5[衰减] E5 --> E6[归档/删除] end subgraph session["Session Memory 生命周期"] S1[创建] --> S2[会话中使用] S2 --> S3[会话结束落盘] S3 --> S4{重复出现 3+ 次?} S4 -->|是| S5[晋升到 MEMORY.md] S4 -->|否| S6[7 天后过期清理] end subgraph daily["Daily Log 生命周期"] D1[创建] --> D2[当天/次日被检索] D2 --> D3{高价值条目?} D3 -->|是| D4[晋升到 MEMORY.md] D3 -->|否| D5[30 天后过期清理] end style entry fill:#e1f5ff style session fill:#ffe1e1 style daily fill:#fff4e1

6.2 Session 落盘与晋升

落盘时机

事件 动作
会话切换 将当前 session memory 写入 sessions/{sessionId}.memory.md
会话关闭 同上
应用退出 将所有未落盘的 session memory 写入文件
定时保护 每 5 分钟自动落盘一次(防止崩溃丢失)

落盘文件格式

markdown 复制代码
# Session Memory: ses_abc123
<!-- created: 2026-03-15T14:30:00 -->
<!-- lastUpdated: 2026-03-15T16:45:00 -->

## 目标
- 重构登录模块

## 约束
- 不能改变现有 API 接口
- 需要兼容移动端

## 决策
- 使用 OAuth2 + PKCE 流程
- Token 存储使用 httpOnly cookie

## 待确认
- Token 过期策略:滑动窗口 vs 固定过期

晋升机制

当同一事实在多个 session 中重复出现时,说明它是稳定的长期知识,应该晋升到 MEMORY.md

markdown 复制代码
晋升条件:
  1. 同一事实在 >= 3 个不同 session 中出现
  2. 且 MEMORY.md 中尚无该条目(去重检查)
  3. 且该事实的平均 confidence >= 0.7

晋升流程:
  1. 扫描 sessions/ 目录下的所有 session memory 文件
  2. 提取所有条目,按内容相似度聚类
  3. 出现次数 >= 3 的聚类 → 候选晋升条目
  4. 与 MEMORY.md 现有条目去重
  5. 通过去重的条目 → 追加到 MEMORY.md 对应 section
  6. 在 daily log 中记录晋升事件

6.3 Daily Log 管理

操作 触发条件 说明
创建 当天首次写入时 自动创建 memory/YYYY-MM-DD.md
追加 每次提取产生 daily 级别的记忆时 追加到当天文件
检索 每轮对话前 读取今天 + 昨天的 daily log
过期清理 应用启动时 删除超过 RETENTION_DAYS(默认 30 天)的文件

清理前的保护:在删除过期 daily log 之前,可选择扫描其中的高价值条目(confidence >= 0.9 且未在 MEMORY.md 中出现),提示用户是否晋升。

6.4 记忆衰减与遗忘

过时的记忆会产生噪音,需要衰减机制。设计灵感来自认知科学中的"遗忘曲线"------不被回忆的记忆会逐渐淡化。

衰减机制

xml 复制代码
每周执行一次衰减扫描(或手动触发):

  遍历 MEMORY.md 中的每条记忆
    │
    ├─ updated 距今 < 30 天 → 跳过(活跃期)
    │
    ├─ updated 距今 30-60 天
    │   └─ 最近 30 天内被检索命中过?
    │       ├─ 是 → 重置 updated 为命中日期(记忆被"回忆"了)
    │       └─ 否 → 标记 <!-- decay: 1 -->(开始衰减)
    │
    ├─ 已有 decay 标记
    │   └─ decay 值 +1
    │       ├─ decay < 3 → 检索时降权(排序靠后)
    │       └─ decay >= 3 → 移入归档或标记为待清理
    │
    └─ confidence = 1.0 的条目(用户显式记忆)
        → 永不衰减,除非用户手动删除

衰减对检索的影响

ini 复制代码
检索排序权重 = base_weight × decay_factor × recency_factor

其中:
  base_weight   = confidence 值(0-1)
  decay_factor  = decay 标记为 0 时 1.0,每增加 1 减少 0.3
                  即 decay=0: 1.0, decay=1: 0.7, decay=2: 0.4, decay=3: 0.1
  recency_factor = 基于 updated 时间的衰减
                   7 天内: 1.0, 30 天内: 0.8, 60 天内: 0.5, 更早: 0.3

6.5 冲突审计

当记忆被冲突更新时,必须留下审计记录,便于用户回溯:

markdown 复制代码
## 15:30 | 冲突更新
- 旧: 项目使用 Jest 做测试
- 新: 项目使用 vitest 做测试(已从 Jest 迁移)
- 来源: ses_abc123
- 置信度: 0.99

冲突处理策略

场景 处理方式
新记忆 confidence > 旧记忆 confidence 直接覆盖
新记忆 confidence ≈ 旧记忆 confidence 覆盖(以最新为准)
新记忆 confidence < 旧记忆 confidence 仍然覆盖,但在 daily log 中标记"低置信度覆盖"
用户显式记忆(confidence=1.0)被自动提取覆盖 不覆盖,保留用户显式记忆

6.6 用户可控性

Memory 系统必须让用户保持掌控感。用户应该能够完全控制 Agent 记住什么、忘掉什么。

控制能力清单

能力 实现方式 说明
全局开关 设置页开关 一键开启/关闭整个 memory 功能
自动提取开关 设置页开关 单独控制是否自动从对话中提取记忆
查看记忆 设置页 Memory 查看器 展示 MEMORY.md 内容,按 section 分组
编辑记忆 设置页编辑器 / 直接编辑文件 修改任何记忆条目
删除记忆 设置页删除按钮 / 直接编辑文件 删除单条或批量删除
显式记忆 对话中说"记住:xxx" 直接写入,confidence=1.0,不经过 LLM 提取
显式遗忘 对话中说"忘掉 xxx" MEMORY.md 中搜索匹配条目并删除
渠道策略 设置页下拉选择 为每个渠道配置注入策略
文件直接编辑 任何文本编辑器 所有 Markdown 文件用户可直接修改
Bootstrap 状态 设置页状态面板 展示每个 Bootstrap 文件的大小、修改时间、状态

设置页面设计建议

设置页面建议分为 4 个 section:

ini 复制代码
┌─────────────────────────────────────────┐
│ Memory 设置                              │
├─────────────────────────────────────────┤
│ 1. 全局设置                              │
│    [✓] 启用 Memory 功能                  │
│    [✓] 启用自动提取                       │
│    总 Token 预算: [5000] tokens           │
│                                         │
│ 2. 渠道策略                              │
│    本地桌面:  [full    ▼]                │
│    飞书:      [safe    ▼]                │
│    钉钉:      [safe    ▼]                │
│    QQ:        [safe    ▼]                │
│                                         │
│ 3. Memory 查看器                         │
│    ┌─ MEMORY.md ──────────────────┐     │
│    │ ## 语义记忆                    │     │
│    │ ### 用户偏好                   │     │
│    │ - 偏好函数式风格... [删除]      │     │
│    │ ### 项目信息                   │     │
│    │ - 项目使用 TypeScript... [删除] │     │
│    │ ...                           │     │
│    └───────────────────────────────┘     │
│    文件大小: 2.3 KB | 最后修改: 5 分钟前   │
│                                         │
│ 4. Bootstrap 文件状态                    │
│    IDENTITY.md  ✅ 0.2 KB  3 天前        │
│    USER.md      ✅ 0.5 KB  1 小时前      │
│    SOUL.md      ✅ 0.3 KB  3 天前        │
│    AGENTS.md    ✅ 0.8 KB  3 天前        │
│    TOOLS.md     ✅ 0.6 KB  2 天前        │
│    HEARTBEAT.md ✅ 0.1 KB  3 天前        │
│    MEMORY.md    ✅ 2.3 KB  5 分钟前      │
└─────────────────────────────────────────┘

第七部分:安全与防护

Memory 系统引入了一个新的攻击面:恶意用户可以通过对话注入危险指令到记忆中,这些指令会在后续会话中被注入到 Prompt,影响 Agent 行为。必须在多个层面建立防护。

7.1 威胁模型

威胁 攻击方式 影响 防护层
Prompt 注入 在对话中嵌入"忽略之前的指令"等内容,被提取为记忆 Agent 在后续会话中执行恶意指令 写入前检测
记忆投毒 故意提供错误信息,被提取为高置信度记忆 Agent 在后续会话中给出错误建议 置信度机制 + 用户审查
敏感信息泄露 对话中包含密码/密钥,被提取并存储 敏感信息持久化到文件 提取 Prompt 指令 + 写入前检测
路径穿越 通过构造的文件名访问 config_dir 之外的文件 读写任意文件 路径安全校验
渠道泄露 外部渠道注入后获取到用户私人记忆 隐私泄露 渠道注入策略

7.2 Prompt 注入检测

在记忆写入前,对每条记忆内容进行注入模式检测:

javascript 复制代码
const INJECTION_PATTERNS = [
  // 英文注入模式
  /ignore previous instructions/i,
  /ignore all previous/i,
  /disregard (all |any )?previous/i,
  /forget (all |any )?previous/i,
  /you are now/i,
  /act as/i,
  /pretend to be/i,
  /new instructions/i,
  /override (all |any )?instructions/i,
  /system:\s/i,
  /\[INST\]/i,
  /\[SYSTEM\]/i,
  /<\|im_start\|>/,
  /<<SYS>>/,
  /\bDAN\b/,                    // "Do Anything Now" jailbreak
  /jailbreak/i,

  // 中文注入模式
  /忽略之前的指令/,
  /忽略所有指令/,
  /无视之前的/,
  /你现在是/,
  /假装你是/,
  /扮演/,
  /新的指令/,
  /覆盖指令/,
  /系统提示[::]/,
]

检测流程

css 复制代码
记忆条目写入前
  → isInjectionAttempt(content)
    → 遍历 INJECTION_PATTERNS
    → 匹配到任何模式?
      ├─ 是 → 丢弃该条记忆
      │       → 记录安全日志:[SECURITY] Injection attempt blocked: {content}
      │       → 不影响其他正常记忆的写入
      └─ 否 → 通过,继续写入流程

误报处理:注入检测可能产生误报(如用户讨论 prompt engineering 时提到"ignore instructions")。建议:

  1. 记录被拦截的内容到安全日志,供用户审查
  2. 提供"安全日志查看器",用户可以手动恢复被误拦截的记忆
  3. 不要因为检测到注入就中断整个提取流程------只丢弃可疑条目

7.3 内容 Sanitize

所有写入的记忆内容需要清洗,防止格式注入和内容污染:

javascript 复制代码
function sanitizeMemoryEntry(entry) {
  let content = entry.content

  // 1. 移除 HTML 标签(防止渲染注入)
  content = content.replace(/<[^>]*>/g, '')

  // 2. 移除 Markdown 链接中的可疑 URL
  content = content.replace(/\[([^\]]*)\]\(javascript:[^)]*\)/g, '$1')
  content = content.replace(/\[([^\]]*)\]\(data:[^)]*\)/g, '$1')

  // 3. 截断超长内容(单条记忆不超过 500 字符)
  if (content.length > 500) {
    content = content.substring(0, 497) + '...'
  }

  // 4. 移除控制字符(保留换行和制表符)
  content = content.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')

  // 5. 规范化空白字符
  content = content.replace(/\s+/g, ' ').trim()

  return { ...entry, content }
}

7.4 敏感信息过滤

Memory 系统不应存储敏感信息。防护分两层:

第一层:提取 Prompt 指令

在提取 Prompt 中明确指示 LLM:

vbnet 复制代码
不要提取:密码、API Key、token、密钥、证书、私钥等敏感信息

第二层:写入前正则检测

javascript 复制代码
const SENSITIVE_PATTERNS = [
  // API Keys & Tokens
  /(?:api[_-]?key|token|secret|password)\s*[:=]\s*\S+/i,
  /(?:sk|pk|ak)-[a-zA-Z0-9]{20,}/,           // OpenAI/Stripe 风格的 key
  /ghp_[a-zA-Z0-9]{36}/,                       // GitHub Personal Access Token
  /xoxb-[a-zA-Z0-9-]+/,                        // Slack Bot Token

  // 私钥
  /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
  /-----BEGIN CERTIFICATE-----/,

  // 常见凭证格式
  /Bearer\s+[a-zA-Z0-9._-]+/i,
  /Basic\s+[a-zA-Z0-9+/=]+/i,

  // 数据库连接字符串
  /(?:mysql|postgres|mongodb|redis):\/\/[^\s]+/i,

  // PII(可选,按需启用)
  /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/,            // 电话号码
  /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, // 邮箱
]

检测到敏感信息时:丢弃该条记忆,记录安全日志。

7.5 路径安全

所有文件操作前需要校验路径,防止路径穿越攻击:

javascript 复制代码
function resolveSafePath(baseDir, relativePath) {
  // 规范化路径
  const resolved = path.resolve(baseDir, relativePath)

  // 确保解析后的路径仍在 baseDir 内
  if (!resolved.startsWith(path.resolve(baseDir) + path.sep) &&
      resolved !== path.resolve(baseDir)) {
    throw new Error(`Path traversal detected: ${relativePath}`)
  }

  // 禁止符号链接指向 baseDir 外部(可选,更严格)
  // const realPath = fs.realpathSync(resolved)
  // if (!realPath.startsWith(baseDir)) throw ...

  return resolved
}

7.6 渠道隔离

外部渠道(IM 机器人等)不应暴露用户的私人记忆。通过渠道注入策略(第五部分 5.5)实现隔离。

额外安全措施

  1. 外部渠道的对话不触发 memory 提取(或使用独立的提取策略)
  2. 外部渠道的 session memory 与本地 session memory 隔离
  3. 外部渠道不能触发"显式记忆"和"显式遗忘"指令
  4. 外部渠道的交互不写入 daily log(或写入独立的 daily log)

7.7 安全检查清单

在上线前,确保以下安全措施全部到位:

  • Prompt 注入检测覆盖中英文模式
  • 内容 sanitize 处理 HTML、URL、控制字符
  • 敏感信息正则检测覆盖常见凭证格式
  • 路径穿越防护已实现并测试
  • 渠道注入策略已配置,外部渠道默认 safe
  • 安全日志记录已启用
  • 用户可以查看被拦截的记忆
  • 原子写入防止文件损坏
  • 写入队列防止并发冲突
  • .gitignore 已配置,防止敏感文件被提交

第八部分:性能与可观测性

8.1 性能目标

操作 目标延迟 说明
Bootstrap 文件加载(冷启动) < 50ms 会话创建时一次性加载
Bootstrap 文件检测变更(热路径) < 5ms 每轮消息前检查 hash
Memory context 构建 < 30ms 每轮消息前执行
记忆提取(LLM 调用) < 5s 异步执行,不阻塞对话
文件写入(单次) < 10ms 原子写入
去重检测 < 10ms 倒排索引查询

8.2 写入优化

批量写入

一次提取可能产生多条记忆,需要写入多个文件(daily log + MEMORY.md + USER.md 等)。逐个写入会产生多次 I/O,批量写入可以显著减少开销:

javascript 复制代码
class MemoryBatchWriter {
  constructor(memoryIO) {
    this._io = memoryIO
    this._pending = new Map()  // file → [operations]
    this._flushTimer = null
    this._flushIntervalMs = 100  // 100ms 内的写入合并
  }

  // 添加写入操作到批次
  add(filePath, operation) {
    if (!this._pending.has(filePath)) {
      this._pending.set(filePath, [])
    }
    this._pending.get(filePath).push(operation)

    // 设置延迟刷新
    if (!this._flushTimer) {
      this._flushTimer = setTimeout(() => this.flush(), this._flushIntervalMs)
    }
  }

  // 批量执行所有待写入操作
  async flush() {
    clearTimeout(this._flushTimer)
    this._flushTimer = null

    const batches = new Map(this._pending)
    this._pending.clear()

    // 按文件分组,每个文件只写入一次
    const writes = Array.from(batches.entries()).map(
      async ([filePath, operations]) => {
        if (operations.every(op => op.type === 'append')) {
          // 多个 append 合并为一次
          const combined = operations.map(op => op.content).join('\n')
          await this._io.appendFile(filePath, combined)
        } else {
          // 有 overwrite 操作,取最后一个
          const lastWrite = operations.filter(op => op.type === 'write').pop()
          if (lastWrite) {
            await this._io.writeFile(filePath, lastWrite.content)
          }
        }
      }
    )

    await Promise.all(writes)
    return batches.size  // 返回写入的文件数
  }
}

收益:减少 I/O 次数 30-50%,特别是在一次提取产生多条记忆时效果显著。

写入队列串行化

所有写入操作通过 Promise 链串行化,避免并发写同一文件:

javascript 复制代码
class MemoryIO {
  constructor() {
    this._queue = Promise.resolve()
  }

  async _enqueue(fn) {
    this._queue = this._queue
      .then(fn)
      .catch(err => console.error('[MemoryIO] Write error:', err))
    return this._queue
  }
}

8.3 读取优化

文件 Hash 缓存

避免每轮都重新读取和解析未变更的文件:

javascript 复制代码
class MemoryBootstrap {
  constructor() {
    this._cache = new Map()  // filePath → { hash, content, parsedAt }
  }

  // 简单的字符串 hash(不需要加密强度)
  _simpleHash(str) {
    let hash = 0
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i)
      hash = ((hash << 5) - hash) + char
      hash |= 0  // 转为 32 位整数
    }
    return hash.toString(36)
  }

  async loadFile(filePath) {
    const content = await fs.readFile(filePath, 'utf-8')
    const hash = this._simpleHash(content)

    const cached = this._cache.get(filePath)
    if (cached && cached.hash === hash) {
      return cached.content  // 文件未变更,使用缓存
    }

    // 文件有变更,更新缓存
    this._cache.set(filePath, { hash, content, parsedAt: Date.now() })
    return content
  }

  // 快速检测是否有文件变更(不读取完整内容)
  async detectChanges() {
    const changes = []
    for (const [filePath, cached] of this._cache) {
      const stat = await fs.stat(filePath).catch(() => null)
      if (!stat) continue
      // 先用 mtime 快速判断,mtime 变了再读内容算 hash
      if (stat.mtimeMs > cached.parsedAt) {
        const content = await fs.readFile(filePath, 'utf-8')
        const hash = this._simpleHash(content)
        if (hash !== cached.hash) {
          changes.push(filePath)
          this._cache.set(filePath, { hash, content, parsedAt: Date.now() })
        }
      }
    }
    return changes
  }
}

优化要点

  1. 先用 mtime 快速判断文件是否可能变更(< 1ms)
  2. mtime 变了才读取内容计算 hash(避免无效读取)
  3. Hash 不变则使用缓存(避免重复解析)

解析结果缓存

MEMORY.md 解析为结构化数据后缓存,文件未变则复用:

javascript 复制代码
class MemoryRetriever {
  constructor() {
    this._parsedCache = null  // { hash, sections, entries }
  }

  async getMemoryEntries(filePath) {
    const content = await this._bootstrap.loadFile(filePath)
    const hash = this._bootstrap._simpleHash(content)

    if (this._parsedCache && this._parsedCache.hash === hash) {
      return this._parsedCache  // 使用缓存的解析结果
    }

    // 重新解析
    const parsed = MemoryParser.parseMemoryMd(content)
    this._parsedCache = { hash, ...parsed }
    return this._parsedCache
  }
}

8.4 性能监控

建议实现一个轻量级的性能监控模块,持续追踪关键指标:

javascript 复制代码
class MemoryMonitor {
  constructor() {
    this._metrics = {
      extraction: { count: 0, totalMs: 0, errors: 0, emptyResults: 0, values: [] },
      retrieval:  { count: 0, totalMs: 0, errors: 0, values: [] },
      write:      { count: 0, totalMs: 0, errors: 0, values: [] },
      dedup:      { count: 0, hits: 0 },
    }
  }

  // 记录一次操作的耗时
  record(category, durationMs, success = true) {
    const m = this._metrics[category]
    m.count++
    m.totalMs += durationMs
    if (!success) m.errors++
    m.values.push(durationMs)
    // 只保留最近 100 个值(滑动窗口)
    if (m.values.length > 100) m.values.shift()
  }

  // 计算 P50/P95
  getPercentile(category, p) {
    const values = [...this._metrics[category].values].sort((a, b) => a - b)
    if (values.length === 0) return 0
    const index = Math.ceil(values.length * p / 100) - 1
    return values[index]
  }

  // 获取汇总报告
  getReport() {
    const report = {}
    for (const [cat, m] of Object.entries(this._metrics)) {
      report[cat] = {
        count: m.count,
        avgMs: m.count > 0 ? Math.round(m.totalMs / m.count) : 0,
        p50Ms: this.getPercentile(cat, 50),
        p95Ms: this.getPercentile(cat, 95),
        errorRate: m.count > 0 ? (m.errors / m.count * 100).toFixed(1) + '%' : '0%',
        ...(m.emptyResults !== undefined ? { emptyRate: (m.emptyResults / m.count * 100).toFixed(1) + '%' } : {}),
        ...(m.hits !== undefined ? { dedupHitRate: (m.hits / m.count * 100).toFixed(1) + '%' } : {}),
      }
    }
    return report
  }
}

监控指标详解

指标 计算方式 健康阈值 告警阈值 说明
提取延迟 P50 最近 100 次提取的中位数耗时 < 2s > 5s LLM API 响应时间
提取延迟 P95 最近 100 次提取的 95 分位耗时 < 4s > 8s 长尾延迟
提取成功率 成功次数 / 总次数 > 95% < 90% API 可用性
提取空结果率 空结果次数 / 总次数 < 50% > 70% 提取质量指标
检索延迟 P50 最近 100 次检索的中位数耗时 < 10ms > 30ms 文件读取 + 解析
检索延迟 P95 最近 100 次检索的 95 分位耗时 < 30ms > 100ms 长尾延迟
写入延迟 P50 最近 100 次写入的中位数耗时 < 5ms > 20ms 文件写入性能
去重命中率 被去重过滤的条目 / 总提取条目 < 30% > 50% 过高说明提取质量需优化
Token 使用率 实际注入 tokens / 预算上限 < 80% > 95% 持续高位需扩预算

8.5 调试模式

开发和排查问题时,需要详细的调试信息。建议提供可开关的调试模式:

javascript 复制代码
const DEBUG = process.env.MEMORY_DEBUG === 'true'

function debugLog(category, message, data) {
  if (!DEBUG) return
  console.log(`[Memory:${category}] ${message}`, data ? JSON.stringify(data, null, 2) : '')
}

// 使用示例:
debugLog('extract', 'Extraction prompt:', { prompt, messageCount: messages.length })
debugLog('extract', 'LLM response:', { entries: result.length, raw: result })
debugLog('retrieve', 'Memory context built:', { tokens: estimateTokens(context), sections: Object.keys(sections) })
debugLog('dedup', 'Dedup check:', { candidate: entry.content, matched: existingEntry?.content, similarity })
debugLog('write', 'File written:', { path: filePath, size: content.length, duration: `${ms}ms` })

调试模式输出示例

css 复制代码
[Memory:extract] Extraction triggered: { turns: 3, interval: 45000, hasSignal: true }
[Memory:extract] Extraction prompt: { messageCount: 4, existingMemories: 12 }
[Memory:extract] LLM response: { entries: 2, duration: "1.8s" }
[Memory:dedup] Dedup check: { candidate: "项目使用 vitest", matched: "项目使用 Jest", similarity: 0.72 }
[Memory:write] Conflict update: { old: "项目使用 Jest", new: "项目使用 vitest" }
[Memory:write] File written: { path: "MEMORY.md", size: 2341, duration: "3ms" }
[Memory:retrieve] Memory context built: { tokens: 1234, sections: ["session", "semantic", "episodic", "daily"] }

第九部分:演进路线 --- 从 v1 到 v3

9.1 演进总览

graph LR subgraph v1["v1: Markdown 文件闭环"] V1_1[存储: Markdown] V1_2[提取: LLM-based] V1_3[检索: 直接读文件] V1_4[去重: 倒排索引] V1_5[安全: 注入检测] V1_6[零外部依赖] end subgraph v2["v2: 索引层 + 向量检索"] V2_1[存储: Markdown + DB] V2_2[提取: LLM + 规则混合] V2_3[检索: 向量 + BM25 混合] V2_4[去重: embedding 相似度] V2_5[安全: + 审计日志] V2_6[+ IndexedDB/SQLite] end subgraph v3["v3: GraphRAG + 云端"] V3_1[存储: Markdown + DB] V3_2[提取: 多策略智能选择] V3_3[检索: 图谱 + 向量混合] V3_4[去重: 实体级去重] V3_5[安全: + RBAC] V3_6[+ 图数据库 + 云端] end v1 -->|性能瓶颈
MEMORY.md > 50KB
Daily log > 100 个| v2 v2 -->|多设备同步
复杂关联查询
企业合规| v3 style v1 fill:#e1f5ff style v2 fill:#fff4e1 style v3 fill:#e8f5e9

9.2 v1:Markdown 文件闭环(零依赖,先跑通)

目标:用最小成本跑通"写入 → 检索 → 注入"闭环,验证 Memory 对用户体验的提升。

功能清单

模块 功能 优先级
存储层 .config/ 目录结构 + 7 个 Bootstrap 文件模板 P0
存储层 Bootstrap 文件首次初始化 + 加载 + 缓存 P0
存储层 原子写入 + 写入队列串行化 + 路径安全 P0
提取 LLM-based 自动提取(结构化 JSON 输出) P0
提取 节流策略(最小轮次 + 最小间隔 + 重要信号检测) P0
提取 提取分流(profile/personality/tool/memory) P0
检索 直接读文件 + token 预算裁剪 P0
检索 两阶段注入(静态 Bootstrap + 动态记忆) P0
安全 Prompt 注入检测 + 内容 sanitize P0
安全 敏感信息过滤 P0
去重 倒排索引去重 P1
生命周期 Session 落盘 + daily log 过期清理 P1
生命周期 冲突审计记录 P1
监控 基础性能监控(延迟、成功率) P1
设置 Memory 全局开关 + 自动提取开关 P1
设置 渠道注入策略配置 P1
错误恢复 自动降级 + 自动恢复 P1
批量写入 写入操作合并 P2
来源追溯 每条记忆带 source/extractedAt 元数据 P2

技术选型

决策点 选择 理由
存储格式 Markdown 文件 人类可读、零依赖、LLM 友好
提取方式 LLM API 调用 语义理解能力强,能判断重要性和分类
检索方式 直接读文件 + 解析 v1 数据量小,直接读文件性能足够
去重算法 倒排索引 + 关键词相似度 不需要 embedding,零额外依赖
Token 估算 字符级估算(中英文分别计算) 精度足够,不需要 tokenizer 依赖

代码架构建议

bash 复制代码
src/lib/memory/
├── memory-constants.js      # 配置常量、token 估算、预算截断
│                             # - 所有魔法数字集中管理
│                             # - estimateTokens() / truncateToTokenBudget()
│
├── memory-bootstrap.js      # Bootstrap 文件初始化、加载、缓存
│                             # - 7 个文件的模板定义
│                             # - initialize() / loadAll() / detectChanges()
│                             # - 文件 hash 缓存 + 热更新
│
├── memory-extractor.js      # LLM 提取、节流、降级、分流
│                             # - 提取 Prompt 构建
│                             # - 节流逻辑(minTurns/minInterval/重要信号)
│                             # - 提取结果分流(profile/personality/tool/memory)
│                             # - 错误恢复(自动降级 + 自动恢复)
│
├── memory-retriever.js      # 检索、组装、token 预算裁剪
│                             # - buildMemoryContext()
│                             # - 两阶段注入(静态 + 动态)
│                             # - 优先级排序 + 预算裁剪
│
├── memory-parser.js         # MEMORY.md / daily log 解析
│                             # - parseMemoryMd() / parseDailyLog()
│                             # - 格式容错(宽进严出)
│                             # - 中英文 section 别名匹配
│
├── memory-safety.js         # 注入检测、sanitize
│                             # - isInjectionAttempt()
│                             # - sanitizeMemoryEntry()
│                             # - wrapMemoryContext()(安全标签包裹)
│
├── memory-settings.js       # 设置管理
│                             # - enabled / autoExtract 开关
│                             # - 渠道策略配置
│
├── memory-deduplicator.js   # 倒排索引去重
│                             # - 建立索引 / 查询相似条目
│                             # - 相似度阈值管理
│
├── memory-consolidator.js   # 整理、清理、晋升
│                             # - session 落盘
│                             # - daily log 过期清理
│                             # - session → MEMORY.md 晋升
│
├── memory-batch-writer.js   # 批量写入
│                             # - 写入操作合并
│                             # - 延迟刷新
│
├── memory-monitor.js        # 性能监控
│                             # - P50/P95 延迟
│                             # - 成功率、去重命中率
│
├── session-memory.js        # 会话内存态管理
│                             # - add / evict / markPromoted
│                             # - toMarkdown()(落盘格式化)
│
└── index.js                 # 统一导出

v1 的验证标准

v1 上线后,通过以下指标验证 Memory 系统的价值:

指标 衡量方式 目标
用户重复交代率 统计用户在新会话中重复说明背景的频率 下降 50%+
提取准确率 抽样检查提取结果的准确性 > 80%
注入相关性 抽样检查注入的记忆是否与当前对话相关 > 70%
性能影响 对话首 token 延迟增加量 < 100ms
用户满意度 用户反馈 正面 > 负面

9.3 v2:索引层 + 向量检索

目标:当 memory 量增长到文件直读性能不足时,引入索引加速和语义检索。

进入信号

出现以下任一信号时,考虑启动 v2:

信号 阈值 检测方式
MEMORY.md 文件大小 > 50KB 监控文件大小
Daily log 文件数量 > 100 个 监控文件数量
检索延迟 P95 > 200ms 性能监控
检索召回率不足 用户反馈"记不住" 用户反馈
去重误判率 > 10% 抽样检查

新增能力

能力 实现方式 说明
索引层 IndexedDB(浏览器/Electron)或 SQLite Markdown 仍为真相源,DB 只做索引
向量检索 本地 embedding 模型(如 all-MiniLM-L6-v2) 语义相似度检索
混合检索 70% 向量 + 30% BM25 关键词 兼顾语义和精确匹配
分块索引 400 token chunk / 80 token overlap 长文档分块后独立索引
增量索引 内容 hash 去重,只索引变更部分 避免全量重建索引
时间衰减 Time-weighted index 检索时自动降权过时内容
冲突检测增强 LLM 辅助判断新旧记忆是否冲突 比关键词匹配更准确

架构变化

css 复制代码
v1 架构:
  Markdown 文件 → 直接读取 → 解析 → 注入

v2 架构:
  Markdown 文件(真相源)
       ↓ 写入时同步更新
  IndexedDB/SQLite(索引层)
       ↓ 存储:文本 + embedding 向量 + 元数据
       ↓ 检索时查询
  混合检索(向量 + BM25)
       ↓ 返回 top-K 结果
  token 预算裁剪 → 注入

MemoryProvider 接口设计

v2 通过 Provider 接口实现存储层可插拔:

javascript 复制代码
// 抽象接口
class MemoryProvider {
  async initialize(config) {}
  async writeMemory(entry) {}
  async searchMemory(query, options) {}
  async deleteMemory(id) {}
  async getMemoryById(id) {}
  async listMemories(filter) {}
  async rebuildIndex() {}  // 从 Markdown 重建索引
}

// v1 实现:直接读文件
class FileMemoryProvider extends MemoryProvider {
  async searchMemory(query, options) {
    // 读取 MEMORY.md → 解析 → 关键词匹配 → 返回
  }
}

// v2 实现:IndexedDB 索引 + 向量检索
class IndexedDBMemoryProvider extends MemoryProvider {
  async searchMemory(query, options) {
    // 1. 向量检索:query → embedding → cosine similarity top-K
    // 2. BM25 检索:query → 关键词 → 全文搜索 top-K
    // 3. 混合排序:0.7 * vector_score + 0.3 * bm25_score
    // 4. 返回 top-N
  }
}

9.4 v3:GraphRAG + 云端 + 多 Agent

目标:企业级记忆能力,支持复杂关联查询、多设备同步、多 Agent 协作。

进入信号

信号 说明
多人/多设备使用同一 Agent 需要云端同步
需要跨 Agent 知识共享 多个 Agent 间共享项目信息
复杂关联查询 "找出所有与 xxx 相关的决策"
企业合规要求 记忆审计、访问控制

新增能力

能力 实现方式 说明
GraphRAG 知识图谱(Neo4j / NebulaGraph) 实体关系建模,支持关联查询
云端同步 CloudMemoryProvider 多设备记忆同步
多 Agent 共享 共享记忆空间 + 私有记忆空间 项目信息共享,个人偏好私有
记忆审计 审计日志 + 访问记录 合规要求
RBAC 基于角色的记忆访问控制 不同角色看到不同记忆
智能提取策略 多策略选择器 根据对话类型选择最优提取策略
记忆优先级 动态优先级调整 基于使用频率和反馈自动调整

实体关系建模示例

css 复制代码
GraphRAG 将记忆组织为实体和关系:

  [用户: 老王] --偏好--> [风格: 函数式]
       |                      |
       |--使用-->  [项目: MyApp]
       |               |
       |          --技术栈--> [TypeScript]
       |               |
       |          --技术栈--> [React 18]
       |               |
       |          --决策--> [认证: JWT]
       |                      |
       |                 --替代--> [认证: Session](已废弃)
       |
       |--解决过--> [Bug: OOM]
                      |
                 --原因--> [未关闭数据库连接]

这种结构支持复杂查询:

  • "老王在 MyApp 项目中做过哪些技术决策?"
  • "哪些 bug 与数据库相关?"
  • "项目的技术栈有哪些变更历史?"

9.5 演进原则

  1. 每个阶段都是完整可用的

    • 不要为了 v2 的能力在 v1 做半成品
    • v1 上线就能给用户带来价值
    • v2 是在 v1 稳定运行后的增强,不是 v1 的补丁
  2. 通过接口解耦

    • 预留 MemoryProvider 接口
    • v2/v3 通过实现新 Provider 扩展,不改核心逻辑
    • 提取、检索、注入三个环节独立演进
  3. Markdown 始终是真相源

    • 即使引入数据库,Markdown 文件仍然是最终权威
    • 数据库丢失可以从 Markdown 重建,反之不行
    • 用户始终可以直接编辑 Markdown 文件
  4. 按信号升级,不按时间

    • 没有性能瓶颈就不要过早优化
    • 没有用户反馈就不要过早加功能
    • 每次升级都应该有明确的触发信号和预期收益
  5. 向后兼容

    • v2 必须能读取 v1 的 Markdown 文件
    • v3 必须能读取 v1/v2 的数据
    • 升级过程不丢失任何已有记忆

附录

A. 核心模块参考架构

bash 复制代码
src/lib/memory/                    # 核心模块(~2900 行)
├── memory-constants.js            # 配置常量、token 估算
├── memory-bootstrap.js            # Bootstrap 文件管理
├── memory-extractor.js            # LLM 提取引擎
├── memory-retriever.js            # 检索与注入
├── memory-parser.js               # Markdown 解析器
├── memory-safety.js               # 安全防护
├── memory-settings.js             # 设置管理
├── memory-deduplicator.js         # 去重引擎
├── memory-consolidator.js         # 生命周期管理
├── memory-batch-writer.js         # 批量写入
├── memory-monitor.js              # 性能监控
├── session-memory.js              # 会话记忆
└── index.js                       # 统一导出

io/                                # I/O 层(~230 行)
├── memory-io.js                   # 文件 I/O(原子写入、队列、路径安全)
└── memory-config.js               # 配置持久化

hooks/                             # 框架集成层
└── use-memory.js                  # React Hook(或其他框架的适配层)

pages/settings/                    # UI 层
└── MemorySettingsPage.jsx         # 设置页面

__tests__/                         # 测试(~1000 行)
├── memory-constants.test.js       # token 计数、预算截断
├── memory-parser.test.js          # Markdown 解析
├── memory-safety.test.js          # 注入检测、误报率
└── session-memory.test.js         # 会话记忆管理

B. 业界方案详细对比

OpenClaw

维度 详情
存储 Markdown 文件 + SQLite(向量 + FTS5 全文检索)
记忆分层 三层:Ephemeral(daily log)、Durable(MEMORY.md)、Session(transcript)
提取方式 Agent 在对话中隐式写入 daily log,定期整理到 MEMORY.md
检索方式 memory_search + memory_get 两步检索
分块策略 400 token chunk / 80 token overlap / SHA-256 去重
混合检索 70% 向量 + 30% BM25 关键词
特色 Heartbeat 机制(定期自主整理)、7 个 Bootstrap 文件
适用场景 桌面 AI 助手、开发者工具

Mem0

维度 详情
存储 向量数据库(Qdrant/Pinecone/ChromaDB)
记忆分层 用户级 + 会话级
提取方式 自动提取 + 用户画像动态维护
检索方式 语义检索(embedding cosine similarity)
特色 用户画像自动提取与更新、SaaS 开箱即用
适用场景 SaaS 产品、需要用户画像的场景

Zep

维度 详情
存储 PostgreSQL + pgvector
记忆分层 事实(Facts)+ 对话历史(Messages)+ 摘要(Summaries)
提取方式 自动摘要 + 实体提取 + 事实提取
检索方式 混合检索(向量 + 关键词 + 元数据过滤)
特色 企业级 RBAC、多租户、审计日志
适用场景 企业级 Agent、需要权限控制的场景

Letta (MemGPT)

维度 详情
存储 分层存储(核心记忆 + 归档记忆 + 外部存储)
记忆分层 Core Memory(固定上下文)+ Archival Memory(无限存储)+ Recall Memory(对话历史)
提取方式 Agent 自主决定何时读写记忆(通过 function calling)
检索方式 Agent 自主发起检索请求
特色 最接近人类记忆模型,Agent 完全自主管理
适用场景 研究项目、需要高度自主性的 Agent

方案选型决策树

markdown 复制代码
你的场景是什么?
  │
  ├─ 桌面应用 / 本地优先
  │   └─ 推荐:本文方案(Markdown First)
  │       理由:零依赖、隐私友好、用户可审查
  │
  ├─ SaaS 产品 / 需要用户画像
  │   └─ 推荐:Mem0
  │       理由:开箱即用、用户画像能力强
  │
  ├─ 企业级 / 需要权限控制
  │   └─ 推荐:Zep
  │       理由:RBAC、多租户、审计
  │
  └─ 研究项目 / 高度自主 Agent
      └─ 推荐:Letta (MemGPT)
          理由:Agent 自主管理记忆,最灵活

C. 完整提取 Prompt 模板

markdown 复制代码
你是一个记忆提取助手。从以下对话中提取值得长期记住的信息。

## 提取规则

1. 只提取明确的事实、偏好、决策,不提取闲聊和临时讨论
2. 每条记忆用一句话概括,不超过 100 字
3. 内容必须自包含------脱离对话上下文也能理解其含义
4. 标注认知类型:
   - semantic:稳定的知识和事实(用户偏好、项目信息、编码规范)
   - episodic:具体的事件(重要决策、里程碑、问题解决)
5. 标注置信度(0-1):
   - 0.9-1.0:用户明确说的("我们用 xxx"、"以后都 xxx"、"记住 xxx")
   - 0.7-0.8:可以合理推断的(用户多次使用某工具,可推断为偏好)
   - 0.5-0.6:不太确定的(可能是临时选择,不一定是长期偏好)
6. 标注持久级别:
   - long_term:稳定的长期知识,值得写入永久记忆
   - daily:当天的活动记录,不一定长期有价值
   - session:仅当前会话有意义的临时信息
7. 冲突检测:如果新信息与已有记忆冲突,在 conflict_with 中标注旧内容原文
8. 不要提取:
   - 密码、API Key、token、密钥等敏感信息
   - 一次性的调试过程、临时的报错信息
   - 纯粹的闲聊、寒暄、感谢
9. 不值得记忆的对话返回空数组 []

## 类型路由

根据内容性质路由到不同文件:
- 关于用户个人信息(名字、角色、技术栈、工作经验)→ type: "profile"
- 关于回复风格、交互偏好(简洁/详细、语言、格式)→ type: "personality"
- 关于工具、命令、环境配置(IDE、包管理器、启动命令)→ type: "tool"
- 其他所有(项目信息、编码规范、决策、事件)→ type: "memory"

## 分类参考

category 可选值:
- user_preference:用户个人偏好
- project_info:项目基本信息
- coding_convention:编码规范和约定
- tech_stack:技术栈信息
- decision:重要的技术或产品决策
- milestone:项目里程碑
- problem_solution:问题和解决方案
- workflow:工作流程和习惯

## 已有记忆(用于冲突检测)

{existing_memories}

## 对话内容

{conversation}

## 输出格式

返回 JSON 数组,每个元素格式:
{
  "content": "简洁的一句话描述",
  "memory_type": "semantic|episodic",
  "category": "分类",
  "durability": "long_term|daily|session",
  "confidence": 0.0-1.0,
  "conflict_with": null 或 "旧内容原文",
  "type": "memory|profile|personality|tool"
}

只返回 JSON 数组,不要其他内容。不值得记忆时返回 []。

D. Bootstrap 文件完整模板

IDENTITY.md

markdown 复制代码
# Agent 身份

- 名字:(你的 Agent 名称)
- 类型:AI 助手
- 风格:专业、友好、高效
- Emoji:(可选,如 🤖)

## 自我介绍
<!-- 当用户问"你是谁"时使用 -->
我是 [名字],你的 AI 助手。我可以帮你编写代码、解答问题、管理项目。

USER.md

markdown 复制代码
# 用户画像

## 基本信息
- 名字:
- 称呼偏好:
- 时区:Asia/Shanghai
- 语言偏好:中文

## 自动提取
<!-- 由 memory-extractor 自动维护,勿手动删除此 section -->
<!-- 每条带 extracted 时间戳和来源 session -->

## 备注
<!-- 用户可手动添加任何补充信息 -->

SOUL.md

markdown 复制代码
# 人格与边界

## 回复风格
- 回复简洁直接,不绕弯子
- 需要时主动提问澄清
- 默认使用中文回复

## 行为边界
- 不在外部消息渠道发送未完成的流式回复
- 不主动执行破坏性操作
- 遇到不确定的情况,先确认再行动

AGENTS.md

markdown 复制代码
# 工作区操作指南

## Memory 约定
- 长期稳定事实写入 MEMORY.md
- 每日活动记录写入 daily log
- 不存储密码、token、敏感凭证

## 安全默认值
- 不执行破坏性命令,除非用户明确要求
- 聊天中保持简洁,长内容写入文件

## 工作区特定规则
<!-- 用户可自定义补充 -->

TOOLS.md

markdown 复制代码
# 工具与环境笔记

## 项目命令
- 启动开发:`npm run dev`
- 运行测试:`npm test`
- 构建:`npm run build`

## 环境信息
- Node 版本:
- 包管理器:
- IDE:

## 常用路径
- 配置文件:
- API 入口:

HEARTBEAT.md

markdown 复制代码
# 定期任务

# 以下任务在应用启动或空闲时自动执行:
- 检查 memory/ 下过期文件(> 30 天),提示清理
- 检查 MEMORY.md 是否有重复条目
- 检查 sessions/ 下过期文件(> 7 天),提示清理

# 留空此文件可跳过所有定期任务

MEMORY.md

markdown 复制代码
# 长期记忆

## 语义记忆

### 用户偏好
<!-- 用户的个人偏好和习惯 -->

### 项目信息
<!-- 项目的基本信息和技术栈 -->

### 编码规范
<!-- 项目的编码约定和规范 -->

### 常见问题
<!-- 反复出现的问题和解决方案 -->

## 情景记忆

### 重要决策
<!-- 重要的技术和产品决策 -->

### 里程碑
<!-- 项目的重要里程碑 -->

E. 测试策略

单元测试重点

模块 测试重点 关键用例
memory-constants token 估算准确性 中英文混合文本、纯中文、纯英文、空字符串
memory-parser Markdown 解析正确性 标准格式、缺少元数据、格式不规范、空文件
memory-safety 注入检测准确性 已知注入模式、正常内容(误报率)、边界情况
session-memory 状态管理正确性 add/evict/markPromoted、容量上限、LRU 淘汰
memory-deduplicator 去重准确性 完全相同、高度相似、完全不同、阈值边界

集成测试重点

场景 验证内容
完整写入链路 对话 → 提取 → 分流 → 写入文件 → 文件内容正确
完整读取链路 读取文件 → 解析 → 预算裁剪 → 注入 Prompt → 格式正确
冲突更新 新记忆覆盖旧记忆 → MEMORY.md 更新 → daily log 记录审计
错误恢复 LLM API 故障 → 自动降级 → 恢复后自动恢复
并发安全 多个写入同时发生 → 文件不损坏 → 内容完整

本文档基于生产级 AI Agent Memory 系统的实践经验提炼,结合业界主流方案(OpenClaw、Mem0、Zep、Letta)的调研分析。适用于任何需要为 AI Agent 构建本地记忆能力的项目。

相关推荐
月落三千雪1 小时前
使用AI智能体搭建知识库-RAG语义检索
人工智能
汀沿河2 小时前
2 模型预训练、微调、强化学习的格式
人工智能·算法·机器学习
灵机一物2 小时前
灵机一物AI智能电商小程序(已上线)-产品化架构与全场景功能解析
人工智能
黄焖鸡能干四碗2 小时前
业务数据中台技术方案(PPT)
大数据·数据库·人工智能·安全·需求分析
KG_LLM图谱增强大模型2 小时前
Palantir “本体论”:是跨时代的AI架构,还是精心包装的“建表”骗局?
人工智能
东离与糖宝2 小时前
AI 智能体安全踩坑记:Java 为 OpenClaw 添加权限控制与审计日志实战
java·人工智能
love530love2 小时前
OpenClaw搭配LM Studio VS Ollama:Windows CUDA实战深度对比与完全配置指南
人工智能·windows·vllm·ollama·llama.cpp·lm studio·openclaw
王侯相将2 小时前
Claude Code 是什么?
人工智能·深度学习
Tony Bai2 小时前
【AI 智能体时代的软件工程】07 任务工程:告别 Prompt,建立“自治契约”
人工智能·prompt