☰
AI长期记忆为何越存越乱?Graphiti实体去重与冲突检测实践
2026/10/30 17:06:18 网站建设 项目流程

AI 记忆如果只会"存进去、取出来",用不了多久就会变成一堆互相矛盾的话。尤其是当你让 Agent 长期跑业务、做客服、当个人助理时,最烦人的不是它记不住,而是它把新旧信息混在一起,今天说用户喜欢 A,明天又因为一条旧记录说用户喜欢 B。我在这段时间研究 Graphiti(一个专门给 AI 做长期记忆的框架)时,把实体去重、事实冲突检测、记忆更新这几个大头逐个跑了一遍,现在把整个思路和踩坑记录整理出来。这篇内容适合正在做 Agent 记忆层、知识库系统,或者被"越存越乱"困扰的开发者。我会从"乱在哪"讲起,再逐个拆 Graphiti 的应对机制,最后给一些我实测下来觉得值得注意的细节。

1. 先别急着加向量库:AI 记忆为什么越存越乱

先说个反直觉的结论:AI 记忆变乱,往往不是因为"存得太多",而是因为"只存不整理"。

很多团队做记忆系统的时候,第一反应是把用户对话、知识片段、业务数据一股脑转成向量塞进向量数据库,然后靠 embedding 相似度来召回。这套方案在"记一件事"的场景下没问题,可一旦跑上几个月,问题就陆续出来了。

1.1 向量库是"硬盘",不是"记忆系统"

向量库本质上做的是"存储 + 相似度检索",它不负责判断两条信息是不是在说同一件事。比如用户今天说"我住在北京朝阳区",三个月前说"我常驻北京",六个月前说"我在北京望京上班"。这三条消息在向量空间里靠得足够近,能被同时召回,但系统不知道它们描述的是同一个地址事实,甚至还会把"常驻"和"上班地点"当成两个独立条目。

更麻烦的是,向量相似度是按语义距离算的,而"同一个实体"和"语义相似"根本不是一回事。"北京"和"北京朝阳区"语义上高度相似,但一个是城市、一个是区,如果直接合并就丢信息;反过来,"张三"和"李先生"在字面上毫不相似,却可能因为上下文指向同一个人。向量库没法做这种推理。

所以第一步是得先承认:AI 记忆不应该只是"能查到的历史记录",它得形成一张有结构的网——哪里是实体、实体之间什么关系、每条事实发生在什么时间、可信度多高。这正是 Graphiti 这类工具存在的原因。

1.2 事实更新不是"覆盖文件",是"修正关系网"

再往深一层看,记忆乱还有一个根源:很多人把"更新记忆"理解成"把旧记录删掉,写入新记录"。这在文件系统里是对的,但在记忆系统里行不通。

举个实际例子:一个销售助理 Agent 跟踪某个客户,客户最开始说"我们公司今年预算 50 万",三个月后改口"预算实际是 80 万,之前那个数字是初步估算"。如果系统直接把"50 万"覆盖成"80 万",那"初步估算"这个背景信息就丢了;如果保留两条记录不做处理,那后续问答时 Agent 就不知道信哪条。

真实世界的事实更新,本质是"旧事实降权 + 新事实生效 + 关系重连":客户这个实体不变,但"预算金额"这条关系对应的值变了,同时"时间"和"可信度"这些元信息也要跟着调整。Graphiti 在处理这种情况时有个明显特点——它不会抹掉旧记录,而是给旧记录打上"已失效/已过期"的标记,让新记录在时间维度上占据主导权。

也就是说,要想记忆不乱,得同时管好三件事:存哪些实体、实体间怎么去重、一条事实和另一条事实冲突时怎么处理。这三件事也就是 Graphiti 的核心发力点。

2. 时间感知的知识图谱:Graphiti 的核心设计思路

Graphiti 不是一个通用的图数据库,它是专门围绕"时间"和"实体关系"来组织 AI 记忆的框架。我理解它的核心就一句话:每条记忆都是图上的一条边,这条边带有明确的时间区间和事实状态。

2.1 双时间轴:事件发生时间和信息写入时间

