Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

记忆算法概述

⚠️ 文档状态:本文档为算法设计总览(2026-03 修订)。实现状态对照(2026-08): 检索——baseline 向量 top-k ✅、HippoRAG 式 PPR(weighted_ppr_fp)✅、Soul-Retr 的 混合分级检索(LLM 判断循环/PoG/记忆向量更新/反馈回路)❌ 未实现(实际为固定三步管线, 见 检索与联想);巩固——滑动窗口摘要 ✅,LLM 拆解整合 ❌ (见 巩固算法);遗忘——Ebbinghaus 衰减 ✅、遮罩法 ✅ (三档 NoAction/MaskOnly/Revised,见 遗忘算法),记忆锚点 ❌。 状态机 Working/Idle ✅。

记忆算法是SoulMem中另一极为关键的版块。它的功能是对记忆图进行操作

相对主流的实践中,LLM通常扮演大脑的角色,由LLM来调配各个模块的运作,这一机制通常是由**“工具”来实现的,即将外部模块封装为可供LLM调用的一系列工具。或者利用一个明确的工作流**,LLM只是其中的一个环节

SoulMem的设计属于类似工作流的思路(只不过准确来说是一个有状态的工作图),LLM在其中只是作为一个强大的末端执行器,这种思路有以下好处:

  • 更加可控,决策逻辑部分由人构思实现
  • 更少的token用量
    • 即便在工作流的模式中,我们依然要使用诸如提示词工程的手段,其token用量仍比工具调用要少,因为工具的使用方法通常也是通过提示词注入LLM的上下文的。这也导致LLM通常不能驱动大量的工具(嗯跟CPU一个道理)
      • (也更加省钱)
  • 更加普适
    • 工具调用能力不同的模型有很大差异,工具调用能力较差的模型,可能完全无法通过工具的方式运作。这是不希望看到的,尤其对于角色扮演任务来说,人物性格的遵循能力可能要更为重要,这会导致一些潜在的优秀模型无法使用。

因此,既然LLM作为末端执行器,那么获取执行所需的数据,和整个记忆系统的状态维护,就都是我们需要实现的算法。因此更准确的说,记忆算法的根本目的就是收集提供给LLM的数据,和维护记忆系统的自身状态

:SoulMem主要的状态维护是一个状态机,主要由两个状态,WorkingIdleWorking表示当前正在积极处理用户交流,即检索正在频繁工作。idle表示当前没有用户交流,检索没有工作)

模仿人类的记忆功能,记忆算法可以分为以下三大类

  • 检索/联想
  • 巩固
  • 遗忘

下面将分别介绍这三类算法


检索/联想

检索/联想算法指的是,对于一个给定的查询要求(暂且考虑为用户的信息本身),通过对记忆(子)图的搜索,找到符合查询要求的信息。检索出的信息经过某种手段(这种手段不是检索/联想算法的一部分),将作为提供给LLM的上下文使用。

baseline - 基于向量相似搜索的top-k方法

这是最经典的RAG思路,将记忆内容输入嵌入模型获得向量化的索引,在检索时,将用户信息以同样的模型向量化,通过一些算法(如HNSW)与记忆系统中的内容做相似性搜索(通常为余弦相似度),选取相似度最高的k条记忆,这k条记忆将被送入大模型的上下文。

这一方法有很多变种,例如加上Rerank模型来使top-k有更好的的相关性等。

这一方法的优点在于简单快速,不过缺点同样非常明显:

  • 它不能处理复杂查询关系,例如多跳问题
    • 多跳问题的经典案例, “姚明的妻子的父亲的出生地是哪里”
  • 它不能进行联想
    • “钟离假死” 和 “米哈游”在向量空间上距离应该会比较大,但是这两个实体通常是相关的,如果现在某个数字生命看到了“钟离假死”,采用此方法,它不太会输出类似“玩米哈游玩的”这类语言

因此,后续的方法对这些缺点进行了改善。

类HippoRAG - 基于知识图谱和PPR算法的检索

是的我们跳过了很大一段的发展历程直奔类似GraphRAG的一系列方法。

