点击上方蓝字关注我们
在上篇中,我们基于一段约2万字的描述“漫威世界”的文本使用Microsoft GraphRAG框架与工具完成了索引阶段。由于默认输出为本地目录中parquet格式存储的文件(理解成可以通过Pandas读写的数据库文件),我们借助Python代码与Cypher语言将它们导入Neo4j图数据库,从而能够更直观的分析与使用已经构建的知识图谱:
现在让我们一起探索Microsoft GraphRAG的查询(Query)阶段:
- 官方查询工具与API
- 深入Microsoft GraphRAG查询原理
- 基于Neo4j实现自定义GraphRAG查询
官方查询工具与API
如何在已经构建的Microsoft GraphRAG知识图谱上完成查询呢?首先简单来看官方提供的两种主要用法。
【借助命令行工具】
Microsoft GraphRAG提供了CLI工具用于在已经构建的知识图谱上进行查询,你可以在配置好基本的环境变量(或者配置文件)后,直接使用命令进行查询调用。这里了解几个重要的查询参数:
- community_level :社区的层级。这是使用leiden算法识别社区时生成的一个级别(level)数值,值越高意味着社区越小,默认是2。可以使用如下命令在导入的Neo4j库中分析不同级别的社区数量:
MATCH (c:\_\_Community\_\_)
RETURN c.level AS level, COUNT(*) AS count
ORDER BY level
- response_type :一个描述响应类型和格式的自然语言文本。这是对响应结果格式的要求,比如默认的“Multiple Paragraphs”,表示用多段落的方式输出响应;你也可以修改成“Single Sentence“等。
- method : 可以是 local 与 global 。 这是Microsoft GraphRAG支持的两种核心查询模式,local(本地)模式通常用于针对具体事实的提问; global(全局)模式则是为了支持QFS型的查询任务,即建立在高层语义理解基础之上的概要性问题。
这是一个local模式的查询例子:
python -m graphrag.query
--root ./msgraphrag
--method local
--community\_level 2
'X教授的名字是怎么来的?他和天启之间有怎样的联系'
这是一个global模式的查询例子:
python -m graphrag.query
--root ./msgraphrag
--method global
--community_level 2
'请概括介绍文章中复仇者联盟的相关信息'
【借助API使用】
除了使用命令行工具做测试,还可以使用官方API进行开发,当然过程会较繁琐,但好处是可以借助代码与其他应用作灵活集成。官方已经 在源代码的“ examples_notebooks "目录下提供了详细的 local/global_search.ipynb 文件,你可以参考其中的讲解,拷贝相关代码后作简单修改即可使用,此处 不再做详细介绍。
深入Microsoft GraphRAG查询原理
现在让我们来深入了解Microsoft GraphRAG在 查询阶段的内部过程与原理,这需要结合官方文档介绍与源代码查看来完成。 这些原理将指导我们在后面基于Neo4j实现自己的查询过程。
【local模式查询】
Microsoft GraphRAG的local模式查询的基本过程如下:
图片来自官方文档
local模式查询的主要方法是 结合相关的知识图谱结构化信息与原始文档的非结构化数据,构建用于增强生成的上下文,并借助LLM获得响应。 因此非常适合回答关于特定事实的问题(比如某个实体的信息与关系等)。大致过程如下:
-
在进行查询时,首先根据输入的查询问题与对话历史,从知识图谱中识别出最相关的实体(即在Neo4j库中标签为__Entity__的节点)。这一步主要借助实体节点的描述信息(description)的嵌入向量来实现。
-
从这些实体开始,提取更多的相关信息。包括:
- 关联的原始文本块。提取其文本内容
- 关联的社区。提取其社区报告
- 关联的实体。提取其实体描述信息
- 关联的关系。提取其关系描述信息
- 关联的协变量。由于默认不生成,这里忽略
并对这些提取的信息进行排序与筛选,最终形成参考的上下文。
- 借助LLM与提示模板,输入上下文与原始问题,生成最终响应。
【global模式查询】
Microsoft GraphRAG的global模式查询的基本过程如下:
global模式查询的架构相对简单,它采用了分布式计算中的Map-Reduce架构。可以简单概括为:
1. MAP过程: 根据用户输入问题与对话历史,查询指定层次结构上(community_level)的所有社区报告,对这些社区报告分成多个批次生成带有评分的中间响应(RIR),评分用来表示这个观点的重要性
2. Reduce过程: 对中间响应进行排序,选择最重要的观点汇总并作为参考的上下文,最后交给LLM生成最终响应结果
global查询模式的问题是 响应质量可能会受到输入的社区级别参数的影响 。如果层次较低,则报告较为详细,响应可能会更全面,但所需的时间和模型成本较高。所以具体需要在使用时根据实际情况作权衡考虑。
基于Neo4j实现自定义GraphRAG查询
了解内部原理后,可以在已经导入到Neo4j的知识图谱基础上自定义实现自己的检索与响应过程。 这里我们参考上面的查询过程原理,在Neo4j中的知识图谱基础上自定义实现一个local模式的查询过程, 大致过程如下(基于LangChain):
- 创建一个检索相关实体的向量索引
由于需要根据用户问题从知识图谱所有节点中检索出最相关的实体(导入时设置的标签为__Entity__),这需要利用到实体节点的一个属性:description_embedding,即节点描述信息的嵌入向量。下面是一个例子:
因此我们需要在description_embedding上创建一个向量索引,并基于此索引来检索相关实体即可。使用如下Cypher语句在Neo4j创建这个索引:
CREATE VECTOR INDEX entity\_index IF NOT EXISTS FOR (e:\_\_Entity\_\_) ON e.description\_embedding
OPTIONS {indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity\_function`: 'cosine'
}}
如何验证这个索引是否有用呢?可以创建一个简单的向量组件来测试这个索引(名字为entity_index) :
......
os.environ["AZURE\_OPENAI\_API\_KEY"] = '自行准备'
os.environ["AZURE\_OPENAI\_ENDPOINT"] = '自行准备'
#模型准备,后续也会用到
text\_embedder = AzureOpenAIEmbeddings(
azure\_deployment='text-embedding-3-small',
openai\_api\_version='2024-05-01-preview',
)
llm = AzureChatOpenAI(
deployment\_name='gpt-4o-mini',
openai\_api\_version='2024-05-01-preview',
)
#以下为测试代码,实际不需要:使用Langchain的Neo4jVector组件,从已经创建的neo4j中的向量索引进行检索测试
entity\_vector = Neo4jVector.from\_existing\_index(
text\_embedder,
url=NEO4J\_URI,
username=NEO4J\_USERNAME,
password=NEO4J\_PASSWORD,
index\_name='entity\_index',
text\_node\_property='description', #这个不可少,用来指定节点的text属性字段
)
result = entity\_vector.similarity\_search("复仇者联盟",top\_k=5)
print(result[0].page\_content)
结果输出如下,证明这个索引是有效的:
2. 提取更多相关信息。 在检索出的多个实体基础上,进一步检索其关联信息,包括关联的文本块、社区报告、内部关系(即检索出来的节点之间)、外部关系等,并将这些信息组装成上下文,用于后续的生成。
这里可以利用 Neo4jVector 组件的一个输入参数 retrieval_query 来实现这个过程。
该参数作用是: 允许你自定义一个Cypher代码片段,该片段会添加到到默认的Neo4j向量检索语句后一起执行,并最终要求返回text,score,metadata三个字段,以用于Langchain构建检索的返回对象。
如果你查看LangChain代码,可以看到最终执行的Cypher语句默认是这样构造的:
read_query = (
"CALL db.index.vector.queryNodes(k, $embedding) "
"YIELD node, score "
) + retrieval_query
这里可以看到,在默认的向量检索节点代码后,添加了retrieval_query片段。因此你可以利用这个参数,来接收向量检索输出的node和score,做后续处理。
现在我们定义一个后续处理的代码片段,在检索出的node基础上(这里就是关联的实体),进一步检索其他关联信息。然后把这个片段用retrieval_query参数传入即可。这个Cypher片段如下:
lc\_retrieval\_query = """
//接收向量检索输出的node,在此基础上进一步检索
WITH collect(node) as nodes
//查找最相关的文本块,输出text属性
WITH
collect {
UNWIND nodes as n
MATCH (n)<-[:HAS\_ENTITY]->(c:\_\_Chunk\_\_)
WITH c, count(distinct n) as freq
RETURN c.text AS chunkText
ORDER BY freq DESC
LIMIT $topChunks
} AS text\_mapping,
//查找最相关的社区,输出summary摘要(如果没有weight,用cypher设定)
collect {
UNWIND nodes as n
MATCH (n)-[:IN\_COMMUNITY]->(c:\_\_Community\_\_)
WITH c, c.rank as rank, c.weight AS weight
RETURN c.summary
ORDER BY rank, weight DESC
LIMIT $topCommunities
} AS report\_mapping,
//查找最相关的其他实体(nodes外部),输出描述
collect {
UNWIND nodes as n
MATCH (n)-[r:RELATED]-(m)
WHERE NOT m IN nodes
RETURN r.description AS descriptionText
ORDER BY r.rank, r.weight DESC
LIMIT $topOutsideRels
} as outsideRels,
//查找最相关的其他实体(nodes内部),输出描述
collect {
UNWIND nodes as n
MATCH (n)-[r:RELATED]-(m)
WHERE m IN nodes
RETURN r.description AS descriptionText
ORDER BY r.rank, r.weight DESC
LIMIT $topInsideRels
} as insideRels,
//输出这些实体本身的描述
collect {
UNWIND nodes as n
RETURN n.description AS descriptionText
} as entities
//返回text,score,metadata三个字段
RETURN {Chunks: text\_mapping, Reports: report\_mapping,
Relationships: outsideRels + insideRels,
Entities: entities} AS text, 1.0 AS score, {source:''} AS metadata
"""
这里的Cypher语句虽然较长,但其实并不复杂。就是对向量检索输出的node搜集后,检索更多相关信息(社区报告、其他实体、关系等),最后合并输出,注意这里必须输出text,score,metadata三个属性,这是Langchain构建输出对象的需要。
剩下的工作就很简单了,只需要将上面的测试向量检索的代码稍做修改即可: 去掉text_node_property参数,增加retrieval_query参数 :
#创建neo4j向量存储对象,注意传入retrieval_query参数lc_vector = Neo4jVector.from_existing_index( text_embedder, url=NEO4J_URI, username=NEO4J_USERNAME, password=NEO4J_PASSWORD, index_name='entity_index', retrieval_query=lc_retrieval_query,)#chain,并调用获得响应。此处可参考Langchain文档学习chain = RetrievalQAWithSourcesChain.from_chain_type( llm, chain_type="stuff",
retriever=lc_vector.as_retriever(search_kwargs={"params":{
"topChunks": topChunks, "topCommunities": topCommunities, "topOutsideRels": topOutsideRels, "topInsideRels": topInsideRels, }}))response = chain.invoke( {"question": "复仇者联盟与钢铁侠有什么关系?"}, return_only_outputs=True,)print(response['answer'])
如果一切正常,你将可以看到类似的输出:
大功告成!
如果你习惯使用LlamaIndex框架,也可以采用类似方法实现。当然在实际使用中,你还可以根据自身的需要,进一步优化这里的检索召回策略。甚至可以结合查询重写、其他索引(如普通向量索引)策略、Rerank模型等实现更复杂的RAG范式,以获得最佳效果,这很好地扩充了Microsft GraphRAG的应用场景。
除了local模式的查询外,global模式也可自定义实现。如果说local模式的关键在于如何召回相关上下文,global模式的关键则在于map与reduce过程的提示模板,感兴趣的朋友可参考Microsfot GraphRAG源代码中的提示模板自行实现。
END
点击下方关注我,不迷路
交流请识别以下名片并说明来源