简介面向计算机相关专业毕业生、课程设计与项目实战学习者这份源码以知识图谱为核心实现古诗词问答系统的完整流程涵盖知识图谱构建、问句解析、答案检索与前端交互等关键环节。压缩包共521个文件、约50.58MB核心为98个Python源码文件与18个pyc配合24个JSON数据文件、8个HTML页面以及CSS/JS前端资源另有129个txt文本和183张图片便于查看运行说明、界面效果与项目目录适合在毕设、课设、期末大作业或初期项目演示中直接参考。已有332人学习浏览资源包同时提供前端页面、Python后端代码和JSON/文本数据文件解压后可先通过README了解项目结构并快速启动。代码经过运行测试评审分为96.5可在已有问答逻辑上替换古诗词语料、扩展知识图谱实体与关系也能作为二次开发和答辩演示的基础。1. 古诗词问答系统本科毕设里“投入产出比”最高的知识图谱方向A同学拿到“基于知识图谱的古诗词问答系统python源码本科毕设”这个题目时第一反应通常是慌又要处理古诗文数据又要建知识图谱还得做问答和界面听着像三个大作业的量。但实际拆开看这个方向的每一块技术都有成熟套路数据有公开来源图谱有图数据库承接问答用规则模板就能覆盖大多数问法真正难的其实是“把几块串起来”而不是“发明新算法”。它适合有Python基础、想在毕设里同时展示数据工程能力和系统开发能力的人也适合想用最少的价格买一个完整项目做参照、然后二次开发的选手。这篇笔记就按“数据怎么备 → 图谱怎么建 → 问答怎么写 → 坑在哪”的顺序把这条路走一遍。2. 数据工程把古诗词JSON拆成知识图谱三元组的三步走2.1 本体设计实体、关系、属性先定好省得返工知识图谱的本质是“实体-关系-实体”的三元组所以动手写解析器之前先要把本体Ontology想清楚。古诗词问答系统里最常见的实体类型其实就五类诗人、诗作、朝代、诗句、意象。关系则是这些实体之间的连接比如“诗人写诗作”“诗作属于朝代”“诗作包含诗句”“诗句蕴含意象”。我一般会先用一张表把本体定下来发给导师确认后再写代码。这个动作看起来慢实际是省时间的——很多同学做到一半发现“意象”这个实体没设计进去回头补数据比一开始花半天设计更痛苦。实体类型关键属性示例Poet诗人name、dynasty、style、birth_year某唐代诗人、山水田园派Poem诗作title、dynasty、genre诗/词/曲《溪上偶题》Dynasty朝代name、start_year、end_year唐、宋Line诗句content、poem_id“山月照人归溪风送客远”Image意象name、meaning月、柳、流水关系上我习惯用“主谓宾”命名法(Poet)-[:WRITE]-(Poem)、(Poem)-[:BELONG_TO]-(Dynasty)、(Poem)-[:CONTAINS]-(Line)、(Line)-[:EVOKE]-(Image)。命名别看是小事后面写Cypher查询全靠它大小写和方向错了查询结果就是空的。2.2 解析器编写从原始JSON到三元组的Python实现数据源通常是一份JSON文件每首诗包含标题、作者、朝代、正文段落。解析器的任务就是循环这份JSON输出一组组(头实体, 关系, 尾实体)。这个环节我一般直接用Python标准库加一个简单的清洗函数不需要引入pandas量级不大时反而更直观。import json def parse_poetry(raw_path: str, output_path: str): 把原始古诗JSON解析为三元组CSV with open(raw_path, r, encodingutf-8) as f: poems json.load(f) triples [] for poem in poems: author poem.get(author, ).strip() title poem.get(title, ).strip() dynasty poem.get(dynasty, ).strip() # 三元组1诗人 - 写 - 诗作 if author and title: triples.append((author, WRITE, title)) # 三元组2诗作 - 属于 - 朝代 if title and dynasty: triples.append((title, BELONG_TO, dynasty)) # 三元组3诗作 - 包含 - 诗句 for line in poem.get(paragraphs, []): line line.strip().replace(, ).replace(。, ) if line: triples.append((title, CONTAINS, line)) with open(output_path, w, encodingutf-8) as f: for h, r, t in triples: f.write(f{h}\t{r}\t{t}\n) print(f生成 {len(triples)} 条三元组)代码逻辑不复杂但有几个点值得强调。第一.strip()必须加很多开源数据的作者名和标题自带空格或换行。第二诗句合并时去掉标点是因为问答场景里用户大概率不会带着逗号问去掉标点能让检索更宽松。第三输出用\t分隔是为了后面导入Neo4j时统一用CSV格式处理。这段代码跑完之后检查一下三元组的条数和样本看看有没有明显的脏数据。不要急着进下一步数据错了后面全错到时排查起来才是真正的耗时大头。2.3 清洗三规则去重、对齐、补全一个都不能省数据清洗是知识图谱项目里最不性感但最决定成败的环节。我总结成三条硬规则每条都吃过亏。去重是第一条。JSON里可能出现同一首诗被抓了两次或者同一诗人名字格式不一样“王维”和“王 维”。我通常在解析器里维护一个集合(头实体, 关系, 尾实体)三元组整体去重因为图数据库导入时重复执行CREATE会生成一模一样的多条边到后面统计“写了多少首诗”就会翻一倍。对齐是第二条。数据源里朝代可能写“唐”也可能写“唐朝”、“唐代”这会导致图谱里出现“两个唐朝节点”。我一般准备一个简单的别名映射表把“唐代”“唐”统一映射到唐这个规范名解析时直接查表替换。补全是最容易被忽略的。很多JSON里的诗没有朝代、没有作者这时候不能直接把空字符串写进三元组否则图谱里会出现(, WRITE, 诗名)这种无效边。处理方式就是跳过或者打标记在日志里输出缺失数量做到心中有数。3. 存储与检索Neo4j导入与前几类查询的Cypher写法3.1 为什么选Neo4j多跳查询对比MySQL的JOIN写法这个项目叫“知识图谱”存储层就绕不开图数据库。对比MySQLNeo4j的核心优势体现在“多跳查询”上——比如用户问“某诗人的诗里用了哪些意象”在MySQL里要先查诗作、再查诗句、再关联意象三张表JOIN两次在Neo4j里一条Cypher就能从诗人节点沿着关系走到意象节点。更关键的是当查询路径再深一层“与某诗人同朝代的诗人写过的诗有哪些意象”MySQL的SQL会变得非常冗长而Neo4j只是在路径模式里多加一个节点层级而已。选Neo4j还有一个答辩层面的好处它自带可视化界面Neo4j Browser里可以直接展示图谱路径演示效果比贴SQL查询结果强太多。数据集不大的情况下社区版足够用不需要上并发集群那些企业功能。3.2 LOAD CSV 批量导入文件准备与Cypher命令数据进Neo4j最常见的做法是把解析好的三元组分成节点表poets.csv、poems.csv、lines.csv和关系表relations.csv然后用LOAD CSV批量导入。这里有个新手常犯的错把三元组直接一股脑塞进一个CSV导入时每个字段都建节点结果产生一堆重复实体。我习惯的做法是先导入实体节点用MERGE而不是CREATE。MERGE会先查找节点是否存在存在就不重复建这正好和前面清洗阶段的“去重规则”呼应。// 导入诗人节点MERGE保证同一诗人只出现一次 LOAD CSV WITH HEADERS FROM file:///poets.csv AS row MERGE (poet:Poet {name: row.name}) SET poet.dynasty row.dynasty, poet.style row.style// 导入诗作-诗人关系先MATCH到已有节点再MERGE边 LOAD CSV WITH HEADERS FROM file:///relations.csv AS row MATCH (poem:Poem {title: row.title}) MATCH (poet:Poet {name: row.author}) MERGE (poet)-[:WRITE]-(poem)参数说明WITH HEADERS表示第一行是列名file:///指向Neo4j安装目录下的import文件夹CSV必须放那里MERGE在节点上防重复在关系上防重复边。如果导入时提示找不到节点九成是CSV里实体名和导入阶段不一致注意检查空格和别名映射。3.3 查询模板设计先定问题类型再写Cypher才不走弯路问答系统的核心不是“直接执行任意自然语言”而是先把常见的问法抽象成有限个类型每个类型对应一条Cypher模板。我按毕设场景拆了六类覆盖了绝大多数需求意图用户问法示例Cypher模板QUERY_AUTHOR“《溪上偶题》的作者是谁”MATCH (p:Poem {title:$title})-[:WRITE]-(a:Poet) RETURN a.nameQUERY_POEMS“某唐代诗人写过哪些诗”MATCH (p:Poet {name:$name})-[:WRITE]-(poem:Poem) RETURN poem.titleQUERY_DYNASTY“某位诗人是哪个朝代的”MATCH (p:Poet {name:$name})-[:BELONG_TO]-(d:Dynasty) RETURN d.nameQUERY_IMAGE“某首诗里有哪些意象”MATCH (p:Poem {title:$title})-[:CONTAINS]-(l:Line)-[:EVOKE]-(i:Image) RETURN i.nameQUERY_SAME_DYNASTY“和某诗人同朝代的有谁”MATCH (p:Poet {name:$name})-[:BELONG_TO]-(d:Dynasty)-[:BELONG_TO]-(other) RETURN other.nameQUERY_LINE“某意象出现了哪些诗句”MATCH (i:Image {name:$name})-[:EVOKE]-(l:Line) RETURN l.content建议把$title、$name统一用参数传递一是防注入二是Neo4j可以缓存执行计划查询更快。写模板时注意关系方向Neo4j的关系是有向的(p)-[:WRITE]-(poem)和(poem)-[:WRITE]-(p)等价但手写时方向反了会直接返回空结果这个问题我在避坑章里还会细说。4. 问答链路意图识别、槽位填充与Flask接口的完整实现4.1 问句解析规则模板为什么比深度学习更合适毕设场景问答系统里最容易被过度设计的是“意图识别”这一环。很多同学一看“问答”两个字就想上BERT、微调大模型实际做出来效果不稳定不说答辩时也讲不清“你的模型为什么在这个数据集上有效”。古诗词问答的提问方式非常固定翻来覆去就是“作者是谁”“哪个朝代”“写过什么诗”用规则模板做意图匹配准确率能到95%以上而且每一个判断逻辑都可以直接贴到论文里评审一看就懂。规则模板的核心思路是两步先匹配意图关键词“作者”“朝代”“意象”再抽取实体诗名、诗人名。实体抽取用词典匹配就够了把清洗后的所有诗人名、诗名、意象名加载成一个列表问句里哪个词命中列表就作为槽位值填入模板。这套方法对毕设数据规模来说是“刚刚好”的复杂度。4.2 问答核心实现从问句到Cypher再到答案的Python代码我习惯把问答核心单独放一个模块输入是用户问句输出是(答案文本, 证据Cypher, 证据路径)三元组这样后面做前端展示和答辩演示都方便。核心代码如下# -*- coding: utf-8 -*- from py2neo import Graph import re graph Graph(bolt://localhost:7687, auth(neo4j, password)) # 加载实体词典来自清洗阶段的输出 def load_dict(path: str) - list: with open(path, r, encodingutf-8) as f: return [line.strip() for line in f if line.strip()] poem_dict load_dict(poem_titles.txt) poet_dict load_dict(poet_names.txt) image_dict load_dict(image_names.txt) def extract_entity(question: str, candidates: list): 基于词典匹配返回第一个命中的实体名 for entity in candidates: if entity in question: return entity return None def parse_question(question: str): 规则匹配先定意图再抽实体返回Cypher模板和参数 # 问作者 if (作者 in question and 是谁 in question) or 谁写的 in question: title extract_entity(question, poem_dict) if title: cypher (MATCH (p:Poem {title:$title})-[:WRITE]-(a:Poet) RETURN a.name AS answer) return {intent: QUERY_AUTHOR, cypher: cypher, params: {title: title}} # 问诗作 if 写过哪些诗 in question or 作品有哪些 in question: poet extract_entity(question, poet_dict) if poet: cypher (MATCH (p:Poet {name:$name})-[:WRITE]-(poem:Poem) RETURN collect(poem.title) AS answer) return {intent: QUERY_POEMS, cypher: cypher, params: {name: poet}} # 问意象 if 意象 in question or 表达了什么 in question: title extract_entity(question, poem_dict) if title: cypher (MATCH (p:Poem {title:$title})-[:CONTAINS]-(l:Line) -[:EVOKE]-(i:Image) RETURN collect(i.name) AS answer) return {intent: QUERY_IMAGE, cypher: cypher, params: {title: title}} return None def run_answer(question: str): 执行问答流程返回答案与证据路径 parsed parse_question(question) if not parsed: return {answer: 我没理解这个问题换个问法试试, path: []} result graph.run(parsed[cypher], **parsed[params]).data() # 同时查一条证据路径用于前端展示 path_cypher parsed[cypher].replace(RETURN, RETURN path,).split( AS )[0] if result: answer result[0].get(answer, 未找到) else: answer 未找到答案 return {answer: answer, intent: parsed[intent], params: parsed[params]}代码里值得注意的几个参数和设计选择。第一graph.run(cypher, **params)用的是参数化查询$title和$name是Cypher里的占位符传参方式保证了安全性。第二collect()把多行结果聚合成一个列表这样前端直接显示[诗A,诗B]就行不用再处理后端返回结构。第三这里刻意不在parse_question里执行查询而是把Cypher和参数返回给上层好处是以后想加日志、加证据路径展示、或者做批量测试都不用改逻辑。4.3 接口与前端Flask路由与演示页面的取舍毕设演示环节系统至少要有一个网页能让评委输入问题并看到答案。后端我用Flask搭两个路由就够一个服务静态页面一个接收问题并返回JSON。没必要上重型前端框架一个HTML页面加一点原生JavaScript就能完成交互。from flask import Flask, request, jsonify, render_template app Flask(__name__) app.route(/) def index(): # 渲染前端页面 return render_template(index.html) app.route(/api/ask, methods[POST]) def ask(): data request.get_json() question data.get(question, ) result run_answer(question) return jsonify(result) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)前端页面这部分我强烈建议在index.html里放一个“示例问题”区域把预设的六类问题做成按钮点击就能填充到输入框。这个设计不是偷懒而是答辩时的刚需——评委不可能想得出合适的问法如果你让他自由输入他问出系统没覆盖的句型场面就尴尬了。给几个示例问题让他点既能保证演示流畅又展示了你对意图类型的完整覆盖。5. 避坑手册跑通古诗词知识图谱问答的5个高频翻车点5.1 py2neo版本升级引发的“玄学”报错现象按网上的老教程写的from py2neo import Node, Relationship, Graph在本地能跑换了台电脑就报ImportError。代码没动过报错却变了。原因py2neo的4.x和5.x版本API不兼容。5.x里NodeMatcher被挪到了py2neo.matching模块Graph.run()的返回结果也从RecordList变成了Cursor很多老代码直接照搬就会出事。解决最稳的办法是把版本锁死。项目里建一个requirements.txt明确写py2neo4.3.0或neo4j5.14.0按你用的Neo4j版本选不要用pip install py2neo裸装。如果代码已经报了NodeMatcher的错把导入改成from py2neo.matching import NodeMatcher就能兼容5.x。5.2 中文CSV导入Neo4j的乱码与路径问题现象LOAD CSV导入后节点名字变成æåº或者“唐”显示成乱码更诡异的是有些行导入成功有些行失败。原因Windows下导出的CSV默认是GBK编码Neo4j的LOAD CSV只认UTF-8。另一个常见坑是CSV里的字段值带着双引号或换行符被解析成了新行。解决先用记事本或Python把CSV统一转成UTF-8可以带BOM再放进Neo4j的import目录。字段值里如果含逗号记得用引号包起来否则会被当成新列。检查方式很简单导入前先用LOAD CSV WITH HEADERS FROM file:///poets.csv AS row RETURN row LIMIT 5预览几行能看到中文就说明编码没问题。5.3 查询超时图谱规模多大才会卡现象单条查询正常但用户连续问几个问题后接口响应越来越慢甚至Neo4j Browser里出现Neo.DatabaseError.General.UnknownError日志提示查询超时。原因古诗词语料如果导入得很“全”包括每一句诗、每个意象、每个典故图谱节点会到几万甚至十几万。这时如果查询里写了不带限制条件的路径匹配比如MATCH (a:Poet)--(b:Poem)--(c:Image) RETURN c.nameNeo4j会扫描大量中间节点。解决第一所有查询模板的路径里都加上节点类型避免全表扫描第二在RETURN前加LIMIT 10答案本来就不需要几十条第三给高频属性建索引启动时执行CREATE INDEX FOR (p:Poem) ON (p.title)诗人名、诗名、意象名都各建一个。这三步做完小数据量下查询基本是毫秒级。5.4 分词器拆碎诗人名和诗名现象用jieba做问句分词输入“某唐代诗人的代表作有哪些”分词结果把诗人的三字名拆成了两个字加一个字实体词典匹配不到返回“未找到答案”。原因jieba默认词典是通用语料人名的识别能力很差尤其是古诗人名本身在通用语料里出现频率就低。解决两条路。一种是绕开分词直接做“子串匹配”也就是把整个问句和词典里的实体名做in判断这是我在4.2节代码里用的方法简单且完全避开分词误差。另一种是必须用分词时把全部诗人名、诗名加载进jieba的自定义词典jieba.load_userdict(poet_names.txt)并在加载时指定词频足够高。我个人更推荐子串匹配毕设场景的实体名长度短、没有歧义直接匹配比分词后匹配更可靠。5.5 答辩现场Neo4j服务没启动的“黑匣子”现象答辩前一切正常上台演示时页面打开输入问题结果转圈转了几秒然后报错ServiceUnavailable: The server is not available。原因Neo4j是独立服务电脑重启后它不会自动启动。大部分同学是在自己笔记本上开发从没模拟过“冷启动”的演示流程现场才发现服务没起。解决写一个启动检查脚本放在项目根目录双击就能跑# 检查Neo4j状态未启动则拉起 neo4j status || neo4j start sleep 5 curl -s http://localhost:7474 || echo Neo4j连接失败请检查端口答辩前把整个流程从“开机→启动数据库→启动Flask→打开网页”完整走一遍。另外把Neo4j的auth密码写进环境变量或配置文件别hardcode在代码里还临时换密码现场最容易出这种低级问题。6. 给答辩加分的最后一步答案溯源与证据路径回显如果系统只是“问一句答一句”评委很难直观感受到知识图谱和普通数据库检索的区别。我建议在最后花半天时间加一个“答案溯源”功能回答完用户问题后在前端页面上同时展示一条从问句实体到答案实体的图谱路径让评委亲眼看到“知识图谱上的推理链路”。实现方式不复杂。在run_answer里我已经预留了path这个返回字段只需要在生成Cypher时用RETURN path替代RETURN answer然后把路径上的节点和关系解析成前端可渲染的结构。比如问“某诗的作者是哪个朝代的”图谱路径就是诗作节点 →(WRITE)→ 诗人节点 →(BELONG_TO)→ 朝代节点前端用三个节点和两个箭头就可以把这个链路画出来。比折线图更好的还有一个设计把路径上的中间节点做成可点击的标签点击诗人节点自动填充新问题“这个诗人写过哪些诗”。这个交互既自然又展示了系统具备“多轮引导”的能力评委对这个细节的印象分往往很高。再加一个同朝代诗人推荐比如回答完“某诗人是哪个朝代的”之后追加一句“同朝代还有哪些诗人”本质就是把3.3节里的QUERY_SAME_DYNASTY模板串到主查询后面数据量小的时候一次请求就能返回。这两个功能加起来不超过50行代码却能让系统从“单点问答”变成“可探索的知识图谱”答辩时你可以自信地说“这不仅是问答还具备沿着图谱关系做发散检索的能力。”我自己的习惯是每次给系统加一个功能前都先问一句“这个功能在答辩时能不能用一句话讲清楚”讲不清楚的就砍掉。知识图谱问答的毕设核心从来不是算法多深而是链路完整、逻辑清晰、演示稳定。沿着这个标准走数据清洗做扎实、查询模板设计合理、再把证据路径这个亮点加上这套系统的完成度已经超过大多数同题目的作品。希望帮到你。本文还有配套的精品资源点击获取