AI 记忆为什么越存越乱?从 Graphiti 讲透实体去重、事实冲突与长期记忆更新

很多人第一次做 AI Agent Memory,思路非常直接:

用户聊天
↓
提取重要信息
↓
存数据库
↓
以后搜索回来

比如用户说:

我叫张三,是 Java 后端工程师。

于是存一条:

张三
→ profession
→ Java 后端工程师

过几天又说:

张三最近开始做 Go 了。

继续存:

张三
→ uses
→ Go

看起来一切正常。

但真实系统连续运行几个月以后,知识图谱可能开始变成这样:

在这里插入图片描述

张三
张三同学
小张
@zhangsan
Zhang San
zhangsan

系统里出现了 6 个节点。

更麻烦的是,它们其实:

都是同一个人。

与此同时,事实也开始越来越乱:

张三 → worksAt → A公司

张三 → worksAt → B公司

张三 → worksAt → C公司

系统如果不知道这些事实之间存在:

更新
替代
冲突

就可能得出:

张三目前同时在 A、B、C 三家公司工作。

这就是长期 AI Memory 真正困难的地方。

不是:

怎么存进去?

而是:

存进去以后,如何保证知识不会越来越脏?

今天我们继续看一个开源项目:

Graphiti

项目:

https://github.com/getzep/graphiti

上一篇我们讲了 Graphiti 的:

Temporal Knowledge Graph

也就是:

valid_at
invalid_at
reference_time
Episode

今天继续往下一层:

Entity Resolution + Deduplication + Fact Invalidation

翻译成人话就是:

实体到底是不是同一个?

事实是不是重复的?

新事实是不是推翻了旧事实?

旧知识应该删除还是失效?

这些能力,决定了一个 AI Agent 的记忆:

是越用越聪明,还是越用越混乱。


一、先看最简单的实体重复问题

假设用户第一次说:

张三负责支付系统。

系统抽取:

Entity:
张三

Entity:
支付系统

关系:

张三
→ responsibleFor
→ 支付系统

过几天又有一份会议纪要:

小张负责支付平台后端改造。

LLM 又抽出:

Entity:
小张

Entity:
支付平台

关系:

小张
→ responsibleFor
→ 支付平台

如果什么都不处理,知识图谱里就出现:

张三 ─────→ 支付系统

小张 ─────→ 支付平台

但现实可能是:

张三 = 小张

支付系统 = 支付平台

于是正确图应该是:

        张三
         │
         │ responsibleFor
         ↓
      支付系统

而不是两个孤立子图。


二、这就是 Entity Resolution

这个问题通常叫:

Entity Resolution

或者:

Entity Linking
Entity Deduplication
Entity Matching

虽然这些概念严格来说并不完全一样,但对于今天理解 AI Memory,可以先简单理解成:

判断新出现的实体,与已有实体是不是同一个现实对象。

例如:

OpenAI
Open AI
OpenAI Inc.
OPENAI

是不是同一个?

再比如:

苹果
Apple
苹果公司
Apple Inc.

很可能是同一个 Organization。

但:

Apple

也可能代表:

苹果这种水果

这时候:

光比较名字就不够了。


三、最简单的去重方式:字符串相等

我们自己实现一个最小版本。

entities = {
    "张三",
    "李四",
    "王五"
}

新实体:

new_entity = "张三"

直接判断:

if new_entity in entities:
    print("实体已经存在")

这只能解决:

张三
=
张三

但解决不了:

张三
=
张三同学

更解决不了:

OpenAI
=
OpenAI Inc.

四、可以先做标准化

比如:

def normalize_name(name: str) -> str:

    return (
        name
        .strip()
        .lower()
        .replace(" ", "")
    )

那么:

print(
    normalize_name(
        "Open AI"
    )
)

结果:

openai

再:

print(
    normalize_name(
        "OPENAI"
    )
)

也是:

openai

这样可以解决:

