☰
基于Neo4j的医疗知识图谱问答机器人源码解析与工程实践
2026/10/10 12:30:32 网站建设 项目流程

简介:基于Neo4j图数据库的医疗知识图谱智能问答机器人,是一份由导师认可的高分毕业设计源码包,面向计算机相关专业正在准备毕设、课程设计或期末大作业的学生,也适合需要实战练习知识图谱开发的中高级学习者。项目以医疗领域真实数据为背景,围绕知识图谱构建与智能问答闭环展开,完整覆盖实体关系抽取、图谱数据导入、问题意图分析、CQL查询生成、答案抽取及前端交互展示等环节;代码经过严格调试可直接运行,并附带项目使用说明与超详细注释,便于理解Neo4j图数据库与Python结合的实际开发流程。资源打包为zip压缩包,共36个文件,整体大小15.34MB,包含8个py源码文件、10个txt说明文档、5个pyc缓存文件,以及png/jpg图片、js/css/html等前端素材;目录结构清晰,模块划分合理,方便按需拆解学习,也可直接作为课程设计或期末项目的参考实现。目前已有701人学习下载,适合需要快速上手医疗知识图谱问答系统的进阶学习者。 从这一套源码的实际运行结果来看,医疗知识图谱问答机器人的核心不是模型多聪明,而是把“疾病、症状、科室、药品、食物”这些实体和关系组织成一张能在秒级返回查询结果的图。你拿到的这份Python基于Neo4j图数据库的医疗知识图谱智能问答机器人源码,本质是一条完整的工程建设链路:文本数据入库、规则问句解析、CQL 生成、交互式回复。它适合三类人:正在做毕业设计需要可演示系统的学生;想理解“知识图谱 + 规则问答”怎么落地的后端开发;以及准备做医疗领域垂直对话原型、但不想从零攒数据的产品或算法工程师。下面我按拆解顺序把每条关键路径和实际踩过的坑讲透。

2. 系统拆解:从病历文本到图数据库的建图链路

MedicalChatbots-main ├── build_medicalgraph.py # 建图脚本:实体抽取 + 关系写入 ├── data/ # 原始文本与中间数据 │ └── medical.json # 处理后的结构化病例(示例) ├── dict/ # 自定义词典,配 jieba 使用 ├── question_analysis.py # 问句意图识别与实体抽取 ├── get_cql.py # 根据意图模板生成 CQL 查询 ├── get_answer.py # 执行 CQL 并组装自然语言答案 ├── chat_robot.py # 控制台交互主程序 ├── keyword_template.py # 意图匹配的词典与模板定义 ├── static/ # 前端静态资源(用于 Web 版扩展) └── clear_graph.py # 清空 Neo4j 图数据,方便重建

2.1 数据从哪来:dict 词典与 data 目录的分工

这套源码里最容易被忽略的其实是dict/目录,它直接决定了后续实体识别的上限。项目没有采用深度模型做 NER,而是用 jieba 分词 + 自定义词典,把“慢性胃炎”“十二指肠溃疡”“阿莫西林胶囊”这类医学词汇优先切分成完整实体。我自己跑的时候,第一遍没把dict路径加进jieba.load_userdict(),结果“胃溃疡”被切成了“胃”和“溃疡”,问句解析时实体一个都匹配不上。

胃溃疡 100 nz 十二指肠溃疡 100 nz 阿莫西林胶囊 100 nz 上腹部疼痛 100 nz

data/medical.json是从原始文档整理出来的节点和关系描述,每一条记录包含实体名称、类别、属性。建图脚本读取后执行节点去重,再按实体对 + 关系类型写入边。这里有个细节:如果同一对实体之间有多条同类型关系,Neo4j 默认会重复建边,所以我一般会在写入前用MERGE而不是CREATE,源码里的build_medicalgraph.py已经做了这个处理,但你自己扩展新数据时容易忽略。

2.2 建图脚本的三个关键函数

build_medicalgraph.py的核心逻辑可以拆成三块:连接配置、节点写入、关系写入。

def create_graph(self): # 连接 Neo4j,认证方式根据版本调整 self.graph = Graph("http://localhost:7474", auth=("neo4j", "123456")) self.graph.delete_all() self.create_node() self.create_relationship() def create_node(self): # 先按实体类型建节点 for entity_type, entities in self.medical_data.items(): for entity in entities: cql = f"MERGE (n:{entity_type} {{name: '{entity}'}})" self.graph.run(cql) def create_relationship(self): # 遍历预定义的关系三元组,用 MERGE 写边 for relation in self.relationships: cql = f""" MATCH (a:{relation['head_type']} {{name: '{relation['head']}'}}) MATCH (b:{relation['tail_type']} {{name: '{relation['tail']}'}}) MERGE (a)-[r:{relation['rel_type']}]->(b) """ self.graph.run(cql)

