news 2026/10/26 10:09:03

以疾病为中心的知识图谱问答系统:从Neo4j建模到自然语言查询落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
以疾病为中心的知识图谱问答系统:从Neo4j建模到自然语言查询落地

简介:这是一套基于Python构建的以疾病为中心的医药领域知识图谱问答系统源码,面向医疗信息化研发者、知识图谱与自然语言处理学习者,可用于快速掌握医药知识结构化建模与自动问答的实现路径。压缩包共31个文件,包含8个Python源码文件,覆盖图谱构建、问题分类、问句解析、答案检索、对话交互等核心模块;另有9张PNG架构图与效果图、9个TXT词典及说明文档、1个JSON图谱数据文件、1个PPTX演示文稿,整体约49MB,目录按功能分类清晰。目前已有307人学习浏览。该源码完整呈现了从医学数据准备、知识抽取与图谱构建,到基于规则模板的问答匹配与答案生成的全流程;其中PYC文件便于直接运行测试,JSON数据保留了医药实体关系,配合图片与PPT可快速理解模块划分与问答路由设计,是学习知识图谱落地应用的合适参考。

1. 疾病为中心的医药知识图谱问答系统,解决的不只是“搜答案”

基于疾病为中心的医药领域知识图谱问答系统,核心思路是把医学知识组织成以“疾病”为核心节点的图结构,然后让用户用自然语言提问,系统把问句翻译成图查询并返回确定答案。它和搜索引擎式问答最大的区别在于:搜索给一堆链接,问答给一个明确结论,比如“阿司匹林和布洛芬能一起吃吗”这种复合用药问题,只有顺着图中“药物—相互作用—药物”的边才能推出来。这套方案适合做医疗科普、诊前预问诊、健康管理导诊的团队,也适合拿来当医学NLP或知识图谱课程设计的完整骨架。我最初用它是为了处理医疗科普问答,后来发现最值钱的部分不是单轮检索,而是把语义关系折叠进查询路径里。

2. 图谱怎么建:疾病节点上的实体、关系与数据清洗

2.1 本体设计:以疾病为中心,先定实体类型和关系方向

医学知识图谱最忌讳一上来就堆数据。没有本体约束,实体和关系会越抽越乱,后面问答层根本无法用统一模板去查。常见做法是以“疾病”为hub节点,把症状、药物、检查、科室、并发症、高危人群、饮食注意这些外围实体挂上去。我一般会在设计文档里先画一张关系矩阵,明确每一种关系的主体、客体、方向和反向关系名。

关系名主体客体用途是否必须建反向
临床表现疾病症状查询“XX病有什么症状”否
常用药物疾病药物查询“XX病吃什么药”建议建
禁忌药物药物疾病查询“谁不能用这个药”是
确诊检查疾病检查项目查询“怎么确诊XX病”否
建议科室疾病科室查询“XX病挂什么科”否
可能并发疾病疾病查询并发症是
相互作用药物药物复合用药禁忌查询是

关系方向一旦定下来,后面写查询模板就省事。反向关系缺了,会导致“这个药哪些人禁用”这类从药物出发的查询写不出来。一个可以复制到实践中的标准是:每一条正向关系必须问自己一句——用户会不会从另一头问过来?会,就建反向。

2.2 数据清洗:别名映射与实体对齐是源头工程

构建图谱的第一步不是导入,而是清洗。医药领域的数据来源通常有结构化数据库、公开医学百科、药品说明书,这三类源的实体命名口径完全不一致。比如“阿莫西林”在说明书写“阿莫西林胶囊”,在医学百科写“阿莫西林”,在临床指南里可能写“羟氨苄青霉素”。如果不对齐,同一个药会在图里出现三个节点。

我先维护一份统一的别名映射表,用python做标准化处理:

import csv from collections import defaultdict alias_map = defaultdict(set) # 读取人工整理的别名表,格式: canonical_name, alias with open('drug_alias.tsv', 'r', encoding='utf-8') as f: for row in csv.reader(f, delimiter='\t'): canonical, alias = row[0].strip(), row[1].strip() alias_map[canonical].add(alias) # 清洗时把所有别名归一到标准名 def normalize_entity(entity: str) -> str: entity = entity.strip().lower() entity = entity.replace('片', '').replace('胶囊', '') entity = entity.replace('(', '(').replace(')', ')') for canonical, aliases in alias_map.items(): if entity == canonical or entity in aliases: return canonical return entity