我最早看 Graphiti 的资料时,最不理解的一个设计是它把时间分成两套:一套是事实发生的时间(valid time),一套是这条信息进入系统的时间(transaction time)。后来自己做知识库做多了才意识到,这两套时间如果不分开,记忆系统根本没法处理"事后补充/事后纠正"。

举个例子:用户 5 月 1 日和 AI 说"我下周要去上海出差",这条事实的发生时间其实是 5 月 8 日那周,而不是 5 月 1 日;但系统写入时间肯定是 5 月 1 日。如果只记写入时间,5 月 9 日系统检索"用户当前在哪",就会得到"他下周要去上海"这种已经过时的内容。如果只记发生时间,那 5 月 1 日之前的检索又完全看不到这条信息。

Graphiti 把两者分开之后,表达能力立刻就不一样了:你可以问"用户现在的安排是什么",它基于 valid time 找当下生效的事实;也可以问"系统是什么时候知道这件事的",它基于 transaction time 查记忆写入顺序。这种双时间建模在长期记忆场景里几乎是必需的。

提示:如果你的 Agent 只处理当日会话,双时间轴是过度设计;但只要涉及跨天、跨周的记忆融合,这就是刚需。我见过不少项目把时间戳简化成"一个 datetime 字段",最后做事实冲突检测时根本说不清哪条更新。

2.2 实体-关系-边:记忆检索是"沿图导航",不是"相似度捞取"

Graphiti 把记忆组织成知识图谱:节点是实体(人、组织、地点、事件等),边是事实(比如"张三——就职于——某公司"),每条边上有时间区间、置信度、来源等属性。检索的时候,不是拿 query 去全库做向量相似度,而是先定位相关实体,再沿着图的关系往外扩展。

这个过程很像人在回忆事情:你想起"那个姓王的客户"时,不是把整本通讯录翻一遍,而是先定位"王"这个人,然后沿着"和他相关的合同、会议、报价"这些关系逐步展开。图结构天然适合这种"从实体出发的联想式检索",这也是它比向量库更适合做长期记忆的原因。

当然,Graphiti 也不排斥向量,它在实体定位时会用 embedding 做初始匹配,但匹配完之后一定会落到实体 ID 上,再通过图的关系去取事实。这算是一种"向量召回 + 图谱精排"的组合思路。

2.3 增量构建:每来一条消息,只更新局部子图

如果把知识图谱想象成一张越滚越大的网,那最怕的就是每次新增消息都要全图重建。Graphiti 的做法是增量更新:新消息进来后,先用 LLM 抽取实体和关系,再尝试把抽到的实体接到已有节点上,能匹配就更新已有节点的属性和关系,匹配不到才新建节点。

这个思路听起来简单,实际做到位很考验细节:接错了节点,会把两个无关实体的信息合并,造成"事实污染";新建节点太频繁,又会膨胀成碎片图。所以增量构建的质量,很大程度取决于下一节要讲的实体去重机制。

3. 实体去重:同一件事有十个说法,怎么判定是一个人

实体去重是 Graphiti 这类系统中最容易翻车、也最体现工程水平的一环。我实测下来的体感是:去重不是"匹配字符串",而是"在多条证据之间做概率决策"。

3.1 为什么"名字一样"远远不够

先看一个我实际测试时遇到的案例。有两条记录:

  • 记录 A:用户吐槽"那个销售经理小李,报价特别不靠谱"
  • 记录 B:公司系统里"李强,销售经理,负责华东区"

如果系统靠字符串匹配,"销售经理"和"小李"就对不上,更别说把"小李"这个称呼映射到"李强"这个人。但如果只靠"销售经理"这个职位去匹配,又可能把两个不同的销售经理合并成一个。

实体去重的难点就在这里:同一个实体的称呼方式变化极大——全名、昵称、职位指代、甚至"上次和我对接的那个人"这种描述性指代,而不同实体的称呼又可能高度重叠。

