1. 项目全貌与毕设选题思路
如果你正在为毕业设计选题发愁,又恰好对知识图谱、问答系统、自然语言处理这些方向感兴趣,那基于知识图谱的《三国演义》知识问答系统是一个非常值得做的题目。它既有文学文本的数据背景,又有图谱构建、语义解析、深度学习排序这些硬核技术点,做出来的东西还能直接演示、直接答辩,老师一眼就能看懂你在干什么,比纯做算法调参要好呈现得多。
这个项目的核心,是把《三国演义》原著中的实体(人物、地点、事件、官职、势力)和它们之间的关系(父子、君臣、同盟、交战、谋略)抽出来,组织成一张结构化的图,然后让用户用自然语言提问,系统返回准确答案。比如问“诸葛亮的师父是谁”“赤壁之战发生在哪一年”“刘备和曹操是什么关系”,系统都能基于图谱中的三元组回答。
它适合谁做?我身边不少同学是从三个方向上手的:一是做传统Web开发的,想往AI方向靠一靠;二是学算法出身,但不想只跑公开数据集,想找一个有数据工程、有业务背景的题目;三是真的喜欢三国,想把兴趣变成毕业设计。无论哪一种,这个项目都能满足。而且它天然是一个“大数据+深度学习+前端展示”三位一体的综合系统,论文里不缺创新点,答辩时不缺素材。
对于毕设来说,这个选题最大的优势是数据可控、规模适中。《三国演义》一共一百二十回,人物上千,关系复杂,但不像通用互联网数据那样海量且嘈杂。你可以把整个EPUB或TXT原文作为原始语料,再结合百科的半结构化数据做扩充,既不用愁数据量太大跑不动,也不用担心数据太少没法体现效果。整体难度处于“努力跳一跳能够得着”的水平。
2. 知识图谱构建:从原著文本到结构化三元组
2.1 实体识别与关系抽取的工程化方案
知识图谱构建是整个系统的地基。地基没打好,后面问答再花哨也是空中楼阁。
第一步是定义本体(Schema)。我先用纸笔列出了《三国演义》里最常见的六类实体:人物、地点、事件、时间、官职、势力。比如“诸葛亮”是人物,“赤壁之战”是事件,“荆州”是地点。然后定义关系类型,我实际用的关系有二十多种,但真正高频、有效的大概是这些:
| 关系类型 | 例子 |
|---|---|
| 父子/母子 | 刘备与刘禅,孙坚与孙策 |
| 兄弟 | 孙策与孙权,曹丕与曹植 |
| 君臣 | 刘备与诸葛亮 |
| 隶属(势力) | 关羽属于蜀汉 |
| 参与(事件) | 诸葛亮参与赤壁之战 |
| 交战(对手) | 曹操与刘备交战 |
| 担任(官职) | 诸葛亮的丞相 |
| 出生地 / 活动地 | 诸葛亮居于南阳 |
| 时间(事件发生时间) | 赤壁之战发生于建安十三年 |
| 谋士/盟友/敌人 | 诸葛亮与司马懿互为对手 |
关系定义太多会导致抽取困难,太少又支撑不起问答。如果只做一个人物关系问答系统,我建议优先做“人物-人物”关系和“人物-事件-时间”关系,这两类是用户最常问的。
抽取阶段,最忌讳一上来就上深度学习,因为标注数据不够。我自己是分三步走的:第一步,用正则和词典做第一轮粗抽。比如把“XX之妻”“XX弟”“XX对XX说”这类句式收集起来,写正则模板直接套。第二步,用百科半结构化数据做补充。很多公开的百科页面有现成的“人物关系”表格,可以直接爬下来转成三元组。第三步,用基于BERT微调的序列标注模型做兜底,专门识别前面规则漏掉的实体和对齐关系。
这里有一个非常容易被低估的点:实体对齐。诸葛亮、孔明、卧龙、诸葛孔明,这四个词在原著中指的是同一个人,但在图谱中如果不做合并,问“孔明的师父是谁”和“诸葛亮的师父是谁”就会得到两套答案。我的做法是建一个“别名表”,每个实体选一个规范名,其他别名统一映射过去。这个表不需要模型,手工整理常见别名就够了。百科重定向可以帮忙,但人工核对一遍最稳。
2.2 图谱存储与可视化选型
存储层我选的是Neo4j,没有之一。原因很简单:图数据库的Cypher查询语言,对“找关系路径”这类操作几乎是零成本。比如用户问“刘备和曹操之间是什么关系”,底层需要找到一条路径,这在Neo4j里就是一条MATCH p=(a)-[*1..3]-(b) RETURN p,查询快、写起来也不复杂。如果用MySQL存三元组表,要自己写递归查询,效率低不说,代码还难看。
可视化方面,Neo4j自带的Browser可以用于调试,但做成毕设演示我建议用ECharts。我做的系统里,前端展示的是固定范围的子图,比如输入“诸葛亮”,就展示与他直接相邻的所有实体和关系,用ECharts的关系图(graph)类型渲染成节点和边。这样用户一眼就能看到自己问的问题在图谱中的位置,所谓“知识图谱”的演示效果就出来了。
数据导入这块有个小经验:不要一行一行在Neo4j里创建节点,效率太低。把三元组整理成CSV文件,用Neo4j自带LOAD CSV或者Python的py2neo库批量导入。我最终导入了大约1200个实体、3200条关系。这个规模在Neo4j里就是小意思,查询毫秒级返回。
3. 问答系统核心:从问题解析到答案检索
3.1 问题分类与意图识别策略
问答系统的难点不在答案检索,而在把用户的自然语言问题解析成机器能理解的查询意图。中文口语用户会问“诸葛亮住在哪”,也会问“卧龙先生隐居地”,还可能会问“孔明在哪出山”,句子表述差异极大,要统一到同一个意图上。
我的做法是把问题解析拆成两个子任务。
第一个子任务是命名实体识别。从问句里抽取出目标实体。因为问句长度短、领域固定,用词典匹配其实就够了,不需要加载一个很大的BERT模型。我把人名表、地名表、势力表、事件表、官职表全部加载进来,先用最长的词典条目优先匹配,比如“赤壁之战”优先于“赤壁”,免得拆错。再结合一个简单的CRF模型兜底,处理那些词典外的新词。实测准确率已经能到九成以上。
第二个子任务是问题类型判别。我把问题归为六大类:人物关系、事件时间、事件参与人、人物其他信息(字、号、籍贯、官职)、地理位置、数量/统计。这里我用了BERT分类模型,因为问题类型不像实体那样容易靠词典枚举,句子的语义特征更明显。我收集并标注了大约5000条问题样本,涵盖模板变化,用小规模的bert-base-chinese微调了一个多分类模型。分类准确率在测试集上做到96%左右,完全够用。
这里有个容易被忽略的坑:同义改写。比如“谁杀了关羽”“关羽被谁害死”“关羽的死因是什么”,如果只做分类和实体识别,这三种问法的处理路径完全不同。我的经验是,在问题进入分类前,先做一层归一化。把“被谁”“谁杀了”统一成“凶手”,“死于”“死因”统一成“死亡事件”,这样图谱查询模板就不用写太多。我建了一个“问题改写规则库”,里面写了几十条正则表达式,效果立竿见影。
3.2 模板匹配与Cypher查询生成
分类完成之后,核心工作是把意图转换成Cypher查询语句。
以“人物关系”类问题为例,用户问“刘备和曹操是什么关系”,系统抽取到实体“刘备”和“曹操”,问题类型是“关系”。那我只需要生成一条Cypher:
MATCH (a:人物 {name:'刘备'}), (b:人物 {name:'曹操'}) MATCH p = (a)-[r]-(b) RETURN type(r) AS relation, properties(r) AS props如果存在直接关系,直接把关系名返回。如果直接关系不存在,就做多跳查询,最多跳到三层,把中间路径展示给用户,比如“刘备—盟友—孙权—敌对—曹操”,两个人物之间的关系就有了中间人,这样的回答更丰富,也更容易解释给答辩评委听。
再举一个“事件时间”类问题:“赤壁之战发生在哪一年?”抽取到“赤壁之战”事件,然后查它的time属性:
MATCH (e:事件 {name:'赤壁之战'}) RETURN e.time“谁参加了赤壁之战”则查事件和人物之间的参与关系。
为了避免每次根据不同的问法写几十个if分支,我构建了一个“模板映射表”。第3.1节归一化后的问题,会落到预设模板中,例如“谁+动词+实体”对应“事件参与人查询模板”,“实体+什么时候+动词”对应“事件时间查询模板”。每个模板都带有一个Cypher生成器函数,用最简单的方式完成“问题意图到图查询”的转换。这种基于规则的方式虽然看起来笨,却是最稳定、最容易在论文里解释清楚的做法。
4. 深度学习算法在问答系统中的应用
4.1 基于向量检索的候选答案召回
如果只用模板匹配,系统只能回答预先设计好的那几类问题,遇到开放式的、模板覆盖不到的问法就会哑火。为了让系统有“智能感”,我把深度学习用在了答案召回和重排序两个环节。
召回阶段用的是稠密向量检索。我先把知识图谱中的所有三元组编码成文本描述,比如(刘备, 君臣, 诸葛亮)变成“刘备的臣子是诸葛亮”,(诸葛亮, 参与, 赤壁之战)变成“诸葛亮参与了赤壁之战”,然后统一用Sentence-BERT模型编码成向量,存到向量数据库里。用户提问时,同样把问题编码成向量,通过余弦相似度召回Top-20相关的三元组描述。这个阶段不需要严谨的逻辑,只需要把可能的答案范围扩大,宁多勿缺。
向量模型我用了中文预训练模型,比如shibing624/text2vec-base-chinese,这个模型轻量、效果稳定,在普通CPU上也能跑,不需要租GPU。如果毕设条件好一点,可以换用更大规模的模型,但实测下来对最终准确率的提升非常有限。原因是知识图谱里的三元组描述本身是模板化生成的,结构相对简单,小模型的语义表达能力已经足够。
这里需要提一个重要细节:召回和模板匹配不是两个独立系统,而是并联关系。我在后端做了一个策略调度器,优先尝试模板匹配,如果模板匹配的置信度低(比如问题分类器输出的置信度低于0.7),就转入向量检索召回。这样既保证了高频问题的准确率,又保留了长尾问题的一定覆盖率。
4.2 基于预训练模型的答案重排序
召回出来的Top-20候选答案中,通常有一大半是噪声。这时需要做重排序,把最可能回答用户问题的候选排到最前面。
我的重排序方案是构建一个二分类模型:输入“用户问题”和“候选答案描述”,模型输出一个0到1的匹配分数。这个模型还是基于BERT做微调,训练数据是人工构造的“问题-答案对”。正样本是从图谱中抽取的、真正能回答问题的三元组描述;负样本则是随机挑选的、不相关的三元组描述。训练数据我生成了两万对,比例大约1:2,正样本少一点,负样本多一点,这是为了让模型学会区分“确实相关”和“看似相关但不对”。
微调用的是transformers库的AutoModelForSequenceClassification,标签就是0和1,训练时间在两小时内能完成,数据量小,模型也收敛得很快。推理时,把用户问题和20个候选描述分别拼接成[CLS]问题[SEP]候选[SEP],送入模型,获取score,然后排序取Top-1返回给用户,同时把Top-3的候选作为“相关推荐”展示在界面上。
做这一步的最直接好处是,系统能处理类似“曹魏阵营中,哪位将领最常与蜀汉交战”这种没有办法直接用模板匹配到的问题。虽然召回阶段可能返回一堆交战记录,但重排序会根据问题语义把最相关的几条排到最上方,再通过一个简单的聚合逻辑输出“赵云”“关羽”这样的答案。这正是“知识图谱+深度学习”结合的创新所在,论文的创新点之一就可以写这个。
5. 毕设论文架构与代码工程实现要点
5.1 论文目录与创新点写作思路
很多同学拿到这种题目,不知道论文怎么写。我当年的论文结构可以作为参考,大致是这样的:
第一章:绪论。写研究背景与意义,比如传统浏览器搜索返回网页列表,用户需要自己归纳,而知识图谱问答系统能直接返回结构化答案。再写国内外研究现状,提一下通用领域知识图谱(如GeoNames、Freebase)和开放域问答系统(如IBM Watson)的进展,然后引出要做的事。
第二章:相关技术。这部分不要写成名词解释手册,而要重点写“为什么选它”。比如Neo4j相比MySQL的优势、BERT的Attention机制如何适用于短文本匹配、Sentence-BERT在召回场景下如何解决向量相似度计算效率问题。老师最反感的就是大段抄教材定义,要写成有自己理解的技术选型对比。
第三章:系统需求分析与总体设计。画功能模块图、用例图、系统架构图。注意架构图不要用Mermaid,而是用绘图工具画好插图。模块划分要清晰:数据层、图谱构建层、问答解析层、答案检索层、前端展示层。
第四章:知识图谱的构建。写实体定义、关系定义、抽取管道、数据清洗、实体对齐、Neo4j导入过程。附上几个可视化截图,展示图谱规模。
第五章:问答系统的设计与实现。写问题分类、意图识别、模板映射、Cypher生成、向量召回、重排序。这是论文最核心的章节,每个模块都要配上流程图(用visio画)和代码片段。
第六章:实验结果与分析。这一章在毕设中很关键,很多人只写“系统能回答问题”就没了,显得特别单薄。建议做三组实验:第一组,测试不同问题类型的正确率,用标注好的500个问题评测,给出精确率、召回率、F1值;第二组,消融实验,比如“没有向量召回只有模板匹配”和“模板匹配+向量召回+重排序”的对比,体现深度学习的贡献;第三组,对比实验,可以换成开源的通用问答模型如基于检索式或基于生成式的Baseline,展示你的系统在领域答案正确率上的优势。
第七章:总结与展望。写遇到的困难、解决过程、未来可以扩展的方向,比如加入多轮对话、扩展到其他典籍。
这里说一个答辩技巧:一定要准备一个“创新点”总结,哪怕只有一句话。我总结的三个创新点是:针对文学作品设计了领域专用的本体结构和关系体系;将模板匹配与深度学习向量检索结合,兼顾准确率和覆盖率;构建了中文古典文学作品的知识图谱问答流程,有可迁移性。实际答辩时,老师问得最多的就是“你这个系统和现有通用问答系统有什么不同”,用这句话挡回去就对了。
5.2 代码工程结构与环境配置
代码仓库必须结构清晰,答辩评阅老师可能会打开你的GitHub或压缩包看一遍。我建议按以下目录组织:
sanguo-qa/ ├── frontend/ # 前端工程(Vue 3 + ECharts) ├── backend/ # 后端服务(FastAPI) │ ├── app/ │ │ ├── api/ # REST接口 │ │ ├── graph/ # 图谱查询逻辑 │ │ ├── nlu/ # 问题分类与命名实体识别 │ │ ├── retriever/ # 向量召回 │ │ └── reranker/ # 重排序模型 ├── data/ # 原始文本、标注数据、三元组CSV ├── notebooks/ # 数据清洗与模型训练脚本 ├── model/ # 训练好的模型权重 ├── docs/ # 论文、开题报告、答辩PPT └── requirements.txt后端我用FastAPI而不是Flask,原因很简单:自带OpenAPI文档,前端联调时可以随时在浏览器里测试接口。前端只需要两页,一个聊天窗口、一个图谱可视化页面,不必做得太花哨。
环境配置的坑要提前踩:Neo4j的版本要和你用的py2neo版本匹配。我当初踩过一个坑,Neo4j 5.x和py2neo 4.x不兼容,连接一直报认证错误,折腾了一晚上。后来换成neo4j官方Python驱动,或者直接用Neo4j的HTTP接口,就稳定多了。如果答辩现场担心网络问题,建议把所有依赖打包成Docker镜像,在答辩电脑上一键启动。这个细节很加分,有一次答辩就是因为系统能离线演示,被评委多夸了两句。
6. 常见问题与排坑实录
6.1 关系抽取准确率低、数据稀疏怎么办
这是大家做知识图谱毕设时遇到最多的两个问题。
关系抽取准确率低,通常是因为规则模板写得太粗糙。比如“刘备对诸葛亮说”这种句子,容易被规则误判成“下属关系”。我的经验是:先统计高频关系模式,用依存句法分析辅助,比如拿到一句话的主谓宾,再判断主语和宾语之间的语义角色。不要指望一个规则解决所有情况。更现实的方案是“人工抽检+批量修正”:随机抽200组三元组,人工标出错误率,如果超过15%,就反向回查对应的规则,把误报来源修掉。
数据稀疏则集中在某些关系上。比如“贿赂”“联姻”这类关系在全书中出现次数很少,三元组数量不够,问答时很容易答不上来。我的应对是给每个关系类型设置了最低数量阈值,低于5条的关系直接合并到“其他关系”类别中。用户询问时如果命中稀疏关系,系统不会返回空,而是返回相关实体的“概述”信息,把图谱中没有直接答案的情况也给予友好提示。
另外,实体歧义问题也值得注意。《三国演义》中“荆州”既指州郡,也可以指代城池;同名人物可能出现演义和历史的混淆。我的处理是在导入图谱时给实体加domain属性,标注其所属类别,查询时优先匹配和问句上下文一致的类别。比如用户问“谁占据荆州”,优先匹配地名,而不是人物。
6.2 系统演示与答辩环节的实战建议
毕设答辩时,系统的实际表现比任何理论讲解都重要。如果你现场演示时,问了一个常见问题,系统却返回错误或空白,那印象分会打折扣。我建议你建立一个“演示用测试集”,大概三十个问题,覆盖六大类问法,每个问题都提前测过,确保万无一失。不要即兴输入,免得遇到文本编码错误之类的意外。
如果答辩时被问到“你这里用了深度学习,和直接用规则有什么区别”,不要回避,直接说出实验中对比数据。我的实验中,规则模板的准确率是82.7%,加入向量召回和重排序后,准确率提升到了91.3%,覆盖率提升了18个百分点。这组数字是我花了整整一个星期测试出来的,也是论文第六章最扎实的数据支撑。
最后,关于时间规划:如果从现在开始做,建议第一周完成数据准备和本体设计,第二周到第四周做实体关系抽取和入库,第五周到第七周做问答模块,第八到第十周完成前端展示和模型微调,最后三周写论文和调试。千万不要把代码放到最后一个月,因为问答系统的联调需要大量时间。我见过太多同学前期慢悠悠,后期通宵赶论文,质量堪忧。我自己做完这个项目的体会是:最难的不是算法,而是把数据工程和算法模型串成一条线。你要是能把这条线走通,无论毕业设计还是未来的简历,都会多一份扎实的底气。