DLAI-知识图谱-RAG-笔记-全-
DLAI 知识图谱 RAG 笔记(全)
001:引言 🚀

在本课程中,我们将学习如何利用知识图谱来增强检索增强生成(RAG)应用。知识图谱是一种强大的工具,它能通过强调事物间的关系来组织和存储数据,从而提升信息检索的准确性和上下文相关性。
什么是知识图谱?🧠
上一节我们介绍了课程目标,本节中我们来看看知识图谱的基本概念。
知识图谱提供了一种存储和组织数据的方式,其核心在于强调事物之间的关系。与传统的关系型数据库(将数据组织成带行和列的表)不同,知识图谱采用基于图的结构。这种结构包含:
- 节点:代表事物或实体,例如人或公司。
- 边:代表这些实体之间的连接或关系,例如雇主与雇员的关系。
因此,当你听到“节点”时,可以将其理解为事物或实体;当你听到“边”时,可以将其理解为事物之间的关系。
图谱中的每个节点或边还可以存储额外的信息:
- 对于节点,可以存储实体的详细信息,例如一个人的姓名、邮箱等。
- 对于边,可以存储关系的详细信息,例如雇佣关系中的职位、入职日期等。
节点和关系的图结构非常灵活,相比关系型数据库,它能更方便地对现实世界的某些部分进行建模。
知识图谱的优势 ⚡

了解了基本结构后,我们来看看知识图谱的优势所在。
知识图谱使得表示和搜索深层关系变得更加容易,因为关系本身就是数据库的一个组成部分,而不仅仅是两个表之间共享的键。这使得查询执行速度更快,能更高效地定位所需数据。
正因如此,提供产品搜索功能的网络搜索引擎和电子商务网站都将知识图谱视为提供相关搜索结果的关键技术。事实上,当你在谷歌或必应上搜索某位名人时,侧边栏返回的卡片信息,就是通过知识图谱检索得到的。

知识图谱与RAG的结合 🤖
我们已经看到了知识图谱在搜索中的威力,那么它如何与RAG结合呢?
当你将知识图谱与嵌入模型结合时,就拥有了一个非常强大的工具,可用于与大语言模型(LLM)一起执行检索增强生成(RAG)。
这是因为你可以利用图谱中存储的关系和元数据,来提高检索文本的相关性,从而传递给语言模型。在一个基础的RAG系统中,你想要查询或对话的文档首先会被分割成更小的片段或块,然后使用嵌入模型将这些文本块转换为向量。转换成向量形式后,你可以使用余弦相似度等相似性函数来搜索文本块,以找到与你的提示相关的部分。
但事实证明,将这些文本块存储在知识图谱中,为你从文档中检索相关数据开辟了新的途径。你不仅可以进行基于文本嵌入的相似性搜索,还可以检索一个文本块,然后遍历图谱以找到其他相关的文本块,从而为你的LLM提供更完整的上下文。在本课程中,你将看到这种方法如何揭示基于相似性的RAG可能遗漏的文本源之间的连接。

课程实践项目与目标 📈
理论部分已经介绍完毕,接下来我们将进入实践环节,看看本课程具体要构建什么。
在本课程中,你将学习如何构建一个知识图谱,来表示公司需要向美国证券交易委员会(SEC)提交的一系列财务表格。SEC是一个负责监管市场、保护投资者的美国政府机构。
以下是本课程的学习路径:
- 知识图谱入门:你将学习知识图谱的基础知识,并了解如何使用Neo4j的查询语言Cypher来探索和修改一个有趣的电影数据图。
- 结合嵌入模型:你将看到如何将Neo4j与文本嵌入模型结合使用,在知识图谱中为文本字段创建向量表示。
- 构建第一个图谱:你将构建一个知识图谱来表示一组SEC表格,并使用LangChain通过从该图谱中检索文本来执行RAG。
- 连接多个图谱:你将再次为第二组SEC表格构建知识图谱,使用一些链接数据将两个图连接起来,并学习如何使用更复杂的图查询来跨多组文档执行检索。
所有这些步骤结合在一起,将使你能够对SEC数据提出一些非常有趣的问题。
总结 🎯


本节课中我们一起学习了知识图谱的核心概念及其在RAG中的应用价值。我们了解到,知识图谱通过节点和边来结构化地表示实体及其关系,这种结构在表示复杂关系和高效查询方面具有优势。当与嵌入模型结合时,知识图谱能超越传统的相似性搜索,通过关系遍历为LLM提供更丰富、关联性更强的上下文,从而显著提升RAG系统的效果。在接下来的课程中,我们将动手实践,一步步构建属于我们自己的知识图谱RAG系统。
002:知识图谱基础 🔍

在本节课中,我们将要学习知识图谱的基础概念。我们将了解知识图谱如何作为一种数据结构,通过节点和节点之间的关系来存储信息。
概述
知识图谱是一种将信息存储在节点及其关系中的数据结构。本节我们将深入探讨节点、关系、标签和属性等核心概念,并通过一个简单的例子来理解数据模式是如何在图中形成的。
节点与关系
上一节我们介绍了知识图谱的基本思想。本节中,我们来看看构成图谱的两个基本元素:节点和关系。
节点是数据记录。我们可以通过绘制一个非常小的图来开始探索其在知识图谱中的含义。这里我们有一个只包含一个“事物”的图:一个名叫Andreas的人。
在文本表示中,我们使用括号()来表示一个节点。例如,(Person)表示这里有一个“人”节点。
现在,让我们添加另一个节点。我们有了这个人Andreas,以及另一个名叫Andrew的人。在文本表示中,我们同样使用括号:(Person), (Person)。现在我们有了一个数据模式:一个包含人的图。
当然,要构成一个完整的图,我们不仅需要“事物”,还需要这些“事物”之间的关系。这些关系也是数据记录。我们有一个“人”Andreas,一个“人”Andrew,以及一个“Andreas认识Andrew”的关系,并且这个“认识”关系始于2024年。
在文本表示中,节点用括号()表示,关系则用箭头-->表示,并带有类型。模式现在不仅仅是“人,人”,而是“人 -[认识]-> 人”。

如果你还记得计算机科学或数学中的图论知识,节点通常也被称为“顶点”,而我们数据结构中的关系可能被称为“边”。这些是不同的术语,但描述的是完全相同的概念。
关系的本质

作为数据结构,我们使用“关系”这个词而非“边”的原因是,关系本质上是一对节点以及关于这对节点的信息。你可以认为关系实际上包含了两个节点。例如,这里有一个关系包含了Andreas,并且方向性地也包含了Andrew。
扩展我们的图
为了稍微扩展我们的小图,我们引入一个新节点:课程“Knowledge Graphs for RAG”。
我们知道“人”Andreas“教授”这门课程。因此,我们有了两种新节点之间的新数据模式:我们已有的人节点和这个新的课程节点。
Andrew也与这门课程有关系:Andrew“介绍”了这门课程。因此,我们同时拥有了人与人之间、以及人与课程之间的关系。
如果你看底部的文本表示,这会变成一个稍长的数据模式。但你可以仔细阅读它:有一个人从左到右“教授”一门课程;另一方面,从外向内读,有一个人“介绍”那门课程。将所有内容放在一行中,你会有从左到右的方向,也有从右到左的方向,意味着课程在中间。
这里有一个关于如何在知识图谱中进行数据建模的有趣旁注:你可以说Andreas这个人是一位“教师”,Andrew这个人是一位“介绍者”。但我们并不真的需要在人本身添加这些额外的标签,因为Andreas“教授”课程,所以他是一位教师;Andrew“介绍”课程,所以他是一位介绍者。因此,你不需要给实体(即事物或人)本身添加额外的标签,只需使用关系来进一步限定这些人在图中的角色是什么。
节点与关系的属性
现在,我们正式介绍节点的标签。我们已经引入了“人”和“课程”的概念,在知识图谱中我们称这些为节点的“标签”。标签是一种将多个节点分组在一起的方式。例如,对于所有是人的节点,我们赋予“Person”标签;对于所有是课程的节点,我们赋予“Course”标签。
节点作为数据记录,当然也有值,这些值就是“属性”。属性是以键值对的形式存储在节点内部的。我们知道“人”Andreas有一个名为name的属性,其值是Andreas。同样,Andrew有一个name属性,值是Andrew。对于课程,它有一个title属性,值是Knowledge Graphs for RAG。

