☰
基于知识图谱的古诗词问答系统:从源码到实战的毕设指南
2026/10/25 11:18:31 网站建设 项目流程

简介:这是一套面向计算机相关专业本科生与项目实战学习者的古诗词问答系统源码,以知识图谱为核心技术路线,可作为毕业设计、课程设计或期末大作业的完整参考方案。项目围绕古诗词实体与关系构建图谱,并实现自然语言问答交互,适合人工智能、软件工程等方向的学生理解知识图谱落地流程。压缩包共521个文件,约50.58MB,其中98个py文件承载核心逻辑与图谱构建脚本,129个txt与24个json用于诗词语料及图谱数据存储,另有jpg、css、js、html等前端与静态资源,以及少量md说明文档,整体结构清晰、模块划分明确。目前已有331人学习下载。代码经测试可正常运行,答辩评审平均分达96.5分,读者可据此掌握图谱构建、问答匹配与前后端联调思路,并在此基础上修改扩展功能。

1. 从一份本科毕设源码说起:知识图谱怎么把古诗词问答做成能跑的系统

很多同学拿到「基于知识图谱的古诗词问答系统」这个题目时,第一反应是去搜一份现成源码,解压、装依赖、跑起来,然后截图交差。但真正动手才会发现,古诗词这个领域和通用百科问答完全不是一回事:一首诗有作者、朝代、体裁、意象、典故、情感标签,诗句之间还有化用、唱和、同题的关系,这些关系用一张二维表根本存不下。知识图谱的价值就在这里——它把「李白」和「《静夜思》」和「唐代」和「五言绝句」和「思乡」这些实体用边连起来,问答时沿着边走就能推理出答案,而不是靠关键词匹配硬猜。

这份本科毕设源码要解决的核心问题,是让一个 Python 后端能听懂「李白写过哪些思乡的诗」这类自然语言问题,把它翻译成对图谱的查询,再把结果组织成人话返回。适合谁看:正在做毕设、需要一套能讲清楚原理又能跑通的代码的同学;也适合想入门知识图谱构建与问答、但被 Neo4j 和 SPARQL 劝退的开发者。下面我按「图谱怎么建 → 问句怎么解析 → 查询怎么执行 → 坑在哪」的顺序,把这条链路拆开讲。

2. 古诗词知识图谱的构建:从 CSV 到 Neo4j 的实体关系落地

2.1 为什么古诗词适合用图数据库而不是关系表

先想清楚选型理由,不然后面写代码全是玄学。古诗词数据的典型特征是「多跳关系密集」:想知道「杜甫写给李白的诗里提到了哪些地名」,路径是 杜甫 → 赠诗 → 李白 → 诗中地名 → 具体地点,这在关系型数据库里要 join 四张表,而在图数据库里就是一次两跳遍历。另一个特征是「schema 不固定」,有的诗有典故标注,有的没有,图数据库的边可以按需扩展,不用改表结构。

常见做法是用 Neo4j 作为存储,Python 侧用py2neo或官方neo4j驱动。我一般会先把原始数据整理成两类 CSV:节点文件和关系文件。节点包括诗人、诗题、朝代、体裁、意象、情感;关系包括「创作」「属于朝代」「属于体裁」「包含意象」「表达情感」「赠予」「唱和」。

提示:本科毕设的数据量通常在几千到几万条,Neo4j 社区版完全够用,不要一上来就上分布式图库,那是给自己找麻烦。

2.2 用 Python 批量导入节点和关系的可复现脚本

下面这段脚本做两件事:读取 CSV,用MERGE语句幂等地写入节点和关系。MERGE而不是CREATE是关键,重复执行不会产生重复节点。

