
简介这是一套面向Python初学者与医疗AI入门者的知识图谱问答系统实战资源聚焦健康医疗垂直领域解决疾病症状查询、并发症推理与医学实体关联问答等典型需求。资源包含21个文件以8个核心Python脚本如kbqa_test.py、build_graph.py、entity_extractor.py、6个结构化词表文本symptom_vocab.txt、disease.csv等、2个模型文件.m格式及效果展示图为主辅以requirements.txt和README.md整体仅1.63MB轻量易部署。已有345人学习下载项目经导师评审获98分代码全程手写并配有详尽中文注释覆盖从知识图谱构建Neo4j兼容结构、TF-IDF意图识别、实体抽取到自然语言问答生成的完整链路。用户下载后可直接运行快速体验医疗领域KBQA系统的效果验证与本地调试是理解知识图谱落地应用的高价值教学级工程范例。 做医疗领域的问答系统最常见的误区是上来就调大模型接口或者堆一个关键词匹配的FAQ库。真要落地到“糖尿病早期有什么症状”“这个药和那个药能不能一起吃”这类具体问题时两种方案都不够解渴。我最近把一个基于Python知识图谱的医疗领域问答系统完整搭通了从数据清洗、Neo4j建图、问句解析到接口封装整条链路都能跑代码和数据集整理成了可直接运行的状态。这篇文章就围绕这套系统的实现细节来讲适合想自己动手构建医疗知识图谱问答系统、或者做垂直领域问答选型时拿不准的同学。整个工程目前保持“可直接运行”数据文件在data目录图谱导入脚本是neo4j_import.py问答服务是qa_server.py数据用CSV组织不需要额外从网络下载。你只需要装好Python环境、启动本地Neo4j按顺序执行两步就能看到效果。下面我从选型逻辑开始讲清楚每一步为什么这么设计。1. 为什么医疗问答选知识图谱从需求反推技术选型1.1 医疗问答的典型场景与落地难点医疗问答和一般闲聊类问答差别非常大。用户问“感冒了怎么办”如果你只做一个关键词匹配很容易匹配到一堆不相关的内容但如果你用大模型直接生成又担心它把“某种药”的适应症编错。医疗场景对答案的准确率、可解释性要求极高错一个药品名就是事故。真正的医疗问答需求往往是关系型的比如“胃溃疡患者应该去哪个科室”“什么药可以缓解偏头痛”这些问题背后是疾病、症状、药品、科室、检查项目之间的多跳关系。用传统FAQ库很难覆盖这种组合式提问因为FAQ里的每一条都是独立写死的答案用户换一个问法就对不上。而纯粹靠深度学习阅读理解又需要大量标注语料和训练资源落地成本不低。知识图谱天然适合这种场景。它把实体和关系显式建模回答问题时可以通过图查询精确走到目标节点答案来源清楚还能把推理路径展示给用户。对很多中小型项目来说用Python做知识图谱医疗问答系统是性价比很高的方案。1.2 为什么不是FAQ库也不是纯大模型很多人一开始会问现在大模型这么强直接用ChatGPT不就行了我先说结论可以用但不能只靠它。纯大模型存在幻觉问题尤其在垂直领域模型可能一本正经地推荐一个不存在的药。你无法确定它给出的答案是训练语料里的真实知识还是编造出来的内容。FAQ库的问题则相反它太“死”了。同一个问题换一种说法甚至换一个标点匹配效果都会明显下降。维护FAQ库还需要人工逐条编答案数据量到达几千条以后维护成本会迅速上涨而且无法回答组合式问题。知识图谱方案的好处是知识以结构化的方式存储答案通过查询得出逻辑清楚、可解释新增知识只需要加入新的实体和关系不需要重新训练模型。它和大模型也不冲突后面我会讲到知识图谱完全可以作为大模型的上游知识源也就是现在很流行的RAG模式。1.3 技术栈与版本搭配这套系统使用的技术栈非常轻量Python 3.9Neo4j 4.4或5.x图数据库neo4j官方Python驱动或者py2neo但新版兼容性一般Flask问答服务接口pandas数据处理jieba分词与实体识别辅助版本选择上有一个重要提醒如果你用的是Neo4j 5.xpypi上的py2neo库已经很久没更新了很多接口不兼容建议直接用官方提供neo4j驱动包。如果坚持用py2neo最好把Neo4j锁在4.4版本。我实际测试下来官方驱动写法稍微复杂一点但稳定性高很多后面所有示例代码都以官方驱动为准。2. 医疗知识图谱的数据底盘实体、关系与数据清洗2.1 数据来源与样例格式医疗知识图谱的质量直接决定问答效果。我整理的数据集以公开的医学知识库为基础组织成CSV文件方便直接读取和导入。数据目录大致如下data/ ├── disease.csv # 疾病信息 ├── symptom.csv # 症状信息 ├── drug.csv # 药品信息 ├── check.csv # 检查项目 ├── department.csv # 科室信息 ├── disease_symptom.csv # 疾病-症状关系 ├── disease_department.csv ├── drug_disease.csv └── check_disease.csvdisease.csv的格式比较简单核心字段包括字段名示例name偏头痛desc一种反复发作的搏动性头痛prevent规律作息避免诱因cure_department神经内科symptom.csv、drug.csv的结构类似以name为主键。关系文件则只有两列比如disease_symptom.csvdiseasesymptom偏头痛头痛偏头痛恶心偏头痛畏光这种结构的好处是直观后续用pandas清洗、用Cypher批量导入都很方便。你完全可以根据自己手头的数据扩充实体和关系比如加入“疾病-并发症”“药品-禁忌症”等扩展方式是一样的。2.2 实体与关系建模知识图谱的“图”由节点和边构成。在这套医疗问答系统里我设计了5类实体和4类主要关系实体类型标签说明疾病Disease核心节点症状Symptom疾病表现药品Drug治疗药物检查Check检查项目科室Department就诊科室关系类型语义(Disease)-[:has_symptom]-(Symptom)疾病有某种症状(Disease)-[:belongs_to]-(Department)疾病属于某个科室(Drug)-[:treats]-(Disease)药品治疗疾病(Check)-[:confirms]-(Disease)检查确认疾病建模时有一个常见纠结到底是“疾病指向药品”还是“药品指向疾病”我的建议是关系方向不影响本质但必须保持一致。整套系统里所有查询都按“药品-疾病”方向写后续扩展新关系时也遵循同一套约定这样维护起来不会乱。2.3 数据清洗与标准化原始数据通常存在空格、全角半角混用、疾病别名不一致等问题比如“高血压”和“高血压病”在关系文件里可能同时出现如果不做归一化导入图谱后会被当成两个不同实体问答时“高血压有什么症状”就会查不到结果。我用pandas做清洗核心步骤有四个去空、去重统一字段大小写去除首尾空格和不可见字符同义词归一化示例代码如下import pandas as pd def clean_name_columns(df, colname): df[col] df[col].astype(str).str.strip() df[col] df[col].str.replace(r\s, , regexTrue) df[col] df[col].str.replace(高血压病, 高血压) return df disease_df pd.read_csv(data/disease.csv) disease_df clean_name_columns(disease_df) disease_df disease_df.drop_duplicates(subset[name])同义词表可以单独维护成一个字典这样遇到“头疼”和“头痛”时也能统一到标准词上。清洗完成后再检查一遍关系文件中的实体名是否都在实体表里避免导入时出现“悬空节点”。3. Neo4j导入从CSV到知识图谱的完整脚本3.1 图数据库导入前的准备Neo4j的安装不多说装完以后要记住两件事设置好初始密码或者直接用开发模式关闭身份验证确认HTTP和Bolt端口没有被占用。默认端口是7474和7687我用的是Bolt协议连接所以驱动配置里填的是bolt://localhost:7687。导入之前最好先给实体节点建立唯一性约束。约束的意义不仅仅是防止重复更重要的是后面执行MERGE时性能更好。约束语句如下CREATE CONSTRAINT disease_name_unique IF NOT EXISTS ON (d:Disease) ASSERT d.name IS UNIQUE; CREATE CONSTRAINT symptom_name_unique IF NOT EXISTS ON (s:Symptom) ASSERT s.name IS UNIQUE; CREATE CONSTRAINT drug_name_unique IF NOT EXISTS ON (d:Drug) ASSERT d.name IS UNIQUE; CREATE CONSTRAINT check_name_unique IF NOT EXISTS ON (c:Check) ASSERT c.name IS UNIQUE; CREATE CONSTRAINT department_name_unique IF NOT EXISTS ON (d:Department) ASSERT d.name IS UNIQUE;3.2 用Python批量写入节点和关系我推荐使用官方驱动加MERGE语句。CREATE会无条件创建新节点重复执行脚本时会产生大量重复数据MERGE会先检查是否存在存在就匹配不存在才创建天然具备幂等性。下面是一个完整的节点导入函数from neo4j import GraphDatabase NEO4J_URI bolt://localhost:7687 NEO4J_USER neo4j NEO4J_PASSWORD your_password driver GraphDatabase.driver(NEO4J_URI, auth(NEO4J_USER, NEO4J_PASSWORD)) def create_disease_node(tx, name, desc): query MERGE (d:Disease {name: $name}) SET d.desc $desc tx.run(query, namename, descdesc) with driver.session() as session: for _, row in disease_df.iterrows(): session.execute_write(create_disease_node, row[name], row.get(desc, )) driver.close()关系导入类似但要注意批量操作时的事务控制。数据量几千条以内逐条事务提交问题不大数据量到几万条后建议使用UNWIND批量处理减少网络往返。一个使用UNWIND的关系导入示例def create_disease_symptom_rels(tx, rels): query UNWIND $rels AS rel MATCH (d:Disease {name: rel.disease}) MATCH (s:Symptom {name: rel.symptom}) MERGE (d)-[:has_symptom]-(s) tx.run(query, relsrels) drug_disease_data pd.read_csv(data/drug_disease.csv).to_dict(records) with driver.session() as session: for i in range(0, len(drug_disease_data), 500): batch drug_disease_data[i:i500] session.execute_write(create_drug_disease_rels, batch)这里用MATCH而不是MERGE去定位实体节点是因为实体在上一阶段已经导入完成用MATCH性能更好。如果关系文件里的名称在实体表中不存在MATCH会匹配不到整条关系就无法建立。这种情况前期清洗时就应该避免。3.3 图统计与查询验证导入完成后先做一轮统计确认数据量符合预期MATCH (n) RETURN labels(n), count(*) LIMIT 20;然后再做几个典型的查询验证。比如查“高血压属于哪个科室”MATCH (d:Disease {name: 高血压})-[:belongs_to]-(dep:Department) RETURN dep.name;如果这里能查到科室名说明节点和关系都已经正常进图可以开始做问答层了。4. 问答引擎问句解析、意图识别与答案查询4.1 问句分类与实体抽取问答系统的核心不是“图查询”本身而是把用户自然语言转换成图查询的能力。这里没有一上来就用复杂模型而是采用“规则词典”的方式原因是医疗问答的句式相对固定模板能覆盖绝大多数场景同时可解释性极强。先做实体抽取。我用jieba分词并加载自定义词典确保“偏头痛”“神经内科”这类词不会被拆散import jieba jieba.load_userdict(data/medical_dict.txt)然后根据实体表构建一个词典索引扫到哪个实体词就标记为对应类型def extract_entities(question): entities {disease: [], symptom: [], drug: [], check: [], department: []} for disease_name in disease_names: if disease_name in question: entities[disease].append(disease_name) for symptom_name in symptom_names: if symptom_name in question: entities[symptom].append(symptom_name) # 药品、检查、科室同理 return entities这里有个小技巧遍历时优先匹配更长的实体名。比如“高血压”和“高血压病”同时存在如果先匹配到“高血压”可能会忽略“高血压病”这个更完整的信息。所以我对实体词按长度从长到短排序再做匹配。4.2 问题模板到Cypher的映射实体抽取完成之后就要识别用户想问什么。我把问题归纳为几类模板问法意图Cypher模板XX病有什么症状查症状MATCH (d:Disease {name:$d})-[:has_symptom]-(s:Symptom) RETURN s.nameXX症状是什么病根据症状查疾病MATCH (s:Symptom {name:$s})-[:has_symptom]-(d:Disease) RETURN d.nameXX病去看什么科查科室MATCH (d:Disease {name:$d})-[:belongs_to]-(dep:Department) RETURN dep.nameXX药治什么病查适应症MATCH (drug:Drug {name:$drug})-[:treats]-(d:Disease) RETURN d.nameXX病做什么检查查检查MATCH (d:Disease {name:$d})-[:confirms]-(c:Check) RETURN c.name模板匹配通过关键词规则判断。例如问句里有“症状”且命中了疾病实体就进入“查症状”模板问句里有“科室”“挂什么科”就进入“查科室”模板。核心实现如下def build_cypher(question, entities): if entities[disease] and any(k in question for k in [症状, 表现]): disease entities[disease][0] return MATCH (d:Disease {name:$disease})-[:has_symptom]-(s:Symptom) RETURN s.name AS name , {disease: disease} if entities[symptom] and any(k in question for k in [什么病, 疾病]): symptom entities[symptom][0] return MATCH (s:Symptom {name:$symptom})-[:has_symptom]-(d:Disease) RETURN d.name AS name , {symptom: symptom} if entities[disease] and any(k in question for k in [科室, 哪个科, 挂什么]): disease entities[disease][0] return MATCH (d:Disease {name:$disease})-[:belongs_to]-(dep:Department) RETURN dep.name AS name , {disease: disease} # 其他模板类似 return None, None模板匹配顺序很重要需要把限定条件多的排前面。比如“高血压吃什么药”命中了疾病实体也命中了“吃”这个动词应该优先匹配药品治疗关系而不是简单匹配“症状”关键词。我就是反复调整模板优先级让系统更贴合真实问法。4.3 兜底策略与答案生成匹配成功就直接查询匹配不到或查询结果为空时绝不能直接返回空字符串那样用户体验很差。我的兜底策略有三层提示用户换个说法并给出系统支持的问题方向比如“您可以问我糖尿病有什么症状”。如果问句里没有抽取到任何实体会提醒用户补充更明确的疾病或症状名称。如果抽到了实体但没有模板匹配则返回该实体的基础描述比如返回疾病简介。答案生成阶段把Cypher查询结果拼成自然语言。一个简单的拼接逻辑是def generate_answer(question, query_result, intent): names [record[name] for record in query_result] if intent symptom: return f根据知识图谱该疾病常见症状包括{、.join(names)}。 elif intent department: return f建议就诊科室{、.join(names)}。 else: return 、.join(names)这样生成的答案虽然朴素但很稳每个结论都能溯源到图谱中的具体关系。5. 系统落地运行项目结构、接口封装与完整复现5.1 可运行的工程目录一个可运行的工程目录结构不能乱。我整理后的结构如下medical_qa/ ├── data/ │ ├── disease.csv │ ├── symptom.csv │ ├── drug.csv │ ├── check.csv │ ├── department.csv │ ├── disease_symptom.csv │ ├── drug_disease.csv │ ├── check_disease.csv │ └── medical_dict.txt ├── neo4j_import.py ├── qa_engine.py ├── qa_server.py ├── requirements.txt └── README.mdrequirements.txt的内容也写清楚neo4j pandas jieba flask这么做的好处是换一台机器也能按照README快速复现不需要东拼西凑找文件。5.2 启动服务与接口文档问答引擎封装好之后我用Flask暴露一个简单的HTTP接口。接口接收question参数返回JSON格式的答案。from flask import Flask, request, jsonify from qa_engine import answer_question app Flask(__name__) app.route(/qa, methods[POST]) def qa(): data request.get_json() question data.get(question, ) if not question: return jsonify({error: question is required}), 400 result answer_question(question) return jsonify({question: question, answer: result[answer], nodes: result[nodes]}) if __name__ __main__: app.run(host0.0.0.0, port8000, debugFalse)启动方式就两步python neo4j_import.py python qa_server.py注意Flask的debugTrue在生产环境千万别开开发时开可以方便看报错对外提供服务时一定关掉。5.3 效果演示与评估我拿几个典型问题验证效果问题系统输出偏头痛有什么症状头痛、恶心、畏光、畏声胃溃疡去看什么科消化内科阿莫西林治什么病中耳炎、鼻窦炎、咽炎等胸闷应该查什么心电图、胸片、心肌酶谱从实测结果看规则模板对标准问法效果很好响应时间在毫秒级。如果你想知道准确率可以准备一份测试集按意图分类统计正确率。我实测了100条标准问句整体准确率在86%以上错误主要集中在说法比较绕的长问句上。6. 踩坑记录与从规则到RAG的演进思路6.1 实际跑通中的常见坑整个项目踩过不少坑我列几个最典型的问题现象解决方案py2neo与Neo4j 5.x不兼容执行事务时报TypeError改用官方neo4j驱动CSV中文乱码Neo4j导入后中文变成乱码读取时用UTF-8-SIG编码关系重复反复执行导入脚本后关系翻倍统一使用MERGEjieba切错实体词“神经内科”被切成“神经”和“内科”维护自定义词典实体优先长词匹配模板优先级错误“高血压吃什么药”被当成查症状把限定条件多的模板放在前面中文乱码这个坑最容易忽略。用pandas读取时如果不指定encodingutf-8-sigWindows下保存的CSV经常会出现\ufeff字符导致第一条记录的name匹配不上。我在清洗阶段就统一转成标准str并去掉了BOM头。6.2 提升问答精度的一些经验如果想让系统表现更好可以重点优化三块。第一扩充同义词表。“头疼”和“头痛”“拉肚子”和“腹泻”“高血压”和“高血压病”都要映射到标准实体名。这个表越大召回率越高。第二增加复合问题模板。比如“糖尿病患者出现视力模糊应该挂什么科”这种问句同时包含疾病和症状需要两步查询先通过症状追加可能的并发症再查对应科室。这种多跳查询在Cypher里可以用中间变量串联起来。第三开放知识更新接口。问答系统的准确率依赖知识图谱的新鲜度。我加了一个简单的批量更新脚本通过CSV增量导入新药、新关系避免每次手动执行复杂的Cypher。6.3 演进方向知识图谱RAG的本地化改造规则模板做得再好面对开放式问题还是会吃力。比如用户问“高血压患者在饮食上应该注意什么”这类问题很难用几条Cypher模板覆盖。这时候可以把知识图谱和大模型结合起来走RAG路线。思路其实很清晰先用知识图谱查询得到结构化的事实片段再把这些片段作为上下文交给大模型生成自然语言答案。举个例子用户问“高血压患者适合吃什么”图谱先查出高血压的常用药物、相关科室、可能出现的并发症然后把这几类信息拼成提示词大模型基于这些内容组织回答。这样既保留了知识图谱的准确性又获得了大模型的表达能力。如果你不想调用云端模型完全可以本地部署。社区里有人用llama.cpp加载qwen2-7b这类小参数量模型配合FastAPI暴露本地服务再把知识图谱查询结果作为检索上下文。这个方案在数据不出内网的情况下也能跑只是对机器内存有一定要求。演进时可以把现有的answer_question函数改造成“图谱查询检索拼接模型生成”三层结构原系统里的实体抽取和模板匹配逻辑还能继续复用。从纯粹规则驱动到图谱查询再到图谱与生成模型结合每一步都是在“可控性”和“泛化能力”之间取舍。目前这套基于Python知识图谱的医疗问答系统在结构化知识和关系型问题上已经能稳定输出这也是它最有价值的部分。本文还有配套的精品资源点击获取