记忆的写入与召回

记忆不是「聊天记录摘要」,而是一张独立的表:有作用域、有重要度、有访问计数,召回时用相似度 + 重要度 + 时间衰减 + 访问频率 + 作用域加成五个分量打分。理解这个公式,就理解了为什么有些记忆反复出现、有些从不再出现。

手动新增 source = manual 对话提取 source = conversation 工作会话提取 source = work 向量化 vec_memory_embeddings 召回打分 0.4 相似度 + 0.3 重要度 0.2 时间衰减 + 0.1 访问频率 作用域过滤 Global 恒命中 Session 同 TopicId · Project 同 ProjectId 注入上下文 默认取前 5 条 命中即更新访问时间与次数
三条写入路径共用一套向量与打分:写入决定「有什么」,打分与作用域决定「这次给模型看什么」。

记忆模型

字段说明
ScopeGlobal / Session / Project,默认 Global
ProjectId / TopicId作用域载体:Project 必须带 ProjectId,Session 必须已有 TopicId
Sourcemanual(手填)/ 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 记忆巩固。