参数说明:head_type和tail_type是节点标签,比如Disease、Symptom、Department;rel_type是关系类型,常见的是belongs_to、has_symptom、recommend_drug。这里建议在写入前先统计实体数量,因为当数据量超过几万条时,逐条RUN会非常慢,源码默认的规模在一千到三千实体之间,逐条执行可以接受,但初学者容易误以为这是性能优化方案。

2.3 为什么用图数据库而不是 MySQL

这个系统放到 MySQL 里也能做:三张表存实体、关系、属性,然后JOIN查询。但你要知道,知识图谱问答的问题类型天然包含多跳查询,比如用户问“胃溃疡应该挂什么科”,链路是Disease -> Department,还隐含着Disease -> Symptom -> Department的间接路径。在关系型数据库里,超过两跳的查询 SQL 会写得非常痛苦,而且每扩展一种关系就要改表结构。

Neo4j 的 Cypher 用MATCH表达路径非常天然,比如“疾病到科室的直接关系”只要一句话:

MATCH (d:Disease {name:'胃溃疡'})-[:belongs_to]->(dept:Department) RETURN dept.name

而且对于图遍历,Neo4j 的复杂度与遍历深度相关,而不是与全表行数强相关。这也是毕业设计答辩时一个很好的亮点:你不用吹算法,直接把“为什么选 Neo4j”讲清楚就能得分。

2.4 建图结果的验证方法

脚本跑完别急着关终端,先做两件事:

# 在 Neo4j Browser 中执行 MATCH (n) RETURN count(n) MATCH (n)-[r]->() RETURN type(r), count(r)

第一句确认节点总量,第二句确认每种关系的数量。我第一次运行就发现has_symptom方向反了——原数据里写的是Symptom <- has_symptom <- Disease,但关系定义写反,导致问答时“胃溃疡有哪些症状”返回空。所以强烈建议建立一张检查表,至少验证三个最常用的查询通路,再进下一环节。

3. 问答链路:问句解析到 CQL 生成,谁负责什么

3.1 question_analysis.py:意图识别与实体抽取的规则核心

question_analysis.py不是机器学习模型,它采用的是“模板 + 关键词”的规则方法。它先对用户输入做 jieba 分词,然后在分词结果中匹配dict里的实体词,比如“胃溃疡”“拉肚子”。同时它维护意图模板,判断用户到底在问“症状”“科室”“药品”还是“饮食建议”。

# keyword_template.py 中的意图模板示例 medicine_template = { "query": ["治疗", "吃什么药", "用什么药", "药"], "entity_type": "Disease", "rel_type": "recommend_drug" }

匹配逻辑是这样的:先遍历所有模板,如果问句里出现模板的触发词,就标记为对应意图;然后从分词结果里抽实体;最后把(实体, 意图)传给get_cql.py。我在实际调试中遇到最多的问题是“触发词太短”,比如单独一个“药”字,用户说“药怎么吃”也会被误判为“推荐药品”,但“怎么吃”其实是用法用量的问题。这种用规则写系统就绕不开误判,所以要给每个模板设优先级,长触发词优先。

3.2 get_cql.py:模板参数如何变成 Cypher

get_cql.py的核心是一个trans_to_cql函数,它接收实体类型、实体名称、关系类型,然后拼出一条 Cypher 查询。这里的拼串必须注意防注入,虽然本地运行威胁不大,但毕业设计评审老师可能会问到,所以我统一改成参数化查询:

def trans_to_cql(question_type, entity, rel_type): # 实体值来自用户输入,必须参数化 cql = [ "MATCH (d:{entity_type} {{name: $name}})-[r:{rel_type}]->(target) " "RETURN target.name" ] return cql[0].format(entity_type=entity["type"], rel_type=rel_type)

执行时使用graph.run(cql, name=entity["name"]),$name占位符由 Neo4j 驱动处理,避免把引号等特殊字符拼进查询。这个习惯我从一个翻车现场学来的:当时词典里有实体叫“幽门螺杆菌(阳性)”,括号和引号直接让 Cypher 语法报错,参数化之后就再没出过这种问题。

3.3 get_answer.py:把查询结果组织成自然语言

这一层做的事情很机械:拿到target.name列表后,按不同意图拼接回复。比如推荐药品的返回结果是多个药名,get_answer.py会拼接成“根据您的提问,建议使用:阿莫西林胶囊、甲硝唑片……”,如果查询结果为空,则回复“暂时没有找到相关信息,请换个说法”。

def answer(self, question, graph): # 返回 (答案字符串, CQL 语句) cql, entity = self.trans_to_cql(question) results = graph.run(cql, name=entity["name"]) answer_text = "、".join([r["target.name"] for r in results]) return answer_text or "暂未收录该问题,请尝试调整症状描述。"

