☰
大模型长期记忆实战:层级图与双路径检索方案详解
2026/10/20 2:46:36 网站建设 项目流程

1. 这个项目到底在解决什么问题

1.1 大模型记性不好的根因,不是模型不行

做LLM应用的人早晚都会撞上同一堵墙:上下文窗口是固定的,而对话和任务会无限增长。你可以在提示词里塞三千行历史记录,但模型真正"记得住"的内容非常有限。越往前的信息越容易被后续内容稀释,本质上不是模型没有看到,而是注意力机制在处理长序列时会被大量信息摊薄,早期的关键细节慢慢就模糊了。

这个项目标题里的"Long-Term LLM Memory"指的就是这件事。Mnemis是我在一个内部实验项目里用的代号,语义上来自"记忆"的词根,目标很直接:让大模型在跨越很长时间、很多轮对话之后,仍然能把关键事实和概念关系找回来。

先说清楚一个误区:很多方案把长期记忆简单地理解为"把历史内容存起来,用的时候塞进上下文"。这个思路在demo里能跑,但一到真实场景就会出问题。因为你塞进去的往往是一堆原始流水账,里面有大量噪声。比如用户三周前提到的一个需求,中间被另一个需求覆盖了,真正的关键信息藏在两百轮对话深处。你不可能每次查询都把全部历史塞给模型,那样成本高、效率低,而且效果也未必好。

想要长期记忆真正可用,核心问题不是存储,而是检索。存储只需要解决"有没有"的问题,检索需要解决"能不能在需要的那一刻、用足够高的精度把最关键的信息拿出来"。而检索的精度,直接取决于你用什么数据结构来组织记忆,以及用什么策略来查找。

1.2 记忆不是"存下来",而是"想起来"

我们平时说的"长期记忆",其实可以被拆成三个层次:

  • 短期记忆:当前上下文窗口内的对话内容,模型可以直接访问,但轮次增多后会溢出
  • 工作记忆:当前任务需要的摘要和临时结构化信息,通常靠提示词里的人工摘要维护
  • 长期记忆:跨会话、跨任务需要保留的事实、偏好、关系和历史决策

大部分RAG类方案做的就是第三层的一部分,但它们的做法通常是"把文本切成块,做向量索引,然后按相似度召回"。这种方式解决的是"能找到相关文本",但距离"想起来"还有不少距离。因为人的记忆不是按文本块组织的,而是按概念和关系组织的。你回忆一个项目时,脑子里出现的是"这个项目有哪些目标、谁负责、卡在哪个环节、下一步要做什么",而不是"第38条文本片段说了什么"。

Mnemis这个项目想验证的是一个假设:如果把记忆组织成层级图结构,再用两条互补的检索路径去访问,长期记忆的召回质量和可解释性都会明显好于单纯的文本块向量检索。

先说结论:这个假设在实验里是成立的。在跨越多个主题、涉及多轮演化关系的测试问题上,双路径检索的准确率比纯向量召回高出不少,而且返回的结果能直接看到推理链路——也就是说,系统会告诉你"我是通过哪条路径找到这段记忆的"。

这个项目适合谁参考?如果你在做聊天机器人的长期记忆、智能体工作流的状态管理、知识库问答系统的检索优化,或者单纯对"大模型记忆"这个方向感兴趣,这篇文章会把设计思路、关键实现和踩坑记录完整拆给你看。

2. 为什么最终选了层级图结构

2.1 纯向量检索的问题,出在"相似度"本身

先别急着眼红图结构,得先聊清楚向量检索的边界。

向量检索的原理是把文本映射成高维向量,然后用余弦相似度或内积来找"语义相近"的内容。它在处理"语义匹配型"问题(比如"自动驾驶的风险有哪些"和"无人驾驶的隐患")时表现很好。但在长期记忆场景里,它会暴露三个结构性的弱点。

