记忆系统功能设计文档
一、产品定位
MJ Studio 的记忆系统旨在让 AI 助手真正"认识"用户,实现跨对话的智能记忆和个性化服务。不同于简单的对话历史,记忆系统能够:
- 持久化用户偏好:记住编程风格、技术栈偏好、工作习惯
- 积累项目知识:理解用户的项目背景、技术选型、业务目标
- 学习失败教训:避免重复相同的错误决策和配置
- 跨会话连续性:从上次对话无缝延续,无需重复背景介绍
二、用户感知到的效果
场景 1:编程偏好记忆
第一天对话:
用户:"我喜欢用 TypeScript,代码风格偏向函数式编程,多用组合而不是继承"
助手:"好的,我记住了你的偏好!"一周后对话:
用户:"帮我重构这个用户管理模块"
助手:"基于你喜欢的函数式风格,我建议使用纯函数和组合模式,
用 TypeScript 的类型系统保证安全性..."场景 2:失败教训学习
第一次失败:
用户:"Docker 构建又失败了,网络超时"
助手:"我记录了这个问题,你的环境需要使用 proxy-env 来运行 docker 命令"后续提醒:
用户:"我要启动 docker compose"
助手:"提醒:你的环境需要代理,建议使用 'proxy-env docker compose up -d'"场景 3:项目知识积累
多次对话后:
用户:"帮我写个数据库查询"
助手:"根据你的项目使用 Drizzle ORM + SQLite,这里是查询代码:..."
(无需每次重复说明技术栈)三、记忆的分类和层级
3.1 按作用域分类
| 作用域 | 说明 | 示例 | 命名空间 |
|---|---|---|---|
| 用户级 | 跨所有助手、对话共享的长期记忆 | 编程习惯、技术栈偏好、项目背景 | user:{userId} |
设计原则:
- 助手特定记忆 → 使用助手的 System Prompt 实现(如角色设定、专业领域)
- 对话上下文 → 依靠 LLM 工作记忆(最近消息)和对话压缩功能
3.2 按内容类型分类
| 类型 | 说明 | 示例 | 重要性权重 |
|---|---|---|---|
| 偏好 | 用户的主观喜好 | "喜欢TypeScript"、"偏好简洁代码" | 0.9 |
| 事实 | 客观的用户信息 | "正在开发MJ Studio"、"使用Nuxt 4" | 0.8 |
| 教训 | 失败经验和解决方案 | "Docker需要代理才能构建" | 0.85 |
| 目标 | 用户的待办和计划 | "计划添加视频生成功能" | 0.7 |
| 情境 | 临时的上下文信息 | "刚才提到的那个API" | 0.4 |
3.3 按时间维度分类
| 维度 | 生命周期 | 遗忘策略 |
|---|---|---|
| 核心记忆 | 长期(永久) | 不遗忘,除非用户删除 |
| 工作记忆 | 中期(30天) | 按访问频率衰减 |
| 临时记忆 | 短期(7天) | 快速衰减 |
四、用户界面设计
4.1 记忆管理页面
位置:设置页面 → 记忆管理(/settings/memories)
页面结构:
┌─────────────────────────────────────────────┐
│ [搜索记忆...] [清空所有记忆] │
├─────────────────────────────────────────────┤
│ 筛选: │
│ [ 全部 | 偏好 | 事实 | 教训 | 目标 ] │
├─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ 🎨 偏好 | 3天前 │ │
│ │ 我喜欢函数式编程,多用组合少用继承│ │
│ │ 访问:5次 | 重要性:90% [删除] │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 💡 事实 | 1周前 │ │
│ │ 正在开发 MJ Studio 多模态 AI 工作台 │ │
│ │ 访问:12次 | 重要性:85% [删除] │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ ⚠️ 教训 | 5天前 │ │
│ │ Docker 构建需要使用 proxy-env 代理 │ │
│ │ 访问:3次 | 重要性:80% [删除] │ │
│ └─────────────────────────────────────┘ │
│ │
│ [加载更多] │
└─────────────────────────────────────────────┘4.2 对话中的记忆提示
助手回复时的记忆引用:
助手:"基于你的偏好(函数式编程 🧠),我建议..."
────────────────
点击可查看完整记忆记忆更新确认:
助手:"我记住了这个偏好!✅
已添加到记忆:「喜欢使用 TypeScript 严格模式」
[查看] [撤销]"4.3 记忆对用户的呈现维度
用户在管理界面看到的每条记忆包含:
- 内容:记忆的文本描述
- 类型:偏好/事实/教训/目标(图标 + 标签)
- 时间:创建时间(相对时间显示)
- 活跃度:访问次数
- 重要性:0-100% 的评分
- 来源:从哪次对话提取(可点击跳转)
- 状态:活跃/已遗忘(已遗忘的记忆置灰但保留)
4.4 隐私控制
临时对话模式:
- 位置:对话输入框旁边的开关
- 效果:开启后,本次对话不提取记忆,也不使用已有记忆
- 用途:敏感话题、测试场景
记忆开关:
- 位置:设置页面
- 选项:
- ✅ 启用记忆系统
- ✅ 自动提取记忆(关闭则需手动确认)
- ✅ 对话中显示记忆引用
五、记忆的提取和召回机制
5.1 技术选型:Zep
我们使用 Zep 作为记忆系统的核心引擎,原因如下:
- 时序感知:每个记忆追踪生效时间和失效时间,避免返回过时信息
- 知识图谱:维护实体和关系,支持复杂的记忆推理
- 混合检索:结合语义搜索(向量)+ 关键词搜索(BM25)+ 图遍历
- 自动提取:内置 LLM 驱动的记忆提取逻辑
- 开源自托管:通过 Docker 部署,数据完全可控
5.2 记忆提取机制
提取时机:
| 触发条件 | 说明 |
|---|---|
| 异步批量提取 | 每 5 轮对话提取一次(推荐,降低成本) |
| 流式完成后 | AI 回复生成完成后立即提取 |
| 手动触发 | 用户点击"记住这个"按钮 |
提取流程:
用户发送消息
↓
AI 生成回复(流式输出)
↓
流式完成后,触发记忆提取(异步,不阻塞用户)
↓
1. 获取对话上下文(最近 10 条消息,~4000 tokens)
↓
2. 调用 LLM 进行记忆提取
System Prompt: "从以下对话中提取值得长期记住的信息..."
输入: 最近 10 条消息
输出: 结构化记忆列表(JSON)
↓
3. LLM 返回记忆候选
[
{ content: "用户喜欢函数式编程", type: "preference", importance: 0.9 },
{ content: "项目使用 Nuxt 4", type: "fact", importance: 0.8 }
]
↓
4. 将记忆存储到 Zep
- 生成 embedding(向量化)
- 存储到知识图谱
- 检测并解决冲突(相同主题的旧记忆)
↓
5. 同步元数据到本地数据库(memories_metadata)
↓
6. 通过全局事件通知前端(memory.created)提取窗口策略:
| 配置项 | 值 | 理由 |
|---|---|---|
| 窗口大小 | 最近 10 条消息(5 轮对话) | 平衡上下文完整性和成本 |
| Token 限制 | ~4000 tokens | 足够提取关键信息,成本可控 |
| 提取频率 | 每 5 轮对话 | 避免过度提取,降低 API 成本 |
为什么不用完整对话历史:
- 成本问题:100k tokens 的对话历史每次提取成本高昂
- 噪声问题:大部分对话内容是临时性的,不值得长期记忆
- LLM 能力:最近 10 条消息已足够 LLM 理解上下文并提取关键信息
5.3 记忆召回机制
召回时机:
每次用户发送新消息时,在生成 AI 回复之前执行召回。
召回流程:
用户发送新消息
↓
1. 构建召回查询
查询内容 = 用户当前消息 + 最近 3 条上下文消息
示例: "用户: 帮我重构这个模块\n上下文: 刚才讨论了用户管理..."
↓
2. 向 Zep 发起混合检索
- 语义搜索:query embedding 与记忆 embedding 的相似度
- 关键词搜索:BM25 算法匹配关键词
- 图遍历:基于实体关系查找相关记忆
- 时序过滤:只返回有效记忆(validUntil 为 null 或未来)
↓
3. Zep 返回 Top-3 相关记忆
[
{ content: "用户喜欢函数式编程", relevance: 0.92, type: "preference" },
{ content: "项目使用 TypeScript", relevance: 0.85, type: "fact" },
{ content: "避免使用 class 继承", relevance: 0.78, type: "lesson" }
]
↓
4. 将记忆注入到 System Prompt
System Prompt:
"""
你是一个 AI 助手。
关于用户的背景信息:
- 用户喜欢函数式编程
- 项目使用 TypeScript
- 避免使用 class 继承
(原有的助手提示词...)
"""
↓
5. 组合完整上下文,调用 LLM 生成回复
上下文 = System Prompt(含记忆)+ 最近 10 条消息 + 用户新消息
↓
6. 流式返回 AI 回复召回策略:
| 配置项 | 值 | 理由 |
|---|---|---|
| 召回数量 | Top-3 | 避免噪声,聚焦最相关的记忆 |
| 查询增强 | 当前消息 + 最近 3 条上下文 | 提升召回准确性 |
| 时序过滤 | 只返回有效记忆 | 避免过时信息干扰 |
| 相关性阈值 | 0.7 | 低于此分数的记忆不注入 |
为什么不用 MCP/函数调用让 LLM 主动搜索:
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| 被动注入(我们的方案) | 确定性强、延迟低、成本可控 | LLM 无法主动搜索 | ✅ 适合我们 |
| MCP/函数调用 | LLM 自主决策何时搜索 | 不确定性高、多次往返、成本高 | ❌ 不适合 |
| 小模型路由 | 成本低 | 召回质量依赖小模型能力 | 🤔 可作为优化方向 |
我们选择被动注入方案,因为:
- 每次对话都需要记忆:用户期望 AI 始终"记得"他们的偏好
- 延迟优化:提前注入记忆,无需等待 LLM 决策是否搜索
- 成本可控:Top-3 记忆注入固定成本,而函数调用可能多次往返
5.4 核心组件
Zep 服务(Docker)
├── 时序知识图谱存储(Postgres + pgvector)
├── 向量检索引擎(混合搜索)
└── 记忆提取/更新 API
应用层封装(server/services/memory.ts)
├── MemoryService 类
├── 提取逻辑(extractFromConversation)
├── 召回逻辑(buildRecallQuery + search)
├── 冲突解决(时序优先)
└── 遗忘机制(软删除 + 时间衰减)
前端组合式函数(app/composables/useMemory.ts)
├── 记忆检索
├── 记忆管理
└── 实时更新(通过全局事件)5.5 数据库扩展(本地元数据)
表:memories_metadata
| 字段 | 类型 | 说明 |
|---|---|---|
| id | integer | 主键 |
| zepMemoryId | string | Zep 中的记忆 ID |
| userId | integer | 用户 ID |
| type | enum | preference/fact/lesson/goal/context |
| importance | float | 重要性评分 0-1 |
| confidence | float | 置信度 0-1 |
| accessCount | integer | 访问次数 |
| lastAccessedAt | timestamp | 最后访问时间 |
| isForgotten | boolean | 是否已遗忘(软删除) |
| validFrom | timestamp | 事实生效时间(时序关键) |
| validUntil | timestamp | 事实失效时间(可为 null) |
| supersedes | integer[] | 覆盖了哪些旧记忆的 ID |
| sourceConversationId | integer | 来源对话 ID |
| sourceMessageId | integer | 来源消息 ID |
| createdAt | timestamp | 创建时间 |
| updatedAt | timestamp | 更新时间 |
设计说明:
- Zep 负责核心存储和检索
- 本地数据库存储元数据(类型、统计)
- 时序字段(validFrom/validUntil):处理偏好变化,确保时序正确性
- 关系字段(supersedes):追踪记忆演化链条
- 软删除通过
isForgotten字段实现 - 仅用户级记忆:移除了 scope/scopeId 字段,所有记忆都是用户级
5.6 API 端点
| 端点 | 方法 | 功能 |
|---|---|---|
/api/memories | GET | 获取记忆列表(分页、筛选) |
/api/memories/search | POST | 语义搜索记忆 |
/api/memories | POST | 手动添加记忆 |
/api/memories/:id | DELETE | 删除记忆(软删除) |
/api/memories/:id/restore | POST | 恢复已遗忘的记忆 |
/api/memories/extract | POST | 从对话提取记忆(内部) |
/api/memories/stats | GET | 记忆统计(总数、类型分布) |
5.7 核心服务方法
MemoryService 类:
extractFromConversation(messages)- 从对话提取记忆(固定窗口:最近 10 条)buildRecallQuery(userMessage, recentContext)- 构建召回查询(消息 + 最近 3 条上下文)search(query, userId, options)- 混合搜索(语义 + BM25 + 图遍历)add(content, type, metadata)- 添加记忆update(id, data)- 更新记忆(访问次数、重要性)resolveConflict(newMemory, oldMemory)- 冲突解决(时序优先)softDelete(id)- 软删除记忆restore(id)- 恢复记忆list(userId, filters, pagination)- 列出记忆(支持时间范围过滤)calculateRelevance(memory)- 计算记忆相关性(时间衰减 + 访问频率)markForForgetting(threshold)- 批量标记低价值记忆getStats(userId)- 获取统计信息
5.8 记忆冲突解决机制
问题示例:
第1天:用户说"我喜欢用 Vue 3"
第30天:用户说"我现在更喜欢用 React"处理策略(时序优先原则):
- 检测冲突:同类型、同主题的记忆
- 标记失效:旧记忆的
validUntil = 新记忆的 validFrom - 建立关系:新记忆的
supersedes = [旧记忆 ID] - 保留历史:旧记忆不删除,支持时间推理("你5个月前喜欢Vue")
召回优先级:
- 有效记忆(validUntil 为 null 或未来)> 已失效记忆
- 最近的记忆 > 旧记忆
- 高置信度 > 低置信度
5.9 前端 Composable
useMemory:
memories- 响应式记忆列表search(query, filters)- 搜索记忆list(filters, page)- 分页列出remove(id)- 删除记忆restore(id)- 恢复记忆stats- 统计信息
六、遗忘机制
6.1 遗忘策略
相关性评分公式:
相关性 = 时间衰减系数 × (1 + 访问频率权重) × 重要性权重
其中:
- 时间衰减系数 = exp(-0.01 × 天数)
- 访问频率权重 = log(1 + 访问次数)
- 重要性权重 = importance (0-1)遗忘阈值:
- 相关性 < 0.1:标记为已遗忘
- 相关性 < 0.3:降低重要性评分
- 相关性 > 0.7:视为活跃记忆
6.2 定时任务
每日凌晨执行(server/tasks/memory-maintenance.ts):
- 计算所有记忆的相关性评分
- 标记低于阈值的记忆为已遗忘
- 检测矛盾的记忆(如偏好冲突)
- 生成记忆质量报告
6.3 软删除设计
已遗忘的记忆:
- ✅ 保留在数据库中(
isForgotten = true) - ✅ 不参与检索和推理
- ✅ 用户可在管理界面查看和恢复
- ✅ 支持导出备份
真正删除的触发条件(可选,暂不实现):
- 用户手动"永久删除"
- 已遗忘超过 365 天且从未恢复
七、MVP 阶段计划
7.1 目标
- ✅ 部署 Zep 服务(Docker)
- ✅ 实现基础的记忆提取和检索
- ✅ 使用测试对话观察记忆质量
- ❌ 不集成到生产对话流程
7.2 测试场景
准备 3 类测试对话:
编程偏好对话:
- "我喜欢函数式编程"
- "我常用 TypeScript 严格模式"
- "我不喜欢用 class 继承"
项目信息对话:
- "我在开发 MJ Studio"
- "项目用 Nuxt 4 + SQLite"
- "遇到了 Docker 网络问题"
失败教训对话:
- "Docker 需要使用 proxy-env"
- "SQLite 不支持某些复杂查询"
- "上次用这个方案失败了"
7.3 观察指标
- 提取准确性:是否正确识别出关键信息?
- 细节保留:专有名词、具体参数是否被记住?
- 类型分类:自动分类为偏好/事实/教训是否准确?
- 去重效果:相似表述是否被合并?
- 检索质量:搜索"编程偏好"能否找到相关记忆?
7.4 MVP 不包含的功能
- ❌ 自动遗忘(手动测试遗忘逻辑)
- ❌ 对话中的实时记忆引用
- ❌ 用户管理界面
- ❌ 与助手的集成
八、正式实施阶段
8.1 集成点
对话生成流程(详细):
用户发送消息
↓
1. 获取工作记忆(最近 10 条消息)
↓
2. 构建召回查询(当前消息 + 最近 3 条上下文)
↓
3. 检索长期记忆(Top-3 相关记忆,时序过滤)
↓
4. 组合上下文:
- System Prompt(基于长期记忆)
- Messages(工作记忆)
↓
5. LLM 生成回复(流式输出)
↓
6. 异步提取记忆(流式完成后,最近 10 条消息窗口)
↓
7. 检测冲突并解决(时序优先)
↓
8. 发送全局事件通知前端关键实现细节:
- 工作记忆和长期记忆分离
- 召回查询增强(避免"好的按第一个方案办"无法召回)
- 提取不阻塞流式输出
- 每 5 轮批量提取一次(降低成本)
8.2 全局事件集成
新增事件类型:
memory.created- 记忆创建memory.updated- 记忆更新memory.deleted- 记忆删除memory.forgotten- 记忆被遗忘memory.restored- 记忆恢复
事件订阅(app/composables/useMemory.ts):
typescript
globalEvents.on('memory.created', (memory) => {
// 更新本地记忆列表
})8.3 用户设置
设置页面新增选项:
- 记忆系统总开关
- 自动提取记忆(关闭则需手动确认)
- 对话中显示记忆引用
- 临时对话模式(全局快捷键 Ctrl+Shift+T)
九、性能和成本考虑
9.1 延迟控制
- 记忆检索:< 200ms(Zep 优化后)
- 记忆提取:异步执行,不阻塞对话
- 批量操作:使用后台任务
9.2 存储成本
- Zep 存储:向量数据库(Postgres + pgvector)
- 本地元数据:SQLite(轻量)
- 预估:1000 条记忆约 10MB(含向量)
9.3 API 调用成本
实际成本估算(GPT-4o Mini,$0.15/1M input):
| 操作 | 频率 | Token/次 | 月调用量 | 月成本 |
|---|---|---|---|---|
| 记忆提取 | 每 5 轮 | 4k tokens | 80,000 | $48 |
| 召回查询 | 每次对话 | Embedding only | 300,000 | $39 |
| 冲突检测 | 提取时触发 | 向量计算(免费) | - | $0 |
| 总计 | - | - | - | $87/月 |
对比全上下文方案:
- 全上下文:~$750/月(每次发送完整历史)
- 我们的方案:$87/月
- 节省 88%
十、后续演进方向
10.1 短期(3 个月内)
- ✅ 完成 MVP 测试
- ✅ 集成到对话流程
- ✅ 上线记忆管理界面
- ✅ 收集用户反馈
10.2 中期(6 个月内)
- ✅ 自动遗忘机制上线
- ✅ 记忆质量自动评估
- ✅ 多助手间记忆共享策略
- ✅ 记忆导出/导入功能
10.3 长期(1 年内)
- ✅ 基于用户反馈训练记忆选择策略
- ✅ 多模态记忆(图片、视频)
- ✅ 记忆可视化(知识图谱展示)
- ✅ 记忆推理能力(从多条记忆推导新知识)
十一、风险和挑战
11.1 技术风险
- Zep 稳定性:开源项目维护不确定性
- 细节丢失:尽管双重索引,仍可能丢失极细节信息
- 向量检索准确性:依赖 Embedding 模型质量
缓解措施:
- 本地备份所有记忆(SQLite 元数据)
- 关键记忆人工审核
- 定期测试检索质量
11.2 产品风险
- 用户困惑:记忆系统可能让用户感到"被监视"
- 错误记忆:AI 误解用户意图,存储错误信息
- 隐私担忧:敏感信息被记录
缓解措施:
- 首次使用引导和说明
- 记忆可见化,用户随时删除
- 临时对话模式
- 敏感信息自动过滤(正则 + LLM 检测)
11.3 成本风险
- 向量存储成本:长期运行后可能膨胀
- LLM 调用成本:记忆提取增加 API 调用
缓解措施:
- 遗忘机制控制总量
- 使用小模型进行记忆提取
- 批量处理降低调用频率
十二、成功指标
12.1 MVP 阶段
- ✅ 提取准确率 > 80%
- ✅ 检索相关性 > 75%
- ✅ 细节保留率 > 70%(专有名词、数字)
12.2 正式上线后
- ✅ 用户启用率 > 60%
- ✅ 记忆系统带来的对话满意度提升 > 20%
- ✅ 平均每用户记忆数稳定在 100-500 条
- ✅ 遗忘率 < 30%(大部分记忆保持活跃)
12.3 长期目标
- ✅ 对话重复说明背景的频率降低 > 50%
- ✅ 用户感知到的"AI 理解我"程度显著提升
