近来 NebulaGraph 社区在 LLM + Graph 和 Graph RAG 领域进行了深入的探索和分享。在 LlamaIndex 和 LangChain 中,NebulaGraph 引入了一系列知识图谱和图存储工具,支持编排、图谱与大模型间的交互。之前,NebulaGraph 布道师古思为作为这项工作的主要贡献者已向大家详细介绍了如何构建图谱、Text2Cypher、GraphRAG、GraphIndex 等方法,并展示了相关示例与效果。
最近,ArisGlobal 公司的工程师 Wenqi Glantz 对基于 NebulaGraph 和 LlamaIndex 的所有 Graph + LLM、RAG 方法进行了全面的实验、评估、综述、总结和分析,并给出了深刻的结论。
此文在 Twitter 和 LinkedIn 上获得了广泛认可。在得到 Wenqi 的同意后,我们为大家提供了中文翻译,期望为大家在 Graph + LLM 方法的探索和实践中提供更多的洞见和参考。
由于 Wenqi Glantz 全家都是 Philadelphia Phillies(费城费城人棒球队,下文仅做英文展示)的铁杆粉丝,因此,在本文中她将会使用知识图谱,确切点是图数据库 NebulaGraph 来查询这只位于费城的 Major League Baseball(大联盟棒球队,下文仅做英文展示)Philadelphia Phillies 的信息。
这里,我们将使用维基百科·Philadelphia Phillies 页面作为其中一个数据源。此外,因为最近费城球迷为我们喜爱的球员 Trea Turner 发起了 standing ovation(起立致敬是指演奏、比赛等项目结束时,听众或观众起立鼓掌之行为)事件,我们还将使用一段评论这个大事件的 YouTube 视频作为另一个数据源。
现在,我们的架构图是这样的:
(作者提供的架构图)
如果你熟悉知识图谱和图数据库 NebulaGraph,可以直接跳到“RAG 具体实现”章节。如果你不熟悉 NebulaGraph,请继续往下读。
知识图谱是一种使用图结构的数据模型或拓扑来集成数据的知识库。它是一种表示现实世界实体及其相互关系的方式。知识图谱常用来实现搜索引擎、推荐系统、社交网络等业务场景。
知识图谱一般有两个主要组成部分:
compete in
(参赛)可能连接 “Philadelphia Phillies” 的节点和 “Major League Baseball” 的节点。三元组是知识图谱的基本数据单元,由三个部分组成:
在下面的三元组示例中,“Philadelphia Phillies”是主体,“compete in”是谓词,“Major League Baseball”是客体。
(Philadelphia Phillies)--[compete in]->(Major League Baseball)
而图数据库通过存储三元组来高效地存储和查询复杂的图数据。
Cypher 是由图数据库支持的一种声明性图查询语言。通过 Cypher,我们告诉知识图谱我们想要什么数据,而不是如何得到结果数据。这使得 Cypher 查询更易读、更好维护。此外,Cypher 易上手使用,且能够表达复杂的图查询。
以下,是一个 Cypher 的简单的查询示例:
%%ngql MATCH (p:`entity`)-[e:relationship]->(m:`entity`) WHERE p.`entity`.`name` == 'Philadelphia Phillies' RETURN p, e, m;
该查询语句将找到与棒球队“Philadelphia Phillies”相关的所有实体。
NebulaGraph 是市面上最好的图数据库之一。它是开源、分布式的,并且能处理包含万亿条边和顶点的大规模图,而延迟仅为毫秒级。很多大公司在广泛地使用它,进行各种应用开发,包括社交媒体、推荐系统、欺诈检测等。
要实现 Philadelphia Phillies 的 RAG,我们需要在本地安装 NebulaGraph。借助 Docker Desktop 安装 NebulaGraph 是最便捷的方式之一。详细的安装说明可以在 NebulaGraph 的文档中找到。
如果你不了解 NebulaGraph,强烈建议去熟悉下文档。
NebulaGraph 的首席布道师古思为,以及 LlamaIndex 团队精心撰写了一份关于知识图谱 RAG 开发的综合指南。从这本指南中我学到了很多知识,我建议你在读完本文之后也去读下这个指南。
现在,利用我们从指南中学到的知识,开始逐步地介绍使用 LlamaIndex、NebulaGraph 和 GPT-3.5 构建 Philadelphia Phillies RAG。
源码可参考我的 GitHub 仓库:https://github.com/wenqiglantz/llamaindex_nebulagraph_phillies,当中包括了项目完整的 JupyterNote。
除了 LlamaIndex,我们还要安装一些库:
ipython-ngql
:一个 Python 包,帮你更好地从 Jupyter Notebook 或 iPython 连接到 NebulaGraph;nebula3-python
:连接和管理 NebulaGraph 的 Python 客户端;pyvis
:用最少的 Python 代码快速生成可视化网图的工具库;networkx
:研究图和网络的 Python 库;youtube_transcript_api
:可获取 YouTube 视频的转录/字幕的 Python API。%pip install llama_index==0.8.33 ipython-ngql nebula3-python pyvis networkx youtube_transcript_api
我们还要设置 OpenAI API 密钥并配置应用程序的日志记录:
import os import logging import sys os.environ["OPENAI_API_KEY"] = "sk-####################" logging.basicConfig(stream=sys.stdout, level=logging.INFO)
假设你已经在本地安装了 NebulaGraph,现在我们可以从 JupyterNote 连接它(注意:不要尝试从 Google Colab 连接到本地的 NebulaGraph,由于某些原因,它无法工作)。
按照下面的步骤和代码片段来操作下:
phillies_rag
的图空间os.environ["GRAPHD_HOST"] = "127.0.0.1" os.environ["NEBULA_USER"] = "root" os.environ["NEBULA_PASSWORD"] = "nebula" os.environ["NEBULA_ADDRESS"] = "127.0.0.1:9669" %reload_ext ngql connection_string = f"--address {os.environ['GRAPHD_HOST']} --port 9669 --user root --password {os.environ['NEBULA_PASSWORD']}" %ngql {connection_string} %ngql CREATE SPACE IF NOT EXISTS phillies_rag(vid_type=FIXED_STRING(256), partition_num=1, replica_factor=1); %%ngql USE phillies_rag; CREATE TAG IF NOT EXISTS entity(name string); CREATE EDGE IF NOT EXISTS relationship(relationship string); %ngql CREATE TAG INDEX IF NOT EXISTS entity_index ON entity(name(256));
创建新的图空间后,再来构建下 NebulaGraphStore
。参考下面的代码段:
from llama_index.storage.storage_context import StorageContext from llama_index.graph_stores import NebulaGraphStore space_name = "phillies_rag" edge_types, rel_prop_names = ["relationship"], ["relationship"] tags = ["entity"] graph_store = NebulaGraphStore( space_name=space_name, edge_types=edge_types, rel_prop_names=rel_prop_names, tags=tags, ) storage_context = StorageContext.from_defaults(graph_store=graph_store)
是时候加载数据了。我们的源数据来自 Philadelphia Phillies 的维基百科页面和一个关于 Trea Turner 在 2023 年 8 月收到 standing ovation 的 YouTube 视频。
为了节省时间和成本,我们先检查下本地 storage_context
来加载 KG 索引。如果存在索引,我们就加载索引。如果不存在索引(例如初次访问应用程序时),我们需要加载这两个源文档(上文提到的维基百科页面和 YouTube 视频),再构建 KG 索引,并在项目 root 目录的本地 storage_graph 中持久化地存储 doc、index 和 vector。
from llama_index import ( LLMPredictor, ServiceContext, KnowledgeGraphIndex, ) from llama_index.graph_stores import SimpleGraphStore from llama_index import download_loader from llama_index.llms import OpenAI # define LLM llm = OpenAI(temperature=0.1, model="gpt-3.5-turbo") service_context = ServiceContext.from_defaults(llm=llm, chunk_size=512) from llama_index import load_index_from_storage from llama_hub.youtube_transcript import YoutubeTranscriptReader try: storage_context = StorageContext.from_defaults(persist_dir='./storage_graph', graph_store=graph_store) kg_index = load_index_from_storage( storage_context=storage_context, service_context=service_context, max_triplets_per_chunk=15, space_name=space_name, edge_types=edge_types, rel_prop_names=rel_prop_names, tags=tags, verbose=True, ) index_loaded = True except: index_loaded = False if not index_loaded: WikipediaReader = download_loader("WikipediaReader") loader = WikipediaReader() wiki_documents = loader.load_data(pages=['Philadelphia Phillies'], auto_suggest=False) print(f'Loaded {len(wiki_documents)} documents') youtube_loader = YoutubeTranscriptReader() youtube_documents = youtube_loader.load_data(ytlinks=['https://www.youtube.com/watch?v=k-HTQ8T7oVw']) print(f'Loaded {len(youtube_documents)} YouTube documents') kg_index = KnowledgeGraphIndex.from_documents( documents=wiki_documents + youtube_documents, storage_context=storage_context, max_triplets_per_chunk=15, service_context=service_context, space_name=space_name, edge_types=edge_types, rel_prop_names=rel_prop_names, tags=tags, include_embeddings=True, ) kg_index.storage_context.persist(persist_dir='./storage_graph')
在构建 KG 索引时,需要注意以下几点:
max_triplets_per_chunk
:每个块提取三元组的最大数。将其设置为 15,可覆盖大多数(可能不是所有)块中的内容;include_embeddings
:说明创建 KG 索引时,是否包含数据的 Embedding。Embedding 是一种将文本数据表示为数据语义的向量法。它们通常用来让模型理解不同文本片段之间的语义相似性。当设置 include_embeddings=True
时,KnowledgeGraphIndex
会在索引中包含这些嵌入。当你想在知识图谱上执行语义搜索时,include_embeddings=True
会很有用,因为 Embedding 可用来找到与查询在语义上相似的节点和边。现在,让我们跑一个简单的查询。
比如说,告知一些 Philadelphia Phillies 队的信息:
query_engine = kg_index.as_query_engine() response = query_engine.query("Tell me about some of the facts of Philadelphia Phillies.") display(Markdown(f"<b>{response}</b>"))
这是从 Philadelphia Phillies 队的维基百科页面中得到的概述,是个非常不错的简述:
再用 Cypher 查询下:
%%ngql MATCH (p:`entity`)-[e:relationship]->(m:`entity`) WHERE p.`entity`.`name` == 'Philadelphia Phillies' RETURN p, e, m;
该查询将匹配与 Philadelphia Phillies 相关的所有实体。查询结果将会返回与 Philadelphia Phillies 队相关的所有实体、它们与 Philadelphia Phillies 队的关系,以及 Philadelphia Phillies 队实体本身的列表。
现在,让我们在 Jupyter Notebook 中执行下这个 Cypher 查询:
可以看到,结果返回了 9 条数据。
下面,运行 ipython-ngql
包中的 ng_draw
命令,它能在一个单独的 HTML 文件中渲染NebulaGraph 查询的结果;我们得到了以下的图形。以 Philadelphia Phillies 节点为中心,它延伸出 9 个其他节点,每个节点代表 Cypher 查询结果中的一行数据。连接每个节点到中心节点的是边,表示两个节点之间的关系。
非常酷的是,你还可以拖动节点来操作图形!
现在,我们对 NebulaGraph 的基本知识有了初步的了解,让我们深入一点。
下面根据 KG 索引,让我们使用不同的方法查询知识图谱并观察它们的结果。
query_engine = kg_index.as_query_engine()
这种方法通过向量相似性查找 KG 实体,获取连接的文本块,并选择性探索关系。是 LlamaIndex 基于索引构建的默认查询方式。它非常简单、开箱即用,不用额外的参数。
kg_keyword_query_engine = kg_index.as_query_engine( # setting to false uses the raw triplets instead of adding the text from the corresponding nodes include_text=False, retriever_mode="keyword", response_mode="tree_summarize", )
这个查询用了关键词来检索相关的 KG 实体,来获取连接的文本块,并选择性地探索关系以获取更多的上下文。而参数retriever_mode="keyword"
指定了本次检索采用关键词形式。
include_text=False
:查询引擎只用原生三元组进行查询,查询不包含对应节点的文本信息;response_mode="tree_summarize"
:返回结果(响应形式)是知识图谱的树结构的总结。这个树以递归方式构建,查询作为根节点,最相关的答案作为叶节点。tree_summarize
响应模式对于总结性任务非常有用,比如:提供某个话题的高度概括,或是回答某个需要考虑周全的问题。当然,它还可以生成更复杂的响应,比如:解释某个事物发生的真实原因,或者解释某个过程涉及了哪些步骤。kg_hybrid_query_engine = kg_index.as_query_engine( include_text=True, response_mode="tree_summarize", embedding_mode="hybrid", similarity_top_k=3, explore_global_knowledge=True, )
通过设定 embedding_mode="hybrid"
,指定查询引擎为基于向量的检索和基于关键词的检索二者的混合方式,从知识图谱中检索信息,并进行去重。KG 混合检索方式不仅使用关键词找到相关的三元组,它也使用基于向量的检索来找到基于语义相似性的相似三元组。所以,本质上,混合模式结合了关键词搜索和语义搜索,并利用这两种方法的优势来提高搜索结果的准确性和相关性。
include_text=True
:同上文的字段一样,用来指定是否包含节点的文本信息;similarity_top_k=3
:Top K 设定,它将根据 Embedding 检索出最相似结果的前三个结果。你可以根据你的使用场景弹性地调整这个值;explore_global_knowledge=True
:指定查询引擎是否要考虑知识图谱的全局上下文来检索信息。当设置 explore_global_knowledge=True
时,查询引擎不会将其搜索限制在本地上下文(即,一个节点的直接邻居),而是会考虑知识图谱的更广泛的全局上下文。当你想检索与查询不直接相关,但在该知识图谱的更大上下文中有关的信息时,这可能很有用。基于关键词的检索和混合检索二者主要区别,在于我们从知识图谱中检索信息的方法:基于关键词的检索使用关键词方法,而混合检索使用结合 Embedding 和关键词的混合方法。
vector_index = VectorStoreIndex.from_documents(wiki_documents + youtube_documents) vector_query_engine = vector_index.as_query_engine()
这种方式完全不处理知识图谱。它基于向量索引,会先构建文档的向量索引,再从向量索引构建向量查询引擎。
from llama_index import QueryBundle from llama_index.schema import NodeWithScore from llama_index.retrievers import BaseRetriever, VectorIndexRetriever, KGTableRetriever from typing import List class CustomRetriever(BaseRetriever): def __init__( self, vector_retriever: VectorIndexRetriever, kg_retriever: KGTableRetriever, mode: str = "OR", ) -> None: """Init params.""" self._vector_retriever = vector_retriever self._kg_retriever = kg_retriever if mode not in ("AND", "OR"): raise ValueError("Invalid mode.") self._mode = mode def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: """Retrieve nodes given query.""" vector_nodes = self._vector_retriever.retrieve(query_bundle) kg_nodes = self._kg_retriever.retrieve(query_bundle) vector_ids = {n.node.node_id for n in vector_nodes} kg_ids = {n.node.node_id for n in kg_nodes} combined_dict = {n.node.node_id: n for n in vector_nodes} combined_dict.update({n.node.node_id: n for n in kg_nodes}) if self._mode == "AND": retrieve_ids = vector_ids.intersection(kg_ids) else: retrieve_ids = vector_ids.union(kg_ids) retrieve_nodes = [combined_dict[rid] for rid in retrieve_ids] return retrieve_nodes from llama_index import get_response_synthesizer from llama_index.query_engine import RetrieverQueryEngine from llama_index.retrievers import VectorIndexRetriever, KGTableRetriever # create custom retriever vector_retriever = VectorIndexRetriever(index=vector_index) kg_retriever = KGTableRetriever( index=kg_index, retriever_mode="keyword", include_text=False ) custom_retriever = CustomRetriever(vector_retriever, kg_retriever) # create response synthesizer response_synthesizer = get_response_synthesizer( service_context=service_context, response_mode="tree_summarize", ) custom_query_engine = RetrieverQueryEngine( retriever=custom_retriever, response_synthesizer=response_synthesizer, )
LlamaIndex 构建了一个 CustomRetriever
。如上所示,你可以看到它的具体实现。它用来进行知识图谱搜索和向量搜索。默认的 mode
OR
保证了两种搜索结果的并集,结果是包含了这两个搜索方式的结果,且进行了结果去重:
KGTableRetriever
)获得的细节;VectorIndexRetriever
)获得的语义相似性搜索的详情。到目前为止,我们已经探索了使用 KG 索引构建的不同查询引擎。现在,来看看另一个由 LlamaIndex 构建的知识图谱查询引擎——KnowledgeGraphQueryEngine
。看下面的代码片段:
query_engine = KnowledgeGraphQueryEngine( storage_context=storage_context, service_context=service_context, llm=llm, verbose=True, )
KnowledgeGraphQueryEngine
是一个可让我们用自然语言查询知识图谱的查询引擎。它使用 LLM 生成 Cypher 查询语句,再在知识图谱上执行这些查询。这样,我们可以在不学习 Cypher 或任何其他查询语言的情况下查询知识图谱。
KnowledgeGraphQueryEngine
接收 storage_context
,service_context
和 llm
,并构建一个知识图谱查询引擎,其中 NebulaGraphStore
作为 storage_context.graph_store
。
KnowledgeGraphRAGRetriever
是 LlamaIndex 中的一个 RetrieverQueryEngine
,它在知识图谱上执行 Graph RAG 查询。它接收一个问题或任务作为输入,并执行以下步骤:
一个下游任务,如:LLM,可以使用这个上下文生成一个反馈。看下下面的代码片段是如何构建一个 KnowledgeGraphRAGRetriever:
graph_rag_retriever = KnowledgeGraphRAGRetriever( storage_context=storage_context, service_context=service_context, llm=llm, verbose=True, ) kg_rag_query_engine = RetrieverQueryEngine.from_args( graph_rag_retriever, service_context=service_context )
好了,现在我们对 7 种查询方法有了不错的了解。下面,我们用一组问题来测试下它们的效果。
问题 1:告诉我 Bryce Harper 相关信息
下图展示了 7 种查询方式对这一问题的回复,我用不同的颜色对查询语言进行了标注:
这是我基于结果的一些看法:
KnowledgeGraphQueryEngine
和 KnowledgeGraphRAGRetriever
,都返回了我们正在查询的主题——Bryce Harper 的关键事实——只有关键事实,没有详情的阐述;问题 2:Trey Turner 收到的 standing ovation 是如何影响他的赛季表现?
这个问题是特意设计的,来自 YouTube 视频,这个视频专门讲述了这个 standing ovation 事件——Philly 的粉丝们对 Trea Turner(因为 YouTube 把他的名字误写为“Trey”而不是“Trea”,所以我们在问题中使用“Trey”)的支持。
看下 7 种查询方法的回答列表:
这是我基于结果的一些看法:
KnowledgeGraphQueryEngine
返回了以下语法错误。可能原因是 Cypher 生成不正确,如下面的摘要截图所示。看起来 KnowledgeGraphQueryEngine
在提高其 Text2Cypher 能力上还有提升空间;KnowledgeGraphRAGRetriever
返回了关于 Trea Turner 的 standing ovation 事件的最基础信息,显然这个回答是不理想的;小结下:如果将全面的上下文数据正确地加载到知识图谱中,KG 基于向量的检索似乎比上述任何其他查询引擎做得更好。
问题 3:告诉我一些 Philadelphia Phillies 当前球场的事实。
看下 7 种查询方法的回答列表:
这是我基于结果的一些看法:
KnowledgeGraphQueryEngine
找不到任何关于 Philadelphia Phillies 队当前球场的事。似乎这又是一次自然语言自动生成 Cypher 有问题;KnowledgeGraphRAGRetriever
找不到任何关于当前球场的事实;基于上面 3 个问题在 7 个查询引擎上的实验,比较了 7 个查询引擎的优点和缺点:
哪个查询引擎最适合,将取决于你的特定使用情况。
我们在这篇文章中探讨了知识图谱,特别是图数据库 NebulaGraph,是如何结合 LlamaIndex 和 GPT-3.5 为 Philadelphia Phillies 队构建了一个 RAG。
此外,我们还探讨了 7 种查询引擎,研究了它们的内部工作,并观察了它们对三个问题的回答。我们比较了每个查询引擎的优点和缺点,以便更好地理解了每个查询引擎设计的用例。
希望本篇文章对你有所启发,相关代码请查看 GitHub 仓库:https://github.com/wenqiglantz/llamaindex_nebulagraph_phillies/tree/main
Happy coding!