利用知识图谱提高 RAG 应用的准确性
来源:互联网
时间:2026-07-29 15:43:18
在 RAG 应用中使用 Neo4j 和 LangChain 构建和检索知识图谱信息的实用指南

图片由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上获取。