☰
医学知识图谱问答系统源码解析:基于Neo4j与Python的完整实践
2026/10/3 8:52:42 网站建设 项目流程

简介:这套源码是面向医学信息处理场景的Python知识图谱问答系统,适合有Python基础、关注人工智能医学应用的开发者,解决医学知识检索效率与专业查询门槛问题。资源包共31个文件,以Python源码为核心,覆盖知识图谱构建、问题分类、意图解析、答案检索等模块,并搭配医学数据、示例图片、说明文档与演示文稿,约49.19MB,已有176人学习下载。

项目以build_medicalgraph.py构建知识图谱,question_parser.py解析查询意图,answer_search.py完成答案检索,整合为完整问答链路;数据目录内置医学术语与实体关系,便于复现与二次开发。下载即可获得全部源码、配套医学数据、readme文档、运行效果图与PPT汇报材料,适合中高级开发者系统学习。

1. 医学知识图谱问答系统:先从一份能跑的 Python 源码说起

我最初接触这份源码,是因为要给一个医疗信息检索的 demo 找骨架。翻完chatbot_graph.py和build_medicalgraph.py后发现,它并不是那种只给界面、逻辑全靠猜的玩具项目——从数据准备、实体词典,到 Neo4j 图谱构建、问题意图分类,再到 Cypher 查询和答案组织,整条链路是完整闭环的。也就是说,你拿到的是「上传原始医疗数据 → 构建医学知识图谱 → 跑起一个能对话的问答机器人」的全套工程代码,而不是某个孤立算法片段。这套东西对两类人最有用:一类是想在课程设计或简历项目里落地知识图谱问答的在校生,另一类是刚接触医学信息处理、想快速验证 Neo4j + Python 技术栈的开发者。下面我按自己拆项目的习惯,从数据怎么来、图谱怎么建、问答链路怎么走,到部署和踩坑,一条条给你讲透。

2. 先看数据怎么来:prepare_data、data 与词典文件的角色

2.1 原始数据形态:从 JSON 到词典文件

打开data目录能看到的是一批 txt 词典,包括disease.txt、drug.txt、food.txt、producer.txt、symptom.txt、check.txt、department.txt。这些文件本质上就是实体词典,每条记录对应一个医学实体名,比如疾病名称、药品名称、检查项目、所属科室等。在问答系统的 pipeline 里,它们承担两个任务:一是用于构建知识图谱时的实体节点去重,二是用于question_classifier.py做问题实体抽取时的匹配源。

再看prepare_data目录,里面是原始数据的准备逻辑。常见做法是先抓取或整理出结构化的医疗数据,再写成中间 JSON 文件。代码包里medical.json就是这个中间产物,它把实体属性、实体间关系组织成了统一的键值结构。build_data.py和data_spider.py的分工很清晰:后者负责从公开数据源采集,前者负责清洗和结构化。我一般建议直接在build_data.py里改数据源路径,因为它把解析逻辑收口在一个地方,方便后续维护。

2.2 数据清洗时的候选生成与最大匹配

max_cut.py是整个数据准备链路里容易被忽略但很关键的文件。它的作用是对文本做基于词典的最大正向匹配分词。为什么要自己做分词而不是直接上 jieba?因为在医学场景下,通用分词器经常把「过敏性鼻炎」切成「过敏性」和「鼻炎」,而医学实体要求全词匹配,否则图谱里的节点就对不上。max_cut.py实现的是正向最大匹配算法,逻辑提炼出来就是:

def max_forward_cut(sentence, word_dict): # 正向最大匹配:从句首开始,按最大词长优先匹配 result = [] i = 0 max_len = 5 # 根据词典中最长实体长度调整,一般为 5~8 while i < len(sentence): matched = False # 从当前指针位置,由长到短尝试匹配 for j in range(max_len, 0, -1): word = sentence[i:i + j] if word in word_dict: result.append(word) i += j matched = True break if not matched: # 词典中找不到,按单字切分并跳过 result.append(sentence[i]) i += 1 return result

