Python知识图谱医疗问答系统源码实战:Neo4j与Cypher查询 简介这份资源是面向计算机、人工智能、通信等专业学生与开发者的知识图谱医疗领域问答系统完整实现包含可直接运行的源码与配套数据适合作为毕业设计、期末大作业或课程设计参考也便于初学者进阶学习。压缩包共188个文件约115.09MB以41个Python源码文件为核心辅以44个txt数据说明、21个html前端页面、10个js脚本、8个db数据库文件及22张png图表另有css、json、names、labels等配置与模型文件整体结构完整覆盖从数据存储到问答交互的主要环节。目前已有139人学习下载项目经过调试测试答辩评审分达到98分具备较高参考价值。读者可据此理解知识图谱构建、医疗实体关系存储与问答逻辑实现并在此基础上修改调整扩展出不同功能快速完成自己的项目开发与学习任务。1. 从一份能跑起来的 Python 医疗知识图谱问答源码说起医疗问答系统这几年在院内导诊、慢病随访、药品咨询这些场景里被反复提起但真正落到代码层面很多人卡在第一步数据从哪来、图谱怎么建、问句怎么变成图数据库能听懂的查询。标题里这套「基于 Python 知识图谱医疗领域问答系统」的完整源码加数据解决的正是这条链路——它把医疗实体、疾病症状、药品禁忌这些结构化知识塞进图数据库再用 Python 把自然语言问句翻译成图查询最后把答案拼回人话。适合谁适合已经会写 Python 基础语法、想找一个能直接跑通的知识图谱问答项目练手的开发者也适合做课程设计或院内小工具原型的同学。它不追求大模型那种泛化能力胜在链路完整、可控、可解释你改一条规则就能看到答案变化这对理解知识图谱问答系统的骨架比调 API 有用得多。2. 医疗知识图谱问答系统的技术选型与数据建模2.1 为什么用 Neo4j 而不是关系型数据库存医疗知识医疗领域的知识天然是网状结构。一个「2 型糖尿病」节点往上连着「内分泌代谢疾病」这个分类往旁连着「多饮多尿」这些症状往下连着「二甲双胍」这类治疗药物药物又连着「肾功能不全者慎用」这样的禁忌。如果用 MySQL 存你得建疾病表、症状表、药物表、关系表一次「糖尿病有哪些症状和常用药」的查询要 join 三四张表写起来痛苦读起来更痛苦。图数据库把关系当成一等公民。在 Neo4j 里上面那个查询就是一句 Cypher从疾病节点出发沿着「HAS_SYMPTOM」和「TREATED_BY」两条边各走一跳把邻居捞出来。常见做法是节点标签用Disease、Symptom、Drug、Check、Department关系类型用大写下划线比如HAS_SYMPTOM、TREATED_BY、NEED_CHECK、BELONG_TO。这种建模方式的好处是扩展新关系时不用改表结构加一种边类型就行。选型上还有一点Neo4j 自带 Cypher 查询语言和可视化界面调试图谱时能直接看到节点和边比对着数据库表想象关系直观太多。对于医疗这种实体类型多、关系复杂的领域可视化调试省下的时间远超学习 Cypher 的成本。2.2 医疗实体和关系的本体设计本体建模决定了图谱能回答哪些问题。医疗领域常见的实体类型和关系可以这样规划实体类型标签示例疾病Disease2型糖尿病、高血压症状Symptom多饮、多尿、头晕药物Drug二甲双胍、阿卡波糖检查项目Check空腹血糖、糖化血红蛋白科室Department内分泌科食物Food苦瓜、燕麦并发症Complication糖尿病肾病关系类型对应关系方向含义HAS_SYMPTOMDisease → Symptom疾病表现出的症状TREATED_BYDisease → Drug疾病常用药物NEED_CHECKDisease → Check确诊需要的检查BELONG_TODisease → Department疾病归属科室RECOMMEND_EATDisease → Food推荐食物NO_EATDisease → Food忌口食物HAS_COMPLICATIONDisease → Complication并发症ACCOMPANY_WITHDisease → Disease并发疾病这套本体不是拍脑袋定的它对应了医疗问答里最高频的几类问句「XX 有什么症状」「XX 吃什么药」「XX 要做什么检查」「XX 挂什么科」「XX 不能吃什么」。每加一种关系系统就能多回答一类问题。我一般建议先把这七八种关系跑通再根据实际问句日志去补。2.3 用 Python 把医疗数据灌进 Neo4j数据准备阶段通常拿到的是 CSV 或 JSON 格式的医疗三元组。下面这段代码演示如何用py2neo把疾病-症状-药物数据批量写入 Neo4jfrom py2neo import Graph, Node, Relationship import csv # 连接 Neo4j默认 bolt 协议端口 7687 graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) def load_disease_data(csv_path): 从 CSV 读取疾病数据并写入图谱 with open(csv_path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: # 用 merge 避免重复创建同名节点 disease Node(Disease, namerow[disease]) graph.merge(disease, Disease, name) # 症状节点和关系 for sym in row[symptoms].split(|): if sym.strip(): symptom Node(Symptom, namesym.strip()) graph.merge(symptom, Symptom, name) rel Relationship(disease, HAS_SYMPTOM, symptom) graph.merge(rel) # 药物节点和关系 for drug in row[drugs].split(|): if drug.strip(): drug_node Node(Drug, namedrug.strip()) graph.merge(drug_node, Drug, name) rel Relationship(disease, TREATED_BY, drug_node) graph.merge(rel) if __name__ __main__: load_disease_data(data/disease.csv) print(导入完成)这段代码的关键在merge而不是create。create每次调用都会新建节点同一疾病出现两次就会产生两个同名节点图谱直接脏掉。merge会先按标签和属性查存在就复用不存在才建。参数上Node(Disease, name...)里的name是主键属性graph.merge(node, Disease, name)明确告诉 Neo4j 用name做去重依据。CSV 的格式建议长这样disease,symptoms,drugs 2型糖尿病,多饮|多尿|体重下降,二甲双胍|阿卡波糖 高血压,头晕|头痛|心悸,氨氯地平|缬沙坦导入完成后在 Neo4j Browser 里跑一句MATCH (n) RETURN count(n)确认节点数再跑MATCH (d:Disease)-[r]-(s) RETURN d,r,s LIMIT 25看看关系有没有连对。这一步别省图谱建错了后面问答全歪。3. 问句解析与 Cypher 查询生成3.1 医疗问句的分类与意图识别用户问「糖尿病有什么症状」和「糖尿病吃什么药」句式几乎一样但要走不同的关系边。所以问句解析的第一步是意图分类。医疗问答里高频意图就那么几类问症状、问药物、问检查、问科室、问忌口、问并发症。常见做法是用关键词匹配加规则兜底简单可靠不需要训练模型。INTENT_RULES { symptom: [症状, 表现, 有什么反应], drug: [吃什么药, 用什么药, 药物, 治疗], check: [检查, 怎么确诊, 做什么化验], department: [挂什么科, 哪个科, 科室], food_no: [不能吃, 忌口, 禁忌食物], complication: [并发症, 会引起什么] } def detect_intent(question): 基于关键词匹配识别问句意图 for intent, keywords in INTENT_RULES.items(): for kw in keywords: if kw in question: return intent return unknown参数说明INTENT_RULES的键是意图标签值是该意图的触发词列表。匹配顺序会影响结果比如「糖尿病并发症吃什么药」同时命中complication和drug谁先匹配返回谁。我一般把更具体的意图放前面complication比drug具体就排在前面。这套规则的上限很明显问句换个说法就可能漏但对课程设计和原型够用而且每条规则都能解释出问题好排查。3.2 从问句中抽取疾病实体识别了意图还得知道用户问的是哪个病。医疗实体抽取的难点在于疾病名称长短不一「糖尿病」和「2型糖尿病」是两个不同节点用户可能只说了「糖尿病」。常见做法是拿图谱里已有的疾病名称做词典对问句做最大正向匹配。def extract_disease(question, disease_list): 从问句中匹配图谱中已有的疾病名称优先匹配长词 # 按长度降序保证「2型糖尿病」先于「糖尿病」被匹配 sorted_diseases sorted(disease_list, keylen, reverseTrue) for disease in sorted_diseases: if disease in question: return disease return None # 从 Neo4j 拉取所有疾病名称作为词典 disease_list [r[name] for r in graph.run( MATCH (d:Disease) RETURN d.name AS name )]这里排序是关键。如果不按长度降序「2型糖尿病」的问句会先匹配到「糖尿病」查出来的结果就少了 2 型特有的信息。这个坑我在实际项目里踩过症状列表看着对但总觉得缺东西查了半天才发现是实体匹配粒度的问题。3.3 意图到 Cypher 的映射模板意图和实体都有了接下来拼 Cypher。每种意图对应一个查询模板CYPHER_TEMPLATES { symptom: MATCH (d:Disease {{name: $disease}})-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name AS answer , drug: MATCH (d:Disease {{name: $disease}})-[:TREATED_BY]-(dr:Drug) RETURN dr.name AS answer , check: MATCH (d:Disease {{name: $disease}})-[:NEED_CHECK]-(c:Check) RETURN c.name AS answer , department: MATCH (d:Disease {{name: $disease}})-[:BELONG_TO]-(dep:Department) RETURN dep.name AS answer , food_no: MATCH (d:Disease {{name: $disease}})-[:NO_EAT]-(f:Food) RETURN f.name AS answer , complication: MATCH (d:Disease {{name: $disease}})-[:HAS_COMPLICATION]-(c:Complication) RETURN c.name AS answer } def query_graph(intent, disease): 根据意图和疾病名执行图查询 template CYPHER_TEMPLATES.get(intent) if not template: return [] results graph.run(template, diseasedisease).data() return [r[answer] for r in results]注意模板里用了$disease参数化查询而不是字符串拼接。字符串拼接会有注入风险虽然医疗问答场景下用户不太可能恶意注入但参数化是习惯也避免疾病名里带特殊字符时查询报错。graph.run(template, diseasedisease)的第二个参数就是绑定变量Neo4j 会安全处理。3.4 答案组装与多轮追问拿到查询结果后要拼成自然语言。最简单的做法是按意图套话术模板ANSWER_TEMPLATES { symptom: {disease}的常见症状包括{items}。, drug: {disease}的常用治疗药物有{items}。, check: 确诊{disease}通常需要做以下检查{items}。, department: {disease}一般挂{items}就诊。, food_no: {disease}患者应避免食用{items}。, complication: {disease}可能引发的并发症有{items}。 } def build_answer(intent, disease, items): if not items: return f抱歉暂时没有查到{disease}的相关信息。 template ANSWER_TEMPLATES.get(intent, {items}) return template.format(diseasedisease, items、.join(items))多轮追问是加分项。用户先问「糖尿病有什么症状」接着问「那吃什么药」第二句里没有疾病名需要把上一轮的疾病实体继承下来。实现方式是在会话状态里存一个last_disease当本轮没抽到疾病实体时用它兜底。这个逻辑不复杂但能让交互体验上一个台阶。4. 避坑与排查医疗知识图谱问答落地时的五个真实问题4.1 图谱导入后查询为空节点标签大小写不一致现象数据导入脚本跑完没报错但问答时所有查询都返回空列表。原因Neo4j 的标签和关系类型区分大小写。导入时写的是Node(Disease)查询模板里写成了MATCH (d:disease)标签对不上自然查不到。解决统一标签命名规范实体标签首字母大写关系类型全大写下划线。导入和查询用同一套常量别在两处手写字符串。可以在代码里定义LABEL_DISEASE Disease这样的常量两边引用同一个变量。4.2 疾病实体匹配到短词答案缺项现象问「2型糖尿病吃什么药」返回的药物列表和「糖尿病」一样缺少 2 型特有的药物。原因实体抽取时没有按长度降序匹配「糖尿病」先于「2型糖尿病」命中。解决抽取前对疾病词典按字符串长度降序排序保证长实体优先匹配。如果图谱里疾病名有别名还要维护一张别名映射表把「二型糖尿病」「II型糖尿病」都映射到标准名。4.3 意图识别被关键词误触发现象用户问「糖尿病会引起高血压吗」系统识别成drug意图返回了一堆药物。原因「治疗」这个词出现在drug的关键词列表里而问句里可能带了「治疗」二字或者关键词匹配顺序把drug排在了complication前面。解决调整关键词列表把容易误触发的宽泛词去掉换成更具体的短语。同时把意图匹配顺序按具体程度排列complication、food_no这类具体意图排在drug、symptom前面。更稳的做法是给每个意图算命中关键词数量取命中最多的意图而不是第一个命中就返回。4.4 Neo4j 连接超时或认证失败现象Python 脚本报py2neo.errors.ConnectionUnavailable或AuthError。原因Neo4j 服务没启动、bolt 端口被防火墙拦了、密码不对或者用了neo4j默认密码没改导致首次登录强制改密。解决先在浏览器访问http://localhost:7474确认服务活着再用cypher-shell或 Neo4j Browser 验证账号密码。Python 连接串确认是bolt://不是http://端口 7687 不是 7474。密码如果改过auth参数要同步更新别硬编码在代码里用环境变量或配置文件读。4.5 答案拼接时列表为空导致话术尴尬现象查询返回空列表系统回复「糖尿病的常见症状包括。」冒号后面空的。原因build_answer没处理空结果直接join空列表得到空字符串。解决在拼接前判断items是否为空为空时返回兜底话术比如「抱歉暂时没有查到该疾病的相关信息建议咨询专业医生」。医疗场景下兜底话术要谨慎不能给出可能被当成医疗建议的回复明确说「建议咨询医生」比编一个答案安全。5. 让问答更准的两个进阶技巧同义词扩展与查询结果排序规则匹配的问答系统最大的短板是用户换个说法就歇菜。用户说「消渴」你图谱里只有「糖尿病」匹配直接失败。解决办法是维护一张医疗同义词表在实体抽取前先把问句里的同义词替换成标准名。SYNONYM_MAP { 消渴: 糖尿病, 二型糖尿病: 2型糖尿病, II型糖尿病: 2型糖尿病, 血压高: 高血压, 血糖高: 高血糖 } def normalize_question(question): 把问句中的同义词替换为标准疾病名 for alias, standard in SYNONYM_MAP.items(): if alias in question: question question.replace(alias, standard) return question这张表不用一次建全从实际问句日志里攒。每次遇到匹配失败的问句人工判断一下是不是同义词问题是就加一条。跑一段时间覆盖率就上来了。参数上注意替换顺序如果「糖尿病」本身也是某个别名的子串要先替换长别名再替换短的否则会误替换。第二个技巧是查询结果排序。同一个疾病可能对应多个药物用户往往想知道哪个最常用。可以在关系上加一个weight属性导入数据时按临床指南或药品说明书里的推荐等级赋值查询时按权重降序返回。# 带权重的查询模板 CYPHER_DRUG_WEIGHTED MATCH (d:Disease {name: $disease})-[r:TREATED_BY]-(dr:Drug) RETURN dr.name AS answer, r.weight AS weight ORDER BY r.weight DESC 导入时给关系加权重rel Relationship(disease, TREATED_BY, drug_node, weight3) graph.merge(rel)权重怎么定一线用药给 3二线给 2三线或辅助给 1。没有明确分级的数据可以按药品说明书中该适应症的推荐强度粗略赋值。这样用户问「糖尿病吃什么药」返回的第一个就是临床最常用的一线药物而不是随机顺序。这两个技巧叠加后问答准确率会有肉眼可见的提升。同义词扩展解决「问法不同」的问题权重排序解决「答案顺序」的问题都是规则系统里投入产出比很高的改动。我自己做这类项目时先把主链路跑通再花半天时间补同义词表和权重效果比一开始就上复杂模型实在得多。希望帮到你。本文还有配套的精品资源点击获取