☰
AI长期记忆系统设计拆解:让大模型跨会话记住用户偏好
2026/10/22 4:52:50 网站建设 项目流程

做AI对话类项目做到后面,几乎所有人都会撞上同一堵墙:模型明明很聪明,可一换会话就什么都不记得了。用户上周交代的偏好、前两天聊过的方案细节、刚刚在对话里确认过的事实,全部清零。这种“聊完就忘”的体验放在聊天机器人、AI助手上特别致命,因为用户默认你应该记得他。于是就有了 Mindbase 这个项目——一个介于大模型与应用之间的长期记忆系统,专门帮AI把“该记的东西”沉淀下来,跨会话、跨时间地想起来。

它不是某个具体的模型,也不是一个简单把对话丢进数据库的脚本,而是一套完整的记忆架构,包含文本切块、向量化、存储索引、召回策略、遗忘机制这些环节。这篇文章从设计思路、核心模块、落地实现到踩坑记录,完整拆解一遍我做这套系统的过程。想给AI助手加记忆能力、做角色扮演机器人、或者做知识库问答的朋友,应该都能在里面找到直接能用的方案。

1. 为什么AI需要一套长期记忆系统

1.1 大模型“聊完就忘”的困境

主流大模型的对话能力再强,本质上还是无状态的:你发给它一个请求,它返回一个结果,这个请求结束以后,之前的对话内容就不存在于模型内部了。很多产品会做“多轮对话”,但那是把最近几轮的文字重新拼起来再发给模型,属于用上下文窗口硬撑,撑到窗口上限就得开始“丢记忆”。

实际体感是这样的:用户和AI聊了半小时,连续确认了三个关键偏好——不用辣椒、预算控制在二十以内、周五下午不能安排会议。下一轮对话如果系统没有做记忆处理,AI会把这三个信息全部忘掉,用户必须从头再交代一遍。体验上就像每次换了个新客服。

所以长期记忆系统的直接价值就是:把散落在历史对话里的关键信息提取出来、存好、在合适的时机重新注入上下文。它不是模型能力的替代,而是模型能力的补丁,解决的是“状态连续性问题”。

1.2 长期记忆的产品价值在哪里

长期记忆带来的不只是“少重复说几遍”这种体验提升,它直接决定AI能不能从“工具”变成“助理”。

举几个典型场景:

  • 个人AI助手:记住用户的作息习惯、工作节奏、常去的地点,推荐内容才有意义。
  • 角色扮演类AI:记住角色的经历、关系进展、用户给它设定的名字,剧情才能连续。
  • 企业知识问答:记住某个员工的角色权限、关注领域、历史决策逻辑,回答才有个性化。
  • 自动化Agent:跑一个多步骤任务中途挂了,重启后能接着干,靠的是持久化的状态记忆。

没有长期记忆的AI,每次都是从零开始理解用户。有了长期记忆,AI才具备“越来越懂你”的基础。这是从“能用”到“好用”的分水岭。

1.3 Mindbase 的定位:一个独立的记忆层

我做 Mindbase 的时候定了一个原则:记忆系统不跟具体模型绑定,也不跟宿主应用绑定,它是一层独立的中间件。模型可以换,应用可以重写,但记忆数据是资产,要能稳定沉淀。

所以 Mindbase 的核心边界是这样的:

  • 输入:一段带时间戳的对话文本、用户行为事件、或任意结构化信息。
  • 处理:切块、向量化、抽取摘要、整理实体关系。
  • 输出:一组可检索的记忆项,带重要度评分、时间衰减因子、关联标签。
  • 对外接口:写入、查询、更新、遗忘。

这样设计的好处是,不管底层接的是对话机器人、任务型Agent还是内容推荐系统,上层逻辑都不用动,只要调统一接口。项目做下来,这个“解耦”的决策帮我省了大量返工。

2. 整体架构拆解:把记忆拆成三层

2.1 三层记忆模型怎么分

做记忆系统最忌讳的就是把所有东西一股脑往向量库里塞。塞进去以后你会发现,检索时经常把无关紧要的日常寒暄召回回来,真正重要的用户偏好反而排到很后面。