因为关系也是数据记录,它们具有方向、类型和属性。我们意识到我们有“认识”、“教授”和“介绍”这些关系。对于每一个关系,我们都可以通过添加属性来附加额外信息。例如,“认识”关系有一个since属性,值为2024;“教授”关系也有一个year属性,值为2024;“介绍”关系则有一个for属性,说明介绍了课程的哪个部分。
以下是节点和关系属性的总结:
- 节点属性示例:
(Person {name: ‘Andreas’})(Person {name: ‘Andrew’})(Course {title: ‘Knowledge Graphs for RAG’})
- 关系属性示例:
-[:KNOWS {since: 2024}]->-[:TEACHES {year: 2024}]->-[:INTRODUCES {for: ‘Section 2’}]->
知识图谱的定义
那么,什么是知识图谱?
知识图谱是一种将信息存储在节点和关系中的数据库。节点和关系都可以拥有属性(键值对)。节点可以被赋予标签以帮助对它们进行分组。关系总是具有类型和方向。

现在你对知识图谱是什么有了一些概念,并且开始对描述图结构的模式有了一些直觉。
总结
本节课中,我们一起学习了知识图谱的基础构成。我们明确了节点是代表实体的数据记录,关系是连接节点并带有方向的数据记录。节点可以拥有标签进行分类,以及属性来存储详细信息。关系则拥有类型和属性。这些元素共同构成了知识图谱的结构化数据模式。

接下来,让我们在代码中应用这些概念,使用一个真实的电影数据图。为此,你将使用一种名为Cypher的查询语言。请加入下一课来尝试一下。
003:使用Cypher查询知识图谱 🧠



在本节课中,我们将学习如何使用Cypher查询语言与一个包含演员和电影数据的知识图谱进行交互。我们将从基础查询开始,逐步探索更复杂的图模式匹配、条件筛选、数据修改等操作。
概述与环境设置
上一节我们介绍了知识图谱的基本概念。本节中,我们来看看如何通过代码与一个具体的知识图谱进行交互。
首先,我们需要导入必要的Python包并设置Neo4j数据库的连接环境。
import os
from langchain_community.graphs import Neo4jGraph
# 设置环境变量
os.environ["NEO4J_URI"] = "bolt://localhost:7687"
os.environ["NEO4J_USERNAME"] = "neo4j"
os.environ["NEO4J_PASSWORD"] = "password"
os.environ["NEO4J_DATABASE"] = "neo4j"
# 创建Neo4j图实例
kg = Neo4jGraph(
url=os.environ["NEO4J_URI"],
username=os.environ["NEO4J_USERNAME"],
password=os.environ["NEO4J_PASSWORD"],
database=os.environ["NEO4J_DATABASE"]
)
了解数据图谱结构
在开始查询之前,我们需要了解这个知识图谱的结构。图谱包含两种主要节点:Person(人物)和Movie(电影)。它们之间存在多种关系。
以下是节点和关系的说明:
- Person节点属性:
name(姓名),born(出生年份)。 - Movie节点属性:
title(标题),tagline(宣传语),released(上映年份)。 - 人物与电影的关系:
ACTED_IN(出演),DIRECTED(导演),WROTE(编剧),PRODUCED(制片),REVIEWED(评论)。 - 人物之间的关系:
FOLLOWS(关注)。例如,某人关注了某位影评人。
节点的“角色”(如演员、导演)由其与电影的关系决定,而非节点本身的标签。
基础Cypher查询
Cypher是Neo4j的图查询语言,使用模式匹配来查找图中的数据。一个基本查询由MATCH(匹配模式)和RETURN(返回结果)子句构成。
查询所有节点数量
让我们从最简单的查询开始:统计图谱中所有节点的数量。
cypher = """
MATCH (n)
RETURN count(n) AS number_of_nodes
"""
result = kg.query(cypher)
print(result)
# 输出: [{'number_of_nodes': 171}]
这个查询匹配了图中所有节点((n)),并返回其计数。结果显示该图谱共有171个节点。
查询特定类型的节点
我们通常只关心特定类型的节点,例如所有电影。这可以通过在节点模式中添加标签来实现。
以下是查询电影数量的方法:
cypher = """
MATCH (m:Movie)
RETURN count(m) AS number_of_movies
"""
result = kg.query(cypher)
print(result)
# 输出: [{'number_of_movies': 38}]
查询表明图中有38部电影。我们可以用同样的方法查询人物数量。
cypher = """
MATCH (p:Person)
RETURN count(p) AS number_of_people
"""
result = kg.query(cypher)
print(result)
# 输出: [{'number_of_people': 133}]
条件查询与属性匹配
精确匹配查询
如果我们想查找特定的节点,可以在MATCH子句中使用花括号{}来指定属性的精确值。
例如,查找演员“Tom Hanks”:
cypher = """
MATCH (tom:Person {name: 'Tom Hanks'})
RETURN tom
"""
result = kg.query(cypher)
print(result)
# 输出Tom Hanks节点的所有属性
同样,我们可以查找电影“Cloud Atlas”:
cypher = """
MATCH (movie:Movie {title: 'Cloud Atlas'})
RETURN movie
"""
result = kg.query(cypher)
print(result)
返回特定属性
我们不必返回整个节点,可以只返回感兴趣的属性。
例如,只返回电影“Cloud Atlas”的上映年份和宣传语:
cypher = """
MATCH (movie:Movie {title: 'Cloud Atlas'})
RETURN movie.released, movie.tagline
"""
result = kg.query(cypher)
print(result)
# 输出: [{'movie.released': 2012, 'movie.tagline': 'Everything is connected'}]
范围查询
当不知道精确值时,可以使用WHERE子句进行条件匹配,例如范围查询。

查找所有90年代(1990年至1999年)上映的电影:
cypher = """
MATCH (m:Movie)
WHERE m.released > 1990 AND m.released < 2000
RETURN m.title
"""
result = kg.query(cypher)
print(result)
# 输出90年代电影标题列表
查询节点间的关系
知识图谱的核心价值在于关系。现在我们来查询更“图”化的模式。
查询演员及其出演的电影
基本的图模式是“人物-出演-电影”。以下查询返回演员姓名和他们出演的电影标题,并限制为前10条结果。
cypher = """
MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie)
RETURN actor.name, movie.title
LIMIT 10
"""
result = kg.query(cypher)
print(result)
查询特定演员的电影
我们可以结合条件查询,查找特定演员(如Tom Hanks)出演的所有电影。
cypher = """
MATCH (tom:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(movie:Movie)
RETURN tom.name, movie.title
"""
result = kg.query(cypher)
print(result)
查询共同出演的演员

图查询的强大之处在于可以轻松扩展模式。例如,查找与Tom Hanks共同出演过电影的所有演员。
cypher = """
MATCH (tom:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(movie:Movie)<-[:ACTED_IN]-(coactor:Person)
RETURN coactor.name AS co_actor_name, movie.title AS movie_title
"""
result = kg.query(cypher)
print(result)
这个查询匹配了这样的模式:Tom Hanks出演了一部电影,同时另一位演员(coactor)也出演了同一部电影。
修改图谱数据
除了查询,Cypher也可以用于修改图谱数据,包括删除和创建。
删除关系


