简介:基于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_probability |
| Symptom | 症状 | name |
| Drug | 药品 | name、drug_desc |
| Food | 食物 | name、food_attribute |
| Check | 检查项目 | name |
| Department | 科室 | name |
关系类型表如下,起点终点和业务含义都列清楚。这里是整个图谱的骨架,也是后面写 Cypher 模板的依据。
| 关系类型 | 起点 | 终点 | 一句话描述 |
|---|---|---|---|
| HAS_SYMPTOM | Disease | Symptom | 感冒 → 发热 |
| DRUG_OF | Disease | Drug | 感冒 → 复方氨酚烷胺片 |
| NEED_CHECK | Disease | Check | 肺炎 → 胸部X线 |
| DEPARTMENT_OF | Disease | Department | 高血压 → 心血管内科 |
| FOOD_OF | Disease | Food | 高血压 → 低盐食品,用属性区分宜吃/忌吃 |
| COMPLICATION_OF | Disease | Disease | 糖尿病 → 视网膜病变 |
三条典型三元组可以先肉眼过一遍:“感冒 - 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 版本组合:先统一版本再谈复现
知识图谱这种项目,跑不起来的首要原因常常不是代码问题,而是版本组合没对上。这个组合我建议直接照抄,能少走一半弯路。
| 组件 | 建议版本 | 说明 |
|---|---|---|
| Python | 3.8 | 3.9 以上也能跑,但个别依赖会有 warning |
| Neo4j | 3.5.x 社区版 | 资源里的库文件是 3.x 存储格式 |
| py2neo | 4.1.x | 与 Neo4j 3.5 通过 HTTP 通信的常见搭配 |
| JDK | 1.8 | Neo4j 3.5 只认 Java 8 |
| Flask | 2.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_HOME="C:\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_path="dict/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(f"MATCH (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(), key=lambda x: len(x[0]), reverse=True): 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, name=disease_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(force=True) question = payload.get("question", "").strip() result = searcher.search(question) result["code"] = 200 return jsonify(result) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)auth元组里的密码要和 Neo4j 里设置的一致,这是连接能否成功的命门。get_json(force=True)表示只要请求体是合法 JSON 就解析,不强制要求 Content-Type 头。debug=False是因为 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.x,py2neo 用 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-8,jieba 加载时第一个词被读成了\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 章的流程走,正常半小时内能看到图谱,然后就能在页面上和它对话。希望帮到你。
本文还有配套的精品资源,点击获取