为了账号安全,请及时绑定邮箱和手机立即绑定

将微软GraphRAG知识图谱集成到Neo4j中

将MSFT GraphRAG的输出存储在Neo4j中,并利用LangChain或LlamaIndex来实现:本地和全局检索器

这是用ChatGPT生成的图片。

微软最近发布的一个名为GraphRAG(GraphRAG)的实现引起了相当大的关注。在我的上一篇博客里,我讨论了图的构建方法,并探讨了研究论文(研究论文)中提到的一些创新方面。总体来说,输入到GraphRAG库中的是一些包含各种信息的源文档。这些文档通过大型语言模型(LLM)被处理,以提取文档中出现的实体及其关系的结构化信息。然后,利用这些提取的信息来构建知识图谱。

高级索引管道技术,如微软在GraphRAG论文中所描述的,图片来自作者

在知识图谱构建完成后,GraphRAG库利用图算法组合,特别是莱登社区检测算法,以及大语言模型的提示,生成知识图谱中实体和关系组成的社区的自然语言摘要。

在这篇文章里,我们将从GraphRAG库得到的输出并存入Neo4j,然后利用LangChain和LlamaIndex的编排框架从Neo4j中直接设置检索器。

代码和 GraphRAG 的输出可以在 GitHub 上访问,让您能够跳过 GraphRAG 的提取步骤。

数据集

本文中提到的数据集是英国作家查尔斯·狄更斯的《圣诞颂歌》这部作品,这部作品可通过Gutenberg项目免费下载。

查尔斯·狄更斯的《圣诞欢歌》免费下载,由志愿者们数字化和校对。

我们选择了这本书作为源文档,因为它在这份introductory documentation中被特别提及,这样我们就能轻松地进行提取操作。

构建图

即使你可以跳过图提取过程,我们还会讨论我认为最重要的几个配置选项。例如,图提取过程可能会非常耗费token,成本也很高,因此,使用像gpt-4o-mini这样相对便宜但性能良好的LLM进行测试是有意义的。gpt-4-turbo版本可以显著降低费用,同时保持较好的准确性,如这篇博客文章中所述,

GRAPHRAG_LLM_MODEL=gpt-4o-mini  # GPT-4o-mini 模型

最重要的配置是确定我们要提取的实体类型是什么。默认情况下,系统会自动提取组织、人物、事件和地理实体。

GRAPHRAG_ENTITY_EXTRACTION_ENTITY_TYPES=组织,人物,事件,地点

这些默认的实体类型可能适用于书本内容,但请确保根据实际处理的文档领域进行适当的调整,以适应具体的应用场景。

另一个重要的配置是最大获取值。作者们发现并确认,我们也独立验证过,LLM在一次提取过程中并不能提取所有可用的信息。

提取的实体数量,取决于文本块的大小——图片摘自《GraphRAG论文》(根据CC BY 4.0许可协议使用)

分拣配置允许LLM执行多次提取过程。在上述图像中,我们可以清楚地看到,在进行多次提取时,我们提取了更多的信息。多次提取过程会消耗大量令牌资源,因此使用像gpt-4o-mini这样的更经济的模型有助于降低成本。

    GRAPHRAG_ENTITY_EXTRACTION_MAX_GLEANINGS=1

此外,默认情况下不会提取主张或协变量数据。你可以通过设置 GRAPHRAG_CLAIM_EXTRACTION_ENABLED 配置来启用它。

GRAPHRAG_CLAIM_EXTRACTION_ENABLED=False #图形提取功能是否开启(False表示关闭)
GRAPHRAG_CLAIM_EXTRACTION_MAX_GLEANINGS=1 #最大提取次数设置为1

似乎反复出现的一个主题是,并非所有的结构化信息都能一次提取完成。因此,这里也提供了相应的采集配置设置。

还有一个挺有意思的地方,不过我还没来得及深入研究的是提示调优这部分。提示调优是可选的,不过强烈建议尝试一下,因为它可以显著提升准确性。