大小写
空格
简单格式

问题。

但:

张三

和:

小张

依然完全不同。


五、进一步可以维护 Alias

比如:

aliases = {

    "张三": [
        "张三",
        "小张",
        "zhangsan",
        "@zhangsan"
    ],

    "OpenAI": [
        "OpenAI",
        "Open AI",
        "OpenAI Inc."
    ]
}

查询:

def resolve_alias(name):

    for canonical, values in (
        aliases.items()
    ):

        if name in values:

            return canonical

    return name

执行:

print(
    resolve_alias(
        "小张"
    )
)

得到:

张三

这就是一个最简单的:

Canonical Entity

设计。


六、Canonical Entity 是什么?

知识图谱里最好给同一个现实对象一个:

Canonical Node

比如所有:

张三
小张
Zhang San
@zhangsan

最终都指向:

person:1001

可以理解:

张三
     \
小张 ──→ Person-1001
     /
@zhangsan

然后所有事实:

worksAt
hasSkill
worksOn
likes

都挂在:

Person-1001

这个节点上。

而不是分别挂在:

张三
小张
zhangsan

三个节点上。


七、真实 Entity Resolution 不能只看名字

例如:

王伟

中国可能有无数个。

假设知识库里有:

王伟
Java 工程师
北京
A 公司

又出现:

王伟
产品经理
深圳
B 公司

名字虽然完全相同:

王伟 = 王伟

但显然不一定是同一个人。

所以判断实体是否相同,需要综合:

Name
Type
Description
Attributes
Relationships
Context

例如:

姓名相同

+

公司相同

+

职位相似

+

邮箱一致

那么:

同一个人的概率

就很高。


八、可以自己实现一个简单打分

例如:

def entity_score(
    a,
    b
):

    score = 0

    if (
        a["name"]
        ==
        b["name"]
    ):
        score += 50

    if (
        a.get("email")
        ==
        b.get("email")
        and
        a.get("email")
    ):
        score += 50

    if (
        a.get("company")
        ==
        b.get("company")
    ):
        score += 20

    if (
        a.get("type")
        ==
        b.get("type")
    ):
        score += 10

    return score

实体:

a = {
    "name": "张三",
    "email": "zhangsan@example.com",
    "company": "A公司",
    "type": "Person"
}

另一个:

b = {
    "name": "张三",
    "email": "zhangsan@example.com",
    "company": "A公司",
    "type": "Person"
}

执行:

print(
    entity_score(a, b)
)

结果:

130

那么可以设置:

if entity_score(a, b) >= 80:
    print("认为是同一个实体")

真实系统当然会更复杂。

但基本思路类似。


九、Embedding 也可以参与 Entity Resolution

比如:

支付系统

和:

支付平台

字符串不同。

但描述:

公司内部用于订单支付、
退款与资金处理的核心系统

非常接近。

于是可以分别生成:

Embedding

计算:

Cosine Similarity

如果:

名称接近

+

描述相似

+

关系相似

那么:

可能是同一个 Entity。

所以现代 Entity Resolution 通常不是:

if name == name

这么简单。

而是:

字符串
+
Embedding
+
结构
+
上下文
+
LLM

联合判断。


十、Graphiti 为什么需要 LLM 参与去重?

因为很多实体判断:

本身就是语义问题。

比如:

GPT-5.6 Sol

和:

GPT 5.6 Sol

容易判断。

但:

支付平台

和:

公司支付核心系统

是不是一个?

单靠字符串距离:

很难。

LLM 可以结合:

Entity Name
Entity Type
Description
Existing Candidates
Episode Context

判断:

新实体究竟应该创建新 Node,还是合并到已有 Node。

Graphiti 当前源码中也专门存在类似:

dedupe_nodes
node_operations

这样的实体解析/去重逻辑。

这说明:

Entity Deduplication 不是附加功能,而是长期知识图谱的核心维护流程。


十一、实体重复会造成什么后果?

假设系统没有去重。

产生:

Node 1:
张三

Node 2:
小张

Node 3:
Zhang San

然后信息分别挂上去:

张三
→ worksAt
→ A 公司
小张
→ hasSkill
→ Go
Zhang San
→ worksOn
→ Payment Platform

用户问:

张三会什么技术?

系统只查:

张三

得到:

没有 Skill。

但其实:

小张
→ hasSkill
→ Go

已经存在。

知识不是没有。

而是:

被拆散了。


十二、实体去重本质是在“合并上下文”

如果确认:

张三 = 小张

最终应该:

张三
├── worksAt → A公司
├── hasSkill → Go
└── worksOn → Payment Platform

原本三个孤岛:

事实 A
事实 B
事实 C

重新聚合到:

Canonical Entity

上。

这对 Agent 特别重要。

因为:

Agent 的理解能力很大程度取决于上下文是否连接完整。


十三、但是实体去重还不是最难的问题

更麻烦的是:

Fact Conflict。

比如:

张三
→ worksAt
→ A公司

后来又出现:

张三
→ worksAt
→ B公司

这两个事实:

到底是冲突?

还是同时成立?

答案:

不一定。

如果:

worksAt

允许兼职:

A公司 + B公司

可能同时成立。

如果语义是:

currentEmployer

通常只能有一个。

所以事实冲突判断:

必须理解 Property 的语义。


十四、这就是 Ontology 为什么重要

假设定义:

currentEmployer

是:

Functional Property

意味着:

一个 Person
最多只有一个 currentEmployer

那么:

张三
→ currentEmployer
→ A公司

后来出现:

张三
→ currentEmployer
→ B公司

新事实很可能意味着:

A 公司已经不是当前雇主。

而如果属性是:

hasSkill

出现:

张三 → hasSkill → Go

张三 → hasSkill → Java

完全不冲突。

因为:

一个人可以拥有多个技能。

这说明:

冲突检测必须结合关系类型。


十五、新事实出现以后,最粗暴的方法是什么?

直接:

DELETE 旧事实

例如:

DELETE FROM memory
WHERE
    subject = '张三'
    AND predicate = 'worksAt';

然后:

INSERT B公司

结果最终只剩:

张三
→ worksAt
→ B公司

看似干净。

但问题是:

历史丢了。

以后用户问:

张三以前在哪里工作?

系统:

不知道。

十六、Graphiti 的思路不是直接删除,而是失效

这就和我们上一篇 Temporal Knowledge Graph 连起来了。

旧事实:

张三
→ worksAt
→ A公司

不是删除。

而是:

valid_at:
2025-01-01

invalid_at:
2026-03-01

新事实:

张三
→ worksAt
→ B公司

记录:

valid_at:
2026-03-01

invalid_at:
null

这样:

现在
→ B公司

而:

以前
→ A公司

都能回答。

Graphiti 项目现在也明确把:

invalidated_edges

作为事实解析/冲突处理流程中的重要结果之一。


十七、所以“冲突”并不等于“错误”

这是很重要的一点。

旧事实:

iPhone 售价 7999

新事实:

iPhone 售价 7499

不能简单说:

7999 是错误数据。

它可能在:

上个月

是真的。

所以正确表达应该是:

7999
valid:
1 月 ~ 3 月

7499
valid:
3 月 ~ 当前

这就是:

State Transition

状态变化。


十八、Agent Memory 最需要这种能力

比如:

用户当前主语言

2025:

Go

2026:

Python

如果直接覆盖:

历史消失。

如果都保存为有效事实:

当前状态不明确。

最合理:

Go
valid_at:
2025-01

invalid_at:
2026-04

以及:

Python
valid_at:
2026-04

invalid_at:
null

Agent 问:

用户现在主要写什么?

得到:

Python

问:

他之前主要写什么?

得到:

Go

十九、还有第三种问题:重复事实

