一、建立基本的知识图谱并导入neo4j
这里我举例用的属性表、关系表,大概格式如下
id | 名字 | 颜色 |
---|---|---|
a1 | 苹果 | 红色 |
start | rel | end |
---|---|---|
a1 | 属于 | b1 |
启动neo4j(关于neo4j的安装此处不再赘述)
import pandas as pd
from py2neo import Graph, Node, Relationship
graph = Graph("bolt://localhost:7687", auth=("neo4j", "xxxxxx")) # Xxxxx是你自己的密码
# 导入属性表
def create_nodes(file_name, label, tx):
df_1 = pd.read_csv("./graph_data/"+ file_name)
df = df_1.dropna(subset=['id'])
for index, row in df.iterrows():
node = Node(label, id=row['id'], **row.drop('id').to_dict())
tx.merge(node, label, "id")
attribute_tables = {
"attributes_a.csv":"a",
"attributes_b.csv":"b",
}
tx = graph.begin()
try:
for file_name, label in attribute_tables.items():
create_nodes(file_name, label, tx)
# 导入关系表
relationship_types = {
"rel_a_b.csv": "属于",
}
for file_name, rel_type in relationship_types.items():
df = pd.read_csv("./graph_data/"+ file_name)
for index, row in df.iterrows():
start_label, end_label = file_name.split("_")[1], file_name.split("_")[2].split(".")[0] # 这里的start_label, end_label根据自己的实际需求来定
start_node = graph.nodes.match(start_label, id=row['start']).first()
end_node = graph.nodes.match(end_label, id=row['end']).first()
if start_node and end_node:
rel = Relationship(start_node, rel_type, end_node)
tx.merge(rel)
else:
print(f"无法找到节点: {row['start']} 或 {row['end']}")
tx.commit()
print("成功导入!")
except Exception as e:
tx.rollback()
print(f"导入失败:{e}")
运行后在neo4j中的效果大概是这样:
二、尝试基于Ollama+QWEN大模型做一个基于KG的检索提问
1、关于ollama本地部署的教程
部署ollama后,可以根据需求随便run模型(记得用管理员模式启动)
本地部署 Ollama_ollama本地部署-CSDN博客
Ollama在Windows11部署与使用qwen(自学使用)_ollama qwen-CSDN博客
2、搭建整个RAG的框架
(1)插件安装
这里有一个前置步骤要解决,就是在输入问题后,根据问题中的实体在KG里面进行检索需要一个允许进行检索的插件,下载和安装教程如下:
https://blog.csdn.net/m0_38064529/article/details/125206908
https://blog.csdn.net/zz_dyx/article/details/135172438
https://blog.csdn.net/m0_53573725/article/details/136964980
安装成功!
尝试创建索引:
CREATE FULLTEXT INDEX companyIndex IF NOT EXISTS
FOR (n:a)
ON EACH [n.名字, n.颜色]
验证索引是否创建成功:
CALL db.indexes()
使用索引进行查询示例:
CALL db.index.fulltext.queryNodes("appleIndex", "搜索词")
YIELD node, score
RETURN node, score
如果你需要创建向量索引(用于语义搜索),语法是:
CREATE VECTOR INDEX entity_embedding IF NOT EXISTS
FOR (n:Entity)
ON n.embedding
OPTIONS {
indexType: 'vector',
similarity: 'cosine',
dimension: 384
}
有一个十分重要的注意点!!!在 Neo4j 5.x 版本中,全文索引的创建语法发生了变化,而网上很多教程给出的语句都是4版本的,一定要注意区分!不然容易报类似这样的错误:
There is no procedure with the name db.index.fulltext.createNodeIndex registered for this database instance.
(2)将知识图谱内容转换为大模型可以理解的上下文
根据提问中的实体,找到图谱中的实体和关系,以及头节点、尾节点各自的属性。图谱中通常是以字典型存的,需要转换为语义信息。
def format_context(results):
"""
将查询结果格式化为上下文字符串,方便后续规范prompt
"""
context_parts = []
for record in results:
node = record['node']
rel_type = record['relationship_type']
related = record['related'] # 尾节点
# 获取节点的所有属性
node_props = {k: v for k, v in node.items()}
related_props = {k: v for k, v in related.items()}
# 构建描述性文本
node_desc = ", ".join([f"{k}: {v}" for k, v in node_props.items() if v is not None and str(v) != 'nan'])
related_desc = ", ".join([f"{k}: {v}" for k, v in related_props.items() if v is not None])
context_part = f"节点【{node_desc}】通过关系【{rel_type}】连接到【{related_desc}】"
context_parts.append(context_part)
#print(context_parts)
return "\n".join(context_parts)
输出结果大概是:
节点【苹果】通过关系【拥有子类】连接到【红富士,颜色:红,气味:甜】
具体的形式都可以自己定,在后续prompt的过程中看大模型更能理解哪种形式。
(3)设计prompt
这个就见仁见智了,我设计的基本可以得到答案,仅供参考:
prompt = f"""
你是一个拥有很多农业知识的专家。针对我提出的问题,第一步请直接搜集答案:
问题:{question}
第二步结合我给出的知识图谱信息作为补充放入答案:
知识图谱背景信息:
{context}
请将两部分结合用中文提供详细且准确的回答。
"""
三、RAG构建部分全流程代码以及不足之处
#!/usr/bin/python
# encoding=utf-8
from neo4j import GraphDatabase
from transformers import AutoModelForCausalLM, AutoTokenizer
from langchain.chains import RetrievalQA
from langchain_community.vectorstores import Neo4jVector
import ollama
import math
# Neo4j 数据库连接配置,另一种方式
uri = "bolt://localhost:7687"
user = "neo4j"
password = "********" #自己的密码
# 创建驱动对象
driver = GraphDatabase.driver(uri, auth=(user, password))
# 在neo4j里提问
def query_neo4j(query):
with driver.session() as session:
result = session.run(query)
return [record for record in result]
def generate_response(question):
# 使用Ollama生成回答
response = ollama.generate(model="llama3:8b", prompt=question)
return response['response']
def Index_Create():
# 建立索引
create_index_query = """
CREATE FULLTEXT INDEX companyIndex IF NOT EXISTS
FOR (n:fruit) # 这里是你建的图谱中实际实体的类型
ON EACH [n.颜色, n.气味]
"""
query_neo4j(create_index_query)
def Get_Node():
# 查询Neo4j获取相关信息。
neo4j_query = f"""
CALL db.index.fulltext.queryNodes("fruitIndex", "{question}")
YIELD node, score
MATCH (node)-[r]-(related)
RETURN DISTINCT node, type(r) as relationship_type, related, score
LIMIT 3
"""
results = list(query_neo4j(neo4j_query))
return results
def format_context(results):
"""
将查询结果格式化为上下文字符串,方便后续规范prompt
"""
context_parts = []
for record in results:
node = record['node']
rel_type = record['relationship_type']
related = record['related'] # 尾节点
# 获取节点的所有属性
node_props = {k: v for k, v in node.items()}
related_props = {k: v for k, v in related.items()}
# 构建描述性文本
node_desc = ", ".join([f"{k}: {v}" for k, v in node_props.items() if v is not None and str(v) != 'nan'])
related_desc = ", ".join([f"{k}: {v}" for k, v in related_props.items() if v is not None])
context_part = f"节点【{node_desc}】通过关系【{rel_type}】连接到【{related_desc}】"
context_parts.append(context_part)
#print(context_parts)
return "\n".join(context_parts)
def generate_prompt_answer(question, context):
"""
生成回答
"""
prompt = f"""
你是一个拥有很多农业知识的专家。针对我提出的问题,第一步请直接搜集答案:
问题:{question}
第二步结合我给出的知识图谱信息作为补充放入答案:
知识图谱背景信息:
{context}
请将两部分结合用中文提供详细且准确的回答。
"""
#print("流程已进入llm回答阶段……")
try:
response = generate_response(prompt)
return response
except Exception as e:
return f"生成回答时出错:{e}"
if __name__ == "__main__":
while True:
question = input("请输入你的问题:") # 把question变为全局变量
if question.lower() in ["exit", "quit"]:
break
results = Get_Node()
if not results:
print("没有找到相关的信息。")
continue
context = format_context(results)
answer = generate_prompt_answer(question, context)
print(f"问题:{question}")
print(f"回答:{answer}")
不足:
1、对话提问的question通常是一个句子,在图谱中查询的输入应该是一个实体的名称。也就是我这里面其实差了一步提取实体名词的过程。目前通常有两种思路:①利用传统的深度学习的方法(现在应该有现成的工具)提取一句话中的重要实体;②直接再次调用大模型识别句子中的实体,优点就是更省事儿,缺点是费时,大模型一个问答来回llama3 在cpu上大约要2分钟。我目前的代码中其实这一块实际搜索的时候为了测试流程通畅性,直接将“苹果”代替了{question}。
neo4j_query = f"""
CALL db.index.fulltext.queryNodes("fruitIndex", "苹果")
YIELD node, score
MATCH (node)-[r]-(related)
RETURN DISTINCT node, type(r) as relationship_type, related, score
LIMIT 3
"""
results = list(query_neo4j(neo4j_query))
return results
2、 如果一个实体关联的信息很多,对于LIMIT 的限制需要重新考量。我这里为了测试方便先写了limit 3(上面代码中),即只显示3个相关的节点和关系。在neo4j中,这个limit指的是显示3条结果,而不是根据输入的实体信息给出排名前三的实体,以及相关的关系,这跟我们想要检索的实际需求是不太匹配的。
3、大模型是由token限制的,如果想要查询的实体关系和属性极多,可能会出现超出token的情况,要么是读不全上下文,要么是输出受限。可能在实际应用的过程中给用户展示的依然需要两个部分,一个是这个GraphRAG,另外就是传统数据库的检索答案,类似现在的百度搜索,先给AI的答案,下面再是其他链接。
一个简单的问答实例:
这里换了一个数据,是公司信息的,给出一个输出的样例(pycharm里面,还没有做前端)
下一篇可能会探索一下构建前端chat或者检索页面,以及连GPU跑的问题,还有针对不同GPU资源要换什么大模型基座。