几个月前,“基于知识的问答系统”(KBQA)听起来还是个挺前卫的概念。但到了现在,随着大型语言模型(LLMs)的持续演进,结合了检索增强生成(RAG)的KBQA,门槛已经大大降低了。

知识图谱
简单来说,知识图谱(Knowledge Graph)是一种用来表示现实世界中的实体及其相互关系的数据结构。它通过节点(代表实体)和边(代表实体间的关系)来构建一个连通网络。
它的核心目标,是把散落四处的数据组织成有结构的信息,让机器能真正理解这些信息,进而在搜索、问答、推荐等场景中提供更智能的服务。
知识图谱的基本组成部分
1、实体(Entity):知识图谱里的基本单元,代表现实世界的对象,人、地点、物品都可以。
2、属性(Attribute):描述实体的特征,比如人的姓名、年龄,地点的经纬度等。
3、关系(Relation):表示实体之间的连接。比如,“Tom 是 Mary 的朋友”,这就是关系。
举个例子
Mary had a little lamb,
You’ve heard this tale before;
But did you know she passed her plate,
And had a little more!
如果把这句童谣画成知识图谱,大概就是这个样子:

为什么要使用知识图谱?
知识图谱的价值体现在多个维度。
第一,可以对图运行算法,计算任何节点的中心性,从而判断一个概念在整个知识体系里的重要程度。
第二,能够分析那些彼此关联或断开的概念集,或者通过计算概念群体,更深入地理解主题内容。
第三,它还能帮你发现表面上不相关的概念之间,可能存在的隐秘联系。
举个例子,单靠传统的语义相似度搜索来找上下文,其实并不总能奏效。尤其当查询本身信息不足,无法明确表达意图时,或者相关信息分散在长篇大论的文本中时,问题就来了。
想象一下这个查询:
告诉我《百年孤独》中何塞·阿尔卡迪奥·布恩迪亚的家谱。
要知道,这本书里记录了七代何塞·阿尔卡迪奥·布恩迪亚,而且差不多一半的人物都叫何塞·阿尔卡迪奥·布恩迪亚。如果只用一个简单的RAG流程去回答这个问题,结果可能会非常棘手,甚至根本搞不定。
除此之外,RAG还有个明显的短板——它无法引导你提出更精准的问题。在很多情况下,提出正确的问题,比拿到答案本身更为关键。
而图增强生成(Graph Augmented Generation,GAG)在一定程度上能弥补RAG的这些不足。更理想的是,我们可以灵活地将两者组合,构建一个“图增强-检索增强生成”的流水线,取长补短,效果事半功倍。
将任何文本语料库创建为一个知识图谱
实现流程图:

1. 将文本语料库切分成块(chunks),并为每个块分配一个chunk_id。
2. 针对每个文本块,利用大型语言模型提取其中的概念及其语义关系。我们把这种关系赋予权重W1。同一对概念之间可能存在多种关系,每种关系都是一对概念之间的边。
3. 考虑到在同一文本块中间出现的概念,由于上下文相邻,本身也具有相关性。我们把这种关系赋予权重W2。注意,同一对概念可能会出现在多个块中。
4. 将相似的概念对合并,计算其总权重,并串联它们的关系。这样一来,对于任意不同的概念对,只会有一条边。这条边有了特定的权重,以及一系列关系作为名称。
5. 同时,系统还会计算每个节点的度数和节点所属的社区,分别用来调整图中节点的大小和着色。
技术栈
1、Mistral 7B
使用 Mistral 7B Openorca 从文本块中提取概念。它对系统提示指令的理解和执行能力都很到位。
2、Ollama
Ollama 可以轻松在本地托管任何模型。Mistral 7B OpenOrca 版本可以直接配合 Ollama 使用,开箱即用。
3、Pandas
4、NetworkX
一个Python库,让处理图结构变得超级简单。
5、Pyvis
用于可视化的Python库。它利用Python生成Ja vaScript图形,实现可视化。
完整代码
1、初始化
import pandas as pd
import numpy as np
import os
from langchain.document_loaders import PyPDFLoader, UnstructuredPDFLoader, PyPDFium2Loader
from langchain.document_loaders import PyPDFDirectoryLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from pathlib import Path
import random
## 输入数据目录
data_dir = "cureus"
inputdirectory = Path(f"./data_input/{data_dir}")
print(inputdirectory.absolute().as_uri())
## 生成的结果文件(以CSV格式)存储的位置
out_dir = data_dir
outputdirectory = Path(f"./data_output/{out_dir}")
2、加载文档
loader = DirectoryLoader(inputdirectory, show_progress=True)
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(
chunk_size=1500,
chunk_overlap=150,
length_function=len,
is_separator_regex=False,
)
pages = splitter.split_documents(documents)
print("chunks数量 = ", len(pages))
print(pages[3].page_content)

3、创建包含所有分块的数据框
from helpers.df_helpers import documents2Dataframe
df = documents2Dataframe(pages)
print(df.shape)
df.head()

