AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


一文说透微软GraphRAG的理论与实践
发布日期:2024-10-15 20:14:56 浏览次数: 1723 来源:风叔云



今年,微软开源了自主研发的Microsoft GraghRag,犹如一枚重磅炸弹,给市场带来了非常大的反响。开源后不到半年,Github上就获得了18k的star,增长迅猛。

在这篇文章中,风叔就详细拆解一下微软GraphRag的原理,并结合源代码,观察实际运行效果。


微软GraphRAG的原理

微软GraphRag的原理并不复杂,下面的一张图即可解释清楚。首先,我们仍然需要从多个原始知识文档构建知识图谱,这一部分与上篇文章介绍的GraphRAG并无太大区别;然后利用社区检测算法(比如leiden算法),把知识图谱划分成多个社区/聚簇,并利用LLM生成这些社区的自然语言摘要;在回答总结性问题时,先在社区执行查询获得中间答案,最后汇总生成全局性的答案。

可以看到,相比以往RAG结合知识图谱的方式,微软GraphRAG最大的不同就是引入了“社区”这个概念社区,即围绕某个主题的一组紧密相关的实体与关系信息,比如“三国演义”这个社区,就会关联到众多的诸侯国、主公、将领、谋士、事件等实体及其关系。

从实现流程来看,微软GraphRAG与传统RAG仍然一致,包括索引阶段和查询阶段两大环节,查询阶段相对简单,最复杂的是索引阶段。

索引阶段的主要过程如下所示:

1. 文本块拆分 将原始文档拆分成多个文本块,这个过程与经典RAG的处理过程一样。

2. 实体与关系提取: 对文本块借助LLM分析,提取实体与关系,这个过程与普通的Graph RAG也类似。

3. 生成实体与关系摘要 为提取的实体与关系生成简单的描述性信息,这是区别普通Graph RAG的一个步骤。这些描述性信息,会作为一个属性存放在实体/关系的Graph节点中。这种摘要的好处是可以借助嵌入向量更准确的对这些实体与关系进行检索,带来的负面影响是由于需要为大量的实体与关系生成描述信息,将会产生很多的LLM调用,这可能带来过长的耗时与昂贵的大模型API开销。

4. 检测与识别社区 借助社区检测算法,在Graph中识别出多个社区。

5. 生成社区摘要: 借助LLM生成每个社区的摘要信息,用来了解数据集的全局主题结构和语义,这也是微软GraphRAG的核心价值所在,也是回答全局性问题的关键。

我们再来看一下检索查询阶段,微软GraphRAG有两种查询模式,local模式和global模式。local(本地)模式通常用于针对具体事实的提问;global(全局)模式则是为了支持全局型的查询任务,即建立在高层语义理解基础之上的概要性问题。

上图是local模式,其查询的主要方法是结合相关的知识图谱结构化信息与原始文档的非结构化数据,构建用于增强生成的上下文,并借助LLM获得响应,因此非常适合回答关于特定事实的问题(比如某个实体的信息与关系等)。其主要过程如下所示:

  1. 在进行查询时,首先根据输入的查询问题与对话历史,从知识图谱中识别出最相关的实体(即在Neo4j库中标签为__Entity__的节点)。这一步主要借助实体节点的描述信息(description)的嵌入向量来实现。
  2. 从这些实体开始,提取更多的相关信息,包括关联的原始文本块、关联的社区、关联的实体和关联的关系,并对这些提取的信息进行排序与筛选,最终形成参考的上下文。
  3. 借助LLM与提示模板,输入上下文与原始问题,生成最终响应。


上图是global模式,其查询的架构相对简单,采用了分布式计算中的Map-Reduce架构,可以简单概括为:

  1. MAP过程:根据用户输入问题与对话历史,查询指定层次结构上(community_level)的所有社区报告,对这些社区报告分成多个批次生成带有评分的中间响应(RIR),评分用来表示这个观点的重要性
  2. Reduce过程:对中间响应进行排序,选择最重要的观点汇总并作为参考的上下文,最后交给LLM生成最终响应结果
  3. global查询模式的问题是响应质量可能会受到输入的社区级别参数的影响。如果层次较低,则报告较为详细,响应可能会更全面,但所需的时间和模型成本较高。所以具体需要在使用时根据实际情况作权衡考虑


微软GraphRAG实战

