将Neo4j与LangChain结合,在RAG应用中高效构建与检索知识图谱的实战教程

图像来源:DALL-E生成
GraphRAG已成为热点,正逐步成为传统向量检索的强力补充。该方案借助图数据库形态,把数据组织成节点与关系,从而显著提升搜索信息的上下文深度与相关性。原本在向量数据库中难以有效处理的结构化数据,在图模型里反而如鱼得水——图天然擅长呈现与存储互联数据,能轻松捕获不同数据类型间的复杂关联与属性。而在RAG应用中,融合结构化图数据与非结构化向量搜索,恰好可以实现优势互补,达成更优的检索效果。这正是本文要演示的核心流程。
知识图谱的价值毋庸置疑,但如何创建?这往往是最大痛点:需要采集数据、设计图结构,还得对领域知识与图建模有深入理解。好消息是,大语言模型在语言理解和上下文把握上的强大能力,正好可以用来简化知识图谱的创建过程。通过分析文本数据,这些模型能自动识别实体、梳理关系,并建议在图结构中的最佳表示方式。
基于这些探索成果,LangChain新增了首个图构建模块,下面就来演示它的具体用法。
完整代码可在GitHub上获取。
Neo4j环境配置
首先需要准备一个Neo4j实例。最简单的方式是在 Neo4j Aura 上启动一个免费实例,获取云端数据库;也可以下载 Neo4j桌面应用,在本地创建数据库。
|
Shell 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 Key,本演示将使用其模型进行后续操作。
数据导入
以伊丽莎白一世的维基百科页面为例。利用LangChain的加载器从维基百科抓取文档并做分块处理。
|
Shell # Read the wikipedia article raw_documents = WikipediaLoader(query="Elizabeth I").load() # Define chunking strategy text_splitter = TokenTextSplitter(chunk_size=512, chunk_overlap=24) documents = text_splitter.split_documents(raw_documents[:3])
|
接下来基于文档构建知识图谱。这里使用 LLMGraphTransformer 模块,它能大幅简化在图数据库中构建并存储知识图谱的过程。
|
Shell 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的函数调用模型,后续会逐步扩展更多选项。这里选用最新的GPT-4。注意,生成图的质量与模型能力直接相关,理论上应选择最强模型。 LLMGraphTransformer 返回图文档对象,通过 add_graph_documents 方法导入Neo4j。参数 baseEntityLabel=True 会为每个节点额外添加 __Entity__ 标签,有助于提升索引与查询性能;include_source=True 则将节点链接到原始文档,便于追溯与上下文理解。
在Neo4j浏览器中可以查看生成的图结构(下图仅展示局部)。
生成的局部知识图谱
请注意,此图只是完整生成图的一部分。
RAG混合检索
图生成完毕后,采用混合搜索策略:将向量搜索、关键词搜索与图搜索结合,用于RAG应用。
结合(向量+关键词)与图搜索的混合检索流程。图片来源:作者
上图展示了搜索流程:用户提出问题后,RAG检索器同时执行关键词搜索和向量搜索来查找非结构化文本,同时从知识图谱中提取结构化信息。Neo4j原生支持关键词索引和向量索引,因此单个数据库系统即可承载三种搜索类型。收集到的数据统一输入LLM,生成最终答案。
非结构化数据检索器
使用 Neo4jVector.from_existing_graph 方法为文档启用关键词和向量检索支持。该方法会配置混合搜索索引,目标节点标签设为 Document。若文本嵌入值尚未存在,系统会自动计算。
|
Shell 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 方法实现。
|
Shell # 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)
|
测试一下效果:
|
Shell entity_chain.invoke({"question": "Where was Amelia Earhart born?"}).names # ['Amelia Earhart']
|
成功检测到问题中的实体后,接下来用全文索引将它们映射到知识图谱。首先定义一个全文索引,并编写一个支持拼写容错的全文查询函数。
|
Shell 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()
|
将各个部分组合起来。
|
Shell # 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模板检索相关节点的邻域信息。测试一下:
|
Shell 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的上下文。
|
Shell 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链。
|
Shell 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() )
|
测试混合RAG的效果:
|
Shell 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链能适应对话场景中的后续问题。由于使用了向量和关键词搜索,后续问题需要重写才能优化搜索效果。
|
Shell 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应用。这只是一个起点,后续将有更多改进计划。
完整代码可在GitHub上获取。