这段代码的逻辑是先把形态词去掉,再做精确别名匹配。参数上要注意,replace('片', '')只适合实体名是“药名+剂型”的拼接场景,不能对症状名做同样处理,否则“偏头痛”会被拆成“偏头”。清洗这一步决定问答准确率的上限,我见过太多项目因为一个实体对齐没做好,最后NLP跑得再好,查询也返回空。

2.3 导入Neo4j:先建唯一约束,再批量写入

把清洗后的三元组装进Neo4j,顺序有讲究。先建唯一约束,再导数据,否则重复写入会让图谱出现同实体的多个节点,问答查出来会有冗余。

from py2neo import Graph, Node, Relationship graph = Graph("bolt://localhost:7687", auth=("neo4j", "password")) # 建唯一约束,保证每个实体只有一个节点 graph.run("CREATE CONSTRAINT IF NOT EXISTS FOR (d:Disease) REQUIRE d.name IS UNIQUE") graph.run("CREATE CONSTRAINT IF NOT EXISTS FOR (m:Medicine) REQUIRE m.name IS UNIQUE") def import_triples(triples): batch_size = 500 for i in range(0, len(triples), batch_size): batch = triples[i:i+batch_size] tx = graph.begin() for subj, rel, obj in batch: s_node = Node("Disease", name=subj) o_node = Node("Medicine", name=obj) # 合并而非创建,避免破坏唯一约束 tx.merge(s_node, "Disease", "name") tx.merge(o_node, "Medicine", "name") tx.merge(Relationship(s_node, rel, o_node)) tx.commit()

这里batch_size设成500,是因为单个事务过大时,Neo4j内存占用会突然拉高,小批量能减少锁竞争。merge而不是create,是这套导入的核心,它按指定的唯一键去匹配已存在的节点,不产生重复。导入完成后,还可以顺手做一次全库校验,统计每个类型节点的数量,防止关系数明显对不上。

3. 问句怎么解析:意图识别与实体的先后顺序

3.1 先判意图再抽实体,比先抽实体再分类更稳

医药问答的问法相对固定,但实体密度很高。比如“我最近老是咳嗽、低烧,会不会是肺炎,应该吃什么药”,这句话里同时出现症状、疾病、药物三类实体,如果先做实体识别,模型会把“肺炎”和“咳嗽”都抽出来,后面反而不知道用户到底要问什么。我的做法是先做意图分类,用模板把问句框架约束住,再在意图框架内去填槽位,这样实体的作用位置是确定的。

意图分类层的设计不需要太重。对医药问答这个封闭域,规则模板加关键词匹配就足够扛住大部分流量。例如“XX病有什么症状”“XX病吃什么药”“XX和XX能一起吃吗”“XX挂什么科”这四类高频意图,可以直接用正则捕获。

import re INTENT_PATTERNS = [ ("symptom_of_disease", r"(.+?)(病|症).*?(什么症状|临床表现|有哪些表现)"), ("medicine_for_disease", r"(.+?)病.*?(吃什么药|用什么药|用药)"), ("drug_interaction", r"(.+?)和(.+?)能.*?(一起吃|同服|合用)"), ("department_for_disease", r"(.+?)病.*?(挂什么科|去哪个科|看什么科)"), ] def parse_intent(question: str) -> tuple: for intent, pattern in INTENT_PATTERNS: m = re.search(pattern, question) if m: return intent, m.groups() return "unknown", ()

正则参数要注意两点。第一,r"(.+?)(病|症)"里的非贪婪匹配保证“肺炎”这种以炎结尾的疾病也能被捕获,而不是吞掉整个句子。第二,意图判定之后还要做槽位解析,从m.groups()里取实体文本,再拿实体文本去对齐图谱中的标准名。这种规则做法在很多开源知识图谱问答项目里仍然保留,原因是医学问句的句式变化有限,规则维护成本远低于标注样本的训练成本。

3.2 实体识别:词典匹配优先,CRF做兜底