HippoRAG将文档内容,预先通过LLM进行IE(信息抽取, Information Extraction)生成知识图谱,在检索时,通过向量搜索对应到图上的一些节点。以这些节点为起点执行PPR算法。由于PPR算法的结果是各个节点对于给定起点列的“重要程度”,因此去PPR分数的top-k个,作为送给LLM的内容。

这个方法高效的解决了上述两个问题:

  • 多跳问题
    • PPR的算法是一个在图上游走的过程,他可以提取需要多步推理的知识问答问题
  • 联想
    • 虽然HippoRAG主要是构建知识库用的,但这样的算法结构同样可以支持“梗文化”相关的联想,只要在知识图谱中将两个实体用边关联,PPR算法就可以发现这些节点,虽然可能与人脑的联想机制有出入,但实现了相似的效果。

但是,考虑到SoulMem的具体场合,HippoRAG显得略微有些不足:

  • 连接强度的考虑
    • 由于SoulMem系统包含遗忘机制,边的连接具有一个权重,代表连接强度,强度越高的节点之间越容易发生联想,这代表边权需要被考虑在PPR算法的执行中
      • 需要使用PPR算法的变种
  • 节点的考虑
    • HippoRAG构建的是传统的三元组知识图谱,不适合表示一段动态的经历,这部分由SoulMem的数据结构解决,本文不再赘述

Soul-Retr(为了方便写文档,暂时先叫这个吧)

基于以上两种典型的检索方式,我们需要在此基础上进行改进,形成Soul-Retr算法。

有模式记忆的混合分级检索(最高实现优先级)

流程如下:

  • 分解子问题,并根据角色设定,以及当前的心情等状态生成检索指导(要检索到什么程度,随便有内容就行还是仔细分析判断?检索什么倾向的内容,具体情境或者抽象概念?)
  • 找到种子节点,这部分基于向量相似性搜索加取top-k
  • 通过记忆向量扩展查询子图
    • 扩展的判断式中,论文是基于节点之间的相似性的,我们肯定是不能这么干的,毕竟网络热梗可以让两个本来毫不相干的概念关联起来,图的拓扑结构是我们判断关联的唯一判据。
    • 我们可以考虑变更这一项,比如换成搜索倾向(倾向于是找情景记忆还是语义记忆?),当前心情(你知道的,人在不耐烦的时候不太喜欢想一些搞七搞八的东西)等指标的混合,这会让记忆子图的扩展更加灵活。
  • 让LLM判断是否足够
  • 不足够,运行PPR变种(考虑边权的那种,边权动态构建,构建的参数如上所述),取top-k加入查询子图
  • LLM判断是否足够
  • 不足够,退化为PoG,说明问题看起来非常复杂,例如高学术研究哲学讨论,这种情况下人也得慢慢思考,可以接受
  • 不管怎么样我们都根据查询子图更新记忆向量(用ReMindRAG的方法),人的思维路径就是越激活越强的
    • PPR在这里有点小问题,PPR得到的是单独的节点,但没法根据PPR结果形成达到结果节点的路径,我们或许有两种方式处理:
      • 扩展子图把结果节点包进去,感觉非常暴力
      • 先从起始节点建立与PPR节点的假关联(dummy),让LLM去分析哪条路径贡献得到了这个结果,如果有条假关联被激活了,那么我们用A*算法找一条真正能从这个起始节点到那个目标节点的一条路径并更新其上的记忆向量
  • 我们还可以引入外部反馈,角色回答后用户会给出反馈,LLM分析我们的检索结果是好还是坏(这大概只有在角色认真并在意这个的时候才会需要),通过外部反馈再去根据反馈的那个检索的查询子图去更新记忆向量,外部反馈权重比内反馈大。

基于EdgePush-PPR(上一个方法的其中一部分)

基于HippoRAG的缺点,我们把边权考虑进去,采用EdgePush-PPR算法。

