基于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 到 neo4jbuild_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, namerow[author], dynastyrow[dynasty]) graph.merge(author_node, Author, name) # 再建诗作节点正文存入 content 属性 poem_node Node(Poem, titlerow[title], contentrow[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改成对应的 IPauth(neo4j, 123456)里的用户名密码要在运行前建好默认 neo4j 初始密码是neo4j首次登录会被强制改密。3. 问答系统的“大脑”问句分类与实体识别的实现方式3.1 QuestionClassifier.py 在做什么问答系统里最难的不是检索而是理解用户问题。用户可能会问“李白的诗有哪些”“静夜思的作者是谁”“描写秋天的诗有哪些”这三种完全不同句式的问题但系统都需要把它们映射到对应的查询模板上。QuestionClassifier.py就是负责这件事的。这个文件的核心是一个规则模型混合分类器。它先加载vocabulary.json词典和poem_classification.json分类标签然后执行两件事实体识别在问句里用关键词匹配找出诗名、作者、类型、朝代实体意图分类根据识别出的实体类型组合把问题归类到预设的查询模板上。这类设计的精妙之处在于它没有用复杂的 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 怎么把意图翻译成 Cypherget_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 的虚拟环境安装tensorflow1.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(host0.0.0.0, port5000)参数说明host0.0.0.0允许局域网内其他设备访问演示时用手机访问电脑 IP 加 5000 端口会比较有展示效果不需要对外开放的话就改成host127.0.0.1避免安全风险。从那以后我每次拿到类似的课设资源都会强制走一遍“先看数据流脚本顺序 → 再跑自检 → 最后改端口和密码”的流程能少踩一半的坑。这份古诗词问答系统的最大价值不在于它已经把模型跑通而在于它的模块边界足够清晰爬虫只管拿数据、建图只管写 neo4j、分类器只管识别、回答器只管拼 Cypher。你有任何一步想替换成自己的思路都不会牵一发动全身。希望帮到你。本文还有配套的精品资源点击获取