2026/10/11 0:43:44

Python基于知识图谱的医疗问答系统毕设源码与实现

Python基于知识图谱的医疗问答系统毕设源码与实现 简介本资源为面向高校计算机相关专业毕业设计的Python医疗问答系统完整项目包基于知识图谱技术构建适合正在准备毕设或希望学习知识图谱与Django全栈开发的学生及开发者。项目采用B/S访问结构后端基于Django框架数据存储使用MySQL并集成Neo4j图数据库支撑知识图谱的存储与查询功能覆盖管理员登录、后台首页、医疗问答页面、问答管理、修改密码与个人信息维护等模块配套说明文档详细梳理了需求分析、可行性分析、数据库E-R图与系统流程设计。压缩包共1208个文件约183.55MB包含py源码、html模板、css与js前端资源、png与gif图像素材、jar依赖包、db数据库文件及sql脚本等目录结构完整便于按模块查阅与二次开发。目前已有1260人学习下载可作为毕业设计选题参考、知识图谱问答系统入门实践与项目排错复盘的实用素材。1. 从一份毕设源码说起医疗问答系统为什么值得用知识图谱重做一遍如果你正在做毕业设计或者带学生做毕设大概率见过这样的场景一个基于规则匹配的医疗问答 demo用户问“感冒了吃什么药”系统靠关键词命中返回一段硬编码文本换一种问法就答非所问。这类系统最大的问题不是界面丑而是它没有“知识”——它不知道感冒和上呼吸道感染是同一类问题不知道阿莫西林属于抗生素更不知道孕妇禁用某些药物。Python 基于知识图谱的医疗问答系统本质上就是把这层“知识”从代码里抽出来放进图数据库让问答变成一次图上的推理查询。它适合三类人想拿一个完整毕设的本科生、想练手知识图谱构建的 Python 开发者、以及需要快速搭一个垂直领域问答原型的工程师。源码、数据库、说明文档三件套的意义在于你不用从零设计 schema而是直接在一个可运行的骨架上改。2. 知识图谱怎么建从医疗文本到 Neo4j 图数据库2.1 为什么医疗问答非要用图结构而不是 MySQL 几张表关系型数据库做医疗问答不是不行但会很别扭。医疗知识的核心是“实体—关系—实体”的三元组疾病、症状、药物、检查、科室、食物之间互相牵连。用 MySQL 存你得建疾病表、症状表、关系表一次多跳查询比如“得这个病的人通常做什么检查这些检查在哪个科室开”要写多层 JOINSQL 又长又慢。图数据库把关系当一等公民多跳查询就是沿着边遍历写起来直观跑起来也快。常见做法是 Neo4j它自带 Cypher 查询语言和 Python 的 py2neo 或官方 neo4j 驱动配合很顺。选型上如果你只是毕设规模几万节点、十几万关系Neo4j 社区版完全够用不用上集群。另一个选择是 NebulaGraph但部署成本高毕设没必要。我一般会建议先用 Neo4j 把流程跑通schema 设计对了后面换库只是驱动层的事。医疗知识图谱的 schema 通常包含这几类节点和关系节点类型示例属性关系类型关系方向示例Disease 疾病name, desc, causehas_symptomDisease → SymptomSymptom 症状namebelong_toSymptom → DiseaseDrug 药物name, usage, tabootreatDrug → DiseaseCheck 检查name, costneed_checkDisease → CheckDepartment 科室namebelong_toDisease → DepartmentFood 食物name, do_eatrecommend_eatDisease → Food这张表不是让你照抄而是让你理解schema 决定了后面问答能回答哪些问题。如果你只建疾病和症状那用户问“吃什么药”就答不了。所以建图之前先想清楚问答的意图分类。2.2 用 Python 把结构化医疗数据灌进 Neo4j 的最小脚本假设你手里有一份 CSV 格式的医疗三元组数据很多公开医疗数据集或自己爬取整理后都能转成这个格式字段是entity1, relation, entity2。下面这个脚本做三件事连 Neo4j、批量建节点、批量建关系。from py2neo import Graph, Node, Relationship import csv # 连接 Neo4j默认 bolt 端口 7687改成你自己的密码 graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) # 用 MERGE 而不是 CREATE避免重复节点 def create_node(label, name): node Node(label, namename) graph.merge(node, label, name) # 按 label name 唯一约束合并 return node def load_triples(csv_path): with open(csv_path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: e1, rel, e2 row[entity1], row[relation], row[entity2] # 这里简化处理根据关系类型推断节点标签 label1 Disease if rel in (has_symptom, need_check, recommend_eat) else Entity label2 Symptom if rel has_symptom else Entity n1 create_node(label1, e1) n2 create_node(label2, e2) # 关系用 MERGE 避免重复边 rel_obj Relationship(n1, rel.upper(), n2) graph.merge(rel_obj) if __name__ __main__: load_triples(medical_triples.csv) print(导入完成)逻辑说明graph.merge是幂等操作跑第二遍不会产生重复节点这对毕设反复调试很重要。参数上auth里的密码是你安装 Neo4j 时设的默认用户是 neo4j。label1和label2的推断逻辑是简化版实际项目中你应该在 CSV 里加一列label1, label2或者写一个映射字典否则所有节点都叫 Entity查询时没法按类型过滤。提示导入前先在 Neo4j Browser 里执行CREATE CONSTRAINT FOR (d:Disease) REQUIRE d.name IS UNIQUE建唯一约束能大幅加快 merge 速度也防止脏数据。2.3 从非结构化文本抽三元组规则 词典的轻量方案毕设数据往往不是现成三元组而是疾病百科页面那种大段文本。这时候你需要做实体识别和关系抽取。别一上来就上 BERT毕设时间有限规则 词典的方案更可控。核心思路先维护一个疾病词典、症状词典、药物词典然后用句子模板匹配。import re disease_dict {感冒, 高血压, 糖尿病} symptom_dict {头痛, 发热, 咳嗽, 乏力} drug_dict {阿莫西林, 布洛芬, 二甲双胍} # 模板疾病 的症状包括 症状列表 pattern re.compile(r(?Pdisease[\u4e00-\u9fa5]?)的症状(?:包括|有|主要有)(?Psymptoms[\u4e00-\u9fa5、])) def extract(text): triples [] for m in pattern.finditer(text): disease m.group(disease) if disease not in disease_dict: continue symptoms re.split(r[、], m.group(symptoms)) for s in symptoms: if s in symptom_dict: triples.append((disease, has_symptom, s)) return triples text 感冒的症状包括头痛、发热、咳嗽高血压的症状主要有头痛、乏力。 print(extract(text))这段代码的输出是[(感冒, has_symptom, 头痛), (感冒, has_symptom, 发热), ...]。参数说明词典决定了召回率模板决定了准确率。实际使用时模板要写十几条覆盖“治疗”“常用药”“宜吃”“忌吃”“检查项目”等关系。如果文本里疾病名不在词典里会被过滤掉这是故意的——宁可少抽不要抽错因为错误三元组会污染整个图谱后面问答就会给出荒谬答案。3. 问答引擎怎么做从用户问句到 Cypher 查询3.1 意图识别与实体链接把“我头疼发烧怎么办”翻译成图查询用户输入是自然语言系统要做的第一步是判断他在问什么类型的问题第二步是找出问句里的医疗实体。意图分类不用上深度学习用关键词 模板就能覆盖毕设 80% 的场景。常见意图有症状查疾病、疾病查症状、疾病查药物、疾病查检查、疾病查科室、疾病查饮食。intent_patterns { disease_by_symptom: [什么病, 可能是什么, 得了什么], symptom_by_disease: [症状, 表现], drug_by_disease: [吃什么药, 用什么药, 药物], check_by_disease: [做什么检查, 检查项目], department_by_disease: [挂什么科, 哪个科室], food_by_disease: [吃什么食物, 饮食, 忌口], } def detect_intent(question): for intent, keywords in intent_patterns.items(): for kw in keywords: if kw in question: return intent return unknown def link_entity(question, entity_dict): # 简单的最长匹配实际可用 jieba 词典 for entity in sorted(entity_dict, keylen, reverseTrue): if entity in question: return entity return Nonedetect_intent返回意图标签link_entity返回问句里命中的疾病或症状名。注意实体链接的顺序先匹配长词避免“高血压”被“血压”截断。如果问句里同时出现疾病和症状优先取疾病作为查询锚点因为图谱里疾病是中心节点。3.2 意图到 Cypher 的映射表与参数化查询意图识别完接下来是拼 Cypher。不要用字符串拼接用参数化查询防止注入也方便调试。cypher_map { disease_by_symptom: MATCH (s:Symptom)-[:HAS_SYMPTOM]-(d:Disease) WHERE s.name $entity RETURN d.name AS disease LIMIT 10 , drug_by_disease: MATCH (d:Disease)-[:TREAT]-(drug:Drug) WHERE d.name $entity RETURN drug.name AS drug, drug.usage AS usage LIMIT 10 , check_by_disease: MATCH (d:Disease)-[:NEED_CHECK]-(c:Check) WHERE d.name $entity RETURN c.name AS check_item LIMIT 10 , } def answer(question): intent detect_intent(question) entity link_entity(question, all_entities) if intent unknown or entity is None: return 抱歉我暂时无法理解这个问题。 cypher cypher_map.get(intent) if not cypher: return 该问题类型暂未支持。 result graph.run(cypher, entityentity).data() return result参数说明$entity是 Neo4j 驱动支持的参数占位符graph.run(cypher, entityentity)会把 entity 安全地传进去。LIMIT 10是防止返回过多结果毕设演示时前端展示不了太多。如果返回空列表说明图谱里没有对应关系这时候可以给一个兜底话术比如“知识库中暂未收录该疾病的药物信息”。3.3 多跳推理当用户问“高血压患者头疼可能是什么病”单跳查询只能回答直接关系但医疗问答里经常需要多跳。比如用户说“我有高血压最近头疼可能是什么问题”这需要先找到高血压的常见并发症再和头疼这个症状做交集。Cypher 的多跳写起来很自然multi_hop_cypher MATCH (d1:Disease {name: $disease})-[:HAS_COMPLICATION]-(d2:Disease) MATCH (d2)-[:HAS_SYMPTOM]-(s:Symptom {name: $symptom}) RETURN d2.name AS possible_disease LIMIT 5 这个查询先沿HAS_COMPLICATION找到高血压的并发症再检查这些并发症是否有“头疼”症状。参数$disease和$symptom分别来自实体链接的结果。实际项目中HAS_COMPLICATION这个关系需要你在建图时从医学文本里抽取如果数据源没有可以退而求其次用“同科室 共享症状”做相似度推荐但那就是另一个话题了。注意多跳查询容易返回空结果因为图谱关系稀疏。建议在问答层加一个降级策略多跳无结果时回退到单跳只回答已知信息并提示“根据现有知识高血压本身也可能引起头疼建议就医确认”。4. 避坑与排查医疗问答系统最容易翻车的 5 个地方4.1 现象导入数据后查询返回空但 Neo4j Browser 里明明有节点原因Python 驱动连接的数据库和 Browser 里看的不是同一个。Neo4j 4.x 之后默认数据库是neo4j但如果你创建了多数据库驱动可能连到了system库。另一个常见原因是标签大小写不一致建图时用了Disease查询时写了diseaseCypher 标签是大小写敏感的。解决在连接时显式指定数据库Graph(bolt://localhost:7687, auth(neo4j, pwd), nameneo4j)并在查询前用MATCH (n) RETURN labels(n) LIMIT 5确认标签名。4.2 现象问答结果重复同一个疾病返回好几遍原因建图时用了CREATE而不是MERGE导致同一个实体被多次创建或者关系被重复插入。另一个可能是 Cypher 查询没有去重多跳路径产生了笛卡尔积。解决建图阶段统一用MERGE并在关键属性上建唯一约束。查询阶段在RETURN后加DISTINCT例如RETURN DISTINCT d.name AS disease。4.3 现象用户问“感冒吃什么药”系统返回了“阿莫西林”但感冒通常是病毒性的原因图谱里的TREAT关系没有区分“适用”和“慎用”也没有记录药物类型。很多医疗数据源里疾病和药物的关系是粗粒度的直接拿来用会给出不严谨的建议。解决在关系上加属性比如TREAT {type: 常用药}或{type: 慎用}查询时过滤WHERE r.type 常用药。如果数据源没有这个信息至少在问答结果里加一句“以上药物仅供参考请遵医嘱”这是医疗问答的底线。4.4 现象实体链接把“高血压”识别成了“血压”导致查错疾病原因词典匹配时没有做最长优先或者分词工具把“高血压”切成了“高/血压”。用 jieba 时默认词典可能不包含“高血压”这个医学词。解决加载自定义词典jieba.load_userdict(medical_dict.txt)并在匹配时按实体长度降序排列。更稳妥的做法是同时用 AC 自动机做多模式匹配保证长词优先命中。4.5 现象前端输入一句话后端卡住好几秒才返回原因Cypher 查询没有走索引Neo4j 在全图扫描。尤其是WHERE s.name $entity这种如果name上没有索引节点多了就会慢。解决给所有参与查询的属性建索引CREATE INDEX FOR (d:Disease) ON (d.name)症状、药物同理。另外限制LIMIT避免一次返回上千条。如果还慢检查是不是多跳查询没有限制深度Cypher 里用[:HAS_COMPLICATION*1..2]控制跳数。5. 进阶技巧用 Flask 把问答系统包成 API并做一轮人工评测5.1 用 Flask 暴露 /ask 接口让前端或 Postman 能调毕设答辩时老师不会只看代码他要看到能跑起来的东西。用 Flask 包一层 API 是最快的方式。from flask import Flask, request, jsonify from your_qa_module import answer # 假设问答逻辑在 answer 函数里 app Flask(__name__) app.route(/ask, methods[POST]) def ask(): data request.get_json() question data.get(question, ).strip() if not question: return jsonify({code: 400, msg: 问题不能为空}) try: result answer(question) return jsonify({code: 200, question: question, answer: result}) except Exception as e: return jsonify({code: 500, msg: str(e)}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)逻辑说明request.get_json()接收 JSON 格式的 POST 请求返回也是 JSON。debugTrue方便开发时看报错但部署时要关掉。参数上端口 5000 是 Flask 默认如果被占用改成 5001。这个接口可以直接被 Vue 或 React 前端调用也可以用 curl 测试curl -X POST http://127.0.0.1:5000/ask \ -H Content-Type: application/json \ -d {question: 感冒的症状有哪些}5.2 人工评测准备 50 条问题按意图分类统计准确率系统能不能用不能靠感觉。我一般会准备一个测试集覆盖六种意图每种 8 到 10 条然后人工判断返回结果是否正确。下面是一个简单的评测脚本框架test_cases [ {q: 感冒的症状有哪些, intent: symptom_by_disease, expect: 头痛}, {q: 高血压吃什么药, intent: drug_by_disease, expect: 降压药}, # ... 更多用例 ] correct 0 for case in test_cases: result answer(case[q]) # 简化判断期望关键词出现在返回结果里 if case[expect] in str(result): correct 1 else: print(f错误{case[q]} - {result}) print(f准确率{correct / len(test_cases) * 100:.1f}%)这个脚本很粗糙但能快速暴露问题。如果某一类意图准确率特别低就去检查对应的 Cypher 和实体链接逻辑。比如“疾病查症状”准确率低往往是症状词典覆盖不够或者图谱里HAS_SYMPTOM关系方向反了。5.3 一个我踩过的坑别在答辩前一天改 schema最后说一个血泪经验。我见过有人答辩前一天觉得节点标签不够优雅把Disease改成Illness结果所有 Cypher 查询全挂连夜改代码。知识图谱的 schema 是地基定下来之后改标签、改关系名、改方向都会引发连锁反应。如果非要改写一个迁移脚本把所有旧标签的节点批量重命名然后跑一遍全量测试用例。更稳妥的做法是在项目初期就把 schema 写进说明文档后面只加不改。希望帮到你。本文还有配套的精品资源点击获取