记忆的写入与召回
记忆不是「聊天记录摘要」,而是一张独立的表:有作用域、有重要度、有访问计数,召回时用相似度 + 重要度 + 时间衰减 + 访问频率 + 作用域加成五个分量打分。理解这个公式,就理解了为什么有些记忆反复出现、有些从不再出现。
记忆模型
| 字段 | 说明 |
|---|---|
Scope | Global / Session / Project,默认 Global |
ProjectId / TopicId | 作用域载体:Project 必须带 ProjectId,Session 必须已有 TopicId |
Source | manual(手填)/ conversation(对话提取)/ work(工作会话提取) |
Category | 分类标签,随内容一起给模型看 |
Importance | 默认 0.5,创建与更新都会 Clamp(0, 1) |
AccessCount / LastAccessedAt | 召回副作用:命中即 +1 并刷新时间,两者参与打分 |
Score | 只在检索结果里返回,不落库 |
三条写入路径
| 路径 | 行为 |
|---|---|
POST /api/memories | 手动新增;不允许手动建 Session 作用域(会话记忆由提取产生),Project 作用域必须给 ProjectId |
POST /api/memories/extract/{topicId} | 从对话提取(conversation);自动入口按「用户消息数 / 时间 / 密度」三个信号判断是否值得提取 |
POST /api/memories/extract-work/{sessionId} | 从工作会话提取(work);每累计 10 条用户消息触发一次,且仅当会话关联受管项目时才提取 |
「每 10 条用户消息」与「仅受管项目」这两条约束是有意的:工作会话噪音大、token 贵,宁可不提取也不要写一堆无意义记忆进来。
召回与打分
| 分量 | 权重 / 公式 |
|---|---|
| 相似度 | 0.4 × similarity(向量检索) |
| 重要度 | 0.3 × importance |
| 时间衰减 | 0.2 × exp(-0.05 × 距上次访问天数) |
| 访问频率 | 0.1 × log(1 + AccessCount) / log(51),50 次封顶 |
| 作用域加成 | 按作用域额外加权,Global 恒命中,Session 只匹配同 TopicId,Project 只匹配同 ProjectId |
结果按总分降序取前 K 条:注入上下文时默认 topK = 5,检索页默认 topK = 10。被命中的记忆会立刻写回 LastAccessedAt 与 AccessCount——这意味着用过的记忆下次更容易被用到,是一个正反馈回路,排查「某条无关记忆总出现」时要看它的访问计数。
工作会话注入时用的是「全局 ∪ 当前项目」两段并集(带入项目的 ManagedProjectId),这样项目规则类记忆只在对应项目内生效。
存储与检索介质
- SQLite 走
sqlite-vec虚表vec_memory_embeddings;PostgreSQL 走MemoryEmbeddings的vector列。 - 目前没有记忆的全文(FTS)检索路径:召回完全是向量 + 打分。
- 编辑记忆会重算 embedding;删除是软删并同时移除 embedding 与 vec 行。
- 没有记忆的版本历史表:编辑即覆盖。
观测与常量
| 常量 | 值 |
|---|---|
| 打分权重 | Alpha = 0.4 · Beta = 0.3 · Gamma = 0.2 · Delta = 0.1 |
| 衰减系数 | DecayLambda = 0.05 |
| 工作会话自动提取间隔 | AutoExtractInterval = 10 条用户消息 |
| 对话自动提取时间信号 | TimeTriggerMinutes = 30 |
实作要点
- 作用域选错是记忆问题的主因:项目专属的约定不要写成
Global,否则会污染其他项目的对话。 - 重要度是唯一可以人工干预的长期杠杆:把噪音记忆降到
0.1附近,它自然会被衰减掉。 - embedding 模型换了要重新索引记忆,否则相似度分量失真;这一点与笔记索引同源。
- 记忆的自动提取与 Dream 巩固是两件事:前者写、后者合;整理质量看 Dream 记忆巩固。