有以下几个考虑点:

  • 要怎样对文本进行向量化
    • 直接向量化?或者附加上这句话的tags的向量?或者其他的方式
    • 这将很大程度上影响起始节点
  • “边权的构造”
    • 连接强度是肯定包含的,那么其他的呢?例如节点类型,是否应该纳入考虑?
  • 分级路由策略?
    • baseline已经能应对相当一部分的纯日常简单问答场合
    • 可以考虑简单问题直接使用baseline,复杂问题再运行PPR
  • PPR次数?
    • HippoRAG是1次,但是我们有不同类型的节点,我们应该期望结果中三种节点的占比如何(或者认为占比不重要无需考虑)?
    • 如果要多次,每次PPR的参数是否要有变化,有怎么样的变化?
  • 以及其他奇奇怪怪的细节问题~

这些问题或许不能完全立马确定,我们可以考虑先选择简单的实现,发现效果不良后再进行改进

这个方法的好处是前辈们的工作比较多,理论也比较完善,效果是有一定保证的。

基于神经动力学(应该?)(较低优先级)

我们假设图具有多个有向环路

如果我们直接把节点看做一个神经元的细胞体,那么经过向量相似性搜索后,我们可以认为初始节点被一个初始电信号**“激活”。初始节点将沿着它的轴突传向邻近的神经元**,邻近的神经元因此被激活进一步传递信号给下一个神经元。这样以涟漪的形式扩散开去。

这个系统应当具有一个稳态,由于整个数据结构是图的形式,神经元相互连接形成回环,最终如果达到稳态,那么每个神经元都应该保有一定的**“电位”,电位越高代表激活强度越高**,我们可以选电位的top-k个节点送给LLM。

这个扩散过程应当可以构建动力学方程,即转化为求解稳态分布的问题。

这个方法和PPR的区别在于,PPR是一个人从起始节点开始随机游走,任意时刻它只会位于一个节点上。而此方法是一个更倾向于扩散的系统,电信号从起点以涟漪扩散传播,它更好的模拟了神经元的生理行为。

重要!!!

在描述本方法时,笔者大量使用了不确定性的词语,因为笔者并没有进行数学上的验证,笔者并不确定

  • 有环路的图是否一定可以收敛到一个稳态
  • 是否可以构建起动力学方程
  • 是否可以高效的求解这个方程获得稳态分布
  • 获得的稳态分布中,电位top-k节点是否代表应该选取的节点

本方法必须要求图中具有环路,若没有环路,则信号以此传递,最终稳态电位都是0,必然不可行,因此对于无环的图,需要成环处理(虽然PPR也需要处理无出度节点的问题)。

本方法唯一具有的优势可能就是它更好的模拟了人脑的生物过程,如果前面所说都能成立,它或许会有更好的效果。但从工程角度考虑,此方法绝对不应该被优先实现


巩固

巩固指的是将记忆向长期记忆的方向转化的过程,即从短期 -> 工作, 从工作 -> 长期,这是持久化记忆的关键。它的目的是维护记忆系统的自身状态,主要是增加和更新内容

通常来说,巩固只在Idle状态下执行。

巩固算法的参考比较少,具有自我进化的记忆系统本来就少(

短期 -> 工作

这部分应该是目前SoulMem最完善的算法,主要处理的是**“增加”**。

我们维护一个滑动窗口,滑动窗口存储用户对话LLM生成原始信息。滑动窗口具有一个固定大小,他是一个FIFO(先进先出)队列。我们把信息加入滑动窗口叫做“滑入”,把因为滑动窗口已满并且又有新信息加入而有信息移出滑动窗口的过程叫做“滑出”。

滑入的信息中,每间隔一个滑动窗口的大小,就会被打上一个标记,这个标记也可以由一些机制强制打上(例如对话结束,切换话题等时候)。每当带有标记的信息滑出滑动窗口时,触发一次**“摘要”,将当前滑动窗口中的信息送入LLM,让LLM进行总结,LLM输出的内容称为“摘要记忆”**。

摘要记忆只有一份,当存在摘要记忆且有带标记的信息滑出时,将摘要记忆滑动窗口的内容同时送往LLM,生成一份新的摘要记忆。这样,摘要记忆中就持续的记录了当前对话中的信息。到此处,这是一个经典的管理LLM上下文的方法。

在每次有用户信息时,检索算法会被调用。不论采用何种检索算法的实现,总是有一些节点被提取出来。记录这些节点的提取时间戳(每一次都记),提取次数等信息,这些记录称为**“提取记录”**。

:如何编写提示词生成符合预期要求的摘要不属于记忆算法的考虑范围内,这部分调整,实验起来都非常方便,可以留到最后解决)