规则意图只能给出实体的大概位置,真正把“肺炎”和“咳嗽”对齐到图谱节点,还要一层实体识别。我首先做基于AC自动机的词典匹配,把所有标准实体名和别名构建成词典树,再对问句做最长匹配。优先选择词典,因为药品说明书、医学术语的拼写高度固定,这比训练模型可预期得多。

from ahocorasick import Automaton def build_automaton(entity_list): automaton = Automaton() for idx, entity in enumerate(entity_list): automaton.add_word(entity, (idx, entity)) automaton.make_automaton() return automaton # 返回最长匹配结果,避免“阿莫西林”被切成“阿莫”和“西林” def extract_entities(question: str, automaton) -> list: hits = [] for end_index, (idx, entity) in automaton.iter(question): start_index = end_index - len(entity) + 1 hits.append((start_index, end_index, entity)) # 按起始位置排序并做重叠合并,保留最长片段 hits.sort() return merge_overlap(hits)

这段代码的关键在merge_overlap,重叠匹配时保留覆盖范围更长的那个实体。比如“阿莫西林胶囊”同时会命中“阿莫西林”和“阿莫西林胶囊”,后者才是真正的标准名。这个环节的参数主要是词典规模,一般医药项目实体词典在几千到两万之间,构建自动机仅需几秒,单句匹配在毫秒级。

词典匹配查不到的实体,用CRF兜底。我在实践中给CRF喂的是BIO标签序列,特征模板只用当前字、前一字、后一字的词性和是否为词典词。这个轻量特征配置在疾病、症状实体上的F1大约能到0.86,够用。如果你手上有标注数据,再考虑换成BERT-NER,否则模型训练和推理成本都不合算。

3.3 槽位填充与实体归一化

抽出来的实体是“阿莫西林胶囊”,图谱里的标准名是“阿莫西林”,这中间还要过一遍归一化。把上一步抽取的实体文本重新送进normalize_entity函数,统一成标准名,再去图谱里查询。如果归一化后在图谱中找不到对应节点,说明词典没覆盖,此时我会把问句原样记录下来,归入“未登录实体”集合,定期补充别名表。

4. 从自然语言到Cypher:查询模板、参数和答案生成

4.1 查询模板库:每个意图对应一条参数化Cypher

问答系统的精华在查询模板层。意图识别确定了用户要什么,实体识别确定了对谁查,模板则确定怎么从图里取答案。每条模板都是参数化的Cypher语句,参数位用{entity}占位,执行前再替换。

// 查询某疾病常用药物:先匹配疾病节点,再取指向药物的边 MATCH (d:Disease {name: $disease})-[:常用药物]->(m:Medicine) RETURN m.name AS medicine_name, m.usage AS usage, m.contraindication AS contraindication LIMIT 10 // 查询两种药物能否同服:检查是否存相互作用关系 MATCH (m1:Medicine {name: $drug1}), (m2:Medicine {name: $drug2}) OPTIONAL MATCH (m1)-[r:相互作用]->(m2) RETURN m1.name AS drug1, m2.name AS drug2, CASE WHEN r IS NULL THEN '无已知相互作用' ELSE r.description END AS interaction

第一条模板的LIMIT 10是防爆机制,有些疾病关联的药物动辄几十种,全部返回既慢也不友好。第二条模板是问答系统最能出价值的一类查询,把实体对的相互作用关系返回成一句话,比在搜索框里翻十几个链接强得多。

4.2 填参、执行与异常过滤

模板说清楚了,还要有代码把这些步骤串起来,这里特别注意对用户输入做参数化查询,不要手工拼接字符串。如果直接把问句拼进Cypher,一个包含引号的反引号片段就可能让查询炸掉。