Graphiti 处理这个问题的思路是"综合证据打分":

  • 名称相似度只是其中一个信号,而且权重不能太高;
  • 属性重叠度(比如都提到"销售经理"、"华东区")是第二信号;
  • 关系上下文是更强的信号:如果两个候选实体都连接到了同一个客户、同一个项目,那它们很可能是同一个。

3.2 属性对比、关系上下文和时间维度:三个组合出结果

具体到实现层面,我建议把去重决策拆成三个维度来打分,融合后才能下结论:

判断维度示例信号去重权重判断
名称/别名"小李" vs "李强","北京" vs "北京市"只能作为弱信号,单独用会误杀
属性重叠职位、地区、组织名称这些带结构化的字段重叠中等强度信号,重叠越多越可信
关系上下文是否连接到同一客户、同一项目、同一时间段最强信号,关系重叠比字面重叠可靠得多
时间维度两条记录是否存在同一时间段内出现辅助信号,跨时间太久的同名实体要谨慎合并

我自己的经验是:名称占 20% 权重、属性 30%、关系上下文 50%,时间维度作为一票否决项(如果两个实体的活跃时间段完全错开,哪怕名字属性全相似,也要保留分裂)。

3.3 合并的动作不是"二选一",而是"融合"

去重决策做完之后,真正难的是怎么合并。如果你只是把旧节点删掉、把信息塞进新节点,那会导致关联到旧节点的大量历史边(过去的对话、历史事实)全部断掉。

Graphiti 的做法更像是"融合":

  • 保留存活节点的 ID 作为 canonical ID;
  • 把所有关联边从被合并节点迁移到存活节点,同时保留边的原始时间戳;
  • 把被合并节点的别名、属性并入存活节点的属性列表;
  • 在属性上标记"merge provenance",记录这条属性是来自哪条原始记录的,方便将来回滚和溯源。

我记得有一次跑测试,系统把"住在北京的用户"和"经常分享北京本地新闻的用户"合并成了一个实体,结果导致一条"用户曾经提到公司在北京"的旧记忆被错误迁移到这个实体上,后续问答直接串线。原因就是合并时把事件类实体和属性类实体混为一谈,教训就是:实体类型不同,宁可另建节点,也不要合并。

4. 事实冲突:用户说改口了,系统凭什么信新的

光会合并实体还不够,记忆系统更关键的是处理"事实冲突"。所谓事实冲突,就是两条事实在描述同一个对象时给出了矛盾的信息。比如用户先说"我每周二健身",后来又说"我最近不健身了"。

4.1 冲突不是简单的"新旧覆盖"

很多初版实现遇到冲突就按时间戳覆盖——新来的说法取代旧说法。这个粗暴方案在 70% 的场景下够用,但剩下 30% 会让你抓狂:

  • 场景一(事后澄清):用户说"我住在杭州",走到楼下改口说"其实我住的是滨江区,刚才说得不精确"。这时候如果覆盖,其实丢掉了更细粒度的信息;合理做法是保留"杭州"作为大范围,把"滨江区"作为精确值,两者并存且后者的置信度更高。
  • 场景二(条件性事实):用户说"工作日我在市中心的办公室,周末我在家办公"。这根本不算冲突,而是同一条事实在不同条件下的两个分支。把这种条件信息建模成"带条件的关系",比简单覆盖明智得多。
  • 场景三(真改口):用户说"我不用了,卸载了",之前记录是"我正在用某 APP"。这才是真正的版本更新,旧值失效、新值生效。

所以冲突检测的第一步,不是"谁新听谁的",而是先判断两条事实是"同一维度的新旧更替",还是"不同维度/不同条件下的并存"。

4.2 置信度、来源与时间戳的博弈

Graphiti 处理冲突时,我理解它的核心逻辑是给每条事实维护三样东西:

  • valid interval(有效时间区间):这条事实从什么时候开始生效、到什么时候失效;
  • 置信度(confidence):系统对这条事实有多确定;
  • 来源(source)和事务时间:谁在什么时候告诉系统的。

新事实进来后,系统不是直接删旧记录,而是做一轮"冲突仲裁":先看新旧事实是否指向同一实体 + 同一关系类型 + 同一维度,是才进入仲裁流程;然后比较三者的加权得分,谁高谁作为当前主版本;被淘汰的一方不会销毁,而是标记为"superseded"(被取代)状态,保留在历史层。

