首页 / 资讯中心 / 文章详情

知识图谱医疗问答系统拆解:Neo4j与Python实战指南

知识图谱医疗问答系统拆解:Neo4j与Python实战指南 ★ FEATURED ARTICLE
简介一套以Neo4j图数据库为核心的医疗知识图谱智能问答机器人项目面向知识图谱课程设计、毕业设计及Python自然语言处理入门者。项目将医疗知识建模为实体-关系图谱实现问句意图识别、Cypher查询生成与答案返回可帮助读者快速掌握知识图谱构建和问答系统开发全流程。压缩包共36个文件大小约15.33MB主要包含Python源码及编译文件.py/.pyc、实体关系文本数据.txt、Web前端页面.html/.css/.js、图谱可视化截图.png/.jpg及说明文档.md/.json覆盖医疗数据整理、知识抽取、实体识别、图谱构建、问答解析与前端展示完整环节。已有479人学习。项目内置build_medicalgraph.py用于一键建图question_analysis.py与get_answer.py负责问句解析和答案检索并提供clear_graph.py等运维脚本与可视化界面可直接运行调试也适合二次开发用于课程设计、毕业设计或知识图谱技术实战。1. 为什么一个医疗问答大作业值得拆开看知识图谱与Neo4j的选型逻辑如果你的 Python 大作业刚好抽到“知识图谱”方向又不想只做一个查数据库的假问答那医疗领域是最容易出效果、也最容易讲清楚的地方。这份资源不是一套 PPT而是一个能跑的医疗知识图谱问答工程用 Neo4j 存实体和关系用 Python 把问句拆成实体和意图再拼成 Cypher 查询最后把答案组织成自然语言返回。它的价值在于把“知识图谱 问答系统 Neo4j 操作”完整串起来适合做课程设计、毕业设计也适合第一次接触图数据库的从业者拿来当参考工程。我要提醒的是它不是一个开箱即用的产品代码里有不少值得改的硬编码和版本适配点但恰恰是这些地方才是你答辩时能讲出深度的素材。2. 按模块拆开这份工程从数据入库到问句解析的完整链路拿到这类压缩包我习惯先不看 README直接看文件清单。因为 README 往往只写“怎么启动”不写“每个文件为什么存在”。把文件按职责分组后你会发现这套工程的边界非常清楚几乎没有把所有逻辑堆在一个 main.py 里这是它值得参考的第一个原因。2.1 先认清工程里的三类文件入口、业务逻辑与资源数据从文件命名就能看出分层意图。main.py是服务入口chat_robot.py是问答主流程question_analysis.py、keyword_template.py、get_cql.py、get_answer.py四个模块分别承担自然语言分析、模板匹配、Cypher 生成和答案组装。build_medicalgraph.py负责建图clear_graph.py负责清理data和dict放语料与词典static放前端页面。我按职责整理了一份对应关系文件/目录职责运行时机build_medicalgraph.py读取数据创建节点和关系首次启动前执行一次clear_graph.py清空图库便于重建需要重置数据时执行main.py启动问答服务每次演示时执行chat_robot.py调度问答链路处理异常被 main.py 调用question_analysis.py分词、实体识别、意图判断每个问句触发keyword_template.py维护问题模板与意图映射被 question_analysis 调用get_cql.py把实体和意图拼成 Cypher意图确定后触发get_answer.py执行查询并整理答案CQL 生成后触发static前端页面与静态资源浏览器访问时使用这里要注意get_answer.py虽然叫 answer但它通常不做“自然语言生成”更多是把图查询返回的记录转成列表再套一层模板。真正有算法含量的是question_analysis.py和keyword_template.py它们决定了这句“胃痛应该看什么科”到底被识别成“症状查科室”还是“疾病查科室”。2.2 图谱是怎么建出来的build_medicalgraph.py 做的事我打开这类工程时最爱看建图脚本因为它能直接反映数据质量和关系设计。常见做法是从 csv 或 json 里读出“头实体、关系、尾实体”三元组然后用 py2neo 连接 Neo4j逐条 merge 节点和关系。核心代码一般长这样# build_medicalgraph.py 建图核心逻辑关键部分 from py2neo import Graph, Node, Relationship # 连接图数据库默认端口 7474生产环境建议改用 bolt 7687 g Graph(http://127.0.0.1:7474, auth(neo4j, 123456)) for head, rel, tail in triplets: # merge 而不是 create避免重复导入时产生大量重复节点 head_node g.merge(Node(Entity, namehead), Entity, name) tail_node g.merge(Node(Entity, nametail), Entity, name) g.merge(Relationship(head_node, rel, tail_node)) print(graph build finished)这段代码有两个关键点。第一merge的语义是“有则匹配无则创建”所以重复执行不会把同一个疾病节点插入两遍第二关系类型直接用数据里的rel字符串比如HAS_SYMPTOM、DEPARTMENT这要求数据源里的关系名必须统一否则会出现has_symptom和HAS_SYMPTOM两种边问答时查不到。我曾见过一份数据里“属于科室”和“belongs_to”混用结果模板匹配时只认识其中一种造成大量空答案。2.3 问答请求怎么流转从 main.py 到 chat_robot.py 再到四个解析模块启动时main.py一般只做两件事创建chat_robot对象然后进入循环等待输入。真正的逻辑在chat_robot.py它的伪代码流程是接收问句 → 交给question_analysis.py做实体抽取和意图判断 → 拿到实体与意图后交给get_cql.py拼 CQL → 交给get_answer.py查库并返回答案。如果中间任何一步失败就回退到兜底话术类似“这个问题我还在学习中”。这种拆法的好处是每个环节都可以单独测试。比如我经常先单独跑question_analysis.py输入“发烧吃什么药”看它到底抽出哪些实体再决定是改词典还是改模板。如果所有逻辑都在chat_robot.py里改一次意图规则就要重启整个服务排查效率会低很多。3. 复现时最容易翻车的五个地方Neo4j 适配、中文乱码与空答案排查这类工程在大学里被跑过很多次但几乎每次都会有人卡在环境问题上。不是代码逻辑多复杂而是 Neo4j 版本、Python 依赖和数据编码之间互相打架。下面五条是我实际复现时踩过、也帮别人排过的坑按出现频率排序。3.1 Neo4j 版本与 py2neo 接口对不上常见报错与适配现象启动build_medicalgraph.py时直接报AttributeError: Node object has no attribute merge或者Unknown function exists。原因py2neo 的 API 在不同版本里差异很大。老版本习惯写成node.merge()新版本推荐用graph.merge(node, primary_label, primary_key)Cypher 函数也一样Neo4j 4.x 之后很多老写法被移除了。解决我一般固定一套组合来跑比如 py2neo 4.x 配 Neo4j 4.x或者 py2neo 5.x 配 Neo4j 5.x。装依赖时不要用pip install py2neo然后什么都不管先看项目里是否写了版本要求如果没有就按照pip install py2neo4.4.0这种形式锁定一个版本。如果代码里用的Node.merge老接口要么降低 py2neo 版本要么把建图脚本改成graph.merge(node, Entity, name)的新接口。3.2 中文乱码与 CSV 编码Windows 环境的经典坑现象数据导入后Neo4j Browser 里节点名显示成åé¢ç或者 Python 读 CSV 时直接抛UnicodeDecodeError: gbk codec cant decode byte。原因Windows 下默认编码是 GBK而大多数开源医疗数据是 UTF-8。用open()不指定编码时Python 会用系统默认编码读文件遇到中文就翻车。解决读文件统一写成open(path, r, encodingutf-8-sig)注意utf-8-sig比utf-8更稳它能自动处理文件开头的 BOM 头。如果数据是别人用 Excel 编辑后另存的建议在代码里同时处理utf-8和utf-8-sig两种编码或者先打开文件看一眼字节内容再决定。写 CSV 时也指定encodingutf-8-sig不然用 Excel 打开导出的结果又会乱码。3.3 图里没数据却答得出来先分清“空库”和“没匹配”现象问答接口返回的答案永远是兜底句子“暂时无法回答”但单独执行get_cql.py生成的 CQL 却能查到数据。原因这个现象最迷惑人。它表示 CQL 没问题但入口模块没把实体正确传下去或者question_analysis.py返回的实体名和图里的节点名不完全一致。比如用户说“胃疼”词库里存的是“胃痛”模板匹配到了但又没做归一化CQL 查的就是一个不存在的节点。解决先数节点再查实体用下面这段小脚本做两步定位from py2neo import Graph g Graph(bolt://127.0.0.1:7687, auth(neo4j, 123456)) # 第一步看全库有多少节点 total g.run(MATCH (n) RETURN count(n) AS c).data() print(节点总数:, total) # 第二步看目标实体是否存在name 改成问题里抽出来的实体 target g.run(MATCH (n {name: $name}) RETURN n LIMIT 1, name胃痛).data() print(目标实体:, target)如果节点总数是 0那是建图没成功如果节点总数正常但目标实体查不到问题在实体归一化。很多工程的dict目录里放着同义词表目的就是把用户口语映射到标准实体名这一步没做好后面全白搭。3.4 服务端口被占用或认证失败连接串该怎么统一现象运行main.py后提示连接失败或者在 Neo4j 的 HTTP 端口 7474 能打开页面但 Python 连 bolt 端口 7687 连不上。原因Neo4j 默认只开一个协议有时候用户在安装时只保留了 HTTP没启用 bolt更常见的是初始密码没改代码里写的neo4j/123456和实际密码不一致。解决先把连接信息收拢到一个变量或配置文件里。Graph(bolt://127.0.0.1:7687, auth(neo4j, 123456))这个写法里bolt://走 7687http://走 7474两者不要混用。如果你只确认了 HTTP 端口通就把连接串换成http://127.0.0.1:7474如果你装了新版 Neo4j第一次登录会强制改密码改完后再回到工程里同步密码。最省事的检查方式是用 Neo4j Browser 登录一次能登录说明账号密码没问题剩下的就是 Python 连接串的问题。3.5 清理图谱的后悔药clear_graph.py 的正确打开方式现象重复跑了几次build_medicalgraph.py发现节点数量翻倍或者问答结果出现重复答案。原因建图脚本虽然用了merge但如果数据里存在空字符串或关系名不一致仍然会产生“看起来相同其实不同”的节点。更常见的是有人为了图省事在脚本里改用create跑一次插一遍。解决clear_graph.py就是后悔药。它的核心逻辑通常是执行MATCH (n) DETACH DELETE n清掉所有节点和关系。但我建议不要只在出错时用它而是养成固定重建流程先清空再建图再数节点。如果你拿到的工程里没有这个脚本也可以自己写三行代码from py2neo import Graph g Graph(bolt://127.0.0.1:7687, auth(neo4j, 123456)) g.run(MATCH (n) DETACH DELETE n) print(已清空图数据库)注意这个操作没有确认机制一旦执行不可恢复。我在自己的环境里跑无所谓但如果你连的是团队共用的 Neo4j千万别直接执行先看一眼库里有没有别人建的数据。4. 把自然语言变成 CQL模板匹配与查询组合的实战细节前面把工程链路讲清楚了但真正的核心在四个解析模块里。很多知识图谱大作业最薄弱的地方就是这里因为建图很容易把问句变成图查询很难。这份资源用的是“关键词模板 规则解析”路线不是深度学习方案好处是依赖少、可解释性强适合答辩时一步步讲给人听。4.1 关键词模板表keyword_template.py 里的规则长得什么样模板表是整个问答系统的规则底座。常见结构是每种意图对应一组触发词和一条 CQL 模板。拿“症状查询”来说模板可能长这样# keyword_template.py 中的意图模板示例结构示意 TEMPLATES { query_symptom: { keywords: [有什么症状, 症状是, 临床表现, 会怎么样], cqltpl: ( MATCH (n {name: __ENTITY__})-[:HAS_SYMPTOM]-(s) RETURN s.name AS answer LIMIT 10 ) } }这里我故意把实体名写成了__ENTITY__占位符而不是 Python 的{}格式化。因为 CQL 里本身就有很多花括号比如{name: ...}如果用str.format()去填很容易因为花括号数量对不上而报KeyError。用replace(__ENTITY__, entity)看起来土但不会踩格式化坑。模板设计的另一个关键点是关键词不要过度重叠。比如“有什么症状”和“症状是”这两个模板如果匹配顺序不对用户问“这个病有什么症状”可能先被“有什么”这种泛关键词截胡导致意图偏到别的地方。我一般会给模板加优先级字段或者把更长、更具体的词放在前面匹配。4.2 question_analysis.py先分词再落实体最后锁意图这个模块是意图识别的核心它通常分三步走。第一步是实体识别从问句里找出疾病名或症状名常见做法是拿dict目录里的词表做最大正向匹配第二步是意图分类拿上一步的结果去和keyword_template.py里的模板关键词比对看命中哪类模板第三步是输出结构化信息返回(entity, intent)这样的元组给get_cql.py。这里最常见的坑是实体抽取和意图判断没有先后顺序。正确逻辑应该是先抽实体再用“去掉实体后剩下的问句部分”去匹配意图。比如“胃痛应该挂什么科”如果把整句拿去匹配“应该挂什么科”那实体就变成了“胃痛应该”显然是错的。常见做法是把实体词从问句里抠掉剩下的部分参与模板匹配准确率会高很多。# 伪代码思路先抽实体再匹配模板 def analyze(question): entity extract_entity(question) # 从 dict 词表里做最长匹配 rest question.replace(entity, ) # 去掉实体后的部分用于意图判断 intent match_intent(rest) # 和 TEMPLATES 的 keywords 比对 return {entity: entity, intent: intent}如果你把这段讲清楚答辩时老师基本不会再纠结你“为什么不用 BERT”。因为对一个小型垂直问答系统来说规则方法足够快而且每一条结果都能溯源这对知识图谱应用来说是一种优势。4.3 get_cql.py从实体对到 Cypher 语句的桥get_cql.py的价值在于屏蔽了模板细节。它接收question_analysis.py的结构化结果再从keyword_template.py里取出对应的 CQL 模板把实体填进去生成最终查询语句。这里有一个容易被忽视的点单实体问答和多实体问答的 CQL 结构完全不同。比如“胃痛有什么症状”是单实体查询一条MATCH就能解决但“胃痛和发烧一起应该挂什么科”就可能涉及两条路径甚至需要MATCH (a)-[...]-(b)再合并去重。很多工程的get_cql.py只处理了单实体遇到双实体就返回空。我实际用下来建议在模板设计阶段就把必须双实体才能答的问题单独列一批避免所有问题都挤在单模板里。给一个常见的单实体 CQL 生成逻辑# get_cql.py 核心伪代码 def get_cql(entity, intent): tpl TEMPLATES[intent][cqltpl] entity entity.strip().strip(的).strip(呢) # 清洗口语词 return tpl.replace(__ENTITY__, entity)清洗这一步非常关键。用户输入的“胃痛呢”“胃痛的话”“胃痛怎么办”都可能在实体前后带口语杂质。如果只做strip()清洗不干净CQL 查不到节点答案就是空的。我给这类函数加过很多次strip(的 呢 吗 啊 呀 应该)的代码表面看是在处理字符串实际上是在做规则层面的实体归一化。4.4 get_answer.py答案不是查出来就完还要做展示加工很多人以为答案就是从graph.run(cql)里取数据然后拼成字符串返回。实际上get_answer.py还要处理三件事去重、排序、兜底。图数据库里同一关系可能被重复创建导致结果里出现多个一模一样的症状名。去重逻辑一般写成# get_answer.py 结果整理伪代码 def make_answer(records): seen set() answers [] for r in records: val r.get(answer) if val and val not in seen: seen.add(val) answers.append(val) if not answers: return 我还没有学会这个问题换个说法试试 return 、.join(answers[:5]) # 最多给 5 条避免答案过长这个函数看起来简单但决定了用户体验的上下限。不加去重时用户问“感冒有什么症状”可能返回“发烧、发烧、咳嗽、咳嗽”第一眼就会让人觉得系统是坏的。我习惯在测试阶段专门用重复数据验证这一层看看make_answer是否能正确收敛。4.5 一个完整的问句走查从“某某病有什么症状”到返回文本把整条链路串起来假设用户输入“类风湿有什么症状”。question_analysis.py先从词表里抽出“类风湿”剩下的“有什么症状”命中query_symptom意图get_cql.py生成MATCH (n {name:类风湿})-[:HAS_SYMPTOM]-(s) RETURN s.name AS answerget_answer.py执行后拿到一组症状去重、截断、拼接成“关节肿痛、晨僵、关节畸形”返回。整个过程没有任何模型推理但每一步输出都可以打印出来调错非常方便。5. 跑起来只是开始启动顺序、数据校验与前端观测这套工程跑起来的门槛不高但要让它稳定复现启动顺序必须固定。我见过不少人把build_medicalgraph.py和main.py同时跑结果前端已经在请求接口了后台图谱还没建完自然全是空答案。正确的顺序应该是下面这样。5.1 启动前的四件事Neo4j、依赖、数据导入、端口检查第一确认 Neo4j 服务已经启动浏览器能打开 7474 页面。第二确认 Python 依赖安装完整至少要有py2neo、flask如果前端要调接口、flask-cors这类常见库。第三跑一次建图脚本。第四检查 7687 或 7474 从 Python 能不能连通。我习惯把这四件事写成一个启动清单而不是靠记忆。启动命令大概是# 终端 1确认 Neo4j 服务状态neo4j 安装路径按你自己环境调整 neo4j start # 终端 2第一次运行前先建图输出节点数量确认成功 python build_medicalgraph.py # 终端 3启动问答服务 python main.py这里有个细节neo4j start在 Linux 下是后台运行在 Windows 下通常需要打开 Neo4j Desktop 或neo4j console保持前台。如果你发现 Python 连不上先别怀疑代码先在终端里敲curl http://127.0.0.1:7474能返回 JSON 说明服务没问题。5.2 用一条命令跑通主流程再用脚本做回归main.py通常是一个命令行交互或 Web 服务。如果是命令行交互启动后直接输入问句看输出如果是 Web 服务访问static目录下的页面。但无论哪种形式我都建议额外写一个回归脚本把测试问句和期望答案放在一起每次改完模板后跑一遍# test_questions.py 回归测试样例自己加的文件 cases [ (感冒有什么症状, 咳嗽), (胃痛挂什么科, 消化内科), (高血压要注意什么, 低盐饮食), ] for question, expect in cases: result ask(question) # 调用你的问答接口 ok 通过 if expect in result else 失败 print(question, -, result, ok)这个脚本的价值在改模板时特别明显。很多时候你为了修一个问题改了一个关键词结果另外三个问题全被带偏。没有回归脚本这些问题要等到演示时才暴露非常被动。5.3 从日志和返回内容判断问题出在哪个环节如果答案是空或兜底话术先不要急着改模板按顺序打三行日志实体识别结果、意图识别结果、生成的 CQL。大多数问题一眼就能看出来。如果实体是空问题在dict词表如果实体正确但意图是空问题在keyword_template.py的关键词覆盖如果实体和意图都正确但查不到数据问题在build_medicalgraph.py的关系类型或节点名。把这套排查逻辑背下来比翻源码快得多。5.4 前端 static 目录里的交互层不是重点但不能缺这部分通常是一个简单的 HTML 页面通过 Flask 提供静态文件服务用户输入问题页面向后端发 Ajax 请求拿到答案后渲染在页面上。它不涉及核心算法但却是演示时最能加分的部分。如果static里的页面样式太旧你可以只改 CSS不用动后端逻辑。6. 再往前一步把模板问答扩成半开放问答的三种低成本做法如果你不满足于固定的“症状查科室、疾病查症状”模板想让它显得更聪明又不想上大模型可以从三个方向低成本扩展。第一个方向是扩充同义词表和停用词表。很多答非所问的问题根源是用户口语和知识图谱实体对不上。把“吃啥药”和“用药建议”映射到同一意图把“那个”“这个”“想了解”等干扰词提前过滤问答准确率能立刻提升一截。第二个方向是给get_answer.py加结果置信度。如果某类问题只查到一条结果就不加“可能”这类模糊词如果查不到就输出“建议咨询医生”而不是硬编。第三个方向是记录每次问答的失败日志每周看一眼哪些问句没匹配到意图再把高频问句补进模板。我见过最小的改动只靠这三个技巧就把一套固定模板系统撑过了一个完整课程周期。我记得某次内部评审前我没有先跑clear_graph.py就直接重建图谱导致旧关系和新增关系混在一起演示时同一个问题返回了六条重复答案。从那以后我每次拿到这类工程都强制走一遍“清空重建→单问验证→回归脚本”三步流程再去看前端效果。这套方法不仅适用于这份医疗问答资源也适用于任何 Neo4j 规则问答的工程项目。希望这篇拆解能让你少走几步弯路拿到资源后直接跑通主线再把力气花在值得深挖的意图解析和模板设计上。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站