Agent开发——知识图谱

Agent开发——知识图谱,使用知识图谱的五元组解决记忆问题

前言

使用简单的知识图谱来解决Agent的记忆问题,继“记忆压缩”、RAG之后的第三种记忆存取形式

观前提示:本文所使用的并不是GraphRAG,而仅仅是知识图谱,与其说是记忆,更像是古早的知识图谱+LLM的知识库形式

原理

和我们之前的两种记忆解决方案一样,大致流程就是:

上一轮对话结束——> 保存对话内容 ——> 之后的对话需要记忆搜索 ——> 搜索到保存的对话记忆 ——> 如此循环

具体流程图如下:

img1

以上的实现思路源自 开源项目娜迦Agent, 及其作者 b站:柏斯阔落

实现

获取数据库

约束唯一性,仅第一次运行即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建唯一性约束确保数据一致性
constraints = [
"CREATE CONSTRAINT Person IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE",
"CREATE CONSTRAINT Position IF NOT EXISTS FOR (po:Position) REQUIRE po.name IS UNIQUE",
"CREATE CONSTRAINT Organization IF NOT EXISTS FOR (o:Organization) REQUIRE o.name IS UNIQUE",
"CREATE CONSTRAINT Item IF NOT EXISTS FOR (i:Item) REQUIRE i.name IS UNIQUE",
"CREATE CONSTRAINT Concept IF NOT EXISTS FOR (c:Concept) REQUIRE c.name IS UNIQUE",
"CREATE CONSTRAINT Time IF NOT EXISTS FOR (t:Time) REQUIRE t.name IS UNIQUE",
"CREATE CONSTRAINT Event IF NOT EXISTS FOR (e:Event) REQUIRE e.name IS UNIQUE",
"CREATE CONSTRAINT Activity IF NOT EXISTS FOR (a:Activity) REQUIRE a.name IS UNIQUE"
]

for constraint in constraints:
get_graph().run(constraint)
1
2
3
4
def get_graph():
# 连接到Neo4j数据库
graph = Graph("neo4j://localhost:7475", auth=("neo4j", "yourpassword"))
return graph

对话内容拆分五元组

在AI时代之前,这属于NLP领域,有了AI之后,只要文本内容不长或者你舍得Token,AI进行chunking的处理显然是最理想的。

五元组的定义 [“主语”,“主语类型”,“谓语(关系)”,“宾语”,“宾语类型”]
如句子,“小明喜欢吃苹果”,可拆分为
[“小明”,“人物”,“喜欢吃”,“苹果”,“物品”]

在将来检索时,只需要搜索“喜欢吃”即可搜索到相关五元组

此处的prompt来自 开源项目娜迦Agent

使用第三方AI(在此处为agent_nopic,没有任何Tool和上下文的纯LLM)进行五元组分割,若内容没有实际意义则返回空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def extract_quintuples(text:str):
"""使用结构化输出的五元组提取"""
system_prompt = f"""
从以下中文文本中抽取有价值的五元组(主语-主语类型-谓语-宾语-宾语类型)关系,以 JSON 数组格式返回。

## 提取规则
1. 只提取**事实性**信息,包括:
- 具体的行为和动作
- 明确的实体关系
- 实际存在的状态和属性
- 用户表达的具体需求、偏好、计划


2. 严格过滤以下内容:
- 比喻、拟人、夸张等修辞手法
- 时间的陈述(如“现在是14点”)
- 虚拟、假设、想象的内容
- 纯粹的情感表达(如"我很开心"、"你真棒")
- 赞美、讽刺、调侃等主观评价
- 闲聊中的无关信息
- 重复或冗余的关系

3. 类型包括但不限于:人物、地点、组织、物品、概念、时间、事件、活动等。

## 示例
输入:小明在公园里踢足球。
输出:[["小明", "人物", "踢", "足球", "物品"], ["小明", "人物", "在", "公园", "地点"]]

输入:你像小太阳一样温暖。
输出:[] (比喻句,不提取)

输入:我喜欢吃苹果和香蕉。
输出:[["我", "人物", "喜欢吃", "苹果", "物品"], ["我", "人物", "喜欢吃", "香蕉", "物品"]]

输入:如果我是鸟,我会飞到月球。
输出:[] (假设内容,不提取)

请从文本中提取有价值的事实性五元组:
{text}

除了JSON数据,请不要输出任何其他数据,例如:```、```json、以下是我提取的数据:。
"""

msgs = []
msgs.append({
"role": "system",
"content": system_prompt
})
msgs.append({
"role": "user",
"content": text
})

res = get_agent_nopic().invoke({
"messages":msgs
})

latest_message = res["messages"][-1]
if latest_message.content:
res=latest_message.content.strip()

quintuples = json.loads(res)
print(f"提取到 {len(quintuples)} 个五元组")
print([tuple(t) for t in quintuples if len(t) == 5])
return [tuple(t) for t in quintuples if len(t) == 5]

五元组存入Neo4j

对话内容进行了五元组拆分后,即可存入知识图谱
注意:上面的拆分的返回结果为 quintuple的tuple,即一句话会被拆分为多个五元组,所以需要for导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def store_quintuples(quintuples:tuple):
graph = get_graph()
if len(quintuples)==0:
return False
print("存储开始:")
for subject, subject_type, relation, object_, object_type in quintuples:
print(f"主语: {subject} (类型: {subject_type})")
print(f"谓语: {relation}")
print(f"宾语: {object_} (类型: {object_type})")
print("---")
h_label = subject_type
t_label = object_type
h_node = Node(h_label, name=subject, entity_type=subject_type)
t_node = Node(t_label, name=object_, entity_type=object_type)

