news 2026/10/9 22:15:45

基于neo4j知识图谱的古诗词问答系统构建实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于neo4j知识图谱的古诗词问答系统构建实战解析

简介:基于知识图谱的古诗词问答系统,以Neo4j图数据库存储诗作、诗人、朝代及语义关联,是面向知识图谱课程大作业与Python开发者的完整参考实现。资源共43个文件,压缩包仅828KB,涵盖13个txt文本(语料/停用词)、11个csv数据表(诗词及实体关系)、11个py脚本(数据爬取、图谱构建、问题分类与答案检索)、2个json配置(词表与分类映射)以及model模型等辅助文件。项目内含主程序、爬虫模块、建图模块与问答处理模块,可完整跑通“数据采集—知识融合—图谱构建—问答推理”链路,并附有已训练模型、停用词表与分类配置,便于直接对照调试和二次开发。整套资源结构清晰,覆盖知识抽取、实体对齐、图谱查询等关键环节,有助于理解知识图谱系统的工程化落地。系统支持“某诗人的代表作”“某词牌名作品”等典型问答场景,已有298人学习下载,适合用Python与Neo4j完成知识图谱大作业,或希望快速搭建古诗词问答原型的读者学习参考。

1. 知识图谱遇上古诗词:这个问答系统把 neo4j 变成了考点记忆库

做知识图谱大作业的同学,最容易在选数据库和凑数据这两件事上磨掉一个月。而这份基于知识图谱的古诗词问答系统,用 Python 搭建了从爬虫、数据清洗、图谱构建到问句分类、答案检索的完整闭环,数据库用的是 neo4j,不是那种只写了几个 SPARQL 查询的玩具 Demo。它最打动我的一点是:目录里同时躺着SpiderPoem.py、build_graph.py、Train.py和main.py,这意味着你拿到手不是一条被包装好的黑匣子,而是一条能拆开看、能单独替换模块的真实项目。适合正在做知识图谱课设、需要快速出成果又想把技术点讲清楚的同学。

2. 数据管线的搭建:从爬虫抓取到 neo4j 图谱落地

2.1 先搞懂项目的数据流:四个脚本各干一件脏活

打开压缩包后,你会看到SpiderPoem.py、read_csv.py、merge_csv.py、export_data.py、build_graph.py这一排脚本,它们刚好串成一条线性数据流。我第一次跑的时候没有按顺序来,直接执行build_graph.py,结果报“csv 文件不存在”,这才老老实实看了目录里的 README 注释。

这条管线用自然语言描述是这样的:

SpiderPoem.py(联网采集古诗文) → 原始 txt / json(临时落盘) → read_csv.py + merge_csv.py(解析 + 按作者/朝代去重合并) → CSV 中间文件(trainData / poemData) → export_data.py(把 CSV 整理成图谱导入表) → build_graph.py(连接 neo4j 建节点和关系)

值得注意的一个细节是:项目里poemData和trainData是分开的,前者服务图谱导入,后者服务模型训练。这种“数据双写”的做法在课设里不多见,但很有工程味道——同一份古诗数据,喂给图数据库做问答检索,喂给分类器做问题意图识别,两边的数据格式要求完全不同,分开管理才能避免互相污染。

2.2 爬虫脚本怎么改:UA、延迟和目标字段

SpiderPoem.py是一个 scrapy 风格的单文件爬虫,但市面上写古诗爬虫的脚本一抓一大把,这个项目的可取之处在于它的字段设计。它每抓一首诗,保存的不只是“标题+正文”,而是把作者、朝代、类型、正文、译文、赏析分成了六个独立字段。

为什么必须这样设计?因为知识图谱的查询能力依赖属性粒度。如果你把所有内容塞进一个content字段,neo4j 里就只能做全文搜索,谈不上“图谱”。拆开之后,每个字段天然对应一个节点属性,后续 Cypher 查询就可以写WHERE n.author = '李白' AND n.dynasty = '唐'这种精确过滤。

爬虫脚本里最需要调的是DOWNLOAD_DELAY和USER_AGENT列表,我一般会这样改成适合本地快速采集的值:

# SpiderPoem.py 中的配置区 DOWNLOAD_DELAY = 1.5 # 单个请求间隔,单位秒;对方服务器压力小,不容易被限流 USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" ] # 字段解析示例:把拿到的 HTML 块映射成六元组 def parse_poem(html): return { "title": extract_by_class(html, "cont"), "author": extract_by_class(html, "au"), "dynasty": extract_by_class(html, "dy"), "type": extract_by_class(html, "type"), "content": extract_main_text(html), "appreciation": extract_by_class(html, "shangxi") }