假设三个 Episode:

Episode 1:
张三负责支付系统。
Episode 2:
张三目前负责支付平台。
Episode 3:
支付系统的负责人是张三。

如果:

支付系统 = 支付平台

并且:

张三 = 张三

那么三条提取结果很可能其实都是:

张三
→ responsibleFor
→ 支付系统

如果全部存:

Edge 1
Edge 2
Edge 3

就又产生:

Edge Duplication。


二十、为什么重复 Edge 也有问题?

查询:

张三负责什么?

可能返回:

支付系统
支付系统
支付系统

更严重的是,如果做:

COUNT

统计:

张三负责 3 个项目

实际上只有:

1 个。

所以:

Node 要去重,Edge 也要去重。

Graphiti 源码当前同样存在:

dedupe_edges
edge_operations

这样的处理。


二十一、一个简化版 Edge 去重怎么写?

定义:

from dataclasses import dataclass


@dataclass
class Fact:

    subject: str

    predicate: str

    object: str

数据:

facts = [

    Fact(
        "张三",
        "responsibleFor",
        "支付系统"
    ),

    Fact(
        "张三",
        "responsibleFor",
        "支付系统"
    ),

    Fact(
        "李四",
        "maintains",
        "PostgreSQL"
    )
]

去重:

unique = {}

for fact in facts:

    key = (
        fact.subject,
        fact.predicate,
        fact.object
    )

    unique[key] = fact

最终:

facts = list(
    unique.values()
)

即可去掉完全一样的 Triple。


二十二、但语义重复更麻烦

例如:

张三
→ responsibleFor
→ 支付系统

和:

张三
→ owns
→ 支付平台

如果:

responsibleFor ≈ owns

并且:

支付系统 = 支付平台

那么:

两条 Edge

可能其实表达同一个事实。

这就无法通过:

tuple == tuple

判断。

又需要:

Ontology
Embedding
LLM
Context

参与。


二十三、所以知识图谱维护至少有三层去重

第一层:

字符串级

OpenAI
OPENAI
Open AI

第二层:

实体级

张三
小张
@zhangsan

→

Person-1001

第三层:

事实级

张三负责支付系统

支付系统负责人是张三

张三负责公司的支付平台

最终可能归并为:

Person-1001
→ responsibleFor
→ PaymentSystem-001

二十四、Episode 为什么不能删除?

假设 Edge 被合并以后:

三条 Episode
↓
一个 Fact

是不是可以把三个 Episode 删除?

最好不要。

因为它们是:

Evidence。

例如:

Fact:
张三负责支付系统

来源:

Episode 1:
会议纪要

Episode 2:
员工资料

Episode 3:
项目文档

以后可以知道:

这条 Fact
被多个 Source 支持。

这对:

可信度
审计
解释

都非常重要。


二十五、这就形成一套非常漂亮的数据结构

可以理解成:

Episode 1 ─┐
           │
Episode 2 ─┼──→ Fact ───→ Entity
           │
Episode 3 ─┘

比如:

会议纪要 ─┐
员工资料 ─┼→ 张三 responsibleFor 支付系统
项目文档 ─┘

Fact 只有一个。

Evidence 可以有多个。

这比简单:

Chunk → Vector DB

表达能力强很多。


二十六、如果新 Episode 推翻旧 Fact 呢?

例如:

Episode 1:
2025-01

张三负责支付系统。

生成:

Fact A:

张三
→ responsibleFor
→ 支付系统

后来:

Episode 2:
2026-05

支付系统负责人已经调整为李四。

系统应该判断:

新 Fact:

李四
→ responsibleFor
→ 支付系统

可能与旧:

张三
→ responsibleFor
→ 支付系统

构成状态更新。

于是:

Fact A

invalid_at:
2026-05

Fact B:

valid_at:
2026-05

二十七、知识更新流程可以概括成 7 步

一个新的 Episode 进入:

1. Extract Entity