我实际测过一种有意思的情况:旧事实来自用户主动输入(置信度可以给到 0.9),新事实来自 AI 从用户日志里推断(置信度只有 0.6)。这时候哪怕新的时间戳更晚,也不能直接覆盖旧的,因为来源可信度差太多了。时间戳只是仲裁的输入之一,不是裁决者。

4.3 冲突仲裁的实际判定流程

我在自己的小项目里复刻了一套简易流程,跑下来效果还可以,分享给你们参考:

  1. 新事实抽取完成,先定位主实体,把候选边按照"实体 + 关系类型 + 对象实体"分组;
  2. 对每个分组,计算新旧事实的 embedding 相似度,低于阈值(比如 0.82)就视为"不同维度的事实",不做覆盖;
  3. 高于阈值则进入冲突仲裁:按"来源可信度 × 时间衰减 + 是否近距离重述 + 是否为用户主动修正"几项综合打分;
  4. 如果仲裁结果是"新事实胜出",就把它设为主版本,旧事实标记为已取代,且关系上保留时间边界;
  5. 如果仲裁结果是"存疑",宁可保留两条事实并在检索结果中标注"存在冲突",也不要擅自抹掉任何一条。

第 5 点尤其重要。我在真实项目里发现,LLM 生成的"确定性事实"非常容易带幻觉,如果系统每次都无脑信任新的,反而会把正确的旧记忆污染掉。保留冲突、显式标注,给上层 Agent 一个"这里有两个说法"的信号,比自作主张解决冲突更稳妥。

在 Graphiti 里,这种"事实冲突检测"逻辑从底子上就支持,因为每条边都是不可变写入的(append-only),修改不是 UPDATE 而是 CREATE 新版本边,天然适合做版本化的事实管理。

5. 长期记忆更新:如何做到"记得住"又"改得动"

记忆系统的最后一块拼图是"更新策略"。去重和冲突解决决定的是"单次记忆怎么进网",而长期记忆更新决定的则是"这张网怎么随着时间自动演化"。

5.1 增量更新:每轮对话最多改一个局部子图

增量更新听着简单,核心在于"控制范围"。我见过失败案例是这样的:每次新消息进来,就把整个知识图谱跑一遍重嵌入、重组装,结果系统越跑越慢,而且频繁的全量重算会让已有关系里的时间属性全部错乱。

Graphiti 的风格是:每次只处理当前消息涉及到的实体附近的一小圈子图。拿"用户每周二健身"这条消息举例,它先定位到用户实体,检查"健身"这个事件类节点是否存在,存在则更新该节点的时间属性和来源,不需要新建节点;不存在才新建,并和用户实体之间建立一条"参与/计划"的边。

这种局部更新看起来很克制,但恰恰保证了长期运行的稳定性:你不需要因为一次小规模消息改动就全图重算,所有旧记忆的时间属性也不会被意外篡改。做长期记忆系统,克制比激进更重要。

5.2 关系衰减:旧事实不是"删除",是"降权"

时间一长,新事实不断叠加,旧事实如果一直以同等权重存在,记忆图就会噪音满天飞。这时候需要一种"衰减"机制。

Graphiti 的边模型天然支持这点——每条边都有 valid interval,而系统在检索时可以按当前时间动态过滤:只返回"当前有效"的边,或者把"已过期"的边权重拉低。

我的实操心得是:不要用"定时删数据"的方式做衰减。删除是不可逆的,而 AI 记忆的价值恰恰在于保留完整的时间线,方便回溯"用户为什么现在偏好了 A,是不是曾经说过 B"。更合理的做法是:

  • 边仍然保留在库里,但标记为"inactive";
  • 检索时按权重排序,inactive 权重默认减半;
  • 定期做一次归档压缩,把超过 N 个月且从未被检索过的 inactive 边移入冷存储。

这样记忆图既能反映当下状态,又不丢失历史证据链。真实世界的事实更新也一样——我们不会因为某人换了个手机号,就删掉他所有的历史联系记录。

