微信扫码
与创始人交个朋友
我要投稿
Graph retrieval-augmented generation (GraphRAG) 正在逐渐流行,并成为传统向量搜索检索方法的有力补充。这种方法利用了图数据库的结构化特性,将数据组织为节点和关系,以增强检索信息的深度和上下文相关性。
图(Graphs)非常擅长以结构化的方式表示和存储异构且相互连接的信息,轻松捕捉跨不同数据类型的复杂关系和属性。相比之下,向量数据库在处理这种结构化信息时常常遇到困难,因为它们的优势在于通过高维向量处理非结构化数据。在你的RAG应用中,你可以通过非结构化文本将结构化的图数据与向量搜索结合起来,以实现两全其美的效果。这就是我们在这篇博客文章中将要展示的内容。
知识图谱很棒,但你如何创建一个呢?构建一个知识图谱通常是最具挑战性的步骤。它涉及到数据的收集和结构化,这需要对领域和图建模都有深入的理解。
为了简化这一过程,我们一直在尝试使用大型语言模型(LLMs)。凭借它们对语言和上下文的深刻理解,LLMs可以自动化知识图谱创建过程中的重要部分。通过分析文本数据,这些模型能够识别实体,理解它们之间的关系,并建议如何最好地在图结构中表示它们。
作为这些实验的结果,我们已经将图形构建模块的第一个版本添加到了LangChain中,我们将在这篇博客文章中展示它。"
这段文字讨论了图数据库在表示结构化信息方面的优势,以及构建知识图谱的挑战。文章提到了使用大型语言模型(LLMs)来简化知识图谱的创建过程,并且介绍了一个名为LangChain的系统中新增的图形构建模块。
相关代码:https://github.com/tomasonjo/blogs/blob/master/llm/enhancing_rag_with_graph.ipynb
Neo4j环境设置您需要设置一个Neo4j实例。请按照这篇博客文章中的示例操作。最简单的方法是在Neo4j Aura上启动一个免费实例,它提供Neo4j数据库的云实例。或者,您也可以通过下载Neo4j Desktop应用程序并创建本地数据库实例来设置本地Neo4j数据库实例。
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密钥,因为我们将在此博客文章中使用他们的模型。
数据摄取在这个演示中,我们将使用伊丽莎白一世的维基百科页面。我们可以使用LangChain加载器无缝地从维基百科获取和分割文档。
# Read the wikipedia articleraw_documents = WikipediaLoader(query="Elizabeth I").load()# Define chunking strategytext_splitter = TokenTextSplitter(chunk_size=512, chunk_overlap=24)documents = text_splitter.split_documents(raw_documents[:3])
现在是时候根据检索到的文档构建一个图了。为此,我们实现了一个LLMGraphTransformer模块,它极大地简化了在图数据库中构建和存储知识图谱的过程。
llm=ChatOpenAI(temperature=0, model_name="gpt-4-0125-preview")
llm_transformer = LLMGraphTransformer(llm=llm)
# Extract graph data
graph_documents = llm_transformer.convert_to_graph_documents(documents)
# Store to neo4j
graph.add_graph_documents(
graph_documents,
baseEntityLabel=True,
include_source=True
)
你可以定义你希望知识图谱生成链使用哪个LLM(大型语言模型)。目前,我们只支持从OpenAI和Mistral调用函数的模型。然而,我们计划在未来扩展LLM的选择范围。在这个例子中,我们使用的是最新的GPT-4。请注意,生成图的质量在很大程度上取决于你所使用的模型。理论上,你总是想使用最有能力的一个。LLM图转换器返回的是图文档,可以通过add_graph_documents方法导入到Neo4j中。baseEntityLabel参数为每个节点分配一个额外的__Entity__标签,增强了索引和查询性能。include_source参数将节点链接到它们原始的文档,有助于数据可追溯性和上下文理解。
你可以在Neo4j浏览器中检查生成的图。
注意,这张图片只代表了生成图的一部分。
RAG应用的混合检索在图生成之后,我们将使用一种混合检索方法,它结合了向量和关键词索引以及用于RAG应用的图检索。
图表展示了一个检索过程,从用户提出问题开始,然后问题被提交给RAG检索器。这个检索器使用关键词和向量搜索来查找非结构化文本数据,并将其与从知识图谱中收集的信息结合起来。由于Neo4j具有关键词和向量索引的功能,你可以用单一的数据库系统实现所有三种检索选项。这些来源收集的数据被输入到LLM中,以生成并提供最终答案。
非结构化数据检索器你可以使用Neo4jVector.from_existing_graph方法为文档添加关键词和向量检索。该方法配置了针对标记为Document的节点的混合搜索方法的关键词和向量搜索索引。此外,如果缺少文本嵌入值,它还会计算这些值。"
这段话描述了在用户提问时启动的检索过程,并说明了如何使用RAG检索器结合关键词和向量搜索来检索非结构化文本数据以及如何从知识图谱中收集信息。还提到了利用Neo4j的关键词和向量索引功能,可以在单一数据库系统中实施所有三种检索选项,并将收集到的数据输入到LLM中以生成最终答案。最后,解释了如何使用Neo4jVector.from_existing_graph方法来增强对文档的检索能力。
vector_index = Neo4jVector.from_existing_graph(
OpenAIEmbeddings(),
search_type="hybrid",
node_label="Document",
text_node_properties=["text"],
embedding_node_property="embedding")
然后可以使用similarity_search方法来调用向量索引。
图检索器另一方面,配置图检索涉及更多步骤,但提供了更大的自由度。这个例子将使用全文索引来识别相关节点并返回它们的直接邻域。
图检索器首先识别输入中的相关实体。为了简化过程,我们指示LLM识别人物、组织和地点。为此,我们将使用LCEL(可能是一个模型或工具的缩写),并利用新添加的with_structured_output方法来实现这一点。
# Extract entities from text
class Entities(BaseModel):
"""Identifying information about entities."""
names: List[str] = Field(
...,
description="All the person, organization, or business entities that "
"appear in the text",
)
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are extracting organization and person entities from the text.",
),
(
"human",
"Use the given format to extract information from the following "
"input: {question}",
),
]
)
entity_chain = prompt | llm.with_structured_output(Entities)
让我们来测试一下:
entity_chain.invoke({"question": "Where was Amelia Earhart born?"}).names# ['Amelia Earhart']
很好,现在我们可以在问题中检测到实体了,让我们使用全文索引将它们映射到知识图谱上。首先,我们需要定义一个全文索引和一个函数,这个函数将生成允许一些拼写错误的全文查询,这里我们不会详细介绍。
graph.query(
"CREATE FULLTEXT INDEX entity IF NOT EXISTS FOR (e:__Entity__) ON EACH [e.id]")
def generate_full_text_query(input: str) -> str:
"""
Generate a full-text search query for a given input string.
This function constructs a query string suitable for a full-text search.
It processes the input string by splitting it into words and appending a
similarity threshold (~2 changed characters) to each word, then combines
them using the AND operator. Useful for mapping entities from user questions
to database values, and allows for some misspelings.
"""
full_text_query = ""
words = [el for el in remove_lucene_chars(input).split() if el]
for word in words[:-1]:
full_text_query += f" {word}~2 AND"
full_text_query += f" {words[-1]}~2"
return full_text_query.strip()
现在让我们把所有东西放在一起。
# Fulltext index query
def structured_retriever(question: str) -> str:
"""
Collects the neighborhood of entities mentioned
in the question
"""
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函数首先检测用户问题中的实体。接下来,它遍历检测到的实体,并使用Cypher模板来检索相关节点的邻域。让我们测试一下!
print(structured_retriever("Who is Elizabeth I?"))# Elizabeth I - BORN_ON -> 7 September 1533# Elizabeth I - DIED_ON -> 24 March 1603# Elizabeth I - TITLE_HELD_FROM -> Queen Of England And Ireland# Elizabeth I - TITLE_HELD_UNTIL -> 17 November 1558# Elizabeth I - MEMBER_OF -> House Of Tudor# Elizabeth I - CHILD_OF -> Henry Viii# and more...
最终检索器正如一开始提到的,我们将结合非结构化和图检索器来创建传递给LLM的最终上下文。"
这段话提到了将非结构化数据检索器和图检索器的结果结合起来,以生成最终的上下文信息,这个上下文将被用来作为输入传递给一个大型语言模型(LLM)。
def retriever(question: str):print(f"Search query: {question}")structured_data = structured_retriever(question)unstructured_data = [el.page_content for el in vector_index.similarity_search(question)]final_data = f"""Structured data:{structured_data}Unstructured data:{"#Document ". join(unstructured_data)}"""return final_data
由于我们正在使用Python,我们可以简单地使用f-string来连接输出。
定义RAG链我们已经成功实现了RAG的检索组件。接下来,我们将引入一个提示,该提示利用集成的混合检索器提供的上下文来生成响应,完成RAG链的实现。
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
chain = (
RunnableParallel(
{
"context": _search_query | retriever,
"question": RunnablePassthrough(),
}
)
| prompt
| llm
| StrOutputParser()
)
由于我们正在使用Python,我们可以简单地使用f-string来连接输出。
定义RAG链我们已经成功实现了RAG的检索组件。接下来,我们将引入一个提示,该提示利用集成的混合检索器提供的上下文来生成响应,完成RAG链的实现。
chain.invoke({"question": "Which house did Elizabeth I belong to?"})# Search query: Which house did Elizabeth I belong to?# 'Elizabeth I belonged to the House of Tudor.'
我还整合了一个查询重写功能,使得RAG链能够适应对话环境,允许提出后续问题。鉴于我们使用向量和关键词搜索方法,我们必须重写后续问题以优化我们的搜索过程。
chain.invoke({"question": "When was she born?","chat_history": [("Which house did Elizabeth I belong to?", "House Of Tudor")],})# Search query: When was Elizabeth I born?# 'Elizabeth I was born on 7 September 1533.'
你可以看到,'When was she born?' 首先被重写为 'When was Elizabeth I born?'。然后使用重写后的查询来检索相关的上下文并回答问题。
简化增强RAG应用随着LLMGraphTransformer的引入,生成知识图谱的过程现在应该变得更加顺畅和易于访问,这使得任何希望利用知识图谱提供的深度和上下文来增强其RAG应用的人都能更加容易。这只是一个开始,因为我们计划了很多改进。
完整代码地址:https://github.com/tomasonjo/blogs/blob/master/llm/enhancing_rag_with_graph.ipynb
53AI,企业落地应用大模型首选服务商
产品:大模型应用平台+智能体定制开发+落地咨询服务
承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2024-03-30
2024-04-26
2024-05-10
2024-04-12
2024-05-28
2024-05-14
2024-04-25
2024-07-18
2024-04-26
2024-05-06
2024-12-22
2024-12-21
2024-12-21
2024-12-21
2024-12-21
2024-12-20
2024-12-20
2024-12-19