↓

2. 找候选 Existing Entity

↓

3. Entity Resolution

↓

4. Extract Relation / Fact

↓

5. Edge Deduplication

↓

6. Contradiction Detection

↓

7. 新 Fact 写入
   旧 Fact 必要时 invalid

这就是一个很典型的:

Incremental Knowledge Graph

构建过程。

重点是:

Incremental

不是每天:

把整个图删掉重建。

二十八、为什么“增量更新”对 Agent 非常重要?

一个个人 AI 助手每天可能产生:

几十
几百

条新消息。

不可能每说一句话:

重新处理过去一年的全部聊天。

更合理:

New Episode
↓
只和相关 Entity / Fact 比较
↓
局部更新 Graph

所以系统必须有:

Candidate Retrieval

能力。


二十九、Candidate Retrieval 是什么意思?

新 Entity:

小张

不应该和:

整个数据库里的 1000 万 Entity

逐个比较。

而是先召回:

最可能相同的 10~50 个节点。

例如通过:

Name Search
BM25
Embedding
Entity Type
Group

得到:

张三
张小三
张强
Zhang San

然后再让:

规则 / LLM

判断。

整个过程:

New Entity
↓
Candidate Search
↓
Top K Candidates
↓
Dedup Decision
↓
Merge / New Node

这就比:

N × N 比较

高效得多。


三十、我们自己做一个最小 Entity Resolution

假设实体:

existing_entities = [

    {
        "id": "person-1001",
        "name": "张三",
        "aliases": [
            "小张",
            "zhangsan"
        ],
        "company": "A公司"
    },

    {
        "id": "person-1002",
        "name": "李四",
        "aliases": [
            "小李"
        ],
        "company": "B公司"
    }
]

新实体:

new_entity = {
    "name": "小张",
    "company": "A公司"
}

实现:

def resolve_entity(
    new_entity,
    entities
):

    for entity in entities:

        names = {
            entity["name"],
            *entity.get(
                "aliases",
                []
            )
        }

        if (
            new_entity["name"]
            in names
        ):

            return entity["id"]

    return None

结果:

print(
    resolve_entity(
        new_entity,
        existing_entities
    )
)

输出:

person-1001

三十一、如果没有找到,就创建 Node

例如:

resolved_id = resolve_entity(
    new_entity,
    existing_entities
)

然后:

if resolved_id is None:

    print(
        "Create new entity"
    )

else:

    print(
        "Merge into:",
        resolved_id
    )

这是整个 Entity Resolution 最核心的两个结果:

MATCH

或者:

CREATE

三十二、合并以后属性怎么办?

这是另一个问题。

已有:

old = {
    "name": "张三",
    "company": "A公司",
    "skills": [
        "Java"
    ]
}

新数据:

new = {
    "name": "小张",
    "skills": [
        "Go",
        "Redis"
    ]
}

不能直接:

old.update(new)

否则:

name

可能变成:

小张

覆盖 Canonical Name。

更合理:

old["aliases"].append(
    "小张"
)

old["skills"] = list(
    set(
        old["skills"]
        +
        new["skills"]
    )
)

得到:

name:
张三

aliases:
小张

skills:
Java
Go
Redis

所以:

Entity Merge 不是简单 Dictionary Update。


三十三、不同属性需要不同 Merge Strategy

例如:

Alias

Append

Skill

Union

Email

可能:

Replace / Verify

currentCompany

Temporal Update

Birthday

可能:

Conflict Check

Biography

可能:

Regenerate Summary

所以一个成熟知识系统会针对不同:

Property

设计不同:

Merge Policy。


三十四、这就是 Schema / Ontology 的另一个价值

如果系统知道:

hasSkill

是:

Multi-valued

就可以:

Union

如果知道:

currentEmployer

是:

Single-valued

就可以:

Invalidate old
+
Add new

如果:

birthDate

出现两个值:

1990-01-01
1992-01-01