from neo4j import GraphDatabase class KBQAEngine: def __init__(self, uri, user, password): self.driver = GraphDatabase.driver(uri, auth=(user, password)) self.templates = { "medicine_for_disease": ( "MATCH (d:Disease {name: $disease})-[:常用药物]->(m:Medicine) " "RETURN m.name AS medicine_name, m.usage AS usage " "LIMIT 10" ), "drug_interaction": ( "MATCH (m1:Medicine {name: $drug1}), (m2:Medicine {name: $drug2}) " "OPTIONAL MATCH (m1)-[r:相互作用]->(m2) " "RETURN m1.name AS drug1, m2.name AS drug2, " "CASE WHEN r IS NULL THEN '无已知相互作用' ELSE r.description END AS interaction" ), } def answer(self, intent: str, slots: dict) -> dict: if intent not in self.templates: return {"error": "unsupported_intent"} cypher = self.templates[intent] with self.driver.session() as session: result = session.run(cypher, **slots) records = [record.data() for record in result] return {"records": records, "count": len(records)}

slots字典里的键必须和Cypher参数名一一对应,否则Neo4j会抛MissingParameter。我在实际项目里还会加一层参数校验,比如slots里的实体值不得超过20个字符,超过就直接拒绝——医药实体名普遍较短,这能挡住异常输入,也能挡住无意义的扫描查询。参数化查询带来的另一层好处是Neo4j会缓存执行计划,同一个模板重复查询时,速度会比拼接字符串快不少。

4.3 答案生成:直接给结论,而不是给数据

图查询返回的是记录列表,但用户不是来读Cypher结果的。答案生成要按意图把记录整理成自然语言。我的处理方式分三种:属性型答案直接拼句子,列表型答案做排序截断,路径型答案把路径上的节点串成一段话。

以“阿司匹林和布洛芬能一起吃吗”为例,查询结果只有一行,answer逻辑把它渲染成:“阿司匹林与布洛芬存在相互作用:两药同属非甾体抗炎药,合用会增加胃肠道出血风险,不建议同时服用。”如果图里没有这条关系,也不要直接说“没有”,而是返回“暂未收录该药物的相互作用信息”,把未知和否定区分开,这在医疗场景里是底线要求。

5. 避坑与排查:医药知识图谱问答系统的五类高频问题

5.1 实体别名归一化失配,导致答案为空

现象:问“布洛芬缓释胶囊”有结果,问“芬必得”返回空。原因:品牌名、通用名、别名没有同步进词典和归一化表,图里只存在标准名节点。解决:把品牌名统一映射到通用名,在build_automaton前把所有别名加进词典,normalize_entity里做一层“品牌名->通用名”的转换。排查时可以随机抽样问句,把未命中实体的文本全部打印出来,人工补别名表,连续补两周后覆盖率能从70%拉到95%以上。

5.2 关系方向缺失,反向查询全部落空

现象:“这个药哪些人禁用”能查到,“这个病不能用什么药”查不到。原因:图谱只建了“疾病-禁忌药物→药物”的方向,没建反向关系,Cypher无从匹配。解决:在schema设计阶段就为每条关系决定是否建反向,导入时成对写入。排查方法是列出所有关系类型,人工确认是否覆盖了用户最大概率的反向问法。

5.3 长问句多实体同时命中,槽位填充错乱

现象:“我高血压还有糖尿病,能不能吃这种感冒药”被解析成用降压药治疗感冒。原因:意图模板中(.+?)非贪婪捕获了“高血压还有糖尿病”,然后当成单一疾病实体。解决:在槽位填充阶段对捕获文本做二次切分,识别“还有”“以及”“并且”等连接词,把复合实体拆成多个查询条件,分别查询后合并结果。

5.4 术语包含导致匹配过度,答案张冠李戴

现象:问“慢性胃炎”返回的结果中包含“慢性胃炎伴糜烂”的用药,但用户实际问的是一般慢性胃炎。原因:AC自动机最长匹配时,“慢性胃炎”作为更短词被“慢性胃炎伴糜烂”覆盖掉,导致意图对应的实体指向了更具体的病种。解决:在匹配时区分精确匹配和前缀匹配,如果问句文本和词典词完全相等,优先用精确匹配;只有问句中出现了额外修饰词时才用最长匹配。

5.5 图谱膨胀后查询变慢,Cypher超时

现象:数据量从几千条涨到几十万条后,常见查询从几十毫秒涨到几秒,前端直呼卡死。原因:节点没有按类型加索引,MATCH走了全库扫描。解决:为所有高频查询的节点属性创建索引或约束,比如Disease.name和Medicine.name。排查方法是开启Neo4j的PROFILE命令,看查询计划里是不是出现了NodeByLabelScan,出现就是缺索引。