第一个弱点是语义相近不等于事实相关。举个例子,用户在一个项目讨论里提到"服务器成本超预算",另一个完全不相关的对话里也提到了"服务器成本控制"。向量召回会把两段都捞出来,因为它们语义上靠得很近。但真正需要的只有第一段。单纯做相似度,模型分不清"同一个话题下的变体"和"不同话题下的偶合"。

第二个弱点是丢失了时间与因果顺序。文本块索引记录的是静态内容,但记忆是有演化过程的:一个决定被推翻、一个结论被修正、一个需求被替换。如果检索结果里没有这条演化链,模型拿到的可能是一个过时的版本。比如用户上周说"我们优先做A方案",本周改成"A方案暂停,先做B"。向量库里这两段文本的相似度可能很高,但模型不知道哪个是新的、哪个被否决了。

第三个弱点是关系型问题几乎无解。像"上次说要改价格方案的那个客户,最终确认了没有"这种查询,涉及实体(客户)、属性(价格方案)、状态(是否确认)、时间线(上次)。向量检索只能找到语义相近的片段片段,然后靠模型自己拼关系链,拼不拼得对全看运气。

2.2 层级图不是"复杂化",而是"结构化"

图结构天然擅长表达关系,而层级结构则解决了一个图数据库单独解决不了的问题:导航效率。

如果一个图只有实体节点和关系边,节点数量一旦上去,检索时你就得在同一层级里撒网捞鱼。比如你有十万个节点,用户问一个跟"支付流程"相关的问题,你得先想办法定位到"支付流程"这个大节点附近,再展开子图。没有层级,这一步的代价很高,而且容易跑偏。

层级图的做法是给节点分层:

  • 顶层是抽象概念,比如"项目A""支付流程""客户沟通决策"
  • 中间层是从概念派生出的子主题,比如"项目A-里程碑""支付流程-退款异常"
  • 底层是具体的记忆片段,比如某次对话中关于退款超时的痛诉、某次评审的结论

有了这层结构,检索路径就变成了"先定位概念,再沿层级下沉"。这有点像城市地图:你要找一家藏在巷子里的小馆子,不会在全城所有街道里乱搜,而是先定位到"哪个区、哪条街",再顺着街口往巷子里走。层级图本质上就是给记忆建立了一套城市级别的导航系统。

2.3 层级怎么分才合理

这是整个项目里最难拿捏的部分。分太粗,顶层概念会变成"什么都装"的大筐,检索时所有内容都挤在一个入口;分太细,层级数量膨胀,建图和维护成本成倍增加,而且检索时容易因为"路径选择错误"错过正确内容。

我试验下来比较稳定的分层原则有三条:

  1. 按"可独立决策"来分:一个节点如果能在检索中被当作独立入口使用,就值得单独建层。比如"退款策略"可以作为一个节点,而"退款策略里的一句话"不足以成为节点。
  2. 按"生命周期"来分:长期不变的信息(用户的基本偏好)放高层,短期易变的信息(这周的任务进展)放低层。
  3. 按"聚合粒度"来分:底层节点要能聚合到上层概念。如果底层某几个节点无法抽象出共同的上级,说明它们根本不在一块儿,应该拆开。

在实际建图时,我还有一个补充技巧:不要人工纯手工建层级,那样太慢而且容易主观。先让大模型对对话记录做一次聚类和摘要,生成候选概念节点和层级关系,再由人来校验和修正。人工负责判断"这个摘要准不准、层级合不合理",大模型负责批量处理。Mnemis项目里,建图环节我用的是一个"摘要-抽象-链接"三步流程,后面会细说。

层级图的最大收益,还体现在可解释性上。向量检索返回的结果,你只能看到"相似度分数",不知道它为什么选这几段。图检索返回的结果,你有完整的路径:用户问的"退款问题"从"支付流程"节点进入,经过"退款异常"子主题,最终命中了具体的对话片段。这种可追溯性对调试系统和人工审核都极其重要。

3. 双路径检索:一次查询两条腿走路

3.1 语义概念路径:用"感觉"找方向

Mnemis的"双路径"设计,思路其实很朴素:既然单一路径各有短板,那就用两条路径互补着来。

