利用 NLP 构建混合图,赋能 RAG 和 GraphRAG 应用
•ingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: inherit;color: rgb(1, 155, 252);">90%的人将 GraphRAG 与微软构建图的方法(或其变体)以及在其上启用搜索相关联。
ingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;text-indent: -1em;display: block;margin: 0.2em 8px;">•ingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: inherit;color: rgb(1, 155, 252);">8%的人将 GraphRAG 定义为使用 LLM 生成的 Cypher 查询或 text-to-any_graph_language(例如 Cypher 或 SPARQL)查询 LPG(标记属性图)或 RDF(资源描述框架)图。
• 剩下的2%要么不确定,要么正在探索不同的可能性。
就我个人而言,我并不完全认同前两个定义中的任何一个,我想解释一下原因。
首先,我必须承认,我认为微软的 GraphRAG 是一个非常酷的想法。在未来五年左右的时间里,它可能会被广泛采用,甚至成为 GraphRAG 方法中的主流选择。
然而,今天,对于大规模工业用途来说,它仍然过于昂贵且不切实际。现实情况是,大多数公司缺乏采用这种方法的时间、预算和信心。相反,考虑到当前的限制,他们更倾向于选择标准的“vanilla”(基础版)向量数据库,这更可行。信心——因为事实上,目前还没有成千上万个 GraphRAG 投入生产的例子(可能由于上述原因)。
在我看来,Text-To-Cypher 或 Text-To-SPARQL 技术是微软 GraphRAG 的一个很好的替代方案(尽管它们也可以一起使用),而且我已经看到了一些非常好的应用案例。然而,也存在一些缺点。首先,它需要大量昂贵的 LLM 调用来生成查询。其次,在你和你的知识库之间总是存在一层不确定性——你依赖于你编写提示的质量,以及所选模型执行和构建 Cypher 或 SPARQL 查询的有效性。此外,额外的处理步骤会增加响应时间,而更高的实现复杂性会增加挑战。总而言之,这项技术对于某些应用来说非常有前景且强大,但其适用性取决于具体的用例。
作为一名顾问和 GenAI 解决方案开发人员,我的目标是以任何规模提供 GraphRAG——从小型实现到大型企业级解决方案。
扩展通常会带来权衡,尤其是在准确性或效率方面。因此,在复杂度和成本可控的前提下,探索更高效的解决方案是有价值的。如果复杂度较低且成本效益高的解决方案仍然能够提供令人满意的结果,那么将其保留在工具箱中是值得的,对吗?
考虑到这一点,本文提出的方法是利用图的力量进行 RAG(检索增强生成),而无需为图创建本身付出高昂的成本。挑战在于构建和维护一个有用的图,同时尽可能减少对 LLM 的依赖——或者,理想情况下,使用小型的本地 LLM,而不是调用昂贵的大型云模型 API。
前段时间,我发表了两篇 Medium 文章,介绍了一种用于构建 RAG 图的新方法,称为固定实体架构 [1-2]。
其核心思想是构建一个分层图:
• **第一层:本体层,**用于定义领域本体。由于本体的范围通常有限,因此这一层的大小保持固定或几乎固定。
•第二层:文档层——由文档块组成,类似于你在任何向量数据库中找到的内容。将向量索引应用于这一层并直接查询它将产生标准的向量数据库搜索。
•第三层(可选):实体层——这一层由从每个文档块中提取的实体(例如,使用 spaCy)组成。由于这些实体经常在文档中重复出现,因此它们充当“粘合”层,从而增强搜索结果。
在这两种情况下,我都演示了一种无需依赖 LLM 即可创建图的方法。然而,这种方法的一个主要挑战是构建本体层。考虑以下事实:
• 并非所有数据集都属于明确定义的领域。
• 主题专家 (SME) 并不总是可以帮助构建本体。
由于这些限制,我开始探索消除对固定本体层需求的方法。
为什么要使用分层图?
Neo4j 允许对单个内部标签进行向量索引。如果节点具有不同的标签,则需要为每个标签构建单独的索引——这在执行向量搜索时并不总是可行的。
当然,在某些情况下,拥有更多的节点类型是有意义的,例如,当需要严格的本体区分/过滤时。但是,在我的例子中,到目前为止还没有必要。通常情况下,两到三层的设计是较为合理的。因此,解决标签索引限制的变通方法是将相同的内部标签分配给层内的所有节点,同时将实际标签、名称和元数据存储为节点属性。
如何在不依赖你自己的大脑或万亿参数 LLM 的情况下从文本中提取信息?这就是经典 NLP(自然语言处理)可以成为有价值工具的地方。
值得一提的是,当我开始搜索最佳库和 NLP 模型时,无论是在 GPT-3.5 时代之前还是之后,我都感到震惊。许多 NLP 库和模型已经停止维护,这令人遗憾。似乎它们已被遗弃和几乎被遗忘,这真是太可惜了,因为它们具有巨大的潜力。
尽管如此,在现实世界的行业需求和实际限制的驱动下,我决定接受探索 NLP 驱动方法的挑战。我的目标是构建一个图,以增强标准向量数据库的性能。
快速说明:既然这项技术已经被探索到一定程度,我强烈鼓励读者进一步尝试。到目前为止,我所做的一切只是触及了 NLP 驱动图结构的全部潜力。
在深入研究 NLP 驱动的 RAG 图的实现并讨论结果之前,我想首先提供我对不同 GraphRAG 类型及其应用的看法。
当我提到 Microsoft GraphRAG 时,我不仅包括 Microsoft Research 在 [3] 中发布的原始方法,还包括自那时以来出现的各种更轻量级的改编,例如 [4-5]。
这些方法通常涉及:
• 使用 LLM 从大型文本语料库中提取实体和关系
• 使用 LLM 总结提取的信息
• 允许用户查询摘要和/或基于社区的摘要
虽然存在不同的实现,但基本原理保持不变:使用 LLM 从文本构建知识图。
下图(图 1)展示了我从行业角度出发,何时以及为何使用不同的基于图的向量搜索进行 RAG 系统的看法。
在决定是否使用图或标准向量数据库时,可以参考一些关于何时选择一个而不是另一个的指南 [6-7]。
一旦你决定采用 GraphRAG 解决方案,此信息图表就适用。在这里,我重点介绍了在构建图之前需要考虑的关键因素。
1.数据量——你的知识库中存在多少数据?
2.预算限制——你构建图的预算有多紧张?
3.本体可用性:
• 你是否有一个清晰、结构化的本体?
• 你的知识库是否属于可以构建强大本体层的固定领域?
• 还是你的数据多样、分散且缺乏明确定义的领域知识?
这些因素会严重影响你的 GraphRAG 解决方案的设计、可行性和效率。
一旦你回答了三个关键问题——数据量、预算限制和本体可用性——你就可以确定适合你用例的 GraphRAG 方法。
重要的是要注意,图 1 并未涵盖所有可能的场景。一些混合方法也是可行的,并且技术之间的界限并非严格固定。
尽管如此,我观察到以下趋势:你拥有的数据越多,你就越需要仔细评估你的投资。如果你有足够的预算并且需要非常高的准确性,那么微软的解决方案是一个强大的选择。
但是,如果预算限制是一个问题(几乎总是如此),你可能需要牺牲准确性并选择几乎不使用 LLM 的解决方案。在这种情况下,最好的方法是建立一个本体层并构建一个固定实体架构图。
如果你难以定义本体、缺乏对数据的深入了解或面临高数据复杂性,我建议构建一个 NLP 驱动的图。在以下部分中,我将演示如何实现这一点。
接下来,我们将着手构建一个图,以一块巧克力棒的成本构建一个图(考虑到所涉及的电力成本)。
技术设置
对于这个项目,我使用了:
• 一台配备 32GB RAM 和 6GB 内置 GPU 的商务笔记本电脑。
• 作为 Docker 容器在 WSL (Ubuntu) 上运行的 Neo4j 社区版。
• 一个包含 660 个 PDF 文件的数据集和一个数据预处理管道,其中一些修改取自 NVIDIA RAG Blueprint(https://github.com/NVIDIA-AI-Blueprints/rag/tree/v1.0.0)。
如前所述,NLP 驱动的图源自固定实体架构,但有一个关键区别——我放弃了本体层。
这意味着该图将包含:
1.文档层——包含文档块,类似于标准向量数据库
2.令牌层——提取的令牌充当额外的连接节点,从而提高搜索性能
通过利用 NLP 而不是 LLM 繁重的处理,这种方法可以显著降低成本。
数据预处理管道遵循以下关键步骤:
1.分块——我使用了 NVIDIA RAG Blueprint 中的预先编写的函数将文档拆分为更小的片段。
2.嵌入——我使用了 Hugging Face 模型“intfloat/e5-base-v2”来嵌入块,而不是默认的 NVIDIA 方法。这是我之前提到的唯一蓝图预处理管道修改。
3.图构建——一旦数据被处理,我就在 Neo4j 中构建了第一个图层(如下图 2 所示),其中所有块节点都被标记为 Document。
下面你将找到一个代码示例,用于使用文档层填充 Neo4j 数据库。下面的代码演示了如何将文档块添加到 Neo4j 数据库中,并建立相邻块之间的 NEXT 和 PREV 关系。
defadd_chunks_to_db(chunks,doc_name):
prev_node_id=None
fori,chunkinenumerate(chunks):
#Escapesinglequotesinthechunkcontent
escaped_chunk=chunk.replace("'","\\'")#Createthechunknode
query=f'''
MERGE(d
ocument{{
chunkID:"{f"chunk_{i}"}",
docID:"{doc_name.replace("'","\\'")}",
full_text:'{escaped_chunk}',
embeddings:{embeddings.embed_documents(chunk).tolist()}}}
)
RETURNelementId(d)asid
'''
result=run_query(query)
chunk_node_id=result[0]['id']#Ifthisisnotthefirstchunk,createaNEXTrelationshiptothepreviouschunk
ifprev_node_idisnotNone:
query=f'''
MATCH(c1
ocument),(c2
ocument)
WHEREelementId(c1)=$prev_node_idANDelementId(c2)=$chunk_node_id
MERGE(c1)-[:NEXT]->(c2)
MERGE(c2)-[
REV]->(c1)
'''
run_query(driver,query)
prev_node_id=chunk_node_id请注意,我在这里构建文档链。我添加了每个文档的块,这些块通过边在两个方向上连接:一个称为 NEXT,指向下一个块,另一个称为 PREV,指向上一个块。因此,我有一个看起来像这样的图(见图 2):
在这里,你可以识别出我已添加到图中的 660 个 PDF 中的 4 个。链从 chunk_0 开始,以 chunk_n 结束。
使用第一层,你可以轻松地在其上应用你的第一个向量和文本索引,例如:
query='''
CREATEVECTORINDEXvector_index_document
IFNOTEXISTS
FOR(d
ocument)
ON(d.embeddings)
OPTIONS{indexConfig:{
`vector.dimensions`:768,
`vector.similarity_function`:'cosine'
}}
'''以及文本索引:
query='''
CREATEFULLTEXTINDEXtext_index_documentFOR(n
ocument)ONEACH[n.full_text]
'''现在,人们可以将此图用作标准向量数据库。你只需执行以下操作:
defpure_rag(query):
my_query_emb=emb.embed_query(query)
query=f"""
CALLdb.index.vector.queryNodes('vector_index_document',10,$user_query_emb)
YIELDnodeASvectorNode,scoreasvectorScore
WITHvectorNode,vectorScore
ORDERBYvectorScoreDESC
RETURNelementId(vectorNode),vectorNode.docID,vectorNode.full_textasdocument_text,vectorScore
LIMIT10
"""
params={'my_query':my_query,'user_query_emb':my_query_emb.tolist()}
results=run_query(query,params)
returnpd.DataFrame(data=results)就是这样!让我们在 NVIDIA 数据集上尝试一下,并根据它包含的数据查询一些内容。
我使用的 LLM 是 NVIDIA NIM 模型“meta/llama-3.3-70b-instruct”,来自 Try NVIDIA NIM APIs(https://build.nvidia.com/explore/discover)。请注意,我没有设计任何复杂的查询模板,只是传递用户问题和前 10 个检索到的段落。
但是,我们构建图不仅仅是为了纯粹的标准向量数据库功能,对吗?让我们从中获得更多!
图为数据添加了语义推理。即使没有经典的 RDF 世界语义推理,图——通过连接实体——也有助于更深入地理解数据。此外,我在之前的文章中假设,总是存在某种搜索不对称性,它可以发挥一定的作用。这种搜索不对称性也称为幅度敏感性。点积受向量幅度的影响,这意味着如果被比较的向量具有显著不同的幅度,它可能无法可靠地表示相似性 [9]。
在使用文档层创建图之后,我们需要一种方法来为文本块创建“粘合剂”。我们没有本体,我们的假设虽然简单,但却是现实情况的真实反映:我们拥有大量数据,并且我们不完全知道这些数据是关于什么的,但我们希望从中提取最大的价值。我们的目标是构建一个词汇图,该图利用 GraphRAG 的所有优势,而不会在此过程中花费太多钱。
我建议为此利用 NLP 技术。首先,让我们从每个文本块中提取令牌、二元语法和三元语法。我使用了名为 sparkNLP(https://sparknlp.org/models?task=Named+Entity+Recognition) 的 NLP 库,该库允许你利用本地 GPU 的强大功能来处理大量文档。下面是我用于令牌提取的代码片段。
frompyspark.sqlimportSparkSession
fromsparknlp.baseimport*
fromsparknlp.annotatorimport*
fromsparknlpimportDocumentAssembler,Finisher
importsparknlp#InitializeSparksession
spark=sparknlp.start()#Sampledata
#CreateDataFramefromthelistofdocuments
data=spark.createDataFrame([(i,doc)fori,docinenumerate(documents)],["id","text"])#DocumentAssembler
document_assembler=DocumentAssembler()\
.setInputCol("text")\
.setOutputCol("document")#Tokenizer
tokenizer=Tokenizer()\
.setInputCols(["document"])\
.setOutputCol("token")#NGramGeneratorforbigrams
bigram_generator=NGramGenerator()\
.setInputCols(["token"])\
.setOutputCol("bigrams")\
.setN(2)#NGramGeneratorfortrigrams
trigram_generator=NGramGenerator()\
.setInputCols(["token"])\
.setOutputCol("trigrams")\
.setN(3)#Finishertoconvertannotationstostring
finisher=Finisher()\
.setInputCols(["bigrams","trigrams"])\
.setOutputCols(["finished_bigrams","finished_trigrams"])\
.setCleanAnnotations(False)#Pipeline
pipeline=Pipeline(stages=[
document_assembler,
tokenizer,
bigram_generator,
trigram_generator,
finisher
])#Fitandtransformthedata
model=pipeline.fit(data)
result=model.transform(data)#Showtheresults
pandas_df=result.select("text","finished_bigrams","finished_trigrams").toPandas()
#StoptheSparksession
spark.stop()创建令牌实体后,你可以将它们添加到图中,建立与从中提取它们的块的连接。该方法简单而稳健,你可以再次在此层上应用两个索引,正如我之前演示的那样。通过这种方式,我们创建了第二层,其中所有节点都标记为“Token”。我在标签属性中包含了标签“token”、“bigram”和“trigram”,以及令牌本身作为名称属性和关联的嵌入。以下示例显示了用于创建令牌节点的 Cypher 查询,以及用于构建相应向量索引的查询:
#createtokennode
query="""MERGE(t:Token{label:"Token",
name
token,
embeddings
token_embeddings
})RETURNelementId(t)astoken_node_id"""也为二元语法和三元语法执行此操作。
接下来,创建索引:
#createvectorindexontokenembeddings
query='''CREATEVECTORINDEXvector_index_tokenIFNOTEXISTS
FOR(n:Token)
ON(n.embeddings)
OPTIONS{indexConfig:{
`vector.dimensions`:768,
`vector.similarity_function`:'cosine'
}}图 4 显示了已创建的二元语法节点的示例。请注意,包含令牌、二元语法和三元语法的整个层具有内部标签“Token”,允许将向量索引一次应用于所有节点。
到目前为止一切顺利:我们有一些在不同文档中部分共享的令牌,这使得所有内容在某种程度上相互连接。然而,不幸的是但并不令人惊讶的是,第一次 RAG 尝试并没有给出比仅执行纯 RAG 更好的结果。
我们需要解锁图的全部潜力的是使用上下文、逻辑和语义将实体彼此连接起来。这一任务面临挑战:我们需要避免对 GPT 或其他超大规模语言模型的依赖。考虑到我们已经拥有超过 26.2 万个节点,使用大型模型会显著增加计算成本,超出我们的预算范围。
有许多好的开源模型可用。但是,三元组提取可能是一项具有挑战性的任务。最好的方法是使用较小的 Transformer 模型并针对此特定任务对其进行微调。更为理想的选择是自行微调该模型,但对于此演示文稿,我使用了 Hugging Face 中的预训练模型。bew/t5_sentence_to_triplet_xl(https://huggingface.co/bew/t5_sentence_to_triplet_xl) 模型已在 FLAN-t5-xl 的 XL 版本上进行了微调。此模型比 GPT-4 小约 600 倍,因此它可以轻松地安装在我的计算机上,没有任何问题。该模型经过专门调整以从文本中提取三元组。根据该模型的所有者 Brian Williams 的说法,该模型尚未完善,是的,结果并不总是像我希望的那样准确,但我们的目标不是最高的准确性——只是以最小的成本获得非常好的准确性就足够了。
我提取了文本块并将它们传递给模型。该模型创建了许多三元组(即主语-谓词-宾语的组合),这些三元组随后被映射到令牌节点,从而导致图中总共有超过 65 万条边。
以下是三元组映射的一小段代码:
defprocess_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'
bject_emb.tolist(),
'subject':subject,
'predicate':predicate,
'object'
bject_}similarSubjects_query="""
CALL(){
//Searchforthesubjectduplicates
CALLdb.index.vector.queryNodes('vector_index_token',10,$subject_emb)
YIELDnodeASvectorNode,scoreasvectorScore
WITHvectorNode,vectorScore
WHEREvectorScore>=0.96
RETURNcollect(vectorNode)ASsimilarSubjects
}
WITHsimilarSubjects
OPTIONALMATCH(n:Token{name:toLower($subject)})
WITHsimilarSubjects+CASEWHENnISNULLTHEN[]ELSE[n]ENDASallSubjects
UNWINDallSubjectsASsubject
RETURNcollect(subject)ASsimilarSubjects
"""
similarSubjects=run_query(similarSubjects_query,params)[0]['similarSubjects']similarPredicates_query="""
CALL(){
//Searchforthepredicateduplicates
CALLdb.index.vector.queryNodes('vector_index_token',10,$predicate_emb)
YIELDnodeASvectorNode,scoreasvectorScore
WITHvectorNode,vectorScore
WHEREvectorScore>=0.96
RETURNcollect(vectorNode)ASsimilarPredicates
}
WITHsimilarPredicates
OPTIONALMATCH(n:Token{name:toLower($predicate)})
WITHsimilarPredicates+CASEWHENnISNULLTHEN[]ELSE[n]ENDASallPredicates
UNWINDallPredicatesASpredicate
RETURNcollect(predicate)ASsimilarPredicates
"""
similarPredicates=run_query(similarPredicates_query,params)[0]['similarPredicates']similarObjects_query="""
CALL(){
//Searchfortheobjectduplicates
CALLdb.index.vector.queryNodes('vector_index_token',10,$object_emb)
YIELDnodeASvectorNode,scoreasvectorScore
WITHvectorNode,vectorScore
WHEREvectorScore>=0.96
RETURNcollect(vectorNode)ASsimilarObjects
}
WITHsimilarObjects
OPTIONALMATCH(n:Token{name:toLower($object)})
WITHsimilarObjects+CASEWHENnISNULLTHEN[]ELSE[n]ENDASallObjects
UNWINDallObjectsASobject
RETURNcollect(object)ASsimilarObjects
"""
similarObjects=run_query(similarObjects_query,params)[0]['similarObjects']query="""
UNWIND$similarSubjectsASsubject
UNWIND$similarPredicatesASpredicate
UNWIND$similarObjectsASobject
WITHsubject.nameASsubjectName,predicate.nameASpredicateName,object.nameASobjectName,subject,predicate,object
MERGE(subjectNode:Token{name:toLower(subjectName)})
ONCREATESETsubjectNode.embeddings=$subject_emb,subjectNode.triplet_part='subject'
ONMATCHSETsubjectNode.triplet_part='subject'
//MERGE(predicateNode:Token{name:toLower(predicateName)})
//ONCREATESETpredicateNode.embeddings=$predicate_emb,predicateNode.triplet_part='predicate'
//ONMATCHSETpredicateNode.triplet_part='predicate'
MERGE(objectNode:Token{name:toLower(objectName)})
ONCREATESETobjectNode.embeddings=$object_emb,objectNode.triplet_part='object'
ONMATCHSETobjectNode.triplet_part='object'
MERGE(subjectNode)-[r:predicate{name:toLower(predicateName)}]->(objectNode)
ONCREATESETr.label='triplet',r.embeddings=$predicate_emb
ONMATCHSETr.label='triplet'RETURNsubjectNameASsubject,predicateNameASpredicate,objectNameASobject
"""
final_params={
'similarSubjects':similarSubjects,
'similarPredicates':similarPredicates,
'similarObjects':similarObjects,
'subject_emb':subject_emb.tolist(),
'predicate_emb':predicate_emb.tolist(),
'object_emb'
bject_emb.tolist()
}
results=run_query(query,final_params)print(f"Processedtriplet:{triplet}")
returnresults图 7 展示了在仅使用文档层检索的相同问题上使用混合 RAG/GraphRAG 方法的结果,代表纯 RAG(图 3)。答案更加全面且深入。
请注意,我没有执行任何实体解析或实体链接,这肯定会是下一步,并且很可能会提高性能。此外,对于两个检索测试,我都传递了 10 个检索到的文本段落。GraphRAG 花费的时间几乎是 RAG 的两倍。虽然我们牺牲了一些延迟,但我们获得了更好的答案准确性。
下面给出了使用三元组关系的检索函数。
deftriplets_driven_retrieval(my_query):
my_query_emb=emb.embed_query(my_query)query="""
CALLdb.index.vector.queryNodes('vector_index_token',300,$user_query_emb)
YIELDnodeAStoken,scoreAStokenScore
CALL(token,tokenScore){
MATCH(token)
WHEREtoken.triplet_partISNOTNULL
OPTIONALMATCH(token)-[:predicate]->(object)
OPTIONALMATCH(object)-[:predicate]->(subject)
OPTIONALMATCH(subject)-[:CONTAINS]->(doc
ocument)
RETURNDISTINCTdoc,tokenScoreasscore,1ASisTripletPath
ORDERBYtokenScoreDESC
LIMIT200UNION
MATCH(token)
WHEREtoken.triplet_partISNULL
MATCH(token)-[:CONTAINS]-(doc
ocument)
RETURNDISTINCTdoc,tokenScoreasscore,2ASisTripletPath
ORDERBYtokenScoreDESC
LIMIT200
}
RETURNDISTINCTdoc.full_textASdocument_text,score,isTripletPath
ORDERBYscoreDESC
LIMIT100UNIONCALL(){
CALLdb.index.vector.queryNodes('vector_index_document',10,$user_query_emb)
YIELDnodeASdoc,scoreasvectorScore
WITHdoc,vectorScore
ORDERBYvectorScoreDESC
RETURNDISTINCTdoc,
vectorScoreASscore,3ASisTripletPath
ORDERBYvectorScoreDESC
LIMIT10
}RETURNDISTINCTdoc.full_textASdocument_text,score,isTripletPath
ORDERBYscoreDESC
LIMIT10
"""params={'user_query_emb':my_query_emb.tolist()}
results=run_query(query,params)
df=pd.DataFrame(data=results)returndf你可以通过优化查询逻辑,以最佳方式遍历图结构。让我们看一下上面介绍的 GraphRAG Cypher 查询正在做什么。该查询分几个步骤构建。首先,我们使用向量索引在令牌节点上匹配用户查询。我们检查令牌是否具有名为 triplet_part 的属性(这些属性是从生成的三元组映射的令牌)。当我们遍历三元组并到达主语节点时,我们获取指向它的所有宾语节点,并选择附加到这些节点的所有文档块,对其进行排序和限制搜索。如果令牌没有三元组对,则只需直接遍历到其对应的文档块。在查询的第二部分中,我们执行标准的 RAG 搜索并使用向量索引选择文档。
该查询仍有进一步优化的空间。作为旁注,我还使用了 spaCy 的命名实体提取,提取令牌分类标签,如 ORG、DATE 等(请参见标题图像中的红色节点)。但是,结果不是很好,因此我坚持使用两层架构。
有趣的是,对于简单的用户问题“系统中提到了哪些公司?”,Cypher 查询的子图是什么样的(图 8)。子图中展示了基于用户问题构建的主语、宾语及其关联的结构。
结果由两个不同的部分组成:从标准 RAG 部分检索的、大部分不相交的文本块和一组具有主要“主语”三元组节点的节点,在本例中为:“公司”。此表示可以显著帮助检索查询的优化阶段。
本方法的主要目标是展示混合图 RAG 的应用,它提供了创建标准 RAG 功能图并使用“图的力量”对其进行增强的可能性,即使用其内容的语义、遍历实体关系并以各种你定义的方式检索信息。文献表明,每种技术,RAG 和 GraphRAG,在某些任务上表现更好 [6-7]。此应用程序主要旨在展示混合图 RAG,将经典 RAG 与 GraphRAG 相结合,但也可以用作标准 RAG 方法。
作为一个想法,代理可以在以后过滤掉询问特定事实的专用问题,并且可以执行经典的 RAG 查询,绕过上面显示的第一个 Cypher 部分。需要多跳推理或一些总体上下文的问题可以重定向到 GraphRAG 世界。所有这些都是可能的,但不是强制性的;你总是可以选择其中一个。
最重要的是,上面介绍的 NLP 驱动的架构使你可以灵活地选择你的 RAG 方法,并为 RAG 解决方案开辟新的视野。
总之,本文介绍了一种 NLP 驱动的方法来构建知识图,该图执行混合 RAG/GraphRAG,用于 RAG 应用程序,而无需过度依赖 LLM。该方法涉及分层图,而无需包含固定的本体。
初步结果表明,使用此混合检索回答的问题会产生更全面和更有见地的答案,为后续探索及在大型 GenAI 项目中的潜在应用奠定了基础。
| 欢迎光临 链载Ai (https://www.lianzai.com/) | Powered by Discuz! X3.5 |