恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于疾病为中心的医药知识图谱问答系统源码解析与实战
首页
资讯中心
/
基于疾病为中心的医药知识图谱问答系统源码解析与实战
基于疾病为中心的医药知识图谱问答系统源码解析与实战
发布时间:2026/10/3 21:02:54
简介这份资源是面向医药信息化与知识图谱方向的Python问答系统设计源码适合具备一定Python与自然语言处理基础的学习者、毕业设计开发者及医疗AI入门研究者。项目以疾病为中心构建医药领域知识图谱并实现自动问答与分析服务核心模块涵盖图谱构建、问题分类、问句解析、答案检索及聊天机器人集成可帮助读者理解从数据准备到问答落地的完整链路。资源包共31个文件约49.19MB包含8个Python源码、9个PNG可视化图、9个文本数据文件、1个JSON图谱数据、1个PPT演示文稿及若干编译文件目录中区分了数据、文档与代码模块便于按功能查阅。目前已有307人学习下载。通过该源码读者可获取知识图谱构建与自动问答的工程实现思路参考实体关系抽取、意图识别与答案搜索的代码组织方式并借助可视化图与演示文稿快速把握系统架构适合作为课程设计或项目复现的参考模板。1. 从一份医药知识图谱问答源码说起它到底能跑出什么医药领域做问答最怕的不是模型不够大而是答出来的东西没有依据。你问「二甲双胍能不能和某类药一起吃」通用大模型能给你一段读起来很顺的话但这段话背后的药物相互作用数据从哪来、有没有过期、是不是把两种不同适应症的药混在一起没人说得清。这份「基于疾病为中心的医药领域知识图谱的问答系统设计源码」解决的正是这个问题它把疾病、药物、症状、检查、科室这些实体和它们之间的关系用知识图谱的方式组织起来再在图上做检索和推理最后用自然语言把答案吐出来。整套东西是 Python 写的包含图谱构建、数据入库、问句解析、答案生成几个模块适合做课程设计、毕业设计也适合想入门知识图谱问答的开发者拿来拆。它不追求通用闲聊只盯着医药这个垂直领域把「答得准、答得有据可查」放在第一位。2. 图谱怎么建从疾病中心出发的实体关系设计2.1 为什么以疾病为中心而不是以药物或症状为中心医药知识图谱的建模起点决定了后面查询能有多顺。以药物为中心建图你查「阿司匹林」能拉出一堆适应症和副作用但用户真实提问往往是「我头疼发烧该吃什么」——这是从症状出发找药中间必须经过疾病这个枢纽。以疾病为中心图谱的骨架就是「症状 → 疾病 → 药物」这条主链路再挂上检查、科室、并发症、禁忌这些分支。这样建出来的图天然贴合问诊的思维路径先根据症状锁定可能的疾病再由疾病推荐对应药物同时给出需要做的检查和该挂的科室。源码里实体类型大致分这么几类我按实际建图经验整理成表方便你对照自己的数据做裁剪实体类型典型属性在问答里的作用疾病名称、别名、简介、发病部位图谱核心节点连接症状与药物症状名称、所属系统用户输入入口用于疾病匹配药物名称、通用名、剂型治疗推荐结果检查名称、检查类型辅助诊断建议科室名称、上级科室就医引导食物名称、禁忌类型忌口提示关系类型主要有疾病-有症状-症状、疾病-用药物-药物、疾病-需检查-检查、疾病-就诊科室-科室、疾病-并发症-疾病、疾病-忌吃-食物。这套 schema 不算复杂但覆盖了常见问诊场景。你要做扩展比如加「药物-相互作用-药物」直接在关系表里加一行就行不用动查询主逻辑。2.2 数据从哪来怎么清洗成三元组源码本身不带完整数据集这是这类项目的常态——医药数据有版权和合规问题作者一般只给结构和少量样例。我一般会从公开的医药百科类页面、药品说明书结构化字段里抽数据用爬虫或人工整理成 CSV。清洗环节最关键的是实体对齐同一个疾病在不同来源里可能叫「2型糖尿病」「II型糖尿病」「成人发病型糖尿病」得先归一到一个标准名否则图谱里会出现多个孤立节点查询时匹配不上。下面这段是实体归一化的常见写法用别名表把各种叫法映射到标准名# entity_normalize.py # 把不同来源的实体名称统一到标准名避免图谱出现重复节点 import pandas as pd # 别名映射表key 是各种叫法value 是标准名 ALIAS_MAP { 2型糖尿病: 2型糖尿病, II型糖尿病: 2型糖尿病, 成人发病型糖尿病: 2型糖尿病, 非胰岛素依赖型糖尿病: 2型糖尿病, } def normalize(name: str) - str: 输入原始实体名返回标准名没有映射就返回原名 name name.strip().replace( , ) return ALIAS_MAP.get(name, name) def clean_triples(csv_path: str) - pd.DataFrame: 读取原始三元组 CSV对头尾实体做归一化 df pd.read_csv(csv_path) # 假设列名为 head, relation, tail df[head] df[head].apply(normalize) df[tail] df[tail].apply(normalize) # 去掉自环和空值 df df[df[head] ! df[tail]] df df.dropna(subset[head, relation, tail]) return df if __name__ __main__: triples clean_triples(raw_triples.csv) triples.to_csv(clean_triples.csv, indexFalse) print(f清洗后三元组数量: {len(triples)})这段代码的逻辑很直白先定义别名映射再对每条三元组的头实体和尾实体分别归一化最后过滤掉自环自己指向自己和空值。参数上ALIAS_MAP需要你根据自己的数据源补充映射越全图谱的连通性越好。clean_triples里的列名head/relation/tail要和你的 CSV 对齐不一致就改读取逻辑。跑完输出clean_triples.csv这就是入库前的干净数据。提示别名表不要一次性写死建议单独维护一个 CSV方便后续增补。医药实体别名极多靠代码里硬编码迟早会漏。2.3 用 Neo4j 建图节点、关系与索引数据清洗完下一步是入库。源码默认用 Neo4j 做图存储这也是知识图谱项目里最常见的选型——Cypher 查询写起来直观可视化也方便调试。建图分两步先建约束和索引再批量导入三元组。# build_graph.py # 把清洗后的三元组导入 Neo4j from neo4j import GraphDatabase import pandas as pd URI bolt://localhost:7687 AUTH (neo4j, your_password) # 改成你自己的密码 driver GraphDatabase.driver(URI, authAUTH) def create_constraint(tx): 给疾病、药物等核心实体建唯一约束防止重复节点 for label in [Disease, Drug, Symptom, Check, Department, Food]: tx.run(fCREATE CONSTRAINT IF NOT EXISTS FOR (n:{label}) REQUIRE n.name IS UNIQUE) def insert_triple(tx, head, relation, tail, head_label, tail_label): 插入一条三元组动态指定节点标签 query ( fMERGE (a:{head_label} {{name: $head}}) fMERGE (b:{tail_label} {{name: $tail}}) fMERGE (a)-[r:{relation}]-(b) ) tx.run(query, headhead, tailtail) def load_data(csv_path: str): df pd.read_csv(csv_path) with driver.session() as session: session.execute_write(create_constraint) for _, row in df.iterrows(): # 这里需要你根据关系类型推断头尾标签示例里简化处理 session.execute_write( insert_triple, row[head], row[relation], row[tail], row.get(head_label, Entity), row.get(tail_label, Entity), ) if __name__ __main__: load_data(clean_triples.csv) print(图谱导入完成)逻辑说明create_constraint给每类实体加唯一约束这是防重复的关键不加的话同一个疾病会被反复 MERGE 成多个节点。insert_triple用 MERGE 而不是 CREATE保证节点和关系已存在时不重复创建。参数上URI和AUTH要换成你本地 Neo4j 的地址和密码head_label/tail_label在示例里做了简化实际项目里你应该根据关系类型建一张映射表比如「有症状」关系的头是 Disease、尾是 Symptom这样节点标签才准确。导入完成后在 Neo4j Browser 里跑一句MATCH (n) RETURN count(n)看节点总数再跑MATCH ()-[r]-() RETURN count(r)看关系数两个数字对得上清洗后的三元组规模就说明入库没问题。3. 问句怎么解析从自然语言到图谱查询3.1 意图识别与实体抽取的轻量方案图谱建好了用户不会用 Cypher 提问他说的是「糖尿病有什么症状」。这一层要做两件事判断他问的是哪类问题问症状、问药物、问检查、问科室以及从句子里抽出实体「糖尿病」。源码里用的是基于规则加词典的轻量方案没有上深度学习模型好处是依赖少、跑得快、可解释。意图识别靠关键词匹配句子里出现「症状」「表现」就归为问症状出现「吃什么药」「用药」归为问药物出现「检查」「化验」归为问检查。实体抽取靠词典匹配把图谱里所有疾病名、药物名加载成一个词典用最大正向匹配扫一遍句子命中的就是实体。这套方案在垂直领域够用因为医药问句的句式相对固定不像开放域那么发散。# question_parser.py # 轻量问句解析意图识别 实体抽取 import jieba # 意图关键词表 INTENT_KEYWORDS { symptom: [症状, 表现, 有什么反应], drug: [吃什么药, 用药, 药物, 怎么治], check: [检查, 化验, 做什么检查], department: [挂什么科, 科室, 看哪个科], food: [忌口, 不能吃, 饮食], } def load_entity_dict(path: str) - set: 从文件加载实体词典每行一个实体名 with open(path, encodingutf-8) as f: return {line.strip() for line in f if line.strip()} def detect_intent(question: str) - str: 返回意图标签匹配不到就返回 unknown for intent, keywords in INTENT_KEYWORDS.items(): if any(kw in question for kw in keywords): return intent return unknown def extract_entity(question: str, entity_dict: set) - list: 用 jieba 分词后匹配词典返回命中的实体列表 words jieba.lcut(question) return [w for w in words if w in entity_dict] if __name__ __main__: entities load_entity_dict(disease_dict.txt) q 糖尿病有什么症状 print(意图:, detect_intent(q)) print(实体:, extract_entity(q, entities))逻辑说明detect_intent遍历意图关键词表命中即返回顺序上把更具体的意图放前面能减少误判。extract_entity先用 jieba 分词再拿词去词典里比对。参数上disease_dict.txt需要你从图谱里导出所有疾病名生成一行一个。这套方案的天花板在于实体名必须和词典完全一致用户说「糖尿病」能中说「糖病」就中不了所以词典要尽量覆盖别名——这也是前面实体归一化要做的原因。3.2 把意图和实体翻译成 Cypher解析出意图和实体后要把它映射成图谱查询。每种意图对应一条 Cypher 模板实体填进去执行。这一步是问答系统的核心模板写得好不好直接决定答案质量。# query_template.py # 意图到 Cypher 模板的映射 CYPHER_TEMPLATES { symptom: ( MATCH (d:Disease {{name: $entity}})-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name AS answer ), drug: ( MATCH (d:Disease {{name: $entity}})-[:USE_DRUG]-(m:Drug) RETURN m.name AS answer ), check: ( MATCH (d:Disease {{name: $entity}})-[:NEED_CHECK]-(c:Check) RETURN c.name AS answer ), department: ( MATCH (d:Disease {{name: $entity}})-[:BELONG_TO]-(dept:Department) RETURN dept.name AS answer ), } def build_query(intent: str, entity: str): 根据意图取模板返回 (cypher, params) template CYPHER_TEMPLATES.get(intent) if not template: return None, None return template, {entity: entity} def run_query(driver, intent: str, entity: str) - list: cypher, params build_query(intent, entity) if not cypher: return [] with driver.session() as session: result session.run(cypher, params) return [record[answer] for record in result]逻辑说明模板里用$entity做参数占位避免字符串拼接带来的注入风险也方便 Neo4j 缓存执行计划。build_query负责取模板和组装参数run_query执行并收集结果。参数上关系名HAS_SYMPTOM、USE_DRUG这些必须和你建图时用的关系名完全一致大小写敏感写错了查出来就是空。这里只列了四种意图实际项目里可以继续加比如「并发症」对应COMPLICATION关系。注意Cypher 模板里的关系名和节点标签是硬编码的改图谱 schema 时一定要同步改这里否则会出现「图谱里有数据但查不出来」的玄学问题。3.3 答案生成把查询结果拼成一句人话查到结果后直接返回一个列表太生硬用户要的是句子。答案生成这层做模板填充根据意图选一个句式把实体和结果填进去。比如问症状模板是「{entity}的常见症状包括{answers}」问药物模板是「针对{entity}常用药物有{answers}」。结果为空时给一句兜底话术比如「暂时没有查到{entity}的相关信息请确认疾病名称是否正确」。# answer_generator.py # 把查询结果拼成自然语言答案 ANSWER_TEMPLATES { symptom: {entity}的常见症状包括{answers}。, drug: 针对{entity}常用药物有{answers}。, check: {entity}通常需要做这些检查{answers}。, department: {entity}建议就诊科室{answers}。, } def generate_answer(intent: str, entity: str, results: list) - str: if not results: return f暂时没有查到{entity}的相关信息请确认疾病名称是否正确。 template ANSWER_TEMPLATES.get(intent, {entity}的相关信息{answers}。) return template.format(entityentity, answers、.join(results))逻辑说明generate_answer先判空再取模板填充。、.join(results)把列表拼成顿号分隔的字符串读起来更自然。参数上模板句式可以按你的语气调整但要注意别把「症状」和「药物」的模板搞混否则会出现「糖尿病的常见症状包括二甲双胍」这种翻车答案。到这一步一条完整的问答链路就通了输入问句 → 解析意图和实体 → 查图谱 → 生成答案。4. 避坑与排查跑这套源码最容易卡住的五个地方4.1 图谱导入后查询为空现象数据明明导进去了Neo4j Browser 里MATCH (n) RETURN n能看到节点但问答查出来永远是空列表。原因九成是关系名或节点标签大小写不一致。建图时写的是HAS_SYMPTOM查询模板里写成has_symptomCypher 匹配不到或者建图时节点标签是Disease查询时写成disease。解决在 Neo4j Browser 里跑CALL db.labels()看实际标签跑CALL db.relationshipTypes()看实际关系名拿这两个结果去核对查询模板逐字对齐。建议把标签和关系名抽成常量建图和查询共用一份从根上杜绝不一致。4.2 实体抽取匹配不到现象用户问「糖尿病有什么症状」意图识别对了但实体列表是空的导致查询没执行。原因jieba 分词把「糖尿病」切成了「糖尿」和「病」或者词典里存的是「2型糖尿病」而用户说的是「糖尿病」粒度对不上。解决把图谱里所有实体名和别名都加进词典包括简称和全称对 jieba 加载自定义词典jieba.load_userdict(entity_dict.txt)让分词器认识这些专业词。如果还不行退一步用子串匹配兜底遍历词典看哪个实体名是问句的子串命中就取。4.3 Neo4j 连接报认证失败现象跑导入脚本时报AuthError或Unauthorized连不上数据库。原因密码不对或者 Neo4j 版本和驱动版本不匹配。Neo4j 4.x 和 5.x 的默认认证方式有差异老驱动连新库容易出问题。解决先确认 Neo4j 服务起来了浏览器能打开http://localhost:7474再用neo4j/你的密码在 Browser 里登录一次确认密码正确然后检查pip show neo4j的驱动版本5.x 库建议用 5.x 驱动。密码忘了就在 Neo4j 里执行ALTER USER neo4j SET PASSWORD 新密码重置。4.4 答案模板串味现象问「糖尿病吃什么药」答出来是「糖尿病的常见症状包括二甲双胍」。原因意图识别错了把问药物识别成了问症状。关键词表里「吃什么药」和「症状」有重叠或者匹配顺序不对。解决调整INTENT_KEYWORDS的遍历顺序把更具体、更长的关键词放前面或者改成打分制每个意图算命中关键词数量取最高分。另外模板和意图的映射要单独测一遍写个单元测试把每种意图跑一次看输出句式对不对。4.5 数据量一大就慢现象图谱几千个节点时查询很快上到几万、几十万节点后问答响应明显变慢。原因没建索引每次查询都在全图扫描或者 Cypher 模板写得太宽泛一次拉回大量无关节点。解决给常用查询属性建索引CREATE INDEX FOR (d:Disease) ON (d.name)疾病名、药物名这些高频查询字段都要建。Cypher 里尽量用LIMIT限制返回条数答案生成也不需要几百条结果取前十条足够。如果还慢考虑把热点查询结果做缓存同一实体同一意图短时间内直接返回缓存。5. 进阶玩法把问答准确率再往上抬一截5.1 用同义词扩展提升召回轻量方案最大的短板是「换个说法就查不到」。用户说「消渴症」词典里只有「糖尿病」匹配就断了。解决办法是维护一张同义词表在实体抽取阶段做扩展命中「消渴症」时同时把「糖尿病」也加进候选实体查询时两个都试哪个有结果用哪个。# synonym_expand.py # 同义词扩展把用户说法映射到图谱标准实体 SYNONYM_MAP { 消渴症: 糖尿病, 糖病: 糖尿病, 高血压病: 高血压, } def expand_entities(entities: list) - list: 对抽取到的实体做同义词扩展去重后返回 expanded set(entities) for e in entities: if e in SYNONYM_MAP: expanded.add(SYNONYM_MAP[e]) return list(expanded)逻辑说明expand_entities在原有实体基础上追加同义词映射后的标准名用 set 去重。参数上SYNONYM_MAP要持续维护可以从用户实际提问日志里挖——把查不到结果的问句收集起来人工标注同义词反哺这张表。这是提升召回最划算的投入比换模型见效快。5.2 多跳查询从症状反推疾病前面讲的都是「已知疾病查属性」但真实场景里用户常常是「我头疼发烧可能是什么病」——这是从症状反推疾病需要多跳查询。Cypher 里反向匹配就行# 症状反推疾病 REVERSE_TEMPLATE ( MATCH (s:Symptom)-[:HAS_SYMPTOM]-(d:Disease) WHERE s.name IN $symptoms RETURN d.name AS disease, count(s) AS match_count ORDER BY match_count DESC LIMIT 5 )逻辑说明这条查询找出所有包含给定症状的疾病按命中症状数量降序排列取前五个作为候选。match_count越高说明这个疾病和用户描述越吻合。参数上$symptoms是症状实体列表需要你先从问句里抽出所有症状。这个功能实用性强但要注意别把常见症状比如「发热」权重给太高否则什么病都能匹配上可以给症状加个逆文档频率权重越罕见的症状权重越高。5.3 验证问答效果准备一份测试集改完代码怎么知道有没有变好拍脑袋不行得有测试集。我一般会手工构造 50 到 100 条问答对覆盖每种意图每条标注标准答案然后跑脚本算准确率。测试维度样本量建议判定标准意图识别每类意图 10 条意图标签完全正确实体抽取每类意图 10 条实体列表包含标准实体答案正确性每类意图 10 条答案与标准答案语义一致空结果兜底10 条无结果时返回兜底话术跑测试的脚本很简单读测试集逐条走完整链路比对结果统计准确率。每次改完意图关键词或 Cypher 模板都跑一遍看准确率是涨是跌。这套习惯我是被坑出来的——有次改了个关键词顺序问药物的准确率从 90% 掉到 60%没测试集根本发现不了上线后被用户问「糖尿病吃什么药」答成症状那叫一个尴尬。从那以后我每次动解析逻辑都强制先跑一遍测试集再提交。希望这份源码和这些踩坑记录能帮你少走点弯路。本文还有配套的精品资源点击获取