不能自动覆盖。

应该:

Flag Conflict

这说明 Ontology 不只是:

让知识图谱看起来更规范。

它还可以指导:

Knowledge Update。


三十五、一个简单的 Fact Update Engine

我们自己实现:

from dataclasses import dataclass
from datetime import datetime


@dataclass
class Fact:

    subject: str

    predicate: str

    object: str

    valid_at: datetime

    invalid_at: (
        datetime | None
    ) = None

定义:

SINGLE_VALUE_PROPERTIES = {
    "currentEmployer",
    "currentCity",
    "currentRole"
}

加入新事实:

def add_fact(
    facts,
    new_fact
):

    if (
        new_fact.predicate
        in
        SINGLE_VALUE_PROPERTIES
    ):

        for fact in facts:

            if (
                fact.subject
                ==
                new_fact.subject

                and

                fact.predicate
                ==
                new_fact.predicate

                and

                fact.invalid_at
                is None
            ):

                if (
                    fact.object
                    !=
                    new_fact.object
                ):

                    fact.invalid_at = (
                        new_fact.valid_at
                    )

    facts.append(
        new_fact
    )

三十六、测试公司变化

旧事实:

facts = [
    Fact(
        subject="张三",
        predicate="currentEmployer",
        object="A公司",
        valid_at=datetime(
            2025,
            1,
            1
        )
    )
]

新事实:

new_fact = Fact(
    subject="张三",
    predicate="currentEmployer",
    object="B公司",
    valid_at=datetime(
        2026,
        3,
        1
    )
)

执行:

add_fact(
    facts,
    new_fact
)

最后:

张三
currentEmployer
A公司

valid:
2025-01-01
→
2026-03-01

以及:

张三
currentEmployer
B公司

valid:
2026-03-01
→
现在

这就是 Graphiti 类系统最核心的知识更新思想之一。


三十七、如果 Predicate 是 hasSkill 呢?

新:

张三
→ hasSkill
→ Go

旧:

张三
→ hasSkill
→ Java

因为:

hasSkill

不在:

SINGLE_VALUE_PROPERTIES

所以:

Java

不会失效。

结果:

Java
+
Go

共同保留。

这才符合真实语义。


三十八、不过“不会 Go 了”又怎么办?

用户明确说:

我现在已经不写 Java 了。

这时候不是添加:

张三
→ hasSkill
→ Java

而是需要理解:

已有 Java Skill
应该失效?

这就涉及:

Negation
Fact Invalidation
Temporal Reasoning

比简单:

insert triple

复杂得多。


三十九、这也是 LLM 很适合参与的一步

比如两条内容:

旧:

张三目前负责支付系统。

新:

张三已经不再负责支付系统,
负责人调整为李四。

让传统规则判断:

第二句话具体否定了什么?

并不简单。

LLM 可以帮助抽取:

Invalidate:

张三
responsibleFor
支付系统

以及新增:

李四
responsibleFor
支付系统

这就是:

LLM Extraction + Graph Maintenance

结合的地方。


四十、所以 LLM 不是数据库

这里其实有一个很重要的工程原则。

不要让 LLM:

直接随意修改整个知识图谱。

更合理:

Episode
↓
LLM 提取候选知识
↓
Schema Validation
↓
Entity Resolution
↓
Edge Deduplication
↓
Conflict Detection
↓
Temporal Update
↓
Graph Database

也就是说:

LLM 负责理解,系统负责约束。

这个思路非常重要。


四十一、Graphiti 为什么适合长期 Agent?

因为真正长期运行以后,系统不断面对:

新 Entity
旧 Entity

新 Fact
旧 Fact

重复
冲突
变化
补充
纠错

而不是:

一次建图
永远不变。

Graphiti 强调:

Incremental Graph Construction

就是因为现实世界:

一直在变化。


四十二、一个个人 Agent 的真实例子

第一次聊天:

用户:
我主要做 Java 后端。