这里有个产品层面的取舍:join用的顿号对中文友好,英文系统则用逗号。如果结果超过 5 个,我一般会截断并提示“还有更多,请进入详情查看”。毕业设计里把这一步做好,演示时用户问“胃炎吃什么药”能看到整齐结果,观感比干巴巴的列表强很多。

3.4 chat_robot.py:主循环与命令行交互

chat_robot.py是整个系统的入口,它维护一个循环,接收输入,依次调用question_analysis、get_cql、get_answer,最后打印结果。源码里还保留了一个“退出”关键词,输入quit或exit就结束。

我建议给这个主循环加一条兜底规则:如果意图识别置信度不高(触发词匹配少于两个),返回“请问您是想了解症状、科室还是用药建议”。这个小改动对毕设答辩特别有用,因为评委经常故意输入无关文本测试系统边界,有兜底回复比直接崩溃要体面得多。

4. 部署与运行:Neo4j 版本选择、Python 依赖和启动顺序

4.1 环境配置与 Neo4j 版本坑

首先,这份源码基于 Python 3.x,建议用 3.8 或 3.9。依赖库主要是py2neo、jieba,以及 Flask(如果目录里的static对应 Web 版)。这里最大的坑是py2neo的版本兼容性:py2neo 4.x 对应 Neo4j 3.5 时代,py2neo 5.x 则把 API 改成了Graph()构造方式变了,源码里如果写的是from py2neo import Graph,你用 py2neo 5.x + Neo4j 4.x 通常没问题,但 Neo4j 5.x 之后官方驱动迭代很快,py2neo 很容易报 401 Unauthorized 或握手失败。

我自己验证过的组合是Neo4j 4.4 + py2neo 5.2.2 + Python 3.9,稳定不报错。如果你用 Neo4j 5.x,建议直接改成官方neo4j驱动,代码改动量不大,但源码里的graph.delete_all()和graph.run()都要换写法。下面是两种驱动最直观的对比:

操作py2neo 5.xneo4j 官方驱动
连接Graph("bolt://localhost:7687", auth=("neo4j","123456"))GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j","123456"))
执行graph.run(cql)session.run(cql)
清空graph.delete_all()session.run("MATCH (n) DETACH DELETE n")
参数graph.run(cql, name=name)session.run(cql, name=name)

4.2 启动顺序:先建图,再跑问答

这个项目的启动顺序有讲究。第一步启动 Neo4j 服务,第二步运行clear_graph.py清空旧数据,第三步运行build_medicalgraph.py建图,最后才运行chat_robot.py。

# 启动 Neo4j(不同系统命令不同,mac/linux 用 neo4j console) neo4j start # 建图前先清空,避免重复节点 python clear_graph.py # 写入全部实体与关系 python build_medicalgraph.py # 启动问答循环 python chat_robot.py

clear_graph.py内部就是执行一句MATCH (n) DETACH DELETE n。这个脚本在重复复现实验时非常有用,否则你每次重新运行build_medicalgraph.py都会产生大量重复关系,而且因为MERGE的存在,它还不会报错,但图会无限制膨胀。我第一次复现时没清库,节点数查出来 3000,关系数却到了 8000,明显是重复写边,所以强烈建议每次重建都先清一次。

4.3 常见依赖安装指令

环境问题占项目复现失败的最大比例,我把完整安装序列在虚拟环境里执行了几遍,以下指令最省事:

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install py2neo==5.2.2 jieba flask

如果你是离线环境,py2neo和jieba的 wheel 包要提前下载,源码包里如果有requirements.txt就按那个来,没有的话按上面三个装。flask只有在你要跑 Web 界面时才需要,纯控制台问答可以不用。

4.4 首次运行验证清单

跑完chat_robot.py后,输入“胃溃疡有什么症状”和“拉肚子挂什么科”这两个最基础的问题。如果都有答案,说明整条链路已经通了一半;如果第一个有结果第二个没有,大概率是dict词典里缺少“拉肚子”这个实体词,而不是建图问题。这个排查思路要记住:先查实体是否被识别,再查图里是否有对应关系。

5. 常见问题与踩坑:从连接失败到中文乱码

5.1 Neo4j 连不上:报错“Authorization”或“Service Unavailable”

现象:build_medicalgraph.py运行时报py2neo.errors.ServiceUnavailable或Unauthorized。原因:Neo4j 默认仅允许本机 localhost 访问,且首次启动会强制修改初始密码neo4j/neo4j;如果你在 neo4j.conf 里启用了server.default_listen_address=0.0.0.0,密码又没改,就出现认证失败。解决:先在浏览器打开http://localhost:7474,用默认账号neo4j、默认密码neo4j登录并按提示改密码到123456(或直接改成源码里的密码),然后重启 Neo4j。如果还不行,检查http://localhost:7474是否返回 JSON 而不是“无法访问”。