假设我们发现人物“Emil Eifrem”(Neo4j创始人)被错误地标记为出演了电影《黑客帝国》。我们可以删除这条ACTED_IN关系。


首先,确认这条关系存在:
cypher = """
MATCH (emil:Person {name: 'Emil Eifrem'})-[:ACTED_IN]->(movie:Movie)
RETURN emil.name, movie.title
"""
result = kg.query(cypher)
print(result)
然后,使用DELETE子句删除该关系:
cypher = """
MATCH (emil:Person {name: 'Emil Eifrem'})-[r:ACTED_IN]->(movie:Movie)
DELETE r
"""
# 执行删除操作,不返回结果
kg.query(cypher)
再次运行查询确认关系已被删除。
创建节点
使用CREATE子句可以向图谱中添加新节点。
例如,创建一个代表自己的新人物节点:
cypher = """
CREATE (andreas:Person {name: 'Andreas'})
RETURN andreas
"""
result = kg.query(cypher)
print(result)
创建关系
创建关系需要先匹配到要连接的两个节点,然后使用MERGE或CREATE来建立关系。MERGE会检查关系是否已存在,避免重复创建。
在“Andreas”和“Emil Eifrem”之间创建一个WORKS_WITH关系:
cypher = """
MATCH (andreas:Person {name: 'Andreas'})
MATCH (emil:Person {name: 'Emil Eifrem'})
MERGE (andreas)-[:WORKS_WITH]->(emil)
RETURN andreas, emil
"""
result = kg.query(cypher)
print(result)
总结
本节课中我们一起学习了使用Cypher查询语言与知识图谱交互的核心技能。我们从统计节点数量等简单查询开始,逐步掌握了按标签和属性筛选节点、进行范围查询等操作。接着,我们探索了知识图谱的核心——关系查询,学会了如何查找演员的电影以及共同出演者。最后,我们还实践了如何使用DELETE、CREATE和MERGE子句来删除关系和创建新的节点与关系,从而修改图谱数据。

这些操作是构建基于知识图谱的检索增强生成(RAG)应用的基础。在下一节课中,我们将学习如何将图谱中的文本字段转化为向量嵌入,并将其添加到图谱中,以实现向量相似性搜索。
004:为RAG准备文本



在本节课中,我们将学习如何为知识图谱中的文本字段创建向量嵌入,以便在RAG系统中实现基于向量的相似性搜索。我们将从设置环境开始,逐步完成创建向量索引、生成文本嵌入以及执行向量搜索查询的完整流程。


RAG系统通常从使用文本的向量表示开始,将用户的提示与非结构化数据中的相关部分进行匹配。

为了能够以同样的方式在知识图谱中找到相关文本,你需要为图谱中的文本字段创建嵌入。让我们看看如何做到这一点。
环境设置与导入
首先,你需要导入一些必要的包,就像我们在上一个笔记本中所做的那样。我们还将设置Neo4j连接。
你将加载与上一个笔记本相同的环境变量,但现在包括一个名为OPENAI_API_KEY的新变量,我们将用它来调用OpenAI的嵌入模型。
最后,和之前一样,我们将使用Neo4jGraph类来创建与知识图谱的连接,以便我们可以向其发送查询。
以下是设置步骤的代码:
# 导入必要的包
import os
from neo4j import GraphDatabase
from langchain.graphs import Neo4jGraph
# 设置环境变量(示例,实际应从安全位置加载)
os.environ["NEO4J_URI"] = "bolt://localhost:7687"
os.environ["NEO4J_USERNAME"] = "neo4j"
os.environ["NEO4J_PASSWORD"] = "password"
os.environ["OPENAI_API_KEY"] = "your-openai-api-key-here"
# 创建Neo4j图连接
graph = Neo4jGraph(
url=os.environ["NEO4J_URI"],
username=os.environ["NEO4J_USERNAME"],
password=os.environ["NEO4j_PASSWORD"]
)
创建向量索引 🏗️
上一节我们建立了与知识图谱的连接。本节中,我们来看看启用向量搜索的第一步:创建向量索引。
在以下代码的第一行,我们正在创建一个向量索引。我们将其命名为movie_tagline_embeddings,并指定仅当该索引不存在时才创建它。
我们将为标签为Movie的节点(我们称之为m)创建索引,并针对这些节点的tagline属性创建和存储嵌入。
在设置索引时,我们还可以通过index_config对象传递一些选项。有两个重要的参数:向量本身的维度大小,这里设置为1536(这是OpenAI嵌入模型的默认大小);以及相似性函数,OpenAI推荐使用余弦相似度,因此我们在这里指定。
// 创建向量索引
CREATE VECTOR INDEX movie_tagline_embeddings IF NOT EXISTS
FOR (m:Movie) ON (m.tagline)
OPTIONS {indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: 'cosine'
}}
现在,为了验证索引是否已创建,你可以让Neo4j显示所有的向量索引。
这个Cypher查询简单直接,如下所示:
// 显示所有向量索引
SHOW VECTOR INDEXES
查询结果可能如下所示。我们可以看到我们之前指定的名称,索引已准备就绪,并且它是一个向量索引。
为索引填充数据 📥
现在你已经有了一个向量索引,接下来需要用数据填充它。我们将通过一个三步查询来完成。
以下是填充数据的步骤:
- 首先,使用熟悉的
MATCH子句匹配所有标签为Movie且tagline属性不为空的电影节点。 - 接着,为每部电影的
tagline计算一个嵌入向量。这是通过调用genai.vector.encode()函数完成的。在这个函数中,我们需要传入几个参数:要编码的值(即m.tagline)、要使用的嵌入模型(openai),以及包含OpenAI API密钥的配置。 - 最后,执行查询。
这里使用的$openaiApiKey是一种查询参数,它可以在Cypher语句中替代硬编码的值。在执行查询时,我们会传入一个参数字典来提供这个值。
// 为所有电影的tagline生成嵌入并填充索引
MATCH (m:Movie) WHERE m.tagline IS NOT NULL
WITH m, genai.vector.encode(
m.tagline,
"openai",
{
token: $openaiApiKey
}
) AS embedding
CALL db.create.setNodeVectorProperty(m, 'taglineEmbedding', embedding)
RETURN count(m)
运行此查询可能需要几秒钟,因为它会调用OpenAI API为数据集中的每部电影计算向量嵌入。
验证生成的嵌入 ✅
现在,你可以查看标签行以及计算出的文本嵌入,以了解我们运行的查询产生了什么结果。
让我们从结果中提取出标签行本身,看看它是什么。由于我们只处理了一部有标签行的电影,它的标签行是“Welcome to the real world. Super.”。
我们也可以看看嵌入向量的样子。为了简洁,我们只查看前10个值。
最后,为了验证我们得到的嵌入向量大小是否正确(我们期望是1536维),我们将计算其长度。
以下是验证步骤的代码示例:
# 假设 `result` 是上面查询返回的结果
# 获取第一个结果的tagline和嵌入向量
movie_data = result[0]
tagline = movie_data['m.tagline']
embedding = movie_data['embedding']
print(f"Tagline: {tagline}")
print(f"First 10 values of embedding: {embedding[:10]}")
print(f"Vector size (length): {len(embedding)}")
输出应显示向量大小为1536,符合预期。
我们只查看了一部具有标签行和标签行嵌入的电影,但我们之前运行的查询为数据库中的每部电影都计算了嵌入。因此,我们现在可以实际查询数据库,对这些电影进行向量相似性搜索。
执行向量相似性搜索 🔍
上一节我们验证了生成的嵌入向量。本节中,我们来看看如何使用这些嵌入进行搜索。
我们将从指定想要提问的问题开始,并寻找可能匹配该问题的相似电影。例如,问题是:“有哪些关于爱情的电影?” 请记住,我们是在tagline上做的向量索引,因此这将针对那些标签行进行相似性搜索。
以下是执行搜索的步骤:
- 首先,使用之前相同的函数调用
genai.vector.encode来计算问题的嵌入向量。我们需要传入问题文本、使用openai模型,并传递API密钥。这个函数调用的结果我们将赋值给一个叫做questionEmbedding的变量。 - 接着,调用另一个函数
db.index.vector.queryNodes来实际执行向量相似性搜索。我们要查询之前创建的名为movie_tagline_embeddings的索引。这里有一个有趣的参数topK,它表示我们只想要最相似的K个结果,而不是返回所有结果。 - 然后,传入我们刚刚计算出的
questionEmbedding。相似性搜索会说:“在这个所有标签行的索引中,这是我们为问题计算的嵌入向量,请进行相似性计算并返回结果。” - 最后,从结果中,我们希望能够获取找到的节点(将其重命名为
movie)以及相似性分数。我们将返回电影的标题、标签行和分数。
我们传入了一些查询参数:OpenAI API密钥本身、我们提出的问题(这将被计算成嵌入向量),以及topK值(这里设为5,表示我们只想要最相似的5个结果)。
// 执行向量相似性搜索
WITH genai.vector.encode(
$question,
"openai",
{
token: $openaiApiKey
}
) AS questionEmbedding
CALL db.index.vector.queryNodes(
'movie_tagline_embeddings',
$topK,
questionEmbedding
) YIELD node AS movie, score
RETURN movie.title AS title, movie.tagline AS tagline, score
运行此查询后,我们得到了像《Joe Versus the Volcano》这样的电影标题,其标签行是“A story of love, lava, and burning desire.”。浏览所有这些标签行,可以发现它们与“关于爱情的电影”这个问题匹配得相当好。

