ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

知识图谱+古诗词问答系统源码解析:从Neo4j建模到NLP意图识别

知识图谱+古诗词问答系统源码解析:从Neo4j建模到NLP意图识别

简介:这是基于知识图谱的古诗词问答系统完整源码,来自本科毕设项目,评审得分96.5分,适合计算机相关专业学生用于毕业设计、课程设计或项目实战练习。系统融合知识图谱构建与问答交互,代码经过完整测试,运行稳定,可作为二次开发基础。压缩包共521个文件,包含98个Python源码、129个txt说明与数据文件、24个json配置、19个css样式、16个js脚本及8个html页面,另有图片、字体、日志等辅助材料,包体约50.58MB,目录结构清晰。已有332人学习下载,适合正在筹备毕设或想提升知识图谱项目经验的学习者。资源内附详细说明文档,下载后可对照梳理问答流程、图谱构建逻辑与前端展示方式;如遇运行问题,作者支持远程教学。整体内容完整,适合从零搭建或在此基础上扩展新功能,也是答辩演示与初期立项的良好参考。

1. 知识图谱 + 古诗词问答系统,这个毕设源码包里最值钱的是哪一段

打开这个压缩包之前,我先说结论:这套基于知识图谱的古诗词问答系统,核心不是那几十个前端 CSS 文件,而是藏在背后的问答链路——把自然语言问题拆成实体和意图,再去 Neo4j 图库查关系,最后拼回一句人话。它适合两类人:一是正在做毕设、需要完整可运行项目的计算机专业学生,二是想练手知识图谱 + NLP 工程落地的开发者。这个包已经把数据、图模型、查询脚本和前端页面串通了,你拿到的不是一堆零散文件,而是一条能跑通的主链路。

2. 系统架构与数据建模:一条请求从 Bootstrap 页面到 Neo4j 图库的完整路径

2.1 三层架构与四个功能模块

这套系统的整体结构不复杂,是典型的 B/S 三层:前端展示层、后端服务层、图数据库存储层。前端用 Bootstrap 4.6 和 nifty 管理模板搭页面,后端用 Python Web 框架提供接口,存储层用 Neo4j 存古诗词的实体和关系。源码包里前端资源文件占了很大比重,bootstrap.4.6.min.css、nifty.min.css、font-awesome.min.css、datatables.bootstrap.css 这些名字我一眼扫过去就知道,页面不是从零写的,而是基于开源后台模板改的。这种做法在本科毕设里很常见,省时间,而且 nifty 自带响应式布局和按钮、卡片、表格样式,做演示效果比原生 HTML 好看很多。

功能模块按我拆完的代码看,可以切成四块:数据管理模块负责把古诗词的 JSON 或 CSV 数据导入 Neo4j;知识图谱查询模块负责处理前端传过来的实体名和关系类型,生成 Cypher 语句;问答模块负责把用户输入的自然语言问题匹配成意图并抽取实体;前端展示模块负责把问答结果、诗人信息和诗词列表呈现在页面上。这四块是串行的,前端输入问题 → 后端做 NLP 处理 → 生成 Cypher 查询 → Neo4j 返回结果 → 前端渲染。整个链路里最需要看懂的其实是第三步,因为多数毕设的翻车点都集中在 Cypher 拼接和中文实体匹配上。

数据链路方面,这套系统的数据是预先导入的,不是运行时爬取。源码包里应该有一份诗词数据文件,导入脚本把每个诗人、每首诗词、每个朝代作为节点,把“写过”“属于”“包含意象”作为关系写入图库。页面查询时,用户输入“李白的诗有哪些”,后端先把“李白”抽成实体,把“诗”映射到作品类型,然后生成一条查询李白节点所有出边关系的 Cypher。Neo4j 把结果以 JSON 形式返回,Python 后端再套一层模板,拼成前端能直接渲染的格式。

提示:拿到源码后,先别急着跑前端,先确认 Neo4j 里有没有数据。很多运行失败的案例是数据没导入,页面却一直在请求。

2.2 古诗词知识图谱怎么建:实体、关系与属性设计