# 合并时使用对应标签和唯一键 "name"
graph.merge(h_node, h_label, "name")
graph.merge(t_node, t_label, "name")

r = Relationship(h_node, relation, t_node, head_type=subject_type, tail_type=object_type)
graph.merge(r)
return True

五元组搜索keyword

搜索五元组中keyword任何一个可能出现的地方,即五元的四个where “OR”

1
2
3
4
5
6
7
8
9
10
11
12
13
def search_quintuples(keyword:str):
graph = get_graph()
query = """
MATCH (e1)-[r]->(e2)
WHERE e1.name CONTAINS $kw OR e2.name CONTAINS $kw
OR e1.entity_type CONTAINS $kw OR e2.entity_type CONTAINS $kw
OR type(r) CONTAINS $kw
RETURN e1.name, e1.entity_type, type(r), e2.name, e2.entity_type LIMIT 5
"""

results = graph.run(query, kw=keyword).data()
print("关键词搜索结果:"+str(results))
return results

Tool

1
2
3
@tool(description="知识图谱记忆搜索")
def graph_search_tool(input:str):
return memory.graph_memory.search_quintuples(input)

Tool-Prompt

和之前的RAG检索的Prompt几乎一致

1
2
3
4
5
6
graph_search_tool:
- 核心能力:入参为用户输入语句中的关键词,从知识图谱精准检索相关的记忆以在对话情景中展现日常对话的;
- 出参:与关键词相关的几条记忆知识;
- 使用场景:当用户谈到涉及“你还记得”、“上次”、“忘了”、“回想”等涉及记忆的场景,你现有短期记忆无法精准解答时,调用此工具获取此前的长期记忆;
- 调用规则:若tool返回的此前记忆也与对话内容无关,则可以忽略该记忆;若返回的记忆与对话内容相关,则结合记忆与上下文情景与用户对话;
- 示例:user:"你还记得我之前说过我最爱吃的甜品吗",入参传入“甜品”或“爱吃”,得到记忆并结合记忆与上下文情景继续对话;

效果

首先,来一句能用于记忆的话题

1
2
3
用户输入:我最爱的乐队是Fall Out Boy
AIMessage(content="Fall Out Boy啊!那首《Sugar, We're Goin Down》可是超级经典呢。
看来Master喜欢这种充满活力的流行朋克风格?下次要不要一起听听他们的新专辑,或者聊聊你最 喜欢他们的哪首歌?" ........)

对话结束后启动五元组分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
提取到 3 个五元组
[('Master', '人物', '最爱的乐队是', 'Fall Out Boy', '组织'), ('Fall Out Boy', '组织', '拥有歌曲', "Sugar, We're Goin Down", '物品'), ("Sugar, We're Goin Down", '物品', '风格是', '流行朋克', '概念')]
存储开始:
主语: Master (类型: 人物)
谓语: 最爱的乐队是
宾语: Fall Out Boy (类型: 组织)
---
主语: Fall Out Boy (类型: 组织)
谓语: 拥有歌曲
宾语: Sugar, We're Goin Down (类型: 物品)
---
主语: Sugar, We're Goin Down (类型: 物品)
谓语: 风格是
宾语: 流行朋克 (类型: 概念)
---

看看Neo4j的webui

img2

已经存进去了

挑起记忆对话

此前已关闭RAG功能,并删除短期记忆

1
2
3
4
5
用户输入:你还记得我说过我最爱的乐队是什么吗
{'name': 'graph_search_tool', 'args': {'input': '最爱的乐队'},.....}
关键词搜索结果:[{'e1.name': 'Master', 'e1.entity_type': '人物', 'type(r)': '最爱的乐队是', 'e2.name': 'Fall Out Boy', 'e2.entity_type': '组织'}]
AIMessage(content='当然记得啦,Master最爱的乐队是Fall Out Boy!
他们的歌总是很有活力,每次听都让人忍不住跟着节奏摇摆呢。Master最近有在听他们的新歌吗?'。。。。。。)

大成功!

总结

看到这里,不知道你发现没有,知识图谱的记忆系统的keyword搜索是直接拿着str进去搜的,与RAG的语义相似的向量不同,本方法搜的是什么就是什么,“喜欢”与“爱”,“西红柿”和“番茄”这类词搜索的结果是不同的!

这也是我在使用知识图谱做记忆系统后十分纠结的原因,想要缓解这一问题的一个方法就是开放此前的五元组分割的尺度,将记忆系统扩大为一个真正的知识库,否则在记忆效果上它是被RAG吊打的。

然而,知识图谱在记忆搜索深度上也是吊打RAG的,比方说我上述的例子:
“Master喜欢Fall Out Boy”,一般RAG搜索“喜欢的乐队”只会出现“Master喜欢Fall Out Boy”这样一句话,而知识图谱如果多搜索几轮可以沿着知识网发散到各处。

综上,RAG在这类Agent的记忆解决方面效果是优于知识图谱的,但是RAG的记忆深度不及知识图谱,也不具有记忆的关联搜索能力。
因此,知识图谱作为RAG无法有效搜索记忆时的补充也是不错的选择,可以将RAG的优先级设置为最高,并将知识图谱搜索的深度提高(注意Token),二者配合即为一个更加完善的记忆系统。

知识图谱(五元组) RAG 记忆压缩(短期记忆)
记忆效果 一般(无法识别同义词) 较强(可以识别同义词,但是没有深度) 短期极强,长期极弱
记忆深度 极强(如果你不缺Token,可以在搜索时直接BFS、DFS) 短期极强,长期极弱
Token花费 高(线性增长)
可视化效果 强(Neo4j的可视化图) 强?(直接看json文本)