探索与实验 🧪
既然我们已经构建了一个可以执行向量相似性搜索的查询,并且提出了一个问题,那么现在是探索电影数据集的好时机,可以通过询问关于其他电影可能存在的不同问题来进行。
例如,让我们尝试搜索关于冒险的电影。
我们将保存这个问题,并再次运行查询。
# 更改问题并重新运行查询
new_question = "What movies are about adventure?"
# ... (使用新问题重新执行上面的Cypher查询)
结果可能会包含《Cast Away》、《Ninja Assassin》等电影,听起来像是冒险题材。《Joe Versus the Volcano》也再次出现,它似乎是关于爱情和冒险的,也许值得加入你的观看列表。
这是一个很好的时机,可以暂停视频,尝试自己更改问题来探索电影数据集,询问具有不同特质的电影,看看能得到什么样的结果。
总结 📝
本节课中,我们一起学习了如何为知识图谱中的文本创建嵌入向量并将其添加到图谱中,从而为RAG应用启用向量搜索功能。我们涵盖了从创建向量索引、使用外部API(如OpenAI)生成嵌入,到执行向量相似性搜索查询的完整流程。

到目前为止,在所有示例中,你一直在使用一个现有的数据库。但是,要构建你自己的RAG应用程序,你需要从头开始构建一个知识图谱来表示和存储你的数据。在下一课中,让我们来看看如何做到这一点。
005:从文本构建知识图谱 🏗️


在本节课中,我们将运用之前学到的知识,开始为一些公司必须向美国证券交易委员会提交的财务文件构建知识图谱。我们将学习如何解析、处理这些文档,并将其转化为图数据库中的节点,为后续的检索增强生成系统打下基础。

财务文档简介 📄

上一节我们介绍了知识图谱的基本概念,本节中我们来看看将要处理的具体数据源。

公司每年需要向SEC提交许多财务报告。其中一份重要的表格是Form 10-K,它是公司活动的年度报告。这些表格是公开记录,可以在SEC的EDGAR数据库中访问。