提示调整 ⚙️GraphRAG 提供了创建领域适配模板的能力,用于生成知识图谱。这个步骤……microsoft.github.io

配置完成后,我们可以按照说明运行图提取流程,该过程包括以下步骤。

管道中的步骤图:图像来自GraphRAG论文,该图像根据CC BY 4.0许可使用

该提取管道执行上述图像中的所有蓝色步骤。请参阅我的先前博客文章,了解更多信息。MSFT GraphRAG库的图提取管道输出一组parquet文件,如Dulce行动示例所示。

这些 Parquet 文件可以轻松导入到 Neo4j 图数据库,方便后续分析、可视化和检索。我们可以使用免费的云 Aura 实例或设置本地的 Neo4j 环境(安装指南)。我的朋友 Michael Hunger 主要完成了将 Parquet 文件导入到 Neo4j 的工作。在这篇博客里,我们将略过导入的具体步骤,它包括从五个或六个 CSV 文件中导入数据并构建知识图谱的过程。想了解更多关于 CSV 导入的内容,可以看看 Neo4j 图学院课程

导入代码文件作为一个Jupyter notebook在GitHub上提供,以及示例GraphRAG的输出结果。

blogs/msft_graphrag/ms_graphrag_import.ipynb at master · tomasonjo/博客 支持我图数据科学博客文章的Jupyter笔记本……github.com

导入完成后,我们就可以打开Neo4j浏览器来验证并可视化图的一部分。

部分图表,由作者自己绘制。

图形分析

在实现检索器之前,我们将进行简单的图分析,以便熟悉提取的数据内容。首先定义数据库连接,并实现一个执行Cypher语句(图数据库查询语言)并输出Pandas DataFrame的函数。

NEO4J_URI="bolt://localhost"  
NEO4J_USERNAME="neo4j"  
NEO4J_PASSWORD="password"  

driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))  

def db_query(cypher, params: Dict[str, Any] = {}):  
    """执行Cypher语句并将结果转换为DataFrame"""  
    # 执行Cypher查询并将结果转换为DataFrame  
    return driver.execute_query(  
        cypher, parameters_=params, result_transformer_=Result.to_df  
    )

在进行图提取时,我们采用了300的块大小。之后,作者把默认的块大小改成了1200。我们可以用以下Cypher语句来验证块大小。

db_query(  
  "MATCH (n:__Chunk__) RETURN n.n_tokens AS token_count, count(*) AS count"  
)  
# _token_count count,  
# 300         230,  
# 155         1  
MATCH (n:__Chunk__) 这里表示匹配标签为`__Chunk__`的节点.

230个片段块包含300个token,而最后一个片段则只有155个token。我们现在来看一个实例实体及其描述。

这行代码是从数据库中查询一个实体的名称和描述(只返回一个结果):
db_query(  
  "MATCH (n:__Entity__) RETURN n.name AS name, n.description AS description LIMIT 1"  
)

结果:

示例实体及其描述。(图片来源:作者)

项目古腾堡似乎在书的某个地方被提到,可能是在开头。我们可以通过观察发现,描述可以包含比实体名称更多的细节和复杂信息,这比MSFT GraphRAG论文中提到的保留文本中更复杂和细微信息的方法要详细得多。

我们也来看看例子中的关系吧。

    db_query(  
      "MATCH ()-[n:RELATED]->() RETURN n.description AS description LIMIT 5"  
    )

结果如下:

示例关系说明。作者提供的图片。

微软的GraphRAG不仅超越了仅提取实体间的简单关系类型,还能详细描述这些关系。这种能力让它能够捕捉到更加细微和具体的信息。

我们还可以分析一个社区及其描述。

    db_query("""  
      MATCH (n:__Community__)   
      RETURN n.title AS 标题, n.summary AS 摘要, n.full_content AS 全文 LIMIT 1  
    """)

结果怎么样

这是作者提供的一张社区示例图片的描述。

