
简介基于Python的医疗知识图谱自动问答系统源码面向医疗信息化开发者、自然语言处理学习者和知识图谱初学者覆盖从医疗数据清洗、实体关系建模到智能问答展示的完整链路。系统以病症、药物、科室、食物等医学词库为基础借助Neo4j构建知识图谱并利用NLTK/spaCy完成语义解析、实体识别与关系抽取最终通过Flask界面返回答案。压缩包共26个文件包括8个Python核心脚本、8个txt医学词库、2个JSON医疗数据集还有5个XML配置与1张演示截图整体约15.54MB目录按数据准备、图谱构建、问答检索等模块划分便于定位与二次开发。已有184人学习/下载。读者可参照data_spider爬虫、build_medicalgraph建图、question_classifier问句分类、answer_search答案检索等脚本快速复现一个可运行的医疗问答原型也适合作为毕业设计或科研项目起点。1. 基于python的医疗知识图谱自动问答系统源码从实体识别到答案生成的整链路一个真实到有点扎心的场景业务方丢过来一批医疗科普文档要求做一个能回答“高血压患者能不能吃柚子”“头痛挂什么科”这类问题的原型系统。第一反应是上大模型但标注数据没有、GPU资源有限、回答还要可溯源。这个时候“知识图谱模板问答”的路线反而最稳——把医疗实体和关系抽出来放进图数据库问句进去走一遍实体识别和意图分类最后用图查询锁定答案。本标题指向的正是这样一套工程用 Python 完成知识图谱构建、问答检索和答案生成的闭环。对要快速落地原型的后端工程师或者需要完成课设的在校生这套方案的价值在于每个环节都能本地复现跑通后再逐步替换成深度学习模型也不迟。2. 医疗知识图谱自动问答的架构设计与实体抽取方案2.1 医疗问答系统的最小闭环图谱存储、检索、答案生成把一个完整的医疗自动问答系统拆开看可以切成三个相互独立又按序衔接的模块图谱构建模块负责把非结构化的医疗文本转成三元组并写入图数据库问答模块接收用户问句先做实体识别和意图分类再组装成图查询语句答案生成模块拿到查询结果后按不同类型的问题模板生成自然语言回答。实体识别环节可以用一个反直觉的结论来定基调在垂直领域词典匹配的效果不一定比序列标注模型差尤其当实体类型集中在疾病、症状、药品、科室四类时一本覆盖度足够高的词典加上规则修正往往就能达到 90% 以上的准确率。原因并不复杂——医疗实体绝大多数是长度稳定的名词短语歧义主要来自简称和别名这恰好是词典能做好的场景。只有当测试集里频繁出现从未见过的实体时才需要引入真正的命名实体识别模型。2.1.1 核心数据模型设计图数据库的存储模型直接决定查询语句的写法。以常见的病-症-药关系为例节点和边的关系设计如下节点类型示例属性关系类型方向语义Diseasename、department、symptom_summarytreatsDisease - DrugDrugname、usage、dosage、contraindicationtreatsDisease - DrugSymptomname、descriptionhas_symptomDisease - SymptomDepartmentname、location、descriptionbelongs_toDisease - Department实际落地时建议把 does_not_treat禁用关系单独建一类关系不要用属性字段标注否则 Cypher 查询里每个匹配模式都要带额外的 WHERE 条件写起来啰嗦索引利用率也差。2.2 中文医疗文本的实体识别词典与正则结合词法层面的实体识别我常用的做法是双数组字典树加载自定义词典再配合规则处理数字和剂量单位。核心代码骨架如下from pygtrie import Trie class MedicalEntityExtractor: def __init__(self, dict_path: str): self.trie Trie() self.entity_types {} with open(dict_path, r, encodingutf-8) as fp: for line in fp: parts line.strip().split(\t) if len(parts) 2: continue word, etype parts[0], parts[1] self.trie[word] True self.entity_types[word] etype def forward_max_match(self, text: str): entities [] i 0 while i len(text): matched None for j in range(min(len(text), i 12), i, -1): if self.trie.get(text[i:j]): matched text[i:j] break if matched: entities.append((matched, self.entity_types[matched])) i len(matched) else: i 1 return entities正向最大匹配的顺序是从最长候选窗口开始逐个缩短直到在词典中命中窗口上限设成 12 是因为医疗实体的最长常见名不超过 6 个汉字留出余量处理加括号的剂型描述。这里的词典文件格式是每行一个词条用 tab 分隔词语和类型标签。如果词典规模超过十万条pygtrie 的查询耗时仍能稳定在毫秒级不会成为问答接口的性能瓶颈。规则层补充正则用来抓取词典里没有、但格式特征明显的实体比如“每天三次每次两片”里的频次和剂量。正则表达式的编写有一个更容易维护的组织方式——按实体类型分组配置并用优先级控制匹配顺序剂量单位优先于普通名词匹配。3. 构建医疗知识图谱数据清洗、三元组抽取与 Neo4j 导入3.1 清洗策略把非结构化文本变成可入库的三元组拿到原始医疗数据后的第一件事是清洗与结构化。常见的数据形态有两种网页抓取的问答对以及结构化程度不高的科普文章。前者能直接获得「问题-答案」对后者需要先做分句和关键词定位再按规则抽取出 (疾病, 治疗, 药品) 这类关系。清洗动作建议围绕以下几个维度做规则化处理清洗类别具体规则落地效果空白字符去除全半角空格、不间断空格避免同一实体因空格产生重复节点括号统一中文括号全部转半角保留括号内容“阿司匹林肠溶片”不拆成两个实体缩写归一建立别名映射表如“高血压→原发性高血压”规避多词一义导致的图分裂剂量清洗用正则提取“每次 x 片/毫升/克”结构化Drug.dosage属性禁忌识别关键词匹配“禁用、不宜、忌用”后的实体建立does_not_treat关系三元组抽取环节在语料质量稳定时规则和模式匹配的性价比远高于监督学习。可以基于依存句法分析定义模式如果句子中出现“用于治疗”“对...有效”这类触发词则把主语映射为 Drug、宾语映射为 Disease。这里需要注意语境筛选——同样的动词结构出现在否定句里表达的关系正好相反。因此否定词的预处理必须前置否则“不适用”“不能用于”会被错误抽成正向治疗关系。3.1.1 批量写入 Neo4j 的 Cypher 模板与参数控制逐条执行 CREATE 语句的效率过低。生产可用的写法是先用 UNWIND 批量装载配合定期提交事务控制内存占用UNWIND $batch AS row MERGE (d:Disease {name: row.disease}) MERGE (m:Drug {name: row.drug}) MERGE (d)-[r:treats]-(m) SET r.evidence row.source, r.confidence row.scoreMERGE按名称去重避免数据源里同一疾病出现多次时生成重复节点。业务上如果同一个药品在多个数据源中出现row.score可以取图谱融合时候的重合度加权结果。写完后立即执行索引创建保证后续问答查询走索引而不是全表扫描CREATE CONSTRAINT disease_name IF NOT EXISTS ON (d:Disease) ASSERT d.name IS UNIQUE; CREATE CONSTRAINT drug_name IF NOT EXISTS ON (m:Drug) ASSERT m.name IS UNIQUE;3.2 从清洗到入库的自动化方案清洗和入库如果用手工跑数据一多就会出幺蛾子。完整流程可以抽象为一个 Python 类调用入口如下from neo4j import GraphDatabase class KnowledgeGraphBuilder: def __init__(self, uri, user, password, batch_size500): self.driver GraphDatabase.driver(uri, auth(user, password)) self.batch_size batch_size def load_cleaned_data(self, filepath): with open(filepath, r, encodingutf-8) as fp: rows [json.loads(line) for line in fp if line.strip()] for i in range(0, len(rows), self.batch_size): batch rows[i:i self.batch_size] with self.driver.session() as session: session.run( UNWIND $batch AS row MERGE (d:Disease {name: row.disease}) MERGE (m:Drug {name: row.drug}) MERGE (d)-[r:treats]-(m) SET r.evidence row.source , batchbatch )batch_size设为 500 是一个兼顾内存和提交频率的经验值设置过大时单次事务里包含的节点过多Neo4j 服务端容易出现堆内存溢出设置过小则事务提交次数太多总写入耗时明显变长。同样的 PyDriver 连接在导入完以后可以继续复用于在线问答不需要为查询阶段单独建立连接池。4. 自动问答系统的检索实现从问句解析到答案生成4.1 意图分类与槽位填充的轻量实现问答模块的第一层是“理解问题”。真实场景里的问法五花八门但如果只服务垂直医疗知识图谱可以先把问题分成几类症状查询疾病、疾病查询用药、疾病挂号科室、药物禁忌查询。这类封闭域的意图分类完全可以用关键词规则完成既不需要训练语料也能保证线上可解释性。每个意图维护一组槽位关键词用字典映射优先级和槽位名intent_regex { query_drug_by_disease: { keywords: [吃什么药, 用什么药, 怎么治疗, 能治], slot: disease }, query_department_by_disease: { keywords: [挂什么科, 哪个科室, 看什么科], slot: disease } }这里有个工程细节意图命中后槽位不一定恰好落在实体提取的结果里。比如“高血压应该挂什么科”实体提取阶段抽出“高血压”但“挂什么科”未被识别成任何实体。因此槽位填充使用问句原文而不是实体序列提取出的实体只用来确认槽位的实体类型最终生成的查询参数以原文定位为准。4.2 Cypher 模板拼接与相似度兜底检索意图和槽位确定后拼接对应的 Cypher 查询。模板拼接阶段必须使用参数化查询不能直接把用户输入拼进字符串否则图数据库接口就会变成注入入口。参考实现def build_cypher(intent, entity_name): if intent query_drug_by_disease: return ( MATCH (d:Disease {name: $name})-[r:treats]-(m:Drug) RETURN m.name AS drug, r.evidence AS evidence LIMIT 10 ), {name: entity_name} if intent query_department_by_disease: return ( MATCH (d:Disease {name: $name})-[:belongs_to]-(dep:Department) RETURN dep.name AS department ), {name: entity_name}拼查询的本质是根据意图决定图遍历的边类型而不是把整个用户句子视为查询条件。若槽位无法从问句中提取出任何医疗实体就需要走自定义的兜底逻辑——把问句与图谱中的实体名做字符级相似度检索from rapidfuzz import fuzz def fuzzy_find_disease(query, candidate_names, threshold62): best_name, best_score None, 0 for name in candidate_names: score fuzz.partial_ratio(query, name) if score best_score: best_score, best_name score, name return best_name if best_score threshold else Nonepartial_ratio计算的是子串匹配得分适合短问句和实体名存在重叠的情况。阈值调到 62 是为了在漏召回和误召回之间取平衡——小于 60 时大量无关实体混进来高于 70 时“高血压注意事项”这类问法将匹配不到“高血压”。候选集从 Neo4j 的 label 扫描拿到全量实体名如果实体规模超过几十万候选集生成就要改为前缀索引或 n-gram 索引。4.3 答案生成从图查询结果到可读文本拿到查询结果之后不能直接把图数据原样返回。医疗问答的答案需要区分类型组织成模板句比如疾病查询用药的回答格式为“针对[疾病]可以考虑使用[药品]。依据[来源]”。如果查询结果为空则进入答案兜底流程——返回“图谱中暂无相关记录请咨询专业医生”这一步也可以接一个大模型做开放域补全但输出必须附上“仅供参考”的免责声明。模板中可以包含“建议就医”之类的常规提示但不要硬编码医生的判断逻辑体系边界要清晰图谱怎么存就怎么答图谱里没有的信息不进行自由发挥。5. 用 python 运行与调试这套源码环境配置与 zip 包常见坑5.1 解压与目录结构检查标题里的源码是 zip 压缩包形式拿到手后第一件事是完整解压不要直接在压缩包内双击运行。用命令行解压可以避开图形界面下中文文件名乱码的问题unzip medical_qa_system.zip -d medical_qa_system cd medical_qa_system tree -L 2常见源码包的目录一般包含 data原始语料与词典、kg_builder图谱构建脚本、qa_engine问答引擎、serverWeb 接口层和 requirements.txt。没有__init__.py的目录不能被当作 Python 包导入碰到 ModuleNotFoundError 时要先检查这里再检查依赖。5.1.1 解压失败与文件损坏的处理如果解压过程报invalid zip archive: could not find eocd通常说明下载不完整或文件被非正常中断。先查看文件大小是否与发布说明一致再用zip -T测试压缩包完整性zip -T medical_qa_system.zip输出OK才能继续。若提示缺失文件优先重新下载而不是强行修复。zip 文件恢复工具的适用范围很有限只有中央目录损坏而局部条目完好时才有机会。5.2 依赖安装与 Neo4j 连接参数进入源码目录后建议先建虚拟环境再装依赖python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt如果 requirements.txt 缺失或版本不适配至少需要保证以下核心依赖齐全依赖库用途常见坑neo4j连接图数据库版本不匹配导致认证握手失败jieba中文分词不加载自定义词典时医疗实体被拆碎flask / fastapi提供问答 HTTP 接口与 pydantic 版本冲突rapidfuzz相似度检索与 python 版本不兼容时编译失败连接 Neo4j 的配置一般集中在config.py或.env中NEO4J_URI bolt://localhost:7687 NEO4J_USER neo4j NEO4J_PASSWORD your-password本地调试时如果出现failed to establish connection优先检查 Neo4j 服务是否已启动再用cypher-shell验证账号密码是否正确。不要一上来就怀疑源码有问题源码包里内置的可能是 Docker 或本地服务的连接地址实际运行环境不同时修改这三项配置即可。5.2.1 Python 版本兼容性的副作用医疗 NLP 相关依赖对 Python 版本相对挑剔。python 3.10 以下的版本运行某些新版依赖时会报编译错误反之python 3.12 上运行旧版tensorflow或paddlepaddle也大概率失败。如果源码包中的代码使用了旧式写法如collections.Iterable在 python 3.10 以后会触发DeprecationWarning此时改collections.abc.Iterable即可不建议因此降级 Python 主版本。6. 用问句集自动化验证问答效果与图谱迭代问答系统的正确性不能靠人工一条条测应该准备一份带标签的问句测试集写成回归脚本每次改动图谱或检索逻辑后自动跑一遍。问句集可以按意图分类组织每条标记期望的答案实体是否出现在结果中qa_cases [ {question: 高血压吃什么药, expect: [硝苯地平, 氨氯地平], intent: query_drug}, {question: 头痛挂什么科, expect: [神经内科], intent: query_department}, {question: 阿司匹林能治头痛吗, expect: [是], intent: query_drug_effect}, ] def evaluate(qa_cases): hits, total 0, len(qa_cases) for case in qa_cases: result answer_question(case[question]) hit any(exp in result.get(answer, ) for exp in case[expect]) hits int(hit) return hits / total这个脚本既可以在每次修改entity_extractor后给出精确率变化也能在更换 jieba 自定义词典时暴露回归问题。部分问法在图谱构建阶段就注定答不准比如实体提取正确但图谱里没有对应的关系边这类失败需要回到数据侧补三元组而不是在问答模块里强行加规则。图谱的增量更新同样可以用脚本完成每天新增的问句中没有命中的实体累加到一定量后人工审核并补进词典和三元组。这个闭环就是医疗知识图谱自动问答系统源码最有价值的地方——迭代路径清晰每一步都可以度量跑久了以后积累的 error case 还能成为后续引入大模型微调的起始数据。本文还有配套的精品资源点击获取