这些文档通常包含大量文本,涵盖行业趋势、技术概述等丰富信息。我们的目标是将这类数据提取到知识图谱中,以便后续与这些财务信息进行交互式问答。
数据预处理与提取 🔧
在将数据导入图谱之前,我们需要对原始文件进行解析和清理。下载的Form 10-K文件实际上是XML格式。
以下是处理这些XML文件的主要步骤:
- XML解析与清理:首先进行基本的正则表达式清理,遍历XML文件以找到我们实际需要的文本块。
- 使用Beautiful Soup:利用这个Python库将部分XML转换为易于操作的Python数据结构。
- 提取关键信息:从中提取关键标识符,例如CIK,这是公司在SEC系统中的中央索引键。
- 定位核心文本:我们主要关注Item 1、Item 1A、Item 7和Item 7A这几个部分,它们是文档中我们将要进行对话的大型文本主体。
完成这些工作后,我们将数据转换为JSON格式,以便于导入并开始创建知识图谱。
构建图谱的实施计划 📋
在回到Notebook之前,让我们先规划一下实施步骤。
我们了解到每个表格都有不同的文本部分,需要将其分割成块。我们将使用LangChain来完成这个任务。一旦所有文本块准备就绪,每个块都将成为图中的一个节点。节点将包含原始文本以及作为属性的元数据。
节点就位后,我们将创建一个向量索引。在该索引中,我们将计算文本嵌入,为每个文本块填充索引。最后,完成所有这些步骤后,我们将能够进行相似性搜索。
代码实现:加载与处理数据 💻
现在,让我们回到Notebook,开始具体的工作。
首先,加载一些有用的Python包,包括LangChain中的一些优秀工具。同时,从环境变量中加载一些全局变量,并设置一些在后续图谱创建过程中要使用的常量。
在本课中,您将处理单个10-K文档。在实践中,您可能有数百或数千个文档。这里采取的步骤需要对所有文档重复执行。
让我们从设置文件名开始,然后加载一个JSON文件。
# 设置要使用的文件名
first_file_name = "path/to/your/10k.json"
# 加载JSON文件
import json
with open(first_file_name, 'r') as f:
data = json.load(f)
我们可以检查一下,确保它看起来像一个正确的字典。然后查看可用的键,这些是Form 10-K中熟悉的字段,如item1、item1A等,以及特殊的标识符、公司名称和原始来源链接。
文本分块处理 ✂️
让我们查看item1的文本内容。由于文本量很大,我们只查看前1500个字符。这正是进行分块处理的目的。我们不会将整个文本存储在单个记录中。
我们将使用LangChain的文本分割器来分解它。这里使用递归字符文本分割器。
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=2000,
chunk_overlap=200
)
item1_text_chunks = text_splitter.split_text(data['item1'])
分割完成后,我们可以查看分块列表的长度,例如可能有254个块。最后,查看其中一个块的内容,确认文本格式正确。
创建分块辅助函数 ⚙️
准备好文本分割器后,我们可以设置一个辅助函数。该函数将遍历文件中的每个部分,创建文本块,然后将它们全部转换为可用于将数据加载到图谱本身的对象。
以下是该函数的核心逻辑概述:
- 初始化一个列表来累积创建的所有块。
- 加载JSON文件。
- 循环遍历每个部分名称。
- 对于每个部分,提取文本并使用文本分割器进行分块。
- 遍历所有文本块,为每个块创建数据记录,包含文本本身、当前处理的条目、块序列ID以及元数据。
def split_form_10k_data_from_file(file_path):
all_chunks = []
# ... 加载文件、循环部分、分块、创建记录的代码 ...
return all_chunks
调用这个辅助函数处理文件,输出结果将是一个记录列表,每个记录代表一个块及其元数据。
将数据合并到知识图谱中 🗃️
我们将使用Cypher查询将块合并到图谱中。这是一个MERGE语句,它首先尝试匹配,如果匹配失败则创建新节点。
MERGE (c:Chunk {chunkId: $chunkParam.chunkId})
ON CREATE SET
c.text = $chunkParam.text,
c.item = $chunkParam.item,
c.formId = $chunkParam.formId,
...
为了运行此查询,我们需要使用之前保存的数据库位置、用户名、密码等参数来初始化Neo4j集成。然后调用查询方法,传入查询字符串和参数字典。
确保数据唯一性与创建索引 🔑
在调用辅助函数创建知识图谱之前,我们需要额外一步以确保不重复数据。我们将创建一个唯一性约束,它同时也是一个索引,用于确保所有具有相同标签的节点中,某个属性是唯一的。
CREATE CONSTRAINT unique_chunk IF NOT EXISTS
FOR (c:Chunk)
REQUIRE c.chunkId IS UNIQUE
运行此查询后,我们可以检查索引,确认新的唯一性约束已创建。
现在,我们准备循环遍历所有块,对每个块运行MERGE查询,并传入相应的参数。完成后,可以运行一个简单的MATCH查询来统计节点数量,验证数据已成功导入。
创建向量索引并生成嵌入 🧠
接下来,我们将创建另一个索引,这次是向量索引,用于为文本块创建文本嵌入。
CREATE VECTOR INDEX `form-10k-chunks` IF NOT EXISTS
FOR (c:Chunk) ON c.textEmbedding
OPTIONS {indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: 'cosine'
}}
我们可以检查该索引是否已创建并处于在线状态。然后,使用一个查询来匹配所有块,调用OpenAI获取每个块的嵌入,最后将嵌入设置到每个节点的属性上。这个过程可能需要一些时间。
至此,图谱中包含了带有文本嵌入的文本块节点,但还没有任何关系。
实现向量搜索与RAG系统 🔍
现在我们已经有了一个包含带文本嵌入的文本块的知识图谱,可以创建一个辅助函数来使用Neo4j执行向量搜索。这与上一课的做法完全相同。
由于我们处理的表格来自一家名为NetApp的公司,可以尝试使用新的向量搜索辅助函数来询问关于NetApp的信息。
然而,向量搜索只返回相似的文本片段。如果我们想创建一个能提供实际问题答案的聊天机器人,可以使用LangChain构建一个RAG系统。
最简单的方法是使用Neo4j Vector接口,这使得Neo4j在底层看起来像一个向量存储。配置指定了几个重要事项,使用了我们在本课开头设置的全局变量。
我们将向量存储转换为检索器,然后使用LangChain框架中的RetrievalQAWithSourcesChain,这是一个专门用于问答交互的链。
from langchain.chains import RetrievalQAWithSourcesChain
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model_name="gpt-3.5-turbo")
chain = RetrievalQAWithSourcesChain.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vector_store.as_retriever()
)
我们还可以创建一个漂亮的辅助函数来格式化问题和答案的显示。
进行问答测试与提示工程 💬
完成所有工作后,我们终于可以进行有趣的问答测试了。
既然我们知道图谱中有NetApp的信息,可以问:“NetApp的主要业务是什么?” 系统会返回一个实际的答案,而不仅仅是可能包含答案的原始文本。这正是我们使用LLM的目的。
我们还可以尝试其他问题,例如询问NetApp的总部所在地。
现在有了LLM的参与,我们可以提出各种有趣的问题,甚至可以给LLM一些指令。例如,我们可以要求“用一句话告诉我NetApp的情况”。
为了展示一个有趣的现象,我们可以询问一个听起来与NetApp相似的不同公司,比如“告诉我关于Apple的情况”。LLM可能会产生幻觉,给出一个与NetApp描述相似的答案。
我们可以通过提示工程来尝试修复这个问题,例如在提示中添加:“如果你不确定答案,请说‘我不知道’。” 这样通常能得到更诚实、更好的回答。
总结 📝
在本节课中,我们一起学习了如何从财务文档(Form 10-K)构建知识图谱。我们涵盖了从数据加载、文本分块、创建图谱节点和唯一性约束,到建立向量索引并生成文本嵌入的完整流程。最后,我们利用Neo4j作为向量存储,结合LangChain构建了一个简单的RAG系统,实现了基于文档的智能问答。

然而,本节课我们主要将Neo4j用作向量存储,并未充分发挥其作为知识图谱的优势。在下一节课中,我们将为节点添加关系,为聊天应用注入更强大的图计算能力。
006:向知识图谱添加关系



概述
在本节课中,我们将学习如何为已构建的知识图谱节点添加关系。这些关系将保留原始文档的结构,从而增强每个文本块(chunk)的上下文信息。我们将连接各个文本块,并将它们链接到代表整个文档的“表单”节点。
导入包与设置变量
首先,我们需要导入必要的Python包并设置一些将在本教程中使用的全局变量。
# 导入必要的包
import some_packages
# 设置全局变量
global_variable = "value"
对于所有要发送到Neo4j数据库的查询,我们将再次使用LangChain的集成工具Neo4jGraph。
创建表单节点
你已经有了代表文本块的节点。现在,你需要创建一个新的节点来代表整个10-K表单本身。
这个10-K表单节点将有一个Form标签和以下属性:
form_id:表单的唯一标识符。source:指向SEC原始10-K文档的链接。cik:SEC的中央索引密钥。cusip:CUSIP代码。