第一条路径我叫它语义概念路径。它的工作方式跟传统向量检索接近,但有两个关键区别。

第一,索引对象不是原始文本块,而是层级图中的节点。也就是说,我们要先通过建图流程把原始对话整理成"节点+关系"的结构,然后对所有节点做向量化。顶层概念节点有向量,中间层子主题节点有向量,底层记忆片段也有向量。查询时,我们把用户问题转成向量,先在节点空间里做相似度召回。

这么做有一个直观好处:召回的不再只是一堆割裂的文本片段,而是带有层级的"候选入口"。比如用户问"上次说的那个支付延迟问题,后来怎么解决的",向量匹配的可能是底层的一个片段("支付延迟已确认是银行回调慢"),也可能是中间层的子主题节点("支付流程-异常处理")。两种节点都能用,但用途不同,底层节点提供事实,中间层节点提供上下文聚合。

第二,语义路径不止做一层召回。第一轮召回拿到一批相关节点后,我会在它们周围做一次小范围扩展,把相邻的父节点和子节点也捞进来。这个"沿层级扩展"的步骤,能把孤立的片段重新放回它所属的主题上下文里。很多向量检索方案失败在只看片段本身,而没有把这个片段放回它所属的"上下文树"里看。加上扩展步骤之后,召回内容的质量提升非常明显。

3.2 结构化关系路径:用"关系"找依据

第二条路径是结构化关系路径。它完全不依赖向量相似度,而是在图结构上跑遍历。

做法是:先解析用户的查询,提取出其中的实体和关系线索。比如"客户A确认了价格调整方案吗"这个查询,提取出的线索是:实体"客户A"、"价格调整方案",关系线索是"确认"。然后我们在图里定位这些实体节点,沿着关系边向外遍历,查找与这些实体有直接或间接关系的事实节点。

这条路径的优势在于它能捕捉精确关系。向量召回会告诉你有相关文本,但"客户A和价格方案之间到底是什么关系"需要结构化路径来回答。一次典型的遍历大概是这样的:

  • 定位到节点"客户A"
  • 遍历"客户A - 涉及项目 -> 项目X"
  • 再由"项目X - 包含决策 -> 价格调整方案"
  • 最终找到"价格调整方案 - 状态 -> 已确认"这个断言性质的节点

这种情况如果只靠向量检索,你得先召回一堆可能包含相关信息的片段,再让模型从片段里抽关系,误差会被放大。关系路径本质上是在做约束条件下的精确查找,像数据库里的关联查询,只不过"表"换成了图,"SQL"换成了一次受限的图遍历。

但关系路径也不是万能的。它在实体和关系比较明确时表现极好,一旦用户查询是模糊口语化的表达,比如"那个事情怎么样了",实体解析就无从下手。所以它必须跟语义路径配合:语义路径负责处理模糊表达,先把候选范围拉出来,关系路径负责在明确的实体关系上进行精确匹配。

3.3 两条路径的结果怎么融合

双路径不是简单地把两组结果去重合并就完事。如果处理不好,反而会出问题。比如语义路径返回了A、B、C三个节点,关系路径返回了B、D、E,如果你直接把两组拼一起,B被重复计算,整体排序也会失真。

我在项目里用的融合策略分三步:

第一步是归一化打分。两组召回结果都做分数归一化,让它们在同一个量纲下比较。语义路径用向量相似度归一化,关系路径用"关系命中次数+距离衰减因子"折算成分数。距离越远、关系链越长的节点,分数衰减越厉害,防止关系路径拿一堆绕了五层关系才连上的边角料填充结果。

第二步是特权加权。两个通道对不同类型的查询有不同优势。如果查询解析出的实体信息充分,关系路径的权重会提高;如果查询是泛泛的、没有明确实体指向的,语义路径的权重会提高。这个权重我一开始用固定比例,但之后发现可以根据查询本身的可解析度动态调整,效果更好。