社区有标题、摘要和由大规模语言模型生成的全文内容。我还没看到作者在检索时是使用全文内容还是仅使用了摘要,但我们可以在两者中任选其一。我们可以在全文内容中观察到引用,这些引用指向了信息来源的实体及其关系。有趣的是,有时大规模语言模型会因引用过长而将其删减,如下例所示。

    [数据:实体对象 (11, 177);关联 (25, 159, 20, 29, +等)]

没有办法展开 +更多 这个按钮,所以这是一个处理长引用时大型语言模型采用的有趣方法。

我们现在来评估这些分布。我们先从检查从文本块中提取的实体数量的分布开始。

    entity_df = db_query(
        """  
    MATCH (d:__Chunk__)
    RETURN count {(d)-[:HAS_ENTITY]->()} AS entity_count
    """
    )
    # 绘制实体数量分布图
    plt.figure(figsize=(10, 6))
    sns.histplot(entity_df['entity_count'], kde=True, bins=15, color='skyblue')
    plt.axvline(entity_df['entity_count'].mean(), color='red', linestyle='dashed', linewidth=1)
    plt.axvline(entity_df['entity_count'].median(), color='green', linestyle='dashed', linewidth=1)
    plt.xlabel('实体数量', fontsize=12)
    plt.ylabel('频率', fontsize=12)
    plt.title('实体数量的分布情况', fontsize=15)
    plt.legend(['均值: ' + str(entity_df['entity_count'].mean()), '中位数: ' + str(entity_df['entity_count'].median())])
    plt.show()

结果

从文本块中提取实体数量的分布。图片由作者提供。

记住,每个文本块包含大约300个令牌。因此,提取的实体数量相对较少,平均每个文本块大约提取到三个实体。提取仅进行了一次,未做多次提取。如果我们增加提取次数,看看分布会如何变化会很有趣。

接下来,我们将评估节点的度数分布。节点的度数是指一个节点所拥有的关系数。

    degree_dist_df = db_query(  
        """  
    MATCH (e:__Entity__)  
    RETURN count {(e)-[:RELATED]-()} AS node_degree  
    """  
    )  
    # 计算均值和中位数  
    mean_degree = np.mean(degree_dist_df['node_degree'])  
    percentiles = np.percentile(degree_dist_df['node_degree'], [25, 50, 75, 90])  
    # 创建一个使用对数轴的直方图  
    plt.figure(figsize=(12, 6))  
    sns.histplot(degree_dist_df['node_degree'], bins=50, kde=False, color='blue')  
    # 使用对数轴的x轴  
    plt.yscale('log')  
    # 添加标签和标题  
    plt.xlabel('节点度(节点的连接数量)')  
    plt.ylabel('计数(对数轴)')  
    plt.title('节点度分布')  
    # 添加均值线、中位数线和分位数线  
    plt.axvline(mean_degree, color='red', linestyle='dashed', linewidth=1, label=f'均值: {mean_degree:.2f}')  
    plt.axvline(percentiles[0], color='purple', linestyle='dashed', linewidth=1, label=f'第25百分位: {percentiles[0]:.2f}')  
    plt.axvline(percentiles[1], color='orange', linestyle='dashed', linewidth=1, label=f'第50百分位: {percentiles[1]:.2f}')  
    plt.axvline(percentiles[2], color='yellow', linestyle='dashed', linewidth=1, label=f'第75百分位: {percentiles[2]:.2f}')  
    plt.axvline(percentiles[3], color='brown', linestyle='dashed', linewidth=1, label=f'第90百分位: {percentiles[3]:.2f}')  
    # 添加图例说明  
    plt.legend()  
    # 最终显示图形  
    plt.show()

成果

节点度的分布。图片由作者提供。

大多数现实世界的网络都遵循幂律度分布,大多数节点的度数相对较小,而一些重要节点的度数则很大。尽管我们这个图很小,节点度依然遵循幂律分布。找出有120个关系(连通了43%的实体)的实体会很有趣。