以下是创建此节点的参数化Cypher查询:
MERGE (f:Form {form_id: $form_info.form_id})
SET f.source = $form_info.source,
f.cik = $form_info.cik,
f.cusip = $form_info.cusip
我们将运行此查询并检查是否成功创建了一个表单节点。
连接节点:创建分块链表
我们的目标是通过添加关系来改善每个文本块的上下文。我们将把文本块彼此连接,并连接到新创建的表单节点,以反映文档的原始结构。
首先,为每个文档部分(section)创建一个节点链表。
以下是查找属于同一表单和同一部分的文本块的查询:
MATCH (c:Chunk)
WHERE c.form_id = $form_id AND c.f10k_item = $f10k_item
RETURN c
ORDER BY c.chunk_seq_id ASC
为了确保我们按正确的顺序获取所有文本块,我们按chunk_seq_id升序排列。查询结果将显示来自同一部分且序列号递增的文本块。
现在,我们将这些文本块收集到一个列表中,并使用apoc.nodes.link过程创建链表关系。
CALL apoc.nodes.link($section_chunk_list, 'NEXT', {avoidDuplicates: true})
此过程将接收一个节点列表和我们想要的关系类型(此处为NEXT),并在每对相邻节点之间创建NEXT关系。avoidDuplicates参数确保我们不会创建重复的关系。
我们可以通过一个Python循环,为所有不同的部分名称执行此操作,从而为每个部分创建链表。
连接文本块与表单
接下来,将文本块连接到它们所属的表单。
以下是创建PART_OF关系的查询:
MATCH (c:Chunk), (f:Form)
WHERE c.form_id = f.form_id
MERGE (c)-[:PART_OF]->(f)
此查询匹配具有相同form_id的文本块和表单节点,并在它们之间创建PART_OF关系。
连接表单与各部分起始块
为了便于在知识图谱中导航,我们还可以添加一种关系,将表单直接连接到每个部分的第一个文本块。
以下是创建SECTION关系的查询:
MATCH (c:Chunk), (f:Form)
WHERE c.form_id = f.form_id AND c.chunk_seq_id = 0
MERGE (f)-[r:SECTION]->(c)
SET r.f10k_item = c.f10k_item
此查询匹配表单和每个部分序列号为0(即第一个)的文本块,创建SECTION关系,并将部分名称作为关系属性存储。
探索图谱:示例查询
现在图谱已构建完成,我们可以尝试一些Cypher查询来探索它。
例如,要获取某个部分的第一个文本块:
MATCH (f:Form)-[r:SECTION]->(c:Chunk)
WHERE f.form_id = $target_form_id AND r.f10k_item = $target_section
RETURN c.chunk_id, c.text
有了第一个文本块的信息,你可以通过跟随NEXT关系来获取该部分的下一个文本块。
使用可变长度路径查找文本块窗口
为了在检索时获得更丰富的上下文,我们可能希望找到一个以某个文本块为中心的“窗口”,即它前后相邻的几个文本块。
我们可以使用可变长度路径来实现这一点。以下查询查找以指定chunk_id为中心,前后各最多一个文本块的窗口:
MATCH path = (c0:Chunk)-[:NEXT*0..1]->(c1:Chunk)-[:NEXT*0..1]->(c2:Chunk)
WHERE c1.chunk_id = $center_chunk_id
RETURN path
ORDER BY length(path) DESC
LIMIT 1
[:NEXT*0..1]表示匹配0到1个NEXT关系。这允许我们处理链表开头或结尾的边界情况。ORDER BY length(path) DESC LIMIT 1确保我们获得匹配的最长路径,即完整的窗口。
在RAG中扩展上下文
知识图谱的核心优势在于,一旦在图中找到一个节点(例如通过向量相似性搜索),你就可以轻松获取其相连的信息。
我们可以自定义向量检索查询,在返回结果前,先通过Cypher查询扩展其上下文。以下是一个基础模板:
// 1. 执行向量搜索(由Neo4jVector内部处理)
// 假设返回变量为 $node 和 $score
// 2. 使用Cypher扩展上下文(例如,查找相邻文本块)
MATCH window_path = (before:Chunk)-[:NEXT*0..1]->($node)-[:NEXT*0..1]->(after:Chunk)
WITH $node, $score, window_path
// 3. 从窗口中的所有块收集文本
WITH $node, $score, [n IN nodes(window_path) | n.text] AS context_texts
// 4. 拼接文本并返回规定格式
RETURN reduce(text = "", t IN context_texts | text + "\n---\n" + t) AS text,
$score AS score,
{chunk_id: $node.chunk_id, source: $node.source} AS metadata
在LangChain中创建检索问答链时,我们可以传入这个自定义的retrieval_query。
# 创建带有窗口上下文的检索器
vector_store = Neo4jVector.from_existing_index(
embedding=embeddings,
index_name="chunk_index",
retrieval_query=custom_cypher_query # 传入我们的自定义查询
)
qa_chain = RetrievalQA.from_chain_type(llm=llm, retriever=vector_store.as_retriever())
通过比较使用默认检索(仅返回单个块)和使用扩展窗口检索的答案,可以看到后者能提供更连贯、包含更多相关细节的上下文,从而帮助大语言模型生成更准确的回答。

总结
本节课中,我们一起学习了如何为知识图谱添加关系结构。我们首先创建了代表整个文档的表单节点,然后通过NEXT关系将同一部分的文本块连接成链表,并使用PART_OF和SECTION关系将文本块与表单关联起来。接着,我们探索了如何使用Cypher查询遍历这些关系,特别是利用可变长度路径来查找文本块窗口。最后,我们将这种能力应用于RAG流程,通过自定义检索查询来扩展向量搜索返回的上下文,从而显著提升问答系统的回答质量。在下一课中,我们将引入另一份包含投资者信息的SEC表单,进一步扩展知识图谱的上下文。
007:扩展知识图谱 📈



在本节课中,我们将引入第二个SEC数据集,以扩展原始申报表格的上下文。这个新数据集提供了关于机构投资经理及其在公司中持有权益的信息。通过将这些数据添加到图谱中,你将能够对合并后的数据集提出更复杂的问题,从而帮助你理解市场动态。


