医疗知识图谱问答系统毕设源码详解:Neo4j+Flask+模板匹配 简介基于Python的医疗领域知识图谱问答系统毕业设计完整项目代码与文档齐备主要面向计算机类专业的毕设学生也适合需要知识图谱、问答系统实战练习的中级学习者。项目经导师指导并获评审99分覆盖医疗知识图谱构建、实体关系抽取、问答检索与前端展示等核心流程代码确保可运行零基础用户按文档也能复现。资源包共188个文件大小约115MB文件类型以Python脚本41个.py、HTML页面与前端资源html/css/js、txt说明文档、数据库及图谱存储文件db为主目录结构清晰便于分别查找后端逻辑、前端界面和数据配置。目前已有78人学习/浏览可作为毕业设计参考也可直接用于课程设计或期末大作业。借助完整的前后端代码、项目文档与已构建的图谱数据可快速理解医疗问答系统的实现思路减少从零搭建的工作量并为论文撰写、系统演示提供直接素材。1. 医疗知识图谱问答系统这套毕设源码到底含了什么做毕设的同学搜“知识图谱问答系统”时最怕遇到两种仓库一种只有 README 和几张截图另一种代码齐全但数据库是空的跑起来问答永远返回“抱歉我不明白”。这套基于 Python 的医疗知识图谱问答系统属于少见的那种——连 Neo4j 图数据库的物理存储文件都一起打包你能在资源里直接看到 neostore.transaction.db、neostore.counts.db 这类原始库文件导入就能用浏览器里点开就是一个带疾病、症状、药物、科室关系的可视化图谱输入“感冒有什么症状”“高血压挂什么科”能返回结构化答案。这套组合很典型Flask 做 Web 层、Neo4j 做存储、jieba 分词加模板匹配做问答代码完整可运行评审 99 分。适合正在做毕业设计的学生也适合想用一周时间把知识图谱从概念落到代码的开发者拿来当课程设计、期末大作业一样成立。2. 技术选型与架构拆解为什么是 Neo4j Flask 模板匹配2.1 存储选型Neo4j 比 MySQL 好在哪医疗领域的真实数据长什么样疾病和症状、药物、科室、并发症之间的连接密度极高一个疾病平均关联十几个症状和药物本质是一张网。用 MySQL 存三元组表看起来每条记录都清楚但一旦要回答“高血压的并发症有哪些、这些并发症又该挂什么科”这种多跳问题就得连续 join 四五张表SQL 写得又长又难维护。Neo4j 里这种查询就是一条路径模式的事Cypher 天生顺着关系走。而且答辩时在浏览器里展示图谱节点连线比给评委看几张 Excel 表直观得多。这个项目里出现了 neostore.relationshipstore.db、neostore.propertystore.db 这类文件说明图谱数据已经构建完毕不是空库——你下载过其他“知识图谱毕设”就会知道很多仓库只有爬虫脚本没有最终产物而这套直接把数据库底层文件给全了这属于最省心的形态。2.2 问答方案选型不堆 BERT 的三条理由问答层是最容易被纠结的地方。有同学问为什么不直接上 BERT 微调做深度学习问答。我的看法是这是毕设不是论文复现模板匹配在这个场景下赢在三点。第一不需要 GPU 和预训练模型下载实验室一台普通电脑就能跑复现门槛低。第二结果可解释答辩时被问到“为什么返回这个答案”你可以直接定位到某条模板和某句 Cypher而不是面对一个神经网络黑匣子。第三数据集好构造医疗领域问法相对固定模板覆盖得住常见问题。模板方案的天花板也清楚没被模板覆盖的问法会落到兜底答案这个缺陷可以在答辩时主动讲然后引出实体链接、相似问法扩展这些改进方向反而显得你思考过边界。整体架构分三层数据层用 Neo4j 存实体与关系通过 py2neo 访问问答层用 jieba 分词加自定义词典做实体识别和意图识别模板匹配生成 Cypher再组装答案展示层用 Flask 提供 HTTP 接口前端页面负责渲染图谱和问答结果。三层之间通过函数调用和 JSON 传递数据职责清楚写文档也好分章节。2.3 数据模型6 类实体 6 类关系这套系统的图谱设计遵循医疗知识图谱构建的常见做法实体类型 6 类关系 6 类。实体表如下。实体标签含义典型属性Disease疾病name、desc、cause、prevent、cure_way、cure_lasttime、cured_probabilitySymptom症状nameDrug药品name、drug_descFood食物name、food_attributeCheck检查项目nameDepartment科室name关系类型表如下起点终点和业务含义都列清楚。这里是整个图谱的骨架也是后面写 Cypher 模板的依据。关系类型起点终点一句话描述HAS_SYMPTOMDiseaseSymptom感冒 → 发热DRUG_OFDiseaseDrug感冒 → 复方氨酚烷胺片NEED_CHECKDiseaseCheck肺炎 → 胸部X线DEPARTMENT_OFDiseaseDepartment高血压 → 心血管内科FOOD_OFDiseaseFood高血压 → 低盐食品用属性区分宜吃/忌吃COMPLICATION_OFDiseaseDisease糖尿病 → 视网膜病变三条典型三元组可以先肉眼过一遍“感冒 - HAS_SYMPTOM - 发热”“感冒 - DRUG_OF - 复方氨酚烷胺片”“高血压 - DEPARTMENT_OF - 心血管内科”。后面的问答系统能回答什么基本由这张关系表决定。2.4 项目目录导读每个目录和文件是干什么的典型的目录布局长这样你拿到资源包后按图索骥即可。medical_kg_qa/ ├── data/ # 原始数据csv 或 json ├── build_graph.py # csv - Neo4j 的图谱构建脚本 ├── app.py # Flask 入口 ├── kg/ │ ├── __init__.py │ ├── entity_recognition.py # 实体识别与预处理 │ ├── question_parser.py # 意图识别与模板匹配 │ └── answer_search.py # 查询执行与答案组装 ├── dict/ │ └── medical_dict.txt # jieba 自定义词典 ├── static/ │ ├── bootstrap.min.css │ ├── info.css │ └── style.css ├── templates/ │ └── index.html └── README.mdstatic 里这几个 css 文件在资源包里能直接看到对应前端页面的样式dict 目录下的医疗词典直接影响实体识别命中率后面会重点讲build_graph.py 是把 csv 数据灌进 Neo4j 的构建脚本。如果资源里另外带了 neo4j 的 data 目录那连这一步都省了直接进第 3 章。3. 环境准备与数据导入让 Neo4j 图谱先跑起来3.1 版本组合先统一版本再谈复现知识图谱这种项目跑不起来的首要原因常常不是代码问题而是版本组合没对上。这个组合我建议直接照抄能少走一半弯路。组件建议版本说明Python3.83.9 以上也能跑但个别依赖会有 warningNeo4j3.5.x 社区版资源里的库文件是 3.x 存储格式py2neo4.1.x与 Neo4j 3.5 通过 HTTP 通信的常见搭配JDK1.8Neo4j 3.5 只认 Java 8Flask2.x轻量路由写法直接py2neo 和 Neo4j 的版本兼容属于这个生态里最玄学的一环。Neo4j 4.x 之后认证协议改动很大py2neo 连接时经常报“password change required”一卡就是半天。既然资源里的库文件明确是 3.x 格式最优解就是整体锁在 3.5 4.1.x别在自己的主力环境里硬升。3.2 安装 Neo4j 并装入自带数据装 Neo4j 社区版和配置 JAVA_HOME 属于同一类操作比配 python 环境变量还简单关键是路径别带中文。完整流程如下。第一步装 JDK 8 并配置环境变量命令行输入java -version能看到 1.8 开头就算过了。第二步下载 Neo4j 3.5 社区版解压。第三步把资源包里的数据库文件覆盖到 Neo4j 安装目录下的 data 文件夹覆盖前先把原有的 data 目录改名备份别直接删。第四步前台启动。# Windows PowerShell在 Neo4j 解压目录执行 $env:JAVA_HOMEC:\Program Files\Java\jdk1.8.0_202 .\bin\neo4j.bat console # Linux / macOS export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 ./bin/neo4j console用 console 前台启动而不是注册成系统服务是为了让你直接看到启动日志报错信息会明确很多。启动成功后浏览器访问http://localhost:7474进入 Neo4j Browser。默认账号 neo4j首次登录会要求修改密码这个密码必须记住后面 py2neo 连接串里的auth(neo4j, 123456)要和它保持一致改了密码这里也要跟着改。如果资源里没有现成的 data 目录也可以自己跑一次图谱构建脚本重建数据命令就一行。python build_graph.py这个脚本做的事情很直接读 data 目录下的 csv 文件用 py2neo 创建节点和关系最后打印写入统计。跑的时候提示连不上数据库先确认 Neo4j 进程还活着再确认脚本里的连接地址和密码。3.3 数据验证用 Cypher 检查图谱是否完整数据库启动后第一时间不要急着跑问答先在 Neo4j Browser 里执行几条 Cypher确认图谱数据真的进来了。// 统计疾病节点数量 MATCH (d:Disease) RETURN count(d) AS disease_cnt; // 统计各关系类型的数量看哪些关系是空的 MATCH ()-[r]-() RETURN type(r) AS rel_type, count(r) AS cnt ORDER BY cnt DESC;这两条能快速暴露问题count 返回 0说明数据没导入成功回头看 build 脚本日志count 有数字但某个关系类型数量为 0说明 csv 里该关系列可能是空值。这两条验证通过了再试一个具体的问答路径。MATCH (d:Disease {name:感冒})-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name LIMIT 10;如果这里能返回发热、咳嗽之类的症状词说明图谱链路是通的可以进入下一步。返回空也不一定全错很可能是疾病实体名不叫“感冒”叫“流行性感冒”先去 data 里确认这个名字的真实写法。4. 问答核心链路拆解从问题文本到 Cypher 的四步流转4.1 问题预处理jieba 自定义词典与停用词问答系统拿到用户问题后的第一件事不是查库而是把文本洗干净。这里用 jieba 做分词但必须加载医疗自定义词典否则“复方氨酚烷胺片”会被切成“复方/氨酚/烷胺/片”实体匹配直接失效。# kg/entity_recognition.py —— 预处理部分 import re import jieba class Preprocessor: def __init__(self, dict_pathdict/medical_dict.txt): jieba.load_userdict(dict_path) # 让复合药名被切成一个词而不是拆成“复方/氨酚烷胺片” jieba.suggest_freq((复方, 氨酚烷胺片), True) self.stopwords {我, 你, 想, 请问, 了, 呢, 下} def clean(self, question): question re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , question) tokens [t for t in jieba.lcut(question) if t not in self.stopwords] return tokens, questionload_userdict 把整个医疗词表装进分词器词表里每一行一个词格式就是纯文本词性标注可加可不加。suggest_freq 只调整切分权重不影响词典本身。clean 里的正则先把标点符号剥掉避免“感冒有什么症状”这种问法里的问号干扰后续匹配。返回两个东西分词后的 token 列表以及去掉标点的干净问题串后续实体匹配直接用干净串做子串查找。4.2 实体识别让系统先知道问题里有什么实体识别的目标是回答一个问题用户这句话里提到了图谱中的哪个实体。最稳的做法是启动时把全库实体名加载到内存构建一个名字到标签的索引查询时直接做子串匹配。# kg/entity_recognition.py —— 实体索引与匹配 from py2neo import Graph class EntityRecognizer: def __init__(self, graph): self.graph graph self.name_index self._build_index() def _build_index(self): index {} for label in (Disease, Symptom, Drug, Food, Check, Department): for rec in self.graph.run(fMATCH (n:{label}) RETURN n.name AS name): index[rec[name]] label return index def match(self, question): hits [] for name, label in sorted(self.name_index.items(), keylambda x: len(x[0]), reverseTrue): if name and name in question: hits.append((name, label)) return hits名字索引只构建一次避免每次问答都全库扫描。匹配遍历所有实体名是 O(N) 的做法这个量级在几千实体的医疗图谱里完全够用想更快可以换成 AC 自动机但毕设阶段没必要。排序按实体名长度降序是个关键细节“心脏病”和“心脏”同时在场时长的先命中避免短词把结果带偏。4.3 意图识别与模板匹配把自然语言翻译成 Cypher实体识别告诉系统“问的是谁”意图识别则要回答“想问什么”。这里用的是关键词打分每个意图配一组关键词问题里命中的关键词越多该意图得分越高。# kg/question_parser.py —— 意图模板定义 INTENT_TEMPLATES { symptom: { keywords: [症状, 表现, 什么感觉], cypher: MATCH (d:Disease)-[:HAS_SYMPTOM]-(s:Symptom) WHERE d.name $name RETURN s.name AS value, reply: {}常见的症状有{}。, }, drug: { keywords: [吃什么药, 用药, 治], cypher: MATCH (d:Disease)-[:DRUG_OF]-(drug:Drug) WHERE d.name $name RETURN drug.name AS value, reply: {}可以使用的药物{}。, }, department: { keywords: [挂什么科, 哪个科室, 看什么科], cypher: MATCH (d:Disease)-[:DEPARTMENT_OF]-(dep:Department) WHERE d.name $name RETURN dep.name AS value, reply: {}建议就诊科室{}。, }, }注意 Cypher 里用的是$name参数占位不是字符串拼接。py2neo 4.x 支持graph.run(cypher, name实体名)这种参数化写法能避免实体名里带单引号之类把查询搞坏。以下是问答执行的胶水层把预处理、实体识别、意图匹配串起来。# kg/answer_search.py —— 问答执行流程 class AnswerSearcher: def __init__(self, graph): self.recognizer EntityRecognizer(graph) self.preprocessor Preprocessor() def search(self, question): _, cleaned self.preprocessor.clean(question) entities self.recognizer.match(cleaned) disease next((e for e in entities if e[1] Disease), None) if not disease: return {answer: 没识别到疾病实体请把疾病名称带上。} intent self._match_intent(cleaned) if not intent: return {answer: 没识别到意图试试问症状、用药或科室。} values self._query(intent, disease[0]) if not values: return {answer: 图谱里暂时没有这条关系的答案换个疾病试试。} return {answer: INTENT_TEMPLATES[intent][reply].format(disease[0], 、.join(values))}这里有一个容易被忽略的约束它优先找 Disease 类型的实体作为查询起点而不是随便拿一个命中实体就去查。原因很简单所有关系都以疾病为中心用户问“感冒吃什么药”命中的实体是“感冒”标签是 Disease直接作为起点“发热”这种症状实体虽然也可能被命中但不会是主查询节点。_query内部就是解析模板里的 cypher调用graph.run(cypher, namedisease_name)取回 value 列表。4.4 Flask 接口与前端联动问答怎么从页面走到图谱后端封装好之后用 Flask 暴露一个 HTTP 接口给前端调用这是最标准的做法。前端拿用户输入的问题POST 给/api/qa后端返回 JSON前端渲染答案。# app.py —— Flask 接口 from flask import Flask, request, jsonify from py2neo import Graph from kg.answer_search import AnswerSearcher app Flask(__name__) graph Graph(http://localhost:7474, auth(neo4j, 123456)) searcher AnswerSearcher(graph) app.route(/api/qa, methods[POST]) def qa(): payload request.get_json(forceTrue) question payload.get(question, ).strip() result searcher.search(question) result[code] 200 return jsonify(result) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)auth元组里的密码要和 Neo4j 里设置的一致这是连接能否成功的命门。get_json(forceTrue)表示只要请求体是合法 JSON 就解析不强制要求 Content-Type 头。debugFalse是因为 debug 模式会开 reloader某些环境下会重复初始化图谱索引。接口起来后用 curl 或者直接在页面问答框里测试都可以。curl -X POST http://localhost:5000/api/qa \ -H Content-Type: application/json \ -d {question: 感冒有什么症状}期望返回的 answer 字段里包含“发热、咳嗽”这类症状词。前端页面把答案渲染到对话区域图谱可视化部分从另一个接口取节点和关系数据用 vis.js 或 echarts 画连线static 里那几个 css 文件就是给这套页面用的。整个链路到这里就闭环了。5. 避坑指南复现这套项目最常见的 5 个坑复现知识图谱项目踩坑是必然的区别只在于踩之前知不知道坑在哪。下面 5 条是按出现频率排的前两条属于环境问题后三条属于数据与代码问题。5.1 环境与依赖版本组合是最大的坑现象py2neo 连接 Neo4j 时报Connection refused或者password change required程序直接抛异常。原因py2neo 4.x 用的是 HTTP 基本认证Neo4j 4.x 之后数据库默认强制要求修改初始密码初次连接会先返回一个必须改密的响应py2neo 不会自动处理。解决锁版本Neo4j 用 3.5.xpy2neo 用 4.1.x。如果你机器上已经装了 Neo4j 4.x别硬凑单独下一个 3.5 版端口错开。资源里的库文件本身就是 3.x 格式升到 4.x 意味着要重新建库得不偿失。现象Neo4j 启动时窗口一闪而过或者控制台报Unsupported Java version。原因Neo4j 3.5 只兼容 Java 8机器上默认的 JDK 是 11 或 17。解决装 JDK 1.8 并显式指定 JAVA_HOME。Windows 上在启动命令前用$env:JAVA_HOME临时指定避免影响机器上其他项目。5.2 数据与数据库Neo4j 起不来的两种姿势现象把资源包的 data 目录覆盖后Neo4j 启动报 store 文件格式错误或者auth相关异常。原因常见是混装了数据目录比如只覆盖了部分文件夹或者覆盖后残留了旧版本的 data 文件导致存储元数据对不上。解决整个 data 目录一起替换别拆开覆盖。还起不来就删除 data 目录下的dbms/auth文件重置认证信息重启后用 neo4j/neo4j 登录再改密码。这个操作不会丢业务数据只是清掉登录凭据。现象Windows 下执行neo4j.bat install-service注册系统服务失败提示权限不足。原因服务注册需要管理员权限普通终端窗口执行会被拒绝。解决直接用neo4j.bat console前台启动开发演示阶段这种方式完全够用日志看得还更清楚。别在服务问题上耗太久这不是技术核心。5.3 代码与运行问答失败先查词典和编码现象问“感冒有什么症状”后端始终返回兜底答案“我还没学会回答这个问题”。原因实体识别没命中。最常见的是medical_dict.txt被存成了带 BOM 的 UTF-8jieba 加载时第一个词被读成了\ufeff感冒和问题里的“感冒”匹配不上。这个坑特别隐蔽肉眼打开词典文件完全看不出来。解决用编辑器把词典另存为“UTF-8 without BOM”格式。改完用下面这段代码自检能正确切出“感冒”一个词就说明词典起作用了。import jieba jieba.load_userdict(dict/medical_dict.txt) print(jieba.lcut(感冒有什么症状))如果输出是[感, 冒, 有, 什么, 症状]基本可以断定是编码问题输出[感冒, 有, 什么, 症状]说明词典加载正常接下来去查意图关键词和实体名是否匹配。提示这个自检脚本建议留着改词典后随时能验证比每次启动整个项目再试快得多。6. 进阶改造与效果验证把医疗图谱换成任意领域的最小改动方案6.1 换领域的最小改动这套系统的核心代码不绑定医疗绑定的只有三处数据文件、实体标签、意图模板。想把医疗图谱换成任意领域最小改动方案是替换 data 目录下的 csv 文件把实体名换成新领域的数据然后改EntityRecognizer._build_index里的标签元组比如电影领域换成 Movie、Actor、Director最后改INTENT_TEMPLATES里的关系和回复模板。最难的不是代码是词典。医疗领域的实体名是相对规整的专有名词换成其他领域后自定义词典要跟着重建。问答系统的准确率一半以上由词典质量决定这是我跑过多个领域改造后最深的体会。模板和关系可以照抄领域词表必须花时间整理。6.2 效果验证用 50 条测试集跑一次基线改造完不知道效果怎么办写一个简单的评测脚本整理 50 条测试问答每条问题配一个期望答案关键词跑一遍看命中率。这个口径是“答案里是否包含期望实体”比字符串完全相等更合理因为答案往往是多个实体拼接的。# evaluate.py —— 问答基线评测 test_set [ (感冒有什么症状, [发热, 咳嗽, 头痛]), (高血压挂什么科, [心血管内科, 心内科]), (糖尿病能吃什么, [低糖]), ] def evaluate(): searcher AnswerSearcher() hit 0 for question, expects in test_set: answer searcher.search(question).get(answer, ) ok any(exp in answer for exp in expects) hit ok print(question, -, OK if ok else FAIL, |, answer) print(命中率: {:.1%}.format(hit / len(test_set)))评测集扩到 50 条命中率能到 85% 以上这套问答系统拿去答辩就是稳的。命中率低于这个值优先查三件事词典有没有覆盖问题里的实体、关系类型名称和数据表是否一致、意图关键词是不是太窄。我从那以后每次接手带 Neo4j 的项目第一件事都是先核对 JDK 版本、Neo4j 版本和 py2neo 版本这三个数再碰数据目录这套血泪经验就是从跑这个医疗问答系统开始养成的。资源里的库文件和代码是齐的按第 3 章的流程走正常半小时内能看到图谱然后就能在页面上和它对话。希望帮到你。本文还有配套的精品资源点击获取