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
可能:
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
非常关键的一步。

浙公网安备 33010602011771号