我参考了认知科学里对记忆的经典分类,把 Mindbase 的记忆分成三层:

  • 工作记忆(Working Memory):当前会话内正在讨论的信息,量很小,随会话结束就可以丢弃或归并。
  • 短期记忆(Short-term Memory):最近几次会话里的重要事实和上下文,需要保留几天到几周,用于最近的连续性。
  • 长期记忆(Long-term Memory):跨越多轮会话沉淀下来的用户画像、稳定偏好、长期目标,保留周期按月甚至按年。

这三层不是三个数据库,而是同一套存储上的三种生命周期管理策略。短期记忆的条目有效期限短,长期记忆的条目只要不被冲突覆盖就一直留着。每次写入时先判断信息属于哪一层,再设置对应的过期策略。

判断规则可以很朴素:凡是包含“用户明确表达的需求、偏好、决定”的,优先进长期记忆;凡是“本次对话的事件、临时状态”的,进短期记忆;工作记忆不落库,只在反向传播时参与上下文组装。

2.2 记忆写入管线

写入不是“把对话原文存起来”这么简单。原始对话里噪音太多,直接存会导致后续检索精确度极低。我设计了一条三层写入管线:

  1. 预处理:把多轮对话按主题聚类,过滤掉寒暄、语气词、重复表达。
  2. 信息抽取:用大模型抽取关键信息,结构化成“实体 + 属性 + 值”的形式,比如“用户 → 不吃辣 → 是”。
  3. 向量化与存储:把抽取结果编码成向量,写入索引,同时保存原始文本作为证据。

这里最关键的是第二步。让模型直接抽取比用规则引擎效果好得多,尤其处理口语化表达的时候。我给模型设计了一套抽取提示词,要求它只输出“值得记住的事实”,每条不超过五十个字,并标注信息类型(偏好、事实、关系、目标、事件)。

抽取结果示例:

类型: 偏好 主体: 用户 属性: 饮食禁忌 值: 不吃辣 时间: 2025-06-10 重要度: 0.9

存储时除了向量,这些元数据字段都要存下来,后面检索排序全靠它们。

2.3 记忆读取与召回策略

读记忆比写记忆更难。难在“在合适的时机想起合适的事”。

我一开始试过最粗暴的方案:每次对话开始前,把最近N条记忆全部塞进上下文。结果上下文爆炸,模型关注点被稀释,反而更笨了。

后来改成“按需召回”:每次对话请求进来,先拿用户的当前输入去检索记忆库,召回最相关的Top K条,再拼接到系统提示词里。召回要考虑的不只是文本相似度,还要叠加时间衰减和重要度。

召回打分公式我用了简单的加权线性组合:

score = 0.6 * 语义相似度 + 0.3 * 重要度 + 0.1 * 时间新鲜度

语义相似度就是向量余弦相似度;重要度是写入时模型给的分数;时间新鲜度按指数衰减计算。权重可以根据场景调,角色扮演类可以把时间新鲜度调低一些,让“久远但重要”的设定更容易被想起来。

召回结果不是直接丢给模型就完事,我会先做一个去重和冲突检测:如果多条记忆对同一个属性给出了不同值,取重要度最高的,同时把冲突信息单独标注出来,让模型知道“这里有矛盾,需要用户确认”。

3. 核心模块细说:嵌入、存储、检索、遗忘

3.1 文本切块与嵌入向量化

把记忆变成向量之前,先要决定“一条记忆”的粒度。粒度太小,比如一句话切一条,检索时容易碎;粒度太大,比如一整段对话揉进去,向量表达的语义就被稀释了。

我实际用的切块策略是“语义段落优先”。先把一段对话按句子拆开,然后用一个轻量的聚类逻辑把相邻、主题相近的句子合并成块,每块控制在五十到一百字之间。规则上就三步:

  • 第一步:按标点和换行切出句子。
  • 第二步:两两相邻句子做向量相似度计算,超过阈值就合并。
  • 第三步:合并完的块如果超过一百五十字,再在中间按最不相似的边界切开。

嵌入模型我选用的是本地部署的开源文本嵌入模型,输出维度768。之所以不用云端接口,一个是对话量上来以后接口调用成本很高,另一个是记忆数据涉及用户隐私,本地向量化心里踏实一些。