5.3 记忆回滚:一次错误更新怎么倒回去

长期运行的 Agent,最怕的是某次错误的实体合并或错误的冲突仲裁把整个记忆图带偏。所以记忆系统必须支持"回滚"。

Graphiti 的 append-only 边结构让回滚变得很自然:要回滚到某个时间点,只要把你关心的那个局部子图的"最新版本边"失效,恢复上一版本边的 active 状态即可。因为它没有物理覆盖,旧版本永远还在,所以回滚不是"找回丢失的数据",只是"切换哪个版本生效"。

我在测试时专门模拟过:制造一次错误的实体合并(把两个不同公司合并成一个),然后试图回滚。如果是"物理合并"方案,回滚几乎等于灾难,你得写脚本把被融合的属性一条条拆回去;而在 append-only 方案里,因为合并操作本身就创建了一条 merge 边记录,回滚只要撤掉这条 merge 边,两个原始实体以及它们的所有属性关系就自动恢复了。

这一点给所有做记忆系统的开发者的建议是:无论你用什么存储,务必保证"写入=追加版本,修改=新增版本",才能换来长期的稳定性和纠错能力。

6. 我实测下来的一些经验和坑

最后这部分不按教程写了,纯粹聊几个我实操中遇到的坑,希望能帮后来的兄弟少走弯路。

第一个坑:抽取阶段就别偷懒。实体去重和冲突检测的下游效果,全看上游 LLM 抽取的质量。如果抽取时已经把"张三从 A 公司离职"硬生生抽成了"张三——就职于——A公司",那下游 Graphiti 再怎么努力也救不回来。我在自己的 pipeline 里加了专门的"关系类型校验"步骤,抽取出来的每条关系都要让 LLM 用自己的话解释一遍"为什么认为是这个关系类型",能显著降低错误率。这个解释过程还能顺便给置信度打分。

第二个坑:别把所有东西都塞进图谱。不是所有知识都值得建模成实体关系。我在测试初期,把用户一句闲聊"今天天气不错"也抽成了事件节点,结果图里漂着一堆毫无关联的"天气事件"节点,检索时全是噪声。后来给抽取模块加了过滤规则:只有和业务实体(人、公司、项目、地点等)产生明确关系的内容才进入图谱,纯闲谈直接落库为原始消息但不建图关系。

第三个坑:关于事实冲突检测的阈值,别拍脑袋定。不同场景下的"冲突"标准差异巨大。客服助手场景下,用户说"我要退款"和"我不退款了"必须被识别为强冲突;但知识库场景下,"该产品主打性价比"和"该产品针对高端用户"可能只是评价角度不同,并不算冲突。我的做法是给关系类型配置一个"冲突敏感度"字段,高敏感度的关系(比如订单状态、用户偏好)使用更严格的相似度阈值,低敏感度的关系给更大的容忍度。

第四个坑:长期记忆更新要留"人工审计"的口子。我始终认为,在短期到中期内,不要指望记忆系统 100% 自动正确。Graphiti 虽然把更新做得很精细,但 LLM 抽取、合并、仲裁依然会出错。我习惯在系统里留一个"记忆管理后台",把所有实体合并操作、冲突仲裁结果按时间列出,支持人工撤消。上线第一个月,我每周都会被审计列表里的一两个错误惊到,后面才慢慢摸清哪些场景要调阈值。记忆系统没有永不犯错的设计,只有最好的纠错机制。

最后一个实在话:别一上来就追求"大而全"。Graphiti 这样的框架很有启发性,但如果你只想给一个简单的 FAQ Bot 做记忆,核心其实只需要版本化的时间戳 + 按固定规则做冲突覆盖,完全不用上图谱。什么时候需要?当你开始发现"覆盖式更新导致旧知识被误删""两个匿名说法分不清是不是同一个人""多轮对话里事实互相打架"这三个问题同时出现时,再来认真看实体去重、事实冲突检测和长期记忆更新这套组合拳,你才会真正理解 Graphiti 每个设计的意义。

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

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

立即咨询