from py2neo import Graph, Node, Relationship import csv # 连接本地 Neo4j,默认 bolt 端口 7687 graph = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) def load_poets(path): with open(path, encoding="utf-8") as f: for row in csv.DictReader(f): # MERGE 保证按 name 唯一,不会重复建点 poet = Node("Poet", name=row["name"], dynasty=row["dynasty"]) graph.merge(poet, "Poet", "name") def load_poems(path): with open(path, encoding="utf-8") as f: for row in csv.DictReader(f): poem = Node("Poem", title=row["title"], content=row["content"], genre=row["genre"]) graph.merge(poem, "Poem", "title") # 建立 诗人-创作->诗 的关系 poet = graph.nodes.match("Poet", name=row["author"]).first() if poet: graph.merge(Relationship(poet, "创作", poem)) load_poets("data/poets.csv") load_poems("data/poems.csv")

逻辑说明:graph.merge(node, label, key)的第三个参数是唯一性约束字段,必须和你在 Neo4j 里建的约束一致,否则每次都会新建节点。参数上,auth里的密码是你初始化 Neo4j 时设的,不是默认的neo4j,第一次登录会强制改密。Relationship的方向建议统一成「主体→动作→客体」,比如「诗人→创作→诗」,后面写查询时方向不会乱。

2.3 意象和情感这类弱实体怎么挂到图上

意象(明月、杨柳、孤舟)和情感(思乡、送别、怀古)不是诗本身,但它们是问答的高频入口。做法是把它们也建成节点,用「包含意象」「表达情感」两条边连到诗上。这里有个坑:意象抽取如果只靠分词,会把「明月光」切成「明月」和「光」,导致误挂。稳妥做法是维护一个意象词典,用词典匹配而不是纯分词。

IMAGERY_DICT = ["明月", "杨柳", "孤舟", "落花", "流水", "鸿雁"] def tag_imagery(poem_title, content): poem = graph.nodes.match("Poem", title=poem_title).first() for word in IMAGERY_DICT: if word in content: img = Node("Imagery", name=word) graph.merge(img, "Imagery", "name") graph.merge(Relationship(poem, "包含意象", img))

参数说明:IMAGERY_DICT建议从 50 到 200 个高频意象起步,覆盖《全唐诗》里出现频率前 10% 的词即可,贪多反而引入噪声。执行完可以用一句 Cypher 验证:MATCH (p:Poem)-[:包含意象]->(i:Imagery) RETURN count(*),数字对不上就回去查词典匹配逻辑。

3. 问句解析:把「李白写过哪些思乡的诗」翻译成图谱查询

3.1 意图识别和实体抽取的分工

问答系统的第一道关卡是理解用户问了什么。古诗词问答的意图其实不多,常见就几类:查作者的作品、查某首诗的作者或朝代、查包含某意象的诗、查表达某情感的诗、查诗人之间的关系。意图识别可以用规则模板,也可以用轻量分类模型。本科毕设阶段,我建议先用规则打底,把准确率做稳,再考虑上模型。

实体抽取要抽的是「诗人名」「诗题」「意象」「情感」「朝代」这几类。难点在于诗人名和意象可能重叠,比如「李白」是诗人,但「李」也可能是姓氏泛称。解决办法是优先匹配长词,并且给每类实体维护独立词典。

import jieba POETS = {"李白", "杜甫", "王维", "白居易", "苏轼"} IMAGERY = {"明月", "杨柳", "孤舟", "落花"} EMOTIONS = {"思乡", "送别", "怀古", "边塞"} def extract_entities(question): words = jieba.lcut(question) result = {"poet": None, "imagery": None, "emotion": None} for w in words: if w in POETS: result["poet"] = w elif w in IMAGERY: result["imagery"] = w elif w in EMOTIONS: result["emotion"] = w return result

逻辑说明:jieba.lcut做精确模式分词,然后逐词比对词典。参数上,词典要按长度倒序匹配,否则「白居易」可能被「白」先命中。如果问句里同时出现诗人和情感,比如「李白写过哪些思乡的诗」,两个槽位都要填上,查询时用 AND 连接。

3.2 意图模板和槽位的映射表

把意图和槽位组合成查询模板,是这套系统最核心的设计。下面这张表是我实际用的映射关系,可以直接抄。

