
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 层级怎么分才合理这是整个项目里最难拿捏的部分。分太粗顶层概念会变成什么都装的大筐检索时所有内容都挤在一个入口分太细层级数量膨胀建图和维护成本成倍增加而且检索时容易因为路径选择错误错过正确内容。我试验下来比较稳定的分层原则有三条按可独立决策来分一个节点如果能在检索中被当作独立入口使用就值得单独建层。比如退款策略可以作为一个节点而退款策略里的一句话不足以成为节点。按生命周期来分长期不变的信息用户的基本偏好放高层短期易变的信息这周的任务进展放低层。按聚合粒度来分底层节点要能聚合到上层概念。如果底层某几个节点无法抽象出共同的上级说明它们根本不在一块儿应该拆开。在实际建图时我还有一个补充技巧不要人工纯手工建层级那样太慢而且容易主观。先让大模型对对话记录做一次聚类和摘要生成候选概念节点和层级关系再由人来校验和修正。人工负责判断这个摘要准不准、层级合不合理大模型负责批量处理。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_depth3) 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(), keylambda x: x[1], reverseTrue)[: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双路径的Recall10比单纯向量检索高出20%到35%。浮动范围很大主要取决于测试集的实体密度和关系复杂度。实体密度越高、关系越复杂双路径的优势就越明显。如果完全是一堆松散的闲聊对话没有明显的实体和关系结构双路径的优势会缩小到10%左右。5. 实操中踩过的坑和排查思路5.1 层级粒度不对召回质量浮动非常大这是我在项目里最先撞上的问题。最开始建图时我把能摘出来的实体都建成了节点结果图里光支付退款银行这类粒度很小的词就有上百个。查询支付流程问题时语义路径照常工作但关系路径会在支付节点周围疯狂扩展把跟支付沾边的所有内容都捞进来当候选最终结果就是一堆重名实体互相干扰排序质量反而下降。调试了一段时间之后我的判断标准变成能作为检索入口的实体才建节点只能作为属性内容的不建节点。比如银行回调延迟应该是一个描述内容挂在退款异常子主题节点下面而不是一个独立节点。如果一个节点在查询时几乎不会单独被问到它就不值得做节点。另一个反复出现的问题是子主题节点划分的颗粒度。一个子主题节点如果覆盖了超过两周的话题内容一定会出现内部碎片化底层节点彼此之间没有紧密的聚合关系。这时候要做一次下沉拆分把一个节点拆成两个或三个更聚焦的节点。推荐每周做一次层级健康度检查新对话持续进入时原来的高层分类会慢慢失真靠人工维护根本跟不上。5.2 双路径结果互相打架语义路径返回的高分节点和关系路径返回的高分节点经常不是同一批。一开始遇到这种情况我以为是融合逻辑有Bug后来发现这其实是正常的两条路径本来就擅长捕捉不同的信息形态。真正的问题场景是这样的当用户问那个事现在到底什么情况语义路径找到的是几个语义相关但都比较老的片段关系路径却因为实体解析失败没能提供增量。融合后的结果偏向老片段导致模型回答过时内容。后来我加了一条兜底规则如果关系路径的实体解析结果为空或者命中节点太少低于阈值则语义路径权重拉满并且只取时间戳最新的节点进最终结果。这样至少保证不会把已经过时的事情当成现状回答。融合阶段还有一个隐蔽的坑两条路径返回的节点内容高度重复但分数相差不多。去重之后剩下的有效节点数可能不够。我的做法是在去重基础上做一次小范围补充召回保证最终喂给模型的节点至少有5个。数量太少的话模型生成回答时会因为信息不足而开始脑补。5.3 长期运行后的图膨胀与遗忘策略这个项目跑了大概六周之后图里的节点数量从最初的几百个涨到了三千多个。查询延迟还在可接受范围内但检索结果的精确度开始下滑。原因并不难猜很多旧节点里包含的信息已经被新节点覆盖或矛盾但旧节点依然躺在图里语义召回时会时不时把它们捞出来。解决遗忘问题我尝试过两种方案。第一种是定期压缩让大模型对一段周期内比如一周的旧节点做一次摘要合并把多条事实合并成一个概览节点挂在同主题之下。旧节点不删除只是把它们挂到归档子节点下检索时不再默认参与。第二种是冲突标记当新节点与旧节点的内容构成更新/替代关系时不是把旧节点删除而是打上已被更新标记。查询时如果命中了旧节点强制把它的后继节点也一起返回保证模型能看到完整的时间线。这两种方案我目前是配合使用的。周期性归档解决的是节点膨胀问题冲突标记解决的是时序一致性问题。它们都不是完美的但组合起来已经能在不人工介入的情况下维持较好的检索质量。5.4 性能与延迟的平衡心得双路径检索比单路径慢是必然的因为它跑了两套查询逻辑。在节点规模几千的情况下延迟还能控制在一次普通API调用之内可以接受。但节点规模一旦过万关系路径的图遍历就会开始成为瓶颈尤其是深度遍历时容易产生指数级的边展开。我的优化手段有三个给关系边加类型过滤遍历时只走属于更新/替代这类高价值边跳过引用这类低信息量边。把检索分成两段第一阶段用较快的粗召回可以用近似最近邻或只走一层关系第二阶段只对粗召回结果做完整的关系路径展开。本质上是用一个漏斗降低无效计算。层级路径缓存。用户在连续对话窗口里的问题通常属于同一个主题第一次检索时计算出的层级路径可以缓存起来后面几次查询直接复用能省掉大量重复的图遍历。写在最后的几个实操心得这个项目做下来我最深的一个感受是长期记忆系统的瓶颈往往不在模型能力而在数据组织方式。给大模型更多的上下文花不了太多成本但把真正关键的信息在正确的时间以正确的结构提出来才是决定体验上限的东西。层级图加双路径这套组合本质上不是复杂技术而是给记忆建立索引、建立导航、建立时间线意识。另外一个小建议不要着急追求大规模建图先拿一小段真实对话记录跑通全流程对照着看检索结果把建图粒度和融合权重的感觉找准再逐步扩大数据范围。我前面犯过的粒度错误、融合错误很大程度上是因为一开始就想一步到位。后续这个项目还可以往两个方向扩展一是把双路径融合的alpha权重替换成可学习的排序模型二是引入记忆的衰减因子让关系强度随着时间自然减弱让长期记忆更接近人的遗忘曲线。如果你也在做LLM记忆方向欢迎拿这套思路回去试试有问题随时交流。