这里max_len是在性能和精度之间的折中:设得太大,短句匹配会做很多无效尝试;设得太小,长实体永远匹配不上。医学实体里「冠状动脉粥样硬化性心脏病」这类名称很长,我一般会把max_len调到 8 附近。词典的加载方式也值得注意——要转成 set 而不是 list,因为 set 的成员判断是 O(1),在data_spider.py抓取大量文本时会明显降低耗时。实际跑数据准备时,建议先小批量跑一遍,把分词结果打印出来看,确认没有把实体拦腰切断后再全量执行。

2.3 实体与关系的结构化输出

数据准备阶段的最终产物是实体表和关系表。实体表涵盖疾病、症状、药物、食物、检查、科室、药品生产商这些类型;关系表则定义了疾病与症状的「表现为」关系、疾病与药物的「治疗用药」关系、疾病与科室的「所属科室」关系、疾病与食物的「宜吃/忌吃」关系等。这些在medical.json中都能找到对应字段。构建图谱时,build_medicalgraph.py会读取这个 JSON,在 Neo4j 里为每个实体创建节点,为每个关系创建边。

一句话总结:数据准备不是杂活,它决定了图谱的边界和问答的精度。后面所有意图识别、实体匹配、查询模板,全部依赖这一步产出的词典和 JSON。

3. 构建医学知识图谱:build_medicalgraph.py 的节点与关系设计

3.1 为什么选 Neo4j 而不是关系型数据库

知识图谱问答的查询模式是「给定实体,找多跳关系」,比如用户问「高血压应该注意什么饮食」,翻译成图谱操作就是:找到“高血压”这个疾病节点,沿着「忌吃」边找到食物节点,再返回这些食物名称。这类多跳查询如果用 MySQL 实现,要么写一堆 JOIN,要么在应用层递归查,性能和代码复杂度都很糟糕。Neo4j 的 Cypher 查询语言天然支持多跳模式匹配,一个MATCH (d:Disease)-[:忌吃]->(f:Food) WHERE d.name='高血压' RETURN f.name就搞定了。这也是这份源码选 Neo4j 的原因。

3.2 建图脚本核心逻辑与 Cypher 写库

build_medicalgraph.py的主干逻辑可以分成三步:连接 Neo4j、清空旧数据、循环写入节点和关系。核心片段如下:

from py2neo import Graph, Node, Relationship # 连接 Neo4j,默认 bolt 端口号 7687,账号密码按本地环境改 graph = Graph("http://localhost:7474", auth=("neo4j", "123456")) # 清空图数据库,避免重复导入产生脏数据 graph.run("MATCH (n) DETACH DELETE n") # 以疾病节点为例,将 medical.json 中的实体写入图库 for disease_item in medical_data["disease_list"]: disease_node = Node("Disease", name=disease_item["name"], desc=disease_item.get("desc", ""), category=disease_item.get("category", "")) graph.create(disease_node) # 处理“常用药品”关系,目标节点为 Drug 类型 for drug_name in disease_item.get("drug_list", []): drug_node = graph.nodes.match("Drug", name=drug_name).first() if drug_node is None: drug_node = Node("Drug", name=drug_name) graph.create(drug_node) rel = Relationship(disease_node, "治疗用药", drug_node) graph.create(rel)

这段代码里有几个工程细节:graph.nodes.match(...).first()是典型的「先查后建」写法,避免重复创建同名节点;get方法带默认值,保证缺失字段不会让脚本崩溃。写入关系前先确认两个节点都存在,否则 Neo4j 会报「关系端点缺失」的错误。对于食品、检查、科室这些节点,逻辑完全一致,区别仅在节点标签和关系类型上。

节点标签建议统一用英文首字母大写形式,比如Disease、Drug、Food、Check,关系用中文描述,比如治疗用药、宜吃、忌吃、所属科室。这种混搭的好处是:查询语句里能一眼看出边的业务含义,同时节点类型保持稳定。建完图后,在 Neo4j Browser 里执行MATCH (n:Disease) RETURN n LIMIT 25,如果能看到节点和关系正确渲染,图谱构建就成功了。

3.3 图谱结构对问答的支撑意义