db_query("""  
  MATCH (n:__Entity__)  
  RETURN n.name AS 名, count((n)-[:RELATED]-()) AS 关系数  
  ORDER BY 关系数 DESC LIMIT 5""")

结果如下

关系最多的事物,作者的图片。

毫不迟疑地,我们可以假设斯克鲁吉是这本书的主要角色。我也会猜测伊贝茨·斯克鲁吉斯克鲁吉实际上是同一个实体,不过由于MSFT GraphRAG缺乏实体解析环节,因此没有被合并为同一个实体。

这也证明了处理数据很重要,因为尽管Project Gutenberg中有13种关系,这些关系并不涉及书中的故事情节。

最后,我们最后将检查一下每个层级社区规模的分布。

    community_data = db_query("""  
      MATCH (n:__Community__)  
      RETURN n.level AS level, count{(n)-[:IN_COMMUNITY]-()} AS members  
    """)  

    stats = community_data.groupby('level').agg(  
        min_members=('members', 'min'),  
        max_members=('members', 'max'),  
        median_members=('members', 'median'),  
        avg_members=('members', 'mean'),  
        num_communities=('members', 'count'),  
        total_members=('members', 'sum')  
    ).reset_index()  

    # 创建箱形图  
    plt.figure(figsize=(10, 6))  
    sns.boxplot(x='level', y='members', data=community_data, palette='viridis')  
    plt.xlabel('层级')  
    plt.ylabel('成员数量')  

    # 添加统计标注  
    for i in range(stats.shape[0]):  
        level = stats['level'][i]  
        max_val = stats['max_members'][i]  
        text = (f"数量: {stats['num_communities'][i]} 个社区\n"  
                f"总成员数: {stats['total_members'][i]} 人\n"  
                f"最少: {stats['min_members'][i]}\n"  
                f"最多: {stats['max_members'][i]}\n"  
                f"中位数: {stats['median_members'][i]}\n"  
                f"平均: {stats['avg_members'][i]:.2f}")  
        plt.text(level, 85, text, horizontalalignment='center', fontsize=9)  

    plt.show()

结果:

每个层级的社区规模分布图,由作者提供。

莱顿算法识别出了三个层级的社区,较高层级的社区通常更大。然而,有一些技术细节我不太清楚,因为每个层级的节点数量实际上并不相同,尽管理论上它们应该相同。另外,当社区在较高层级合并时,为什么级别0有19个社区,而级别1则有22个?作者们在这里运用了一些优化手段和技巧,我还没来得及仔细研究这些内容。

实现检索组件

在本文的最后部分,我们将讨论MSFT GraphRAG中定义的本地检索器和全局检索器。接下来,我们将实现这些检索器并将它们集成到LangChain和LlamaIndex中去。

本地检索工具

本地检索器首先使用向量搜索来找到相关的节点,接着收集与这些节点相关的所有信息,并将这些信息注入LLM的提示中。

本地检索架构。图片来自这个链接:https://microsoft.github.io/graphrag/posts/query/1-local_search/

尽管这个图看起来很复杂,但实际上可以很容易地实施。我们首先通过基于实体描述的文本嵌入进行向量相似性搜索,识别相关的实体。一旦识别出相关的实体,我们就可以遍历相关的文本片段、关系、社区摘要等信息。这种使用向量相似性搜索并遍历整个图的方式可以非常容易地通过 LangChain 和 LlamaIndex 的 retrieval_query 特性实现。

首先,我们要设置向量索引。

    index_name = "实体"  

    db_query(  
        """  
    创建向量索引 """  
        + index_name  
        + """ 如果不存在 FOR (e:__Entity__) ON e.描述嵌入  
    选项 {indexConfig: {  
     `vector.dimensions`: 1536,  
     `vector.similarity_function`: '余弦'  
    }}  
    """  
    )

