简介:本资源是一个基于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(含Neo4j+Flask+Vue三服务)。如果你本地已装Neo4j Desktop,直接用它启动更省资源;若用Docker,注意NEO4J_AUTH=neo4j/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可导入的CSV+Cypher │ ├── clean_data.py # 清洗:统一药品商品名/通用名,补全缺失关系标记 │ ├── generate_cypher.py # 根据schema.yaml生成批量CREATE语句(防手写漏节点) │ └── schema.yaml # 实体属性定义(如Disease必有icd_code, severity_level) ├── app/ # 【后端服务】Flask主程序 │ ├── __init__.py # 初始化Neo4j连接池(max_size=10,防并发打爆) │ ├── models/ # Cypher查询封装,每个方法对应一类问句 │ │ ├── disease_query.py # 查疾病症状、并发症、检查项 │ │ ├── medicine_query.py# 查药品适应症、禁忌、不良反应 │ │ └── interaction_query.py # 查两药联用效应(重点!含置信度排序) │ ├── routes/ # REST接口,/api/ask 接收问句,/api/debug 查当前图谱统计 │ └── config.py # 数据库地址、密码、超时时间(query_timeout=30s) ├── 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里的可查询节点
别被“知识图谱”吓住——本项目的数据构建过程,就是一个程序员熟悉的ETL:Extract(读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.unrestricted=apoc.* dbms.security.procedures.whitelist=apoc.*,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": "偶见"}]关键Cypher:
MATCH (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.py:REST接口的胶水层
@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.conf:
dbms.connectors.default_listen_address=0.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中指定database="neo4j"(已配置,但需确认)。
注意:所有避坑方案都已在项目
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和RECOMMENDED:
entities: 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(database="neo4j") as session: result = session.run( "MATCH (d:Disease {name: $disease})-[:AVOID]->(f:Food) " "RETURN f.name AS food, f.note AS note", disease=disease_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→扩模板→写函数。哪怕只是加一个症状,也走完全部,因为漏掉任何一环,上线后问题都是静默的——用户问不出答案,日志里也不报错,只能靠人工排查。希望帮到你。
本文还有配套的精品资源,点击获取