参数说明:DOWNLOAD_DELAY是爬虫伦理里最重要的参数,设成 0 会把对方站点拖垮,也会让你的 IP 进黑名单;USER_AGENTS是每次请求轮换的浏览器标识,避免请求头过于单一被识别。这里我习惯把延迟调成 1 到 2 秒,课设数据量一般几百首,多用一分钟而已,换来的是整个过程不断流。

2.3 从 CSV 到 neo4j:build_graph.py 的节点与关系设计

build_graph.py是整个项目里最值得逐行读的脚本。它决定了知识图谱长什么样。我看了之后,发现它把古诗领域抽象成了四类节点和四种关系,这个抽象层非常适合课设答辩时讲解。

四类节点分别是:朝代、作者、诗作、类型。关系则这样建:

  • (作者)-[:出生于]->(朝代)
  • (诗作)-[:作者是]->(作者)
  • (诗作)-[:属于类型]->(类型)
  • (诗作)-[:创作于]->(朝代)
# build_graph.py 的核心建图逻辑(精简版) from py2neo import Graph, Node, Relationship graph = Graph("bolt://localhost:7687", auth=("neo4j", "123456")) # 先建作者节点,用 MERGE 保证不重复 author_node = Node("Author", name=row["author"], dynasty=row["dynasty"]) graph.merge(author_node, "Author", "name") # 再建诗作节点,正文存入 content 属性 poem_node = Node("Poem", title=row["title"], content=row["content"]) graph.merge(poem_node, "Poem", "title") # 关系:作者 与 诗作 的连接 rel = Relationship(author_node, "AUTHOR_OF", poem_node) graph.create(rel)

逻辑说明:这里MERGE的作用是按name属性做去重匹配,如果不加这个约束,同一作者被导入两遍后,图谱里会出现两个同名节点,问答系统检索时会返回重复答案。Node的第三个参数"name"是 merge key,意思是“只要 name 相同就认为是同一个节点”。

参数说明:bolt://localhost:7687是 neo4j 的默认 Bolt 端口,如果你的 neo4j 装在远程服务器或 Docker 容器里,要把localhost改成对应的 IP;auth=("neo4j", "123456")里的用户名密码要在运行前建好,默认 neo4j 初始密码是neo4j,首次登录会被强制改密。

3. 问答系统的“大脑”:问句分类与实体识别的实现方式

3.1 QuestionClassifier.py 在做什么

问答系统里最难的不是检索,而是理解用户问题。用户可能会问“李白的诗有哪些”“静夜思的作者是谁”“描写秋天的诗有哪些”这三种完全不同句式的问题,但系统都需要把它们映射到对应的查询模板上。QuestionClassifier.py就是负责这件事的。

这个文件的核心是一个规则+模型混合分类器。它先加载vocabulary.json(词典)和poem_classification.json(分类标签),然后执行两件事:

  1. 实体识别:在问句里用关键词匹配找出诗名、作者、类型、朝代实体;
  2. 意图分类:根据识别出的实体类型组合,把问题归类到预设的查询模板上。

这类设计的精妙之处在于:它没有用复杂的 BiLSTM-CRF 做序列标注,而是用一个基于词典的匹配器完成了实体抽取。对于古诗领域这种封闭集合来说(作者就几百个、诗名就几千个),词典方案的准确率完全不输模型,而且速度极快。对课设来说,这避免了“模型训练半天,线上效果还不如规则”的尴尬。

# QuestionClassifier.py 的识别主流程(伪代码级还原) class QuestionClassifier: def __init__(self): self.vocab = load_json("vocabulary.json") # 词表,格式 { "author": ["李白", "杜甫"], "poem": ["静夜思"] } self.templates = load_json("poem_classification.json") def classify(self, question): entities = {"author": None, "poem": None, "type": None, "dynasty": None} for etype, word_list in self.vocab.items(): for word in word_list: if word in question: entities[etype] = word break # 根据命中的实体组合决定查询模板编号 if entities["poem"] and entities["author"]: return "查询指定作者对某诗的创作关系", entities if entities["type"] and not entities["author"]: return "查询某类型的诗作列表", entities return "无法识别", entities

逻辑说明:这里的循环顺序是有讲究的——先遍历实体类型,再遍历词表。词典文件里把词按类型分组,这样问句里的“静夜思”就不会被误识别成作者名,因为作者词表里没有这个词。但要注意词表不能有交叉词,比如vocabulary.json里如果“秋”同时属于类型和诗名,就可能匹配乱套。