图:

User
→ mainLanguage
→ Java

第二个月:

用户:
最近工作主要写 Go。

不能创建:

User-2

应该 Resolve 到:

同一个 User。

Fact:

Java

可能失效。

新增:

Go

第三个月:

用户:
我最近开始研究 Agent,
主要用 Python 写实验。

新增:

User
→ interestedIn
→ AI Agent

以及:

User
→ uses
→ Python

最后形成:

User
│
├── Java
│   valid: 2025 → 2026-01
│
├── Go
│   valid: 2026-01 →
│
├── Python
│   valid: 2026-03 →
│
└── AI Agent
    interestedIn

这才是:

越用越完整的 Memory。


四十三、如果不去重会怎样?

可能变成:

User
→ Java

用户
→ Go

王先生
→ Python

JavaPub
→ AI Agent

四个 Node。

Agent 看到:

四个人。

事实上:

全是同一个用户。

于是所谓:

长期记忆

实际上只是:

长期堆垃圾。


四十四、企业 CRM 更明显

假设客户:

北京未来科技有限公司

销售录入:

未来科技

邮件:

北京未来科技

合同:

北京未来科技有限公司

如果不做 Entity Resolution:

CRM Customer A

Email Customer B

Contract Customer C

Agent 就无法知道:

它们是同一个客户。

结果:

客户历史
订单
合同
联系人
沟通记录

全部被拆开。

所以:

Entity Resolution 是企业 Agent 的基础能力。


四十五、代码资产也一样

例如:

shiyu-admin
ShiyuAdmin
Rodert/ShiyuAdmin
github.com/Rodert/ShiyuAdmin

其实:

都是同一个 Project。

如果 Agent 管理代码知识:

Issue
PR
Commit
README
Architecture

必须先解决:

Project Identity。

否则整个知识图谱都会碎。


四十六、搜索也会受到实体重复影响

用户搜索:

ShiyuAdmin 权限设计

系统可能只搜索:

ShiyuAdmin

节点。

但:

Rodert/ShiyuAdmin

节点下面却挂着:

RBAC
Permission
Role
Menu

因为 Node 没有 Merge:

检索结果直接丢一半。

所以去重不仅影响:

存储。

还直接影响:

Retrieval Quality。


四十七、一个成熟 Memory Pipeline 应该是什么?

我认为至少应该:

Input
↓
Episode
↓
Entity Extraction
↓
Candidate Retrieval
↓
Entity Resolution
↓
Canonical Entity
↓
Relation Extraction
↓
Edge Resolution
↓
Duplicate Check
↓
Contradiction Check
↓
Temporal Invalidation
↓
Graph Storage
↓
Hybrid Retrieval

而不是:

LLM
↓
INSERT

这么简单。


四十八、这也解释了为什么 Agent Memory 很难

很多教程会写:

vector_db.add(
    conversation
)

然后说:

AI 已经有长期记忆了。

严格来说:

这只是 Long-term Retrieval。

真正的 Memory System 还需要:

Identity
State
Time
Relation
Conflict
Source
Update

至少这些维度。


四十九、Vector Database 解决哪个问题?

Vector DB 主要解决:

哪段历史信息
和当前 Query 最相关?

这是:

Retrieval。

但它并不会天然解决:

张三 = 小张吗?

A公司还是当前公司吗?

这条信息已经过期了吗?

两条事实冲突吗?

哪个事实更新?

这个 Fact 来自哪里?

这些属于:

Knowledge Management。

所以:

Vector Memory

和:

Knowledge Graph Memory

解决的是不同层次的问题。


五十、Graphiti 仍然使用 Hybrid Search

值得注意的是:

Graphiti 并不是有了 Graph 后:

就不要 Embedding。

它依然结合:

Semantic Search
+
BM25
+
Graph

也就是说:

Vector

依然非常重要。

只不过 Vector 的职责从:

整个 Memory 系统