意图触发词示例槽位对应 Cypher 片段
查作品写过、有哪些诗poet(p:Poet)-[:创作]->(poem)
查作者谁写的、作者是谁title(poem)-[:创作]-(p:Poet)
查意象含、出现、描写imagery(poem)-[:包含意象]->(i)
查情感表达、抒发emotion(poem)-[:表达情感]->(e)
查关系和、赠、唱和poet1, poet2(p1)-[:赠予]->(p2)

注意:触发词要覆盖口语变体,比如「写过」和「写了」都要能命中,否则用户换个说法系统就哑了。

3.3 从槽位到 Cypher 的拼装与参数化

拼 Cypher 字符串时,千万不要用 f-string 直接插用户输入,那是注入漏洞。正确做法是用参数化查询,把槽位值作为参数传进去。

def build_query(intent, slots): if intent == "查作品" and slots["poet"] and slots["emotion"]: return ( "MATCH (p:Poet {name: $poet})-[:创作]->(poem)-[:表达情感]->(e:Emotion {name: $emotion}) " "RETURN poem.title AS title, poem.content AS content" ), {"poet": slots["poet"], "emotion": slots["emotion"]} if intent == "查作者" and slots.get("title"): return ( "MATCH (poem:Poem {title: $title})-[:创作]-(p:Poet) " "RETURN p.name AS author" ), {"title": slots["title"]} return None, None

逻辑说明:函数返回 Cypher 语句和参数字典两部分,执行时用graph.run(cypher, **params)。参数说明:$poet这种占位符由驱动负责转义,用户输入再离谱也不会破坏语句结构。如果意图和槽位组合没有对应模板,返回None,上层给用户一个「暂时没理解」的兜底回复,而不是硬查。

4. 查询执行与答案生成:让返回结果像人话

4.1 执行查询并处理空结果

拿到 Cypher 和参数后,执行并判断结果。空结果在古诗词问答里很常见,可能是图谱里确实没有,也可能是槽位抽错了。两种情况要区分对待:如果实体在词典里但图谱查不到,提示「图谱中暂无相关记录」;如果实体根本没抽出来,提示「没听懂,换个说法试试」。

def answer(question): intent = detect_intent(question) slots = extract_entities(question) cypher, params = build_query(intent, slots) if not cypher: return "这个问题我还没学会,换个问法试试?" records = graph.run(cypher, **params).data() if not records: return "图谱里暂时没有找到相关诗词。" return format_answer(intent, records)

参数说明:graph.run(...).data()返回的是字典列表,字段名和 RETURN 里的别名一致。detect_intent可以用关键词命中实现,命中多个意图时按优先级取第一个,优先级建议是「查关系 > 查作者 > 查作品 > 查意象 > 查情感」。

4.2 答案模板与多结果截断

返回多条诗时,不要一股脑全倒出来。我一般限制最多返回 5 条,超出部分提示「还有 N 条,可继续追问」。答案模板按意图区分:查作品返回「诗人 + 共找到 N 首 + 诗题列表」;查作者返回「《诗题》的作者是 XXX」;查意象返回「包含『明月』的诗有……」并附上名句。

def format_answer(intent, records): if intent == "查作品": titles = [r["title"] for r in records[:5]] return f"共找到 {len(records)} 首,包括:" + "、".join(titles) if intent == "查作者": return f"这首诗的作者是 {records[0]['author']}。" if intent == "查意象": lines = [f"《{r['title']}》:{r['content'][:20]}…" for r in records[:5]] return "\n".join(lines) return str(records[:5])

逻辑说明:截断用切片[:5],同时把总数len(records)告诉用户,兼顾信息量和可读性。参数上,content[:20]截取前 20 个字符做摘要,避免整首诗刷屏。如果要做成 Web 接口,把answer函数包一层 Flask 路由即可,返回 JSON。

4.3 用 Flask 暴露一个最小问答接口

from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/qa", methods=["POST"]) def qa(): question = request.json.get("question", "") return jsonify({"answer": answer(question)}) if __name__ == "__main__": app.run(port=5000)

逻辑说明:POST 接收 JSON,字段名question,返回answer。参数上端口用 5000,和前端联调时注意跨域,开发阶段可以加flask-cors。这个接口就是毕设演示的入口,前端页面或 Postman 都能调。

