医疗知识图谱工程实战:从Excel到可扩展问答系统 简介本资源是一个基于Neo4j构建的轻量级医疗问答知识图谱实践项目面向知识图谱初学者、医疗信息化开发者及NLP方向学习者旨在解决医疗领域结构化知识建模与关联查询的实际问题。压缩包共37个文件含13个Python源码涵盖爬虫spider1.py/spider2.py、图数据库操作dbopt、问答处理qaprocess、Django后端模块等、7个XML配置文件.idea工程配置、3个HTML前端页面及1个PNG示意图整体仅78KB轻量易读适合快速理解从数据采集、清洗建模到Cypher查询的全流程。已有5464人学习下载项目完整复现了从Ask120平台爬取疾病-症状-治疗等实体关系、构建节点与边、导入Neo4j并支持基础问答检索的闭环逻辑附带清晰目录结构与可运行Django框架雏形是掌握知识图谱落地医疗场景的优质入门范例。1. 一个能查“高血压能不能吃柚子”的医疗问答系统不是Demo是能跑通、能改、能加病种的完整知识图谱工程你有没有试过在医院候诊时用手机快速查一句“二甲双胍和葡萄柚同服会怎样”不是打开搜索引擎翻五页广告也不是点开某健康App看AI胡编的“建议”而是输入一句话系统直接从真实药品说明书、临床指南、药物相互作用数据库里抽取出结构化结论并告诉你依据来源——这个项目就是干这个的。它不是一个教你怎么画节点关系的Neo4j入门示例而是一个从数据清洗、图谱构建、自然语言问句解析到答案生成全链路打通的可运行工程含3类实体疾病/药品/症状、12种关系如“禁忌”“加重”“缓解”、支持中文问句“糖尿病患者吃阿卡波糖要注意什么”后端用Flask暴露REST接口前端是Vue写的极简问答页。适合想落地知识图谱但被“怎么把PDF指南变成图”卡住的开发者也适合医学信息学课程设计——我拿它给某高校实验室的学生做课程项目三天内跑通替换本地高血压数据集上线测试没碰过图数据库的同学也能照着文档配好环境。核心不在炫技而在每一步都留了钩子数据格式怎么改、问句模板怎么扩、答案排序逻辑怎么调全在源码里明明白白标着注释。2. 技术栈与工程结构为什么选Neo4j Flask Vue而不是LangChain套壳这个项目没上大模型也没堆向量库它解决的是一个更基础、更痛的问题如何让非NLP背景的开发者用确定性规则结构化查询快速交付一个可信度高的垂直领域问答。很多团队一上来就想接LLM结果发现医生问“华法林和维生素K拮抗剂联用是否增加出血风险”模型要么瞎编文献编号要么绕开关键点。而本项目用Neo4j原生Cypher做精准路径匹配配合预定义的问句模式Pattern Matching答案可追溯、可验证、可审计。技术选型不是拍脑袋是踩过坑后的收敛图数据库层Neo4j 5.x社区版选它不是因为“知识图谱必须用Neo4j”而是它的Cypher语法对医疗关系表达太友好。比如“查找所有禁忌高血压患者的药品”一行Cypher就能写完MATCH (d:Disease {name:高血压})-[:CONTRAINDICATED_FOR]-(m:Medicine) RETURN m.name。换成RDF三元组或自建图存储光写个等价查询就得调试半天。项目已适配Neo4j 5.16避开了旧版中apoc插件权限问题后面避坑章细说。后端服务Flask 2.3 Neo4j Python Driver 5.1没用FastAPI因为本项目不需要高并发或异步IO——医疗问答本质是低频、高准确率场景。Flask轻量、调试直观路由清晰对应问答模块。Driver版本锁死5.1因新版对session.read_transaction()的异常处理逻辑变更会导致部分关系查询返回空而不报错血泪经验有次上线后发现“药物相互作用”类问题全无响应查了6小时才发现是driver升级导致的静默失败。前端交互Vue 3 Composition API Element Plus纯静态HTML也能跑但加Vue是为了让“改问句模板”这件事变得可视化。比如新增一个问句类型“XX药的常见不良反应”只需在src/config/questionPatterns.js里加一条正则Cypher模板不用动任何Vue组件逻辑。Element Plus提供现成的对话框和加载状态省去手写loading遮罩的麻烦。提示项目不依赖Docker但提供了docker-compose.yml含Neo4jFlaskVue三服务。如果你本地已装Neo4j Desktop直接用它启动更省资源若用Docker注意NEO4J_AUTHneo4j/password必须和config.py中配置一致否则后端连不上图库。2.1 目录结构每个文件夹都在解决一个具体问题项目解压后是标准Python工程结构但每个目录名都直指功能medical-kg/ ├── data/ # 【数据源头】原始Excel/CSV放这里不是中间产物 │ ├── disease_symptom.xlsx # 疾病-症状关联表含严重程度 │ ├── medicine_disease.xlsx# 药品-疾病适应症/禁忌表 │ └── drug_interaction.csv # 药物相互作用三列drug_a, drug_b, effect ├── graph_builder/ # 【图谱构建】核心脚本把data/转成Neo4j可导入的CSVCypher │ ├── clean_data.py # 清洗统一药品商品名/通用名补全缺失关系标记 │ ├── generate_cypher.py # 根据schema.yaml生成批量CREATE语句防手写漏节点 │ └── schema.yaml # 实体属性定义如Disease必有icd_code, severity_level ├── app/ # 【后端服务】Flask主程序 │ ├── __init__.py # 初始化Neo4j连接池max_size10防并发打爆 │ ├── models/ # Cypher查询封装每个方法对应一类问句 │ │ ├── disease_query.py # 查疾病症状、并发症、检查项 │ │ ├── medicine_query.py# 查药品适应症、禁忌、不良反应 │ │ └── interaction_query.py # 查两药联用效应重点含置信度排序 │ ├── routes/ # REST接口/api/ask 接收问句/api/debug 查当前图谱统计 │ └── config.py # 数据库地址、密码、超时时间query_timeout30s ├── frontend/ # 【前端页面】Vue工程build后静态文件放app/static/ │ ├── src/ │ │ ├── components/ # 问答卡片、答案溯源面板显示“依据《2023版中国高血压防治指南》第5.2条” │ │ ├── config/ │ │ │ └── questionPatterns.js # 问句正则对应Cypher模板关键可配置点 │ │ └── main.js # 挂载Vue实例注入后端API base_url ├── tests/ # 【验证闭环】含3类测试数据清洗断言、Cypher执行结果校验、问句解析覆盖率 └── README.md # 不是“欢迎使用”而是写清“第一步改哪里、第二步测什么、第三步加数据”注意graph_builder/下没有load_to_neo4j.py这种“一键导入”脚本——因为真实项目中图谱更新是分批、带校验、可回滚的。项目用generate_cypher.py输出.cypher文件再由DBA人工审核后执行这是医疗系统上线前的硬性流程。2.2 核心数据流从Excel表格到一句“能吃/不能吃”的答案经过哪几道关卡用户问“高血压患者能吃柚子吗”整个链路如下不是理论是代码实际执行顺序问句预处理app/nlp/preprocess.py去停用词 → 识别实体“高血压”→Disease“柚子”→Medicine→ 标准化名称“西柚”“葡萄柚”→统一为“柚子”。这步用的是基于词典的匹配jieba自定义医疗词典不是BERT——因为医疗名词变化少、歧义低规则更快更稳。模式匹配frontend/src/config/questionPatterns.js遍历预定义的12种问句模板命中/.*[疾病].*能吃.*[药品].*/→ 返回模板IDQ_MEDICINE_CONTRAINDICATION。Cypher生成app/models/medicine_query.py::get_contraindication()将模板中的占位符替换成实际实体生成最终查询MATCH (d:Disease {name: 高血压})-[:CONTRAINDICATED_FOR]-(m:Medicine {name: 柚子}) RETURN m.name AS medicine, 禁忌 AS relation, d.name AS disease如果没查到自动 fallback 到更宽泛的关系[:MAY_AFFECT]-(m)可能影响或[:NOT_RECOMMENDED]-(m)不推荐。答案组装app/routes/ask.py把Cypher结果溯源信息如“该结论来自《药物相互作用数据库V2.1》更新于2024-03”拼成JSON前端渲染时高亮关键词、加“查看依据”按钮。关键点所有环节都可打断、可日志、可替换。比如你想换NER模型只改preprocess.py里的extract_entities()函数想加新问句类型只在questionPatterns.js里加一行想让答案带参考文献链接改models/xxx_query.py里format_answer()方法即可。3. 图谱构建实战三步把Excel疾病表变成Neo4j里的可查询节点别被“知识图谱”吓住——本项目的数据构建过程就是一个程序员熟悉的ETLExtract读Excel、Transform清洗补全、Load生成Cypher。没有黑匣子每一步命令都能复制粘贴执行。3.1 第一步用clean_data.py把脏数据变干净医疗数据最大的坑是名称不统一。比如Excel里同一款药有的写“阿司匹林肠溶片”有的写“拜阿司匹灵”有的只写“阿司匹林”。clean_data.py做了三件事建立映射表读取data/mapping/medicine_name_map.csv项目已提供常见药名映射将别名转为标准通用名。补全缺失字段疾病表里icd_code为空的行用icd10_lookup.py根据疾病描述模糊匹配ICD-10编码调用本地ICD-10数据库非网络请求。关系强度标注对“药物-疾病”关系按原始数据中的“证据等级”列A/B/C级生成confidence_score属性供后续答案排序用。执行命令在项目根目录python graph_builder/clean_data.py --input data/disease_symptom.xlsx --output data/cleaned_disease_symptom.csv注意--output指定的路径必须是data/子目录因为后续脚本默认从此处读取清洗后数据。如果报错“找不到mapping文件”检查data/mapping/是否存在项目包里已包含常用药名映射表含500条。3.2 第二步用generate_cypher.py生成可执行的建图语句清洗后的CSV还是表格要变成图得用Cypher创建节点和关系。generate_cypher.py根据schema.yaml动态生成避免手写CREATE (:Disease {name:高血压, icd_code:I10})这种易错操作。schema.yaml关键片段entities: Disease: required_fields: [name, icd_code] optional_fields: [severity_level, common_symptoms] Medicine: required_fields: [name, atc_code] optional_fields: [dosage_form, manufacturer] relations: CONTRAINDICATED_FOR: from: Medicine to: Disease properties: [evidence_level, confidence_score]执行生成命令python graph_builder/generate_cypher.py --schema graph_builder/schema.yaml --output graph_builder/generated/生成的文件包括create_nodes.cypher批量创建Disease/Medicine/Symptom节点带MERGE防重复create_relations.cypher创建CONTRAINDICATED_FOR等关系用MATCH找节点再CREATE关系index.cypher为name字段建全文索引加速问句实体识别提示生成的Cypher文件不直接执行先用Neo4j Browser粘贴测试小批量数据如前10行确认节点属性、关系方向无误后再全量运行。项目tests/目录下有test_cypher_execution.py可自动化验证生成语句的语法正确性。3.3 第三步在Neo4j中执行建图脚本含权限与性能关键参数Neo4j 5.x 默认关闭apoc插件而本项目create_relations.cypher里用了apoc.periodic.iterate做分批导入防内存溢出。需手动启用编辑Neo4j配置文件conf/neo4j.conf取消注释dbms.security.procedures.unrestrictedapoc.* dbms.security.procedures.whitelistapoc.*,gds.*重启Neo4j服务然后在Neo4j Browser中依次执行// 先建索引必须在导入前否则后续查询极慢 SOURCE /path/to/graph_builder/generated/index.cypher // 再分批导入节点1000条/批 CALL apoc.periodic.iterate( LOAD CSV WITH HEADERS FROM file:///cleaned_disease_symptom.csv AS row RETURN row, CREATE (:Disease {name: row.disease_name, icd_code: row.icd_code}), {batchSize:1000, iterateList:true} ) // 最后导入关系同样分批 SOURCE /path/to/graph_builder/generated/create_relations.cypher注意file:///路径是Neo4j服务端的绝对路径不是你本地路径。把cleaned_disease_symptom.csv放到Neo4j安装目录下的import/文件夹如/var/lib/neo4j/import/然后LOAD CSV才能读到。这是新手最常翻车的点——本地路径写对了但Neo4j根本看不到文件。4. 问答接口开发从问句到答案的7个关键函数以及它们藏在哪后端app/目录不是一堆路由堆砌而是按“问题类型”组织的查询能力模块。每个.py文件对应一类医疗问题函数命名即功能抄作业时直接定位4.1app/models/disease_query.py查疾病本身及关联信息get_symptoms(disease_name: str) → List[dict]查询某疾病的所有症状按severity_level降序排。返回示例[{symptom: 头痛, severity: 中度, frequency: 常见}, {symptom: 眩晕, severity: 轻度, frequency: 偶见}]关键CypherMATCH (d:Disease {name:$disease})-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name AS symptom, s.severity AS severityget_comorbidities(disease_name: str) → List[str]查共病如高血压常伴发糖尿病用[:ASSOCIATED_WITH]关系返回疾病名列表。4.2app/models/medicine_query.py药品核心查询最常用get_contraindications(medicine_name: str) → List[dict]查该药禁忌的疾病如“阿司匹林”禁忌“活动性消化道溃疡”。这是“高血压能不能吃柚子”的底层支撑。get_adverse_reactions(medicine_name: str) → List[dict]查不良反应按发生率排序frequency属性常见/偶见/罕见。get_dosage_guideline(medicine_name: str, disease_name: str) → dict查特定疾病下的用药指导如“高血压患者用氨氯地平的起始剂量”需同时匹配药品和疾病节点。4.3app/models/interaction_query.py药物相互作用精度要求最高get_interaction(drug_a: str, drug_b: str) → dict这是项目最难写的函数。原因原始数据中drug_a和drug_b顺序不固定有的写“华法林阿司匹林”有的写“阿司匹林华法林”且存在“任意一种药”触发相互作用的情况。代码用双重MATCH确保顺序无关// 匹配两种顺序 OPTIONAL MATCH (m1:Medicine {name: $drug_a})-[:INTERACTS_WITH]-(m2:Medicine {name: $drug_b}) OPTIONAL MATCH (m1:Medicine {name: $drug_b})-[:INTERACTS_WITH]-(m2:Medicine {name: $drug_a}) RETURN COALESCE(m1.name, m2.name) AS drug_a, COALESCE(m2.name, m1.name) AS drug_b, ...返回结果带effect_level轻度/中度/重度和clinical_recommendation如“应避免联用”“需监测INR”。4.4app/routes/ask.pyREST接口的胶水层app.route(/api/ask, methods[POST])主入口。接收JSON{question: 高血压患者吃阿卡波糖要注意什么}流程预处理 → 模式匹配 → 调用对应模型函数 → 组装答案 → 记录日志app/logs/query.log。app.route(/api/debug/stats)开发者救命接口返回当前图谱统计{node_count: 1248, relation_count: 3562, disease_count: 89, medicine_count: 421}部署后第一件事就是访问这个接口确认数据导入成功。提示所有模型函数都加了handle_neo4j_error装饰器定义在app/utils/error_handler.py捕获ServiceUnavailable、AuthError等Neo4j特有异常统一返回HTTP 503并带提示语避免前端看到Python traceback。5. 避坑指南部署时踩过的5个坑每个都让我重装过Neo4j这些不是“理论上可能出错”而是我在三台不同配置机器Mac M1、Ubuntu 22.04、Windows WSL2上实测翻过的车。跳过它们能省你至少8小时5.1 现象Flask启动报错Connection refused但Neo4j Browser能正常访问原因Neo4j默认只监听localhost而Flask Docker容器内访问localhost指向容器自身不是宿主机。解决修改Neo4j配置conf/neo4j.confdbms.connectors.default_listen_address0.0.0.0 dbms.connector.bolt.listen_address:7687然后在app/config.py中把NEO4J_URI设为宿主机IP如bolt://172.17.0.1:7687不是localhost。5.2 现象问句“糖尿病吃什么水果”返回空但手动查MATCH (:Disease {name:糖尿病})-[:RECOMMENDED_FOOD]-(:Food)有结果原因questionPatterns.js里水果相关模板正则写成了/.*[疾病].*吃.*[食物].*/但“水果”在词典里被归类为Food实体而预处理脚本preprocess.py默认只识别Disease/Medicine/Symptom三类没加Food。解决在preprocess.py的ENTITY_TYPES列表里加上Food并在data/mapping/food_name_map.csv中维护常见水果标准名。5.3 现象导入大量数据后/api/debug/stats响应超时30s原因Neo4j未建索引COUNT(*)全表扫描。解决执行CREATE INDEX ON :Disease(name)和CREATE INDEX ON :Medicine(name)。项目graph_builder/generated/index.cypher已包含但新手常忘记执行。5.4 现象Vue前端报错Failed to fetch但curl http://localhost:5000/api/ask能通原因浏览器同源策略。Vue开发服务器npm run serve跑在http://localhost:8080而Flask在http://localhost:5000跨域未配置。解决在app/__init__.py中加CORS支持项目已预留代码取消注释即可from flask_cors import CORS CORS(app, resources{r/api/*: {origins: http://localhost:8080}})5.5 现象Neo4j Browser里能查到节点但Flask查询返回空原因Neo4j 5.x默认开启multi-tenancy新建数据库需显式切换。项目默认用neo4j数据库但Browser里可能在system库。解决在Browser中执行:use neo4j切换数据库或在app/config.py中指定databaseneo4j已配置但需确认。注意所有避坑方案都已在项目README.md的“Troubleshooting”章节列出且附带验证命令如telnet 172.17.0.1 7687测端口连通性。别跳过读它——我第一次部署时就因没看这一节重装了三次Neo4j。6. 二次开发实操如何在30分钟内为“痛风”病种添加完整的问答能力这才是项目真正的价值不是给你一个成品而是给你一套可复用的扩展方法论。下面以“为痛风添加症状、禁忌药、饮食建议”为例走一遍从数据准备到上线的全流程。所有操作都在项目目录内完成无需装新工具。6.1 第一步准备痛风专属数据5分钟在data/下新建三个文件严格按项目约定格式data/pain_gout_symptoms.csv症状表disease_name,symptom_name,severity_level,frequency 痛风,关节红肿热痛,重度,常见 痛风,血尿酸升高,中度,必查data/pain_gout_medicines.csv药品表medicine_name,disease_name,relation_type,evidence_level 别嘌醇,痛风,INDICATED_FOR,A 阿司匹林,痛风,CONTRAINDICATED_FOR,Cdata/pain_gout_foods.csv饮食表disease_name,food_name,relation_type,note 痛风,动物内脏,AVOID,高嘌呤诱发急性发作 痛风,牛奶,RECOMMENDED,低嘌呤促尿酸排泄提示relation_type必须是schema.yaml中定义的类型如INDICATED_FOR,CONTRAINDICATED_FOR,AVOID,RECOMMENDED拼错一个字母就会导入失败。6.2 第二步注册新实体类型与关系10分钟修改graph_builder/schema.yaml在entities下加Food在relations下加AVOID和RECOMMENDEDentities: Food: required_fields: [name] optional_fields: [purine_level, note] relations: AVOID: from: Disease to: Food properties: [note] RECOMMENDED: from: Disease to: Food properties: [note]然后运行python graph_builder/generate_cypher.py新生成的create_nodes.cypher会包含Food节点创建语句create_relations.cypher会包含AVOID/RECOMMENDED关系。6.3 第三步扩展问句模板与后端查询10分钟在frontend/src/config/questionPatterns.js中加两条模板{ id: Q_FOOD_AVOID, pattern: /.*[疾病].*不能吃.*[食物].*/, cypherTemplate: MATCH (d:Disease {name: $disease})-[:AVOID]-(f:Food) RETURN f.name AS food, f.note AS note }, { id: Q_FOOD_RECOMMEND, pattern: /.*[疾病].*可以吃.*[食物].*/, cypherTemplate: MATCH (d:Disease {name: $disease})-[:RECOMMENDED]-(f:Food) RETURN f.name AS food, f.note AS note }在app/models/food_query.py新建此文件中实现查询函数def get_avoid_foods(disease_name: str): with driver.session(databaseneo4j) as session: result session.run( MATCH (d:Disease {name: $disease})-[:AVOID]-(f:Food) RETURN f.name AS food, f.note AS note, diseasedisease_name ) return [{food: r[food], note: r[note]} for r in result]在app/routes/ask.py的问句分发逻辑中加入对新模板ID的路由if pattern_id Q_FOOD_AVOID: answer food_query.get_avoid_foods(disease_name) elif pattern_id Q_FOOD_RECOMMEND: answer food_query.get_recommended_foods(disease_name)6.4 第四步验证与上线5分钟执行数据导入按3.3节步骤重启Flask服务cd app python run.py前端重新构建cd frontend npm run build访问http://localhost:8080输入“痛风不能吃动物内脏吗”应返回明确答案从准备数据到看到答案全程可控制在30分钟内。这就是本项目的设计哲学扩展成本应该远低于理解成本。你不需要懂Cypher语法只要会填Excel、会改JS对象、会写一个Python函数就能让系统多回答一类问题。从那以后我每次给新病种加能力都强制走一遍这个四步流程建数据→改schema→扩模板→写函数。哪怕只是加一个症状也走完全部因为漏掉任何一环上线后问题都是静默的——用户问不出答案日志里也不报错只能靠人工排查。希望帮到你。本文还有配套的精品资源点击获取