图谱结构的质量直接决定问答系统能回答什么。比如用户问「肺炎有哪些症状」,系统需要先从Disease节点匹配到“肺炎”,再沿表现为边找到Symptom节点。如果图谱里这个关系缺失或方向反了,答案自然为空。此外,图谱中同一实体的别名问题也值得留意,比如「乙肝」和「乙型肝炎」,如果不同时写入图谱,用户用「乙肝」提问时就匹配不到。代码包里没有单独做实体对齐,实际使用时可以在build_data.py阶段人工维护一份别名映射表,把同义实体归一化后再写入图谱。

4. 问答链路拆解:question_classifier、question_parser 与 answer_search

4.1 问题分类器:怎么判断用户想问什么

question_classifier.py做的事,是把用户的自然语言问题归类到预设意图上。这份源码的意图分类采用基于特征词表的规则方法,而不是深度学习模型。原因很现实:医学问答的意图类别相对固定,规则方法可控、可解释、不依赖训练数据。项目里定义的意图大致可以分成这么几类:

意图类别用户问题示例特征词
疾病症状查询肺炎有什么症状症状、表现、临床
治疗用药查询高血压吃什么药药、治疗、用药
宜吃食物查询糖尿病适合吃什么宜吃、饮食、吃什么
忌吃食物查询感冒不能吃什么忌吃、不能吃、避免
检查项目查询乙肝需要做什么检查检查、检测、查
所属科室查询骨折挂什么科挂科、科室、门诊
疾病定义查询什么是冠心病什么是、定义、介绍

特征词表是硬编码在question_classifier.py里的,每个意图对应一组关键词。分类时遍历所有意图,计算问题文本命中的特征词数量,取命中最多的作为分类结果。如果最高分出现并列,就按优先级取前面的意图,这个优先级其实就是特征词的置信度排序。这种方案虽然简单,但在限定领域内准确率相当高,跑通 demo 完全够用。

4.2 问题解析器:用正则从问题里抠实体

question_parser.py负责两件事:第一,从问题文本中抽取出医学实体;第二,根据意图和实体,生成对应的 Cypher 查询语句。实体抽取的核心是正则匹配,代码结构大概是这样的:

import re def extract_entity(self, question, word_dict): # 按词典中最长实体优先的原则,逐个尝试匹配 for word in sorted(word_dict, key=lambda x: len(x), reverse=True): if re.search(word, question): return word return None

这里有个关键点:实体匹配用的是「最长优先」策略,而不是「最先出现」策略。比如问题「病毒性肺炎吃什么药」,词典里同时有「肺炎」和「病毒性肺炎」,必须优先匹配后者,否则后面 Cypher 查询会定位到错误节点。word_dict的来源就是前面data目录下的那些 txt 文件和medical.json里的实体集合。实际调试时,最常遇到的现象是「实体没抽出来」,这时候优先怀疑词典里没有收录该词,其次才是正则写法问题。

生成 Cypher 的部分,是根据意图和实体拼接查询语句。以「疾病症状查询」和「治疗用药查询」为例:

# 根据意图和实体拼装不同的 Cypher 模板 if question_type == "disease_symptom": cypher = "MATCH (d:Disease)-[:表现为]->(s:Symptom) " \ "WHERE d.name='{entity}' RETURN s.name".format(entity=entity) elif question_type == "disease_drug": cypher = "MATCH (d:Disease)-[:治疗用药]->(dr:Drug) " \ "WHERE d.name='{entity}' RETURN dr.name".format(entity=entity)

你可能注意到了,Cypher 是用字符串拼接出来的,存在注入风险。但由于这个系统的输入是终端命令行,且实体来自可控词典,实际风险不大。如果你要接 Web 接口,建议改成参数化查询,比如py2neo的graph.run(cypher, entity=entity)写法,让实体作为参数传入,而不是拼进字符串。

4.3 答案搜索器:把查询结果转成自然语言答复

answer_search.py是查询链路的最后一环。它从question_parser拿到 Cypher,执行查询得到一组节点,再把这些节点包装成用户能读懂的话。比如查询结果是["布洛芬", "对乙酰氨基酚"],answer_search.py会根据意图类型,在结果前补上合适的引导语:「根据您的查询,常用的治疗药物包括:布洛芬、对乙酰氨基酚」。