5.2 中文写入后变成乱码或空节点

现象:在 Neo4j Browser 中查询MATCH (n:Disease) RETURN n LIMIT 25,看到属性显示\u80c3之类的转义字符,或者节点根本没有name属性。原因:数据文件medical.json的编码不是 UTF-8,Python 读取时用了默认的open()没指定encoding,导致读取后是乱码;写入 Neo4j 后乱码本体被存储。解决:统一用utf-8打开文件,并在build_medicalgraph.py里显式指定:

with open("data/medical.json", "r", encoding="utf-8") as f: data = json.load(f)

另外,Neo4j 4.x 默认的字符集就是 UTF-8,只要终端和配置文件都保持一致,一般不会再乱码。

5.3 有实体但答不出问题:关系方向反了或实体类型不匹配

现象:输入“胃溃疡有什么症状”,返回空;但在图里MATCH (d:Disease {name:'胃溃疡'})-[:has_symptom]->(s) RETURN s.name又能查出结果。原因:question_analysis.py里对这个问题的意图解析产生的rel_type是symptom,而图里的关系类型叫has_symptom,值对不上;或者实体抽出来的是Disease类型,模板里规定的却是Disease -> Symptom -> Department的多跳路径,CQL 没生成完整路径。解决:打开keyword_template.py,把所有关系类型与图里的rel_type名称对齐,并打印trans_to_cql的结果,看 CQL 长什么样,再用 Neo4j Browser 手动执行这个 CQL,可以肯定定位是生成问题还是数据问题。

5.4 jieba 分词把实体切断

现象:“十二指肠溃疡”被切成了“十二指肠”和“溃疡”,导致实体匹配失败。原因:自定义词典dict/userdict.txt没有被载入,question_analysis.py里没有调用load_userdict()。解决:在question_analysis.py初始化时加入一行:

import jieba jieba.load_userdict("dict/userdict.txt")

这里要注意路径,如果脚本在根目录,dict/相对路径没问题;如果从子目录运行,建议改成绝对路径,避免玄学式“昨天还能跑今天不行”。

5.5 重复关系膨胀

现象:多次运行build_medicalgraph.py后,实体数不变但关系数翻倍。原因:虽然写边用了MERGE,但关系类型和两端实体的某个属性(比如name相同但id不同)不满足MERGE的唯一约束。解决:先做节点去重,再建立唯一约束:

CREATE CONSTRAINT ON (d:Disease) ASSERT d.name IS UNIQUE

再在关系写入时合并多个匹配条件。如果数据量不大,直接每次重建前跑clear_graph.py是最省心的。

6. 进阶:把规则模板扩到症状-饮食-检查链,以及交互层优化

源码自带的模板覆盖了“疾病-症状”“疾病-科室”“疾病-药品”这三类最常见问法,但医疗问答还有一个高频场景是“得了这个病该做什么检查”“平时饮食要注意什么”。这些数据在medical.json里可能已经存在,只是没有在keyword_template.py中定义相应模板。我一般会这样扩展:

check_template = { "query": ["检查", "确诊", "做哪些检查"], "entity_type": "Disease", "rel_type": "need_check" } diet_template = { "query": ["饮食", "吃什么好", "忌口", "不能吃"], "entity_type": "Disease", "rel_type": "diet_advice" }

记得先在build_medicalgraph.py的create_relationship里加上对应的关系类型,否则模板定义了也查不到数据。建图脚本里的self.relationships列表看起来像["Disease", "Symptom", "has_symptom"],把它改成["Disease", "Check", "need_check"]后重新跑clear_graph.py和build_medicalgraph.py,新问法就能生效。我用这套方法把系统从 3 类问法扩到了 6 类,答辩现场演示“胃溃疡做什么检查”时,效果非常直接。

交互层优化方面,我给chat_robot.py加了一个简单的人机引导:当用户输入“你好”,回复支持的问题示例,把“疾病症状、科室、药品、检查、饮食”列成一行行提示。这个改动不算技术含量,但对演示节奏极有帮助,因为大多数评委不会凭空想出优质提问,你主动给出提示词,质询问题就会落在系统擅长的范围内。

还有一个小技巧:把get_cql.py生成的 CQL 写到日志文件里。我习惯在每次问答后追加一行:

with open("query_log.txt", "a", encoding="utf-8") as f: f.write(f"{question}\t{cql}\n")

这样复现错误时不用猜,直接看日志就能重现。从那以后,我每次改模板都会强制跑一遍至少 10 条覆盖所有意图的测试问题,再把日志里的 CQL 人工扫一遍,防止新模板把旧问题带偏。这份源码帮我把医疗知识图谱从“论文概念”落成“能演示的系统”,也希望帮你少走那几个连接和编码的弯路。

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

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

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

立即咨询