嵌入这一步还有个容易被忽略的细节:要把“类型前缀”加到文本里再向量化,比如“偏好:用户不吃辣”,比单独写“用户不吃辣”检索效果要好。这个技巧在信息检索领域叫查询域适配,实测能让同类记忆的召回率提高不少。

3.2 向量存储与索引设计

向量数据库这块我对比过几个方案,最后选了一个支持HNSW索引的开源向量库,部署简单,性能也够用。核心配置点在于索引参数:

  • 距离度量:余弦距离。
  • M(每个节点的最大连接数):16。
  • efConstruction(建索引时的动态列表大小):200。
  • efSearch(查询时的候选列表大小):64。

这个组合在千万级向量以下的场景里,召回延迟稳定在几十毫秒,足够对话场景用。如果向量量级更大,可以调大M和efSearch,但索引内存占用会明显上升,这个要看机器配置去平衡。

存储结构上,我把向量跟元数据分开存。向量索引只管召回候选,元数据放在关系表里做过滤。原因是向量库里的元数据过滤能力普遍比较弱,像“按重要度大于0.8过滤”这个需求,靠向量库原生支持效率很差,不如先向量召回,再回表过滤。

每次写入记忆时,我会同时往向量索引里插入一条记录,往元数据表里插入结构化字段,用同一个记忆ID关联。查询时先向量召回ID列表,再回表补全信息、做排序和去重。

3.3 检索打分细节

检索召回质量直接决定整个记忆系统的可用性。向量相似度只能保证“字面相关”,但“重要”和“新鲜”这两个维度是向量索引给不了的,必须在排序阶段额外加。

排序阶段我维护两个辅助字段:

  • importance_score:写入时由抽取模型给出,0到1。
  • last_access_time:每次被成功召回并使用时更新。

然后按下面的公式重排:

final_score = 0.55 * cosine_sim + 0.25 * importance_score + 0.2 * recency_score

其中 recency_score 是经过时间衰减的,比如最近七天内新鲜度拉满,超过三十天按指数曲线降到0.2封底。这样既不会让老记忆完全消失,也不会让太久远的信息一直霸占前排。

还有一个排序技巧:同一条记忆在多次会话中被重复命中时,它的 importance_score 要手动调高。这个叫“强化信号”,效果非常明显。用户反复提到某件事,说明这件事对他很重要,应该逐渐升到长期记忆的顶部。

3.4 记忆更新、合并与遗忘机制

记忆系统只写不删会越滚越大,而且会出现互相矛盾的脏数据。遗忘机制不是可选项,而是必须做的。

我用三种策略保证记忆库干净:

  • 冲突归并:检测到“同一属性、不同值”的新记忆时,用新的覆盖旧的,旧记录进入待确认列表。如果新记忆的重要度明显低于旧记忆,就先不覆盖,把冲突记录下来等用户确认。
  • 定时衰减:短期记忆条目超过有效期(默认三天)且没有再次命中的,自动降级到工作层,逐步降低检索权重。
  • 主动遗忘:超过六十天未被命中的长期记忆,进入“冷冻区”。冷冻区不参与常规检索,但可以人工恢复。这个设计很像人脑,不用彻底删除,只是暂时想不起来。

合并操作我放在每天深夜的离线任务里跑。批量扫描当天写入的记忆,按主体验证冲突,按属性合并同类项。刚上线的时候偷懒没做合并,结果用户的一个偏好被存了十几条碎片,每次检索都召回好几条重复内容,上下文被白白占掉很多位置。

4. 实操:把Mindbase接入一个AI对话流程

4.1 最小系统搭建步骤

先给一个可以直接跑通的最小版本。目录结构如下:

mindbase/ core/ store.py # 向量存储与索引 memory.py # 记忆写入、读取、合并入口 embedder.py # 文本切块与向量化 prompt.py # 抽取提示词模板

第一步做向量化接口。我用以下代码作为嵌入调用的统一入口:

# embedder.py class TextEmbedder: def __init__(self, model_path=None): # model_path 指向本地开源嵌入模型目录 self.model = self._load_model(model_path) self.dimension = 768 def _load_model(self, model_path): # 真实部署时在这里加载本地模型 return None def embed(self, text: str) -> list: # 返回 768 维向量 # 实际实现时调用模型 encode 接口 return self.model.encode(text) def embed_batch(self, texts: list) -> list: return [self.embed(t) for t in texts]

