简介:这是一份基于Python的医疗知识图谱问答系统毕业设计资源,面向计算机专业学生与人工智能初学者。项目采用Django、MySQL和Neo4j构建,完整实现了数据抓取、数据存储、数据处理、智能问答与可视化展示五大模块:借助爬虫采集医疗知识,经过清洗、去重、分类后存入图数据库;问答模块利用自然语言处理技术将用户提问转为查询语句,实现知识匹配与结果反馈。资源包共包含280个文件,涵盖Python源码、前端样式、网页模板、项目文档、演示视频等各类材料,压缩包大小约59.13MB,目录结构清晰,方便按模块查阅。目前已有822人学习浏览。通过学习可深入理解知识图谱的构建流程、基于规则的问答实现思路,以及Django后端与前端页面整合方法,可直接参考搭建同类医疗问答系统,也适合作为毕业设计或课程项目的完整范本。
1. 为什么我盯上了这个题材
先说个背景。这几年知识图谱在工业界已经不是新鲜词了,搜索引擎、客服机器人、风控系统里都在用,但真正能让人“拿来练手”并且跑通全流程的项目其实不多。要么是纯理论讲概念,要么是只给你一个训练好的模型黑盒,根本看不到实体抽取、关系构建、图谱存储、问答匹配这些环节到底怎么串起来的。
我拿到这个“基于Python的医疗知识图谱问答系统”项目时,第一反应是:这正好踩中了知识图谱应用里最典型、也最适合落地的垂直场景。医疗领域实体密集、关系明确——疾病、症状、科室、药品、检查、饮食建议,这些概念之间的连接几乎是天然的图谱结构。用户问“感冒了吃什么药”,系统如果知道“感冒”这个实体和“药”这个实体之间存在“推荐用药”关系,就能直接从图里拎出一条路径给答案,不需要像纯关键词搜索那样靠词面匹配猜意图。
这个项目适合谁?我觉得有三类人特别值得上手。第一类是刚学完Python基础,想做点有完整业务逻辑的项目来武装简历的人,这个项目比写一百个计算器Demo有用得多;第二类是已经接触过NLP,但对图数据库、知识表示还停留在概念层面的人,可以通过手动构建图谱把抽象概念落回地面;第三类是想转行做医疗信息化或做数据产品的朋友,这个项目能让你快速理解领域知识结构化之后能产生什么业务价值。
我自己拿到项目后,最先做的一件事就是把压缩包里的文件结构过了一遍,看看它到底是怎么组织的。这也启动了我整个拆解和复现的过程。下面我从设计思路、核心细节、实操过程、常见问题四个维度展开,把整个系统的里里外外讲透。已经跑过这个项目的人可以对着检查自己有没有漏掉关键环节,还没开始的人可以直接把这篇当施工图纸用。
2. 整体设计思路拆解
2.1 医疗知识图谱系统的六个关键模块
打开项目文件,最明显的就是分层结构。它不是一个单文件的脚本堆砌,而是按照功能边界拆成了几个核心模块。我列一下常见的工程组织方式,也是我这边复现时最推荐的结构:
| 模块 | 核心职责 | 关键技术选型 |
|---|---|---|
| 爬虫/数据采集 | 从公开医学网站抓取疾病、症状、药品等信息 | requests、BeautifulSoup、正则表达式 |
| 数据清洗 | 去重、补全、统一实体名称 | pandas、自定义规则 |
| 实体/关系抽取 | 从结构化或半结构化数据中提取三元组 | 规则模板、jieba分词、人工标注辅助 |
| 知识存储 | 将三元组写入图数据库 | Neo4j、py2neo |
| 问答解析 | 识别用户问题意图与实体 | jieba、正向最大匹配、意图规则库 |
| 答案生成 | 根据图谱路径或属性返回最终结果 | Cypher查询、模板组装 |
这个结构非常像生产环境里一个微型的“数据管道”。数据从源头进来,经过清洗、结构化、入库,最后通过问答接口对外服务。每一步都职责单一,方便单独调试。我特别认可这种设计的原因在于——它把复杂的知识图谱应用拆成了可以独立验证的环节,任何一个环节出问题,你都能快速定位到具体模块,而不是对着一个大函数发呆。
2.2 为什么选择Neo4j作为存储层
医疗知识图谱本身就存在大量多跳关系,比如“高血压”通过“并发症”关系指向“脑卒中”,而“脑卒中”又通过“治疗方式”指向“康复训练”。传统的关系型数据库(MySQL)表达这种多级关系会很痛苦——要么设计一大堆中间表,要么查询时多层JOIN,性能和维护成本都很大。
Neo4j这类图数据库不一样,它的核心数据模型就是“节点-关系-属性”,天然就是为知识图谱这种数据结构准备的。在Neo4j里,查询“高血压可能引发哪些严重疾病后再推荐科室”这种多跳问题,一条Cypher语句就能搞定,不用写一堆JOIN链。实际跑下来,在几十万节点规模下查询响应仍然在几十毫秒级别,这对问答系统来说完全够用。
还有一点是Neo4j的Cypher查询语言对开发者非常友好。它长得很像SQL,但又是用模式匹配的思路来表达“找路径”。比如你写MATCH (d: Disease)-[:HAS_SYMPTOM]->(s: Symptom) WHERE d.name = '感冒' RETURN s.name,读起来几乎是自然语言。这大幅降低了团队协作时的沟通成本。如果你用RDF那一套(比如Jena)做存储,学习曲线会陡峭很多,而且对“属性配置”这类常见需求的支持反而不如图数据库直观。
2.3 自顶向下的图谱构建策略
拿到医疗数据之后,直接一股脑灌进Neo4j是不行的。我这次采用的是“先定骨架,再填血肉”的自顶向下方式。先定义这个医疗图谱要涵盖哪些实体类型和关系类型,然后基于这些schema去约束数据的抽取。
在我这套项目里,实体类型包括:疾病、症状、科室、药品、检查项目、饮食建议,共六类。关系类型则设计成:
- 疾病与症状:HAS_SYMPTOM
- 疾病与科室:DEPARTMENT(建议就诊科室)
- 疾病与药品:RECOMMEND_DRUG
- 疾病与检查:NEED_CHECK
- 疾病与饮食建议:FOOD_SUGGESTION
- 疾病与疾病:COMPLICATION
为什么要先定关系?因为在数据采集时你就可以判断——如果某段网页内容没有落到这些关系的路径上,那它暂时就不是系统关心的信息,可以先丢弃。这样能避免把知识图谱做成一个什么都能塞的杂物间。这个原则非常重要,很多入门项目做到最后图谱变得臃肿难用,就是因为没有schema约束,随便一条“疾病-A-b-人群”的信息都往图里塞。
3. 核心细节解析与实操要点
3.1 医疗数据处理时最容易踩的坑
医疗数据的质量和清洗是决定问答系统体验好坏的最关键因素。从我实际跑完整个项目后的感受来说,最值得警惕的有三个问题。
第一是实体名不统一。同一个疾病在不同网站上叫法不一样,比如“慢性阻塞性肺疾病”和“慢阻肺”,“糖尿病”和“2型糖尿病”在不同资料里经常混用。如果你不做对齐直接在Neo4j里建节点,图谱里就会出现多个长得像但实则是同一实体的节点,问答时用户问“慢阻肺”和“慢性阻塞性肺疾病”就变成走两条不同的路。解决办法是在清洗阶段维护一个同义词映射表,或者用相似度算法做实体对齐。这个项目本身规模不大,我建议先用人工维护映射表,把高频同义词覆盖掉,性价比最高。
第二是关系属性缺失。有些数据只有“疾病-A-药品”的二元关系,但没有说明用法用量、适用人群。对于问答系统来说,“感冒了吃什么药”只返回一个药名是可以的,但如果在图谱里存了“药品-禁忌-人群”这种关系但数据不全,问答时就要设计好“不知道”的兜底逻辑,而不是硬返回不完整甚至错误的信息。
第三是爬取数据的编码问题。很多医疗网站页面是GBK或GB2312编码,直接用requests拿回来用UTF-8解析就全是乱码。处理办法是统一在请求后用resp.encoding = resp.apparent_encoding做一次自动探测,再转成UTF-8入库。这个坑我几乎每次写爬虫都会遇到,建议直接写成通用函数。
3.2 jieba分词与自定义词典的重要性
问答系统第一步要做的,是从用户的自然语言问题中识别出医疗实体。比如用户问“高血压患者头晕应该挂什么科”,你至少需要把“高血压”和“头晕”这两个实体准确切出来,才能去图谱里找路径。
这里jieba分词是很好用的工具,但默认词典对医疗术语支持很烂。直接跑jieba.cut('高血压患者头晕'),很可能把“高血”“压”这类无意义碎片切出来。解决方法是:在建图谱的同时,把所有实体名称导出成一行一个词的文本,通过jieba.load_userdict()加载。这个自定义词典几乎能覆盖图谱内的全部实体,分词准确率会有质的提升。
加载之后,还要注意用户问题的表达变体。比如图谱里的标准实体名是“慢性胃炎”,但用户会说“胃不舒服”“老胃病”。更稳妥的做法是维护一个“问法-实体”的映射,或者在分词后用编辑距离/向量相似度做一次模糊匹配。这套项目如果能加一个模糊匹配层,整体体验会提升一个档次,这也是我认为后续可以优化的第一优先级。
3.3 三类问答意图的区分策略
问答系统不只需要抽取实体,还需要判断用户到底想干什么。同样是提到“高血压”,用户可能是在问“高血压是什么原因引起的”,也可能是问“高血压怎么治”“高血压需要注意什么饮食”。如果只做实体抽取而不分类别,你返回的答案就一定是错位的。
在这个项目里,我把问句意图分成三类:
- 查询症状:问题中含有“表现”“症状”“有什么反应”等关键词
- 查询治疗:问题中含有“怎么治”“吃什么药”“用什么方法”等关键词
- 查询科室:问题中含有“挂什么科”“去哪个科”等关键词
实现上不需要复杂的机器学习模型,用规则关键词匹配就够了。把每个问题丢进来,先做分词,再判断是否命中各类意图的关键词规则,最后结合实体选择对应的Cypher查询模板。这套思路在小规模垂直领域问答里非常可靠,而且逻辑透明、容易加规则。
我曾经看到一个失败的案例——直接用BERT做意图识别,数据集只有几千条,效果反而不如规则匹配稳定,因为样本少、类别不均衡,模型很容易过拟合。所以我的建议是:能上规则就上规则,模型留给那些规则覆盖不了的长尾场景再说,宁可简单可靠,也不要为了炫技增加不可控性。
4. 实操过程与核心环节实现
4.1 环境准备与数据初始化
整个项目跑起来之前,最耗时的是数据准备。我先从公开的医疗健康网站获取了一部分结构化数据,大概包含三百多种常见疾病、上千个症状描述、几百种常用药品和相关科室信息。数据量对这个系统来说不大,但足够支撑一个可演示的问答闭环。
环境上需要准备的东西也很固定:
pip install neo4j py2neo pandas jieba flaskNeo4j的版本我用的是4.x,和py2neo的兼容性比较稳定。启动Neo4j之后,默认密码要记得改掉,然后通过下面这段代码建立连接并清空旧数据:
from py2neo import Graph, Node, Relationship graph = Graph("http://localhost:7474", auth=("neo4j", "your_password")) graph.delete_all() print("Neo4j初始化完成")这一步很关键,因为后续每次重新导入数据都需要一个干净的图谱环境,graph.delete_all()会把所有节点和关系一次性清掉,避免重复导入造成数据膨胀。
4.2 将清洗后的三元组导入Neo4j
数据清洗完毕并整理成三元组之后,导入部分是这个项目最核心的代码环节。先看实体导入,这段代码把所有疾病节点一次性创建出来,并使用MERGE语句来避免重复创建:
from py2neo import Graph, Node graph = Graph("http://localhost:7474", auth=("neo4j", "your_password")) def create_entity_nodes(entity_list, label): for name in entity_list: node = Node(label, name=name) graph.merge(node, label, "name")在py2neo中使用graph.merge而不是graph.create,是因为MERGE会先查找是否已有同名节点,存在就返回已有节点而不新增。这是防止多次运行脚本把相同疾病建出多个节点的重要保障。
关系导入也同样使用merge。比如导入“疾病-推荐用药”关系:
def create_relationship(start_node, end_node, rel_type, rel_name): query = f""" MATCH (a: Disease {{name: '{start_node}'}}) MATCH (b: Drug {{name: '{end_node}'}}) MERGE (a)-[r:{rel_type}]->(b) SET r.name = '{rel_name}' """ graph.run(query)这里用字符串格式直接传参虽然在这套项目里够用,但我还是建议改成参数化查询,避免特殊字符引发语法错误,同时也能防注入风险。
4.3 问答接口的完整实现
问答接口是整个系统的入口。我用Flask搭建了一个HTTP服务,用户通过GET或POST请求把问题传进来,系统返回答案。核心逻辑就三步:实体识别、意图匹配、Cypher查询。
先看实体识别部分。前面提到要加载自定义词典,这里直接拼好图谱内所有实体名再加载:
import jieba def build_custom_dict(): all_names = [] for label in ["Disease", "Symptom", "Drug", "Department", "Check", "Food"]: data = graph.run(f"MATCH (n:{label}) RETURN n.name AS name").data() all_names += [item["name"] for item in data] with open("medical_dict.txt", "w", encoding="utf-8") as f: f.write("\n".join(set(all_names))) build_custom_dict() jieba.load_userdict("medical_dict.txt")然后通过解析结果,提取出哪些词在jieb分词后仍然存在于图谱实体集合中。这里要提醒一下:如果分词结果里同时出现“感冒”和“病毒性感冒”,需要优先匹配更长的实体名,否则会把“病毒性感冒”错误地拆成“病毒性”和“感冒”两个实体。
意图匹配我用了一个简单的关键词规则函数:
def parse_intent(question): symptom_keywords = ["症状", "表现", "反应", "有什么感觉"] drug_keywords = ["药", "怎么治", "治疗", "吃什么"] department_keywords = ["科室", "挂什么科", "去哪个科", "挂号"] if any(k in question for k in symptom_keywords): return "symptom" elif any(k in question for k in drug_keywords): return "drug" elif any(k in question for k in department_keywords): return "department" return "default"这个规则顺序是有讲究的。比如“高血压有什么症状需要吃什么药”这句话既包含“症状”又包含“药”,如果你把“药”的规则放在最前面,系统就会忽略症状只返回药物。更可靠的办法是允许一个问题的多个意图共存,分别抽取后返回组合答案。在实际项目中我一般把意图识别做成一个集合,而不是单一返回。
最后一步是根据意图选择对应的Cypher查询模板。比如查询疾病对应的科室,用下面这一段:
def query_department(entity): query = f""" MATCH (d: Disease {{name: '{entity}'}})-[:DEPARTMENT]->(dep: Department) RETURN dep.name AS department """ results = graph.run(query).data() if results: return results[0]["department"] return "未找到相应科室信息,建议前往医院咨询"运行完整的后端服务后,我用Flask启动一个端口,配合一个简单的HTML页面做前端演示,整个项目就可以在本地完整跑起来了。
4.4 演示效果与响应数据
系统能跑起来后,我用几个典型的用户问题做了验证,结果如下:
| 用户问题 | 识别实体 | 识别意图 | 返回答案 |
|---|---|---|---|
| 感冒了吃什么药 | 感冒 | drug | 推荐药品:感冒灵颗粒、板蓝根颗粒 |
| 高血压有哪些症状 | 高血压 | symptom | 常见症状:头晕、头痛、心悸 |
| 胃溃疡应该挂什么科 | 胃溃疡 | department | 建议就诊科室:消化内科 |
| 糖尿病需要做什么检查 | 糖尿病 | check | 建议检查:空腹血糖、糖化血红蛋白 |
从响应时间来看,单条问题的回答几乎在100毫秒以内,其中大部分耗时在Feign接口调用和图数据库连接上,真正执行查询的时间很短。这个数据足以证明,用Neo4j做垂直领域知识图谱问答系统,在性能上完全没有瓶颈。
5. 常见问题与排查技巧实录
5.1 Neo4j连接失败与密码重置
这是新手最容易卡住的地方。启动项目后连Neo4j报错,通常是两种原因:一是Neo4j服务没有启动,先确认浏览器能不能打开http://localhost:7474;二是密码错误或者密码未修改。Neo4j 4.x首次登录默认用户名是neo4j,密码是neo4j,登录后系统强制要求改密。如果忘了新密码,可以在配置文件里设置:
# 修改Neo4j配置文件,取消认证 dbms.security.auth_enabled=false改完重启Neo4j即可免密访问,但生产环境千万别这么干,本地测试倒无所谓。另外,py2neo新版本对Neo4j 5.x的兼容性还不够好,理论上建议使用Neo4j 4.x版本运行本项目。
5.2 实体识别结果为空时的兜底策略
问答系统最尴尬的场景不是答错,而是用户提了一个图谱里不存在的实体,系统直接返回空白。我在测试时发现,如果用户在问题里用了自定义词典之外的说法,比如“老寒腿”而不是标准名称“风湿性关节炎”,分词后很可能提取不到图谱实体。
针对这个问题,我在项目中加入了“近义词推荐”兜底策略。做法是:当没有匹配到图谱实体时,把分词结果扔进一个基于编辑距离的匹配函数,去图谱里找名称最接近的实体,并返回提示。这个策略在50%以上的情况下都能“救回来”,虽然不够智能,但极大降低了用户感知到的“死路”感。
5.3 问句太长导致误匹配
用户在实际输入时经常把问题说得很长,比如“我最近胃不太舒服,吃完饭老是胀气,会不会是胃炎,要不要去医院挂什么科看一下”。这种问题分词后会得到大量无关词汇,直接做实体匹配很容易把“胃”和“医院”当成实体。
我采取的优化方案是:在意图识别之前,先做一轮实体候选筛选。把所有分词结果与图谱实体进行比较,优先保留那些在图谱中存在的名词,剔除“我”“最近”“会不会”等停用词,并将识别到的多个实体按长度排序,取最长的作为主实体。这样即使问题再长,主实体也能准确找到。如果后续出现多个实体,系统还可以分别查询并组合答案。
注意:在扩展这个系统时,不要一上来就引入昂贵的深度学习模型。先用规则引擎把80%的典型问题处理掉,再根据实际日志看哪些问题是规则覆盖不了的,再慢慢迭代优化,这才是务实的做法。
5.4 图谱数据重复导致查询结果膨胀
这个问题比较隐蔽。如果你用graph.create而不是graph.merge去导入节点,多次运行脚本后,Neo4j里会出现同一名称的多个节点。表面上看查询结果没变,但关系连接会变得混乱,而且查询性能会逐步下降。
排查方法其实很简单,在Neo4j Browser里执行:
MATCH (d: Disease) RETURN d.name, count(*) AS cnt ORDER BY cnt DESC LIMIT 10如果发现同一个名字有多个计数,就说明数据导重复了。解决方法是清理库,然后统一使用MERGE方式重新导入。
6. 这个项目后续还能怎么扩展
跑到这一步,项目已经能完整演示“问-答”闭环,但这只是起点。从工程和业务角度,这个系统还有几个非常自然的扩展方向。
第一个是加入实体链接和属性扩展。目前图谱里只有六类实体和六类关系,还可以加入“病理分型”“发病部位”“易感人群”“药品不良反应”等更细粒度的信息。医疗知识图谱的价值往往就在这些深度属性上,而不是浅层关系。
第二个是引入基础的大模型辅助意图理解。前面说规则匹配够用,是在实体规模有限的前提下。当实体量达到几千甚至上万,用户的问法千奇百怪时,规则的维护成本会迅速升高。届时可以引入LLM做意图识别和实体对齐的候选排序,但依然建议让LLM输出结构化JSON,再由下游规则引擎做最终决策,而不是让模型直接生成答案,这样可解释性和可控性会好得多。
第三个是把问答能力封装成标准API,接入到公众号、小程序或院内导诊系统。很多线下医院的导诊台、线上问诊入口,本质上就是一个垂直知识图谱问答系统。如果再加上语音输入,就能进一步降低使用门槛。
对想走工程方向的学习者来说,把该项目部署到云服务器,加上Docker容器化,再接一个简单监控,整个项目的含金量会再上一个台阶。这不仅是编程能力的练习,更是把一个思想从“数据”变成“服务”的过程。
我个人的体会是,这个看似不大的项目,其实以最少的依赖覆盖了知识图谱全链条的各个核心环节,值得多跑几遍。每跑一遍,你都会在数据处理、图建模、分词规则、查询优化这些点上发现新的改进空间。这远远不止是交作业,更是一次完整的工程思维训练。
如果让我给一个建议,我会说:先别急着改代码,把Neo4j Browser打开,看着图谱里节点之间的关系,再想一个问题——“用户问的每个问题,是怎么在这些关系之间找到答案的?”把这个问题想透了,你才算真正拥有这个项目。
本文还有配套的精品资源,点击获取