第三步是结果去重与上下文融合。相同的节点保留分数高的来源,被两组同时命中但内容有重叠的节点要去掉冗余。最后把融合结果按分数排序,作为召回集交给后续的重排序环节。

这里有个值得注意的细节:最终喂给大模型的内容,不是单纯把召回文本堆进去。我会把每个召回节点所属的层级路径一起带上,比如"支付流程 > 退款异常 > 2025年3月对话片段"。这些路径信息相当于给模型提供了"知道的来源",模型回答时更有依据感,也更容易做多跳推理。

4. 最小可复现的实现方案

4.1 数据结构与建图流程

如果你想在一个项目里复现这套设计,不用一上来就整特别庞大的基础设施。我先说一个够用的最小版本。

先定义几个核心数据结构:

  • 节点:包含id、类型(概念/子主题/片段)、内容摘要、生成时间、向量
  • 关系边:包含起点id、终点id、关系类型(属于/引用/对立/更新)
  • 层级映射:记录每个节点在层级树中的父节点和子节点

建图流程分成三步。

第一步是对话分段与摘要。把原始对话按主题窗口切段,每段交给大模型生成一句话摘要和几个关键词。注意,切段的时候不要只按时间硬切,要做一次话题边界判断。比如"聊完A项目又聊了午饭吃什么"——这两者明显不是一个话题,应该切到不同段里。

第二步是概念聚类。把摘要和关键词再做一次聚类,多个段落如果涉及同一个主题,就在它们上面创建一个中间层子主题节点,再往上归纳出顶级概念节点。这一步我在实验里是让大模型生成候选,然后人工校验。校验重点看两点:聚类是否把不同话题硬塞到同一节点里;摘要是否丢失了关键实体或数值信息。

第三步是关系连接。这一步是双路径检索能跑起来的核心。除了"属于"这种天然的层级关系之外,还要识别跨节点关系,比如"A方案替代了B方案"这个事实,对应的节点之间要建立"更新"关系;"客户的诉求"和"售后团队的反馈"如果是在说同一件事,要有"引用"关系。跨节点关系越丰富,关系路径检索的能力越强,但也越容易产生噪声。我的建议是:先只建立"属于"关系和"更新/替代"关系,跑通流程再逐步丰富。

4.2 核心检索代码逻辑

下面给一段简化的Python代码,模拟双路径检索的核心逻辑。这里为了便于理解,去掉了一些工程化细节,重点是看两条路径的分工和融合方式。

import numpy as np from typing import List, Dict class MnemisRetriever: def __init__(self, graph, embedder): self.graph = graph # 层级图对象 self.embedder = embedder # 向量化模型封装 def retrieve(self, query: str, top_k: int = 10): # 路径1:语义概念路径 q_vec = self.embedder.encode(query) node_scores = self.embedder.similarity(q_vec, self.graph.all_nodes) candidate_ids = [n.id for n in self.graph.all_nodes if node_scores[n.id] > 0.4] # 沿层级扩展:把候选节点的父/子节点一并拉进来 for nid in list(candidate_ids): parent = self.graph.get_parent(nid) if parent: candidate_ids.append(parent.id) for child in self.graph.get_children(nid): candidate_ids.append(child.id) semantic_results = { nid: node_scores[nid] for nid in set(candidate_ids) } # 路径2:结构化关系路径 entities = self.parse_entities(query) # 实体解析 relation_results = {} for entity in entities: anchor = self.graph.get_node(entity) if anchor is None: continue # 沿关系边做受限遍历,深度限制为2-3跳 hits = self.graph.traverse(anchor, max_depth=3) for hit in hits: score = 1.0 / (1 + hit.distance) # 距离衰减 relation_results[hit.id] = max( relation_results.get(hit.id, 0), score ) # 融合:归一化 + 特权加权 sem = self.normalize(semantic_results) rel = self.normalize(relation_results) query_entity_ratio = len(entities) / max(1, len(query.split())) alpha = 0.5 + 0.3 * min(query_entity_ratio, 1.0) alpha = min(alpha, 0.8) fused = {} for nid in set(sem) | set(rel): fused[nid] = alpha * sem.get(nid, 0) + (1 - alpha) * rel.get(nid, 0) # 按融合分数排序,返回top_k个节点及层级路径 ranked = sorted(fused.items(), key=lambda x: x[1], reverse=True)[:top_k] return [ { "node": self.graph.get_node(nid), "score": score, "path": self.graph.get_hierarchy_path(nid), } for nid, score in ranked ]

