基于知識圖譜的LangChain應用實戰(zhàn)
圖檢索增強生成(Graph RAG)正逐漸流行起來,成為傳統(tǒng)向量搜索方法的有力補充。這種方法利用圖數(shù)據(jù)庫的結(jié)構(gòu)化特性,將數(shù)據(jù)以節(jié)點和關系的形式組織起來,從而增強檢索信息的深度和上下文關聯(lián)性。
示例知識圖譜
圖在表示和存儲多樣化且相互關聯(lián)的信息方面具有天然優(yōu)勢,能夠輕松捕捉不同數(shù)據(jù)類型間的復雜關系和屬性。而向量數(shù)據(jù)庫在處理這類結(jié)構(gòu)化信息時則顯得力不從心,它們更擅長通過高維向量處理非結(jié)構(gòu)化數(shù)據(jù)。在 RAG 應用中,結(jié)合結(jié)構(gòu)化的圖數(shù)據(jù)和非結(jié)構(gòu)化的文本向量搜索,可以讓我們同時享受兩者的優(yōu)勢,這也是本文將要探討的內(nèi)容。
知識圖譜的確很有用,但如何構(gòu)建一個呢? 構(gòu)建知識圖譜通常是利用圖數(shù)據(jù)表示的強大功能中最困難的一步。它需要收集和整理數(shù)據(jù),這需要對領域知識和圖建模有深刻的理解。為了簡化這一過程,我們開始嘗試使用大型語言模型(LLM)。LLM 憑借其對語言和上下文的深刻理解,可以自動化知識圖譜創(chuàng)建過程中的大部分工作。通過分析文本數(shù)據(jù),這些模型能夠識別實體,理解它們之間的關系,并提出如何在圖結(jié)構(gòu)中最佳表示這些實體?;谶@些實驗,我們已經(jīng)將圖構(gòu)建模塊的首個版本集成到了 LangChain 中,本文將展示其應用。
相關代碼已在 GitHub 上發(fā)布。
Neo4j 環(huán)境搭建
為了跟隨本文的示例,您需要搭建一個 Neo4j 實例。最簡單的方法是在 Neo4j Aura 上啟動一個免費實例,它提供了 Neo4j 數(shù)據(jù)庫的云版本。當然,您也可以通過下載 Neo4j Desktop 應用程序來創(chuàng)建一個本地數(shù)據(jù)庫實例。
os.environ["OPENAI_API_KEY"] = "sk-"
os.environ["NEO4J_URI"] = "bolt://localhost:7687"
os.environ["NEO4J_USERNAME"] = "neo4j"
os.environ["NEO4J_PASSWORD"] = "password"
graph = Neo4jGraph()
此外,您還需要一個 OpenAI 密鑰,因為我們將在本文中使用他們的模型。
數(shù)據(jù)導入
在本次演示中,我們將使用伊麗莎白一世的維基百科頁面。我們可以利用 LangChain 加載器 輕松地從維基百科獲取并分割文檔。
# 讀取維基百科文章
raw_documents = WikipediaLoader(query="Elizabeth I").load()
# 定義分塊策略
text_splitter = TokenTextSplitter(chunk_size=512, chunk_overlap=24)
documents = text_splitter.split_documents(raw_documents[:3])
現(xiàn)在是時候根據(jù)獲取的文檔來構(gòu)建圖譜了。為此,我們開發(fā)了一個 LLMGraphTransformer 模塊,它極大地簡化了在圖數(shù)據(jù)庫中構(gòu)建和存儲知識圖譜的過程。
llm=ChatOpenAI(temperature=0, model_name="gpt-4-0125-preview")
llm_transformer = LLMGraphTransformer(llm=llm)
# 提取圖數(shù)據(jù)
graph_documents = llm_transformer.convert_to_graph_documents(documents)
# 存儲到 neo4j
graph.add_graph_documents(
graph_documents,
baseEntityLabel=True,
include_source=True
)
您可以指定知識圖譜生成鏈使用哪種 LLM。目前,我們只支持 OpenAI 和 Mistral 的函數(shù)調(diào)用模型。不過,我們計劃未來會擴展 LLM 的選擇范圍。在這個例子中,我們使用的是最新的 GPT-4。需要注意的是,生成的圖譜質(zhì)量很大程度上取決于您使用的模型。理論上,您應該選擇能力最強的模型。LLM 圖轉(zhuǎn)換器返回的圖文檔可以通過 add_graph_documents 方法導入到 Neo4j。baseEntityLabel 參數(shù)為每個節(jié)點添加了一個額外的 __Entity__ 標簽,以增強索引和查詢性能。include_source 參數(shù)則將節(jié)點與其原始文檔關聯(lián)起來,便于數(shù)據(jù)追溯和理解上下文。
您可以在 Neo4j 瀏覽器中查看生成的圖譜。
結(jié)合混合(向量 + 關鍵字)和圖檢索方法。
請注意,這張圖片僅為了清晰展示,只展示了生成圖譜的一部分。
RAG 的混合檢索
在圖譜生成之后,我們將采用一種混合檢索方法,結(jié)合向量和關鍵字索引以及圖檢索技術,用于 RAG 應用。
結(jié)合混合(向量 + 關鍵字)和圖檢索方法。
上圖展示了一個檢索過程,從用戶提出問題開始,然后由 RAG 檢索器處理。這個檢索器結(jié)合了關鍵字和向量搜索來篩選非結(jié)構(gòu)化文本數(shù)據(jù),并將其與從知識圖譜中提取的信息結(jié)合起來。由于 Neo4j 同時支持關鍵字和向量索引,您可以使用單一數(shù)據(jù)庫系統(tǒng)實現(xiàn)所有三種檢索方式。這些來源的數(shù)據(jù)將被送入 LLM,以生成并提供最終答案。
非結(jié)構(gòu)化數(shù)據(jù)檢索器
您可以使用 Neo4jVector.from_existing_graph 方法為文檔添加關鍵字和向量檢索功能。該方法為混合搜索方法配置了關鍵字和向量搜索索引,目標是標記為 Document 的節(jié)點。如果缺少文本嵌入值,它還會自動計算。
vector_index = Neo4jVector.from_existing_graph(
OpenAIEmbeddings(),
search_type="hybrid",
node_label="Document",
text_node_properties=["text"],
embedding_node_property="embedding"
)
然后,您可以使用 similarity_search 方法來調(diào)用向量索引。
圖檢索器
另一方面,配置圖檢索器雖然更為復雜,但提供了更大的靈活性。在這個例子中,我們將使用全文索引來識別相關節(jié)點,然后返回它們的直接鄰域。
圖檢索器示意圖
圖檢索器首先識別輸入中的相關實體。為了簡化,我們指導 LLM 識別人物、組織和地點。為了實現(xiàn)這一點,我們將使用 LCEL 配合新加入的 with_structured_output 方法。
# 從文本中提取實體
class Entities(BaseModel):
"""識別實體相關信息。"""
names: List[str] = Field(
...,
descriptinotallow="文本中出現(xiàn)的所有人物、組織或商業(yè)實體的名稱",
)
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"您正在從文本中提取組織和人物實體。",
),
(
"human",
"請按照給定格式從以下輸入中提取信息:{question}",
),
]
)
entity_chain = prompt | llm.with_structured_output(Entities)
讓我們來實際測試一下:
entity_chain.invoke({"question": "阿梅莉亞·埃爾哈特在哪里出生?"}).names
# ['阿梅莉亞·埃爾哈特']
很好,現(xiàn)在我們能夠在問題中識別出實體,接下來我們將使用全文索引將這些實體映射到知識圖譜中。首先,我們需要定義一個全文索引,并創(chuàng)建一個函數(shù)來生成全文查詢,這個查詢允許一定程度的拼寫錯誤,這里我們不詳細展開。
graph.query(
"CREATE FULLTEXT INDEX entity IF NOT EXISTS FOR (e:__Entity__) ON EACH [e.id]")
def generate_full_text_query(input: str) -> str:
"""
為給定的輸入字符串生成全文搜索查詢。
該函數(shù)構(gòu)建一個適用于全文搜索的查詢字符串。它通過將輸入字符串分割成單詞,并對每個單詞附加一個相似性閾值(允許2個字符變化),然后使用 AND 運算符將它們組合起來。這對于將用戶問題中的實體映射到數(shù)據(jù)庫值非常有用,并且能夠容忍一些拼寫錯誤。
"""
full_text_query = ""
words = [word for word in remove_lucene_chars(input).split() if word]
for word in words[:-1]:
full_text_query += f"{word}~2 AND"
full_text_query += f"{words[-1]}~2"
return full_text_query.strip()
現(xiàn)在,讓我們整合所有步驟。
# 全文索引查詢
def structured_retriever(question: str) -> str:
"""
收集問題中提到的實體的鄰域信息
"""
result = ""
entities = entity_chain.invoke({"question": question})
for entity in entities.names:
response = graph.query(
"""CALL db.index.fulltext.queryNodes('entity', $query,
{limit:2})
YIELD node,score
CALL {
MATCH (node)-[r:!MENTIONS]->(neighbor)
RETURN node.id + ' - ' + type(r) + ' -> ' + neighbor.id AS
output
UNION
MATCH (node)<-[r:!MENTIONS]-(neighbor)
RETURN neighbor.id + ' - ' + type(r) + ' -> ' + node.id AS
output
}
RETURN output LIMIT 50
""",
{"query": generate_full_text_query(entity)},
)
result += "\n".join([el['output'] for el in response])
return result
structured_retriever 函數(shù)首先識別用戶問題中的實體,然后遍歷這些實體,使用 Cypher 模板檢索相關節(jié)點的鄰域信息。讓我們來實際測試一下!
print(structured_retriever("伊麗莎白一世是誰?"))
# 伊麗莎白一世 - BORN_ON -> 1533年9月7日
# 伊麗莎白一世 - DIED_ON -> 1603年3月24日
# 伊麗莎白一世 - TITLE_HELD_FROM -> 英格蘭和愛爾蘭女王
# 伊麗莎白一世 - TITLE_HELD_UNTIL -> 1558年11月17日
# 伊麗莎白一世 - MEMBER_OF -> 都鐸王朝
# 伊麗莎白一世 - CHILD_OF -> 亨利八世
# 等等...
最終檢索器
正如我們一開始提到的,我們將結(jié)合非結(jié)構(gòu)化和圖檢索器來創(chuàng)建最終的上下文,這將傳遞給 LLM。
def retriever(question: str):
print(f"搜索查詢:{question}")
structured_data = structured_retriever(question)
unstructured_data = [el.page_content for el in vector_index.similarity_search(question)]
final_data = f"""結(jié)構(gòu)化數(shù)據(jù):
{structured_data}
非結(jié)構(gòu)化數(shù)據(jù):
{"#Document ".join(unstructured_data)}
"""
return final_data
由于我們使用的是 Python,我們可以使用 f-string 輕松地將輸出合并。
定義 RAG 鏈
我們已經(jīng)成功實現(xiàn)了 RAG 的檢索組件。接下來,我們將引入一個提示,它利用混合檢索器提供的上下文來生成響應,從而完成 RAG 鏈的實現(xiàn)。
template = """根據(jù)以下上下文回答問題:
{context}
問題:{question}
"""
prompt = ChatPromptTemplate.from_template(template)
chain = (
RunnableParallel(
{
"context": _search_query | retriever,
"question": RunnablePassthrough(),
}
)
| prompt
| llm
| StrOutputParser()
)
最后,我們可以測試我們的混合 RAG 實現(xiàn)。
chain.invoke({"question": "伊麗莎白一世屬于哪個家族?"})
# 搜索查詢:伊麗莎白一世屬于哪個家族?
# '伊麗莎白一世屬于都鐸王朝。'
我還加入了一個查詢重寫特性,使得 RAG 鏈能夠適應允許后續(xù)問題的對話環(huán)境。鑒于我們使用了向量和關鍵字搜索方法,我們需要重寫后續(xù)問題以優(yōu)化搜索過程。
chain.invoke(
{
"question": "她何時出生?",
"chat_history": [("伊麗莎白一世屬于哪個家族?",
"都鐸王朝")],
}
)
# 搜索查詢:伊麗莎白一世何時出生?
# '伊麗莎白一世出生于1533年9月7日。'
您可以看到,'她何時出生?' 首先被重寫為 '伊麗莎白一世何時出生?'。然后使用重寫后的查詢來檢索相關上下文并回答問題。
總結(jié)
隨著 LLMGraphTransformer 的引入,生成知識圖譜的過程現(xiàn)在應該更加順暢和易于訪問,這使得任何想要通過知識圖譜提供的深度和上下文來增強其基于 RAG 的應用的人更容易上手。這只是一個開始,因為我們計劃進行更多的改進。
本文轉(zhuǎn)載自 ??AI小智??,作者: AI小智