我们将计算并保存社区权重值,社区权重值定义为社区内实体出现的不同文本片段数。

    db_query(  
        """  
    匹配(n:`__Community__`)<-[:IN_COMMUNITY]-()<-[:HAS_ENTITY]-(c)  
    WITH n, 计算不同的c的数量作为块计数  
    SET n.权重 = 块计数"""  
    )

每个部分的候选数量(文本单元、社区报告等)是可配置的。虽然原始实现的过滤规则基于词元计数稍微复杂一些,这里我们将其简化。我根据默认配置值开发了以下简化的顶级候选过滤设置。

    topChunks = 3,  
    topCommunities = 3,  
    topOutsideRels = 10,  
    topInsideRels = 10,  
    topEntities = 10,

我们将从LangChain的实现开始着手,唯一需要定义的就是retrieval_query,这个部分稍微复杂一点。

    lc_retrieval_query = """  
    WITH collect(node) as nodes  
    // 实体 - 文本单元映射  
    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,  
    // 实体 - 报告映射  
    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,  
    // 外部关系  
    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,  
    // 内部关系  
    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  
    // 这里没有协变量或索赔  
    RETURN {Chunks: text_mapping, Reports: report_mapping,   
           Relationships: outsideRels + insideRels,   
           Entities: entities} AS text, 1.0 AS score, {} AS metadata  
    """  

    lc_vector = Neo4jVector.from_existing_index(  
        OpenAIEmbeddings(model="text-embedding-3-small"),  
        url=NEO4J_URI,  
        username=NEO4J_USERNAME,  
        password=NEO4J_PASSWORD,  
        index_name=index_name,  
        retrieval_query=lc_retrieval_query  
    )

此Cypher查询语句对一组节点进行多项分析任务,以便提取并整理相关文本信息。

  1. 实体-文本单元映射关系:对于每个节点,查询识别链接文本片段(__Chunk__),按每个片段关联的唯一节点数进行聚合,并按频率排序。返回频率最高的片段作为 text_mapping 结果。

2. 实体-报告映射:对于每个节点,查询找到关联的社区(Community),并返回排名和权重最高的社区摘要。

3. 关系:这里提取了与m有关但不在初始节点集内的关系描述(RELATED)。只保留了排名靠前的关系。

4. 内部的关系:就像外部关系一样,但这里只关注那些双方都在初始节点集合内的关系。

5. 实体描述:仅仅收集节点的描述。

最后,查询将收集到的数据整合成一个包含数据块、报告、内部和外部关系及实体说明的结构化结果集,此外,还包括默认得分类和空的元数据对象。你可以选择移除某些检索部分来测试这对结果的影响。

现在你可以使用以下代码来启动检索程序。

docs = lc_vector.similarity_search(  
    "你对Cratchitt家族了解多少?",  
    k=topEntities,  
    params={  
        "topChunks": topChunks,  
        "topCommunities": topCommunities,  
        "topOutsideRels": topOutsideRels,  
        "topInsideRels": topInsideRels,  
    },  
)  
# print(docs[0].page_content)

同样的检索模式也可以通过LlamaIndex实现。对于LlamaIndex,我们首先需要为节点添加元数据,以使向量索引能够正常工作。如果没有为相关节点添加默认的元数据,向量索引将无法工作并返回错误。

    # https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/vector_stores/utils.py#L32  
    from llama_index.core.schema import TextNode  
    from llama_index.core.vector_stores.utils import node_to_metadata_dict  

    content = node_to_metadata_dict(TextNode(), remove_text=True, flat_metadata=False)  

    db_query(  
        """  
      MATCH (e:__Entity__)  
      SET e += $content""",  
        {"content": content},  
    )

