AI知识库

53AI知识库

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


【下】深入Microsoft GraphRAG之查询阶段:基于Neo4j自定义实现检索与生成
发布日期:2024-08-22 08:44:23 浏览次数: 1856 来源:AI大模型应用实践


在上篇中,我们基于一段约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 : 可以是localglobal这是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获得响应。因此非常适合回答关于特定事实的问题(比如某个实体的信息与关系等)。大致过程如下:

1. 在进行查询时,首先根据输入的查询问题与对话历史,从知识图谱中识别出最相关的实体(即在Neo4j库中标签为__Entity__的节点)。这一步主要借助实体节点的描述信息(description)的嵌入向量来实现。

2. 从这些实体开始,提取更多的相关信息。包括:

  • 关联的原始文本块。提取其文本内容

  • 关联的社区。提取其社区报告

  • 关联的实体。提取其实体描述信息

  • 关联的关系。提取其关系描述信息

  • 关联的协变量。由于默认不生成,这里忽略


并对这些提取的信息进行排序与筛选,最终形成参考的上下文。

3. 借助LLM与提示模板,输入上下文与原始问题,生成最终响应。

【global模式查询】

Microsoft GraphRAG的global模式查询的基本过程如下:

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

1. MAP过程:根据用户输入问题与对话历史,查询指定层次结构上(community_level)的所有社区报告,对这些社区报告分成多个批次生成带有评分的中间响应(RIR),评分用来表示这个观点的重要性

2. Reduce过程: 对中间响应进行排序,选择最重要的观点汇总并作为参考的上下文,最后交给LLM生成最终响应结果

global查询模式的问题是响应质量可能会受到输入的社区级别参数的影响。如果层次较低,则报告较为详细,响应可能会更全面,但所需的时间和模型成本较高。所以具体需要在使用时根据实际情况作权衡考虑。

基于Neo4j实现自定义GraphRAG查询

了解内部原理后,可以在已经导入到Neo4j的知识图谱基础上自定义实现自己的检索与响应过程。这里我们参考上面的查询过程原理,在Neo4j中的知识图谱基础上自定义实现一个local模式的查询过程大致过程如下(基于LangChain):

  1. 创建一个检索相关实体的向量索引

由于需要根据用户问题从知识图谱所有节点中检索出最相关的实体(导入时设置的标签为__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($index, $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









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

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

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

联系我们

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

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询