知识图谱建模这部分,是这套系统区别于普通关键词搜索引擎的核心。普通搜索靠数据库 like 匹配,这套系统靠图关系推理。源码里的图模型,我按数据结构还原出来大概是这样的:节点类型有诗人、诗词、朝代、意象、主题五类。诗人节点属性包括姓名、字号、生卒年;诗词节点属性包括题目、正文、注释、译文;朝代节点属性只有朝代名;意象和主题节点则维护各自的名称和解释。

关系设计是图库的灵魂,这套系统里设计了三类关系。第一类是“创作”关系,从诗人指向诗词,用于回答“某某写过什么诗”;第二类是“所属朝代”关系,从诗人指向朝代,用于回答“某某是哪个朝代的”;第三类是“包含意象”和“表达主题”关系,从诗词指向意象或主题,用于回答“哪些诗包含月亮意象”。这种设计考虑得很实际,因为问答系统 80% 的问题都能落到这三类关系上,其他关系再加也只是锦上添花。

Cypher 建图语句的常见写法是这样,源码导入脚本的核心逻辑也类似:

CREATE (poet:诗人 {name: '李白', alias: '太白', dynasty: '唐'}) CREATE (poem:诗词 {title: '静夜思', content: '床前明月光...', dynasty: '唐'}) CREATE (poet)-[:创作]->(poem) CREATE (poem)-[:包含意象]->(imagery:意象 {name: '月光'})

这段 Cypher 做了三件事:创建诗人节点和诗词节点,然后通过[:创作]关系把两者连起来,再建一个意象节点,用[:包含意象]把诗词和意象挂上。这里需要注意,Neo4j 的节点标签和关系类型都支持中文,但中文标签在部分版本的浏览器可视化里会有显示问题,数据本身不受影响。我一般建议标签用中文、属性名用英文,代码里看起来直观,也避免编码踩坑。

导入完成后,验证图是否建成功的方式是执行MATCH (n:诗人) RETURN n LIMIT 10,能返回节点就说明数据进去了。源码里如果带有 import 脚本,通常是把 CSV 逐行读取再逐个创建关系,这种方式慢但不会出错,对毕设数据量完全够用。

2.3 从源码目录看工程组织:先找到这四个文件再开工

拿到源码包后,目录结构一眼能看出这个项目的工程习惯。前端资源集中在一个静态目录下,nifty.min.css 和 nifty-demo-icons.min.css 表明这套前端基于 nifty 后台模板,wiki.css 是这个项目自己加的页面样式,datatables.bootstrap.css 是 DataTables 表格库的 Bootstrap 主题样式。这些文件不是摆设,它们决定了页面布局长什么样。

开工之前我建议先定位四个关键文件。第一个是数据库连接配置文件,通常在工具类或者配置模块里,写的是 Neo4j 的地址、账号和密码,不改这个连不上图库。第二个是数据导入脚本,负责把诗词数据写入 Neo4j,跑一遍才能让问答有数据可查。第三个是问答处理主逻辑文件,里面包含意图识别、实体抽取和 Cypher 生成的函数。第四个是前端问答页面,能看到用户输入和结果展示是怎么绑定的。

找到这四个文件后,先按顺序做三件事:改数据库连接配置,执行数据导入脚本,再启动 Web 服务。如果这样做完页面还是空的,去服务端日志看有没有报错,80% 的情况是 py2neo 连不上 Neo4j,或者 Cypher 执行报语法错误。源码里如果带了 README 文件,里面一般写了启动步骤,但很多 README 写得不全,缺数据库初始化和依赖安装说明,这就需要靠上面的定位思路自己补。

3. 前端实现细节:Bootstrap 4.6 + nifty 模板与 DataTables 表格的页面改造

3.1 样式引用顺序与三个常见渲染问题

前端页面引用的 CSS 文件不少,顺序也很有讲究。从源码里能看到的资源文件清单来推断,页面加载顺序基本是:先加载 Bootstrap 核心样式,再加载 nifty 模板样式覆盖 Bootstrap 默认外观,接着加载 icon 字体和 DataTables 配套样式,最后加载自定义的 wiki.css 覆盖细节。这个顺序不能乱,因为 nifty 依赖 Bootstrap 的栅格系统,如果 Bootstrap 加载在后面,布局会全面错乱;DataTables 的样式又依赖 Bootstrap 的主题变量,加载顺序不对表格就会失去边框和条纹效果。

