GraphRAG 是一种基于图的检索增强方法,由微软开发并开源。它通过结合LLM和图机器学习的技术,从非结构化的文本中提取结构化的数据,构建知识图谱,以支持问答、摘要等多种应用场景。GraphRAG的特色在于利用图机器学习算法进行语意聚合和层次化分析,从而能够回答一些高层次的抽象或总结性问题,这是常规RAG系统的短板。
GraphRAG的核心在于其处理流程,包含2个阶段:Indexing和Querying。
相对于传统的RAG方法,在处理私有数据集时展现出显著的性能提升。它通过构建知识图谱和社区分层,以及利用图机器学习技术,增强了对复杂信息的问答性能,尤其是在需要全面理解大型数据集或单一大型文档的语意概念时。
基于微软官方发布的博客来看[1],微软采用了一个LLM评分器来给GraphRAG和Baseline RAG的表现进行评估。基于一系列的指标,包括全面性(提取的上下文内容的完整性,包括隐含的信息)、人类赋权(提供来源的材料或其他上下文信息)和多样性(提供不同的视角或问题的角度)。从初步评测结果来看,GraphRAG在这些指标上都优于Baseline RAG。
除此之外,微软还实用了SelfCheckGPT来进行忠实度的绝对测量,来确保基于原材料的事实性和连贯性结果。结果也显示出GraphRAG在忠诚度上达到了与Baseline RAG类似的水平。目前微软也在开发一个评估框架,用来进一步衡量上述问题类型的性能。
需要注意的是,默认情况下,GraphRAG使用的是OpenAI的模型。如果要使用AWS Bedrock提供的模型,建议可以使用亚马逊云科技提供的Bedrock Access Gateway方案[2]。
下面是部署流程
# 创建conda环境 conda create -n grag python=3.10 conda activate grag # 安装graphrag pip3 install graphrag # 设置环境 mkdir gragdemo cd gragdemo/ # 初始化环境,创建 .env 和 settings.yaml文件以及默认的配置项 python3 -m graphrag.index --init --root ./ragtest # 基于使用的llm配置 settings.yaml 文件以及.env文件 # 加载示例数据 mkdir -p ./ragtest/input curl https://www.gutenberg.org/cache/epub/24022/pg24022.txt > ragtest/input/book.txt # 生成index python -m graphrag.index --root ./ragtest # 对应Indexing部分输出为 ⠴ GraphRAG Indexer ├── Loading Input (text) - 1 files loaded ├── create_base_text_units ├── create_base_extracted_entities ├── create_summarized_entities ├── create_base_entity_graph ├── create_final_entities ├── create_final_nodes ├── create_final_communities ├── join_text_units_to_entity_ids ├── create_final_relationships ├── join_text_units_to_relationship_ids ├── create_final_community_reports ├── create_final_text_units ├── create_base_documents └── create_final_documents 🚀 All workflows completed successfully. # 执行查询 python -m graphrag.query \ --root ./ragtest \ --method local \ "Who is Scrooge, and what are his main relationships?" # 查询返回 SUCCESS: Local Search Response: # Ebenezer Scrooge and His Key Relationships … ## Ebenezer Scrooge: The Miserly Central Character … ## Scrooge's Past Relationship with Belle … ## Scrooge's Deceased Business Partner: Jacob Marley … ## Scrooge's Relationship with Bob Cratchit and His Family … ## Scrooge's Nephew and Niece … Throughout the story, Ebenezer Scrooge's relationships with these characters serve as catalysts for his personal growth and redemption, as he is confronted with the consequences of his actions and the importance of kindness, generosity, and embracing the Christmas spirit. |
由于查询返回的内容较多,所以并未全部贴出来。从回复的框架来看,在GraphRAG下,对人物的人际关系有了更全面的掌握,这是标准RAG下非常难以实现的效果。
GraphRAG的执行过程(例如前面看到的Indexing过程打印出了很多个步骤)是由一个data pipeline组成,目标是将非结构化的数据经过LLM处理,抽取出有意义的信息,并保存为结构化的数据。在Indexing流程执行完毕后,可以在 output/xxx/artifacts/ 目录下看到一组parquet文件,用来存储提取出来(或者经由LLM处理过后的)有意义的信息。
在前面对文档做Indexing过程时,我们可以看到有一系列的过程,包括:
├── Loading Input (text) - 1 files loaded
├── create_base_text_units
├── create_base_extracted_entities
├── create_summarized_entities
├── create_base_entity_graph
├── create_final_entities
├── create_final_nodes
├── create_final_communities
├── join_text_units_to_entity_ids
├── create_final_relationships
├── join_text_units_to_relationship_ids
├── create_final_community_reports
├── create_final_text_units
├── create_base_documents
└── create_final_documents
GraphRAG的Indexing过程由一组workflow、task、prompt template 以及input/output adapters组成。在上述测试中的默认标准pipeline为:
下面我们结合官方文档[3],梳理整个Indexing的默认workflow流程,一共分为6个阶段。
Workflow的第1个阶段是将输入文档处理并转为TextUnits。TextUnit是在做graph extraction技术的基本单元(也就是一个chunk),也是作为源引用的基本单元。
Chunk size由用户指定,默认为300(overlap默认100)。使用更大的chunk size可以加快处理速度(另一方面,经由验证,改为1200的chunk size,可以得到更positive的结果),但是也会导致较低保真度的输出和较少的有意义的参考文本。
默认为1个document对应多个chunks的关系,但也可以配置多个documents对应多个chunks的关系(适用于document非常短,需要多个文档组成一个有意义的分析单元的场景,例如聊天记录或者推文)。
对于每个text-unit,都会经过text-embedding的操作,并传递给处理pipeline的下一个阶段。
第一阶段的流程如下所示:
前面提到,在构建完索引后,可以在output的artifacts中找到生成的parquet结构化数据文件。其中即包含create_base_text_units.parquet。对应内容为:
可以看到id即为chunk_id。document_ids即为原始document的id。Chunk为切分的原始内容。
在这个阶段,会对text unit进行分析,并提取出组成graph的基本单元:Entities(实体)、Relationships(关系)和Claims(声明)。
流程图为:
Entities & Relationship提取
在第一步Graph Extraction中,我们使用LLM抽取text unit中的实体和关系。输出为每个text unit的sub-graph,包含一组entities及其对应的名称、类型和描述;以及一组relationships及其源、目标和描述。
然后,将这些sub-graph进行合并。任何具有相同名称和类型的entity视为同一个entity,并将对应的描述内容合并为数组。同样,任何具有相同源和目标的relationship视为同一个relationship,并将其描述合并为数组。
Entity & Relationship Summarization
现在我们有了一个实体和关系的图,每个实体和关系都有一个描述列表,我们可以将这些列表总结为每个实体和关系的单个描述(通过 LLM 对所有描述做摘要)。这样,所有的实体和关系都可以有一个简洁的描述。
Entity解析(默认未启用)
图提取的最后一步是解析“在同一个世界里(或者空间里),不同名称的实体,但实际是同一个实体的情况”,通过LLM完成。目前正在探索其他实体解析技术,以希望达到更保守的、非破坏性的方法。
Claim Extraction & Emission
最后,从原始text unit中提取claim。这些claims代表具有评估状态和时间范围的积极事实陈述,保存为Covariates(包含关于实体的陈述,这些陈述可能是time-bound)并输出
原始entities & relationships抽取后生成的对应文件为create_base_extracted_entities.parquet文件。可以看到文件内容只有1行,对应的是entity_graph,可以看到其是一个GraphML格式的图表示。
既然是GraphML的格式,那就表示我们可以将其可视化,对应的可视化图(使用Gephi生成,输出的文件可能不是规整文件,需要二次处理):
从抽取的实体关系来看,可以看到关键人物的实体,例如实体BOB(ID与LABEL均为BOB),类型为”PERSON”(其描述为一个列表,包含了各个相关chunk中对BOB的描述),与之有关系的实体包括TINY TIM、PERTER、BOB’s CHILD等。另一方面,我们也可以看到一些相对来说没有太大意义的实体,例如WEATHER、FIRE、HIM、SUMMER、GAIN等,这些实体对应的类型和描述均为None,且source chunk也仅有1个。
除了节点外,各个边也有相应的描述,例如BOB和PETER之间的关系描述(Bob is Peter’s father...):
在原始图构建出来后,会再次进行加工,生成summarized_entities图。通过对比同一个实体BOB来看。在原始entity图中对BOB的描述为多段,通过空格分隔。在summarized_entities图中,对BOB的描述为1段总结描述。对“边”的描述也是同样的处理方式。
现在有了一个可用的实体和关系图,接下来希望了解它们的社区结构,并用附加信息来增强这个图。这分为两个步骤进行:Community Detection和Graph Embedding。使得我们能够以显式(community)和隐式(embedding)的方式理解图的拓扑结构。
Community Detection
在此步骤中,使用Hierarchical Leiden 算法生成实体社区的层次结构。此方法将对我们的图进行递归社区聚类,直到达到社区大小阈值。这使我们能够理解图的社区结构,并提供一种在不同粒度级别上导航和总结图的方法。
Graph Embedding
在此步骤中,使用 Node2Vec 算法生成图的向量表示。这将使我们能够理解图的隐式结构,并在查询阶段提供一个额外的向量空间,用于搜索相关概念。
Graph Tables Emission
一旦图增强步骤完成,经过文本嵌入后(Entity描述做embedding写入向量数据库),生成最终的Entities表和Relationships表。
在这个workflow中,我们知道首先会做社区检测,对应的结果会保存在create_final_communities.parquet文件中,下面是文件部分内容:
可以看到每行是一个社区的信息,包括所属层级,所包含的关系id以及text unit id。通过pandas进行分析可以发现,一共生成了67个社区,社区层级划分为4个层级。
上文也提到,这个流程最终会生成Entities表和Relationships表的数据,对应的文件分别为create_final_entities.parquet 和 create_final_relationships.parquet。
最终Entities表的内容为:
可以看到对每个实体,都标注了其类型(例如上图的BOB实体类型为PERSON),描述,以及描述对应的embedding向量(会存入向量数据库,默认为lancedb)。
对应的relationship存储结构化后的关系表示:
此时,我们已经拥有了实体和关系的功能性图谱、实体的社区层次结构以及 node2vec 生成图的嵌入式表达。
现在我们要基于社区数据生成每个社区的报告。这使我们能够在多个粒度上对图进行高层次理解。例如,如果社区 A 是顶级社区,我们将获得关于整个图的报告。如果社区是低级社区,我们将获得关于本地集群的报告。
生成社区报告
在这个步骤中,使用 LLM 为每个社区生成摘要。这使我们能够理解每个社区包含的独特信息,并从高层次或低层次的角度提供对图的范围理解。这些报告包含一个概述,以及引用社区子结构中的关键实体、关系和声明。
总结社区报告
在此步骤中,每个社区报告通过 LLM 进行总结,进行简化。
Community Embedding
在此步骤中,我们通过生成社区报告、社区报告摘要和社区报告标题这三段文本的文本嵌入,生成社区的向量表示。
社区表生成
在此时,进行一些记录工作并生成Communities和CommunityReports两张表。
社区总结后的数据会写入create_final_comunity_reports.parquet文件:
可以看到,在表中记录了社区的描述、总结,以及对其做的重要性评级与原因。
这阶段为知识模型创建Documents表。
增加列(仅限 CSV 数据)
如果处理的是 CSV 数据,可以配置工作流向文档输出添加额外字段。这些字段应存在于输入的 CSV 表中。
链接到Unit Text
在此步骤中,将每个文档与第一阶段创建的text-unit链接。这使我们能够理解哪些document与哪些text-unit相关,反之亦然。
文档嵌入
在此步骤中,通过文档片段的平均嵌入生成文档的向量表示。首先对文档进行re-chunk,且不使用overlap,然后为每个chunk生成embedding。然后创建这些chunk的加权平均值(根据 token 数量加权)并将作为document embedding。这可以帮助我们理解文档之间的隐含关系,并帮助我们生成文档的网络表示。
文档表输出
将文档表输出到知识模型中。
这个步骤会输出create_final_documents.parquet文件,内容为:
由于我们只有1个文档,所以仅有1行。
在这个阶段,执行一些步骤以支持现有图中高维向量空间的网络可视化。此时,有两个逻辑图在发挥作用:实体关系图和文档图。
网络可视化工作流
对于每个逻辑图,我们进行 UMAP 降维以生成图的二维表示。这将使我们能够在二维空间中可视化图,并理解图中节点之间的关系。UMAP embedding的结果会作为Nodes表输出。该表的行包括一个指示符,表明节点是文档还是实体,以及 UMAP 坐标。
这部分数据会生成create_final_nodes.parquet文件。从文件内容来看,每个节点是一个实体信息,以及对应的社区、degree、源id等。但是graph_embedding与x、y坐标均为空或0。
从上面的介绍,我们了解到在构建索引的过程中,GraphRAG会生成实体关系图、社区层级结构,以及它们的sumamry、source chunk等各种维度的信息,以向量和结构化的方式进行存储。下面我们介绍在检索时如何使用这些信息来做信息增强。
Query分两种类型,分别为Local Search和Global Search。
Local Search是一种基于Entity的回答模式。它结合知识图谱中的结构化数据和输入文档中的非结构化数据,在查询时通过相关实体信息扩展 LLM 上下文。该方法非常适合回答需要理解输入文档中提到的具体实体的问题(例如,“洋甘菊的治疗属性是什么?”)。
其流程图如下所示:
给定用户查询(或加上对话历史记录),Local Search会从知识图谱中识别出一组与用户输入在语义上相关的实体。这些实体作为进入知识图谱的入口点,能够提取进一步相关的细节,如相连实体、关系、实体协变量(与实体相关的变量)和社区报告。此外,它还从原始输入文档中提取与已识别实体相关的相关文本chunk。然后对这些候选数据源进行优先级排序和过滤,以适应预定义大小的单个上下文窗口,该窗口用于生成对用户查询的响应。
Global Search是基于整个数据集的推理。常规RAG在处理需要跨数据集聚合信息后进行组合回答的场景时很难有很好的表现。例如,“数据中的top 5主题是什么?”这种问题查询效果会很差,因为常规RAG的处理方式是:依赖于数据集中存在语意相似的文本内容的向量检索。如果知识库中没有文本内容包含这个问题的答案,则无法给出高质量的回答。
然而,使用 GraphRAG 可以回答此类问题,因为 LLM 生成的知识图谱的结构可以告诉我们整个数据集的结构(以及主题)。这使得私有数据集能够组织成有意义的语义clusters,并且这些clusters已被预先总结。通过使用我们的全局搜索方法,LLM 在响应用户查询时可以使用这些cluster来总结这些主题,并回答用户对整个数据集的问题。
其流程图如下所示:
给定用户查询(或加上对话历史记录),Global Search使用从图的社区层次结构中指定级别生成的一系列 LLM 社区报告作为上下文数据,以 map-reduce 方式生成响应。在 map 步骤中,社区报告被分割成预定义大小的文本chunks。每个文本chunk然后用于生成一个中间响应,其中包含一个要点列表,每个要点都附有一个表示该要点重要性的数值评级。在 reduce 步骤中,从中间响应中筛选出最重要的要点进行汇总,并将其用作上下文生成最终响应。
全局搜索响应的质量会受到用于社区报告来源的社区层次结构级别的显著影响。较低的层次级别报告更为详细,通常会产生更为全面的响应,但由于报告数量的增加,这也可能增加生成最终响应所需的时间和 LLM 资源。
从初步使用以及对Indexing与Querying的流程了解,可以看到GraphRAG的优点非常明显:
另一方面,其缺点也非常明显:
除此之外,还一个可能的缺点是“指代消解”的情况。从上面我们看到的实体关系图中,可以看到例如HIM、GAIN这类指示意义不明的词,也会存在例如同一实体的不同名称可能成为单独实体节点的情况。从而埋下信息不准确的隐患。
总的来说,笔者仍然认同GraphRAG是一个非常强大的工具,它可以提高从非结构化数据中提取Insight的能力,弥补现有RAG模式的不足。但它在耗时、成本、扩展性等方面仍会是在应用到生产环境中的几大挑战点。
最后,在测试阶段我们使用了默认给的英文数据集。下一步我们使用中文语料构建一个GraphRAG场景,并结合代码的方式再深入介绍检索流程。
[1] GraphRAG: Unlocking LLM discovery on narrative private data: https://www.microsoft.com/en-us/research/blog/graphrag-unlocking-llm-discovery-on-narrative-private-data/
[2] Bedrock Access Gateway: https://github.com/aws-samples/bedrock-access-gateway?tab=readme-ov-file
[3] Indexing Dataflow: https://microsoft.github.io/graphrag/posts/index/1-default_dataflow/