简介这是一份基于Python与Neo4j图数据库的医疗知识图谱智能问答机器人项目源码主要面向计算机相关专业正在准备毕业设计的学生以及需要项目实战练习的学习者。项目经导师指导并认可也适用于课程设计或期末大作业代码经过严格调试确保可运行。压缩包内共含36个文件以8个Python源文件如build_medicalgraph.py、question_analysis.py、get_answer.py、chat_robot.py和10个txt说明文档为主另有js、css等前端静态资源与图片整体大小15.34MB目录结构清晰便于按模块学习并附有必要的配置文件。已有702人学习下载。整个项目涵盖从医疗数据清洗、知识图谱构建与Neo4j导入到问句解析、CQL查询生成、答案获取的完整流程附带超详细注释与项目使用说明能够帮助读者深入理解知识图谱与智能问答机器人结合的实际工程实现适合深入研读与二次开发。1. 医疗知识图谱问答机器人不靠模型靠图谱的毕设通关方案医疗健康问答是Python毕设里的常青选题但大多数同学卡在同一个地方对话逻辑写好了就是答不出有依据的结论。这个基于Neo4j图数据库的知识图谱问答项目走的是另一条路——把疾病、症状、科室、药品的关系全部落进图结构用CQL查询代替模型推理答案直接从图谱里捞。它适合三类人急着交毕设的计算机专业学生、想完整走一遍“本体设计—数据入库—问答解析—CQL生成”全流程的进阶学习者以及手里有零散医疗数据但不知道怎么组织成可查询结构的开发者。关键是它能跑通而且注释量大不用把时间耗在猜代码上。下面按我拆包时的顺序把建图、问句解析、查询生成、排错逐层展开。2. 医疗数据建模与图谱构建节点关系设计决定问答上限2.1 先想清楚“问什么”再定实体和关系拿到源码别急着跑。先打开data目录看原始数据结构再对照build_medicalgraph.py你会发现这套问答的边界完全由实体类型和关系类型定义。包里预设的实体包括疾病disease、症状symptom、科室department、药品drug、检查项目check、食物food和身体部位bodypart。关系则有“疾病-症状-表现”“疾病-科室-就诊”“疾病-药品-推荐用药”“疾病-食物-宜忌”等。也就是说用户能问“头痛挂什么科”系统就能查出头痛属于神经内科还是普通内科用户问“感冒吃什么药”就能返回对乙酰氨基酚这类关联节点。这个设计直接决定后续CQL能查什么。想清楚这层再动手改数据比上来就跑通更有价值。资源里的原始表多为CSV行是实体名列是属性。在建图脚本里每个节点对应一个实体类型每条关系对应一个头实体关系尾实体三元组。前面建模越规范后面get_cql.py的匹配逻辑就越简单。2.2 CSV数据清洗编码、空值、去重的三个固定动作我看到不少同学拿到医疗数据直接入库结果CQL查询时出现中文乱码或节点属性缺失。实际动手前必须做两件事统一文件编码到UTF-8并检查空值。以下是清洗阶段常用的处理方式# data_clean.py 示意正式建图前跑一遍 import pandas as pd df pd.read_csv(data/disease_symptom.csv, encodingutf-8-sig) # 注意utf-8-sig 会去掉BOM头避免第一列首字符出现“\ufeff” df df.dropna(subset[disease, symptom]) df[disease] df[disease].str.strip() # 统一去掉首尾空格防止建出的节点叫“感冒 ” 而不是“感冒” df df.drop_duplicates(subset[disease, symptom]) # 同一对疾病症状重复出现会让CQL返回重复行 df.to_csv(data/disease_symptom_clean.csv, indexFalse, encodingutf-8)逻辑说明先用pandas把原始CSV读进来指定encodingutf-8-sig解决BOM头导致的第一个字段解析异常接着dropna去掉关键列为空的行因为空值建出来的节点只有一个空字符串属性查询时永远命中不了strip和drop_duplicates是配套动作前者治“感冒 ”这类带空格的字段后者治重复三元组。参数上特别提醒如果你的原始数据是GBK编码得先转码否则pandas读进来就是乱码后面建出的图谱全是乱码节点。2.3 执行建图脚本py2neo的节点与关系写入顺序数据洗完后进入build_medicalgraph.py。常见做法是用py2neo驱动连接Neo4j先建约束再批量塞节点最后建立关系。核心逻辑分四步# build_medicalgraph.py 建图核心段简化 from py2neo import Graph, Node, Relationship, NodeMatcher graph Graph(http://localhost:7474, auth(neo4j, 123456)) # 1. 建唯一约束避免重复创建同名实体 graph.run(CREATE CONSTRAINT disease IF NOT EXISTS FOR (n:Disease) REQUIRE n.name IS UNIQUE) # 2. 创建疾病节点从清洗后的DataFrame读取 matcher NodeMatcher(graph) for _, row in df.iterrows(): node Node(Disease, namerow[disease], descrow.get(desc, )) # 如果节点已存在用matcher匹配不重复创建 if matcher.match(Disease, namerow[disease]).first() is None: graph.create(node) # 3. 创建“疾病-症状”关系每条关系都要先定位两端 for _, row in df_relation.iterrows(): d matcher.match(Disease, namerow[disease]).first() s matcher.match(Symptom, namerow[symptom]).first() if d and s: graph.create(Relationship(d, HAS_SYMPTOM, s)) # 4. 手动执行 clear_graph.py 可清空后重建逻辑说明建约束放在最前是保证幂等性的关键——重复跑建图脚本不会生成两份节点创建节点前先通过NodeMatcher检查是否已存在防止主键冲突关系创建必须先拿到两端节点对象如果某条关系里只有一端入库比如“疾病-医生”里的医生节点被漏建就需跳过否则驱动直接报错。参数上graph()里的地址和auth对应本地或者云数据库的地址账号毕设场景默认localhost即可。clear_graph.py的作用是帮你把整库清空再重来改完数据模型后别漏跑它。3. 问句解析与意图识别不训练模型的规则匹配方案3.1 问答类型划分七类意图覆盖九成高频问题这个项目的问答引擎不依赖任何语言模型用的是keyword_template.py里的规则模板。看注释能发现项目预设了七类问答症状查疾病、疾病查症状、疾病查科室、疾病查药品、药品查功效、疾病查饮食、疾病查检查。整理成对比表意图类型触发词模板示例返回实体symptom_disease“XX有什么症状”“XX表现有哪些”Diseasedisease_symptom“XX是什么病”“XX怎么回事”Symptomdisease_department“XX挂什么科”“XX去哪个科室”Departmentdisease_drug“XX吃什么药”“XX用什么药”Drugdrug_disease“XX治什么”“XX有什么用”Diseasedisease_food“XX不能吃什么”“XX宜什么食物”Fooddisease_check“XX做什么检查”“XX怎么确诊”Check每条意图对应一个键键值对里的value就是触发词列表。识别时问句先做字符串匹配命中了哪个模板就把问句归到哪类。这套方案的好处是零训练成本、可解释性强毕设答辩时能直接用流程图讲清楚问句→分词→模板匹配→意图标签→实体抽取→CQL生成。坏处是换个说法就得加模板但这正好是毕设工作量所在。3.2 实体抽取从问句里把“头晕”和“感冒”挖出来意图定了还得知道用户问的是哪个疾病或哪个症状。项目的question_analysis.py里维护了一个实体词典主要来源是dict目录下的疾病名、症状名词汇表。匹配顺序很关键先做长词优先匹配再退到短词防止“急性上呼吸道感染”被拆成“感冒”。# question_analysis.py 实体抽取核心逻辑 import re from keyword_template import entity_dict # 病名/症状名等词典 def extract_entities(question): entities {Disease: [], Symptom: [], Drug: [], Department: []} max_len 0 matched_entity for etype, words in entity_dict.items(): for word in words: if word in question and len(word) max_len: matched_entity word max_len len(word) entities[etype] [word] # 若一个都没匹配到退化为字符级模糊匹配 if not matched_entity: for etype, words in entity_dict.items(): for word in words: if re.search(re.escape(word[:2]), question): entities[Symptom].append(word) break return entities逻辑说明遍历每种实体类型的词表凡是问句里出现过的词都记下来但只保留长度最大的那个作为目标实体解决“长词包含短词”的歧义。比如“慢性胃炎”同时包含“胃炎”按长度取长词查询结果更精确。参数上如果用户问“我最近总是头晕还失眠”而词表里只有“头晕”匹配结果是Symptom头晕若词表加了“失眠”两个词都会进Symptom列表后续生成的CQL需要做多症状联合查询。退化分支用了两字模糊匹配逻辑较简单实测中容易误伤但至少保证了不返回空。3.3 主程序里的解析流水线从输入到结构化查询对象真正把用户输入变成结构化查询对象的是main.py和chat_robot.py。main.py更像启动入口chat_robot.py则把三步串在一起问题分析、CQL生成、答案查询。以下代码详细说明这一流程# chat_robot.py 主对话流水线 from question_analysis import extract_entities, get_intent from get_cql import cql_generator from get_answer import answer_search def chat_bot(question): # 第一步识别意图 intent get_intent(question) # 第二步抽实体 entities extract_entities(question) if not entities[Disease] and not entities[Symptom]: return 理解不了换个说法试试比如高血压挂什么科 # 第三步根据意图实体拼CQL cql cql_generator(intent, entities) # 第四步执行并返回自然语言答案 answer answer_search(cql) return answer if __name__ __main__: while True: q input(用户) if q.strip().lower() q: break print(机器人, chat_bot(q))逻辑说明get_intent是在keyword_template里做规则命中返回一个字符串键extract_entities返回实体字典cql_generator按意图把实体填进预先写好的CQL模板answer_search拿着CQL去Neo4j执行并拼答案。参数上注意对话循环里的退出条件调试时可以随时输入q退出真实跑批时可以把循环改成读取用户输入列表。这个流程的可复用性很强后续想加“疾病传播途径”这类新意图只需在keyword_template加键、在cql_generator加一个模板分支。4. CQL生成与答案回填从查询模板到人话输出4.1 CQL模板设计参数化查询防注入get_cql.py是整个项目的翻译层把“意图实体”翻译成Neo4j能执行的Cypher语句。直接拼接字符串虽然能跑但遇到问句里带引号、括号的情况很容易翻车而且毕设答辩时老师看到字符串拼接也会追问安全性。这个项目采用的常见做法是参数化查询# get_cql.py 意图转CQL模板 def cql_generator(intent, entities): if intent disease_symptom: # 症状查疾病 symptom entities[Symptom][0] return {cql: MATCH (d:Disease)-[:HAS_SYMPTOM]-(s:Symptom) WHERE s.name $name RETURN d.name, params: {name: symptom}} elif intent symptom_disease: # 疾病查症状 disease entities[Disease][0] return {cql: MATCH (d:Disease)-[:HAS_SYMPTOM]-(s:Symptom) WHERE d.name $name RETURN s.name, params: {name: disease}} elif intent disease_department: disease entities[Disease][0] return {cql: MATCH (d:Disease)-[:DEPARTMENT]-(dep:Department) WHERE d.name $name RETURN dep.name, params: {name: disease}} # ... 其余意图略 return None逻辑说明每个意图对应一条CQL模板返回字典里同时带cql和params由get_answer.py执行时用execute(cql, params)传入。用$name占位符而不是直接拼字符串能规避特殊字符导致的语法错误也让查询计划可复用。参数上实体列表取[0]是因为多数问句只含一个目标实体如果你想支持多症状联合查询这里要改成列表推导生成多个MATCH条件。4.2 get_answer.py把查询结果拼成正常中文回答CQL查出来的是节点属性列表直接返回给用户会很难看懂。get_answer.py做的是结果清洗和话术拼接。常用逻辑把所有结果名去重然后按不同意图返回不同格式的句子。# get_answer.py 答案格式化 from py2neo import Graph graph Graph(http://localhost:7474, auth(neo4j, 123456)) def answer_search(query_dict): if not query_dict: return 库里没找到对应答案 res graph.run(query_dict[cql], **query_dict[params]).data() if not res: return 没查到相关记录换种问法试试 names list({r[list(r.keys())[0]] for r in res}) return 、.join(names) def answer_pretty(intent, names): if intent disease_symptom: return f根据症状“{names}”可能对应的疾病有{names} if intent symptom_disease: return f疾病“{names}”的典型症状包括{names} if intent disease_department: return f建议去{names} return 、.join(names)逻辑说明先用set对返回的节点名去重避免重复关系导致同一答案出现多次再按意图套固定话术。参数上list(r.keys())[0]是取查询结果里的第一个返回列对应模板中RETURN后的别名。如果课程设计要求更精细的话术可以在接口里引入“实体别名”映射把“高血压”输出成“高血压患者”。这一步在毕设答辩环节很加好感因为它展示了从数据到表达的完整闭环。4.3 变化同一问题多种问法怎么兼顾模板匹配最大的坑是用户不按模板说话。有一个常见方案是在keyword_template里给每个意图配多组同义触发词。实际代码里每个意图的value是一个词表例如“症状查疾病”的触发词既有“什么症状”又有“有哪些表现”“怎么了”“怎么回事”。我一般会把“得了XX怎么办”也归到症状查疾病里去因为这种问句用户多半是想知道疾病的表现和影响。改法是在各意图的value列表末尾追加触发词比如# keyword_template.py 片段 intent_keywords { symptom_disease: [有什么症状, 有哪些表现, 啥症状, 怎么回事, 怎么办], disease_drug: [吃什么药, 用什么药, 推荐什么药, 该吃点什么], # ... }注意关键词之间不要有包含关系比如“有什么症状”和“有哪些症状”别同时配多余的匹配只会增加歧义。这个表维护得越好后面手测的通过率越高。5. 避坑指南Neo4j建图与问答联调的五个高频翻车点5.1 连接失败Neo4j服务没起或版本不匹配现象运行chat_robot.py报py2neo.database.ClientError或ConnectionRefused提示无法连接localhost:7474。解决先确认Neo4j是否用neo4j.bat console前台启动再确认浏览器能打开7474端口最后核对py2neo版本。这个项目常用的配对是py2neo 4.x对应Neo4j 3.5或4.x如果装了py2neo 2021.2.0以上的新版本api变化较大建议按项目文档锁版本。5.2 CSV首列出现\ufeff节点属性名带BOM头现象建完图后查询“感冒”查不到但Neo4j浏览器里能看到节点名为“感冒”。原因CSV被Excel另存成带BOM的UTF-8pandas直接读取时首列首字符变成“\ufeff感冒”。解决统一用encodingutf-8-sig读入或在建图前把第一列做一次replace(\ufeff, )。这个坑非常隐蔽我拆包时第一轮跑数据就栽在这。5.3 药品和疾病重名导致意图误判现象输入“阿司匹林能治什么”系统却当成“疾病查症状”去查阿斯匹林这个疾病。原因实体词典里Disease和Drug都包含同一个词而entity提取逻辑只看长度、不看意图优先级。解决extract_entities返回时结合intent过滤实体类型比如intent是drug_disease就优先取Drug词典的匹配项而不是Disease词典。5.4 清空图谱后关系残留现象clear_graph.py跑完后重建图发现还有很多橙色关系散落在节点上。原因执行MATCH (n) DETACH DELETE n只删除了节点级内容若脚本只删节点没删关系或者Neo4j的事务没提交就会留下游离Relationship。解决直接用CYPHER语句MATCH ()-[r]-() DELETE r分两步执行先清关系再清节点。推荐每次改数据模型都走这两刀。5.5 中文乱码只在Neo4j浏览器正常接口返回乱码现象get_answer.py返回的答案在Windows控制台显示乱码但Neo4j浏览器里中文正常。原因Windows默认GBK编码的cmd输出UTF-8字符串。解决在项目入口文件最前面加一行sys.stdout.reconfigure(encodingutf-8)或者在运行脚本前把控制台代码页切到65001。这个小改动能让调试体验提升不少。6. 跑一条完整链路并扩展你自己的问答意图建议拿到源码后不要急着改代码先把默认数据跑通一条链路。以“头晕乏力应该挂什么科”为例get_intent先命中“挂什么科”模板归为disease_departmentextract_entities从词表挖出“头晕”“乏力”两个症状实体get_cql取第一个实体生成MATCH查询执行后发现“头晕乏力”并非标准疾病名查空触发兜底分支返回“没查到”。这里能看到项目的边界它对口语化症状组合的容错有限但这正是你可以扩展的空间。更实用的进阶操作是增加一个意图。假设你想支持“感冒一天吃几次药”这类剂量问题需要改三处第一在keyword_template.py加键“drug_dosage”触发词填“吃几次”“用药剂量”“一次几粒”第二在get_cql.py新增一个分支生成MATCH (d:Disease)-[:RECOMMEND_DRUG]-(dr:Drug) WHERE d.name$name RETURN dr.dosage第三在get_answer.py的answer_pretty里加对应话术“XX的用法用量是XX”。这比从头写一个分类器实在得多答辩时也能讲清楚“规则可扩展”这个点。从那以后我每次跑这个项目都强制走一遍三连先清空图谱、再重建数据、最后手测十条典型问题覆盖七类意图。这条习惯帮我省去不少调试时间也让交付出去的demo稳定许多。整套源码负责把骨架搭好你要做的只是往里填自己的医疗数据和新问题模板希望帮到你。本文还有配套的精品资源点击获取