第二步写切块函数。核心逻辑是:按句子切分后,用相邻句子的嵌入相似度做合并,超过阈值就聚成一块。这一步需要调用刚写好的嵌入接口,注意控制调用次数,因为本地模型的批处理效率远高于逐句调用。

4.2 对话前检索与上下文组装

对话流程接入点是“每次模型调用前,先从 Mindbase 取记忆”。伪代码如下:

def build_context(user_input, user_id): # 1. 检索用户相关记忆 memories = mindbase.retrieve( user_id=user_id, query=user_input, top_k=8 ) # 2. 按类型格式化记忆 lines = [] for mem in memories: line = f"[{mem['type']}] {mem['content']}(重要度{mem['importance']:.1f})" lines.append(line) # 3. 拼接到系统提示词之后 memory_block = "\n".join(lines) system_prompt = f"""以下是该用户的历史记忆信息。如果对回答有帮助就参考,不确定就忽略: {memory_block} 请针对用户的问题给出回答。""" return system_prompt

这里有个细节:记忆块不能放在用户输入之后,要放在系统提示词里。放在输入后面的内容会被模型当作“最新的对话内容”来回应,容易干扰语义;放在系统提示词里则是“背景资料”,模型会把它当参考而不会直接回答给用户看。

Top K 的值我建议从8开始调。太少了关键信息召不回,太多了噪音多,上下文也被挤占。实测在768维模型、中等长度对话的场景下,8到12是比较稳的区间。

4.3 对话后记忆写入与异步总结

对话结束后发起记忆写入。注意不能同步阻塞用户请求,要放到后台异步任务里。我用的思路是:把整段会话原文写入原始日志,同时推一条异步任务给抽取模块处理。

# memory.py class MindBase: def log_conversation(self, user_id, messages): raw_text = "\n".join([m["content"] for m in messages]) # 保存原始日志 self.raw_logs.save(user_id, raw_text) # 异步触发抽取 self.async_task.submit( self._extract_and_store, user_id, messages ) def _extract_and_store(self, user_id, messages): # 1. 预处理后组装成抽取输入 cleaned = self.preprocess(messages) # 2. 调用大模型抽取关键信息 memories = self.extract_with_llm(cleaned) # 3. 向量化 + 写入索引 for mem in memories: vec = self.embedder.embed(mem["content"]) self.store.insert( user_id=user_id, vector=vec, meta=mem )

异步这块如果不做,对话响应时间会飙升,用户感知很明显。更合理的做法是把抽取任务放进消息队列,失败自动重试。最小实现里我用线程池也能顶住,前提是QPS不高。

4.4 一个完整示例:记住用户偏好

拿一个最简单的“记住不吃辣”流程演示。用户在一次会话中说:“最近在减肥,吃的清淡一点吧,另外我是完全不吃辣的。”

这条原始对话进入抽取模块后,预测输出两条记忆:

类型: 目标 内容: 用户在减肥,需要清淡饮食 重要度: 0.8 类型: 偏好 内容: 用户完全不吃辣 重要度: 0.95

隔了一天用户再来,什么都没交代,直接问“帮我推荐几个吃的”。检索模块拿到这个query,向记忆库召回。这两条记忆和“吃的”语义相关度高,再加上重要度高,顺利进入上下文。

模型收到的是:

用户历史记忆: [偏好] 用户完全不吃辣(重要度0.95) [目标] 用户正在减肥,需要清淡饮食(重要度0.8)

于是回答里自然避开辣菜,优先给清淡选项。用户会感觉AI“记得我”,这个体验差距非常大。整个链路里最花功夫的不是模型调用,而是让“抽取”这一步足够准,别把“今天天气不错”这种废话也存成记忆。

5. 踩坑实录:记忆系统常见的坑与排查

5.1 检索结果不准怎么办