常见的渲染问题有三个,我在另一个类似项目里都遇到过。第一个问题是页面样式全丢,打开浏览器全是裸 HTML。原因多半是静态文件路径写错,Flask 的静态目录默认是/static,如果模板里写成了相对路径css/bootstrap.min.css,在某些路由下会找不到资源。第二个问题是图标显示成方块,font-awesome.min.css 引用正常但字体文件缺失,或者 CSS 文件里引用的字体路径没有随目录一起拷贝。第三个问题是表格样式正常但弹窗和下拉框错位,这是因为 nifty 的 JS 初始化在 DOM 加载完成之前执行了。

排查这些问题的方法很直接,打开浏览器开发者工具看 Network 面板,哪个 CSS 请求返回 404 就补哪个;然后看 Console 有没有报错,重点看是不是$未定义——这说明 jQuery 没加载或加载顺序在 Bootstrap 之后。前端资源这块,源码基本能直接用,只要 js 和 css 目录结构别动就行。

3.2 问答页面的前后端接口:一次问答请求如何走完闭环

问答页面的交互逻辑很清晰:用户在输入框敲一句自然语言,点提交,前端用 AJAX 把问题发给后端接口,后端处理后返回 JSON 结果,前端把结果渲染成卡片列表。这段交互在源码里是用 jQuery 实现的,因为 nifty 模板本身就依赖 jQuery,不需要额外引入 Vue 或 React。

接口约定通常是这样的,后端提供一个/api/qa路由,接收 POST 请求,请求体是 JSON 格式的{"question": "李白的静夜思表达了什么思想感情"},返回体包含答案文本和可能的图谱节点信息。前端拿到返回数据后,根据字段决定如何展示:是有明确答案的单条信息,还是多条候选列表,还是未命中的兜底回复。

$.ajax({ url: '/api/qa', type: 'POST', contentType: 'application/json', data: JSON.stringify({ question: inputVal }), success: function (resp) { if (resp.status === 'ok') { renderAnswer(resp.answer); if (resp.graphData) renderGraph(resp.graphData); } else { renderFallback(resp.message); } }, error: function () { renderFallback('服务暂时不可用,请稍后再试'); } });

这段代码注意两个点。第一,contentType必须设置为application/json,不然 Flask 后端拿不到request.get_json()的数据,这是前端和后端联调时最容易出的问题。第二,成功回调先判断status再渲染,因为后端可能返回一个带答案的兜底结果,不能直接当正常结果渲染。前端这块做完后,剩下全在后端问答逻辑,那是这套系统的重头戏,我放到第 4 章详细拆。

3.3 DataTables 表格的参数配置与诗词列表展示

源码里的 datatables.bootstrap.css 说明页面里用了 DataTables 表格插件,这个插件大概率用在诗词列表页或者检索结果页。DataTables 的好处是自带排序、分页和搜索,不用自己写表格渲染逻辑,但对中文支持的配置需要单独调。

初始化 DataTables 的常见写法如下,源码里表格配置参数也基本是这套:

$('#poemTable').DataTable({ language: { url: '/static/json/datatables.zh_CN.json' }, paging: true, pageLength: 10, ordering: true, serverSide: true, ajax: { url: '/api/poems', data: function (d) { d.keyword = $('#poemSearch').val(); } }, columns: [ { data: 'title' }, { data: 'author' }, { data: 'dynasty' } ] });

这段配置有几个参数必须解释清楚。language.url指向中文语言包,不加的话分页按钮显示英文的 “Previous”“Next”,在答辩演示时不够好看。serverSide: true表示服务端分页,每次翻页都会重新请求后端接口,适合数据量大的场景;如果数据量只有几百条,改成serverSide: false一次性加载反而更流畅。ajax.data是回调函数,点击搜索按钮时把额外参数拼进请求,后端就能根据keyword过滤诗词。这个配置我第一次用的时候踩过坑,DataTables 默认会把columns的字段映射到后端请求参数里,如果后端接口需要的是keyword而不是search[value],就得像我这样手动加data回调覆盖。

