
简介基于Python的知识图谱医疗领域问答系统完整源码与数据包面向计算机、人工智能、自动化等专业的学生、教师与从业者可用于毕业设计、课程大作业或自学进阶。压缩包包含188个文件主要类型涵盖Python后端脚本、前端页面中的JavaScript与CSS样式、Neo4j图数据库存储文件并配有txt数据文件、png示意图与JSON配置等总大小115.09MB目录结构清晰。项目已有139人学习浏览源于个人毕设答辩评审98分代码调试后可直接运行。包内提供完整后端源码、前端界面、医疗数据以及可导入的图数据库文件能直接还原知识图谱运行环境。读者可跟随源码梳理数据导入、图谱查询、问句解析与答案生成的全链路理解医疗实体关系建模和问答交互实现并可在此基础上二次修改、扩展功能整体学习借鉴价值较高。1. 给医疗问答一个“能查证”的答案Python 知识图谱方案在解决什么跑一个医疗问答系统最怕的不是答不上来而是它斩钉截铁地给一个错误结论你还找不到依据。基于 Python 知识图谱的医疗领域问答系统就是把答案变成可以回溯的证据链症状、疾病、药品、科室先被建成一张图问句再被拆成实体和关系顺着图一路查下去每一条答案都对应一条三元组。这也是这套完整源码数据的方案最常见的落地起点适合想快速验证知识图谱问答路径的开发者。它对垂直领域尤其友好数据规模可控、答案可解释不需要堆大模型。2. 医疗知识图谱问答的数据准备三元组怎么洗、怎么对齐2.1 为什么选知识图谱而不是端到端深度模型拿到“医疗问答”这个需求脑子里通常会有两条路一条是微调一个生成式模型另一条就是知识图谱问答。生成式模型这几年确实火但在医疗这种容错率极低的场景里它有两个很难接受的问题答案不可控错了你找不到是哪条知识把它带偏的更新一条医学共识可能要重训一次。这就是黑匣子效应。知识图谱问答则相反答案是从图里查出来的每一条都能回溯到具体的实体和关系出错了改图谱本身就行。技术路线答案可解释性数据需求更新成本典型场景关键词检索差答非所问多低低泛问答端到端深度模型好但难排查高高开放域闲聊知识图谱问答高中等中垂直领域医疗的实体和关系是相对稳定的疾病、症状、药品、科室之间的关联数量级在几万到几十万条完全在图数据库的承载范围内。这意味着不需要分布式存储一台普通开发机就能跑起来。当然知识图谱问答也有自己的短板比如问句一变花样就查不到这会在第四章专门处理。2.2 医疗数据清洗与三元组抽取这套系统里最花时间的不是写查询而是把原始数据洗成三元组。常见的数据来源是公开的医疗百科、结构化病历资料或者手工整理的表格字段通常长这样疾病、症状、药品、科室、治愈率、注意事项。清洗时先做三件事去空、去重、统一字段名。如果原表里一个疾病占了多行得先把行合并成列表否则后面建图会出现大量重复节点。# -*- coding: utf-8 -*- import pandas as pd from collections import defaultdict def load_and_clean(path): df pd.read_csv(path, encodingutf-8) df df.dropna(subset[疾病, 症状]) df df.drop_duplicates(subset[疾病, 症状, 药品]) return df def build_triples(df): triples [] for _, row in df.iterrows(): triples.append((row[疾病], has_symptom, row[症状])) if pd.notna(row[药品]): triples.append((row[疾病], treats, row[药品])) if pd.notna(row[科室]): triples.append((row[疾病], belongs_to, row[科室])) return triples df load_and_clean(data/medical_raw.csv) triples build_triples(df) print(f生成三元组: {len(triples)} 条)这段代码有两个关键点。一是dropna只作用于疾病和症状药品、科室允许为空因为并不是每个疾病都对应药品。二是三元组的方向性很重要这里统一写成“疾病 - 关系 - 其他实体”后面写查询模板时不用反复调整方向。如果有“哪个科室治这个病”的需求关系方向反过来就麻烦了所以建三元组前先把所有查询场景写出来再定方向不要边导边拍脑袋。2.3 实体对齐与归一化同义词表为什么值得做数据洗完之后最容易踩坑的地方其实是实体对齐。原表里“头疼”“头痛”“偏头痛”会作为三个独立实体存在但用户问“感冒头疼吃什么药”时分词结果和图谱对不上答案自然为空。这就是典型的实体未归一化导致的翻车。我一般会在建图前维护一张同义词映射表把常见说法归一到标准实体名上。synonym_map { 头疼: 头痛, 脑袋疼: 头痛, 发烧: 发热, 感冒发烧: 发热, 拉肚子: 腹泻, } def normalize_entity(term): return synonym_map.get(term, term)注意这里的映射不是简单替换字符串而是把所有指向同一医学概念的词收敛到一个标准名。归一化的粒度要控制住不能把“感冒”和“流感”合并两者在医学上的处理和用药差异很大强行归一反而会让答案变得不可信。同义词表建好后下一步的实体抽取和 Neo4j 节点命名都用标准名这样图谱里不会出现一个症状七八个叫法。3. Neo4j 建图与后端接口把三元组变成能回答问题的查询链路3.1 用 py2neo 批量建图的最小步骤图谱存储我一般直接选 Neo4j社区版够用Cypher 查询写起来直观Python 侧也有 py2neo 可以用。先确认本机 Neo4j 已经启动浏览器打开 7474 端口能看到控制台再用 py2neo 连接。连接参数里端口和认证最容易出错新版本 Neo4j 默认用户名是 neo4j首次登录会强制改密码这个初始密码在创建数据库时就要记住。from py2neo import Graph, Node, Relationship, Subgraph g Graph(bolt://localhost:7687, auth(neo4j, your_password), nameneo4j) def create_graph(triples): nodes {} relations [] for head, rel, tail in triples: head_type, tail_type 疾病, 症状 if rel treats: tail_type 药品 elif rel belongs_to: tail_type 科室 hn nodes.get((head_type, head)) if hn is None: hn Node(head_type, namehead) nodes[(head_type, head)] hn tn nodes.get((tail_type, tail)) if tn is None: tn Node(tail_type, nametail) nodes[(tail_type, tail)] tn relations.append(Relationship(hn, rel, tn)) subgraph Subgraph(list(nodes.values()), relations) g.create(subgraph) create_graph(triples)这段代码的核心是先把实体去重成节点再把三元组转成关系最后用Subgraph一次性写入。写代码时注意两点第一不要每来一条三元组就g.create()一次几万条数据会慢得让人怀疑机器坏了批量提交才是正路第二节点类型必须一致同一个实体在has_symptom关系里是“疾病”到了treats里依然是“疾病”不能用字符串直接拼节点标签。3.2 医疗问答的 4 类 Cypher 查询模板图谱建好之后真正决定问答质量的是查询模板。模板需要覆盖用户最常见的问法前期不必贪多四类就够用疾病查症状、症状查疾病、疾病查药品、症状查科室。每一类对应一条 Cypher参数从问句解析结果里来。问句意图示例问法Cypher 模板疾病查症状感冒有什么症状MATCH (d:疾病 {name:$d})-[:has_symptom]-(s:症状) RETURN s.name症状查疾病头痛可能是啥病MATCH (s:症状 {name:$s})-[:has_symptom]-(d:疾病) RETURN d.name疾病查药品感冒吃什么药MATCH (d:疾病 {name:$d})-[:treats]-(m:药品) RETURN m.name症状查科室头痛挂什么科MATCH (s:症状 {name:$s})-[:has_symptom]-(d:疾病)-[:belongs_to]-(c:科室) RETURN c.name模板可以少但每一句都要调通。实际开发时我会把模板放在一个字典里键是意图名值是带命名参数的 Cypher 字符串这样新增一问只加一行字典项。注意参数一定要用$d这种方式传不要直接拼字符串否则既慢又容易出注入问题。3.3 Flask 封装问答接口图谱查询本身不复杂但系统要能对外服务得包一层 HTTP 接口。常见做法是用 Flask 起一个轻量服务接收 JSON 格式的 question内部调用问句解析和图谱查询最后返回答案和命中的三元组。这样前端无论是网页还是小程序都只对接一个接口。from flask import Flask, request, jsonify app Flask(__name__) def query_graph(intent, entities): if intent disease_symptom: cypher MATCH (d:疾病 {name:$d})-[:has_symptom]-(s:症状) RETURN s.name result g.run(cypher, dentities[disease]) return [r[s.name] for r in result] if intent symptom_disease: cypher MATCH (s:症状 {name:$s})-[:has_symptom]-(d:疾病) RETURN d.name result g.run(cypher, sentities[symptom]) return [r[d.name] for r in result] return [] app.route(/ask, methods[POST]) def ask(): q request.json.get(question, ) parsed parse_question(q) if not parsed[disease] and not parsed[symptom]: return jsonify({answer: 没识别到症状或疾病换个说法试试, triples: []}) answers query_graph(parsed[intent], parsed) return jsonify({answer: 、.join(answers) if answers else 图谱里没找到对应关系, triples: answers}) if __name__ __main__: app.run(host0.0.0.0, port8000, debugFalse)接口层不需要做复杂逻辑它只负责三件事收问题、调解析、查图谱。parse_question返回的 dict 里同时带着实体和意图query_graph根据意图选模板。这里有个细节答案列表用顿号拼接是给用户看的调试时建议把命中路径一起返回前端拿到路径可以展示来源这就是知识图谱问答的可解释性体现。4. 问句解析与意图映射把“感冒吃什么药”拆成图谱查得懂的查询4.1 基于词典 jieba 的实体抽取自然语言进来之后第一步是把实体抽出来。医疗实体大多是领域专有词通用分词的词表覆盖很差直接jieba.lcut(感冒吃什么药)大概率会把“感冒”分成“感”和“冒”。正确做法是先准备三个词典文件疾病词表、症状词表、药品词表然后用jieba.load_userdict加载。词表本身可以从图谱数据里导出保证词表和图谱的实体完全一致。import jieba import os jieba.load_userdict(data/userdict.txt) def load_terms(path): terms set() with open(path, encodingutf-8) as f: for line in f: terms.add(line.strip()) return terms DISEASES load_terms(data/diseases.txt) SYMPTOMS load_terms(data/symptoms.txt) def parse_question(q): tokens set(jieba.lcut(q)) disease tokens DISEASES symptom tokens SYMPTOMS return {disease: disease, symptom: symptom}实体抽取用的是集合交集效率高也省去了训练模型的成本。真正影响效果的是词表质量词表里缺一个词用户再怎么换说法也白搭。我一般会定期从图谱里把所有疾病、症状节点导出来重新生成词表保证两边同步否则新加的实体在问句里永远识别不到。4.2 意图模板匹配与属性映射实体识别只是第一步还要判断用户到底想问什么。意图识别在这里不需要上模型用模板匹配就够了。比如“吃什么药”“用什么药”“怎么治”都指向药品推荐意图“挂什么科”“看什么科”指向科室推荐意图“什么症状”“有哪些表现”指向症状查询意图。把常见说法映射到固定意图名再结合实体类型组合出查询条件。INTENT_PATTERNS { disease_symptom: [什么症状, 有哪些表现, 的症状], symptom_disease: [什么病, 可能是什么, 是什么引起的], disease_drug: [吃什么药, 用什么药, 怎么治疗], symptom_department: [挂什么科, 看什么科, 去哪个科室], } def match_intent(q): for intent, patterns in INTENT_PATTERNS.items(): for p in patterns: if p in q: return intent return symptom_disease这里有个组合逻辑要注意如果同时抽到疾病和症状优先用疾病实体做查询因为疾病是更确定的锚点只有意图是“症状查疾病”时才强制用症状实体当起点。匹配顺序也有讲究更长的模式要放在前面比如“吃什么药”和“药”如果同在一个列表里“吃什么药”必须先命中。模板匹配的边界很清楚覆盖不了所有问法所以必须留兜底。4.3 兜底策略解析失败时的降级回答问句解析一定会遇到识别不出来的情况尤其医疗领域用户口语千奇百怪。兜底策略我一般分两层第一层实体识别失败时把原问题原样返回给前端提示“没识别到症状或疾病换个说法试试”同时可以把候选实体列出来让用户确认第二层实体识别到了但图谱没查到时把问题记到日志里定期人工补图谱。这样做的价值是让系统知道自己的边界在哪里而不是硬凑一个答案。5. 落地排查手册环境、编码、连接与实体识别的 5 个高频坑5.1 Neo4j 连不上密码、端口与驱动版本三个叠加原因现象py2neo连接时报Unauthorized或ConnectionUnavailable浏览器里 neo4j 却登录正常。原因往往是三个叠加第一次启动时改的密码没写对py2neo新版本把password参数换成了auth元组或者协议写错用http://去连 bolt 端口。解决连接串固定用bolt://localhost:7687认证参数写auth(neo4j, 真实密码)不要沿用网上老代码里的password写法。5.2 图谱建好但查询全是空csv 编码和导入顺序的坑现象MATCH能查到节点但关系查询返回空列表。原因大概率是实体归一化没做干净库里同时存在“头痛”和“头疼”两个节点查询用的是“头痛”数据里建的是“头疼”。解决建图前先跑一遍同义词归一把实体统一成标准名再导入。另外 csv 文件如果有中文打开报编码错误时用encodingutf-8-sig读能绕开 BOM 头导致的第一个字段乱码。5.3 实体识别不出来自定义词典没生效或词表不同步现象图谱里明明有“偏头痛”用户问“偏头痛吃什么药”解析结果却是空。原因一般是jieba.load_userdict放在第一次分词之后调用了只有内存里已经有缓存的老词才生效另一个常见原因是词表是旧文件图谱后来加的实体没同步进去。解决项目启动时先加载词典再分词词表生成脚本直接查 Neo4j 导出实体列表。5.4 服务一启动就卡死Neo4j 内存参数和 Python 查询并发现象本地跑得好好的把服务部署到小内存机器上一启动就卡。解决调 Neo4j 的内存配置dbms.memory.heap.initial_size和dbms.memory.pagecache.size都设成 512m 到 1g 级别不要默认值硬扛。Python 侧注意用完连接要关Graph实例全局只建一个别每次请求都新建连接。5.5 Python 版本和依赖安装顺序现象按网上教程装 py2neo 后 import 报错或者 py2neo 和 neo4j 驱动冲突。解决先装 Python 再装 Neo4j最后装依赖。Python 版本选 3.8 左右稳定性最高py2neo 版本和 Neo4j 版本要匹配装完依赖后先跑一个最小的连接脚本验证再全量导入。6. 让答案从“能跑”到“答得准”评估方式与进阶小技巧建完图、跑通接口只是起点。我自己的习惯是准备一份约 100 条的测试集每条问题配一个预期答案然后用一个简单的评估脚本统计实体级准确率和答案级准确率。实体级只看是否识别对了关键实体答案级要求返回结果中必须包含预期实体。这种做法能帮你量化每次改动是变好了还是变差了而不是靠感觉调参。def evaluate(test_set): total len(test_set) entity_right 0 answer_right 0 for question, expected in test_set: parsed parse_question(question) entity_hit bool(parsed[disease] or parsed[symptom]) if entity_hit: entity_right 1 answers query_graph(parsed[intent], parsed) if any(e in answers for e in expected): answer_right 1 print(f实体准确率: {entity_right / total:.2%}) print(f答案准确率: {answer_right / total:.2%})如果实体准确率低优先补词表和同义词映射如果实体准但答案准率低问题出在意图模板或图谱关系上。进阶方向有两个一个是给查询加属性约束比如“儿童感冒吃什么药”需要给药品节点加适用人群属性这比换模型更实用另一个是从模板匹配升级成小模型做意图分类但前提是先把图谱和模板调到 90% 以上再考虑模型。我最早做这类系统时把精力全花在堆页面上后来发现图谱里每增加一种关系问答效果都比多写一百个界面更明显。希望帮到你。本文还有配套的精品资源点击获取
拿不准这条消息跟你有没有关系?
工种不同、批次不同,要求可能差很多。打电话把你的情况说清楚,我们按信阳、平顶山本地的口径给你捋一遍。