以上的部分是在Working状态下工作的。

当从Working状态转为Idle状态时,将摘要记忆送往LLM,将其拆分成多个MemoryNote(也就是记忆图的节点),根据提取记录中的数据,计算这些记忆的提取频率,具体为: $$ f = \frac{n}{T} $$ 其中f提取频率n提取次数T提取首末时间戳之差

提取频率top-k节点,将这些节点,和从摘要记忆中拆出来的多个节点送往LLM,让LLM建立起这些节点的联系。这样我们就可以把从摘要记忆中拆分出的多个节点加入工作记忆,转化完成,摘要记忆也就可以清空了。

说明:

采用提取频率作为筛选提取记录的原因是基于赫布学习理论,赫布学习理论描述了两个神经元之间的共激活频率越高,它们之间的连接就会更紧密。

我们知道在主流的记忆系统中,这一指标往往是相关性而非提取频率,这是因为目前主流的记忆系统服务与Agent工具,主要应用于代码智能体,学术研究,文献检索,推理等理性为主的任务。

然而角色扮演是一个非理性的任务,一个角色完全有理由将“桃子”与一个活生生的角色关联起来,可以从上数学课关联到干饭,甚至可以有更加逻辑上很离谱的关联,例如意大利面42号混凝土,而相关性指标完全无法胜任这些关联任务。除非加以引导提示,大模型通常不会将意大利面和42号混凝土关联,然而这种关联是非常重要的,因为检索算法完全依赖于这些关联。如果意大利面和42号混凝土无法建立关联,那么我们就无法描述这个梗了,所扮演的角色也就更不会进行接梗,这会使角色变得呆板,看起来没有什么生命力。

同时,人脑可能会建立起错误的关联,而相关性指标在设计上,它不允许出现错误的关联,因为这会让基于理性的任务表现水平大幅下降。但人脑的错误关联所犯得一些“小错误”,这也是具有活人感的一个很重要的细节。采用基于提取频率的方式,在设计上允许以上两个问题得到解决,笔者认为这是在角色扮演任务的情境下最合适的指标。

工作 -> 长期

这部分主要处理**“更新”**

这部分目前可以说是空白,目前有一些初步设想:

  • 定期,或者在程序“优雅退出”(指程序接收到退出信号,处理完一切资源清理和状态记录后退出程序)时尝试执行
    • 将工作记忆子图直接写进数据库

两种可能的机制

  • 直接在数据库上进行聚类算法,将一些极其相似的记忆合并
  • 将一部分子图从数据库拉到工作记忆中,执行某些神笔操作,再写回数据库
    • 这一条来源于人脑的记忆回放机制