导入与准备
首先,我们照常导入必要的库,并设置一些全局变量。当然,我们还需要一个Neo4j图实例来连接数据库。
SEC的13F表格由机构投资管理公司提交,用于报告他们投资了哪些上市公司。这些表格以XML文件形式提供。在数据准备阶段,我们从XML中提取特定字段,并将其作为一行添加到CSV文件中。
以下是读取CSV文件的代码:
import csv
with open('form13_data.csv', 'r') as f:
csv_reader = csv.DictReader(f)
form13s = list(csv_reader)
让我们快速查看前几行数据:
print(form13s[:5])
可以看到,这些管理公司都投资了同一家公司(例如NetApp)。每一行都包含关于管理公司本身(如经理姓名、地址、中央索引密钥CIK)以及投资详情(如报告日历、股份数量、价值)的信息。此外,还有关于被投资公司的元数据,如CUSIP代码。
检查数据行数:
print(len(form13s))
共有561行数据,这意味着我们将创建561个相关节点。
创建公司节点
从每一行数据中,我们将创建两个节点:一个用于管理公司,另一个用于它们投资的公司。
公司节点将带有 Company 标签,并以CUSIP标识符确保唯一性。它们还将获得公司名称和完整的CIK属性。
以下是创建公司节点的Cypher查询:
MERGE (c:Company {cusip: $row.cusip})
ON CREATE SET c.name = $row.companyName, c.cik = $row.cik
进行快速完整性检查,确认NetApp公司已被创建:
MATCH (c:Company {name: 'NetApp Inc.'}) RETURN c
由于知识图谱中已存在NetApp的10-K表格,我们可以通过匹配CUSIP标识符,将新创建的公司节点与相关的10-K表格关联起来:
MATCH (c:Company), (f:Form)
WHERE c.cusip = f.cusip
RETURN c, f
匹配成功后,我们可以将表格中的名称变体提取出来,以丰富公司节点的信息:
MATCH (c:Company), (f:Form)
WHERE c.cusip = f.cusip
SET c.names = f.names
最后,在上述配对的基础上,我们创建一个关系,以表明“该公司提交了此表格”:
MATCH (c:Company), (f:Form)
WHERE c.cusip = f.cusip
MERGE (c)-[:FILED]->(f)
创建管理公司节点
管理公司节点将带有 Manager 标签。我们将基于经理的CIK编号确保其唯一性,并设置经理姓名和地址属性。
以下是创建管理公司节点的代码示例:
manager_params = {
'cik': row['manager_cik'],
'name': row['manager_name'],
'address': row['manager_address']
}
query = """
MERGE (m:Manager {cik: $manager.cik})
ON CREATE SET m.name = $manager.name, m.address = $manager.address
"""
进行完整性检查:
MATCH (m:Manager {name: 'Royal Bank of Canada'}) RETURN m
由于有多达561家管理公司,为了避免意外创建重复节点,我们创建一个唯一性约束。同时,我们还可以在经理节点上创建全文索引,以便进行基于相似字符串的关键词搜索。
创建唯一性约束和全文索引:
CREATE CONSTRAINT FOR (m:Manager) REQUIRE m.cik IS UNIQUE;
CREATE FULLTEXT INDEX managerNameIndex FOR (m:Manager) ON EACH [m.name];
现在,我们可以使用Python循环遍历CSV文件中的所有行,为所有管理公司创建节点。
建立投资关系
现在,我们可以使用13F CSV文件中的信息,找到管理公司节点和公司节点的配对。
以下查询用于匹配特定的经理和公司:
MATCH (m:Manager {cik: $investment.manager_cik})
MATCH (c:Company {cusip: $investment.cusip})
RETURN m, c
匹配成功后,我们可以在这些节点之间建立关系。我们将使用 MERGE 创建一个 OWNS_STOCK_IN 关系。为了防止同一经理对同一公司的多次投资记录被重复创建,我们将使用报告日历季度作为该关系的唯一属性。
创建投资关系的完整Cypher查询如下:
MATCH (m:Manager {cik: $os_param.manager_cik})
MATCH (c:Company {cusip: $os_param.cusip})
MERGE (m)-[r:OWNS_STOCK_IN {report_calendar_or_quarter: $os_param.report_calendar_or_quarter}]->(c)
ON CREATE SET r.shares = $os_param.shares, r.value = $os_param.value
RETURN m.name, r.shares, r.value, c.name
运行此查询后,我们可以通过一个快速查询来验证关系是否已正确创建。
最后,我们循环遍历CSV文件的所有行,为每一行数据创建 OWNS_STOCK_IN 关系。
探索知识图谱
我们的知识图谱已经发生了很大变化。我们最初只有10-K表格的文本块,然后连接了这些块并创建了它们所属的表格节点,现在又创建了公司和管理者节点,并将它们全部连接起来。
让我们查看知识图谱的模式,以了解我们所有工作的成果。我们可以刷新图谱模式并打印出来。
节点类型包括:
Chunk:文本块及其属性。Form:表格节点及其属性。Manager:我们创建的管理者节点。Company:我们创建的公司节点。
关系类型包括:
(:Chunk)-[:PART_OF]->(:Form)(:Chunk)-[:NEXT]->(:Chunk)(块之间的链表)(:Form)-[:SECTION]->(:Chunk)(用于找到链表的起点)(:Manager)-[:OWNS_STOCK_IN]->(:Company)(:Company)-[:FILED]->(:Form)
图谱非常适合探索。让我们从一个随机块开始,逐步构建路径,看看能发现什么。
首先,找到一个随机块:
MATCH (c:Chunk)
RETURN c.id AS chunk_id
LIMIT 1
然后,从该块出发,通过 PART_OF 关系找到其所属的表格:
MATCH (c:Chunk {id: $chunk_id})-[:PART_OF]->(f:Form)
RETURN f.source
再扩展一步,找到提交该表格的公司:
MATCH (c:Chunk {id: $chunk_id})-[:PART_OF]->(f:Form)<-[:FILED]-(co:Company)
RETURN co.name
继续扩展,找到投资于该公司的管理者:
MATCH (c:Chunk {id: $chunk_id})-[:PART_OF]->(f:Form)<-[:FILED]-(co:Company)<-[:OWNS_STOCK_IN]-(m:Manager)
RETURN co.name, count(m) AS numberOfInvestors
这将返回公司名称及其投资者数量,验证了我们的图谱构建工作。
利用图谱扩展上下文
你刚刚创建的从文本块到投资者的模式是很有用的信息。你可以利用这些信息来扩展提供给大语言模型(LLM)的上下文。
例如,你可以找到一个公司的投资者,然后创建包含每个投资详情的句子,从而进一步扩展提供给LLM的信息。
我们将使用之前的匹配模式,但这次不是仅仅返回数据,而是将部分数据转换成字符串句子。
以下是将投资信息转换为句子的示例:
sentence = f"{manager_name} owns {shares} shares of {company_name} at a value of ${value:,.0f}."
让我们看看生成的第一句话示例:
KPC Group Inc. owns 37500 shares of NetApp Inc. at a value of $2,814,375.
在RAG工作流中应用
现在,让我们将上述功能应用到RAG工作流中。我们将设置两个不同的LangChain链:一个仅进行常规向量检索,另一个则包含检索查询以获取额外信息。
第一个链(普通链)仅使用向量检索:
plain_chain = setup_chain(vector_store, retriever_type="vector")
我们将定义一个Cypher查询来扩展向量搜索的结果。这个模式现在应该很熟悉了:从一个特定的节点(由向量搜索提供)开始,找到其所属的表格、提交表格的公司,以及投资于该公司的管理者。
投资检索查询示例:
MATCH (chunk:Chunk)-[:PART_OF]->(form:Form)<-[:FILED]-(company:Company)<-[r:OWNS_STOCK_IN]-(manager:Manager)
WHERE chunk.id = $chunk_id
RETURN chunk.score, manager.name, r.shares, r.value, company.name
ORDER BY r.shares DESC
LIMIT 10
然后,我们使用这个扩展查询创建一个新的向量存储和检索器,进而构建一个我们称之为“投资链”的新链。
现在,我们可以尝试几个不同的问题来测试这两个链。
问题1: “用一句话告诉我关于NetApp的信息。”
- 普通链的回答:侧重于公司的业务描述(例如,“NetApp是一家全球领先的云公司...”)。
- 投资链的回答:与普通链类似,因为问题没有明确询问投资者信息,LLM忽略了额外的投资上下文。
问题2: “用一句话告诉我关于NetApp投资者的信息。”
- 普通链的回答:可能尝试从文本块中推断,给出一个模糊的答案(例如,“投资者是多元化的客户群”)。
- 投资链的回答:提供了更具体、基于数据的答案,列出了实际的投资者名称,如KPC Group、Cambridge Investments等。
这为我们提供了一个很好的起点,可以开始进行一些调整。你可以更改从投资信息创建的句子格式,观察这对结果的影响;也可以改变提出的问题,看看不同的提示如何影响LLM的输出。让LLM理解你提供的信息、这些问题如何被解答以及如何被解答,仍然需要一些技巧。我们将在第七课中进一步探索这些内容。
总结

在本节课中,我们一起学习了如何通过引入第二个数据集(SEC 13F表格)来扩展知识图谱。我们创建了代表管理公司和被投资公司的新节点,并在它们之间建立了清晰的投资关系。随后,我们探索了如何利用图谱中这种丰富的连接关系,从已知信息点出发,发现新的关联(如从文本块找到其投资者)。最后,我们将这种图谱查询能力整合到RAG工作流中,通过提供额外的、基于图谱检索的上下文,使大语言模型能够回答更复杂、更具体的问题(例如关于公司投资者的问题),从而超越了单纯向量搜索的能力范围。
008:与知识图谱对话 🗣️



在本节课中,我们将学习如何与已构建的知识图谱进行交互。我们将通过直接查询和利用大语言模型(LLM)生成查询两种方式,探索图谱中的数据,并回答各种问题。
概述
上一节我们完成了知识图谱的构建。本节中,我们将进入有趣的环节:与SEC文档知识图谱进行对话。我们将首先使用Cypher查询语言直接探索图谱,然后利用LangChain和LLM创建一个问答聊天系统。
回顾知识图谱构建模式
让我们先退一步,思考一下我们创建知识图谱的过程。我们遵循了一个清晰的模式:
- 提取:从现有数据源中发现并提取出有价值的信息片段,将其创建为独立的节点。
- 增强:通过某种方式(如添加向量嵌入)来增强这些数据。
- 扩展:将新创建的数据连接到已有的图谱中。
这个“提取-增强-扩展”的模式贯穿了整个课程。从最初的10-K表格数据开始,我们将其分块、创建节点、添加嵌入,最后连接节点。在后续课程中,无论是从文本节点创建分块,还是从分块创建表单,或是从13F CSV文件创建公司和经理人节点,我们都重复了这一模式。
你可以根据想要回答的问题类型,无限地继续这一过程。例如,可以链接提及的公司、提取人物、地点和主题,甚至将用户反馈纳入图谱以持续改进体验。
探索增强后的图谱架构
我们构建的图谱数据得到了进一步扩展。公司和经理人节点都包含地址字符串。我们通过地理编码将这些地址提取为独立的节点,并为它们添加了地理空间索引,从而支持基于距离的查询(例如,“我附近有哪些公司?”)。
以下是本节将要使用的图谱架构示意图:

经理人持有公司的股份,公司提交了已被分块的表格。现在,经理人和公司都连接到地址节点。借助这些地址信息,我们可以提出更有趣的问题。
使用Cypher直接查询图谱
和往常一样,我们首先导入必要的库并创建Neo4j图实例。