4、提取概念
# 使用helpers/prompt函数从文本中提取概念
from helpers.df_helpers import df2Graph
from helpers.df_helpers import graph2Df
如果regenerate设置为True,则会重新生成数据框,并以CSV格式写入,避免重复计算。数据框分为两类:
dfne 是边的数据框(dataframe of edges)。
df 是块的数据框(dataframe of chunks)。
如果regenerate为False,数据框则会从输出目录读取,不再重新计算。这意味着只要数据已经存在,程序就会直接拿来用。
# 要使用大语言模型(LLM)重新生成图,将此设置为True。
regenerate = False
if regenerate:
concepts_list = df2Graph(df, model='zephyr:latest')
dfg1 = graph2Df(concepts_list)
if not os.path.exists(outputdirectory):
os.makedirs(outputdirectory)
dfg1.to_csv(outputdirectory/"graph.csv", sep="|", index=False)
df.to_csv(outputdirectory/"chunks.csv", sep="|", index=False)
else:
dfg1 = pd.read_csv(outputdirectory/"graph.csv", sep="|")
dfg1.replace("", np.nan, inplace=True)
dfg1.dropna(subset=["node_1", "node_2", 'edge'], inplace=True)
dfg1['count'] = 4
# 将关系的权重增加到 4。
# 当计算上下文 proximity 时,将分配权重 1。
print(dfg1.shape)
dfg1.head()
对每个文本块都执行一次提取,并将结果json转换为Pandas数据帧:

5、计算上下文 proximity(上下文相关性)
def contextual_proximity(df: pd.DataFrame) -> pd.DataFrame:
# 将数据框 melt(融化)为节点列表
dfg_long = pd.melt(
df, id_vars=["chunk_id"], value_vars=["node_1", "node_2"], value_name="node"
)
dfg_long.drop(columns=["variable"], inplace=True)
# 用块ID作为键进行自连接,会在同一个文本块中间出现的词之间创建关联。
dfg_wide = pd.merge(dfg_long, dfg_long, on="chunk_id", suffixes=("_1", "_2"))
self_loops_drop = dfg_wide[dfg_wide["node_1"] == dfg_wide["node_2"]].index
dfg2 = dfg_wide.drop(index=self_loops_drop).reset_index(drop=True)
dfg2 = (
dfg2.groupby(["node_1", "node_2"])
.agg({"chunk_id": [",".join, "count"]})
.reset_index()
)
dfg2.columns = ["node_1", "node_2", "chunk_id", "count"]
dfg2.replace("", np.nan, inplace=True)
dfg2.dropna(subset=["node_1", "node_2"], inplace=True)
dfg2 = dfg2[dfg2["count"] != 1]
dfg2["edge"] = "contextual proximity"
return dfg2
dfg2 = contextual_proximity(dfg1)
dfg2.tail()
运行后得到一个与原始数据框非常相似的数据框。

6、合并这2个Dataframe
dfg = pd.concat([dfg1, dfg2], axis=0)
dfg = (
dfg.groupby(["node_1", "node_2"])
.agg({"chunk_id": ",".join, "edge": ','.join, 'count': 'sum'})
.reset_index()
)

7、计算 NetworkX 图
nodes = pd.concat([dfg['node_1'], dfg['node_2']], axis=0).unique()
nodes.shape
输出:(215,)
import networkx as nx
G = nx.Graph()
## 添加节点
for node in nodes:
G.add_node(
str(node)
)
## 添加边
for index, row in dfg.iterrows():
G.add_edge(
str(row["node_1"]),
str(row["node_2"]),
title=row["edge"],
weight=row['count']/4
)
8、对节点进行着色
communities_generator = nx.community.girvan_newman(G)
top_level_communities = next(communities_generator)
next_level_communities = next(communities_generator)
communities = sorted(map(sorted, next_level_communities))
# 社区是指图中的一组节点
print("社区数量 = ", len(communities))
print(communities)
9、创建一个数据表(dataframe)来记录每个社区的颜色
import seaborn as sns
palette = "hls"
## Now add these colors to communities and make another dataframe
def colors2Community(communities) -> pd.DataFrame:
## Define a color palette
p = sns.color_palette(palette, len(communities)).as_hex()
random.shuffle(p)
rows = []
group = 0
for community in communities:
color = p.pop()
group += 1
for node in community:
rows += [{"node": node, "color": color, "group": group}]
df_colors = pd.DataFrame(rows)
return df_colors
colors = colors2Community(communities)
colors

10、给图添加颜色
for index, row in colors.iterrows():
G.nodes[row['node']]['group'] = row['group']
G.nodes[row['node']]['color'] = row['color']
G.nodes[row['node']]['size'] = G.degree[row['node']]
# 将一个 NetworkX 图形 G 转换为一个交互式的网络可视化,并将其保存为 HTML 文件。
from pyvis.network import Network
graph_output_directory = "./docs/index.html"
net = Network(
notebook=False,
# bgcolor="#1a1a1a",
cdn_resources="remote",
height="900px",
width="100%",
select_menu=True,
# font_color="#cccccc",
filter_menu=False,
)
net.from_nx(G)
# net.repulsion(node_distance=150, spring_length=400)
net.force_atlas_2based(central_gra vity=0.015, gra vity=-31)
# net.barnes_hut(gra vity=-18100, central_gra vity=5.05, spring_length=380)
net.show_buttons(filter_=["physics"])
net.show(graph_output_directory, notebook=False)
输出:./docs/index.html。

选择需要查询的节点:

