ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Neo4j医疗知识图谱问答机器人:从图谱构建到智能问答实战拆解

Neo4j医疗知识图谱问答机器人:从图谱构建到智能问答实战拆解 简介这套Python医疗知识图谱智能问答机器人项目面向计算机相关专业大作业、毕业设计及项目实战学习者基于Neo4j图数据库完成疾病、症状、科室等医疗实体建模与自然语言问答匹配。压缩包共36个文件大小15.34MB以Python源码.py、说明文档.txt为主辅以界面截图.png/.jpg、前端脚本.js/.css/.html及数据配置.json覆盖图谱构建、问题分析、CQL查询、关键词模板、聊天机器人等完整模块。项目经导师指导并评审98分源码本地编译可运行、严格调试通过适合作为课程设计样板或毕设基础框架。目前已有296人学习下载后可直接对照使用说明运行验证快速掌握知识图谱问答系统从数据到交互的开发思路。1. Python基于Neo4j图数据库的医疗知识图谱智能问答机器人从零跑到能答症状这名字乍看是毕业设计拆开看其实就是一条完整流水线先拿医疗数据建 Neo4j 知识图谱再通过问题分析匹配实体生成 CQL 查到答案。这份源码里 main.py 是入口chat_robot.py 管对话build_medicalgraph.py 负责把 dict 数据灌进图数据库question_analysis.py 和 get_cql.py 把自然语言问句翻译成图查询。它适合正在做期末大作业、毕业设计或者想练手图数据库和问答系统的从业者。难点不在算法多深而在于把数据、图谱和查询串起来——这套代码把串起来的活都做好了照着跑一遍比看十篇教程都管用。2. 项目结构与运行环境先把文件和 Neo4j 跑起来2.1 文件清单每个 py 在干什么打开压缩包最外层是 MedicalChatbots 目录。别被一堆文件名唬住这张表把核心模块记下来后面调起来就心里有数文件职责main.py程序入口初始化图谱和问答接口build_medicalgraph.py读取 dict 数据构建实体、关系写入 Neo4jchat_robot.py对话主逻辑把用户问题转成交互question_analysis.py分词、实体识别抽取问句里的症状/疾病关键词keyword_template.py定义问题模板和关键词映射规则get_cql.py根据分析结果生成 Cypher 查询语句get_answer.py执行 CQL从返回结果中组织自然语言答案clear_graph.py清空图谱数据方便重跑构建脚本static/前端静态资源配合 Web 界面展示初看会觉得模块有点多其实分层很清晰build_medicalgraph.py 是数据层question_analysis.py 是理解层get_cql.py 是查询生成get_answer.py 是答案层。main.py 负责把这几层按顺序拉起来。这种结构最大的好处是你想改知识图谱的 schema不用动问答逻辑想换问答模板也不用重新建库。2.2 环境准备Python 版本和 Neo4j 安装这套项目依赖 Python 3 和 Neo4j 图数据库。Python 用 3.8 以上就行Neo4j 建议社区版安装时注意设好密码后面 py2neo 连接要用。依赖主要是 py2neo、jieba 分词、Flask如果跑 Web 界面这类常规库。我的习惯是先把虚拟环境建好再统一装依赖避免和系统 Python 打架。mkdir medical_chatbot cd medical_chatbot python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install py2neo jieba flask这里的 py2neo 是让 Python 操作 Neo4j 的驱动库jieba 用来做中文分词flask 用来给问答机器人挂一个 Web 入口。如果只是跑命令行交互flask 可以暂时不装。注意 py2neo 和 Neo4j 的版本匹配是个坑后面第 5 章专门说这里先按下不表。2.3 启动 Neo4j 并导入项目把 Neo4j 启动起来浏览器打开 http://localhost:7474用安装时设的密码登录。然后回到项目目录先跑 clear_graph.py 清空旧数据防止重复构建产生脏数据再跑 build_medicalgraph.py 灌数据最后启动 main.py 进入问答。python clear_graph.py python build_medicalgraph.py python main.py以上顺序不能乱。clear_graph.py 本质是执行MATCH (n) DETACH DELETE n把库里所有节点和关系清掉。build_medicalgraph.py 会重新建节点和关系如果不清空相同疾病和症状会重复创建查询时出现一堆冗余路径。我刚拿到这套代码时为了省事直接跳过第一步结果图谱里重复节点翻倍问答答案也飘后来老老实实每次都清。main.py 启动后如果走 Web 模式默认会监听本机端口看到 “Running on http://127.0.0.1:5000” 之类的输出就说明起来了。如果只想测试问答逻辑也可以直接调 chat_robot.py 里的对话函数传入一句“头疼吃什么药”看返回结果。提示Neo4j 默认端口 7474 是 HTTP 界面7687 是 Bolt 协议端口。py2neo 连接默认走 Bolt防火墙或云服务器记得把这两个端口都放行。2.4 连接 Neo4j 的连接串与密码设置主程序里通常有一行Graph(...)连接语句很多人在这翻车。我的建议是不要写死密码用环境变量或配置文件。常见写法长这样import os from py2neo import Graph NEO4J_URI os.getenv(NEO4J_URI, bolt://localhost:7687) NEO4J_USER os.getenv(NEO4J_USER, neo4j) NEO4J_PASSWORD os.getenv(NEO4J_PASSWORD, 123456) graph Graph(NEO4J_URI, auth(NEO4J_USER, NEO4J_PASSWORD))这样在本地跑不需要改代码部署到服务器也不怕密码硬编码泄漏。有一点要提醒py2neo 4.x 之后把 Graph 的密码参数从password改成了auth(user, pwd)如果你抄的是老教程会看到 TypeError。这个细节第 5 章还会再提。连接建立后可以用一条简单查询验证result graph.run(RETURN 1 AS result).data() print(result) # [{result: 1}]返回[{result: 1}]代表连接成功。如果报AuthError先去 Neo4j 桌面端或命令行把初始密码改掉社区版第一次登录会强制改密码。如果报ServiceUnavailable检查 Neo4j 服务有没有真正启动或者 7687 端口是不是被占用。2.5 main.py 的启动逻辑与常见输出main.py 是整个项目的入口它会按顺序完成三件事一是检查 Neo4j 连接二是加载分词词典三是启动交互接口。源码里常见写法是先调用初始化类再启动 Flask。一个简单示意如下def main(): medical MedicalGraph() medical.create_graph() # 如果图谱空则构建 app Flask(__name__) app.route(/chat, methods[POST]) def chat(): question request.json.get(question) answer chat_answer(question) return {answer: answer} app.run(host0.0.0.0, port5000)注意这里 create_graph() 里最好判断一下图谱是否已经有数据否则每次启动都重建一遍慢且累。判断方式可以是一条计数查询节点数大于阈值就跳过构建。Flask 接口返回 JSON回答在 answer 字段里。如果你只想在命令行测试可以把 app.run 这段注释掉直接在 main 里循环调用 chat_answer。项目根目录下还有 dict 和 data 两个文件夹。dict 里一般是自定义词典比如症状、疾病、药品、检查项目的词表给 jieba 分词用。data 里存放的是更原始的医疗数据可能是结构化文本或 JSONbuild_medicalgraph.py 会读这些文件再抽实体关系。拿到资源后先打开 dict 下的文件确认词表覆盖哪些科室如果词表只有常见病那问“偏头痛怎么缓解”可能识别不出来需要在词表里补词但不要直接改原文件最好复制一份自定义扩展词典用jieba.load_userdict加载。3. 构建医疗知识图谱从 dict 数据到图谱落库的完整流程3.1 实体与关系设计先定义医学 schema医疗问答要回答“症状对应什么病”“病吃什么药”“病挂什么科”图谱里至少要有三类节点疾病Disease、症状Symptom、药品Drug关系就是has_symptom、need_drug、belongs_to_department。有的项目还会加检查项、科室实体。这套代码里 build_medicalgraph.py 做的核心工作就是把这些实体和关系从原始数据中抽出来然后用 py2neo 批量写进 Neo4j。设计 schema 时要注意Neo4j 不是关系型数据库不强制约束节点属性所以很多人随手 CREATE 节点结果同一实体出现多个 label。我的做法是先固定好 label 和属性名比如节点统一用中文名做 name 属性疾病节点 label 叫 Disease症状叫 Symptom。后面查 CQL 就不用猜。3.2 从 dict 读取数据加载实体词表构建脚本一般会先读 dict 下的文件把词表变成 set再用这些词去 data 里匹配句子。这里给出类似项目的加载逻辑def load_entities(path): entities set() with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if line and \t in line: entities.add(line.split(\t)[0]) elif line: entities.add(line) return entities这里把每行词表读进来带词频的取第一列不带词频的整行取。编码必须用 utf-8Windows 上如果文件是 GBK会报UnicodeDecodeError可以用 errorsignore 临时跳过但最好先把文件转成 utf-8。有了词表再用 jieba 对 data 里每条记录分词把命中的词当作实体。这一步听起来简单实际坑在词表不全比如“胃溃疡”在词表里是整体分词却是“胃”“溃疡”。解决办法是自定义词典优先加载或者用最长匹配从词表里反查。3.3 批量写入 Neo4j用 Cypher 还是 py2neo 对象写入方式有两种一种是拼 Cypher 字符串一种是 py2neo 的 Node/Relationship 对象。这个项目大概率用的是 py2neo 对象方式因为可读性好还能复用。常见写法from py2neo import Graph, Node, Relationship graph Graph(bolt://localhost:7687, auth(neo4j, 123456)) def create_disease(graph, name, desc): node Node(Disease, namename, descdesc) graph.create(node) return node disease_node create_disease(graph, 偏头痛, 反复发作的头痛症状) symptom_node Node(Symptom, name搏动性头痛) graph.create(symptom_node) rel Relationship(disease_node, has_symptom, symptom_node) graph.create(rel)Node 的第一个参数是 label后面是键值属性Relationship 第一个参数是起点第二个是关系类型第三个是终点。graph.create 会一次性把这个子图写进去不需要分别建节点再建关系。如果数据量大建议用 graph.merge() 而不是 create()避免重复节点graph.merge(disease_node, Disease, name) graph.merge(symptom_node, Symptom, name) graph.merge(Relationship(disease_node, has_symptom, symptom_node), Disease, name)merge 的操作很像 MySQL 的 upsert指定 label 和唯一键存在就跳过不存在就创建。关系也能 merge不过要先把起终点节点查出来再创建否则容易建立重复关系。3.4 构建完怎么验证图谱质量跑完 build_medicalgraph.py别急着进问答先用 Cypher 统计一下节点和关系数MATCH (n) RETURN labels(n) AS label, count(*) AS number也可以在 Python 里跑result graph.run(MATCH (n) RETURN labels(n) AS label, count(n) AS cnt).data() for row in result: print(row[label], row[cnt])正常情况每个实体类型都会有节点关系数大于节点数。如果某种 label 数量是 0说明 data 里没有对应词表命中的数据或者 dict 词表和实体 label 对不上。此时先看 build_medicalgraph.py 里实体怎么定义的再回 dict 补词。验证关系时还可以抽一条完整路径看看MATCH p(d:Disease)-[r]-(n) WHERE d.name 感冒 RETURN p LIMIT 10在 Neo4j 浏览器里能直观看到“感冒”连接了哪些症状和药品。这一步能看到图谱是否连成网有些项目建完节点彼此孤立问答一查就空全是这种断路导致的。3.5 重建图谱的正确姿势本项目提供了 clear_graph.py但实际使用中更多人喜欢在构建脚本开头加一句强制清理省得手动跑。这里给一个推荐姿势graph.run(MATCH (n) DETACH DELETE n)放在构建脚本最前面每次运行都是全新库。坏处是如果构建中途报错旧数据已经被清了所以更稳妥的先备份再清。备份可以用 neo4j-admin dump但那是重量级操作想轻量就先把 data 文件备份好重跑成本也不高。另外注意构建脚本里如果有 import 外部文件路径要写绝对路径或用 os.path.join 拼接直接相对路径容易因为工作目录不同找不到文件。我经常在 IDE 里直接跑脚本工作目录是项目根目录没问题一旦从别的目录执行相对路径就翻车。3.6 实体消歧同名不同义怎么办医疗词表里同名情况不少比如“感冒”既是疾病名也可能出现在“胃肠感冒”里“红眼病”在眼科和传染病语境下指代不同。构建知识图谱时如果不做消歧两个实体共用同一个 name 属性会导致查询返回混杂结果。常见做法是给实体增加别名属性 alias或者用上下文特征词区分。在构建脚本里当词表命中的实体带有科室前缀时优先存成“科室_病名”属性里保留原词。问答时先看问题里的上下文词再决定查询哪个。进阶优化可以在实体节点上增加 alias 数组构建时用正则把上下文匹配到的科室信息存在属性里查询时用多次匹配。本项目不一定内置这套逻辑但知道这个边界你就不会把“答案偶尔不准确”当成完全不可控的 bug。4. 问答链路拆解问题分析、CQL 生成与答案匹配4.1 问题分析先识别实体再确定意图问答机器人拿到一句“我头疼并且发烧该吃什么药”不会直接拼 CQL而是先拆分把“头疼”“发烧”抽出来把“吃什么药”这个意图识别出来。这套代码里 question_analysis.py 负责这件事keyword_template.py 提供模板匹配。实体识别一般用 jieba 分词 自定义词典词表命中即实体。意图识别靠正则或模板比如“该吃什么药”匹配到 need_drug“挂什么科”匹配到 department。这两步的顺序很关键先抽实体再判断意图因为模板里的占位符要由实体来填充。def ana_question(question): words [w for w in jieba.cut(question) if w.strip()] entities [w for w in words if w in disease_set or w in symptom_set] intent detect_intent(question) return {entities: entities, intent: intent}detect_intent 是模板匹配逻辑。参数说明jieba.cut 返回生成器过滤掉空词entities 只保留在实体词表里出现的词intent 是后续 get_cql.py 决定生成哪种 Cypher 的关键变量。4.2 模板规则keyword_template.py 怎么设计模板文件里常见结构是一组“意图 - 关键词正则”映射。比如intent_keywords { need_drug: [吃什么药, 用什么药, 推荐药, 用药], department: [挂什么科, 去哪个科室, 就诊科室], has_symptom: [有什么症状, 症状表现], cause: [什么原因, 怎么引起, 诱因], }同时对每个意图定义查询模板cql_templates { need_drug: MATCH (d:Disease)-[:need_drug]-(m:Drug) WHERE d.name {disease} RETURN m.name, department: MATCH (d:Disease)-[:belongs_to]-(dept:Department) WHERE d.name {disease} RETURN dept.name, }这些模板的共性是利用疾病实体作为查询起点。所以如果问题里只出现症状实体“头疼”没有疾病实体就得先查“哪个疾病有这个症状”这就是典型的反查路径。keyword_template.py 里如果没有这类模板可以自己加后面第 6 章说怎么扩展。4.3 get_cql.py把自然语言问题变成 Cypher 查询get_cql.py 的作用就是根据 question_analysis.py 的结果选择合适的模板再把实体填进占位符。这里最忌讳的是用字符串拼接直接怼 SQLCQL 同理容易有注入风险还会因为引号问题报错。def get_cql(analysis): intent analysis[intent] entities analysis[entities] if not entities: return None entity entities[0] # 取主实体通常是疾病或核心症状 template cql_templates.get(intent) if template: cql template.replace({disease}, entity) return cql return None参数说明entities 取第一个是因为问题里可能同时出现症状和疾病优先取疾病如果只有一个症状走反查模板。返回 None 时问答系统要给出兜底文案比如“暂时无法回答建议换个说法”。这里还有个没说到位的地方如果问题包含两个实体比如“头疼和发烧怎么办”模板替换只替换第一个第二个实体直接被丢。常见做法是先判断意图如果是“啥病”则反查如果是“吃药”则直接拿第一个疾病节点。有一种更稳的做法先用疾病实体拼一个疾病查询没有疾病就试试症状反查两者都没命中就返回模糊答案。这套代码能不能做到看 get_cql.py 实现的逻辑你可以打开源码自己确认。4.4 get_answer.py查询回来怎么组织语言CQL 执行后返回的是节点或关系数据直接抛给用户会很生硬。get_answer.py 把查询结果包装成“根据您的描述可能是感冒建议服用 999 感冒灵”这样的句子。常见做法是把 Cypher 结果转成列表再套一个 answer 模板def generate_answer(results, intent): if not results: return 抱歉图谱里没有找到对应的信息。 if intent need_drug: drugs [r[drug] for r in results] return 可以尝试这些药 、.join(drugs) if intent department: depts [r[dept] for r in results] return 建议挂 、.join(depts) return 根据查询结果请参考 str(results)注意r[drug]这个 key 名要和 Cypher 里 RETURN 的别名一致。很多项目在这里翻车代码里写 m.name查询里却是RETURN m.name AS drug结果 KeyError。统一别名是最省事的比如RETURN m.name AS drug然后取 r[drug]。整个问答链路的时序大概是用户输入 - chat_robot.py 接收 - question_analysis.py 分析 - get_cql.py 生成查询 - get_answer.py 访问图谱返回答案 - chat_robot.py 输出。你可以把 get_answer.py 里加一行 print(cql) 方便调试看到底生成了什么查询。4.5 chat_robot.py把链路串起来chat_robot.py 是入口层的对话循环负责反复调用上面的步骤。命令行版本通常长这样while True: question input(请输入问题).strip() if question in {exit, quit, 退出}: break analysis ana_question(question) cql get_cql(analysis) if not cql: print(我还没学会这个问题换个说法试试。) continue results graph.run(cql).data() answer generate_answer(results, analysis[intent]) print(answer)开关条件用 set 比多个 if 清晰。如果走 Web 模式chat_robot.py 会提供函数给 Flask 调用不再使用 input。注意别把无限循环直接跑在 Web 线程里会阻塞端口我见过有人把 while True 写到 main.py 里结果页面一直转圈。想扩展多轮对话时只需要在 analysis 里存上下文比如上一次实体是什么连续问“它严重吗”这种代词就需要指代消解。这个项目未必做了多轮但对进阶是个好方向。4.6 多意图与组合查询用户问题常常不只一个意图比如“头疼该吃什么药严重吗”。把这个问题当成单一意图会漏信息。常见做法是先拆分问句再为每个意图生成一条查询最后合并答案。这里注意别把“严重吗”误判成 need_drug分词结果里实体没命中detect_intent 应该返回 severity而不是 fallback 到 need_drug。这需要 intent 优先级表need_drug department severity has_symptom。当一句话命中多个模板时按优先级取最高或者都处理。这套项目里如果有这个逻辑就能答得自然一些如果没有你可以在 keyword_template.py 里加一条优先级字典。5. 避坑与排查Neo4j、py2neo、路径与数据的五个坑5.1 坑 1py2neo 与 Neo4j 版本不匹配现象跑 main.py 时连接 Neo4j 一直报AttributeError: Graph object has no attribute run或者报py2neo.database.ClientError。原因py2neo 4.x 和 5.x 接口差异很大。4.x 里Graph.run()可用5.x 里虽然保留但参数位置变了如果你按 5.x 教程写auth(...)而装的是 4.x就会碰到参数解析问题。Neo4j 4.0 之后也强制了认证老代码里Graph(bolt://..., passwordxxx)直接失效。解决先统一版本。我的习惯是pip install py2neo2021.2.3配 Neo4j 4.x如果 Neo4j 5.x 就至少用 py2neo 2021.2.4 以上。安装完在 Python 里打印py2neo.__version__对照文档调整。最稳妥的方案是用官方neo4j驱动from neo4j import GraphDatabase但改代码成本稍大。5.2 坑 2Neo4j 默认社区版连不上本地服务现象浏览器能打开 7474但 Python 连接时报ServiceUnavailable: Failed to connect to localhost:7687。原因Windows 下 Neo4j 安装后不会默认注册为系统服务需要先手动运行neo4j.bat console启动macOS 下 brew 安装的路径里没有设环境变量命令行neo4j start找不到还有的版本把 HTTP 和 Bolt 绑到了不同 IP。解决Windows 用管理员打开命令行进入 bin 目录执行neo4j.bat console保持窗口别关macOS/Linux 先export PATH$PATH:/path/to/neo4j/bin再neo4j start。启动后看日志输出 Bolt 监听端口确认是 7687 而不是 7688。如果端口被占用lsof -i:7687找到进程改配置文件里的dbms.connector.bolt.listen_address。5.3 坑 3中文文件编码导致构建脚本崩溃现象build_medicalgraph.py 读取 dict 或 data 里的文件报UnicodeDecodeError: utf-8 codec cant decode byte 0xc0。原因资源包里的数据文件在 Windows 上被保存成了 GBK/GB2312但代码用 utf-8 打开。常见于复制粘贴生成的 txt 文件编辑器默认编码不是 utf-8。解决不要用记事本另存为直接在代码里做兼容def open_text(path): for enc in [utf-8, gbk, gb18030, utf-8-sig]: try: with open(path, r, encodingenc) as f: return f.read() except UnicodeDecodeError: continue这样逐个尝试编码命中最少。运行前把文件转成 utf-8 是根治办法命令行可以用iconv -f GBK -t UTF-8 input.txt output.txt但要先确认原始编码。5.4 坑 4图谱构建完但问答查不到答案现象构建脚本正常结束Neo4j 里也能看到节点但问“感冒吃什么药”返回空。原因可能是意图模板没命中感冒实体没有对应 need_drug 的关系或者 get_cql.py 生成时用了错误别名返回结果 key 和模板里取的不一致。还有一种常见原因是节点实际 name 是“感冒/流感”这种组合词实体识别却能拆出“感冒”造成查询结果空。解决先在 Neo4j 浏览器里手动查MATCH (d:Disease {name:感冒})-[:need_drug]-(m) RETURN m.name如果空说明图谱里没有这个关系回去看数据源。如果图谱里有则把 get_cql.py 生成的 CQL 打印出来对比看看 where 条件是不是写成了WHERE d.name {disease}而库里的 name 属性根本不是“感冒”。还有别名问题统一AS drug代码里也取drug两边一致才不翻车。5.5 坑 5重复运行构建脚本导致节点爆炸现象第一次跑正常第二次跑图谱里节点数量翻倍查询变慢答案出现多条重复。原因build_medicalgraph.py 用了graph.create()而不是graph.merge()重复执行没有唯一约束同一实体被多次创建。不少项目里把 clear_graph.py 独立出去但你未必每次都会跑。解决一是每次构建前强制MATCH (n) DETACH DELETE n二是给实体字段加唯一约束CREATE CONSTRAINT ON (d:Disease) ASSERT d.name IS UNIQUE然后在代码里改用 merge。这样即使多跑一次也最多更新属性不会新增节点。这个约束建完保存后又想改数据需要 drop constraint否则重复写入一样报错。排查顺序上先看连接再看数据最后看模板大多数问题都能在这三个层面定位。6. 进阶验证问答效果与扩展图谱维度的两个技巧6.1 用回归测试清单代替一句一句试问答机器人最怕“改一行代码把之前能答的问题答坏了”。我拿到这套项目后第一件事不是急着加功能而是建一个 questions.txt每行一个问题后面跟期望结果。然后写一个小脚本批量调用 chat_robot.py 的核心函数把输出和期望做对比。# questions.txt 感冒吃什么药|感冒|药品 头疼挂什么科|头痛|科室def run_regression(question_file): for line in open(question_file, encodingutf-8): question, _, expected line.partition(|) answer chat_answer(question) status PASS if expected.strip() in answer else FAIL print(status, question, answer)这个脚本我从第一次调试用到现在每次改 keyword_template.py 或 get_answer.py 都会跑一遍。它不检查答案完全相等只检查期望的实体或关键词是否出现在回答里这样能容忍模板措辞变化。6.2 扩展图谱维度加实体和关系只需要动四个文件如果想把这个医疗问答扩展到“化验单解读”或“用药禁忌”核心流程固定。先在 dict 里加词接着在 build_medicalgraph.py 里用 merge 创建新节点和关系再在 keyword_template.py 里加意图关键词最后在 get_answer.py 里加结果格式。改完记得跑一遍回归测试。举例新增“检查项目”实体check_node Node(Check, name血常规) graph.merge(check_node, Check, name)然后在模板里加“要不要做检查”的意图CQL 变成MATCH (d:Disease)-[:need_check]-(c:Check) WHERE d.name感冒 RETURN c.name。四步加起来不超过二十分钟前提是图谱里有这个关系数据。从那以后我每次拿到这种毕设项目都会先建测试 question 清单强制走一遍端到端再改任何代码。很多“答非所问”的玄学其实是实体词表和模板没有同步更新。希望帮到你。本文还有配套的精品资源点击获取
返回列表