4. 问答核心链路:意图识别、实体抽取与 Cypher 模板生成的实战拆解

4.1 意图识别的最小可用实现:规则优先,先覆盖三类高频问题

问答系统的第一步是判断用户在问什么。这套源码里的实现方式不是机器学习模型,而是基于规则的关键词匹配,这也是本科毕设里性价比最高的做法。因为古诗词领域的问法相对固定,意图类别就那么几种,用规则就能覆盖 90% 的场景,不需要训练数据。

意图识别的核心思路这样处理:先定义几个意图类别,再为每个类别写一组触发关键词。源码里常见的意图有“作者查询”“作品查询”“朝代查询”“意象查询”“一句诗接下一句”这几类。判断逻辑先遍历问题文本中是否出现“谁”“作者”“写的”这类词,命中就归为作者查询;出现“哪些诗”“作品”归为作品查询;出现“什么朝代”“哪个朝代”归为朝代查询。

def parse_intent(question): if any(w in question for w in ['谁写的', '作者是谁', '作者']): return 'AUTHOR_OF_POEM' if any(w in question for w in ['哪些诗', '写过', '作品', '代表作']): return 'POEM_BY_AUTHOR' if any(w in question for w in ['哪个朝代', '什么朝代', '生活在']): return 'DYNASTY_OF_AUTHOR' if any(w in question for w in ['什么意思', '表达什么', '思想感情']): return 'POEM_MEANING' return 'UNKNOWN'

这段代码逻辑很直白,但有一个需要调整的地方:关键词列表放在函数外维护会更好,因为后续加新意图只需要改关键词表,不用动主逻辑。而且any(w in question)这种写法在关键词很多时会有效率问题,但毕设数据量下完全察觉不到。如果想让准确率高一点,可以给每个关键词加权重,比如“谁写的”权重高于“作者”,两个意图都命中时取权重高的那个。源码里大概率没有权重这层,但你在答辩时可以提这个改进点,展示思考深度。

4.2 实体抽取:基于领域词典的中文人名与诗名识别

意图识别完,下一步是抽取实体。这套系统的实体抽取不是用现成的 NLP 工具,而是基于领域词典的最大匹配。因为古诗词领域的实体高度受限:诗人就那么几十个,诗词题目也就几百个,维护一个词典比训练一个命名实体识别模型靠谱得多,准确率也能到 95% 以上。

实现上,作者实体抽取的做法是这样:先准备一个诗人名单,按姓名长度从长到短排序,然后对问题文本做正向最大匹配。为什么要按长度倒序?因为“李太白”和“李白”同时出现在词典里,如果不先匹配长的,“李太”会被错误切出来。这个细节在毕设里经常被忽略,答辩时说出来反而是加分项。

poet_dict = ['李太白', '李白', '杜甫', '杜子美', '白居易', '王维'] def extract_entities(question, dict_list): for name in sorted(dict_list, key=len, reverse=True): if name in question: return {'poet': name} return {} question = "李白的静夜思表达了什么思想感情" entities = extract_entities(question, poet_dict) # entities 输出: {'poet': '李白'}

这段代码的关键点是sort(key=len, reverse=True)这行,保证长词优先匹配。诗名抽取同理,把源码数据里的诗词题目做成词典,用同样的函数抽取。实际工程里还可以把诗人别名和字号加进词典,比如把“太白”映射到“李白”,查询时命中别名也能正确关联到诗人节点。源码里如果没做别名映射,你可以自己在词典里加同义字段,这就是一个很好的优化方向。

4.3 从意图到 Cypher:模板拼接与参数绑定的正确姿势

这是全系统最核心的模块。拿到意图和实体后,需要把自然语言问题翻译成 Cypher 查询语句。源码里实现的是一套模板映射逻辑:每个意图对应一条 Cypher 模板,模板里有参数占位符,运行时把实体值替换进去。这里有个关键点,替换必须用参数化查询,而不是字符串拼接,否则容易出现语法错误和注入风险。

