简介:这是一套面向计算机相关专业本科生与项目实战学习者的古诗词问答系统源码,以知识图谱为核心技术路线,可作为毕业设计、课程设计或期末大作业的完整参考方案。项目经导师指导并通过答辩评审,平均分达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', encoding='utf-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='', encoding='utf-8') as nf, \ open(rel_path, 'w', newline='', encoding='utf-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 'unknown'extract_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执行时报Couldn't 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.unrestricted=apoc.*放开权限。
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 服务,再准备一份离线截图兜底。这套古诗词问答的方案,数据干净、图谱规整、问答链路短,是本科毕设里性价比很高的选择,认真做两周能出成果,剩下的时间留给论文和答辩。希望帮到你。
本文还有配套的精品资源,点击获取