我们可以使用 LlamaIndex 中的 retrieval_query 特性来定义检索器函数。与 LangChain 的做法不同,我们将使用 f-string 而不是查询参数来传递这些参数,以传递 top 候选过滤条件。

    retrieval_query = f"""  
    将收集的节点作为节点
    // 实体 - 文本单元映射
    WITH  
    节点,
    对节点进行展开处理 {
        UNWIND 节点 as n
        MATCH (n)<-[:HAS_ENTITY]->(c:__Chunk__)
        WITH c, count(distinct n) as freq
        返回 c.text 作为 chunkText
        按频次降序排列
        限制为{topChunks}个
    } AS text_mapping,
    // 实体 - 报告关联
    对节点进行展开处理 {
        UNWIND 节点 as n
        MATCH (n)-[:IN_COMMUNITY]->(c:__Community__)
        WITH c, c.rank as rank, c.weight AS weight
        返回 c.summary
        按rank和weight降序排列
        限制为{topCommunities}个
    } AS report_mapping,
    // 外部联系
    对节点进行展开处理 {
        UNWIND 节点 as n
        MATCH (n)-[r:RELATED]-(m)
        其中 m 不在 节点内
        返回 r.description 作为 descriptionText
        按r.rank和r.weight降序排列
        限制为{topOutsideRels}个
    } as outsideRels,
    // 内部联系
    对节点进行展开处理 {
        UNWIND 节点 as n
        MATCH (n)-[r:RELATED]-(m)
        其中 m 在 节点内
        返回 r.description 作为 descriptionText
        按r.rank和r.weight降序排列
        限制为{topInsideRels}个
    } as insideRels,
    // 实体详情
    对节点进行展开处理 {
        UNWIND 节点 as n
        返回 n.description 作为 descriptionText
    } as entities
    // 我们这里没有协变量或声明
    返回 "Chunks:" + apoc.text.join(text_mapping, '|') + "\nReports: " + apoc.text.join(report_mapping,'|') +    
           "\nRelationships: " + apoc.text.join(outsideRels + insideRels, '|') +   
           "\nEntities: " + apoc.text.join(entities, "|") AS text, 分数为1.0, 节点[0].id AS id, {{_node_type:节点[0]._node_type, _node_content:节点[0]._node_content}} AS 元数据
    """

此外,返回的内容稍微有点不一样。我们需要返回节点类型和内容作为元数据;否则,检索器会出问题。我们现在只需实例化Neo4j向量存储库,并将其作为查询引擎使用。

neo4j_vector = Neo4jVectorStore(  
    NEO4J_USERNAME,  
    NEO4J_PASSWORD,  
    NEO4J_URI,  
    embed_dim,  
    index_name=index_name,  
    retrieval_query=retrieval_query,  
)  
loaded_index = VectorStoreIndex.from_vector_store(neo4j_vector).as_query_engine(  
    similarity_top_k=topEntities, embed_model=OpenAIEmbedding(model="text-embedding-3-large")  
)

我们现在可以测试GraphRAG的本地检索功能了。

    response = loaded_index.query("你对斯克鲁奇了解多少?")  
    print(response.response)  
    # 斯克鲁奇是一位员工,深受菲吉威家族(特别是菲吉威先生和夫人)的慷慨和节日气氛的影响。他参加了菲吉威家族举办的那场难忘的家庭舞会,这对他的生活产生了重大影响,也体现了故事中关于仁慈和社区精神的主题。

我们可以马上想到的一个改进方法是,通过结合向量和关键词的混合方式来查找相关的实体,而不仅仅使用向量搜索来提升本地检索的效果。

全球检索

这个全局检索器架构稍微简单一点。它似乎会在指定层级上遍历所有社区摘要,生成中间摘要,最后根据这些中间摘要来生成最终的响应。

全局检索架构。图片来自如下所示:https://microsoft.github.io/graphrag/posts/query/0-global_search/

我们得先确定要遍历哪个层级,这并不是一个简单的决定,因为我们不知道哪个方案会更有效。层级越高,社区的规模越大,数量却越少。这是我们不手动检查摘要时唯一能得到的信息。