参数说明:vocabulary.json的格式直接决定分类准确率,词表越长,实体召回越高;但词表里尽量不要收单字词,比如“春”这种字,因为它在太多古诗正文里出现,很容易错配。项目里词表做在model目录旁边,你也可以自己扩展,格式保持 JSON 数组即可。

3.2 Train.py 里的训练数据是怎么准备的

Train.py解决的问题是:当规则匹配不上的时候,系统需要有一个兜底方案来预测意图。它用trainData目录里的 CSV 训练一个文本分类模型,模型文件保存在model.model里。

训练脚本最关键的代码是数据加载部分:

# Train.py 数据读取与标签映射 import pandas as pd df = pd.read_csv("trainData/question_classification.csv") # CSV 两列:question(问题原文), label(意图编号) texts = df["question"].tolist() labels = df["label"].tolist() # 标签转索引,意图空间按 poem_classification.json 定义 label2id = {label: idx for idx, label in enumerate(set(labels))} id2label = {idx: label for label, idx in label2id.items()}

这里有一个非常容易踩的坑:trainData里的 CSV 的 label 列和poem_classification.json里的键必须完全一致,如果 CSV 里写的是中文描述(比如“查询作者”),模型训练没问题,但推理时main.py会用 json 里的键去查,两边对不上就会出现 KeyError。拿到项目后第一件事就是打开这两个文件对比一遍标签集合,不要直接开跑。

训练的模型结构通常是 TextCNN 或浅层 MLP,具体在Train.py里能看到网络代码。这个模型不需要训练很多轮,因为训练数据是模板化生成的,样本量小且模式相对固定,一般训练 20 到 30 个 epoch 就能收敛。如果在自己的机器上训练时 loss 不降,检查vocabulary.json里是否有未登录词,或者训练集里是否出现了空行。

4. 回答链路与查询拼装:main.py 和 get_answer.py 的协作方式

4.1 get_answer.py 怎么把意图翻译成 Cypher

get_answer.py是整个系统中承上启下的模块。它接收QuestionClassifier的输出(意图描述 + 实体字典),然后根据意图拼装出对应的 Cypher 查询语句。

比如用户问“李白的代表作有哪些”,分类器会输出:

{ "intent": "查询某作者的诗作列表", "entities": {"author": "李白", "poem": null, "type": null} }

get_answer.py拿到这个结果后,拼装的 Cypher 长这样:

MATCH (a:Author {name: "李白"})-[:AUTHOR_OF]->(p:Poem) RETURN p.title AS title LIMIT 10
# get_answer.py 的 Cypher 拼装逻辑 def answer(self, question): intent, entities = self.classifier.classify(question) if intent == "查询某作者的诗作列表": cql = "MATCH (a:Author {name: '%s'})-[:AUTHOR_OF]->(p:Poem) RETURN p.title AS title LIMIT 10" % entities["author"] elif intent == "查询诗作的作者": cql = "MATCH (p:Poem {title: '%s'})<-[:AUTHOR_OF]-(a:Author) RETURN a.name AS author" % entities["poem"] else: cql = None return self.graph.run(cql).data() if cql else "这个问题我还没学会,换个说法试试"

逻辑说明:这里的%s是字符串占位符,直接把实体值拼进语句里。注意 Cypher 里的属性值如果是字符串,必须用单引号包住;如果实体本身含有英文单引号(理论上古诗名没有,但作者名里可能混入空格),需要提前做清洗。

参数说明:LIMIT 10是为了防止一个作者的诗作过多导致返回结果撑爆内存;如果想让回答更丰富,可以把上限改成 20 或 50,但输出文本会变长,交互体验反而下降。项目里的图连接对象self.graph默认也是连接bolt://localhost:7687,如果你在第 2 章改过密码,这里必须同步改。

4.2 main.py 的口语化交互与兜底逻辑

main.py是面向用户的入口,跑起来后是一个命令行问答循环。它的逻辑朴素但实用:读入用户输入 → 交给QuestionClassifier→ 用get_answer检索 → 打印答案 → 继续等待输入。

# main.py 的问答循环骨架 from QuestionClassifier import QuestionClassifier from get_answer import AnswerSearcher def chat(): classifier = QuestionClassifier() searcher = AnswerSearcher() while True: question = input("我:").strip() if question in ("quit", "exit"): break answer = searcher.search(question) print("机器人:", answer)

这个脚本里没有用任何 Web 框架,交互就是终端的一问一答。如果你把课设目标定为“能跑通的命令行 Demo”,这已经完全够用;但如果你想把界面做成 Web 页面,可以把chat()里的input换成 Flask 路由,把searcher.search(question)的返回值直接作为 JSON 响应体,这是一条非常自然的改造路径。

4.3 查询速度与关系深度的权衡

