去年五月,我在阅读林语堂的《苏东坡传》时,边读边整理了书中主要人物的关系。最后,借助图数据库 Neo4j (https://neo4j.com)将这些人物关系存储在数据库中,并以可视化的方式呈现。完成后,我还写了一篇读书博客(https://blog.hohoweiya.xyz/2023/05/31/su-shi/),其中展示了这个可视化的人物关系图。当时,怡轩读到这篇博客,邀请我投稿到统计之都。然而由于拖延症发作,一拖就是一年半。
前几天逛 GitHub 时,发现了一个名为LightRAG(https://github.com/HKUDS/LightRAG)的热门项目,其中的“RAG”指的是检索增强生成(Retrieval-Augmented Generation)。这是结合信息检索(retrieval)与生成模型(generation)的自然语言处理技术。RAG 的核心目标是通过引入外部知识库或文档中的相关信息(如一本书),提升生成模型的性能,尤其在需要特定领域知识或上下文信息的任务中有显著的表现。
一个很自然的应用场景就是让语言模型“读”一本书,然后构建基于该书的知识图谱。由于 RAG 的生成过程依赖于外部知识库(“读”书),它能够在生成过程中引用真实的外部信息,特别是在回答涉及特定知识的问题时,表现得更加可靠和准确。
于是便用 LightRAG 对《苏东坡传》进行“阅读”,构建了一个基于书本内容的知识图谱,这样就可以与去年手动整理的知识图谱做一个简单比较。
人工读书
首先,先介绍下去年我手动整理的知识图谱。当时,怡轩提醒我在投稿时可以补充一些数据整理的细节,但心想,哪有什么整理过程啊,所有数据都是一行一行手动输入的(哈哈哈)。
整理出的数据格式如下:
# 数据文件在此仓库中:https://github.com/szcf-weiya/Su-Shi/blob/master/Su-shi.csv $ head Su-shi.csv chapter,chapter_name,location,name,name_zi,name_hao,name_other,career,relation,activities,comments,poem_to,poem_from 1,文忠公,,,,,,,,,,,大略如行云流水,初无定质,但常行于所当行,常止于不可不止。文理自然,姿态横生。 1,文忠公,黄州,李琪,,,,歌伎,,,,,东坡四年黄州住,何事无言及李琪。却似西川杜工部,海棠虽好不吟诗。 2,眉山,眉山,苏洵,明允,老泉,,,父亲,,,, 2,眉山,眉山,苏辙,子由,,颍滨遗老;苏栾城,,弟弟,,,, 2,眉山,眉山,苏轼,子瞻,东坡,,,本人,,,, 2,眉山,眉山,苏序,,,,,祖父,,,, 2,眉山,眉山,程某,,,,,外祖母,,,, 3,童年与青年,眉山,陈太初,,,,道士,同学,,,, 3,童年与青年,眉山,苏辙,,,,,弟弟,,,我初从公,赖以有知。抚我则兄,诲我则师。,我少知子由,天资和且清。岂独为吾弟,要是贤友生。
每一行记录了以下内容:
人物(名字
name
, 字
name_zi
, 号
name_hao
, 其他名字
name_other
)
出现的章节 (
chapter
和
chapter_name
)
我的整理流程很简单:一边读书一边填写表格。随着《苏东坡传》读完,这个表格也逐渐填满。尽管部分数据存在缺失,但关键的人物关系已经基本完整,因此决定使用可视化工具来呈现这些关系。
选择 Neo4j 的原因是我之前接触过知识图谱项目,对这个工具比较熟悉。Neo4j 是一个专门用于存储和处理图数据的图数据库管理系统,它与传统的关系型数据库不同,采用图结构来表示数据:结点 (node) 表示实体,边 (edge) 表示实体之间的关系。这种结构特别适合处理复杂的关系数据,例如社交网络、推荐系统和知识图谱等。
Neo4j 使用 Cypher (https://neo4j.com/docs/cypher-manual/current/introduction/)作为查询语言,类似 SQL,但它专为图结构设计,能够更直观地查询和操作结点、边及其关系。
安装配置 Neo4j
对于 Neo4j 的入门及安装教程,可以参考这篇知乎文章(https://zhuanlan.zhihu.com/p/88745411),不做赘述。这里记录一下在 Ubuntu 系统下根据官方教程(https://neo4j.com/deployment-center/)的安装流程:
wget -O - https://debian.neo4j.com/neotechnology.gpg.key | sudo apt-key add - echo 'deb https://debian.neo4j.com stable 4.4' | sudo tee /etc/apt/sources.list.d/neo4j.list sudo apt-get update sudo apt-get install openjdk-11-jdk sudo apt-get install neo4j
然后安装 apoc (https://github.com/neo4j/apoc)插件:
从 GitHub releases 界面下载与 Neo4j 匹配版本的 Apoc
.jar
文件
将
.jar
文件复制到
$NEO4J_HOME/plugins
目录中
准备完成后,可以通过以下命令启动 Neo4j:
neo4j start
构建数据库
因为我关注的是人物关系,所以只采用原数据中的三列:
name
,
relation
,
career
。以下分三步采用 Cypher 查询语言给每个人 (
name
) 创建一个结点(如有职业
career
信息,则标注),并为每个人与苏轼根据
relation
建立一条联系:
对于职业 (
career
) 不为空的人,创建结点时构造两个标签:
People
和具体职业
career
LOAD CSV WITH HEADERS FROM 'https://github.com/user-attachments/files/17481791/sushi-relation2.csv' AS row WITH row WHERE row.career <> '' CALL apoc.create.node(["People", row.career], {name: row.name, relation: row.relation}) YIELD node RETURN *
对于职业未知的人,创建结点时只赋予
People
的标签
LOAD CSV WITH HEADERS FROM 'https://github.com/user-attachments/files/17481791/sushi-relation2.csv' AS row WITH row where row.career = '' CALL apoc.create.node(["People"], {name: row.name, relation: row.relation}) YIELD node RETURN *
然后对于每一个结点,都建立一个与苏轼的关系,其中具体关系类型便是数据中的
relation
// add relation MATCH (a:People) where a.relation <> '' and a.name <> '苏轼' MATCH (o:People) where o.name = '苏轼' CALL apoc.create.relationship(a, a.relation, {}, o) YIELD rel RETURN *
最后,执行以下查询展示整个关系图:
MATCH (n) RETURN n
AI“读”书
这一部分开始介绍使用大语言模型来“读”书并构建知识图谱。
安装运行 LightRAG
安装流程直接参考官方仓库(https://github.com/HKUDS/LightRAG):
conda create --name lightRAG python=3.10 conda activate lightRAG git clone [email protected] :HKUDS/LightRAG.git cd LightRAG pip install -e .
如果你使用 OpenAI 的模型,需要申请一个
OPENAI_API_KEY
,并设置好环境变量(当然 LightRAG 也支持其他大语言模型):
export OPENAI_API_KEY="sk-..."
准备就绪后,便可以在 Python 运行以下脚本初始化 LightRAG:
from lightrag import LightRAG, QueryParamfrom lightrag.llm import gpt_4o_mini_complete, gpt_4o_complete WORKING_DIR = "./sushi" rag = LightRAG( working_dir=WORKING_DIR, llm_model_func=gpt_4o_mini_complete )
接下来就是让语言模型“读”书了,我们需要一本文字版的《苏东坡传》,比如这个仓库(https://github.com/0voice/expert_readed_books)。下载到本地后,指定存储路径,便可以让 RAG 读书了:
with open("/home/weiya/GitHub/expert_readed_books/人物传/林语堂:苏东坡传.txt" ) as f: rag.insert(f.read())
读书过程中 (
f.read()
),可能会遇到编码问题,比如
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc4 in position 1: invalid continuation byte
这个错误通常是由于文件的编码格式不是 UTF-8 导致的,你可以使用
iconv
工具将文件转换为 UTF-8 编码格式:
iconv -f GB18030 林语堂:苏东坡传.txt -t UTF8 -o 林语堂:苏东坡传_UTF8.txt
如果书本文件过大,一次性读取整本书可能会超出 GPT 的速率限制,出现以下类似错误:
openai.RateLimitError: Error code: 429 - {'error': {'message': 'Rate limit reached for gpt-4o-mini in organization ****** on tokens per min (TPM): Limit 200000, Used 195869, Requested 4412. Please try again in 84ms. Visit https://platform.openai.com/account/rate-limits to learn more.', 'type': 'tokens', 'param': None, 'code': 'rate_limit_exceeded'}}
可以采用一种简单的解决方式:将书本拆分成多个小文件,然后逐一读取。例如,下面将书分成五部分,每部分大约 300 行,依次读取:
with open("/home/weiya/GitHub/expert_readed_books/人物传/sushi_utf8_part1.txt") as f: rag.insert(f.read()) with open("/home/weiya/GitHub/expert_readed_books/人物传/sushi_utf8_part2.txt") as f: rag.insert(f.read()) with open("/home/weiya/GitHub/expert_readed_books/人物传/sushi_utf8_part3.txt") as f: rag.insert(f.read())with open("/home/weiya/GitHub/expert_readed_books/人物传/sushi_utf8_part4.txt") as f: rag.insert(f.read()) with open("/home/weiya/GitHub/expert_readed_books/人物传/sushi_utf8_part5.txt") as f: rag.insert(f.read())
构建数据库
在读书的过程中,LightRAG 已经得到了实体及其之间的关系,并存储在
graph_chunk_entity_relation.graphml
文件中。
为了可视化这些关系,使用 LightRAG 提供的示例程序(https://github.com/HKUDS/LightRAG/blob/main/examples/graph_visual_with_neo4j.py),将
graphml
文件转换为 JSON 格式后导入到 Neo4j 中。在导入之前,清空已有数据以免混淆:
MATCH (n) DETACH DELETE n
最终数据库中存储了 2004 个结点,1150 条关系。为了图形展示的清晰度,仅能展示 300 条关系(
LIMIT 300
)。通过可视化图谱,可以直观地查看不同实体之间的关联。
知识图谱
以下基于已经构建好的图数据库,通过对结点的类型(比如,人
n:PERSON
或地点
n:POSITION
)和关系的类型(比如,友情
r:friendship
)做查询进行一些探索。
亲密关系
首先我们来看看苏东坡的家人:
MATCH (n:PERSON {id: '苏东坡'})-[r]-(m:PERSON)zzf WHERE type(r) CONTAINS 'family' OR type(r) CONTAINS 'love' RETURN n, r, m
图中主要有
“我少知子由,天资和且清。岂独为吾弟,要是贤友生” 的弟弟苏辙(子由)
“十年生死两茫茫,不思量,自难忘。千里孤坟,无处话凄凉”的妻子王弗
这里我们也可以看出一个明显的问题——实体的不唯一,我们都知道“苏子由”,“兄弟子由”,以及“苏辙”都是同一个人,但上图却分成了三个实体。
同样,我们可以看看苏轼的朋友们:
MATCH (n:PERSON {id: '苏东坡'})-[r]-(m:PERSON) WHERE type(r) CONTAINS 'friend' RETURN n, r, m
其中有
“师唱谁家曲,宗风嗣阿谁,借君拍板与门槌,我也逢场作戏莫相疑。溪女方偷眼,山僧莫皱眉,却愁弥勒下生迟,不见阿婆三五少年时”中“山僧”——大通禅师
人生足迹
接着我们可以看看苏轼曾造访过的地方,
MATCH (n)-[r]-(m) WHERE any(label IN labels(n) WHERE label IN ['GEO', 'LOCATION']) RETURN n, r, m LIMIT 300
有两个很明显的地理簇:杭州和西湖。毕竟修浚西湖时建造了“西湖十景”之首的“苏堤春晓”;还有那脍炙人口的“水光潋滟晴方好,山色空蒙雨亦奇。欲把西湖比西子,淡妆浓抹总相宜。”
苏轼一生足迹遍布全中国,小至一叶扁舟,大至一座孤岛:
江山小舟:“壬戌之秋,七月既望,苏子与客泛舟游于赤壁之下。...盖将自其变者而观之,则天地曾不能以一瞬;自其不变者而观之,则物与我皆无尽也,而又何羡乎!...”
承天寺:“念无与乐者,逐步至承天寺,寻张怀民。怀民亦未寝,相与步于中庭。庭下如积水空明,水中藻荇交横,盖竹柏影也。何夜无月,何处无竹柏,但少闲人如吾两人耳。”
海南:“吾始至南海,环视天水无际,凄然伤之曰:‘何时得出此岛也?’已而思之:天地在积水中,九洲在大瀛海中,中国在少海中。有生孰不在岛者?譬如注水于地,小草浮其上,一蚁抱草叶求活。已而水干,遇他蚁而泣曰:‘不意尚能相见尔!’小蚁岂知瞬间竟得全哉?”
文学家
首先看直接与诗词
poet*
的关系:
MATCH p=()-[r]->() WHERE type(r) CONTAINS 'poet' RETURN p LIMIT 300
其中有
至交诗僧参寥:“记取西湖西畔,正春山好处,空翠烟霏。算诗人相得,如我与君稀。约它年、东还海道,愿谢公雅志莫相违。西州路,不应回首,为我沾衣。”——《八声甘州·寄参寥子》
名篇《赤壁怀古》:“大江东去,浪淘尽,千古风流人物。”
然而其中的李师师与苏轼的关系有点令人怀疑,在这本书中并没有提及二人之间的关系,唯一一次出现李师师的地方在于
598: 在苏东坡时代的生活里,酒筵公务之间与歌妓相往还,是官场生活的一部分。.....宋徽宗微服出宫,夜访名妓李师师家。....,可是当日杭州的诗人则为歌女公然写诗。即使是颇负众望的正人君子,为某名妓写诗相赠也是寻常事。在那个时代,不但韩琦、欧阳修曾留下有关妓女的诗,甚至端肃严谨的宰相如范仲淹、司马光诸先贤,也曾写有此类情诗。再甚至精忠爱国的民族英雄岳飞,也曾在一次宴席上写诗赠予歌妓。
但是 LightRAG 给出两人的关系为
苏东坡 interacted with 李师师, acknowledging the role of courtesans in society and expressing admiration for them through poetry.
虽然苏轼确实赠诗给过歌伎李琪——“东坡四年黄州住,何事无言及李琪。却似西川杜工部,海棠虽好不吟诗”,但却不是李师师,这有点张冠李戴之嫌。
接着,我们关注更宽泛的文学
literary*
联系:
MATCH p=()-[r]->() WHERE type(r) CONTAINS 'literary' RETURN p LIMIT 300
其中有
文学名篇:“但愿人长久,千里共婵娟”——《水调歌头》;“江流有声,断岸千尺,山高月小,水落石出。”——《后赤壁赋》;《东坡志林》
需要指出的是《辩奸论》是苏洵所作,但它与苏轼的关系是
literary critique
,
"苏东坡 critiques the harshness of political discourse in his essays, showing the relationship of literary and political commentary."
这对应原文
269: 老苏写《辩奸论》时,苏东坡说他和弟弟子由都认为责骂得太重。
艺术家
现在我们来看艺术方面的关系: