简介:知识图谱驱动的教务智能问答系统Python源码项目,面向需要完成毕业设计、期末大作业或项目实战的计算机相关专业学生,覆盖教务数据整理、Word2Vec模型训练、问句解析与答案检索等关键环节。压缩包共39个文件,体积约1.07MB,包括22个csv教务数据文件(选课、毕业、答辩、奖学金等场景)、7个py核心模块、4个txt规则与停用词说明、3个bin词向量模型,以及png示意图和md说明文档。全部源码经本地编译与严格调试,可正常运行;该大作业曾获导师认可,评审98分,难度适中,便于学习者快速掌握基于知识图谱的问答系统完整流程。目前已有62人学习下载,适合作为课设或毕设参考,也可在此基础上扩展新功能。
1. 当我把“基于知识图谱的教务智能问答系统”当毕设主线后:一个周末才真正跑通
选毕业设计题目时,很多人会在“管理系统”和“算法研究”之间反复纠结。管理系统太普通,算法研究又怕自己做不完。我最后拿到的是一个基于知识图谱的教务智能问答系统,Python 实现,带完整的源码和一篇说明文档。它的核心不是搜索框加数据库,而是把课程、教师、教室、时间这些教务实体抽成图谱结构,让“这门课谁教的”“马原在哪个教室上”这类自然语言问句,能被拆解成图查询并返回精确答案。适合正在做毕业设计或期末大作业的人,前提是你愿意花时间把 Neo4j、问答管线、Cypher 这三样东西串起来,而不是只改个界面交差。
2. 系统架构与知识图谱建模:实体、关系、属性如何撑起问答
2.1 三层架构:数据层、图谱层、问答层的职责边界
这套项目拿到手之后,我习惯先把代码目录过一遍,搞清楚每个文件夹在干什么。它的整体结构并不复杂,本质上是三个层次:最底下是数据层,负责把教务系统导出的 CSV、Excel 或者 MySQL 数据清洗成结构化记录;中间是图谱层,通过 Python 脚本把记录写入 Neo4j 图数据库,形成课程、教师、学院、教室、时间节点以及它们之间的关系;最上层是问答层,接收用户输入的自然语言问句,经过意图识别、实体映射、Cypher 模板生成三步,最后返回答案。
三层之间用接口隔开,数据层只关心“怎么把数据塞进 Neo4j”,不关心用户问了什么;问答层只关心“怎么把问句变成图查询”,不关心数据从哪来。这个边界很重要。我之前见过有人把实体抽取逻辑写在视图函数里,结果换一批数据就要改问答代码,两头耦合,改起来非常痛苦。这套项目的分层虽然简单,但每层之间的调用关系很清晰,你拿到之后可以按层去读源码。
问答层里还有一个容易被忽略的小模块:答案格式化。它的工作是把图查询返回的记录转成自然语言句子。比如查询结果是[("数据结构", "张三")],格式化模块会输出“《数据结构》由张三老师讲授”。如果没有这一层,用户看到的就是裸的数组,谈不上“智能问答”。
2.2 教务领域的实体与关系设计:六类实体与五类关系的取舍
知识图谱的核心是“实体-关系-属性”三元组。在教务场景下,实体的选择直接决定问答系统能回答什么问题。这套代码默认设计了六类实体:课程、教师、学院、教室、时间、学生。关系一共五类:
- 学院开设课程:
(学院)-[:开设]->(课程) - 教师讲授课程:
(教师)-[:讲授]->(课程) - 教师隶属于学院:
(教师)-[:属于]->(学院) - 课程安排在教室:
(课程)-[:安排]->(教室) - 课程依赖先修课:
(课程)-[:先修]->(课程)
这个设计最巧妙的地方是“时间”没有做成关系,而是做成了课程的属性。很多新手建模时容易把时间当成独立节点,结果问“周三下午有什么课”时要写非常绕的 Cypher。这里把上课时间存成Course.time_slot属性,比如"周三 3-4节",查询时直接用字符串匹配,牺牲了一点规范性,但换来了问答开发的效率。对于毕业设计来说,这个取舍完全合理。
如果你想把这套系统扩展成“选课推荐”方向,可以再加一条(学生)-[:选修]->(课程)关系;如果想支持“某位老师这学期带了几门课”的统计类问题,可以在Teacher节点上维护course_count属性,而不是每次实时数关系数量。提前想清楚未来要回答哪几类问题,再去决定要不要加节点和关系,不要一上来就建模成一个大而全的图。
2.3 Neo4j 数据落地:Cypher 批量导入与索引配置
数据从 CSV 进入 Neo4j 的方式有两种:用neo4j-admin import做离线全量导入,或者用 Python 驱动逐条写入。这套项目源码里用的是第二种方式,因为教务数据量级通常只有几千条,逐条写入完全够用,而且代码更直观,方便你 debug。
我一般会把导入脚本拆成三个步骤:先建约束和索引,再合并节点,最后合并关系。以课程节点为例,代码长这样:
from neo4j import GraphDatabase driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "yourpassword")) def create_constraints(session): # 课程节点的 id 属性必须唯一,防止重复导入产生脏数据 session.run("CREATE CONSTRAINT course_id IF NOT EXISTS FOR (c:Course) REQUIRE c.id IS UNIQUE") # 教师节点同理 session.run("CREATE CONSTRAINT teacher_id IF NOT EXISTS FOR (t:Teacher) REQUIRE t.id IS UNIQUE") def upsert_course(session, course_id, name, hours, time_slot): # MERGE 而不是 CREATE,避免重复运行脚本时插入重复节点 session.run( """ MERGE (c:Course {id: $course_id}) SET c.name = $name, c.hours = $hours, c.time_slot = $time_slot """, course_id=course_id, name=name, hours=hours, time_slot=time_slot ) def upsert_teacher(session, teacher_id, name, title): session.run( """ MERGE (t:Teacher {id: $teacher_id}) SET t.name = $name, t.title = $title """, teacher_id=teacher_id, name=name, title=title ) def create_teach_relationship(session, teacher_id, course_id): # 关系幂等:先 MATCH 再 MERGE,防止重复建边 session.run( """ MATCH (t:Teacher {id: $teacher_id}) MATCH (c:Course {id: $course_id}) MERGE (t)-[:讲授]->(c) """, teacher_id=teacher_id, course_id=course_id ) with driver.session() as session: create_constraints(session) upsert_teacher(session, "T001", "张三", "副教授") upsert_course(session, "C101", "数据结构", 48, "周一 1-2节") create_teach_relationship(session, "T001", "C101") driver.close()这段代码里的几个细节值得注意。第一,约束必须在导入数据之前创建,否则重复执行导入脚本会在第二次运行时生成重复节点。第二,MERGE是“有则匹配,无则创建”,配合SET可以做到幂等更新,这是导入脚本能反复跑的关键。第三,关系创建时先MATCH两个节点再MERGE关系,如果节点不存在,这条语句会静默跳过,不会报错。调试的时候你可能会因此找不到某些关系,所以我会在关系创建语句后面加一个打印,输出当前处理的教师和课程编号。
2.4 与 MySQL 存储方案的差异:为什么图数据库更适合问答
这个项目如果换成 MySQL 也能做问答,用SELECT加JOIN实现,但图谱方案的优势在“多跳查询”上体现得特别明显。比如“张老师教的《数据结构》在哪个教室上课”,在关系型数据库里要JOIN三张表,而在图数据库里是一条三段式的路径查询:
MATCH (t:Teacher {name: "张老师"})-[:讲授]->(c:Course)-[:安排]->(room:Classroom) RETURN room.name这不是语法层面的便利,而是图数据库把“关系”作为一等公民存储,遍历路径天然高效。同时,Neo4j 自带的 Browser 界面可以直接把查询结果渲染成图谱,答辩时展示效果远好于一张二维表格。代价是运维成本高一些:需要单独安装 Neo4j,还要处理驱动版本兼容问题。这部分在第五章会具体展开。
3. 问答管线:从自然语言到 Cypher 查询的四步实现
3.1 问句预处理与意图识别:规则兜底加朴素贝叶斯分类
问答层是整个项目里代码量最大、也是最容易出问题的地方。用户可能问“数据结构谁教的”“数据结构是哪个老师带”“我想了解数据结构这门课的老师”,三种问法语义相同,字面差异很大。如果只用关键词匹配,规则会越写越多,最后变成一堆if嵌套。
这套源码的做法是先做意图分类再提取实体。意图主要分成四类:查询课程的授课教师、查询教师所授课程、查询课程的时间和地点、查询课程的先修要求。代码里用了一个非常轻量的方案:朴素贝叶斯分类器加规则兜底。
from sklearn.feature_extraction.text import CountVectorizer from sklearn.naive_bayes import MultinomialNB import jieba # 训练语料:每句话对应一个意图标签 corpus = [ "这门课是谁教的", "数据结构哪个老师带", "张三老师教哪些课", "马原在哪个教室上课", "大学物理什么时候上课", "学数据结构之前要先学什么" ] labels = ["teacher", "teacher", "courses", "location", "time", "prerequisite"] # 自定义分词器:对中文问句做分词,再转成词频向量 def tokenize(text): return list(jieba.cut(text)) vectorizer = CountVectorizer(tokenizer=tokenize) X = vectorizer.fit_transform(corpus) clf = MultinomialNB().fit(X, labels) def predict_intent(question: str) -> str: # 先走规则:包含“谁教/哪个老师”直接判定为 teacher 意图 if any(word in question for word in ["谁教", "哪个老师", "哪位上"]): return "teacher" # 规则没命中再走分类器 vec = vectorizer.transform([question]) return clf.predict(vec)[0] print(predict_intent("人工智能是谁讲的"))这段代码展示了问答系统里最重要的工程思想:规则和模型互补。分类器的好处是能处理没见过的问法,坏处是训练样本太少时容易误判。比如“谁教”这个词如果没出现在训练语料里,朴素贝叶斯可能把它归到错误的类别。所以在分类器前面加一道规则闸门,用少量高强度关键词直接锁定意图,兜底策略能显著提高准确率。
训练语料可以扩充。我后来往里面加了二十来条真实问法,每加几条就重新跑一遍测试集,看有没有把已有问法带偏。如果你完全没有标注数据,也可以跳过 sklearn,直接用jieba分词加关键词评分做意图判断,效果差距不大,但代码量会少很多。
3.2 实体映射:同义词表与编辑距离的双保险
意图识别完成后,下一步是从问句里抽出要查询的实体名称。这一步的难点不是“抽取”,而是“归一”。用户在问答系统里问“马原”,但图谱里存的课程名是“马克思主义基本原理概论”;用户问“高数”,图谱里存的是“高等数学”。如果直接拿原词去匹配节点,必然查不到。
这套系统处理实体归一的方式是“同义词表加编辑距离”。同义词表是手工维护的词典,把常见口语缩写映射到正式名称;编辑距离用来处理“马克思主义基本原理”这种写了一半的情况,通过计算字符串相似度去图谱里找最接近的课程名。
from difflib import SequenceMatcher # 同义词表:左侧是用户口语,右侧是图谱中的正式名称 synonym_map = { "马原": "马克思主义基本原理概论", "高数": "高等数学", "大物": "大学物理", "离散": "离散数学" } course_names_from_graph = [ "马克思主义基本原理概论", "高等数学", "大学物理", "离散数学", "数据结构", "操作系统" ] def resolve_entity(raw_name: str) -> str: # 第一步:查同义词表 if raw_name in synonym_map: return synonym_map[raw_name] # 第二步:计算与所有课程名的相似度,取最高分 best_match = None best_score = 0.0 for name in course_names_from_graph: score = SequenceMatcher(None, raw_name, name).ratio() if score > best_score: best_score = score best_match = name # 相似度阈值设为 0.6,防止把无关课程强行匹配上 return best_match if best_score > 0.6 else None print(resolve_entity("马原")) # 走同义词表 print(resolve_entity("马克思主")) # 走编辑距离代码里的阈值 0.6 是一个经验值。我测试下来,低于这个值的匹配基本都是错的,比如“计算机”和“数据结构”的相似度在 0.5 左右,不该被认为是同一门课。编辑距离在这里用的是SequenceMatcher,它对中文字符串的效果尚可,但它返回的ratio()对短文本的波动比较大。如果你要更稳健,可以换成基于字符 n-gram 的 Jaccard 相似度,尤其适合处理“马克思主”这种带缺字的场景。
3.3 Cypher 模板生成与参数回填
实体归一之后,系统已经知道用户问的课是“数据结构”,意图是“teacher”,接下来要把这两个信息拼成一条 Cypher 查询语句。源码里的做法是预定义模板,把实体值回填进去,而不是用字符串拼接。
CYPHER_TEMPLATES = { "teacher": "MATCH (c:Course {{name: '{course}'}})<-[:讲授]-(t:Teacher) RETURN t.name", "location": "MATCH (c:Course {{name: '{course}'}})-[:安排]->(r:Classroom) RETURN r.name", "time": "MATCH (c:Course {{name: '{course}'}}) RETURN c.time_slot", "prerequisite": "MATCH (c:Course {{name: '{course}'}})-[:先修]->(pre:Course) RETURN pre.name", "courses": "MATCH (t:Teacher {{name: '{teacher}'}})-[:讲授]->(c:Course) RETURN c.name" } def generate_cypher(intent: str, entities: dict) -> str: template = CYPHER_TEMPLATES[intent] # 注意这里用 format 回填,实际使用时应改成参数化查询,详见避坑章节 return template.format(**entities)为什么用模板而不是直接写死 Cypher?因为意图和查询结构是一一对应的,模板把“问句→Cypher”的映射收敛到一张表里,加新意图时只需追加一个模板,不需要动其他代码。这在毕业设计的代码讲解环节非常加分——你可以直接打开这张表,指着它说明“每一种问题类型对应一种图查询模式”。
但这里有一个非常关键的坑:模板里的name属性值来自用户输入,如果直接用format回填,遇到课程名里带单引号或者反斜杠的情况,Cypher 语句会直接语法报错。更严重的风险是类注入攻击——用户在问句里塞一段恶意 Cypher,如果被原样拼进查询语句,会对图数据库造成破坏。所以我在实际改造时把模板改成了参数化写法,用$course占位符配合驱动层的参数传值,这个细节放在第五章详细讲。
3.4 答案组织与多结果处理
Cypher 查询返回的往往是一个列表,比如“操作系统”这门课可能由两个老师共同讲授,或者“先修课程”查询返回了:《离散数学》《程序设计基础》两门课。直接把列表原样输出给用户,体验很生硬。答案格式化模块需要做三件事:判断结果数量,单条结果直接拼接句子,多条结果用顿号或分号连接,空结果返回“暂未查询到相关信息”。
def format_answer(intent: str, result_rows: list) -> str: if not result_rows: return "抱歉,暂未查询到相关信息,可以换一种说法试试。" values = [row[0] for row in result_rows] if intent == "teacher": if len(values) == 1: return f"这门课由{values[0]}老师讲授。" return f"这门课由{'、'.join(values)}老师共同讲授。" elif intent == "location": return f"上课地点在{values[0]}。" elif intent == "time": return f"上课时间是{values[0]}。" elif intent == "prerequisite": return f"先修课程为{'、'.join(values)}。" elif intent == "courses": return f"{values[0]}老师讲授的课程有:{'、'.join(values)}。"这段代码的价值在于把“查询结果”和“展示逻辑”解耦。你如果想让系统更拟人化,可以把“老师讲授”改成“老师主讲的课程包括”,只需要改这一个函数。注意values[0]的索引操作,前提是 Cypher 查询的 RETURN 子句第一列就是你要展示的字段。如果你改动过 Cypher 模板,一定要同步检查这里,否则可能出现标题对了但答案不对的诡异问题。
4. 项目代码落地:Python 工程的目录结构、依赖与启动步骤
4.1 目录结构与各模块职责
把源码包解压之后,第一件事不是急着跑,而是先花十分钟看目录结构。这套项目的组织方式属于“教学型工程结构”,每个文件负责一件事,命名直白,适合在毕业设计文档里画架构图。常见的目录划分如下:
| 目录/文件 | 职责 |
|---|---|
data/raw/ | 存放教务数据的原始 CSV 文件 |
data/processed/ | 清洗后的中间数据,一般有 course.csv、teacher.csv |
graph_builder/ | 知识图谱构建脚本,负责读数据、建约束、写节点和关系 |
qa_engine/ | 问答引擎,包含意图识别、实体映射、Cypher 生成、答案格式化 |
api/ | 后端接口层,一般用 Flask 或 FastAPI 封装问答接口 |
web/ | 前端页面,聊天框样式的 HTML/JS |
config.py | 全局配置,Neo4j 连接参数、同义词表路径 |
requirements.txt | Python 依赖清单 |
这个结构的优点是把“构建图谱”和“问答推理”分开,两者通过 Neo4j 数据库解耦。你可以单独跑graph_builder里的脚本重建图谱,也可以单独启动问答服务做接口测试。如果某一天你想把这个系统改成图书推荐问答,只需要换掉data/raw/里的数据源和graph_builder里的实体定义,qa_engine基本不用动。
4.2 环境准备与依赖安装
照着requirements.txt安装依赖,最核心的包只有四个:neo4j-driver、jieba、sklearn、flask。如果你用的是 Python 3.10 及以上版本,建议先升级 pip 再安装,否则 sklearn 的旧版本可能拉不下来。
# 创建独立虚拟环境,避免污染系统 Python python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate # 升级 pip 后安装依赖 pip install --upgrade pip pip install -r requirements.txt依赖安装这一步最容易翻车的点是neo4j-driver的版本和 Neo4j 服务端的版本不匹配。源码里默认用的是 4.x 版本的驱动,对应的 Neo4j 服务端最好是 4.4 系列。如果你图省事装了一个最新版 Neo4j 5.x,再用 4.x 驱动去连bolt://端口,连接时会报协议握手错误或不支持的版本号。遇到这种情况不要慌,升级驱动到 5.x 即可。
安装完依赖之后,检查本机是否有 Java 环境,因为 Neo4j 是基于 JVM 的。终端执行java -version,如果没有 Java 或版本低于 11,需要先装一个 OpenJDK 11 或 17。这一步常常被忽略,等 Neo4j 启动报 “Unable to start” 的时候才反应过来。
4.3 数据导入与系统启动
环境和依赖就绪后,启动流程分为三步:启动 Neo4j 服务,运行图谱构建脚本,启动问答 API 服务。
# 第一步:启动 Neo4j 服务 neo4j start # 第二步:构建知识图谱,脚本会读取 data/processed/ 下的 CSV 并写入 Neo4j python graph_builder/build_graph.py # 第三步:启动问答服务,默认监听 127.0.0.1:5000 python api/app.py启动完成后,打开浏览器访问http://127.0.0.1:5000,你会看到一个简单的聊天界面。输入“数据结构谁教的”,如果一切正常,系统会返回“这门课由张三老师讲授。”到这里,项目就真正跑通了。
build_graph.py这个脚本在打包资料里可能不叫这个名字,你可以根据目录结构找到对应的入口文件。运行之前确认config.py里的 Neo4j 密码已经改成你自己的,否则会报认证失败。我自己第一次跑的时候就是忘了改密码,花了几分钟排查错误日志才发现认证信息不对。
4.4 把默认数据换成你自己的教务数据
这是绝大多数人拿到项目后想做的第一件事——把系统里的示例数据换成自己学校的课程表。操作并不复杂,只需要维护两张表:课程表和教师表。
课程表course.csv建议包含四列:id(唯一编号)、name(正式课程名)、hours(课时数)、time_slot(上课时间)。教师表teacher.csv包含三列:id、name、title(职称)。再准备一个关系表teach.csv,每行一对教师和课程的编号。
数据文件准备好之后,打开graph_builder/build_graph.py,看它是从哪个目录读的 CSV。有的版本写死的是文件路径,有的是从config.py里读的,后者更省心。我把自己的课程数据填进去,跑了十分钟,图谱就构建完了,直接在 Neo4j Browser 里执行MATCH (c:Course) RETURN c LIMIT 10就能看到新数据。
需要提醒的是,synonym_map里的同义词必须同步更新。你换了新课表,“马原”这个映射就没有意义了,需要把新课程的口语缩写加进去。别忘了同时更新问答层里的示例问法,否则你会发现老问题问不出新答案。
5. 避坑排查:Neo4j 连接、中文分词、Cypher 注入与数据脏乱
5.1 现象:驱动连接 Neo4j 时报 “Unable to connect” 或 socket 超时
原因:大概率是neo4j-driver和 Neo4j 服务端版本不匹配,其次是config.py里的密码或地址写错。最常见的是驱动 4.x 连 Neo4j 5.x,协议握手失败。另一个隐蔽原因是 Neo4j 监听的是bolt://localhost:7687,如果你把地址写成了http://localhost:7474,这个端口不对,自然连不上。
解决:先确认 Neo4j 版本,neo4j --version,然后去requirements.txt看驱动版本号,两者大版本必须一致。如果服务端是 4.4,驱动就用 4.4.x;如果是 5.x,就升级驱动到最新版。改完版本后重新pip install对应驱动。地址统一用bolt://localhost:7687,认证信息保持和 Neo4j 安装时设置的一致。
5.2 现象:问句“大学物理实验在哪个教室”被识别成“查询教师”意图
原因:jieba 默认词典里“物理实验”可能被切分成“物理”和“实验”,而“实验”在分类器训练语料里没有出现过。朴素贝叶斯在未知词面前倾向于根据先验概率分配类别,如果训练语料里“teacher”意图的样本最多,它就把这句也归进去了。这不是模型的错,是训练语料覆盖不够导致的。
解决:先在qa_engine里找到意图分类的synonym_map或规则列表,把“在哪个教室”“教室在哪”这类带地点关键词的规则加到规则兜底的前置判断里,让规则层优先于分类器。然后把“大学物理实验”整词加进 jieba 的自定义词典,确保分词不再切碎。最后扩充分类器的训练语料,把“地点”意图的样本增加到十五条以上,重新训练。
5.3 现象:用户问“C语言程序设计是谁教的”之后,系统返回空白或直接 500 错误
原因:课程名带字母和中文混合,模板生成 Cypher 时没有做转义。我在 3.3 节提到过format回填的危险——如果用户输入里包含单引号、双引号或者特殊符号,拼进 Cypher 语句后会导致查询语法错误。这个场景里的“C语言程序设计”本身没有特殊字符,但如果图谱里的课程名存的是“C语言程序设计(上)”,带有括号和中文全角符号,某些版本驱动会解析失败。
解决:抛弃字符串拼接,改用参数化查询。neo4j-driver支持$name占位符,把参数通过session.run(query, name=course_name)传进去。参数化之后,任何字符都不会被当作 Cypher 语法的一部分,这是根治问题的方法,不应该再靠转义去补漏洞。
5.4 现象:导入脚本重复执行后,图谱里出现多个同名课程节点,问答结果同时返回两个不同的授课教师
原因:构建脚本里创建节点时用的是CREATE而不是MERGE,且没有在id属性上建立唯一约束。第一次运行脚本插入了一遍数据,第二次运行又插入了一遍。虽然 Neo4j 不会报错,但会影响查询准确性——MATCH (c:Course {name: "数据结构"})会匹配到两个节点。
解决:检查graph_builder脚本开头是否执行了CREATE CONSTRAINT。如果没有,在导入前先执行一次唯一约束创建,并把所有节点写入语句从CREATE改成MERGE。然后再跑一次MATCH (n) DETACH DELETE n清空旧数据,重新导入。从那以后,我每次跑导入脚本前都会先确认约束是否存在,这条检查已经写成了固定习惯。
5.5 现象:用户问“我想知道张老师这学期教几门课”,系统返回“暂未查询到相关信息”
原因:这个问法在意图分类中属于“统计计数类”,但系统预设的四类意图模板里没有覆盖。这不是 bug,是功能边界——源码只支持实体属性查询,不支持聚合统计。本质上是不清楚系统的问答边界,误以为它什么都能答。
解决:要么接受这个边界,在 UI 里加一个“可咨询问题示例”的提示框,把系统能答的问题类型列出来;要么扩展一个count意图,对应 Cypher 模板MATCH (t:Teacher {name: $teacher})-[:讲授]->(c:Course) RETURN count(c),然后给分类器补充“教几门课”“带了几门课”这类训练语料。我倾向于前者,因为毕设的系统边界做得清楚,反而好讲;什么都硬加,最后容易在答辩时被问到露怯。
6. 把这套系统改成你的毕设:换领域、加功能与答辩展示
拿到一套能跑的项目只是起点,真正让它在答辩时有说服力,需要做两件事:一是换一个“看起来不一样”的领域,二是把某个环节做得比默认方案深一点。先说换领域。教务问答和图书问答在图谱结构上完全同构:教务的“课程”换成“图书”,“教师”换成“作者”,“教室”换成“图书馆分馆”,意图从“查老师”变成“查作者”,整个graph_builder和qa_engine的代码几乎不用动。我见过一个同学把这个系统改成了“电影问答系统”,把课程换成电影,教师换成导演,教室换成上映影院,照样跑通。所以你别被“教务”两个字限制住,换个数据源,这就是一个新题目。
加功能方面,我建议优先在实体映射上做文章。默认方案用的是同义词表加SequenceMatcher,你可以在网上找一份“课程简称大全”扩充同义词表,或者把SequenceMatcher升级成基于字向量的语义匹配,这会让“人工智能导论”和“AI导论”这类相似但字面无交集的问法也能被正确映射。这个小改进很容易在答辩现场演示:连续输入几个“缩写版”问法,系统全部回答正确,说明你理解实体归一的核心难点。
答辩展示时,不要只把网页演示一遍,那是最后一分钟的事情。我建议按相反顺序展示:先打开 Neo4j Browser,输入MATCH (c:Course {name: "数据结构"})-[:先修]->(pre:Course) RETURN pre.name,现场展示图谱关联;再打开系统网页,输入“数据结构有哪些先修课”,返回结果;然后点开一段日志截图,证明这个问题经过了意图识别、实体映射、Cypher 生成三步。这组演示能同时覆盖建模能力、工程能力和排错能力。
我自己在这套项目上花的时间,有一半耗在 Neo4j 驱动版本兼容和实体映射调参上。那些看起来不起眼的坑,反而成了最后报告里最真实的素材。从那以后,我每次拿到新项目都强制走一遍“先看目录、再跑通最小流程、最后逐层改数据”的流程,这习惯帮我省了无数个深夜。这套资料里的代码未必十全十美,但该有的骨架都在,希望你拿到的版本比我这份少几个坑。希望帮到你。
本文还有配套的精品资源,点击获取