知识图谱快速构建:解读Neo4J工具 LLM Graph Builder

共计 4995 个字符,预计需要花费 13 分钟才能阅读完成。

背景介绍

LLM Graph Builder是Neo4J最近开源出来一个自动构建知识图谱的工具。

该工具利用大模型基于给定的schema可以直接生成Entity GraphLexical Graph,关于这两个概念简单介绍一下,目前业内比较火的KG-RAG技术论文中,提到的次数非常多。

Entity Graph也是目前最常见的实体知识图谱,图谱中每个节点均代表一个真实世界的实体,实体包含有相关的属性,节点之间通过边连接,代表节点与节点之间的关系。例如:
知识图谱快速构建:解读Neo4J工具 LLM Graph Builder
以上这张图是我将一本汽车说明书其中的实体进行了提取,构建了一个关于汽车使用说明书的知识图谱。

Lexical Graph是将原始文档基于一定的规则进行梳理,提取出来的一张图。通常情况下,节点代表Document、Chunk,边的类型包含PART_OF、FIRST_CHUNK、NEXT_CHUNK,通过这些节点和边,将Document分解为多个关联的CHUNK,CHUNK之间也可以通过SIMILAR来进行链接,表达两个CHUNK之间的语义相似度高。例如:
知识图谱快速构建:解读Neo4J工具 LLM Graph Builder

那么Entity Graph和Lexical Graph之间怎么关联呢?
通过Chunk和Entity的HAS_ENTITY关系可以将两个图谱连接起来,这样为了后续RAG应用过程中,既可以召回实体及相关联实体,又可以召回相关联的原始文本,为LLM提供更丰富的上下文。例如:
知识图谱快速构建:解读Neo4J工具 LLM Graph Builder

我们来看代码,分为两部分:从文件中抽取图谱、基于图谱的问答。

上传文件,抽取图谱

代码入口在:score.py extract_knowledge_graph_from_file这个函数中。
知识图谱快速构建:解读Neo4J工具 LLM Graph Builder
其中这几个参数需要重点关注:

file: 需要提取的PDF文件,这里可以直接上传
allowedNodes:这个是可选参数,表示需要构建的图谱中,提前定义好的本体
allowedRelationship:同上,提前定义好的关系

为了提升构建的图谱准确率,这里建议人工提前定义好所有的本体和关系,大模型只需要基于schema进行实体、关系的抽取。

接着看处理文件的逻辑,从extract_graph_from_file_local_fileget_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.pychat_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,重点看最后一部分就好了:
知识图谱快速构建:解读Neo4J工具 LLM Graph Builder

然后进入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/

正文完
 
root
版权声明:本站原创文章,由 root 2024-07-15发表,共计4995字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。