近來 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!
謝謝你讀完本文 (///▽///)