代码里的alpha就是上面说的动态权重。当查询里的实体词比较多时,alpha会偏向关系路径;当查询是语义模糊的自然语言时,alpha自动回到0.5附近,两条路径各占一半。这个权重不需要每次人工调,可以做成一个小的回归模型,也可以直接用规则估算。

实际工程里,我不会直接在Python里遍历图对象,而是用图数据库自带的遍历能力做批量查询。上面的写法只是为了把逻辑说清楚,不要直接搬进生产环境。

4.3 用三类测试问题做效果对比

我在实验里准备了三类测试问题,分别用来打不同方案的脸:

第一类是语义模糊问题:"上次聊的那个事情后来怎么样了?"这种问题实体信息很少,基本只能靠语义路径。具体对比结果是:纯向量检索能靠相似度捞到部分相关片段,但经常捞到相邻主题的干扰项;加上层级扩展后,能稳定定位到正确子主题下的最新片段。

第二类是关系精确问题:"客户A最终确认了哪个版本的价格方案?"这类问题纯向量检索很容易返回语义相近但关系错乱的片段组合,Mnemis的关系路径能直接沿"客户A-项目-价格方案-确认状态"这条结构链找到结果,准确率接近完全命中。

第三类是跨主题演化问题:"我们最开始讨论的离线方案被推翻之后,最后线上方案跟它还有关系吗?"这类问题本质是在问时间线和替代关系。纯向量检索的得分和排序严重受文本表面相似度影响,很难区分"讨论过的旧方案"和"已经落地的新方案";而Mnemis因为有"更新/替代"这类关系边,能清晰地把旧方案和新方案连起来,并同时返回"旧方案被推翻"这个关键事件节点。

从数值上看,在这三类测试集上,Mnemis双路径的Recall@10比单纯向量检索高出20%到35%。浮动范围很大,主要取决于测试集的实体密度和关系复杂度。实体密度越高、关系越复杂,双路径的优势就越明显。如果完全是一堆松散的闲聊对话,没有明显的实体和关系结构,双路径的优势会缩小到10%左右。

5. 实操中踩过的坑和排查思路

5.1 层级粒度不对,召回质量浮动非常大

这是我在项目里最先撞上的问题。最开始建图时,我把能摘出来的实体都建成了节点,结果图里光"支付""退款""银行"这类粒度很小的词就有上百个。查询"支付流程问题"时,语义路径照常工作,但关系路径会在"支付"节点周围疯狂扩展,把跟支付沾边的所有内容都捞进来当候选,最终结果就是一堆重名实体互相干扰,排序质量反而下降。

调试了一段时间之后,我的判断标准变成:能作为检索入口的实体才建节点,只能作为属性内容的不建节点。比如"银行回调延迟"应该是一个描述内容,挂在"退款异常"子主题节点下面,而不是一个独立节点。如果一个节点在查询时几乎不会单独被问到,它就不值得做节点。

另一个反复出现的问题是子主题节点划分的颗粒度。一个子主题节点如果覆盖了超过两周的话题内容,一定会出现内部碎片化,底层节点彼此之间没有紧密的聚合关系。这时候要做一次"下沉拆分",把一个节点拆成两个或三个更聚焦的节点。推荐每周做一次层级健康度检查,新对话持续进入时,原来的高层分类会慢慢失真,靠人工维护根本跟不上。

5.2 双路径结果互相"打架"

语义路径返回的高分节点和关系路径返回的高分节点经常不是同一批。一开始遇到这种情况,我以为是融合逻辑有Bug,后来发现这其实是正常的:两条路径本来就擅长捕捉不同的信息形态。