以“李白的静夜思表达了什么思想感情”这个问题为例,意图是POEM_MEANING,实体是“李白”和“静夜思”,匹配模板应该是:先找到李白创作的静夜思节点,返回其主题关系。

def generate_cypher(intent, entities): if intent == 'POEM_BY_AUTHOR': return ( "MATCH (a:诗人 {name: $poet})-[:创作]->(p:诗词) " "RETURN p.title AS title, p.content AS content" ), {"poet": entities['poet']} if intent == 'POEM_MEANING': return ( "MATCH (a:诗人 {name: $poet})-[:创作]->(p:诗词 {title: $title}) " "OPTIONAL MATCH (p)-[:表达主题]->(t:主题) " "RETURN p.title AS title, collect(t.name) AS themes" ), {"poet": entities['poet'], "title": entities['title']} return None, None

注意这里用了$poet和$title参数占位符,执行时通过 py2neo 的parameters传入。这样做的好处是 Cypher 模板可以复用,而且不需要处理引号转义——如果你直接拼字符串,遇到诗词标题里带引号的情况就会翻车。这套系统的诗词数据里可能没有带引号的标题,但人名里如果出现特殊符号,拼接式写法就会报错,参数化写法永远不会。这是我个人强烈推荐保留的写法,不要因为看着麻烦改回字符串拼接。

4.4 查询结果的后处理:从图数据到自然语言答案的拼装逻辑

Cypher 执行完返回的是节点和关系,不能直接扔给前端。源码里做了一个答案拼装层,把查询结果转换成自然语言文本。比如“李白的作品有哪些”执行 Cypher 后返回的是多行title字段,后处理逻辑就要把这些标题拼成“李白的代表作包括《将进酒》《静夜思》等 N 首”。

def format_answer(intent, rows): if intent == 'POEM_BY_AUTHOR': titles = [r['title'] for r in rows] return '李白共写下 ' + str(len(titles)) + ' 首诗,其中包括《' + '》《'.join(titles[:5]) + '》等作品' if intent == 'POEM_MEANING': themes = rows[0]['themes'] if rows else [] return '这首诗主要表达的主题是:' + '、'.join(themes) return '没有找到相关结果'

这段逻辑里有个值得注意的细节:rows[0]['themes']拿到的是 collect 聚合后的列表,如果图库里诗歌节点没有连主题关系,themes会是空列表,所以要做好兜底,不然前端会渲染出空内容。后处理层在答辩时被问到的概率很高,因为导师往往关心“你是怎么做答案生成的”,你要能说清楚从图查询结果到文本拼接的每一步。

5. 环境搭建与常见问题排查:Neo4j 版本、py2neo 依赖和中文乱码的六个坑

5.1 版本选型参考:Python、Neo4j、py2neo、Flask 怎么配套

这个源码包遇到的问题,我按常见的毕设环境还原一下。Python 版本建议选 3.8 到 3.10 之间,太新可能会遇到依赖包不兼容;Neo4j 社区版建议选 4.x,因为 py2neo 从 5.0 开始只支持 Neo4j 4.x 以上,如果你用 Neo4j 3.5 配新版本 py2neo 会出现握手失败的问题。

版本匹配的参考表如下,这套匹配关系我验证过多次:

组件建议版本说明
Python3.8 ~ 3.103.11 以上部分旧版 gevent 不兼容
Neo4j4.4.x稳定且资料多,社区支持完善
py2neo5.3.x 或 5.2.x需配合 Neo4j 4.x 使用
Flask2.x与 Python 3.8+ 有良好兼容性
DataTables1.11.x与 Bootstrap 4.6 主题配合正常

如果你用 Neo4j 5.x 也没问题,但 py2neo 6.x 的接口变化比较大,源码里的查询写法可能要改。装依赖时不要一股脑装最新版,先看源码里 requirements.txt 提的版本范围,没写的话按上面这个表装,最稳。遇到报错先看版本,别上来就改代码。

5.2 六个高频踩坑记录:现象、原因与解决