现在,我们准备使用Cypher进行探索。以下是一些查询示例:
查找随机经理人及其地址
MATCH (m:Manager)-[:LOCATED_AT]->(a:Address)
RETURN m.name, a
LIMIT 1
此查询返回一个经理人及其地址。注意地址节点中的 location 属性,它存储了经纬度点,是实现地理空间搜索的关键。
通过全文搜索查找特定经理人
CALL db.index.fulltext.queryNodes(‘managerNames’, ‘Royal Bank’)
YIELD node, score
RETURN node.name, score
此查询使用全文索引查找名称包含“Royal Bank”的经理人,并返回匹配节点和分数。
查找哪个州拥有最多的投资公司
MATCH (m:Manager)-[:LOCATED_AT]->(a:Address)
RETURN a.state AS State, count(a.state) AS NumberOfManagers
ORDER BY NumberOfManagers DESC
LIMIT 10
此查询按州聚合,统计经理人数量。
深入探索:加利福尼亚州哪些城市投资公司最多
MATCH (m:Manager)-[:LOCATED_AT]->(a:Address)
WHERE a.state = ‘California’
RETURN a.city AS City, count(a.city) AS NumberOfManagers
ORDER BY NumberOfManagers DESC
LIMIT 10
此查询将范围限定在加州,并按城市进行统计。
基于地理空间索引的邻近搜索
这是最有趣的部分之一。我们可以查找位于某个地点附近(而不仅仅是位于该地点)的公司或经理人。
以下是查找圣克拉拉市附近公司的查询:
MATCH (sc:Address)
WHERE sc.city = ‘Santa Clara’
MATCH (c:Company)-[:LOCATED_AT]->(companyAddress:Address)
WHERE point.distance(sc.location, companyAddress.location) < 10000
RETURN c.name, companyAddress.address
关键部分是 WHERE point.distance(sc.location, companyAddress.location) < 10000。point.distance 是Cypher内置函数,用于计算两点之间的距离(单位为米)。此查询返回距离圣克拉拉市中心10公里范围内的公司。
你可以尝试修改距离、公司名称和城市,观察不同的结果。
利用LLM生成Cypher查询
手动编写Cypher查询可能有一定学习成本。幸运的是,我们可以利用大语言模型(如GPT-3.5)来生成Cypher语句。这里我们使用一种称为小样本学习的技术。
其核心思想是:在提示词中提供一些示例,教导LLM如何为特定任务生成Cypher查询,然后让它执行新任务。
以下是提示词模板的核心结构:
- 任务说明:明确要求生成用于查询图数据库的Cypher语句。
- 指令限制:要求LLM仅使用提供的图谱架构中的关系和属性,不要自行发挥。
- 提供图谱架构:将我们图谱的节点、关系和属性描述传递给LLM。
- 输出格式要求:要求LLM只输出Cypher语句,不要包含解释或道歉。
- 提供示例:这是小样本学习的关键。我们提供一个或多个“问题-Cypher查询”对作为示例。
- 用户问题:最后,附上用户实际提出的问题。
例如,我们可以提供一个示例:
- 问题:
# 旧金山有哪些投资公司? - Cypher查询:
MATCH (m:Manager)-[:LOCATED_AT]->(a:Address) WHERE a.city = ‘San Francisco’ RETURN m.name
当用户提问“圣克拉拉有哪些公司?”时,LLM会学习示例中的模式,将城市名替换,生成相应的查询。

我们使用LangChain来构建这个工作流。创建一个 GraphCypherQAChain,它结合了LLM(ChatOpenAI)和我们已有的知识图谱,并指定使用上述的Cypher生成提示模板。
测试LLM生成查询
- 询问示例中的问题:“旧金山有哪些投资公司?”。LLM成功生成了正确的Cypher。
- 询问新问题:“门洛帕克有哪些投资公司?”。LLM将城市名替换,生成了正确查询。
- 询问未直接教过的问题:“圣克拉拉有哪些公司?”。LLM根据图谱架构,成功推断出需要匹配
Company节点而非Manager节点,生成了正确查询。
教导LLM进行更复杂的查询
最初,LLM不知道如何进行距离查询。我们只需在提示词的示例部分增加一个“邻近搜索”的示例即可。
例如,增加示例:
- 问题:
# 圣克拉拉附近有哪些投资公司? - Cypher查询:
MATCH (sc:Address) WHERE sc.city = ‘Santa Clara’ MATCH (m:Manager)-[:LOCATED_AT]->(ma:Address) WHERE point.distance(sc.location, ma.location) < 10000 RETURN m.name
更新提示词和链之后,LLM就能学会生成包含 point.distance 函数的复杂查询了。

连接回原始文档数据
我们还可以教导LLM回答关于公司业务的问题。这需要查询连接到公司的原始SEC文件分块。
例如,增加示例:
- 问题:
# Palo Alto Networks是做什么的? - Cypher查询:
这个查询通过全文搜索找到公司,然后沿着关系找到其提交的表格(Form),再找到该表格中“Item 1”(业务描述)部分的第一个文本分块,并返回其文本内容。LLM可以利用这个文本来生成最终答案。CALL db.index.fulltext.queryNodes(‘companyNames’, ‘Palo Alto Networks’) YIELD node AS company MATCH (company)-[:FILED]->(f:Form)-[:SECTION {item: ‘1’}]->(chunk:Chunk) RETURN chunk.text LIMIT 1
总结
本节课中,我们一起学习了如何与构建好的知识图谱进行交互。


- 我们首先回顾了“提取-增强-扩展”的图谱构建模式。
- 接着,我们使用Cypher查询语言直接探索图谱,执行了包括简单匹配、全文搜索、聚合统计以及利用地理空间索引进行邻近搜索在内的多种查询。
- 然后,我们引入了一种更强大的方法:利用大语言模型和小样本学习技术来自动生成Cypher查询。我们构建了提示词模板,通过提供少量“问题-查询”示例,成功教会了LLM为各种问题生成正确的Cypher语句,甚至包括它从未见过的复杂查询类型(如距离查询)。
- 最后,我们演示了如何将查询结果(如SEC文档的业务描述文本)反馈给LLM,以生成自然语言答案,实现了从图谱到最终回答的完整流程。

你可以暂停视频,尝试修改提示词中的示例、提出不同的问题,并观察LLM的表现。当它无法生成正确查询时,只需在示例中添加一个相关的查询案例,更新链,即可让它学会新的查询模式。这种结合知识图谱结构化存储和LLM自然语言理解能力的方法,为构建智能问答系统提供了强大而灵活的框架。
009:总结 🎉

在本节课中,我们将对《知识图谱用于RAG》课程进行总结,回顾所学内容,并展望未来的学习方向。
恭喜你完成了本课程的学习。
我希望你享受了构建一个由知识图谱驱动的RAG系统,并探索美国证券交易委员会财务文件细节的过程。
😊,当你能够直接与这类公共记录对话时,分析它们无疑会变得更加有趣。
你在本课程中实践的SEC示例,代表了企业正在利用知识图谱和生成式AI构建的各类应用程序。我希望本课程能激励你构建自己的知识图谱。
如果你想继续学习,Neo4j官网上提供了大量资源。你可以在那里注册一个免费的云端托管Neo4j账户,并了解其他能帮助你构建自己知识图谱的工具。
感谢你坚持学习到课程的最后一节,我迫不及待想看到你构建的作品。
本节课中,我们一起学习了课程的整体回顾与总结。我们祝贺了学习的完成,肯定了构建知识图谱RAG系统的实践价值,并指出了SEC案例的代表性。最后,我们提供了进一步学习的资源路径,并表达了对学习者未来成果的期待。

浙公网安备 33010602011771号