现在我们用一个文档来构建基于微软GraphRAG的demo应用,这里重点关注索引阶段,准备一个白话文三国演义片段的文本文件用于测试。


第一步,微软GraphRag安装和熟悉

使用pip安装graphrag库,建议在虚拟环境下进行。初始化,这里使用msgraphrag作为应用目录:

python -m graphrag.index --init --root ./msgraphrag


初始化以后会在指定的根目录下生成基本的文件与目录结构,重要的包括:

  • input目录:存放输入原始文档(txt或者csv),这里把测试的txt文件放置到该目录下
  • .env与settings.yaml配置文件:你可以在.env或者settings.yaml中修改全部配置项目,默认情况下可以只修改LLM相关的配置,注意目前只支持OpenAI或者Azure OpenAI的模型:
  • prompts目录:这里存放了自动生成的LLM提示模板文件,一共有四个,分别会在流程的不同处理阶段使用:
    • entity_extraction:用于从自然语言文本抽取实体与关系。
    • summarize_descriptions:用于生成实体与关系的描述性文本。
    • community_report:用于生成社区的摘要等报告信息。


尽管可以使用这里初始化的默认提示模板,但强烈建议通过GraphRAG提供的命令来创建自适应提示模板:GraphRAG会提取输入数据的信息,并借助大模型来分析与生成更具有针对性的提示模板。


第二步,创建索引

现在运行下面的命令来创建自适应的提示模板(更多参数请参考官方文档):

python -m graphrag.prompt_tune --language Chinese


执行成功后我们来观察两个prompts目录下新的提示模板文件内容



这里从最后输出的信息可以了解到其基本的处理过程:从拆分输入文本开始,到提取实体与关系、生成摘要信息、构建内存中的Graph结构、从Graph识别社区、创建社区报告、创建Graph中的文本块节点和文档节点等。


第三步,将索引集成到neo4j

在完成索引后,默认情况下,GraphRAG会把构建整个知识图谱所需要的数据持久化到output目录下,并以parquet格式文件的方式存放。

由于parquet文件可以很简单的通过pandas库读取成DataFrame表,所以在了解其结构后,就可以通过Cypher语句导入成Neo4j图数据库中的节点与关系。

这里使用其提供的笔记本文件在本地运行,即可成功地把上面构建的知识图谱数据全部导入到Neo4j数据库。


第四步 检索查询

我们使用上文介绍的local模式来进行检索,首先创建一个检索相关实体的向量索引。

由于需要根据用户问题从知识图谱所有节点中检索出最相关的实体(导入时设置的标签为__Entity__),这需要利用到实体节点的一个属性:description_embedding,即节点描述信息的嵌入向量。

因此我们需要在description_embedding上创建一个向量索引,并基于此索引来检索相关实体即可。使用如下Cypher语句在Neo4j创建这个索引:

CREATE VECTOR INDEX entity_index IF NOT EXISTS FOR (e:__Entity__) ON e.description_embeddingOPTIONS {indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: 'cosine'}}


然后提取更多相关信息。在检索出的多个实体基础上,进一步检索其关联信息,包括关联的文本块、社区报告、内部关系、外部关系等,并将这些信息组装成上下文,用于后续的生成。

这里可以利用Neo4j的Vector组件的一个输入参数retrieval_query来实现这个过程。该参数作用是自定义一个Cypher代码片段,该片段会添加到到默认的Neo4j向量检索语句后一起执行,并最终要求返回text、score、metadata三个字段,以用于Langchain构建检索的返回对象。

现在我们定义一个后续处理的代码片段,在检索出的node基础上(这里就是关联的实体),进一步检索其他关联信息。

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构建输出对象的需要。

剩下的工作就很简单了,把这个片段用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'])


如果运行顺利,将可以看到类似的输出:

周瑜是孙策的结拜兄弟,在孙策去世后,辅佐孙权在赤壁之战中击破曹操。


当然在实际使用中,我们还可以根据自身的需要,进一步优化检索召回策略,比如参考风叔在《RAG实战篇系列》中介绍的查询重写、查询扩展、多级索引、Rerank模型等方式。

除了local模式的查询外,global模式也可自定义实现。如果说local模式的关键在于如何召回相关上下文,global模式的关键则在于map与reduce过程的提示模板,感兴趣的朋友可参考微软GraphRAG源代码中的提示模板自行实现。


53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询