第一条是最经典的连接失败。现象是启动 Web 服务后,第一次问答请求就报 “Authorization failed” 或者连接超时。原因是 Neo4j 默认仅本机验证,或 py2neo 连接串里写了旧版本默认的密码,实际数据库密码被改掉了。解决办法是打开 Neo4j 的配置文件确认初始密码,再在源码的配置类里改成对应账号密码,注意 py2neo 5.x 连接串写法是neo4j://host:7687,是neo4j://不是bolt://。

第二条是 Cypher 中文标签查不到数据。现象是前端输入“李白的诗”,后端日志显示查询成功但返回空。原因是导入时节点标签或属性名带了空格或编码不一致,比如导入用了“诗人”,查询时用了“诗人 ”多一个空格,自然匹配不上。解决方法是在浏览器里直接执行MATCH (n) RETURN labels(n), keys(n) LIMIT 5查看实际存储的标签,再调整模板里的标签名。

第三条是中文乱码。现象是前端页面诗词内容显示成????。原因是 CSV 或 JSON 文件编码不是 UTF-8,或者导入脚本里没指定编码就读取。解决办法是用记事本打开数据文件另存为 UTF-8 编码,同时在导入脚本里强制encoding='utf-8',前端页面再加<meta charset="utf-8">。

第四条是静态文件 404。现象是页面打开没有样式,控制台一堆红色报错。原因是静态目录引用路径和实际目录不匹配。解决办法是检查 Flask 初始化时static_folder参数,以及模板里用url_for('static', filename='...')生成路径,不要写相对路径。

第五条是数据量小但查询慢。现象是每次问答要等两三秒钟。原因是图库里没有为常用属性建索引,比如诗人.name和诗词.title。解决办法是执行CREATE INDEX ON :诗人(name)和CREATE INDEX ON :诗词(title),查询速度立刻提升一个量级。这个优化点在答辩时提出来也是加分项,因为说明你理解 Neo4j 的索引机制。

第六条是 import 脚本重复执行导致数据翻倍。现象是跑了两遍导入脚本,查“李白”返回 20 首而不是 10 首。原因是脚本没有做幂等处理,重复执行时又创建了一遍节点和关系。解决办法是导入脚本开头先执行MATCH (n) DETACH DELETE n清库,或者用MERGE代替CREATE来避免重复创建。这个坑在源码里很常见,但补上后数据质量会明显改善。

6. 把毕设代码改出高级感:追问式问答与图谱排序的两个实战技巧

6.1 单轮问答升级成多轮澄清

源码里的问答是单轮的,问一句答一句,意图书和实体都是独立的。你可以加一个简单的多轮机制,这个改动量小但答辩演示效果很好。做法是:后端新增一个会话存储字典,Key 是 session_id,Value 是上一次对话的意图和实体。当当前提问缺少关键实体,而上一次对话里有足够上下文时,自动补全。

比如用户先问“李白写过哪些诗”,再问“他是哪个朝代的”,第二个问题没有出现实体名“李白”,按单轮规则会返回未知。做了多轮补全后,系统发现当前问题缺实体,就从上一次对话里取实体,从而正确回答“唐代”。这个机制用不到复杂框架,一个全局字典就能搞定,核心代码也就十几行,但能直观展示你对“对话系统”的理解比普通查询系统深一层。

6.2 给关系加权:排序结果更有说服力

源码里查询“包含月亮的诗”返回的是所有关联节点,顺序可能不理想。你可以在诗词和意象的关系上增加weight属性,比如诗中“月亮”出现次数多则权重高,查询时按权重倒序排列并截取前五条。这样答辩时导师问“你怎么保证答案质量”,你就有话可说:不是把所有匹配结果罗列出来,而是按相关性排序取 Top N。

这里有个实现细节,查询时使用ORDER BY必须配合LIMIT才有意义,否则图库会把全量数据取回来排序再截断,性能很差。

从那以后,我每次拿到这类毕设源码都会强制自己先跑一遍导入脚本再开前端页面,确认图库非空再继续改代码,这个习惯帮我避开了一大半无意义的调试时间,希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进