其他参数让我们可以忽略排名或权重低于一定标准的社区,这部分我们在这里不会用到。我们将使用LangChain来实现全局检索器,并像GraphRAG论文里那样使用相同的映射减少提示。因为系统提示很长,这里不会包括这些提示,也不会包含在链的构建中。所有代码都可在notebook里找到。

    def global_retriever(query: str, level: int, response_type: str) -> str:  
        # 全局检索器函数,根据查询、级别和响应类型返回最终的响应。
        community_data = graph.query(  
            """  
            MATCH (c:__Community__)  
            WHERE c.level = $level  
            RETURN c.full_content AS output  
            """,  
            params={"level": level},  
        )  
        intermediate_results = []  
        for community in tqdm(community_data, desc="处理社区"):  
            # 调用映射链,传入问题和上下文数据,获取中间响应
            intermediate_response = map_chain.invoke(  
                {"question": query, "context_data": community["output"]}  
            )  
            intermediate_results.append(intermediate_response)  
        # 调用减少链,传入报告数据、问题和响应类型,获取最终响应
        final_response = reduce_chain.invoke(  
            {  
                "report_data": intermediate_results,  
                "question": query,  
                "response_type": response_type,  
            }  
        )  
        return final_response

我们现在试试看。

print(global_retriever("这段故事到底讲了什么?", 2))

结果

故事主要围绕吝啬鬼埃比尼泽·斯克鲁奇的生活展开,他最初对生活持悲观看法,厌恶圣诞节。他的转变始于他被已故商业伙伴雅各·马利的鬼魂来访,随后是三个代表过去的圣诞精灵、现在的圣诞精灵和未来的圣诞精灵的出现。这些邂逅促使斯克鲁奇反思自己的生活和行为的后果,最终使他拥抱了圣诞节的精神,并经历了显著的个人成长 [数据:报告(32, 17, 99, 86, +更多)]。

雅各·马利和精灵的角色

雅各·马利的鬼魂作为超自然的催化剂角色,警告斯克鲁奇即将有三个精灵来访。每个精灵引导斯克鲁奇经历自我发现之旅,展示他的选择产生的影响并强调了同情心的重要性。精灵向斯克鲁奇揭示了他的行为如何不仅影响了他自己的生活,也影响了他人,特别是强调了赎罪的主题和相互联系 [数据:报告(86, 17, 99, +更多)]。

斯克鲁奇的关系和转变

斯克鲁奇与克里奇特一家的关系,尤其是鲍勃·克里奇特和他的儿子小蒂姆,对他的转变至关重要。通过精灵展现的景象,斯克鲁奇发展了同情心,激励他采取具体行动改善克里奇特一家的生活状况。叙事强调个人行为可以对社会产生深远的影响,斯克鲁奇新发现的慷慨在社区中培养了同情心和社会责任感 [数据:报告(25, 158, 159, +更多)]。

赎罪和希望的主题

整个故事是一个永恒的希望象征,强调了同理心、自我反省和个人改变的潜力。斯克鲁奇从孤独的吝啬鬼到仁慈人物的旅程说明改变永远不会太晚;小小的善举可以产生显著的积极影响,不仅对个人,也对更广泛的社区 [数据:报告(32, 102, 126, 148, 158, 159, +更多)]。

总之,故事体现了圣诞节的变革力量和人类连接的重要性,成为关于赎罪和节日期间一个人如何影响他人的感人的叙述。

响应会相当长且详尽,因为它适用于遍历指定层级上所有社区的全局检索过程。你可以测试一下,如果更改社区层级,响应会如何变化。

这是总结:

在这篇博客文章中,我们演示了如何将微软的GraphRAG集成到Neo4j中,并使用LangChain和LlamaIndex实现检索器。这应该可以让您无缝地将GraphRAG集成到其他检索器或代理中。本地检索器结合了向量相似度搜索和图遍历技术。全局检索器则通过遍历社区摘要来生成全面的响应。这种实现展示了将结构化知识图与语言模型结合起来以增强信息检索和问答能力的强大威力。值得注意的是,这样的知识图在定制和实验上有很大的空间。我们将在下一篇文章中继续探讨这个问题。

代码已上传至 GitHub,可以在这里找到。一如既往,代码可在该链接获取。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消