多结果合并时的去重值得留意。Neo4j 的MATCH返回结果可能包含重复项,特别是通过多路径匹配到同一节点时,必须用set去重后再拼装答案,否则用户会看到重复的药名。此外,这种基于模板的答复方式,本质上没有做推理和排序,所有结果一视同仁地展示。在 demo 场景没问题,如果要接近临床可用,还需要按证据强度或频次给结果排序,那属于后续优化方向了。

4.4 对话机器人:把三件套串成完整流程

chatbot_graph.py是主入口,它把分类、解析、搜索三件套串起来,形成一个循环对话流程。逻辑核心是:接收用户输入 → 调用question_classifier得到意图 → 调用question_parser得到实体和 Cypher → 调用answer_search得到答案 → 打印给用户。判断循环是否结束时,一般用输入是否为quit或exit来退出。

while True: question = input("用户:") if question.strip() in ("quit", "exit"): break # 分类问题意图,例如 disease_symptom / disease_drug question_type = classifier.classify(question) # 解析实体并生成 Cypher 查询 cypher = parser.get_cypher(question, question_type) # 在图谱中执行查询,生成自然语言答案 answer = searcher.search(cypher) print("助手:", answer)

三个角色之间的数据流很干净:分类器只输出意图字符串,解析器只输出 Cypher,搜索器只处理 Cypher 并返回答案文本。每一层都可以独立替换,比如把规则分类器换成 BERT 分类模型,不会影响下游代码。这种解耦设计,是这份源码里最值得学习的地方。

5. 避坑与排查:四个最容易翻车的真实案例

5.1 Neo4j 连不上:服务没起,或者端口写错