真正的问题场景是这样的:当用户问"那个事现在到底什么情况",语义路径找到的是几个语义相关但都比较老的片段,关系路径却因为实体解析失败没能提供增量。融合后的结果偏向老片段,导致模型回答过时内容。后来我加了一条兜底规则:如果关系路径的实体解析结果为空,或者命中节点太少(低于阈值),则语义路径权重拉满,并且只取时间戳最新的节点进最终结果。这样至少保证不会把"已经过时的事情"当成现状回答。

融合阶段还有一个隐蔽的坑:两条路径返回的节点内容高度重复,但分数相差不多。去重之后,剩下的有效节点数可能不够。我的做法是在去重基础上做一次小范围"补充召回",保证最终喂给模型的节点至少有5个。数量太少的话,模型生成回答时会因为信息不足而开始脑补。

5.3 长期运行后的图膨胀与遗忘策略

这个项目跑了大概六周之后,图里的节点数量从最初的几百个涨到了三千多个。查询延迟还在可接受范围内,但检索结果的精确度开始下滑。原因并不难猜:很多旧节点里包含的信息已经被新节点覆盖或矛盾,但旧节点依然躺在图里,语义召回时会时不时把它们捞出来。

解决遗忘问题,我尝试过两种方案。第一种是定期压缩:让大模型对一段周期内(比如一周)的旧节点做一次摘要合并,把多条事实合并成一个概览节点,挂在同主题之下。旧节点不删除,只是把它们挂到"归档"子节点下,检索时不再默认参与。第二种是冲突标记:当新节点与旧节点的内容构成"更新/替代"关系时,不是把旧节点删除,而是打上"已被更新"标记。查询时如果命中了旧节点,强制把它的"后继节点"也一起返回,保证模型能看到完整的时间线。

这两种方案我目前是配合使用的。周期性归档解决的是"节点膨胀"问题,冲突标记解决的是"时序一致性"问题。它们都不是完美的,但组合起来已经能在不人工介入的情况下维持较好的检索质量。

5.4 性能与延迟的平衡心得

双路径检索比单路径慢是必然的,因为它跑了两套查询逻辑。在节点规模几千的情况下,延迟还能控制在一次普通API调用之内,可以接受。但节点规模一旦过万,关系路径的图遍历就会开始成为瓶颈,尤其是深度遍历时,容易产生指数级的边展开。

我的优化手段有三个:

  1. 给关系边加类型过滤,遍历时只走"属于""更新/替代"这类高价值边,跳过"引用"这类低信息量边。
  2. 把检索分成两段:第一阶段用较快的粗召回(可以用近似最近邻或只走一层关系),第二阶段只对粗召回结果做完整的关系路径展开。本质上是用一个漏斗降低无效计算。
  3. 层级路径缓存。用户在连续对话窗口里的问题通常属于同一个主题,第一次检索时计算出的层级路径可以缓存起来,后面几次查询直接复用,能省掉大量重复的图遍历。

写在最后的几个实操心得

这个项目做下来,我最深的一个感受是:长期记忆系统的瓶颈往往不在模型能力,而在数据组织方式。给大模型更多的上下文花不了太多成本,但把真正关键的信息在正确的时间以正确的结构提出来,才是决定体验上限的东西。层级图加双路径这套组合,本质上不是复杂技术,而是给记忆建立索引、建立导航、建立时间线意识。

另外一个小建议:不要着急追求大规模建图,先拿一小段真实对话记录跑通全流程,对照着看检索结果,把建图粒度和融合权重的感觉找准,再逐步扩大数据范围。我前面犯过的粒度错误、融合错误,很大程度上是因为一开始就想"一步到位"。

后续这个项目还可以往两个方向扩展:一是把双路径融合的alpha权重替换成可学习的排序模型,二是引入记忆的衰减因子,让关系强度随着时间自然减弱,让长期记忆更接近人的遗忘曲线。如果你也在做LLM记忆方向,欢迎拿这套思路回去试试,有问题随时交流。

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

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

立即咨询