☰
知识图谱医疗问答系统拆解:Neo4j与Python实战指南
2026/10/9 14:07:25 网站建设 项目流程

简介:一套以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", name=head), "Entity", "name") tail_node = g.merge(Node("Entity", name=tail), "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 "py2neo==4.4.0"这种形式锁定一个版本。如果代码里用的Node.merge老接口,要么降低 py2neo 版本,要么把建图脚本改成graph.merge(node, "Entity", "name")的新接口。

3.2 中文乱码与 CSV 编码:Windows 环境的经典坑

现象:数据导入后,Neo4j Browser 里节点名显示成åé¢ç,或者 Python 读 CSV 时直接抛UnicodeDecodeError: 'gbk' codec can't decode byte。

原因:Windows 下默认编码是 GBK,而大多数开源医疗数据是 UTF-8。用open()不指定编码时,Python 会用系统默认编码读文件,遇到中文就翻车。

解决:读文件统一写成open(path, 'r', encoding='utf-8-sig'),注意utf-8-sig比utf-8更稳,它能自动处理文件开头的 BOM 头。如果数据是别人用 Excel 编辑后另存的,建议在代码里同时处理utf-8和utf-8-sig两种编码,或者先打开文件看一眼字节内容再决定。写 CSV 时也指定encoding='utf-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://走 7687,http://走 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 answer;get_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 + 规则问答的工程项目。希望这篇拆解能让你少走几步弯路,拿到资源后直接跑通主线,再把力气花在值得深挖的意图解析和模板设计上。

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

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

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

立即咨询