PROFILE MATCH (d:Disease {name: $disease})-[:常用药物]->(m:Medicine) RETURN m.name LIMIT 10

执行后会看到计划算子,如果显示NodeByLabelScan而不是NodeUniqueIndexSeek,那就说明这个属性的索引没建或没生效。

6. 让问答系统真正可用:测试集、指标与一条多跳进阶路径

这套系统跑通后,最容易被忽视的就是验证环节。我一般会准备一个固定问答测试集,按意图分类统计元素,例如“疾病症状”类100问、“用药推荐”类100问、“药物相互作用”类50问,每一条都要有标准答案和对应的查询路径。每次改动图谱或修改模板,就用这个测试集回归一遍,计算三项指标:准确率,即返回的答案与标准答案一致的比例;无答案率,即图谱里确实查不到信息的比例;落空率,即图谱里有但没查出来的比例。准确率尽量往90%以上拉,落空率控制在5%以内。

进阶路径我优先推“多跳问答”。单跳查询只匹配一条边,比如“疾病→药物”,但用户真正关心的问题往往需要两步:比如“糖尿病患者感冒了能吃什么感冒药”,要先从“糖尿病”找到禁忌药物集合,再拿感冒药候选去排除这个集合。这种两条边以上的推理,不能靠堆模板解决,更好的方式是做一个简单的知识图谱路径排序模块,把所有两步可达路径枚举出来,按关系可信度打分。这个阶段仍然不需要上大型语言模型。

我个人习惯是把“未登录实体”和“未覆盖意图”两个日志每天看一眼,坚持三个月,系统会越来越结实。这套方案的边界也很清楚:它只能回答被建模进图谱的知识,对没建立的关系给不出答案。真要把它用在临床辅助场景,必须有人工审核兜底。希望这些实践能帮你在自己的医疗知识图谱问答系统上少走弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 10:03:03

Wordle难度预测工程化方案:从美赛C题到可交付数据产品

简介:本资源为2023年美国大学生数学建模竞赛(MCM)C题特等奖(Outstanding Winner)级别论文全文,面向数学建模参赛学生、毕业设计选题者及数据分析学习者,聚焦Wordle单词游戏的数据建模与难度量化…

作者头像 李华
网站建设 2026/10/26 9:59:36

从Agent协作到模型部署:AI落地实践日报

今天是2026年10月1日,假期第一天。我照例把 AI 圈子里值得看的东西整理成了一份日报。这一期的主题很明确:落地。社区里不再只聊哪个模型又刷了榜单,而是开始认真讨论怎么把 Agent、多智能体、模型部署这些概念变成每天能稳定跑起来的系统。打…

作者头像 李华
网站建设 2026/10/26 9:55:14

java学习笔记(十五)——特殊文件及日志技术

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 9:48:48

Spyglass:projectwdir选项的使用

相关阅读 Spyglasshttps://blog.csdn.net/weixin_45791458/category_12828934.html?spm1001.2014.3001.5482 spyglass中的projectwdir选项用于设置项目目录保存的位置,与大多数选项不同的是它只能在项目文件而不能在sg_shell或GUI界面中进行设置。 使用GUI界面创…

作者头像 李华
网站建设 2026/10/26 9:48:46

Spyglass:跨时钟域同步(循环复用器)

相关阅读 Spyglasshttps://blog.csdn.net/weixin_45791458/category_12828934.html?spm1001.2014.3001.5482 简介 循环复用器方案主要用于数据信号跨时钟域同步,该方案将一个控制信号同步至目标时钟域并用其作为驱动目标触发器的多路复用器的选择信号,…

作者头像 李华
网站建设 2026/10/26 9:48:43

Spyglass:CDC官方Hands-on Training(二)

相关阅读 Spyglasshttps://blog.csdn.net/weixin_45791458/category_12828934.html?spm1001.2014.3001.5482 本文是对Spyglass CDC Hands-on Training中第二个实验的翻译(有删改),Lab文件可以从以下链接获取。 Spyglass CDC Hands-on Trai…

作者头像 李华