变成:

Memory Retrieval 的一部分。

这是更合理的架构。


五十一、最终可以把 Agent Memory 分成四层

第一层:

Raw Memory

Episode
Conversation
Document
Event

第二层:

Entity Layer

Person
Company
Project
Product
Technology

并负责:

Entity Resolution

第三层:

Fact Layer

Entity
→ Relation
→ Entity

负责:

Dedup
Conflict
Temporal Validity

第四层:

Retrieval Layer

Embedding
BM25
Graph Traversal
Reranking

给 Agent 提供 Context。

整个架构:

Raw Data
   ↓
Episode
   ↓
Entity Resolution
   ↓
Canonical Entity
   ↓
Fact Resolution
   ↓
Temporal Graph
   ↓
Hybrid Retrieval
   ↓
Agent

五十二、如果自己做 Agent Memory,我会重点关注这几个字段

Entity:

{
  "id": "person-1001",
  "name": "张三",
  "type": "Person",
  "aliases": [
    "小张",
    "zhangsan"
  ]
}

Fact:

{
  "subject": "person-1001",
  "predicate": "currentEmployer",
  "object": "company-2001",
  "valid_at": "2026-03-01",
  "invalid_at": null
}

Source:

{
  "episode_id": "episode-8899",
  "source": "chat",
  "reference_time": "2026-03-01"
}

这样最少已经拥有:

身份

关系

时间

来源

四个关键维度。


五十三、再加 Confidence 会更实用

例如:

{
  "subject": "person-1001",
  "predicate": "likes",
  "object": "Rust",
  "confidence": 0.62
}

为什么只有:

0.62

?

因为用户只是说:

Rust 的所有权模型挺有意思。

这并不能强推出:

用户非常喜欢 Rust。

所以:

Extracted Fact ≠ Absolute Truth。

真实系统最好区分:

明确事实

弱推断

用户偏好

系统推断

否则 LLM 很容易把:

可能

存成:

确定。

五十四、再加 Provenance

Fact:

User
→ interestedIn
→ Rust

来源:

Episode-00123

原文:

最近看了一下 Rust,
所有权机制还挺有意思。

这样以后系统可以:

回查原文。

如果发现抽取错误:

还能重新计算。

这就是为什么保留:

Episode

特别重要。


五十五、最终你会发现:真正困难的不是“记住”

而是:

忘掉什么

合并什么

更新什么

保留什么

什么时候有效

相信到什么程度

人脑记忆其实也类似。

我们不会把每天遇到的所有信息:

原封不动永远保存。

而是在不断:

归纳

合并

修正

更新

遗忘

AI Memory 也一样。


总结

很多人做 Agent Memory 时,关注的是:

怎么让 AI 记住更多?

但一个真正长期运行的系统,更应该先问:

怎么让 AI 的记忆不要越来越乱?

因为随着数据不断增加:

同一个人
会出现不同名字。

同一个项目
会出现不同叫法。

同一个事实
会被多次描述。

旧事实
会被新事实取代。

不同来源
甚至会互相矛盾。

所以长期记忆不能只是:

INSERT
INSERT
INSERT
INSERT

必须不断进行:

Entity Resolution
+
Node Deduplication
+
Edge Deduplication
+
Conflict Detection
+
Fact Invalidation
+
Temporal Update
+
Provenance

Graphiti 值得研究的地方,就在这里。

它不是简单做:

Conversation
↓
Embedding
↓
Vector DB

而是在尝试维护:

一个会随着现实世界不断更新的 Context Graph。

如果只记住一句话:

AI 长期记忆真正难的不是“存进去”,而是知道新信息应该“新建、合并,还是替换旧事实”。

项目:

https://github.com/getzep/graphiti

这也是从:

RAG

走向:

真正 Agent Memory

非常关键的一步。

posted @ 2026-09-27 21:53  JavaPub  阅读(8)  评论(0)    收藏  举报