什么是GraphRAG?你认为GraphRAG意味着什么?如果你能有一个标准的RAG和一个GraphRAG作为组合套装,只需切换查询,会怎么样?
实际上,GraphRAG的具体定义还没有被普遍接受——至少目前还没有。根据我的经验、查阅文献以及和很多人交流,据我估计(向史蒂文·D·莱维特致歉,我知道这不是展示统计数据的正确方法):
- 90% 的人将 GraphRAG 与微软构建图的方法(或其变体)及在此基础上进行搜索联系在一起。
- 8% 的人将 GraphRAG 定义为使用 LLM 生成的 Cypher 查询或将文本转换为任意图语言(如 Cypher 或 SPARQL)来查询 LPG(带标签属性图)或 RDF(资源描述框架)图。
- 剩余的 2% 则要么不确定,要么在探索不同的可能性。
就我个人而言,我对前两个定义都不完全信服,想说说为什么。
首先,我要说微软的GraphRAG确实很是一个非常酷的想法。估计五年左右后,它可能会变得广为人知,甚至可能成为GraphRAG方法中的首选。
然而,今天它仍然过于昂贵且不切实际,无法大规模应用于工业生产。事实是,大多数公司没有足够的时间、预算和信心来采用这种方法。相反,他们更可能选择一个标准的“vanilla”向量数据库,这在当前条件下更为可行。信心——因为实际上还没有成千上万个GrapRAG在实际应用中被广泛采用(可能是因为上述原因)。
在我看来,将文本转换为Cypher或SPARQL的技巧或方法是一种很好的Microsoft GraphRAG(尽管它们也可以一起使用)的替代方案,我见过一些很好的应用实例。然而,这种方法也有一些缺点。首先,生成查询需要大量的LLM调用,这会带来较高的费用。其次,你和知识库之间始终存在一层不确定性——这取决于你如何编写提示,以及所选模型的性能,以及它如何构建Cypher或SPARQL查询。此外,额外的处理步骤会增加响应时间,而较高的实现复杂度也会带来挑战。总的来说,这种技术对于某些应用来说非常有前景且强大,但其适用性仍然取决于具体的应用场景。
2. 效率优化的难题作为顾问和GenAI解决方案开发者,我的目标是帮助GraphRAG从小型项目到大型企业级解决方案,无论大小。
扩大规模往往伴随着妥协,特别是在准确性和效率上。然而,如果一个低成本且高效的解决方案仍然能提供满意的结果,那么将其留在工具箱里也是值得的,不是吗?
基于这一点,所提出的方法是利用图的力量来进行RAG(检索增强生成),同时避免创建图的成本。挑战在于构建并维护一个有用的图,减少对大语言模型的依赖——或者理想情况下,使用小型本地语言模型,而不是昂贵的云模型API。
3. 固定实体架构设计不久前,我在 Medium 上发了两篇文章,介绍了一种新的用于构建 RAG 图的方法,叫做固定实体架构。这种方法叫做固定实体架构 [1–2]。
核心思想是要构建一个分层图。
- 第1层:本体层 — 定义领域本体模型。由于本体模型通常范围有限,这一层的规模相对固定或几乎固定不变。
- 第2层:文档层 — 由文档片段组成,类似于任何向量数据库中的片段。如果对该层应用向量索引并直接进行查询,将获得标准的向量数据库搜索结果。
- 第3层(可选):实体层 — 该层由从每个文档片段中提取的实体(例如使用spaCy)组成。由于这些实体在文档中通常会重复出现,它们起到“粘合”层的作用,增强搜索结果的相关性和质量。
在这两种情况下,我都演示了一种无需使用LLM就能创建图形的方法。然而,这种方法的一个主要难题是如何构建本体层。让我们来看以下几个事实:
- 并非所有的数据集都归属于一个明确界定的领域。
- 主题专家(SME)往往难以找到来协助建立知识框架。
因为这些限制,我开始寻找消除对固定本体层需求的方法。
为什么要用分层图呢?
Neo4j 允许在单个内部定义的标签上进行基于向量的索引。如果节点具有不同的标签,你需要为每个标签分别构建索引——这在进行向量搜索时有时并不方便。
当然,在某些情况下,拥有更多的节点类型是有道理的,例如,当需要严格的ontology区分/过滤时。然而,在我的情况下,到目前为止这并未有此必要。通常来说,选择两到三层比较合理。因此,解决标签索引限制的一种方法是给同一层内的所有节点分配相同的内部标签,同时将实际标签、名称和元数据作为节点属性保存。
4. NLP的强大之处如何在不依赖你自己的大脑或万亿参数的大型语言模型的情况下从文本中提取信息?这时候,传统自然语言处理技术(NLP)就可以成为一个非常有用的工具。
首先值得注意的是,在我开始寻找最佳库和NLP模型无论是GPT-3.5之前还是之后,我感到非常惊讶。其中很多——如果不是全部(请纠正我如果有错,并分享任何好的链接或想法。)——已经不再得到支持、更新或维护。它们仿佛被遗弃,几乎被遗忘,这真是可惜,因为它们拥有巨大的潜力。
然而,受到实际行业需求和现实限制的推动,我决定迎接挑战,探索一种基于NLP的方法。我的目标是构建一个图,以提高标准向量数据库的性能。
快速笔记:既然已经对这项技术有所了解,我强烈建议大家多做做实验。到目前为止,我所做的只是触及了NLP驱动图结构潜力的冰山一角。
5. GraphRAGs 及其潜在应用在深入探讨用于RAG的基于NLP的图的实现并分享结果之前,我想先提供我对不同GraphRAG类型及其应用的看法。
提到Microsoft GraphRAG时,我指的是微软研究院在[3]中发表的原始方法,还包括自那以后出现的各种更轻量的改编,例如[4]和[5]。
这些方法通常涉及:
- 使用大语言模型从大量文本语料库中提取人物和事件信息
- 利用大模型总结提取的信息
- 让用户查询总结或社区提供的汇总
虽然有不同实现方式,但基本原则依然相同:使用大规模语言模型从文本构建知识图谱。
以下信息图(图1)代表了我从行业驱动的角度看待何时和为何使用不同类型的基于图的向量搜索技术用于RAG系统。
首先,如果你在是否使用图或标准向量数据库的选择上犹豫不决,可以参考一下相关建议——有一些关于何时选择其一而非另一的指南[6–7]。
此信息图在您选择GraphRAG解决方案之后适用。我想强调在构建您的图之前需要考虑的关键点。
- 数据量 — 您的知识库中存在多少数据?
- 预算限制 — 您的图构建预算有多紧张?
- 本体可用性 — 您是否有现成的本体?
- 你有一个清晰且结构化的本体吗?
- 是否可以在你的知识库所属的固定领域中构建一个稳健的本体层?
- 还是你的数据多元且分散,并且缺乏明确界定的领域知识吗?
这些因素会极大地影响你的GraphRAG方案的设计的可行性及效率。
图1. 选择适合的图架构用于RAG(RAG解决方案)的决策流程图。
回答了这三个关键问题后,你就可以确定适用于你的用例的GraphRAG方法。
需要指出的是,如图1所示,并未涵盖所有可能的场景。一些混合方法也是可以实现的,并且这些技术之间的界限也是模糊的。
不过,我发现有以下趋势:你拥有的数据越多,你就越需要仔细评估你的投资。如果你预算充足,并且需要非常高的精确度,微软的方案是强有力的选择。
然而,如果预算限制是一个问题(这种情况几乎总是出现),你可能需要在准确性上作出一些妥协,选择几乎不依赖大型语言模型的解决方案。在这种情况下,最好的办法是建立一个本体层,并构建一个固定的实体架构图。
如果你在定义本体论时遇到困难,对数据理解不够深入,或者面临高度复杂的数据,我建议构建一个由自然语言处理技术驱动的知识图谱。在接下来的内容中,我会展示如何做到这一点。
6. 释放NLP的力量注:NLP指自然语言处理。我们现在就开始着手制作一张图表,展示巧克力棒的成本,包括制造过程中所需的电费。
技术配置
在这个项目中,我用到了:
- 一台配有32GB内存和6GB内置GPU的商务笔记本电脑。
- 在WSL(Ubuntu)上作为Docker容器运行的Neo4j社区版软件。
- 一个包含660个PDF文件的数据集和根据NVIDIA RAG蓝图修改的数据预处理管道。
正如前面提到的,由NLP支持的图谱是从固定实体架构派生而来的,与固定实体架构相比,主要的不同点在于我移除了本体层这一部分。
也就是说,这个图形将由以下内容组成:
- 文档层级 — 包含文档片段,类似于标准向量数据库中的内容
- 令牌层级 — 提取的令牌作为额外的连接节点,以提升搜索效率
通过使用NLP而非大量LLM处理,这种方法显著降低成本。
6.2 数据预处理流程以下是一些关键的数据预处理步骤:
- 切分文档——我使用了NVIDIA RAG Blueprint中的预写函数将文档拆分成更小的片段。
- 嵌入——我没有采用NVIDIA的默认方法,而是使用了名为Hugging Face模型‘intfloat/e5-base-v2’来嵌入这些切分的片段。这是我在之前提到的蓝图预处理管道中的唯一修改。
- 图构建——一旦数据被处理,我在Neo4j中构建了第一个图层,所有这些节点都被标记为文档。
下面是一个示例代码,将带有文档层的数据导入Neo4j数据库。
def add_chunks_to_db(chunks, doc_name):
prev_node_id = None
for i, chunk in enumerate(chunks):
# 替换 chunk 内容中的单引号为转义字符
escaped_chunk = chunk.replace("'", "\\'")
# 创建 chunk 节点
query = f'''
MERGE (d:Document {{
chunkID: "{f"chunk_{i}"}",
docID: "{doc_name.replace("'", "\\'")}",
full_text: '{escaped_chunk}',
embeddings: {embeddings.embed_documents(chunk).tolist()}}}
)
RETURN elementId(d) as id
'''
result = run_query(query)
# 将 chunk 节点的 id 赋值给 chunk_node_id
chunk_node_id = result[0]['id']
# 如果不是第一个 chunk,则创建到前一个 chunk 的 NEXT 关系
if prev_node_id is not None:
query = f'''
MATCH (c1:Document), (c2:Document)
WHERE elementId(c1) = $prev_node_id AND elementId(c2) = $chunk_node_id
MERGE (c1)-[:NEXT]->(c2)
MERGE (c2)-[:PREV]->(c1)
'''
result = run_query(query)
prev_node_id = chunk_node_id
注意,我正在这里构建一系列文档。我将每个文档的片段相互连接起来。一个指向下一个片段的叫NEXT,一个指向前面片段的叫PREV。换句话说,因此我得到了一个类似于图2中的图:
图2. 一个文档图层的例子。这张图片中,你可以看到4份文档。
在这里你可以看到在我的图中添加的660个PDF文件中有4个。该序列从chunk_0开始,以chunk_n结束。
有了这一层基础,你可以轻松地在上面应用你的第一个向量和文本索引,比如:
query = '''
创建向量索引 vector_index_document,
如果不存在的话
针对 (d:Document)
基于 (d.embeddings)
配置选项如下: {indexConfig: {
`vector.dimensions`: 768,
`vector.similarity_function`: 'cosine'
}}
'''
对于文本索引:
query = '''
创建全文索引名为 text_index_document 针对 (n:Document) 在每个 [n.full_text] 上
'''
现在你就可以把这个图表当作标准的向量数据库来使用。你只需要按照以下步骤来:
def 纯rag_search(query):
user_query_embed = emb.embed_query(query)
query = f"""
CALL db.index.vector.queryNodes('vector_index_document', 10, $user_query_emb)
YIELD node AS vectorNode, score as vectorScore
WITH vectorNode, vectorScore
ORDER BY vectorScore DESC
RETURN elementId(vectorNode), vectorNode.docID, vectorNode.full_text as document_text, vectorScore
LIMIT 10
"""
参数 = {'my_query': query, 'user_query_emb': user_query_embed.tolist()}
results = 运行查询(query, 参数)
return pd.DataFrame(data=results)
就这样!咱们用NVIDIA的数据集来试试,并根据数据集中的信息提几个问题。
图3. 从文档层进行检索的RAG测试。
我使用的是NVIDIA NIM模型“meta/llama-3.3–70b-instruct”(更多详情请访问试用NVIDIA NIM API)。这里需要注意的是,我没有编写任何复杂的提示语,只是传递给用户的问题和检索到的前10个段落内容。
不过,我们构建的图不仅仅是为了纯粹的标准向量数据库功能,是吧?让我们好好利用它!
6.3. 解锁图的力量图谱为数据增添了语义推理。即使没有传统的RDF世界的那种语义推理,图谱通过连接实体也促进了对数据的更深层次理解。在之前的论文中,我假设总存在某种搜索不对称性,这在某种程度上起着一定作用。这种搜索不对称性也被称为幅度敏感性。点积受向量大小的影响,这意味着如果被比较的向量大小差异显著,它可能无法可靠地表示相似性[9]。
在用文档层创建图谱之后,我们需要一种方式来创建文本片段的“粘合剂”。我们没有一个本体论模型,且我们的假设较为天真,但不幸的是却很现实:我们有大量的数据,但并不完全了解这些数据的内涵,我们希望尽可能从中提取价值。我们的目标是构建一个词汇图,利用GraphRAG的所有好处,同时尽量不花费太多资金。
我建议利用一下NLP技术来实现这一点。首先,让我们从每个文本块中提取词元(tokens)、二元词组(bigrams)和三元词组(trigrams)。我使用了一个名为sparkNLP的NLP库,它可以利用本地GPU的强大功能处理大量文档。以下是我用来提取词元的代码片段。
from pyspark.sql import SparkSession
from sparknlp.base import *
from sparknlp.annotator import *
from sparknlp import DocumentAssembler, Finisher
import sparknlp
# 初始化Spark会话
spark = sparknlp.start()
# 样本数据
# 从文档列表创建DataFrame对象
data = spark.createDataFrame([(i, doc) for i, doc in enumerate(documents)], ["id", "text"])
# 文档组装器
document_assembler = DocumentAssembler() \
.setInputCol("text") \
.setOutputCol("document")
# 分词器
tokenizer = Tokenizer() \
.setInputCols(["document"]) \
.setOutputCol("token")
# NGram生成器生成二元词组
bigram_generator = NGramGenerator() \
.setInputCols(["token"]) \
.setOutputCol("bigrams") \
.setN(2)
# NGram生成器生成三元词组
trigram_generator = NGramGenerator() \
.setInputCols(["token"]) \
.setOutputCol("trigrams") \
.setN(3)
# 转换器将注释转换为字符串
finisher = Finisher() \
.setInputCols(["bigrams", "trigrams"]) \
.setOutputCols(["finished_bigrams", "finished_trigrams"]) \
.setCleanAnnotations(False)
# 管道
pipeline = Pipeline(stages=[
document_assembler,
tokenizer,
bigram_generator,
trigram_generator,
finisher
])
# 拟合并转换数据
model = pipeline.fit(data)
result = model.transform(data)
# 显示结果
pandas_df = result.select("text", "finished_bigrams", "finished_trigrams").toPandas()
# 停止Spark会话
spark.stop()
在创建了标记实体之后,您可以将它们添加到图中,建立与从中提取出来的片段数据之间的连接。这种做法简单且可靠,您可以在这一层再次应用前面展示的两个索引。这样一来,我们创建了第二层,其中所有节点都被标记为“Token”标签。我将标签“token”、“bigram”和“trigram”包含在标签属性字段中,同时将标记本身作为名称属性字段,并关联相应的嵌入向量。以下是一些示例,展示了用于创建标记节点的Cypher查询,以及构建相应向量索引的Cypher查询:
# 创建token节点
query = """MERGE (t:Token {label: "Token",
name: $token,
embeddings: $token_embeddings
}) RETURN elementId(t) as token_node_id"""
也对二元gram和三元gram进行相同处理。
接下来呢,创建索引:
# 创建名为 vector_index_token 的向量索引,如果它还不存在
query = '''CREATE VECTOR INDEX vector_index_token IF NOT EXISTS
FOR (n:Token)
ON (n.embeddings)
OPTIONS: {indexConfig: {
`vector.维度`: 768,
`vector.相似度函数`: '余弦'
}}
如图4所示,一个创建的二元词组节点的例子。请注意,包含词元、二元词组和三元词组的整个层内部都标记为“Token”,这使得向量索引可以一次性应用于所有节点。
图4. 二元组节点示例图
图5展示了一个双层图示的示例:文档(蓝色)和标记(橙色)。
到目前为止一切顺利:我们有一些令牌在不同的文档间部分共享,这使一切在某种程度上相互关联。然而,不幸的是,但也不令人惊讶,最初的RAG尝试(RAG即检索增强生成)并没有比纯RAG取得更好的效果。
我们需要利用上下文、逻辑和语义将实体相互连接,以发挥图的全部潜力。这里就是挑战:我们不希望依赖GPT或其他拥有万亿参数的大型模型。我们的图已有超过262千个节点,使用这样的大型模型会超出我们的“巧克力棒级别”的预算。
6.4 三胞胎:有很多优质的开源模型可用。然而,提取三元组可能是一项具有挑战性的任务。最好的方法是使用较小的变压器模型并对其进行微调以适应这个特定任务。更好的方法是自己动手微调,但为了这次展示,我使用了来自Hugging Face的一个预训练模型。该bew/t5_sentence_to_triplet_xl模型是在FLAN-t5-xl的XL版本上进行微调的。该模型比GPT-4小约600倍,因此在我的电脑上很容易运行而没有任何问题。这个模型特别被调优用于从文本中提取三元组。根据模型的所有者Brian Williams的说法,该模型还没有达到完美,是的,结果并不总是像我期望的那样准确,但我们并不追求最高准确率——只要在最低成本下达到相当好的准确度就足够了。
我把提取的文本片段传给了模型,该模型生成了大量三元组,并映射到Token节点上,最终形成了超过65万条边的图。
图6. 具有三元关系的Token节点子集。谓词是连接主体节点(如“omniverse”)和客体节点(如“图形软件工具”)的边。
这里有一个小小的三元组映射代码段:
def process_triplet(triplet):
subject, predicate, object_ = triplet
subject_emb = embed_query_on_gpu(subject)
predicate_emb = embed_query_on_gpu(predicate)
object_emb = embed_query_on_gpu(object_)
params = {'subject_emb': subject_emb.tolist(),
'predicate_emb': predicate_emb.tolist(),
'object_emb': object_emb.tolist(),
'subject': subject,
'predicate': predicate,
'object': object_}
similarSubjects_query = """
调用 () {
// 调用 db.index.vector.queryNodes 搜索主体的重复项
调用 db.index.vector.queryNodes('vector_index_token', 10, $subject_emb)
返回 node AS vectorNode, score as vectorScore
输出 vectorNode, vectorScore
WITH vectorNode, vectorScore
WHERE vectorScore >= 0.96
返回 collect(vectorNode) AS similarSubjects
}
WITH similarSubjects
可选匹配 (n:Token {name: toLower($subject)})
输出 similarSubjects + 如果 n 为 NULL,则 [],否则 [n] 作为 allSubjects
UNWIND allSubjects AS subject
返回 collect(subject) AS similarSubjects
"""
similarSubjects = run_query(similarSubjects_query, params)[0]['similarSubjects']
similarPredicates_query = """
调用 () {
// 调用 db.index.vector.queryNodes 搜索谓语的重复项
调用 db.index.vector.queryNodes('vector_index_token', 10, $predicate_emb)
返回 node AS vectorNode, score as vectorScore
输出 vectorNode, vectorScore
WITH vectorNode, vectorScore
WHERE vectorScore >= 0.96
返回 collect(vectorNode) AS similarPredicates
}
WITH similarPredicates
可选匹配 (n:Token {name: toLower($predicate)})
输出 similarPredicates + 如果 n 为 NULL,则 [],否则 [n] 作为 allPredicates
UNWIND allPredicates AS predicate
返回 collect(predicate) AS similarPredicates
"""
similarPredicates = run_query(similarPredicates_query, params)[0]['similarPredicates']
similarObjects_query = """
调用 () {
// 调用 db.index.vector.queryNodes 搜索宾语的重复项
调用 db.index.vector.queryNodes('vector_index_token', 10, $object_emb)
返回 node AS vectorNode, score as vectorScore
输出 vectorNode, vectorScore
WITH vectorNode, vectorScore
WHERE vectorScore >= 0.96
返回 collect(vectorNode) AS similarObjects
}
WITH similarObjects
可选匹配 (n:Token {name: toLower($object)})
输出 similarObjects + 如果 n 为 NULL,则 [],否则 [n] 作为 allObjects
UNWIND allObjects AS object
返回 collect(object) AS similarObjects
"""
similarObjects = run_query(similarObjects_query, params)[0]['similarObjects']
query = """
UNWIND $similarSubjects AS subject
UNWIND $similarPredicates AS predicate
UNWIND $similarObjects AS object
WITH subject.name AS subjectName, predicate.name AS predicateName, object.name AS objectName, subject, predicate, object
合并 (subjectNode:Token {name: toLower(subjectName)})
创建时设置 subjectNode.embeddings = $subject_emb, subjectNode.triplet_part = 'subject'
匹配时设置 subjectNode.triplet_part = 'subject'
// 合并 (predicateNode:Token {name: toLower(predicateName)})
// 创建时设置 predicateNode.embeddings = $predicate_emb, predicateNode.triplet_part = 'predicate'
// 匹配时设置 predicateNode.triplet_part = 'predicate'
合并 (objectNode:Token {name: toLower(objectName)})
创建时设置 objectNode.embeddings = $object_emb, objectNode.triplet_part = 'object'
匹配时设置 objectNode.triplet_part = 'object'
合并 (subjectNode)-[r:predicate {name: toLower(predicateName)}]->(objectNode)
创建时设置 r.label = 'triplet', r.embeddings = $predicate_emb
匹配时设置 r.label = 'triplet'
返回 subjectName AS subject, predicateName AS predicate, objectName AS object
"""
final_params = {
'similarSubjects': similarSubjects,
'similarPredicates': similarPredicates,
'similarObjects': similarObjects,
'subject_emb': subject_emb.tolist(),
'predicate_emb': predicate_emb.tolist(),
'object_emb': object_emb.tolist()
}
results = run_query(query, final_params)
print(f"处理的三元组数据: {triplet}")
return results
7. 由NLP驱动的图RAG
图7呈现了混合RAG和GraphRAG方法在仅使用文档层检索时回答相同问题的结果,这与纯RAG(图3)的结果进行对比。答案更加全面,提供了对数据更深入的见解。
请注意,我没有执行任何实体解析或实体链接,这肯定是下一步,而且很可能提高性能。对于两个检索测试,我都传递了恰好10个检索到的文本段落。GraphRAG几乎比RAG慢了一倍。虽然我们在响应速度上有所损失,但我们获得了更准确的答案。
如图7所示,与图3中的简单RAG测试相同的问题也被用来测试GraphRAG的效果。
如下是使用三元组的检索功能。
def triplets_driven_retrieval(my_query):
my_query_emb = emb.embed_query(my_query)
query = """
CALL db.index.vector.queryNodes('vector_index_token', 300, $my_query_emb) # 使用向量索引查询节点
YIELD node AS token, score AS tokenScore # 返回节点和得分
CALL (token, tokenScore) {
MATCH (token)
WHERE token.三元组部分 IS NOT NULL
OPTIONAL MATCH (token)-[:predicate]->(object)
OPTIONAL MATCH (object)-[:predicate]->(subject)
OPTIONAL MATCH (subject)-[:CONTAINS]->(doc:Document)
RETURN DISTINCT doc, tokenScore AS score, 1 AS 是否为三元组路径 # 返回文档,得分和是否为三元组路径标记
按得分降序排列
限制为200 # 限制为200条记录
UNION
MATCH (token)
WHERE token.三元组部分 IS NULL
MATCH (token)-[:CONTAINS]-(doc:Document)
RETURN DISTINCT doc, tokenScore AS score, 2 AS 是否为三元组路径 # 返回文档,得分和是否为三元组路径标记
按得分降序排列
限制为200 # 限制为200条记录
}
RETURN DISTINCT doc.文档全文 AS document_text, score, 是否为三元组路径 # 返回文档全文,得分和是否为三元组路径标记
按得分降序排列
限制为100 # 限制为100条记录
UNION
CALL () {
CALL db.index.vector.queryNodes('vector_index_document', 10, $my_query_emb)
YIELD node AS doc, score AS 向量得分 # 返回节点和得分
WITH doc, 向量得分
按向量得分降序排列
RETURN DISTINCT doc,
向量得分 AS score, 3 AS 是否为三元组路径 # 返回文档,得分和是否为三元组路径标记
按向量得分降序排列
限制为10 # 限制为10条记录
}
RETURN DISTINCT doc.文档全文 AS document_text, score, 是否为三元组路径 # 返回文档全文,得分和是否为三元组路径标记
按得分降序排列
限制为10 # 限制为10条记录
"""
params = {'my_query_emb': my_query_emb.tolist()} # 设置参数
results = run_query(query, params)
df = pd.DataFrame(data=results) # 将结果转换为DataFrame
return df
你可以灵活运用查询逻辑,以最佳方式遍历你的图。接下来让我们来看看上面给出的GraphRAG Cypher查询在做什么。查询是分几个步骤构建的。首先,我们在Token节点上根据向量索引匹配用户查询。我们检查该token是否具有名为triplet_part的属性(这些是来自生成三元组的映射tokens)。当我们遍历到三元组的主体节点时,我们会选择指向它的所有对象节点,并选择这些节点上所附的所有文档片段,按顺序选取并限制搜索结果。如果token没有三元组对应关系,我们只需遍历到与之关联的片段。查询的第二部分,我们进行标准的RAG搜索并利用向量索引选择文档。
我确信这个查询还能进一步优化。顺便说一下,我还使用了命名实体识别,提取了组织名、日期等实体(如标题图中红色标记所示)。但是结果不是很好,所以我还是用了两层结构。
看到一个简单的用户问题,比如“系统中提到了哪些公司?(如图8所示)”时,Cypher查询的子图看起来像什么就变得有趣了。
图8. 当用户问到“系统中提到的哪些公司?”时,回答该问题所获取的子节点图的示例图。
结果由两个不同的部分组成:标准RAG部分检索到的大部分不连续的文本片段,以及一组包含主要“主题”三元组节点的节点,在这种情况下是“公司”。这种表示方法能显著帮助优化检索查询的过程。标准RAG部分是指检索和生成过程。
8 讨论环节所提出的方法提供了一种创建标准RAG功能图的可能,并利用“图的力量”来增强它,即利用其内容的语义信息,遍历实体间的关联关系,并以你定义的多种方式检索信息。文献表明,无论是RAG还是GraphRAG,每种技术在某些任务上表现更佳[6–7]。该应用程序主要是为了展示混合-GraphRAG,即将传统的RAG与GraphRAG结合,也可以当作标准的RAG方法来使用。
这个想法是,专门询问具体事实的问题可以由代理后期过滤,然后可以执行传统的RAG查询,绕过上面提到的第一个Cypher部分。需要多步推理或特定上下文的问题可以转向GraphRAG世界。所有这些都是可能的,但并非必需;你可以根据需要选择其一。
最重要的是,上述由NLP驱动的架构为你提供了选择RAG方法的自由,并为RAG解决方案打开了新的可能性。
9. 结论总之,本文提出了一种基于NLP的构建知识图的方法,该方法采用了RAG和GraphRAG的混合方法,应用于RAG场景,并在不依赖于大规模语言模型的情况下运行。这种方法涉及构建分层图,无需预先定义的本体论。
初步结果显示,这种混合检索法得到的答案更加全面和深入,为进一步探索,并可能在大规模生成人工智能项目中应用打开了更多的可能性。
PS! 感谢您看到这里,继续关注,我们将在下一部作品中更深入地讨论性能优化和其他内容。
参考文献1. 在固定实体架构的图数据库上使用RAG:让你的检索为你所用 | 作者:Irina Adamchic | Medium
2. 图形上高效RAG的三层固定实体架构设计 | 作者:Irina Adamchic | Medium
3. [2404.16130] 从局部到整体:基于查询的摘要生成的基于图的RAG方法
4. LightRAG:一个简单快速的GraphRAG替代工具
6. [2502.11371] RAG 对比 GraphRAG:系统评估与关键见解
7. 向量RAG vs 图谱RAG vs LightRAG | TDG | 技术发展小组
8. https://github.com/NVIDIA-AI-Blueprints/rag/tree/v1.0.0
9. AI中的向量搜索 — 第1部分 — 向量相似性搜索技术 | 作者:奥兹尔 (Serkan Özal) | Medium平台
10. Spark NLP — 模型库
11. bew/t5_sentence_to_triplet_xl(一个模型名称) · Hugging Face(一个开源模型库)