5. 避坑与排查:古诗词问答系统最容易翻车的 5 个地方

5.1 现象:Neo4j 连不上,报ServiceUnavailable

原因通常是三个:Neo4j 服务没启动、bolt 端口被防火墙拦了、密码不对。解决顺序是先neo4j status看服务,再telnet localhost 7687看端口,最后确认auth里的密码是不是初始化时改过的那个。本科毕设常见错误是照抄教程里的默认密码neo4j,但实际第一次登录已经强制改密了。

5.2 现象:节点重复,查「李白」出来好几条

原因是merge时唯一性字段没对上,或者 CSV 里同一诗人名字有空格差异。解决方法是先建唯一性约束:CREATE CONSTRAINT FOR (p:Poet) REQUIRE p.name IS UNIQUE,再重新导入。导入前用strip()清洗字段,全角和半角空格都要处理。

5.3 现象:问「思乡的诗」返回空,但图谱里明明有

原因是情感节点没挂上,或者情感词和用户输入不一致。比如图谱里存的是「思乡」,用户输入「想家」,词典没覆盖。解决办法是维护同义词表,把「想家」「思念故乡」都映射到「思乡」。排查时直接跑MATCH (e:Emotion) RETURN e.name看图谱里到底有哪些情感标签。

5.4 现象:分词把诗人名切碎,实体抽不出来

原因是 jieba 默认词典不含古诗人名。解决办法是jieba.add_word("李白")把诗人名和意象词都加进自定义词典,或者用jieba.load_userdict("dict.txt")批量加载。参数上,自定义词典每行「词 词频 词性」三段,词频给高一点保证优先切出。

5.5 现象:Cypher 查询慢,几秒才返回

原因是没建索引,或者查询写了笛卡尔积。解决办法是给常用查询字段建索引:CREATE INDEX FOR (p:Poet) ON (p.name),诗题、意象名同理。另外检查 Cypher 里有没有漏写关系方向导致全图扫描,EXPLAIN一下执行计划,看到AllNodesScan就要警惕。

6. 把问答准确率再抬一截:同义词扩展与查询回退的实操技巧

系统能跑通之后,真正拉开差距的是准确率。我踩过的最大坑是「用户说法千变万化,词典永远不够」。后来我加了一层同义词扩展,效果立竿见影。做法是维护一个synonyms.json,把情感、意象、意图触发词都做同义映射,查询前先归一化。

import json with open("synonyms.json", encoding="utf-8") as f: SYNONYMS = json.load(f) def normalize(text): for standard, variants in SYNONYMS.items(): for v in variants: if v in text: text = text.replace(v, standard) return text

synonyms.json的结构是{"思乡": ["想家", "思念故乡", "乡愁"], "明月": ["月", "月亮", "月光"]}。参数说明:标准词必须和知识图谱里的节点名完全一致,否则归一化后还是查不到。变体词按长度倒序替换,避免「月」把「月亮」拆坏。

第二层是查询回退。当精确查询返回空时,不要直接说「没找到」,而是降级:先去掉情感槽位只按诗人查,再不行只按意象查,最后给一个「你是不是想问……」的猜测。这个回退链能把很多「差一点就命中」的问题救回来。

回退层级查询条件适用场景
第一层诗人 + 情感完整槽位
第二层仅诗人情感词没匹配上
第三层仅意象诗人名抽错
第四层模糊匹配诗题用户直接报诗名

验证方法很简单:准备 50 条标注好答案的测试问句,跑一遍统计命中率,每次改词典或模板都重跑,看数字是涨是跌。我一般会把测试集和结果存成 CSV,方便对比不同版本。这套东西做完,本科毕设的答辩基本稳了,因为你能说清楚每一层为什么这么设计、边界在哪。

最后一个习惯:每次改完图谱或词典,先跑一遍那 50 条测试问句,别凭感觉觉得「应该没问题」。我吃过这个亏,改了一个意象词,结果把另一个情感查询带崩了,答辩前一天才发现。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询