DALL-E生成的这幅图。
图检索增强生成技术(GraphRAG)越来越受欢迎,成为了强大的补充力量。这种方法利用了其结构化的特性,将数据组织为节点和关系,从而增强检索信息的深度和上下文。
比如一个知识图谱的例子。
图非常适合用来表示和存储异构且相互关联的信息,可以轻松地捕捉跨不同类型数据的复杂关系和属性。相比之下,向量数据库在处理这种结构化信息时往往会感到吃力,因为它们的优势在于通过高维向量来处理非结构化数据这一点。在你的RAG应用程序中,你可以把结构化的图数据与通过非结构化文本的向量搜索结合起来,以达到两者兼得的效果。这就是我们在本文中要展示的。
知识图很棒,我们怎么创建呢?构建知识图谱通常是最具挑战性的一步。这包括收集和整理数据,这需要深入了解领域知识以及图谱建模。
为了简化这个流程,我们一直在尝试使用大型语言模型(LLMs)。凭借其对语言和上下文的深刻理解,LLMs 可以自动化知识图的创建过程中的许多部分。通过分析文本数据,这些模型可以识别实体、理解它们之间的关系,并提出如何在图结构中最好地表示这些实体的建议。
这些实验之后,我们已经将图构造模块的第一个版本添加到了LangChain,并在本文中演示。
代码可以在GitHub上找到。
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提供的API密钥,因为我们在本文中会用到他们的模型。
为了这个演示,我们将使用伊丽莎白一世的维基百科页面。我们可以使用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]) # 将前三篇文章进行分段处理并存储在documents中
现在是时候根据检索到的文档来构建一个图了。为此,我们实现了一个LLMGraphTransformer
模块,这大大简化了知识图在图数据库中的构建和存储过程。
llm=ChatOpenAI(temperature=0, model_name="gpt-4-0125-preview") llm_transformer = LLMGraphTransformer(llm=llm) # 提取图中的数据 graph_documents = llm_transformer.convert_to_graph_documents(documents) # 将数据存储到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检索器。此检索器使用关键词和向量搜索来搜索非结构化的文本资料,并将其与收集到的知识图谱信息结合。由于Neo4j同时支持关键词和向量索引,你可以在单一的数据库系统中实现这三种检索。这些收集的数据会被输入到大型语言模型中,生成并提供最终的答案。
你可以使用 Neo4jVector.from_existing_graph
方法为文档同时添加关键字和向量检索功能。此方法配置关键字和向量搜索索引,以支持混合搜索方法,目标节点是标记为 Document
的节点。此外,如果缺少文本嵌入值,它还会计算这些值以填补空白。
vector_index = Neo4jVector.from_existing_graph( OpenAIEmbeddings(), # OpenAIEmbeddings():使用OpenAI的嵌入模型 search_type="混合", # search_type="混合":混合搜索类型 node_label="文档", # node_label="文档" text_node_properties=["文本"], # text_node_properties=["文本"] embedding_node_property="嵌入" # embedding_node_property="嵌入" ) # vector_index:从现有图谱中生成的向量索引
然后可以使用 similarity_search
方法来调用这个向量索引。
另一方面,配置图形检索更为复杂但更灵活,这个例子将使用全文索引来识别相关的节点,并返回它们的直接邻居。
图形检索。图片由作者提供。
图形检索器首先从识别输入中的相关实体开始。为简化起见,指示模型识别人名、组织和地点。为此,我们将使用LCEL和新添加的with_structured_output
方法来实现这一目标。
# 从文本中提取实体 class Entities(BaseModel): """关于实体识别的信息。""" names: List[str] = Field( ..., description="文本中出现的所有人名、组织名或企业名。", ) prompt = ChatPromptTemplate.from_messages( [ ( "system", "你将从文本中提取组织和个人的实体。", ), ( "human", "请按照给定的格式从以下内容中提取信息:{question}", ), ] ) entity_chain = prompt | llm.with_structured_output(Entities)
我们来试试看:
entity_chain.invoke({"question": "阿梅莉亚·艾尔哈特出生在哪里?"}).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: """ 根据给定的输入字符串生成全文搜索查询。 这个函数构建了一个适合全文搜索的查询字符串。在将用户提问中的实体映射到数据库值时非常有用。它处理输入字符串,将其拆分成单词,并为每个单词添加一个相似度阈值(~2个更改的字符),并将它们使用AND运算符连接起来。这允许一些拼写上的小错误。 """ 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()
我们现在把所有的东西都结合起来吧。
# 全文索引查询:结构化检索问题中提到的实体的邻域信息 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
函数首先从用户的问题中识别实体。然后,它会遍历这些识别出的实体,并利用 Cypher 模板来获取这些相关节点的周边信息。我们现在来试试看!
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 -> 亨利八世 # 等等
正如之前提到的,我们将结合非结构化和图检索模型来生成最终传递给大语言模型的上下文。
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"""结构化数据: {structured_data} 未结构化的数据: ['#Document '.join(unstructured_data)] """ 返回最终数据 final_data
当我们处理 Python 代码时,我们可以简单地用 f-string 来拼接输出结果,把输出结果拼接在一起。
我们已经成功地实现了RAG的检索部分。接下来,我们将介绍一个提示,该提示利用集成的混合检索器提供的上下文生成响应,从而完成整个RAG链的实现。
模板文本 = """根据以下上下文回答问题: {context} 问题:{question} """ prompt = ChatPromptTemplate.from_template(模板文本) chain = ( RunnableParallel( { "context": _search_query | retriever, "question": RunnablePassthrough(), } ) | prompt | llm | StrOutputParser() )
最后,我们可以测试一下混合RAG模型。
chain.invoke({'问题': '伊丽莎白一世属于哪个家族?'}) 搜索:伊丽莎白一世属于哪个家族? '伊丽莎白一世属于都铎家族。'
我还加入了查询重写的功能,使RAG链能更好地适应允许后续提问的对话场景。因为我们采用了向量和关键词搜索的方法,需要重写后续问题来优化搜索过程。
chain链调用( { "question": "她是什么时候出生的?", "chat_history": [("伊丽莎白一世属于哪个王朝?", "都铎王朝")], } ) # 搜索查询:伊丽莎白一世是什么时候出生的? # '伊丽莎白一世出生于1533年9月7日。'
你可以看到她是什么时候出生的?
最初改写为伊丽莎白一世什么时候出生?
。改写后的查询被用来查找相关信息,然后回答这个问题。
随着引入了LLMGraphTransformer
,生成知识图谱的过程更加流畅和易于访问,让希望增强知识图谱的深度和上下文的RAG应用更加轻松。这只是个开始,因为我们还有很多改进的计划。
如果你对我们用大型语言模型生成图表有任何想法、建议或疑问,请随时联系我们。
代码可在 GitHub 找到。