系统里有个值得留意的细节:查询时最多走了两层关系。比如“李白的《静夜思》的赏析”,需要走Poem -> Author -> Poem这种跨实体关联,但脚本没有把关系路径超过三层的查询写死到模板里。这不是偷懒,而是因为古诗领域的知识相对扁平,两层关系已经能覆盖 90% 的自然提问。

如果后续要扩展,可以增加同类型诗作推荐这种三层查询:MATCH (p:Poem)-[:属于类型]->(t:Type)<-[:属于类型]-(rec:Poem) WHERE p.title <> rec.title RETURN rec.title。这里的关键点是<>排除自身,否则推荐会把原诗也带进来。

5. 避坑实操:neo4j 连不上、模型乱报错和依赖装不完

5.1 neo4j 一直报 “Unable to connect to localhost:7687”

现象:运行build_graph.py或main.py时,控制台抛出连接超时或ServiceUnavailable,但 neo4j 桌面版明明显示数据库在运行。

原因:八成是端口不对或认证信息过期。新装的 neo4j 默认 Bolt 端口是 7687,但如果你装的是 neo4j Desktop,每个数据库实例可能被分配了不同的端口(常见的是 7687、7688、7689 之间切换),而且初始密码neo4j在第一次登录后就被强行改掉了。

解决:打开 neo4j Desktop 里的数据库实例详情页,查看 Bolt URL 显示的实际端口号,把这个值同步改到build_graph.py和get_answer.py中Graph()的连接字符串里。密码同理,以你最后一次设置的为准。改完后建议先跑一句print(graph.run("RETURN 1").data())验证连通。

5.2 Python 依赖装了一堆,TensorFlow 版本还是对不上

现象:按requirements.txt安装后,Train.py运行时报AttributeError: module 'tensorflow' has no attribute 'placeholder'或keras相关错误。

原因:这个现象基本可以断定是 TensorFlow 2.x 和 1.x API 混用。项目正文里没有标注依赖版本,但Train.py里如果用了tf.placeholder、tf.Session这类老 API,只能装在 TF 1.15 或使用兼容模式。

解决:最省心的方案是新建一个 Python 3.7 的虚拟环境,安装tensorflow==1.15.0,然后逐个安装其余依赖。如果你只想跑通问答不动训练,可以直接跳过Train.py,因为仓库里已经附带了model.model训练好的模型文件,QuestionClassifier默认是加载模型而不是重新训练。给后续同学的提醒:拿到项目先终端跑python test.py,如果这个脚本能顺利出结果,说明环境基本没大问题。

5.3 分词和停用词文件的路径坑

现象:运行时找不到stop_words.utf8,报FileNotFoundError,但文件明明就在目录里。

原因:脚本里很可能用了相对路径,比如open("utils/stop_words.utf8"),而你是从别的目录启动 Python 的,因此当前工作目录不对,找不到文件。

解决:不要直接python main.py,先cd到项目根目录再执行;或者修改脚本里的文件加载方式,改用基于os.path.dirname(__file__)的绝对路径拼接,这样无论在哪个目录下启动都能找对位置。

# 推荐写法:用脚本所在目录拼路径,避免玄学路径问题 import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) stop_words_path = os.path.join(BASE_DIR, "utils", "stop_words.utf8")

5.4 merge_csv 之后数据量翻倍,图谱里出现重复作者

现象:本来是 500 首诗的 CSV,跑完merge_csv.py后作者节点多出好几倍,答问时“李白”出现两组不同结果。

原因:read_csv.py逐行读取时把 CSV 里同一个作者的变体(比如“李白”和“李太白”)当成两人,或者 merge 时没有按统一规则清洗空格和别名。

解决:在merge_csv.py里加一个字段归一化函数,把全角空格、首尾空格全部剔除,同时维护一个作者别名映射表。这是数据预处理最值钱的五分钟,能省掉后续大量手工清洗。

5.5 模型文件损坏导致分类结果全乱

现象:model.model文件存在,但每次分类结果都是随机或错乱。

原因:通常是模型文件与当前代码结构不匹配,常见于换电脑之后用 git 拉取时二进制文件被破坏,或者poem_classification.json里的标签顺序与模型训练时的顺序不一致。

解决:先跑Train.py重新训练一份模型覆盖原文件;如果不想训练,手动核对poem_classification.json的键顺序是否与trainData里的 label 编码顺序一致。模型文件没有“后悔药”,最好的习惯是训练完立即备份一份到项目外目录。

6. 快速验证与扩展思路:用 test.py 做健康检查,再顺手接上 Web 界面