现象:运行build_medicalgraph.py时,报py2neo.errors.ConnectionUnavailable或Unauthorized。
原因:90% 的情况是 Neo4j 服务没启动,或者默认密码没改。另外,Neo4j 4.x 之后默认启用 bolt 端口 7687,但py2neo连接串如果只用http://localhost:7474,某些版本会尝试用 bolt 协议重定向,导致握手失败。
解决:先确认 Neo4j 已启动(浏览器访问http://localhost:7474能看到管理界面),再用py2neo.Graph("bolt://localhost:7687", auth=("neo4j", "你改的密码"))连接。第一次登录 Neo4j 会强制修改默认密码,连接前务必到管理界面把密码改掉。

5.2 查询结果总为空:不是代码问题,是实体没进图谱

现象:问「肺炎有什么症状」,返回空列表,但图谱里明明能看到肺炎节点。
原因:question_parser里的实体匹配用了re.search,而re.search是子串匹配,不是全词匹配。当用户问「大叶性肺炎有什么症状」时,匹配到的实体是「肺炎」而不是「大叶性肺炎」,图谱里没有单独的「肺炎」节点(只有「大叶性肺炎」),于是查询为空。
解决:区分两种处理策略。一种是实体匹配时优先精确匹配,如果整个问题在词典中有完整实体,用完整实体;另一种是图谱里同时建一个「肺炎」的父类节点,把「大叶性肺炎」作为子类。两个方案各有适用场景,demo 阶段我一般用前者,改动最小。

5.3 重复节点多:写库时没做存在性检查

现象:Neo4j Browser 里查MATCH (n:Drug) RETURN count(n),发现同名药物出现了好几十条。
原因:build_medicalgraph.py写入关系前,对新实体节点直接graph.create(),没先查一下这个名称的节点是否已存在。当同一药物出现在多个疾病的「治疗用药」列表里时,就被重复创建。
解决:在创建任何实体前,先graph.nodes.match("Drug", name=drug_name).first()查一遍,存在则复用,不存在才创建。你也可以在 Neo4j 里给节点加唯一约束,比如CREATE CONSTRAINT ON (d:Drug) ASSERT d.name IS UNIQUE,这样重复写入会直接报错,方便提前发现问题。

5.4 中文路径乱码:Windows 下读数据文件失败

现象:运行时日志显示UnicodeDecodeError,或者在 Windows 终端下打开 txt 文件时中文全部乱码。
原因:data目录下的词典文件如果用的是 UTF-8 编码,而 Windows 下open()默认用 GBK 解码,就会出问题。
解决:所有open()调用统一指定encoding="utf-8",比如open("data/disease.txt", "r", encoding="utf-8")。如果你发现文件本身是 GBK 编码,就改用encoding="gbk"。这是一行代码的问题,但不写清楚能让初学者查一下午。

6. 部署与运行验证:从源码到能对话的完整流程

6.1 环境准备与依赖安装

这份源码的运行环境要求不高,Windows、macOS、Linux 都能跑。Python 版本建议 3.6 到 3.8 之间,因为py2neo和pyahocorasick(如果有用到)在更高版本下可能存在兼容问题。依赖的核心包只有两个:py2neo和Flask(如果需要 Web API)。安装命令:

pip install py2neo==2021.2.3 pip install flask

py2neo的版本是个大坑:4.x 之后的 API 与 3.x 差别很大,比如Graph.run()的返回处理方式不同。源码是按 2021.2.3 版写的,如果你用了最新版 5.x,graph.nodes.match()的写法会有兼容性问题。装完依赖后,建议在 Python 里先跑一句from py2neo import Graph验证安装成功,避免后面排查半天才发现是导入失败。

6.2 按顺序执行三件事:准备数据 → 建图谱 → 跑问答

环境就绪后,按顺序执行数据准备、图谱构建和问答启动三个步骤,完整命令如下:

# 1. 进入项目根目录,先看一下数据文件是否存在 ls data/ cat medical.json | head -n 20 # 2. 执行建图脚本(前提是 Neo4j 已启动) python build_medicalgraph.py # 3. 启动问答系统,进入交互模式 python chatbot_graph.py

第二步执行时,如果终端输出了一堆节点创建日志,没有报错,说明建图成功。第三步启动后,终端会进入对话循环。现在你可以用最基础的四类问题测试系统的响应:

  • 症状类:肺炎有什么症状
  • 用药类:高血压吃什么药
  • 饮食类:糖尿病宜吃什么 / 感冒忌吃什么
  • 科室类:骨折挂什么科

如果这几个问题都能返回非空结果,说明整条链路是通的。此时再问一些变体问题,比如「冠心病的临床表现」或「乙肝不能吃什么」,感受一下系统在实体匹配和意图分类上的边界在哪里。

6.3 给新手的一个验证手法:把中间结果打印出来

如果某个问题没返回期望答案,不要急着改代码。我习惯先在chatbot_graph.py的循环里临时加上打印语句,把每一步的中间产物暴露出来:

print("[DEBUG] 意图:", question_type) print("[DEBUG] Cypher:", cypher) print("[DEBUG] 答案原始结果:", answer)

通过这三行,你能快速定位问题出在哪个环节:意图标错了,是分类器的问题;Cypher 里实体为空,是解析器没抽到;Cypher 正确但结果为空,是图谱数据缺失。这套方法我在拆任何知识图谱问答项目时都会用,排查效率远高于直接改查询模板。

6.4 一个值得尝试的进阶改动:接入简单 Web 接口

命令行交互适合验证功能,如果想展示给评审或朋友看,可以包一层极简的 Flask 接口。核心思路是复用chatbot_graph.py里的三个类,只把input()和print()换成 HTTP 请求和 JSON 响应:

from flask import Flask, request, jsonify from chatbot_graph import ChatBotGraph app = Flask(__name__) handler = ChatBotGraph() @app.route("/qa", methods=["POST"]) def qa(): data = request.get_json() question = data.get("question", "") answer = handler.answer(question) # 实际项目中,ChatBotGraph 需要封装 answer 方法 return jsonify({"answer": answer}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)

注意,源码里chatbot_graph.py的交互逻辑是写在while循环里的,直接拿来做 Web 接口需要把单轮会话逻辑抽成一个answer(question)方法,这个改动大概十五分钟能完成,但对「让项目可演示」这个目标帮助极大。从那以后,我每次拿到类似的知识图谱问答项目,都会先把交互流程跑通一遍,再决定是加 Web 层还是做意图扩展。这套「先跑通主线再动手改」的顺序,算是拆了这么多源码项目后最值钱的一条习惯,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询