共计 4995 个字符,预计需要花费 13 分钟才能阅读完成。
背景介绍
LLM Graph Builder是Neo4J最近开源出来一个自动构建知识图谱的工具。
该工具利用大模型基于给定的schema可以直接生成Entity Graph和Lexical Graph,关于这两个概念简单介绍一下,目前业内比较火的KG-RAG技术论文中,提到的次数非常多。
Entity Graph也是目前最常见的实体知识图谱,图谱中每个节点均代表一个真实世界的实体,实体包含有相关的属性,节点之间通过边连接,代表节点与节点之间的关系。例如:
以上这张图是我将一本汽车说明书其中的实体进行了提取,构建了一个关于汽车使用说明书的知识图谱。
Lexical Graph是将原始文档基于一定的规则进行梳理,提取出来的一张图。通常情况下,节点代表Document、Chunk,边的类型包含PART_OF、FIRST_CHUNK、NEXT_CHUNK,通过这些节点和边,将Document分解为多个关联的CHUNK,CHUNK之间也可以通过SIMILAR来进行链接,表达两个CHUNK之间的语义相似度高。例如:
那么Entity Graph和Lexical Graph之间怎么关联呢?
通过Chunk和Entity的HAS_ENTITY关系可以将两个图谱连接起来,这样为了后续RAG应用过程中,既可以召回实体及相关联实体,又可以召回相关联的原始文本,为LLM提供更丰富的上下文。例如:
我们来看代码,分为两部分:从文件中抽取图谱、基于图谱的问答。
上传文件,抽取图谱
代码入口在:score.py
extract_knowledge_graph_from_file
这个函数中。
其中这几个参数需要重点关注:
file: 需要提取的PDF文件,这里可以直接上传
allowedNodes:这个是可选参数,表示需要构建的图谱中,提前定义好的本体
allowedRelationship:同上,提前定义好的关系
为了提升构建的图谱准确率,这里建议人工提前定义好所有的本体和关系,大模型只需要基于schema进行实体、关系的抽取。
接着看处理文件的逻辑,从extract_graph_from_file_local_file
到get_documents_from_file_by_path
,看到load_document_content
这个方法,里面用了from langchain_community.document_loaders import PyMuPDFLoader
作为PDF的解析器,回到extract_graph_from_file_local_file
函数中,其实第一步就是将上传的PDF文件解析成pages的格式,再看return的processing_source
这个方法。
接下来就是对pages
的处理,首先将其转换成Document
对象,将text和metadata作为对象的属性。接着new了一个对象CreateChunksofDocument,调用split_file_into_chunks这个方法,看方法名就知道,是将pages转换成固定大小的chunk,text_splitter = TokenTextSplitter(chunk_size=200, chunk_overlap=20)
这里用的是TokenTextSplitter,并且窗口大小为200,每个窗口之间重叠20个token,返回出来的chunks参数非常重要,后面很多操作都是基于这个chunks进行。
有了这个chunks之后,第一步做的就是构建lexical graph中的CHUNK节点和FIRST_CHUNK、NEXT_CHUNK、PART_OF关系,具体可以看create_relation_between_chunks
这个函数,这个函数不仅创建了lexical graph的大部分内容,同时还返回了一个chunk的list,这个list将chunk的具体文本内容做了hash,生成每个chunk的唯一chunkid。
从这条语句开始,for i in range(0, len(chunkId_chunkDoc_list), update_graph_chunk_processed):
,程序开始按照特定的步长处理上面返回的这个chunkId_chunkDoc_list,默认的UPDATE_GRAPH_CHUNKS_PROCESSED=20
。
继续看processing_chunks
这个函数,传入的参数是按照上面的20步长为单位,从所有的chunk里面选出来的chunk_list,这个函数非常重要,我直接贴代码:
def processing_chunks(chunkId_chunkDoc_list,graph,file_name,model,allowedNodes,allowedRelationship, node_count, rel_count):
# 将每个chunk节点生成向量并保存到neo4j的向量索引中
update_embedding_create_vector_index( graph, chunkId_chunkDoc_list, file_name)
logging.info("Get graph document list from models")
# 这个函数是基于这20个chunk_list,利用llm抽取node和edge,这个函数后面详细讲,返回的参数为GraphDocument(nodes=nodes, relationships=relationships, source=document)
graph_documents = generate_graphDocuments(model, graph, chunkId_chunkDoc_list, allowedNodes, allowedRelationship)
# 这一步是将抽取出来的实体和关系保存到图数据库
save_graphDocuments_in_neo4j(graph, graph_documents)
# 这一步是生成chunk和entity的关系,还记得上面lexical graph 和 entity graph是怎么连接的吗?就是通过这里的chunk提取出来的entity连接
chunks_and_graphDocuments_list = get_chunk_and_graphDocument(graph_documents, chunkId_chunkDoc_list)
# 同样的,将数据如图
merge_relationship_between_chunk_and_entites(graph, chunks_and_graphDocuments_list)
这里看一下generate_graphDocuments
这个函数,传入的参数还是默认为20大小的chunk_list,跟踪到get_graph_from_OpenAI
这个函数,其中get_combined_chunks
这个函数将原本传过来的20大小的chunk_list变成了combined_chunk_document_list,这个里面是将20个chunk分成了5个一组,一共4组的chunks。
combined_chunk_document_list.append(
Document(
page_content=combined_chunks_page_content[i],
metadata={"combined_chunk_ids": combined_chunks_ids[i]},
)
)
接下来在get_graph_from_OpenAI
这个函数中,首先有combined_chunk_document_list,然后获取到了一个llm,传入get_graph_document_list
函数
llm_transformer = LLMGraphTransformer(
llm=llm,
node_properties=node_properties,
allowed_nodes=allowedNodes,
allowed_relationships=allowedRelationship,
)
首先用了一个LLMGraphTransformer,传入之前定义好的本体类型和关系类型,然后将传入的combined_chunk_document_list里面包含的4组,每组5个chunk,进行图谱抽取,这里每次抽取的单元为5个chunk,不然每个chunk包含的实体信息可能太少了,然后代码里面还开启了线程池,同时进行4组的图谱抽取,抽取完成后将结果返回。
支持基本上图谱抽取的核心流程就结束了,总结一下就是先将用户上传的PDF文件解析成Pages的对象,再将Page对象按照固定大小切割成chunk,这些chunk就组成了lexical graph的主要节点。然后将这些chunk向量化,并且用knn的算法,将相似度高的chunk通过SIMILAR的关系连接起来。接下来就是将这些chunk按照不同的batch进行LLM的图谱抽取,抽取出来的entity同时也将关联到相应的chunk上。
图谱问答
直接找到score.py
中chat_bot
这个函数,重点看QA_RAG
这个函数,处理核心问答逻辑。
先是创建问答流程的history和message作为问答的上下文,下面紧接着有model参数,这个model参数决定了问答流程是基于graph、vector还是graph+vector,这里我们直接看graph+vector的逻辑。
注意看VECTOR_GRAPH_SEARCH_QUERY
这个constant,是通过用户的question,通过向量查询的方式召回相应的entity、relationship和相应的avg_score以及metadata的一条neo4j的sql,重点看最后一部分就好了:
然后进入setup_chat
,重点看create_document_retriever_chain
这个方法,构造了一个chain,这个chain如下:
query_transforming_retriever_chain = RunnableBranch(
(
lambda x: len(x.get("messages", [])) == 1,
(lambda x: x["messages"][-1].content) | compression_retriever,
),
query_transform_prompt | llm | output_parser | compression_retriever,
).with_config(run_name="chat_retriever_chain")
作用是如果当前的messages长度为1,那么认为是用户创建了一个新的会话,并提了一个问题,这时候使用comporession_retrever的检索器,先根据用户的问题,利用上面构造好的neo4j的sql召回相应的实体、关系信息,同时压缩信息防止上下文内容超长。
然后直接返回,看这个函数retrieve_documents
就是执行上面的retriever的chain,通过用户的question从图数据库召回相应的实体和关系。
然后看process_documents
函数就是拿着召回回来的信息作为context,通过Prompt让LLM进行回答
ai_response = rag_chain.invoke({
"messages": messages[:-1],# 这里是历史信息
"context": formatted_docs,# 这里就是之前召回的信息,进行了格式化之后的文本
"input": question
})
返回答案之后,通过summarize_and_log
函数将当前的对话信息让LLM总结出来,替换到message作为历史记录。至此整个图谱问答的核心流程就是这样。
总结一下,就是基于用户的问题,在图谱里面召回相关的实体、关系信息,再将这些信息作为上下文让大模型回答问题。
参考资料
https://github.com/neo4j-labs/llm-graph-builder
https://neo4j.com/labs/genai-ecosystem/llm-graph-builder/