需要思考确定的内容:

  • 如何划定需要更新的区域
    • 图可能很大,全更新不现实,耗时耗空间,甚至因为很可能要调用LLM,还耗钱(
  • 应当进行什么更新
    • 合并?内容的更加好的表示?
    • 是否允许在此阶段建立新的连接?
    • 元数据(metadata)是否需要更新?要更新什么?
  • 如何更新
    • 调用LLM?
    • 或者某些神笔小操作?
  • 其他奇怪的细节地方

这部分主要要考虑的是性能效果的平衡。因此记忆图随着时间增长会越来越大,全量执行一遍遍历不现实,时间不允许,内存占用更不允许(一个vector embedding大概3KB,仅仅是一个embedding向量,节点还包含很多字符串信息,加载到内存里就是个问题,多次加载的话,性能就会出问题)。

如果选区更新,那么效果如何保证。钱的问题,主要来自于LLM的token费用,如果更新的内容很多,调用LLM会产生巨大的token费用。因此需要尤其仔细的设计。


遗忘

遗忘是另一块非常重要的算法,他主要进行状态维护中的**“删”。总得来说,遗忘总是让信息往熵增**的方向移动。

遗忘主要包括两个部分:

  • 连接强度的衰减关系的改变
    • 这模拟了,你能清楚的记忆起两个事物,但你无法从一个事物联想到另一个事物。
    • 这种情况考试中非常常见,例如考完出来对答案的时候,”哎呀我当时怎么没想到“
  • 节点内容的变化
    • 记忆的内容本身也是在变化的,一段经历到后面可能回忆起来就跟当时面目全非了’
    • 俗称“回忆的加工”

实现遗忘机制,不仅仅是因为人会遗忘有时候还健忘的比较厉害,遗忘本身也是一种控制记忆图规模的方式与手段。如果没有遗忘机制,记忆图就会一直增长下去,最终一定需要手动维护,这在实际应用程序上就不太现实了,不可能指望用户做这种枯燥的重复性工作。保持一定的规模,可以保证算法的执行时间得到一定的控制,保证存储空间的稳定。

遗忘本身也是一种筛选机制,他就是大脑中长期的注意力系统,遗忘不常检索到的信息,就相当于增加了常用信息的注意力分数,这也有助于增加检索算法的效果。

主流Agent不会主动实现遗忘机制,甚至要对抗它,想尽一切办法让无论是大模型本身还是RAG系统像超忆症一样记住所有东西。这是由它们主要解决基于理性的任务这一性质决定的。但对于角色扮演,除开某些特定角色(比如什么病娇),大部分角色如果能完全记住所有信息,就会产生一种不真实感

简单来说,你不觉得一个会遗忘的数字生命,拼命让自己尽可能记住和好朋友相关的信息会更可爱吗(雾

题外话:

就算基于理性的任务,遗忘或许也有作用,如果Agent知道自己会遗忘,或许它就不会在某些时候信誓旦旦,一本正经的胡说八道了(

一些理论

艾宾浩斯遗忘曲线

啊对,考试复习的时候总会被某些营销号炒作的概念。但是这里我们会有大用处。

艾宾浩斯遗忘曲线是通过以下方法测量的(有简化),给定一堆随机的字母序列,先记住它们全部,过一段时间再看看记得多少,把能记住的东西的量化指标记录下来,这样形成的曲线。拟合公式如下: $$ R = e^{-\frac{t}{S}} $$ 其中R被记忆的内容S相对记忆强度t时间

艾宾浩斯遗忘曲线是在无意义的字母序列的情景下测定的,对于有语义的部分,这篇文章(广告)中的图表可以表明,对于具有一定语义信息的序列,艾宾浩斯遗忘曲线的形式可能仍然适用

这指明了表征记忆强度的指标是指数型衰减

嵌入向量空间

Vector embedding确实是一个很熟悉的概念了,但是它的一些性质或许需要一些说明。

嵌入模型将文本转化为了具有一定维度的稠密向量(通常是784?维),这意味这信息受到了压缩(信息论的一些相关内容),这也意味着如果将文本送入嵌入向量模型再把嵌入向量送入一个decoder,一定会有一些系统性的误差

嵌入模型可以看做一个复杂的函数,这个函数的值域通常并不覆盖整个向量空间,所以,这个空间中有一些点是没有对应文本的。

这意味着在向量潜空间中对文本进行操作是一种hack的方法:

  • 它具有系统性误差
  • 可能会变换到无定义的点
  • 不同的模型输出不同的向量,性质也会略有不同,它是模型相关的
    • 同时,在向量潜空间中进行操作,意味着必须能decode回去,这导致很可能每个模型都需要一个decoder。
    • 统一的decode方法存在,但相对来说比较复杂,例如ZSInvert。

在向量潜空间中直接操作的唯一好处就在于可以利用数学方法

记忆锚点(暂定)

记忆锚点是作用在单个记忆节点上的,它在节点内容的变化中起作用。

记忆锚点存在于情境记忆中。它锚定了记忆节点中的一些关键细节,使得这些部分熵增的速率小于记忆节点中的其他部分

它对应的是一些“印象深刻的细节”,例如游戏结局中某个角色的台词等。

记忆锚点分为两类:

  • 自发锚点
    • 可以观察到,有些印象深刻的记忆,在第一次经历时就会有一种深刻的印象。例如看到某句台词突然有“醍醐灌顶”的感觉,这句台词很有可能你可以记很长时间
  • 强制锚点
    • 例如考试前突击复习背材料,通过意识刻意地重复,可以锚定其中的关键词,关键表达等。
    • 这类锚点短期内印象非常深刻,但随时间快速衰减,称为强制锚点

锚点在巩固过程中,由调用的LLM生成。

节点内容的遗忘(都是非常初步的一些灵感想法)

潜空间法

潜空间法,顾名思义,在潜空间里完成对文本修改的操作。

对于一个节点的记忆内容,我们分为多个**“记忆微元”**,将这些记忆微元的嵌入向量也存储在节点中。

利用某种算法(没想好,但应该与嵌入向量有关),对每个记忆微元赋予一个熵值,遗忘时,计算熵值的梯度,这样,我们可以通过一个遗忘强度,以及和熵值梯度向量之间的夹角,来控制潜空间中向量的变化方向

遗忘强度应该足够小,否则会大幅改变词语的含义。

把变换后的向量decode回去,改变记忆微元的 文本内容,即完成一次遗忘。

这个方法的优点就是利用了数学方法,比较能够搞操作。

缺点很多:

  • decoder模型很大可能要自己训练,每一个embedding模型都需要训练一个decoder模型

  • 在嵌入向量空间中,编解码有系统性误差,并且有一部分向量空间的点不对应文本

  • 遗忘强度和夹角的选取比较困难

  • 解码后的“遗忘记忆内容”可能根本就没有语义

遮罩法(可能可行的方案)

这个方法最初是为了解决潜空间法中不对应文本的问题诞生的,但发现可以单飞(

同样的,我们需要拆分记忆微元。不同的在于,我们为每个微元创建一个**“遮罩”,并在生成此记忆节点时,生成每个微元的概括性描述**(例如“猫” -> “会哈气的动物”,信息的不确定度的增加了,这是关键)。微元之间可以重叠,也可以形成包含关系

遮罩具有一个**“透明度”,透明度越大,就越能看到原本的信息**,反之,原本的信息就越模糊。透明度的衰减遵循艾宾浩斯遗忘曲线的拟合公式

当微元遮罩的透明度大于阈值时,提取该记忆时,直接提取原内容

当微元遮罩的透明度小于阈值时,提取该记忆时,用xxx替换原信息,并附上微元的概括性描述,如xxx(会哈气的动物),通过提示词工程让LLM理解这种描述的意思。我们要求LLM对于xxx的部分,基于角色的性格进行补全。把LLM补全后的记忆替换原本的记忆,并重新生成微元(或许可以不用)

当微元遮罩的透明度小于一个低于上文所述的阈值的阈值时,我们用xxx替换原信息后,不提供概括性描述,直接让LLM补全。

在这里遮罩的透明度减少即为熵增,因为它增加了LLM所能拿到的信息的不确定度

微元允许重叠和包含,如果不允许,那么句子的整体结构就不会发生大的变化,这种层叠结构也有利于模拟节点内容间不同等级的遗忘。

这种做法模拟了两种不同情境的遗忘:

  • 间隔性的“复习”,此时我们仍然提供概括性的描述,补全的内容不会偏离原内容太多
  • 完全忘记,此时概括性描述不提供,LLM只能纯猜,与原记忆的出入会比较大

这样的做法的优点:

  • 惰性
    • 对于遗忘,我们只需要衰减每个微元的遮罩,这一操作很容易并行,效率较高
    • 当它被提取时,我们才应用遗忘,最大限度减少了LLM的调用次数

连接的遗忘

这部分似乎没什么内容,对连接强度利用艾宾浩斯遗忘曲线的拟合公式进行衰减,低于一定阈值删除连接应该就行。当一个节点没有任何连接的时候,就删掉它。