最常见的问题是:用户明明说了某个偏好,但检索时就是召不回。我排查下来原因基本集中在三个方向:

  • 切块粒度不合适。偏好信息被揉进一大段无关对话里,向量语义被稀释。解决方法是把切块上限调小,并强化抽取阶段的结构化。
  • 嵌入模型不适合中文或口语化表达。这种情况换个对口语容忍度更高的嵌入模型,通常立竿见影。
  • 查询语句跟记忆表述差异太大。比如用户记忆里写的是“不吃辣”,查询输入是“有推荐的川菜吗”,纯向量相似度匹配不上。缓解办法是扩展召回词,在检索前用同义词扩展把“川菜”映射到“辣”“麻辣”,再一起送进向量检索。

5.2 记忆冲突和数据重复

一个用户聊了两个月以后,记忆库里很可能同时存在“用户喜欢喝美式”和“用户现在只喝茶”这两条冲突记忆。如果不处理,模型就会精神分裂,一会儿推荐咖啡,一会儿推荐茶。

我的处理方案是加一层“生命周期合并”:任何一条新的偏好记忆写入时,都和旧的同类记忆做一次比对,如果主体和属性相同,新记忆落地,旧记忆进待确认状态。每天离线的合并任务再针对待确认状态的记录做最终裁决,裁决依据是重要度、时间、以及用户在后续对话中的行为反馈。

数据重复相对好解决,靠向量去重就行。写入前把新记忆向量和已有记忆向量做相似度计算,超过0.92的直接跳过,只更新原记忆的 last_access_time。

5.3 存储膨胀与性能问题

运行越久记忆库越大,检索速度迟早会退化。我遇到过最严重的一次是索引参数没调好,向量量级到了五百万条以后,单次检索跑到接近一秒,完全没法用。

优化手段按顺序做:

  • 第一,清理短期记忆。把超过三十天未命中的自动降到冷冻区,索引体积直接砍掉一大半。
  • 第二,给索引加量化压缩。我用的是乘积量化,向量文件缩小到原来的四分之一,召回精度损失在可接受范围内。
  • 第三,上线前预估向量量级,调好HNSW的批量构建参数。批量写入时把 efConstruction 调到300,写入完成再降回200,避免写入期间查询崩溃。

5.4 隐私与可控性

记忆系统存储的是用户私人信息,这个责任比功能更重要。我说几个自己坚持的底线:

  • 记忆数据默认本地化存储,不主动回传任何云端。
  • 所有记忆项都带时间戳和来源对话ID,用户可以查看自己“被记住了什么”。
  • 提供“遗忘全部记忆”的接口,这是必须有的功能。很多产品把用户记忆当成资产不肯删,但这类功能做出来,用户信任感完全不一样。
  • 抽取阶段的关键信息在写入前做脱敏。我定义了一批敏感实体类型,如身份证、银行卡、手机号,检测到就直接屏蔽,不进入记忆库。

隐私设计不是上线以后补的,而是架构层面从一开始就要留好口子。不然等数据量大了再改,成本翻倍不说,还可能出合规问题。

6. 一些值得继续完善的方向

Mindbase 目前跑通的核心链路是“抽取-存储-召回-注入”,但距离一个真正的个人化记忆系统还有不少路。我在实际使用中感受最深的是,记忆的“关联能力”还远远不够。

现在的记忆都是独立条目,没有把“用户周五下午不能开会”和“用户上周提到儿子周五有家长会”这两件事关联起来。如果能让记忆形成一张知识图谱,模型理解用户的能力会再上一个台阶。这个方向我下一步准备探索,核心是把抽取结果里的实体和关系单独建图,检索时不只召回语义相近的条目,还沿着关系链路把相邻记忆一起带出来。

另外一个小技巧分享给正在做这类系统的朋友:不管是嵌入模型还是向量库,上线前一定要准备一套“标准记忆测试集”,里面放几十条不同类型的记忆和几十个对应的查询问题。每次改完参数、换完模型,先跑一遍测试集,看召回率有没有退化。没有这套测试集,你会发现自己调了一周的参数,实际效果反而越来越差,全靠玄学支撑。

做长期记忆系统,真正的难点不在某一种技术,而在把抽取、存储、检索、遗忘这些环节咬合在一起,形成一个自洽的闭环。每个环节单独看起来都不复杂,但拼在一起,并让它在真实场景里稳定跑起来,就是另一回事了。希望这篇拆解能帮你少走一些我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询