
简介这是一套面向计算机相关专业本科生与项目实战学习者的古诗词问答系统源码以知识图谱为核心技术路线可作为毕业设计、课程设计或期末大作业的完整参考方案。项目经导师指导并通过答辩评审平均分达96.5分代码均经过运行测试适合人工智能、计科、通信、自动化等专业学生下载学习也便于基础较好的读者在此基础上二次修改、扩展功能。压缩包共521个文件约50.58MB其中98个py文件承载问答与图谱构建逻辑24个json与129个txt用于数据存储和语料配置另有html、css、js等前端页面资源及jpg图片、字体文件整体结构清晰、模块分明。目前已有330人学习下载。读者可从中获取知识图谱构建、古诗词实体关系抽取、问答交互实现等关键环节的完整代码与目录组织思路并借助README说明快速理解项目运行方式为毕设答辩与项目立项提供可复用的实践范本。1. 从一份古诗词问答毕设说起知识图谱到底解决了什么问题搜「基于知识图谱的古诗词问答系统python源码」的人多半正卡在毕设选题上想做一个看起来有技术含量、又能跑出效果、还能写进论文的系统。古诗词这个题材很讨巧——数据公开、语料干净、领域边界清晰天然适合用知识图谱来组织。但真正动手时你会发现难点从来不是「怎么把诗存进数据库」而是「用户问『李白写给孟浩然表达仰慕的诗』时系统凭什么能答出来」。传统做法是把诗、作者、朝代塞进几张关系表用 SQL 的 LIKE 去匹配关键词。这种方案在「静夜思的作者是谁」这类问题上能蒙对一旦问法变复杂、涉及多跳关系诗人→好友→作品→意象SQL 就彻底歇菜。知识图谱的价值就在这里它把「诗人、诗作、朝代、意象、情感、地点」抽象成实体把「创作、赠予、属于、包含」抽象成关系让机器能顺着关系链推理。这套思路在工业场景下的知识图谱设计里同样成立只是古诗词的实体类型更少、关系更规整特别适合本科毕设的体量。这篇笔记面向三类人正在做毕设、需要一套能复现的 Python 方案的同学想搞懂知识图谱构建全流程、但被 Neo4j 和 Cypher 劝退的入门者以及已经有一版系统、但问答准确率上不去、想找优化点的人。我会按「数据怎么来 → 图谱怎么建 → 问答怎么答 → 坑在哪」的顺序把一套可落地的方案讲透代码能直接抄参数能直接改。2. 古诗词知识图谱的数据从哪来、实体关系怎么定2.1 语料来源与清洗别一上来就爬全唐诗很多人第一反应是写个 python 爬虫去抓古诗网站结果抓回来一堆 HTML 标签、乱码、重复条目清洗花的时间比建图还多。我的建议是优先用现成的结构化语料。常见做法是找公开的古诗词 JSON 数据集通常包含标题、作者、朝代、正文、标签几个字段几万首诗的体量对毕设完全够用。如果非要自己爬也要先明确只抓哪几个字段别把整页 HTML 存下来。清洗阶段要处理四类脏数据一是正文里的全角空格和换行二是同一首诗在不同来源下的重复三是作者名的不统一「李白」和「李太白」要归一四是缺失字段。下面这段清洗脚本可以直接用import json import re def clean_poem(raw): # 去掉正文里的空白符和不可见字符 content re.sub(r[\s\u3000], , raw.get(content, )) # 作者名归一去掉「唐」「宋」等前缀修饰 author re.sub(r[\[\]()], , raw.get(author, )).strip() # 标题去掉书名号 title raw.get(title, ).strip(《》) # 过滤掉正文过短或字段缺失的脏数据 if len(content) 5 or not author or not title: return None return { title: title, author: author, dynasty: raw.get(dynasty, 未知), content: content, tags: raw.get(tags, []) } def load_and_clean(path): seen set() poems [] with open(path, r, encodingutf-8) as f: for line in f: item clean_poem(json.loads(line)) if item is None: continue # 用「标题作者」做去重键 key item[title] | item[author] if key in seen: continue seen.add(key) poems.append(item) return poems if __name__ __main__: data load_and_clean(poems_raw.jsonl) print(f清洗后剩余 {len(data)} 首)这段逻辑的关键在clean_poem里的过滤条件len(content) 5是为了剔除残句作者和标题为空直接丢弃。去重键选「标题作者」而不是只用标题是因为不同诗人可能写同名诗。参数上如果你的数据集里作者字段带朝代前缀如「唐·李白」把正则改成按「·」分割取后半段即可。2.2 实体与关系的本体设计先画清楚再写代码本体建模是知识图谱的地基很多毕设翻车就翻在这里——实体类型定得太细关系定义得太多最后自己都理不清。古诗词领域的本体其实可以很克制我一般会收敛到五类实体、五类关系实体类型示例说明诗人 Poet李白、杜甫核心实体连接最多诗作 Poem静夜思图谱的主体节点朝代 Dynasty唐、宋用于按时代筛选意象 Image明月、杨柳支撑语义问答的关键情感 Emotion思乡、送别让问答能答「表达什么感情」关系则围绕「谁写了什么、诗里有什么、属于哪个时代」展开Poet-创作-Poem、Poem-属于-Dynasty、Poem-包含意象-Image、Poem-表达情感-Emotion、Poet-好友-Poet。最后这条「诗人好友」关系是加分项它让「李白和谁关系好」这类多跳问题成为可能但需要额外整理一份诗人交游数据量力而行。提示本体不是越全越好。毕设答辩时老师更看重「关系设计是否合理、能否支撑问答」而不是实体类型有多少。五类实体足够覆盖 90% 的常见问题。2.3 用 Neo4j 建图从 CSV 到 Cypher 的完整链路选 Neo4j 而不是自己写图结构是因为它自带 Cypher 查询语言和可视化界面答辩演示时直接截图就很好看。安装 Neo4j Desktop 后新建一个本地库默认地址是bolt://localhost:7687。建图分两步先把清洗后的数据转成 Neo4j 能导入的 CSV再用 Cypher 的LOAD CSV批量写入。import csv def export_nodes(poems, node_path, rel_path): poets, dynasties, images, emotions set(), set(), set(), set() with open(node_path, w, newline, encodingutf-8) as nf, \ open(rel_path, w, newline, encodingutf-8) as rf: nw csv.writer(nf) rw csv.writer(rf) nw.writerow([id, name, label]) rw.writerow([start, end, type]) for p in poems: poets.add(p[author]) dynasties.add(p[dynasty]) for tag in p[tags]: images.add(tag) # 诗作节点 nw.writerow([p[title], p[title], Poem]) # 创作关系 rw.writerow([p[author], p[title], 创作]) rw.writerow([p[title], p[dynasty], 属于]) for tag in p[tags]: rw.writerow([p[title], tag, 包含意象]) for name in poets: nw.writerow([name, name, Poet]) for name in dynasties: nw.writerow([name, name, Dynasty]) for name in images: nw.writerow([name, name, Image]) export_nodes(load_and_clean(poems_raw.jsonl), nodes.csv, rels.csv)导出后在 Neo4j Browser 里执行导入语句。注意LOAD CSV默认从数据库的 import 目录读文件要把 CSV 拷进去// 建唯一约束避免重复节点 CREATE CONSTRAINT IF NOT EXISTS FOR (n:Poem) REQUIRE n.id IS UNIQUE; // 导入节点 LOAD CSV WITH HEADERS FROM file:///nodes.csv AS row MERGE (n:Node {id: row.id}) SET n.name row.name, n:label row.label; // 导入关系 LOAD CSV WITH HEADERS FROM file:///rels.csv AS row MATCH (a:Node {id: row.start}), (b:Node {id: row.end}) CALL apoc.create.relationship(a, row.type, {}, b) YIELD rel RETURN count(rel);这里用了 APOC 插件来动态创建关系类型因为关系类型创作、属于、包含意象是变量普通 Cypher 写不了。装 APOC 只需在 Neo4j Desktop 的插件页勾选安装。参数上MERGE保证幂等重复导入不会产生重复节点如果你数据量大超过十万节点把LOAD CSV换成apoc.periodic.iterate分批提交否则容易内存溢出。3. 问答系统怎么把自然语言变成图谱查询3.1 意图识别与实体抽取规则先行模型兜底问答系统的核心是把「李白写的关于明月的诗有哪些」翻译成 Cypher。这一步分两个子任务识别用户意图是问作者、问意象、还是问情感抽取实体李白、明月。毕设阶段我不建议一上来就上 BERT 微调成本高且效果不一定稳。更务实的做法是「规则模板 词典匹配」先维护一份诗人、意象、情感的词典用 jieba 分词后匹配实体再用关键词判断意图。import jieba POETS {李白, 杜甫, 王维, 孟浩然, 白居易} IMAGES {明月, 杨柳, 流水, 落花, 孤舟} EMOTIONS {思乡, 送别, 爱国, 爱情} def extract_entities(question): words set(jieba.cut(question)) return { poet: list(words POETS), image: list(words IMAGES), emotion: list(words EMOTIONS) } def detect_intent(question, entities): # 按优先级判断意图先具体后笼统 if entities[emotion]: return query_by_emotion if entities[image] and entities[poet]: return query_poet_image if entities[image]: return query_by_image if entities[poet]: return query_by_poet return unknownextract_entities用集合交集做匹配简单但有效。detect_intent的优先级顺序很关键同时出现诗人和意象时要优先走组合查询否则会漏掉「李白明月」这种精确需求。词典可以随着测试不断补充比如发现「玉盘」也是月亮的意象就加进IMAGES。这套规则方案的准确率在古诗词这种封闭领域能到 80% 以上足够毕设答辩。3.2 从意图到 Cypher模板映射与参数化查询意图确定后用模板生成 Cypher。这里必须用参数化查询别用字符串拼接否则遇到带引号的实体名会直接报错也是 SQL 注入的同类隐患。from neo4j import GraphDatabase CYPHER_TEMPLATES { query_by_poet: MATCH (p:Poet {name: $poet})-[:创作]-(poem:Poem) RETURN poem.name AS title LIMIT 10 , query_by_image: MATCH (poem:Poem)-[:包含意象]-(i:Image {name: $image}) RETURN poem.name AS title LIMIT 10 , query_poet_image: MATCH (p:Poet {name: $poet})-[:创作]-(poem:Poem)-[:包含意象]-(i:Image {name: $image}) RETURN poem.name AS title LIMIT 10 , query_by_emotion: MATCH (poem:Poem)-[:表达情感]-(e:Emotion {name: $emotion}) RETURN poem.name AS title LIMIT 10 } class PoemQA: def __init__(self, uri, user, password): self.driver GraphDatabase.driver(uri, auth(user, password)) def query(self, intent, entities): template CYPHER_TEMPLATES.get(intent) if not template: return [] params {k: v[0] for k, v in entities.items() if v} with self.driver.session() as session: result session.run(template, **params) return [record[title] for record in result] def close(self): self.driver.close() qa PoemQA(bolt://localhost:7687, neo4j, 你的密码) print(qa.query(query_poet_image, {poet: [李白], image: [明月]}))params的构造是重点entities里每个值是列表取第一个元素作为查询参数。LIMIT 10是防止返回过多结果拖慢前端。如果你的图谱里诗作节点用的是id而不是name做唯一键把RETURN poem.name改成RETURN poem.id即可。连接参数里密码要换成你本地 Neo4j 设置的密码默认用户是neo4j。3.3 答案生成把查询结果拼成一句人话查到诗名列表后直接返回[静夜思, 月下独酌]太生硬。加一层模板把结果包装成自然语言体验立刻不一样def generate_answer(intent, entities, titles): if not titles: return 没有找到符合条件的诗作换个问法试试 poet entities.get(poet, [])[0] image entities.get(image, [])[0] if intent query_poet_image: return f{poet}写过{len(titles)}首包含「{image}」的诗比如{、.join(titles[:5])}。 if intent query_by_poet: return f{poet}的作品有{、.join(titles[:5])}。 return f找到相关诗作{、.join(titles[:5])}。模板里用len(titles)给出数量、用join列出前五首信息密度刚好。如果要做成 Web 演示把这段接到 Flask 的一个/ask接口上前端一个输入框加一个结果区半天就能搭完。4. 避坑与排查那些让毕设卡壳的真实问题4.1 中文分词把诗人名切碎实体匹配全落空现象问「杜甫的诗」系统返回空结果但图谱里明明有杜甫节点。原因jieba 默认词典不含「杜甫」这类专名可能切成「杜」「甫」导致词典匹配失败。解决用jieba.add_word(杜甫)把诗人、意象词典全部加载进分词器或者直接用jieba.load_userdict(dict.txt)批量导入。这一步不做后面所有实体抽取都是空中楼阁。4.2 Neo4j 导入 CSV 报「找不到文件」现象LOAD CSV执行时报Couldnt load the external resource。原因Neo4j 出于安全限制只允许从数据库的 import 目录读文件你放在项目目录下的 CSV 它读不到。解决找到 Neo4j 安装目录下的import文件夹Desktop 版在数据库设置里能看到路径把 CSV 拷进去路径写成file:///nodes.csv。另外注意 Windows 下路径分隔符要用正斜杠。4.3 关系类型动态创建失败提示未知函数现象执行apoc.create.relationship报函数不存在。原因APOC 插件没装或没启用。解决在 Neo4j Desktop 对应数据库的 Plugins 页安装 APOC安装后重启数据库。如果用的是社区版手动安装还要在neo4j.conf里加dbms.security.procedures.unrestrictedapoc.*放开权限。4.4 问答结果重复同一首诗出现好几次现象查「李白的诗」返回列表里「静夜思」出现三遍。原因图谱里同一首诗被多次CREATE而不是MERGE产生了重复节点。解决导入时统一用MERGE代替CREATE并给Poem的id建唯一约束。已经产生重复的用MATCH (n:Poem) WITH n.name AS name, collect(n) AS nodes WHERE size(nodes) 1找出重复组再删。4.5 多跳查询超时页面转圈半天没反应现象问「李白好友写的诗」查询卡住。原因多跳关系没建索引Neo4j 全图扫描。解决给高频查询的实体属性建索引CREATE INDEX FOR (p:Poet) ON (p.name)意象和情感同理。另外在 Cypher 里尽早用LIMIT截断别等全部匹配完再限制。5. 让问答更聪明从模板匹配到语义扩展的进阶技巧基础版跑通后想拿高分就得在「答得准、答得全」上做文章。第一个技巧是同义词扩展用户问「月亮」图谱里存的是「明月」直接匹配就漏了。维护一张同义词表查询前先把用户词映射到标准意象SYNONYMS { 月亮: 明月, 月: 明月, 玉盘: 明月, 柳树: 杨柳, 离别: 送别 } def normalize(entities): for key in (image, emotion): entities[key] [SYNONYMS.get(w, w) for w in entities[key]] return entities第二个技巧是基于图路径的推理。比如问「和李白同时代的诗人」图谱里没有直接的「同时代」关系但可以通过(p1:Poet)-[:创作]-(:Poem)-[:属于]-(d:Dynasty)-[:属于]-(:Poem)-[:创作]-(p2:Poet)这条路径推出来。写 Cypher 时把这条路径固化成一个模板就能回答一批「隐性关系」问题答辩时这是很好的亮点。第三个技巧是给答案加出处。返回诗名时顺带把原诗正文查出来前端展示成卡片。用户看到「静夜思床前明月光…」信任感立刻上来。实现上把RETURN poem.name改成RETURN poem.name, poem.content答案生成时多拼一段即可。验证效果别只靠自己拍脑袋准备 30 到 50 条测试问题覆盖单实体、多实体、多跳三类统计准确率。我一般会建一个test_questions.csv两列问题和期望答案写个脚本批量跑一遍算命中率。这个数字写进论文的「实验与分析」章节比空谈「系统运行良好」有说服力得多。最后说个血泪经验毕设最怕的不是技术难而是演示当天环境崩了。Neo4j 服务、Python 依赖、前端端口任何一个没起来都尴尬。我的习惯是提前把整个流程写成一个start.sh一条命令拉起数据库和 Web 服务再准备一份离线截图兜底。这套古诗词问答的方案数据干净、图谱规整、问答链路短是本科毕设里性价比很高的选择认真做两周能出成果剩下的时间留给论文和答辩。希望帮到你。本文还有配套的精品资源点击获取