拿到项目后第一件事,不要急着跑main.py,先执行python test.py。这个脚本相当于系统的自检程序,它会自动跑几个预设问题,并把回答打印出来。我的经验是,如果test.py五个问题里有四个能给出合理答案,那说明爬虫、建图、分类、检索这条链路是通的;如果某一个问题答案为空,优先排查对应实体是否已导入图谱,而不是怀疑代码逻辑。

python test.py # 期望看到类似输出: # 问题:静夜思的作者是谁 # 答案:['李白'] # 问题:杜甫写过哪些诗 # 答案:['春望', '登高', ...]

在验证完基础功能后,如果你想在这个项目上拿更高分,我建议走两步扩展。第一步,把命令行交互改成 Flask Web 页面。改造范围很小,新增一个app.py,把main.py里的chat()函数改成接收request.args.get("question")再返回 JSON 即可。第二步,在get_answer.py里增加一个“上下文记忆”功能,比如用户先问“李白的诗有哪些”,接着问“他出生于哪个朝代”,上一步的实体缓存会自动补全问题中的缺失主体,这种小小的体验优化在答辩时特别有说服力。

代码层面的参考写法如下:

from flask import Flask, request, jsonify from get_answer import AnswerSearcher app = Flask(__name__) searcher = AnswerSearcher() @app.route("/qa", methods=["GET"]) def qa(): question = request.args.get("question", "") if not question: return jsonify({"answer": "请输入问题"}) answer = searcher.search(question) return jsonify({"answer": answer}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)

参数说明:host="0.0.0.0"允许局域网内其他设备访问,演示时用手机访问电脑 IP 加 5000 端口会比较有展示效果;不需要对外开放的话,就改成host="127.0.0.1",避免安全风险。

从那以后,我每次拿到类似的课设资源,都会强制走一遍“先看数据流脚本顺序 → 再跑自检 → 最后改端口和密码”的流程,能少踩一半的坑。这份古诗词问答系统的最大价值,不在于它已经把模型跑通,而在于它的模块边界足够清晰:爬虫只管拿数据、建图只管写 neo4j、分类器只管识别、回答器只管拼 Cypher。你有任何一步想替换成自己的思路,都不会牵一发动全身。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 22:15:10

【共创稿事节】HarmonyOS 7重建失败的 6 种典型 case 与降级路径

重建失败的 6 种典型 case 与降级路径 3DGS 重建失败的原因五花八门&#xff1a;输入拍得太烂、物体本身没特征、机器内存不够、重建跑超时、设备不支持、结果出来质量太差。每种失败的降级方案不一样——OOM 该清数据还是保留&#xff1f;超时该用部分结果还是直接放弃&#x…

作者头像 李华
网站建设 2026/10/9 22:13:57

PHP+Vue+微信小程序学习交流平台毕设全流程开发实战

当年我做“基于PHPVue的微信小程序学习交流平台”这个毕业设计的时候&#xff0c;最大的感受不是技术有多难&#xff0c;而是“系统怎么从零到一跑通”这件事远比想象中琐碎。选题要求很直接&#xff1a;用户端用微信小程序&#xff0c;后台管理系统用 Web 页面&#xff0c;后端…

作者头像 李华
网站建设 2026/10/9 22:10:06

渗流模型实现与解读:从达西定律到孔隙网络的工程落地

1. 项目概述&#xff1a;渗流模型不是“水往下漏”那么简单“渗流模型的实现与解读”——这八个字乍看像教科书里的章节标题&#xff0c;但在我带过的十几个跨学科项目里&#xff0c;它几乎每年都会以不同面貌出现&#xff1a;某高校土木系做边坡稳定性仿真时卡在达西定律离散化…

作者头像 李华
网站建设 2026/10/9 22:08:37

论文降AI率实战指南:从检测原理到人工改写方法

1. 先搞清楚“AI率”到底在检测什么&#xff0c;再谈怎么降毕业季一到&#xff0c;我收到的学弟学妹私信里&#xff0c;最高频的问题从“论文格式怎么调”变成了“学长&#xff0c;我的论文被标了高AI率&#xff0c;怎么办”。有人直接把稿子丢进各种“降AI工具”里&#xff0c…

作者头像 李华
网站建设 2026/10/9 22:01:13

游戏测试实习面试全攻略:高频考点与答题框架

1. 拆解这场测试岗面试的真实考察逻辑1.1 为什么游戏测试实习的面试比想象中难很多人对游戏测试工程师这个岗位有误解&#xff0c;觉得就是“玩游戏找bug”&#xff0c;面试应该很水。我当年也是这么想的&#xff0c;结果第一次模拟面试就被问懵了。后来复盘才发现&#xff0c;…

作者头像 李华