首页 / 资讯中心 / 文章详情

医疗知识图谱问答系统实战:从Neo4j构建到临床级SPARQL推理

医疗知识图谱问答系统实战:从Neo4j构建到临床级SPARQL推理 ★ FEATURED ARTICLE
简介这是一套基于Python实现的医疗知识图谱自动问答系统完整源码面向人工智能、自然语言处理与医疗信息化方向的学习者和开发者旨在解决公众对结构化医学知识的即时查询需求适用于课程设计、毕设开发及垂直领域知识图谱实践。资源共26个文件涵盖8个核心Python脚本如question_classifier.py、build_medicalgraph.py、chatbot_graph.py等、5个XML配置与数据定义文件、8个TXT格式的医疗词典含疾病、症状、药物、科室等实体、2个JSON知识库文件medical.json、medical2.json以及1张演示界面截图demo.jpg整体压缩包大小为15.54MB结构清晰模块职责明确。已有184人学习下载。读者可直接运行Flask Web服务获得从数据预处理、Neo4j图谱构建、中文问句解析、Cypher动态查询到答案生成的全链路实现同时掌握医疗实体识别、关系抽取、图数据库交互及前端结果渲染等关键技术环节。1. 医疗知识图谱自动问答系统为什么医生和工程师都在重写“问病”逻辑这不是一个能靠调用几个 API 就跑通的玩具项目。某三甲医院信息科在部署初期发现把「高血压患者能否服用布洛芬」这类问题丢给通用大模型返回结果里混着过时指南、未标注适用人群的药理描述甚至出现「建议咨询医生」这种无效兜底——而知识图谱驱动的问答系统在本地加载完《中国高血压防治指南2023修订版》结构化节点后3秒内给出带证据链的答案「不推荐因NSAIDs可减弱ACEI/ARB类降压效果依据指南第4.2.5条药品说明书禁忌项交叉验证」。这个 ZIP 包的核心价值不是「有图谱」而是把临床术语标准化如「心梗」→「急性心肌梗死 I21.9」、把非结构化指南转成可推理的RDF三元组、再让自然语言问句精准锚定到子图路径上。它适合两类人一是医疗信息化团队想落地轻量级临床辅助模块不依赖云端大模型二是NLP工程师需要一个真实领域非Wiki或电影数据来验证实体链接、关系抽取、SPARQL生成等全链路技术点。你不需要从零建本体但必须亲手调参——因为医学概念的歧义性远超想象「阴虚」在中医图谱里是证候节点在西医语境下却是无对应ICD编码的模糊描述。2. 从源码解压到服务启动四步跑通最小可运行闭环这个 ZIP 包不是「开箱即用」但它的目录结构非常诚实/data/下放着已清洗的医学实体CSV、/kg/里是生成好的Neo4j数据库快照、/src/中的qa_engine.py是真正干活的引擎。我一般会跳过「先读文档」这一步直接用命令验证环境兼容性——因为医疗NLP工具链对Python版本极其敏感。2.1 环境隔离与依赖安装为什么必须用 conda 而非 pip# 创建独立环境关键指定Python 3.9因部分医学NLP库不支持3.10 conda create -n medkg python3.9 conda activate medkg # 安装核心依赖注意顺序先装图数据库驱动再装NLP组件 pip install neo4j5.18.0 # 必须锁定此版本高版本与源码中Cypher查询语法不兼容 pip install jieba0.42.1 transformers4.35.2 torch2.1.0 pip install rdflib6.3.2 # 用于后续RDF转换非运行必需但调试必备提示不要用pip install -r requirements.txt—— 原包里的requirements.txt混入了开发期调试工具如pyvis会导致生产环境启动失败。实际运行只需上述7个包总安装体积控制在1.2GB内。2.2 Neo4j 数据库初始化三分钟加载完成的实操细节源码中的/kg/neo4j_dump/是一个压缩过的数据库快照但直接neo4j-admin load会报错Unsupported database format。这是因为导出时用了 Neo4j 5.12而你的本地 Neo4j 可能是 5.18。正确做法是# 1. 启动 Neo4j确保配置文件 conf/neo4j.conf 中已开启dbms.security.auth_enabledfalse neo4j start # 2. 用浏览器访问 http://localhost:7474输入默认账号密码neo4j / neo4j首次登录强制改密 # 3. 在浏览器内置的 Cypher 控制台中执行以下命令注意不是命令行是Web界面 CREATE DATABASE medkg; USE DATABASE medkg;然后将/kg/neo4j_dump/解压到 Neo4j 的data/databases/目录下重命名为medkg最后重启 Neo4j。验证是否成功# 进入Neo4j Shell需先安装neo4j-shell-tools插件 cypher-shell -u neo4j -p your_new_password MATCH (n:Drug) RETURN count(n) LIMIT 1 # 正常应返回约 12,843表示药品节点加载成功2.3 启动问答服务绕过 Flask 默认端口冲突的硬核方案源码中app.py默认绑定0.0.0.0:5000但很多医院内网已占用该端口。修改方式不是改代码而是用环境变量# 在项目根目录执行注意必须在 medkg 环境下 export FLASK_APPsrc/app.py export FLASK_RUN_PORT8080 export FLASK_RUN_HOST0.0.0.0 flask run --no-reload此时访问http://localhost:8080/health应返回{status:ok,kg_nodes:24512}。若返回500 Internal Server Error大概率是 Neo4j 密码未同步到src/config.py中的NEO4J_PASSWORD字段——这是新手最常翻车的点。2.4 第一次提问用 curl 验证底层逻辑而非网页表单别急着打开网页前端。先用命令行直击核心逻辑curl -X POST http://localhost:8080/ask \ -H Content-Type: application/json \ -d {question: 糖尿病患者吃二甲双胍会低血糖吗}预期返回{ answer: 二甲双胍单用极少引起低血糖但与胰岛素或磺脲类药物联用时风险增加。, evidence: [ { node_type: Drug, name: 二甲双胍, relation: has_adverse_effect, target: 低血糖 } ], confidence: 0.92 }参数说明confidence值由两部分加权得出——实体识别置信度jieba分词规则匹配占40%图谱路径可信度基于ICD编码层级权重占60%。这个比例在src/qa_engine.py的calculate_confidence()函数中可手动调整。3. 知识图谱构建从PDF指南到可查询三元组的完整流水线源码包里/data/raw_guidelines/下的PDF不是摆设。它们是图谱更新的源头活水。但直接扔给OCR血泪经验告诉你医学PDF的表格、脚注、多栏排版会让Tesseract崩溃。必须走「PDF解析→结构化提取→人工校验→图谱注入」四步。3.1 PDF解析放弃通用库用pdfplumber抓取带语义的文本块import pdfplumber def extract_medical_sections(pdf_path): with pdfplumber.open(pdf_path) as pdf: full_text for page in pdf.pages: # 关键按「视觉区块」而非「整页」提取避免跨栏错乱 for obj in page.extract_words(x_tolerance3, y_tolerance5): # 过滤掉页眉页脚含「指南」「第X章」字样且y坐标50的行 if obj[top] 50 and not re.search(r(指南|第[零一二三四五六七八九十\d]章), obj[text]): full_text obj[text] return full_text.strip() # 示例处理《高血压指南》第4章 text_4_2 extract_medical_sections(/data/raw_guidelines/hypertension_2023.pdf)逻辑说明x_tolerance3表示横向字符间距容忍3像素y_tolerance5表示纵向行距容忍5像素。这两个值是反复测试得出的——太小则把「收缩压」拆成「收缩」和「压」太大则把表格内容和正文混在一起。3.2 结构化提取用正则规则模板定位「禁忌症」「适应症」等关键段落医学文本有强模式。比如「禁忌症」段落必然以「【禁忌症】」或「禁忌」开头后接顿号分隔的短语。我们不用BERT做NER而用确定性规则import re def extract_contraindications(text): # 匹配「【禁忌症】」后所有中文字符直到遇到下一个「【」或「\n\n」 pattern r【禁忌症】([\u4e00-\u9fa5、。]?)(?(?:【|\n\n|$)) matches re.findall(pattern, text) if not matches: # 备用模式匹配「禁忌」后内容 pattern r禁忌([\u4e00-\u9fa5、。]?)(?(?:\n\n|$)) matches re.findall(pattern, text) # 清洗去掉括号内注释、合并顿号分隔项 cleaned [] for m in matches: # 去掉「详见第X条」类干扰 m re.sub(r[^]{1,15}, , m) # 按顿号、分号切分 items re.split(r[、], m) cleaned.extend([item.strip() for item in items if item.strip()]) return list(set(cleaned)) # 去重 # 输出示例[严重肝肾功能不全, 妊娠期妇女, 对本品过敏者]3.3 图谱注入将提取结果转为Neo4j可执行的Cypher语句不能直接CREATE必须用MERGE避免重复节点。且医学实体需带标准编码def generate_cypher_for_drug_contraindication(drug_name, contraindications): cypher_list [] # 创建药品节点带ATC编码 cypher_list.append(fMERGE (d:Drug {{name: {drug_name}, atc_code: C07AB02}})) for cond in contraindications: # 创建禁忌症节点带ICD-11编码此处用占位符实际需查ICD库 cond_clean cond.replace(, \\) # 防SQL注入 cypher_list.append(fMERGE (c:Contraindication {{name: {cond_clean}, icd11_code: XX.YY}})) # 创建关系 cypher_list.append(fMERGE (d)-[:HAS_CONTRAINDICATION]-(c)) return ;\n.join(cypher_list) ; # 生成后用neo4j-driver批量执行非逐条否则10万条要跑2小时 from neo4j import GraphDatabase driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, password)) with driver.session() as session: session.run(generate_cypher_for_drug_contraindication(美托洛尔, [心动过缓, 支气管哮喘]))参数说明atc_code和icd11_code是图谱可推理的关键。没有编码的节点在路径查询时会被过滤——这是设计上的主动约束不是bug。4. 问答引擎深度解析SPARQL生成器为何比大模型更可靠源码中/src/qa_engine.py的核心不是LLM而是一个「问句→SPARQL」编译器。它把「高血压吃什么药」编译成MATCH (d:Drug)-[r:INDICATED_FOR]-(c:Disease {name:高血压}) RETURN d.name, r.dosage, r.frequency但医学问句的复杂度远超这个例子。比如「孕妇禁用的降压药有哪些」需同时满足Drug节点、HAS_CONTRAINDICATION关系指向Contraindication节点、且该节点name包含「孕妇」或「妊娠」。4.1 问句解析用 jieba 自定义词典解决「妊娠期」vs「孕期」歧义通用分词库会把「妊娠期高血压」切成「妊娠/期/高血压」导致无法匹配图谱中预设的「妊娠期」实体。解决方案是注入医学专用词典# 在 src/qa_engine.py 开头添加 import jieba jieba.load_userdict(/data/medical_dict.txt) # 内容示例 # 妊娠期 100 nz # 孕期 80 nz # 急性心肌梗死 1000 nz # AMI 500 nz def parse_question(question): words jieba.lcut(question) # 优先匹配长词如「妊娠期」比「妊娠」优先级高 matched_entities [] for i in range(len(words)): for j in range(min(i3, len(words)), i, -1): # 最多匹配3个字 phrase .join(words[i:j]) if phrase in MEDICAL_ENTITIES: # MEDICAL_ENTITIES 是预加载的实体集合 matched_entities.append(phrase) break return matched_entities4.2 SPARQL模板引擎用「槽位填充」替代自由生成不采用Seq2Seq生成SPARQL易出语法错误而是预设6类模板根据问句关键词选择问句关键词匹配模板ID生成SPARQL片段「禁忌」「禁用」「慎用」T4MATCH (d:Drug)-[r:HAS_CONTRAINDICATION]-(c) WHERE c.name CONTAINS $keyword「适应症」「用于」T1MATCH (d:Drug)-[r:INDICATED_FOR]-(c:Disease) WHERE c.name $disease「不良反应」「副作用」T3MATCH (d:Drug)-[r:HAS_ADVERSE_EFFECT]-(a) WHERE a.name CONTAINS $keyworddef select_sparql_template(question): if any(kw in question for kw in [禁忌, 禁用, 慎用, 不宜]): return T4 elif any(kw in question for kw in [适应症, 用于, 治疗]): return T1 elif any(kw in question for kw in [不良反应, 副作用, 常见反应]): return T3 else: return T0 # 默认模板模糊搜索 def fill_template(template_id, entities): if template_id T4: # 从entities中找「孕妇」「哺乳期」等禁忌关键词 keyword next((e for e in entities if 孕妇 in e or 妊娠 in e), 妊娠) return fMATCH (d:Drug)-[r:HAS_CONTRAINDICATION]-(c) WHERE c.name CONTAINS {keyword} RETURN d.name # 其他模板类似...4.3 置信度计算为什么「二甲双胍」的置信度比「格列齐特」高0.15置信度公式在calculate_confidence()中confidence 0.4 * entity_match_score 0.6 * path_reliability_scoreentity_match_score实体匹配的编辑距离归一化值如「二甲双胍」完全匹配得1.0「二甲福明」得0.82path_reliability_score取决于关系边的source属性人工标注的证据等级source: 指南→ 权重1.0source: 药品说明书→ 权重0.85source: 专家共识→ 权重0.75所以当问「二甲双胍禁忌症」时返回的「肾功能不全」节点来自《糖尿病诊疗指南》权重1.0而「格列齐特」的禁忌症数据源是某药企说明书权重0.85——这就是0.15分差的来源。5. 避坑指南五个让医疗问答系统上线失败的真实场景这些不是理论假设而是某三甲医院信息科在灰度测试中记录的原始日志。每一条都对应源码中一个可定位、可修复的点。5.1 现象问「感冒能吃阿莫西林吗」返回空结果原因图谱中「阿莫西林」节点的class属性是Antibiotic但问句解析时未将「感冒」映射到Viral_Infection类型导致MATCH (d:Drug)-[r:CONTRAINDICATED_FOR]-(v:Viral_Infection)无匹配。解决在/data/mapping_rules.json中添加映射{感冒: Viral_Infection, 流感: Viral_Infection, 着凉: Viral_Infection}并在parse_question()中加载该规则。5.2 现象Neo4j 查询超时120s日志显示Query execution time exceeded原因源码中qa_engine.py的execute_cypher()函数未设置超时而某些模糊查询如WHERE n.name CONTAINS 痛会扫描全库。解决在execute_cypher()中添加result session.run(cypher, timeout10.0) # 强制10秒超时并在app.py的/ask接口捕获neo4j.exceptions.TransactionTimedOut异常返回友好提示。5.3 现象同一问句「高血压用药」第一次返回3个药第二次返回5个药原因jieba分词存在随机性因内部使用random.seed()导致实体识别结果波动。解决在qa_engine.py开头固定随机种子import random random.seed(42) # 必须在jieba导入前设置 import jieba5.4 现象问「布洛芬和阿司匹林能一起吃吗」返回「无答案」原因图谱中只有单药节点和单药适应症缺少「药物相互作用」关系类型。源码默认只查询INDICATED_FOR、HAS_CONTRAINDICATION等6种关系未包含HAS_DRUG_INTERACTION。解决在/kg/schema.json中新增关系定义并在select_sparql_template()中增加T7模板匹配「合用」「联用」「一起吃」等关键词。5.5 现象部署到医院内网后所有请求返回502 Bad Gateway原因Nginx反向代理未配置 WebSocket 支持而源码中/ws/health接口用于实时检测Neo4j状态其升级请求被Nginx拦截。解决在Nginx配置中添加location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }6. 进阶技巧用「证据链可视化」说服临床科室接受系统医生不关心技术指标只信「这个答案从哪来」。源码中/src/visualize_evidence.py提供了将SPARQL查询路径转为HTML的能力但默认输出是静态图。我做了三个改造让它真正可用6.1 证据链必须带「指南出处」悬浮提示原可视化只显示节点和关系医生看不到依据来源。我在visualize_evidence.py的generate_html()中插入def add_source_tooltip(node_data): if node_data.get(source): # source字段格式如指南2023第4.2.5条 tooltip f依据{node_data[source]} # 注入到D3.js的node元素title属性中 return ftitle{tooltip} return # 在生成每个node的HTML时调用 node_html fcircle {add_source_tooltip(node)} r12/这样鼠标悬停在「肾功能不全」节点上就会显示「依据指南2023第4.2.5条」。6.2 支持「对比问答」一次展示两个答案的证据强度差异临床常问「为什么选A药不选B药」。我在/app.py新增/compare接口app.route(/compare, methods[POST]) def compare_answers(): data request.get_json() q1 data[question1] # 高血压首选什么药 q2 data[question2] # 高血压合并糖尿病首选什么药 # 分别执行问答获取证据链 result1 qa_engine.ask(q1) result2 qa_engine.ask(q2) # 计算证据链长度、最高权重边、指南引用次数 summary { q1: {evidence_length: len(result1[evidence]), max_weight: max(e[weight] for e in result1[evidence])}, q2: {evidence_length: len(result2[evidence]), max_weight: max(e[weight] for e in result2[evidence])} } return jsonify(summary)前端用ECharts画对比柱状图医生一眼看出「合并糖尿病」的答案证据链更长、权威性更高。6.3 证据链导出为Word嵌入医院标准报告模板临床科室要求所有辅助结论必须生成Word并盖电子章。我用python-docx替换了原包的纯文本导出from docx import Document from docx.shared import Inches def export_to_word(question, answer, evidence): doc Document() doc.add_heading(临床辅助问答报告, 0) doc.add_paragraph(f提问{question}) doc.add_paragraph(f答案{answer}) doc.add_heading(证据链, level1) for i, e in enumerate(evidence, 1): p doc.add_paragraph() p.add_run(f{i}. ).bold True p.add_run(f{e[node_type]}「{e[name]}」→{e[relation]}→「{e[target]}」) p.add_run(f依据{e.get(source, 未标注)}).italic True doc.save(freport_{int(time.time())}.docx)我的习惯每次给临床科室演示前我会用这个导出功能生成3份真实问题的Word报告打印出来放在会议桌上。他们摸到纸张的质感比看屏幕上的JSON更有信任感。技术落地的本质从来不是参数调得多漂亮而是让使用者愿意把它放进自己的工作流里——哪怕只是作为「第二意见」。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站