圖譜增強(qiáng)RAG:用知識(shí)圖譜提升檢索增強(qiáng)生成技術(shù)
检索增强生成 (RAG) 是一种将外部数据源与大型语言模型 (LLMs) 的输出连接的技术。这项技术非常适合让大型语言模型 (LLMs) 访问私有或特定领域的数据,并解决幻觉的问题。因此,RAG 已被广泛应用于许多 GenAI 应用程序,例如 AI 聊天机器人和推荐系统。
通常,一个基本的RAG系统会整合一个向量数据库和一个大模型,其中向量数据库存储和检索关于用户查询的上下文信息,而大模型根据检索到的上下文生成答案。虽然这种方法在很多情况下效果不错,但在处理复杂任务(如多跳推理或需要连接多个信息片段的问题)时,它就显得力不从心了。
举个例子,考虑这个问题:“被打败篡位者阿莱克图斯的人的儿子叫什么名字?”
一般来说,一个基本的RAG回答问题通常会按照以下步骤。
- 找出击败阿莱克图斯的人是谁。
- 研究一下这个人的儿子。
- 找到儿子的名字。
挑战通常出现在第一步,因为基准的RAG是根据语义相似度来检索文本,而不是直接回答复杂的查询,在这些查询中,具体细节在数据集中可能并未明确提到。这种限制使得很难找到所需的具体信息,通常需要昂贵且不实用的解决方案,例如手动创建常见查询的问答对。
为了应对这样的挑战,微软研究院推出了GraphRAG,这是一种全新的方法,通过知识图谱增强RAG检索和生成。接下来我们将介绍GraphRAG的工作原理以及如何利用Milvus向量数据库运行它。
GraphRAG是什么?它是如何工作的?与不像基础型RAG那样使用向量数据库来检索语义相似的文本不同,GraphRAG通过加入知识图谱(KGs)来提升RAG的功能。知识图谱是一种能够存储和链接基于关系的相关或不相关数据的数据结构。它们根据数据之间的关系进行存储和链接。
一般情况下,一个GraphRAG流程通常包含这两个步骤:索引和查询。
GraphRAG流程
_图谱RAG管道(图片来源:GraphRAG论文)_
索引索引流程主要包括四个关键步骤。
- 文本单元分割:整个输入语料被划分为多个文本单元(文本块)。这些块是最小的可分析单元,可以是段落、句子或其他逻辑单元。通过将长文档分割成较小的块,我们可以提取并保留关于输入数据的更详细的信息。
- 实体、关系及声明提取:GraphRAG 使用大型语言模型来识别并提取每个文本单元中的所有实体(人名、地名、组织等实体)、它们之间的关系以及文本中表达的关键声明。我们将使用这些提取的信息来建立初始的知识图谱。
- 层次聚类:GraphRAG 使用 Leiden 技术对初始知识图谱进行层次聚类。Leiden 是一种社区检测算法,能有效识别图中的社群结构。每个聚类中的实体归类到不同的社区,以便进行更深入的分析。
注:社区是指图中的一组节点,这些节点之间高度相连,但与其他密集的子群连接较少。
- 社区概要生成:GraphRAG 使用从下至上的方式为每个社区及其成员生成摘要。摘要会涵盖社区中的重要实体、他们之间的联系以及关键观点。这一步帮助我们了解整个数据集的大致情况,同时也为后续的查询提供了有用的背景信息。
图1:如下所示,使用GPT-4 Turbo生成的知识图。
_(图片来自: Microsoft Research )
查询GraphRAG 具有两种不同的查询方式,适用于不同类型的需求。
这个全球搜索工作流程包括以下几个阶段。
图2-全球搜索数据流程
_图2:全球搜索数据流图(图片来源:微软研究院)_
- 用户查询和对话历史:系统将用户查询和对话历史作为初始信息输入。
- 社区报告批次:系统使用来自指定社区层级的社区报告作为上下文数据。这些社区报告被打乱并分成多个批次(打乱社区报告批次1,批次2……批次N)。
- 评分中间响应(Rated Intermediate Responses):每个社区报告批次进一步划分为预定义大小的文本块。每个文本块生成一个中间响应。响应包含一系列称为要点的信息片段。每个要点都有一个数字评分,表示其重要性。这些生成的中间响应被称为评分中间响应(评分中间响应1,响应2……响应N)。
- 排序与筛选:系统对这些中间响应进行排序与筛选,选择最重要的要点。所选的要点汇总为聚合中间响应。
- 最终回复:聚合中间响应用作上下文生成最终回复。
当用户询问有关如人名、地名、组织等的具体实体的问题时,我们建议您使用本地搜索。具体步骤如下:
图3:本地搜索数据流程
_图3:局部搜索数据流程(图片来源:Microsoft Research )
- 用户查询:系统首先接收用户的查询,这可以是一个简单的问题或更为复杂的查询。
- 相似实体搜索:系统从知识图谱中识别出一组与用户输入语义相关的实体。这些实体作为进入知识图谱的入口。此步骤使用向量数据库如Milvus进行text相似性搜索。
- 实体-文本单元映射:提取的文本单元被映射到相应的实体,去除了原来的文本信息。
- 实体-关系提取:此步骤提取实体及其相应关系的具体的信息。
- 实体-协变量映射:此步骤将实体映射到其协变量,可能包括统计数据和其他相关属性。
- 实体-社区报告映射:社区报告被整合到搜索结果中,包含一些全局性的信息。
- 利用对话历史:如果有,系统使用对话历史更好地理解用户的意图和上下文。
- 响应生成:最后,系统基于之前步骤过滤和排序后的数据构建并回复用户的查询。
在他们的公告博客中,GraphRAG的创建者比较了基线RAG和GraphRAG的表现,并举了一个简单的例子。
使用的数据集GraphRAG的创建者使用了来自新闻文章的暴力事件相关资讯(来自 VIINA 数据集)来进行他们的实验。
注意:此数据集包含敏感内容,因其复杂性以及包含不同观点和不完整信息的特性而被选中。它足够新,以至于没有被包含在LLM基础模型的训练数据中。
实验简介基线RAG和GraphRAG被问了同一个问题,这个问题需要整合数据集中的信息来生成答案。
Q: 数据集里前五大主题是什么?
答案如图所示。基线RAG的结果与战争主题无关,因为向量搜索返回了不相关的信息,导致评估不准确。相比之下,GraphRAG提供了清晰相关的结果,识别了主要主题及支持细节。结果与数据集一致,引用了相关来源。
图4- 基线RAG与GraphRAG在回答复杂问题上的表现对比
如图4所示:BaselineRAG与GraphRAG在回答复杂的总结性问题时的表现
在论文“从局部到全局:面向查询聚焦摘要的图RAG方法一文”[1]中,名为GraphRAG的模型的进一步实验表明,该模型在多步推理和复杂信息摘要方面有显著改进。研究显示,GraphRAG在全面性和多样性方面均优于基线RAG。
- 覆盖面:答案是否全面地覆盖了所有问题的方面。
- 多元性:答案提供的不同视角和见解的丰富性。
我们建议你阅读原始的GraphRAG论文,了解更多关于这些实验的细节。
如何用向量数据库Milvus实现GraphRAGGraphRAG 使用知识图谱来增强 RAG 应用,并依赖向量数据库来检索相关实体。本节将展示如何实现 GraphRAG、创建 GraphRAG 索引以及通过 Milvus 向量数据库进行查询。
前提条件在运行这篇博客中的代码之前,请确保已安装以下依赖库。
命令如下:
pip install --upgrade pymilvus
从GitHub安装graphrag的命令如下:
pip install git+https://github.com/zc277584121/graphrag.git
注:因为目前Milvus的存储功能还没有被官方合并,我们从一个分叉的仓库里安装了GraphRAG.
我们先来看看索引工作流程。
准备数据下载一个约有一千行的文本文件(来自Project Gutenberg),并用它来进行GraphRAG索引。
这个数据集是关于列奥纳多·达·芬奇的资料。我们使用GraphRAG来构建与达·芬奇相关的所有关系的图索引关系,并使用Milvus向量数据库来搜索相关知识来回答问题。
```python
import nest_asyncio # 导入nest_asyncio模块
nest_asyncio.apply() # 应用nest_asyncio
import os
import urllib.request
index_root = os.path.join(os.getcwd(), 'graphrag_index')
os.makedirs(os.path.join(index_root, 'input'), exist_ok=True)
url = "https://www.gutenberg.org/cache/epub/7785/pg7785.txt"
file_path = os.path.join(index_root, 'input', 'davinci.txt')
urllib.request.urlretrieve(url, file_path)
with open(file_path, 'r+', encoding='utf-8') as file:
# 我们只使用前934行,因为之后的内容与这个例子无关。如果你要节省API调用费用,可以将文件截短一些。
lines = file.readlines()
file.seek(0)
file.writelines(lines[:934]) # 如果你想节省API密钥费用,可以减少这个数字。
file.truncate()
初始化工作空间
现在,我们来用GraphRAG给文本文件做索引。首先,我们运行graphrag.index --init
命令来初始化工作空间。
运行以下命令来初始化位于./graphrag_index
目录下的graphrag索引:python -m graphrag.index --init --root ./graphrag_index
。
env
文件和参数
你可以在项目的根目录中找到.env
文件。使用它时,添加你的OpenAI API密钥。
注意要点:
- 我们将在这个示例中使用OpenAI模型;请确保你已经准备好了API密钥。
- GraphRAG 索引成本较高,因为它会用大型语言模型处理整个文本语料库。运行此演示可能会花费几美元。为了节省费用,可以考虑将文本文件截短一些。
索引过程可能需要一些时间。一旦完成,您将在 ./graphrag_index/output/<timestamp>/
找到包含一系列 parquet 文件的新生成的文件夹。
运行此命令来创建或更新一个名为 `graphrag_index` 的索引目录:
python -m graphrag.index --root ./graphrag_index
使用Milvus向量数据库进行查询
在查询阶段中,我们使用Milvus存储用于GraphRAG 本地搜索的实体描述嵌入。这种方法结合了知识图谱中的结构化数据和输入文档中的非结构化数据,通过相关实体信息增强大语言模型的上下文理解,从而提供更精确的答案。
导入 os 模块
import pandas as pd
import tiktokenfrom graphrag.query.context_builder.entity_extraction import EntityVectorStoreKey
from graphrag.query.indexer_adapters import (
# 读取索引协变量(已注释),
read_indexer_entities,
read_indexer_relationships,
read_indexer_reports,
read_indexer_text_units,
)
from graphrag.query.input.loaders.dfs import (
store_entity_semantic_embeddings,
)
from graphrag.query.llm.oai.chat_openai import ChatOpenAI
from graphrag.query.llm.oai.embedding import OpenAIEmbedding
from graphrag.query.llm.oai.typing import OpenaiApiType
from graphrag.query.question_gen.local_gen import LocalQuestionGen
from graphrag.query.structured_search.local_search.mixed_context import (
LocalSearchMixedContext,
)
from graphrag.query.structured_search.local_search.search import LocalSearch
from graphrag.vector_stores import MilvusVectorStore
output_dir = os.path.join(index_root, "output")
subdirs = [os.path.join(output_dir, d) for d in os.listdir(output_dir)]
latest_subdir = max(subdirs, key=os.path.getmtime) # 获取最新输出目录
INPUT_DIR = os.path.join(latest_subdir, "artifacts")
社区报告表 = "create_final_community_reports"
实体表 = "create_final_nodes"
实体向量表 = "create_final_entities"
关系数据表 = "create_final_relationships"
协变量表 = "create_final_covariates"
文本单元表 = "create_final_text_units"
社区层级:2
从索引过程加载索引数据
在进行索引时,将会生成几个 Parquet files。并将其加载到内存中,并将实体的信息描述存储到 Milvus 向量数据库。
查看实体,
# 读取节点表来获取节点的社区和度数据。
entity_df = pd.read_parquet(f"{INPUT_DIR}/{ENTITY_TABLE}.parquet")
# 读取实体嵌入表来获取实体的嵌入数据。
entity_embedding_df = pd.read_parquet(f"{INPUT_DIR}/{ENTITY_EMBEDDING_TABLE}.parquet")
entities = read_indexer_entities(entity_df, entity_embedding_df, COMMUNITY_LEVEL)
description_embedding_store = MilvusVectorStore(
collection_name="entity_description_embeddings"
)
# description_embedding_store.connect(uri="http://localhost:19530") # 适用于 Milvus Docker 服务
description_embedding_store.connect(uri="./milvus.db") # 适用于 Milvus Lite 版本
entity_description_embeddings = store_entity_semantic_embeddings(
entities=entities, vectorstore=description_embedding_store
)
print("实体数量: ", len(entity_df))
entity_df.head()
实体数量:651
图5:实体的屏幕截图
图5:实体截图
阅读人际关系
# 读取关系数据
relationship_df = pd.read_parquet(f"{INPUT_DIR}/{RELATIONSHIP_TABLE}.parquet")
# 读取索引关系
relationships = read_indexer_relationships(relationship_df)
print(f"关系条目数:{len(relationship_df)}")
relationship_df.head() # 显示前几行数据
关系数:二百九十
图6 - 关系截图
图6:关系截图
查看社区报告
report_df = pd.read_parquet(f"{INPUT_DIR}/{COMMUNITY_REPORT_TABLE}.parquet") # 下边代码读取parquet文件
报告数据框 = read_indexer_reports(report_df, entity_df, COMMUNITY_LEVEL) # 读取索引器报告,输入包括报告数据框、实体数据框和社区级别
print(f"报告记录数:{report_df的长度}")
report_df.head()
记录数:45
图7-,报告记录截图
图7:报告内容截图
阅读文本片段
# 读取文本单元的Parquet文件到数据框
text_unit_df = pd.read_parquet(f"{INPUT_DIR}/{TEXT_UNIT_TABLE}.parquet")
# 从数据框中读取文本单元
text_units = read_indexer_text_units(text_unit_df)
print(f"文本单元记录:{len(text_unit_df)}")
text_unit_df.head() # 显示前几行数据
共有文本记录51条
图8所示,截图显示了文本单元记录。
如图所示,图8:文本单元记录的截图
创建一个本地的搜索引擎现在,已经准备好了用于本地搜索引擎的必要数据。可以用这些数据、一个大语言模型(LLM)和一个嵌入模型来创建一个LocalSearch
实例。
我们已经准备好了用于本地搜索服务的必要数据。现在,我们可以用这些数据、一个大语言模型和一个嵌入模型来创建一个 LocalSearch
实例。
api_key = os.environ["OPENAI_API_KEY"] # 环境变量中的 OpenAI API 密钥
llm_model = "gpt-4o" # 或 gpt-4-turbo-preview 模型
embedding_model = "text-embedding-3-small"
llm = ChatOpenAI(
api_key=api_key,
model=llm_model,
api_type=OpenaiApiType.OpenAI,
max_retries=20,
)token_encoder = tiktoken.get_encoding("cl100k_base")text_embedder = OpenAIEmbedding(
api_key=api_key,
api_base=None,
api_type=OpenaiApiType.OpenAI,
model=embedding_model,
deployment_name=embedding_model,
max_retries=20,
)
context_builder = LocalSearchMixedContext(
community_reports=reports,
text_units=text_units,
entities=entities,
relationships=relationships,
covariates=None, #covariates, 待办事项
entity_text_embeddings=实体嵌入,
embedding_vectorstore_key=EntityVectorStoreKey.ID, # 如果向量存储使用实体标题作为ID,就把它设为EntityVectorStoreKey.TITLE
text_embedder=text_embedder,
token_encoder=token_encoder,
)
local_context_params = {
"text_unit_prop": 0.5,
"community_prop": 0.1,
"conversation_history_max_turns": 5,
"conversation_history_user_turns_only": True,
"top_k_mapped_entities": 10,
"top_k_relationships": 10,
"include_entity_rank": True,
"include_relationship_weight": True,
"include_community_rank": False,
"return_candidate_context": False,
"embedding_vectorstore_key": EntityVectorStoreKey.ID, # 设置为 EntityVectorStoreKey.TITLE 如果向量存储使用实体的名称作为 ID
"max_tokens": 12_000, # 根据你的模型的标记限制调整此值(如果你使用的是 8k 限制的模型,一个好的设置可能是 5000)
}
llm_params = {
"max_tokens": 2_000, # 根据您模型的 token 限制来调整此值(如果您使用的是具有 8k 限制的模型,一个合适的范围可能是 1000 到 1500)
"temperature": 0.0,
}
search_engine = LocalSearch(
llm=llm,
context_builder=context_builder,
token_encoder=token_encoder,
llm_params=llm_params,
context_builder_params=local_context_params,
response_type="多个自然段", # 此处填写返回内容的类型和格式,如优先级列表、单段落、多个自然段、多页报告等。
)
发起查询。
result = await search_engine.asearch("告诉我列奥纳多·达·芬奇吧")
print(result.response)
# 列奥纳多·达·芬奇
列奥纳多·达·芬奇,1452年出生于佛罗伦萨附近的芬奇镇,被誉为意大利文艺复兴时期最博学的天才之一。他的全名是莱昂纳多·迪·瑟·皮耶罗·德·安东尼奥·迪·瑟·皮耶罗·迪·瑟·吉多·达·芬奇,是乡村公证人瑟·皮耶罗的自然出生且首位的儿子。他的贡献涵盖了艺术、科学、工程和哲学等多个领域,因此他被誉为基督教时代最博学的天才 [数据:实体 (8)]。
## 早年生活和学习
达·芬奇的天赋很早就被父亲认出,父亲将他的画作带到著名的画家和雕塑家安德烈·德尔·韦罗基奥那里。韦罗基奥被达·芬奇的天赋所打动,于1469年左右接纳他进入自己的工作室,在这里,达·芬奇遇到其他知名艺术家,包括波提切利和洛伦佐·迪·克雷迪 [数据:来源 (6, 7)]。1472年,达·芬奇被接纳为佛罗伦萨画家公会的成员,标志着他职业生涯的开始 [数据:来源 (7)]。
## 艺术杰作
达·芬奇最著名的作品包括《蒙娜丽莎》和《最后的晚餐》。《蒙娜丽莎》以其微妙的表情和细致的背景而闻名,现藏于卢浮宫,是世界上最著名的艺术品之一 [数据:关系 (0, 45)]。《最后的晚餐》描绘了耶稣宣布其中一位门徒将会背叛他的时刻,位于米兰圣玛利亚感恩修道院食堂内 [数据:来源 (2)]。其他重要的作品还包括《岩间圣母》和《绘画论》,此书创作始于1489-1490年 [数据:关系 (7, 12)]。
## 科学与工程贡献
达·芬奇的天才不仅表现在艺术领域,还延伸到科学与工程领域。他在解剖学、光学和水力学等领域进行了重要观察,笔记中充满了许多现代发明的草图和想法。例如,他预见了哥白尼关于地球运动的理论和拉马克对动物的分类 [数据:关系 (38, 39)]。他对光影规律的研究及其在明暗对比方面的掌握,在艺术和科学领域产生了深远影响 [数据:来源 (45)]。
## 赞助关系与职业关系
达·芬奇的职业生涯深受赞助人的影响。米兰公爵洛多维科·斯福尔扎雇佣他作为宫廷画家和总工程师,委托了各种作品,甚至在1499年赠送给他一片葡萄园 [数据:关系 (9, 19, 84)]。后来,达·芬奇受法国国王弗朗索一世的赞助移居法国,国王为他提供王子级的收入,并对他非常重视 [数据:关系 (114, 37)]。达·芬奇在昂布瓦兹附近克卢庄园的晚年生活中,常受国王接见,并由其亲密朋友和助手弗朗切斯科·梅尔齐支持 [数据:关系 (28, 122)]。
## 遗产与影响
列奥纳多·达·芬奇的影响远远超出了他的时代。他在米兰创立了一所绘画学校,他的技巧和教学被他的学生和追随者,如乔万尼·阿姆布罗焦·达·普雷迪和弗朗切斯科·梅尔齐所继承和发扬 [数据:关系 (6, 15, 28)]。他所创作的作品仍然被庆祝和研究,确立了他作为文艺复兴时期最伟大的大师之一的遗产。他将艺术与科学相结合的能力,在艺术和科学领域留下了不可磨灭的印记,激励了无数代艺术家和科学家 [数据:实体 (148, 86);关系 (27, 12)]。
总之,列奥纳多·达·芬奇在艺术、科学和工程领域的无与伦比的贡献,以及他的创新思维和对同时代人以及后代的影响,使他成为人类成就史上的巨人。他的遗产继续激发着人们的敬仰和研究,彰显了他天赋的永恒相关性。
GraphRAG的结果是明确具体的,引用的数据来源也明确标注。
问题生成GraphRAG还可以基于历史查询生成问题,这对于聊天机器人对话中推荐问题的生成很有帮助。该方法将知识图谱中的结构化数据与输入文档中的非结构化数据结合起来,生成与特定实体相关的候选问题。
# 定义了一个本地问题生成器
question_generator = LocalQuestionGen(
# llm 参数指的是...
llm=llm,
# context_builder 参数指的是...
context_builder=context_builder,
# token_encoder 参数指的是...
token_encoder=token_encoder,
# llm_params 参数指的是...
llm_params=llm_params,
# context_builder_params 参数指的是...
context_builder_params=local_context_params,
)
question_history = [
"告诉我关于列奥纳多·达·芬奇的一些事情,",
"列奥纳多早期的作品有哪些,",
]
根据历史生成问题。
candidate_questions = await question_generator.agenerate(
question_history=问题历史, context_data=None, question_count=问题数量
)
候选问题的响应
["列奥纳多·达·芬奇的一些早期作品有哪些,现在存放在哪里?",
"列奥纳多·达·芬奇与安德烈亚·德尔·韦罗基奥的关系是如何影响了他的早期作品?",
'列奥纳多·达·芬奇在米兰期间曾承担哪些重要项目?',
"列奥纳多·达·芬奇的工程技术技能如何为他的项目做出贡献?",
"列奥纳多·达·芬奇与法王弗朗索瓦一世的关系有何重要性?"]
如果你想删除索引来节省空间,你可以删除索引的根目录。
# 导入shutil
#
# shutil.rmtree(index_root)
摘要.
在这篇博客里,我们讨论了GraphRAG,这是一种通过整合知识图谱来增强RAG技术的创新方法。GraphRAG特别适合处理多步推理,回答那些需要关联不同信息的复杂问题。
当与Milvus向量数据库结合时,GraphRAG 能够在大规模数据集中探索复杂的语义关系,提供更准确且具有洞察力的结果。这样的强大组合使得 GraphRAG 成为各种实际GenAI应用中的宝贵资产,为理解和处理复杂信息提供了一个强大的解决方案。
更多- GraphRAG论文:从局部到全局:Graph RAG在查询聚焦摘要方面的研究
- GraphRAG GitHub:https://github.com/microsoft/graphrag
- 其他RAG增强技术:
- 使用HyDE改进RAG——假设文档
- 使用WhyHow增强知识图谱中的RAG
- 如何提高RAG管道的性能
- 使用重排序器优化RAG:角色与权衡
- 构建RAG应用程序的实用技巧和小贴士
- 使用自定义AI模型扩展RAG时面临的基础设施挑战
共同學(xué)習(xí),寫下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章