
简介这是一套面向Python开发者与医疗AI初学者的智能诊断问答系统实战项目聚焦知识图谱构建与向量检索技术在健康医疗场景的落地应用帮助用户掌握从医学知识建模、语义向量化到端到端问答服务部署的完整链路。资源包共188个文件含71个核心Python脚本覆盖NER识别、意图分类、图谱查询与Flask接口、24张可视化图表如疾病关系图、相似度热力图、6个预训练模型文件.pkl/.h5、以及训练/测试/验证用CSV数据集和配套说明文档MD/README整体压缩后仅23.64MB轻量易部署。已有497人学习下载项目结构清晰含可直接运行的bat启动脚本如run_ner_service.bat、分阶段训练数据train/dev/test.csv及词汇表文件slot_vocab、intent_vocab便于复现、调试与二次开发是理解医疗领域知识增强型问答系统工程实践的优质参考样本。1. 这不是另一个“AI问诊”Demo它用Neo4jBERT双引擎把症状-疾病-药物关系真正跑通了你试过在医疗问答系统里输入“持续低热伴夜间盗汗三周右上腹隐痛体重下降5kg”结果返回三条泛泛而谈的“可能为结核或肿瘤请及时就医”这不是模型能力不足而是传统关键词匹配和单一路由检索根本无法建模医学知识间的多跳推理路径。本项目跳出了“问答文本相似度”的惯性思维——它把《默克诊疗手册》《ICD-10编码规范》《药品说明书》等结构化与半结构化数据先注入Neo4j图谱形成「症状→体征→检查指标→疾病→靶点→药物→禁忌」的6层关联网络再用BERT微调后的双塔模型将用户问题与图谱中每个节点而非整篇文档独立编码为768维向量最后通过FAISS实现毫秒级向量召回图遍历验证。实测在test.csv的327个真实临床问句中Top-3准确率达89.3%关键在于它不只返回“答案”而是返回带置信度的推理链路比如“盗汗→结核感染可能性↑→需查PPD/γ-干扰素释放试验→慎用糖皮质激素”。适合已有临床数据沉淀的三甲医院信息科、医疗AI初创团队以及需要落地可解释性诊断辅助工具的科研组——它不替代医生但能把医生从“翻指南→查文献→比对症状”的线性劳动中解放出来。2. 知识图谱构建从train.csv到Neo4j的实体-关系清洗与Schema设计2.1 医学实体识别NER服务启动与字段映射逻辑项目中的run_ner_service.bat并非简单调用预训练模型而是针对中文医疗文本定制的三层NER流水线第一层基于spacy-transformers加载bert-base-chinese识别原始文本中的粗粒度实体如“肺结核”“阿司匹林”“ALT升高”第二层用规则引擎校验实体边界——例如“左下腹痛”必须拆分为“左下腹”解剖部位“痛”症状而“高血压性心脏病”需整体识别为疾病实体第三层对接UMLSUnified Medical Language System概念ID将“心肌梗死”映射到CUI:C0020373确保跨数据源一致性。执行run_ner_service.bat后脚本会读取train.csv含12,843条标注样本输出ner_output.jsonl每行格式为{ text: 患者女62岁主诉反复上腹痛3月伴反酸嗳气, entities: [ {start: 12, end: 16, label: SYMPTOM, text: 上腹痛}, {start: 22, end: 26, label: SYMPTOM, text: 反酸}, {start: 27, end: 29, label: SYMPTOM, text: 嗳气} ], cui_mapping: {上腹痛: C0023805, 反酸: C0035001, 嗳气: C0023804} }提示train.csv字段必须包含text原始问句、label人工标注的实体类型、entity_text实体原文。若字段名不符需修改ner_service/config.py中的CSV_COLUMN_MAP字典。2.2 Neo4j Schema定义与关系抽取规则图谱Schema严格遵循SNOMED CT临床术语标准核心节点类型与关系如下节点类型属性示例关系类型目标节点业务约束:Diseasecui: C0020373,icd10: I25.6HAS_SYMPTOM:Symptom权重字段confidence: 0.92来自文献支持度:Symptomcui: C0023805,severity: moderateTRIGGERS_BY:Drug关系属性mechanism: cholinergic_stimulation:Drugatc_code: C01CA07,half_life: 3.5CONTRAINDICATED_FOR:Disease添加evidence_level: AGRADE证据等级关系抽取不依赖纯规则而是结合dev.csv中的专家标注样本训练BiLSTM-CRF模型。关键步骤执行python ner_relation_extractor.py --input dev.csv --output neo4j_import.cypher生成的Cypher脚本包含批量创建语句// 创建疾病节点去重 CREATE (d:Disease {cui: C0020373, name: 心肌梗死, icd10: I25.6}) // 创建症状节点并建立关系带置信度 CREATE (s:Symptom {cui: C0023805, name: 胸痛}) CREATE (d)-[r:HAS_SYMPTOM {confidence: 0.97, source: UpToDate_2023}]-(s)导入Neo4j前需执行neo4j-admin import --nodesnodes.csv --relationshipsrels.csv其中nodes.csv含id:cui:STRING,name:STRING,labels:STRING三列。2.3 图谱质量验证用Cypher查询检测常见医学逻辑错误部署后必须运行以下验证查询否则向量检索将因脏数据失效// 检查是否存在无出度的疾病节点即未关联任何症状/检查/药物 MATCH (d:Disease) WHERE NOT (d)-[]-() RETURN d.name AS disease_name, count(*) AS orphan_count // 检查药物-疾病关系是否双向矛盾如某药既治疗又禁忌同一疾病 MATCH (d:Disease)-[:TREATS]-(drug:Drug)-[:CONTRAINDICATED_FOR]-(d) RETURN drug.name, d.name // 统计症状节点的平均关联疾病数正常范围3~12低于2说明覆盖不足 MATCH (s:Symptom)-[r:HAS_SYMPTOM]-(d:Disease) RETURN s.name, count(d) AS disease_count ORDER BY disease_count ASC LIMIT 5若发现orphan_count 0需回溯train.csv中该疾病对应的问句是否缺失症状标注若存在双向矛盾关系需核查dev.csv中该药物的禁忌证标注是否错误。3. 向量检索引擎BERT双塔模型微调与FAISS索引构建3.1 双塔模型结构设计与微调策略项目未直接使用BERT原生模型而是采用领域适配双塔架构Query Towerbert-base-chinese 2层Transformer Block参数量减少37%输入用户问句输出768维向量Knowledge Tower独立编码图谱中每个节点非整篇文档对:Disease节点拼接name icd10 description对:Symptom节点拼接name severity duration经相同BERT编码器后池化损失函数Triplet Loss Hard Negative Mining负样本从同疾病簇中采样如“心肌梗死”的负样本选“心绞痛”而非“糖尿病”。微调脚本train_vector_model.py关键参数# config.py MODEL_NAME bert-base-chinese MAX_LENGTH 64 # 中文医疗问句平均长度避免截断关键症状词 BATCH_SIZE 16 LEARNING_RATE 2e-5 # 比通用任务低10倍防止灾难性遗忘 HARD_NEGATIVE_RATIO 0.3 # 30% batch为难负样本训练数据来自train.csv与dev.csv按8:2划分。微调后模型保存为./models/bert_dual_tower/含pytorch_model.bin和config.json。3.2 FAISS索引构建与向量化存储向量检索性能取决于索引类型选择。本项目针对医疗图谱规模约2.1万节点采用IVF-PQ混合索引先用K-means聚类将向量空间划分为100个倒排文件IVF每个聚类内用乘积量化PQ压缩至64字节/向量原768维→64字节内存降低12倍构建脚本build_faiss_index.py核心代码import faiss import numpy as np from transformers import AutoModel, AutoTokenizer # 加载微调后的双塔模型 model AutoModel.from_pretrained(./models/bert_dual_tower/) tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) # 批量编码所有图谱节点示例编码1000个疾病节点 node_texts [心肌梗死 I25.6 急性冠脉综合征的一种..., ...] # 来自Neo4j导出 inputs tokenizer(node_texts, paddingTrue, truncationTrue, max_length64, return_tensorspt) with torch.no_grad(): outputs model(**inputs) vectors outputs.last_hidden_state[:, 0, :].numpy() # [CLS]向量 # 构建IVF-PQ索引 dimension vectors.shape[1] # 768 quantizer faiss.IndexFlatIP(dimension) # 内积相似度 index faiss.IndexIVFPQ(quantizer, dimension, 100, 32, 8) # 100聚类32子向量8bit精度 index.train(vectors) # 训练聚类中心 index.add(vectors) # 添加向量 faiss.write_index(index, faiss_medical.index) # 保存索引文件注意faiss_medical.index需与node_id_mapping.pkl节点ID到索引序号的映射字典一同部署否则召回结果无法对应Neo4j节点。3.3 查询时向量检索与图谱路径融合run_intent_recog_service.bat启动的服务接收HTTP请求执行三阶段融合向量初筛用户问句经Query Tower编码用FAISS搜索Top-50相似节点图谱路径扩展对每个初筛节点在Neo4j中执行2跳遍历如MATCH (n)-[*1..2]-(m) WHERE id(n)$node_id RETURN m获取关联实体重排序按confidence属性加权公式为final_score 0.6 * vector_similarity 0.3 * path_confidence 0.1 * node_degree其中node_degree为节点在图谱中的连接数体现临床重要性。实际请求示例curlcurl -X POST http://localhost:5000/query \ -H Content-Type: application/json \ -d {question: 65岁男性突发胸痛伴大汗心电图ST段抬高} \ -o response.json响应中reasoning_path字段即为可解释的推理链{ answer: 急性ST段抬高型心肌梗死STEMI, reasoning_path: [ {node: 胸痛, type: Symptom, confidence: 0.94}, {node: 大汗, type: Symptom, confidence: 0.87}, {node: ST段抬高, type: ExamFinding, confidence: 0.98}, {node: 急性ST段抬高型心肌梗死, type: Disease, confidence: 0.96} ] }4. 系统集成与生产级部署Flask API封装与性能压测4.1 Flask服务模块化设计与健康检查端点app.py采用分层架构避免单文件臃肿vector_search.py封装FAISS查询与Neo4j路径扩展逻辑graph_service.py提供get_related_nodes(node_id, hops2)等原子操作cache_manager.py用Redis缓存高频问句向量TTL3600秒降低BERT推理负载。关键健康检查端点/health返回结构化状态app.route(/health) def health_check(): # 检查Neo4j连接 try: with driver.session() as session: session.run(RETURN 1).single() neo4j_status UP except Exception as e: neo4j_status fDOWN: {str(e)} # 检查FAISS索引加载 try: index faiss.read_index(faiss_medical.index) faiss_status UP except Exception as e: faiss_status fDOWN: {str(e)} return jsonify({ status: OK if neo4j_status UP and faiss_status UP else DEGRADED, components: { neo4j: neo4j_status, faiss: faiss_status, redis_cache: UP if redis_client.ping() else DOWN } })部署时需配置.env文件NEO4J_URIneo4j://localhost:7687 NEO4J_USERneo4j NEO4J_PASSWORDyour_strong_password FAISS_INDEX_PATH./faiss_medical.index REDIS_URLredis://localhost:6379/04.2 压力测试Locust脚本验证并发能力使用Locust模拟真实问诊流量脚本locustfile.py重点验证单节点QPS极限目标≥120 QPS长尾延迟P95 800ms内存泄漏运行2小时后RSS增长5%。核心测试逻辑from locust import HttpUser, task, between import random class MedicalQAUser(HttpUser): wait_time between(1, 3) # 模拟用户思考时间 task def query_symptom(self): # 从test.csv随机采样问句 questions [ 儿童发热39度伴咳嗽流涕3天, 绝经后阴道出血B超提示子宫内膜增厚, 服用华法林期间INR值升至8.2 ] question random.choice(questions) self.client.post(/query, json{question: question}) # 运行命令locust -f locustfile.py --hosthttp://localhost:5000 --users 200 --spawn-rate 20压测结果需满足指标合格阈值实测值200并发平均响应时间 450ms382msP95延迟 800ms715ms错误率 0.1%0.02%CPU占用 75%68%若P95超限优先优化Neo4j查询——将MATCH (n)-[*1..2]-(m)改为指定关系类型MATCH (n)-[:HAS_SYMPTOM|:TREATS|:CONTRAINDICATED_FOR*1..2]-(m)避免全图扫描。4.3 Docker Compose一键部署与资源隔离docker-compose.yml实现服务解耦version: 3.8 services: web: build: . ports: [5000:5000] environment: - PYTHONUNBUFFERED1 depends_on: [neo4j, redis] deploy: resources: limits: memory: 2G cpus: 1.0 neo4j: image: neo4j:5.16-enterprise volumes: - ./neo4j/data:/data - ./neo4j/plugins:/plugins environment: - NEO4J_AUTHneo4j/your_password - NEO4J_dbms_memory_heap_max__size2g - NEO4J_dbms_memory_pagecache_size1g redis: image: redis:7-alpine command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru ports: [6379:6379]构建镜像前需在Dockerfile中预装关键依赖FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt \ apt-get update apt-get install -y libsm6 libxext6 \ rm -rf /var/lib/apt/lists/* # 预编译FAISS避免容器内编译耗时 RUN pip install faiss-cpu1.7.4 COPY . . CMD [gunicorn, -w, 4, -b, 0.0.0.0:5000, app:app]提示requirements.txt必须锁定版本尤其transformers4.35.2、faiss-cpu1.7.4、neo4j5.16.0避免PyPI版本更新导致兼容性问题。5. 效果调优从test.csv分析bad case并针对性修复5.1 Bad Case归因分析表与修复优先级对test.csv中23个Top-1错误样本进行人工归因分类统计如下错误类型样本数典型案例修复方案优先级实体歧义9“小便黄”被识别为:Drug因数据库中有“黄连”实际应为:Symptom在NER规则中添加否定词过滤“小便黄”→排除含“黄连”“黄芩”等药物名的上下文P0关系缺失7“糖尿病肾病”未关联:Drug节点因train.csv中无该组合标注用neo4j执行MATCH (d:Disease {name:糖尿病肾病}) CREATE (d)-[:TREATS]-(:Drug {name:厄贝沙坦})补全P1向量漂移5“心前区压榨感”与“胸骨后紧缩感”余弦相似度仅0.62应0.85对train.csv中同义症状对做数据增强生成100条变体问句如“胸口像石头压着”P1路径断裂2“肝硬化门脉高压”未链接到:Varices食管胃底静脉曲张在图谱中添加[:CAUSES]-(:Complication)关系并设置confidence: 0.99P0P0级问题必须在上线前修复否则直接影响核心场景准确率。5.2 NER模型增量训练用新样本快速迭代当发现实体歧义问题时无需重训整个模型采用LoRALow-Rank Adaptation微调from peft import get_peft_model, LoraConfig from transformers import AutoModelForTokenClassification base_model AutoModelForTokenClassification.from_pretrained( bert-base-chinese, num_labels12 # 12个医疗实体类型 ) # 配置LoRA仅训练注意力层的低秩矩阵 peft_config LoraConfig( r8, lora_alpha16, target_modules[query, value], # 只适配Q/V矩阵 lora_dropout0.1, biasnone ) model get_peft_model(base_model, peft_config) # 使用新增的50条标注样本训练epochs3batch_size8 trainer.train() model.save_pretrained(./models/ner_lora_finetuned/)LoRA训练仅需原模型15%显存2小时即可完成且能保持原有知识不被覆盖。5.3 向量检索的动态阈值调整技巧FAISS默认返回固定Top-K结果但医疗场景需根据问题确定性动态调整。在vector_search.py中加入置信度校准def search_with_dynamic_k(query_vector, base_k10): # 获取Top-20相似度分数 scores, indices index.search(query_vector.reshape(1, -1), 20) scores scores[0] # 计算分数衰减率若Top-3分数差距0.2说明问题模糊扩大召回 if scores[0] - scores[2] 0.2: k min(25, len(scores)) # 最多召回25个 elif scores[0] - scores[4] 0.05: # Top-5分数密集说明高度确定 k 5 else: k base_k # 重新搜索指定k值 scores, indices index.search(query_vector.reshape(1, -1), k) return scores[0], indices[0] # 调用示例 scores, indices search_with_dynamic_k(user_query_vector)该技巧使模糊问句如“最近不舒服”召回更多候选而明确问句如“阿司匹林禁忌证”精准返回3个核心节点平衡召回率与推理效率。本文还有配套的精品资源点击获取