简介QASystemOnHepatopathyKG-master.zip是一套使用Python实现的肝病知识图谱问答系统完整工程包面向医疗信息检索、知识图谱构建和自然语言处理方向的开发者适合用做毕业设计、课程项目或入门实战。压缩包内共28个文件以9个py源码、8个txt文本、5个xml配置、3个json数据为主并附有说明文档和licensepy模块覆盖数据预处理、医疗图谱构建、问句意图分类、语义解析、答案检索与问答对话等环节txt/json/xml则存储实体词表、关系配置和知识数据。包体仅9.31MB目前已有113人学习下载。工程按完整问答链路组织可直观掌握从原始数据清洗、实体关系构建到自然语言问句匹配的落地方法模块划分清晰、便于二次开发与调试能为理解知识图谱在医疗场景中的实际应用提供很好的参照。1. QASystemOnHepatopathyKG 是干什么的把肝病问句变成图谱查询如果你拿到的是QASystemOnHepatopathyKG-master.zip那大概率是想在本地复现一个“能回答肝病问题”的问答系统。这个系统的核心不是聊天而是把“乙肝患者转氨酶偏高该注意什么”这类自然语言问句拆成意图和实体再映射到 Neo4j 图谱上执行查询、组织答案。它解决的典型痛点是医学指南和教科书写得分散同一个问题在不同页面里翻半天而图谱能把疾病、症状、检查、药物之间的关系一次性关联起来。适合正在做医学知识图谱、垂直领域问答系统或者想拿一个完整项目练手的中级开发者——尤其适合被“图谱怎么落地”卡住的人。2. 肝病知识图谱怎么搭本体设计、实体对齐与 Neo4j 导入问答系统后面没有图就像检索系统后面没有索引。肝病知识组织成图之后“乙肝-表现-黄疸-检查-胆红素”这样的关系才能被一条 Cypher 语句查出来。这一章先解决图谱本身怎么建再解决数据怎么进 Neo4j。2.1 肝病本体的实体与关系设计以“疾病-症状-检查-药物”为骨架肝病领域的本体设计不需要做成通用医学知识图谱那么复杂抓住问答里最常出现的几个类型就够了。常见做法是把实体分成六大类疾病、症状、检查指标、药物、饮食建议、传播途径。属性上保留一个“标准名”字段再挂一个“别名”列表后续问答阶段做实体链接要用。实体类型建议属性示例疾病标准名、别名、发病部位、是否传染乙肝、慢性乙型病毒性肝炎症状标准名、别名、表现位置黄疸、乏力、肝区疼痛检查指标标准名、单位、参考范围谷丙转氨酶(ALT)、胆红素药物标准名、别名、适应症恩替卡韦饮食建议标准名、适用场景低脂饮食、高蛋白饮食关系设计的核心是“问答里需要什么路径就建什么关系”。不要追求把所有实体之间都连接起来否则图谱复杂度和查询维护成本会成倍涨。我一般会问自己一个问题如果用户问“乙肝可以吃什么药”最短的路径是什么答案是“疾病-推荐用药-药物”。这样推到后面的问题比如“乙肝有哪些症状”“黄疸需要查什么”就能把关系清单控制在 810 条以内。推荐用药、推荐检查、表现症状、属于分类、饮食推荐、传播方式这几个关系是肝病问答里出现频率最高的。每条关系都建议带上一个“来源”属性用来标注数据来自指南还是百科这在后面排查答案质量时非常有用。2.2 从结构化表格到三元组合并节点的正确姿势数据源一般是一张 CSV 或者 Excel 表格常见结构是每行一个关系三元组主体、关系、客体。需要抽出一段独立的导入脚本把 CSV 读进来然后用 Cypher 的 MERGE而不是 CREATE写入 Neo4j。MERGE 的优先级是“有则匹配无则创建”这样能避免同一节点的重复创建。import csv from neo4j import GraphDatabase # 连接 Neo4j替换成你的实际地址和密码 driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, your_password)) def import_triple(tx, head, head_type, rel, tail, tail_type): # 注意这里只用 MERGE重复导入不会产生重复节点 cypher MERGE (h:{ht} {{name: $head}}) MERGE (t:{tt} {{name: $tail}}) MERGE (h)-[r:{rel}]-(t) .format(hthead_type, tttail_type, relrel) tx.run(cypher, headhead, tailtail) with open(hepatopathy_triples.csv, encodingutf-8) as f: reader csv.DictReader(f) with driver.session() as session: for row in reader: session.execute_write( import_triple, row[head], row[head_type], row[relation], row[tail], row[tail_type] ) driver.close()这段代码里有两个细节值得注意。第一节点类型的格式化直接拼接进 Cypher 语句因为类型名是程序内部白名单不是用户输入但节点的 name 参数化传参目的是防注入。第二MERGE 需要同时给节点和关系指定标签也就是说如果此前用 CREATE 导入过关系再来跑这段脚本就不会生成重复节点但关系如果没声明唯一性重复执行可能会产生多条相同关系需要在 Neo4j 里做去重。导入完成后做一次抽样验证随便查一条“疾病-推荐用药”路径确认图谱中确实有数据返回而不是空库。MATCH (d:Disease)-[:推荐用药]-(dr:Drug) RETURN d.name, dr.name LIMIT 20;如果这条查询返回了结果说明图谱的数据层已经就位。下一步要处理的是数据质量问题——实体对齐和属性规范化。2.3 实体对齐与属性规范化决定问答命中率的一步图谱构建看起来简单但真正的分水岭在实体对齐。不考虑对齐用户问“乙肝”时图谱里存的是“慢性乙型病毒性肝炎”匹配不上问答系统直接空手而归。常见做法是准备一张“同义词映射表”把所有不同叫法统一指向一个标准名。# 同义词表key 是别名value 是标准名 synonym_map { 乙肝: 慢性乙型病毒性肝炎, 慢性乙肝: 慢性乙型病毒性肝炎, 乙型肝炎: 慢性乙型病毒性肝炎, 转氨酶偏高: 转氨酶升高, 谷丙转氨酶: 丙氨酸氨基转移酶, } def normalize_entity(text): return synonym_map.get(text.strip(), text.strip())这段代码看起来很简单但要注意两点一是同义词表不要只覆盖疾病名还要覆盖症状和检查指标否则问“我转氨酶偏高是什么问题”时仍然对不上考点二是如果有用户会打错字或省略字比如“恩替卡伟”和“恩替卡韦”这种噪音不是靠这张表能完全解决的需要额外做一次基于编辑距离的模糊映射但先别一上来就加否则会引入错误匹配。属性规范化同样影响答案质量。检查指标常见的问题有两种单位不统一比如胆红素有的写 μmol/L有的写 mg/dL参考范围格式不一致有的写“5.1-17.1”有的写“小于 21”。在导入之前把单位统一成一套体系后续做数值范围判断时才不会出现“黄疸判定条件互相矛盾”的尴尬情况。这里我习惯写一个预处理函数把所有单位字段统一转成小写加标准写法同时把范围拆成 min 和 max 两列方便后续比较。3. 问答引擎的核心链路意图识别、实体链接与 Cypher 模板生成图谱就位之后问答系统的核心是三段式流水线先说清楚用户问的是什么类型的问题再把问题里的关键词映射到图谱节点最后把前面两者组合成一个可执行的 Cypher 查询。3.1 意图识别先定问题类型才能决定查询语句结构肝病问答里最常见的意图有四类治疗、症状、检查、饮食。用户问“乙肝吃什么药”是治疗意图“得了乙肝会有什么表现”是症状意图“需要做什么检查”是检查意图“平时饮食上有什么注意”是饮食意图。不用一上来就上 BERT 这类预训练模型。医学问答的公开标注数据很难找少量样本微调不出可靠效果。常见做法是先用关键词规则兜底把常见问法和高频词枚举出来。等规则覆盖不住的时候再考虑模型升级。intent_rules { 治疗: [吃什么药, 用什么药, 治疗, 服用, 禁忌药], 症状: [症状, 表现, 什么感觉, 会不会传染], 检查: [检查, 检测, 指标, 查什么, 确诊], 饮食: [吃什么, 饮食, 忌口, 能吃], # 注意“吃什么药”和“吃什么”的区别 } def detect_intent(question): # 有重叠时按优先级返回治疗优先于饮食否则“吃什么药”会匹配错 priority [治疗, 症状, 检查, 饮食] for intent in priority: for keyword in intent_rules[intent]: if keyword in question: return intent return 其他这个规则版本有两个明显的坑代码里我用注释标了一个。第一个坑是“吃什么药”和“吃什么”都会命中“饮食”关键词“吃什么”所以需要把“治疗”意图放在优先级最前面。第二个坑是“会不会传染”从字面上看像症状但语义上是传播方式如果图谱里建了“传播方式”关系就需要给它单独建一个意图类型避免归到症状以后生成错误查询。对源码包里已经写好的模型型意图分类器可以直接调用它的预测函数但规则版本也不应该删掉——它是模型无法覆盖的长尾问法的兜底方案。在工程落地时两种方案并行是常态。3.2 实体链接用字典匹配而不是纯靠分词实体链接这一步最容易犯的错误是直接用分词工具分完再查库。分词工具可能把“乙型肝炎”切成“乙型”和“肝炎”这种切碎之后的片段往往查不到图谱节点。更可靠的做法是用 AC 自动机做基于词表的匹配一次扫描就能命中所有出现在问句里的实体。import pyahocorasick # 构建自动机把图谱里所有标准名和别名都加进去 entity_index {} automaton pyahocorasick.Automaton() def add_entity(etype, name, alias_list): for alias in [name] alias_list: automaton.add_word(alias, (etype, name)) entity_index[alias] (etype, name) # 最终构建 automaton.make_automaton() def link_entities(question): linked [] for _, (etype, std_name) in automaton.iter(question): linked.append({type: etype, std_name: std_name}) # 去重保留第一次出现的意图类型 seen set() result [] for item in linked: if item[std_name] not in seen: seen.add(item[std_name]) result.append(item) return result这个方案的参数关键在于词表覆盖范围。图谱里有标准名但用户常说的可能是别名所以构建自动机时要把别名也加进去。另外自动机匹配有重叠时可能一次返回多个结果比如“乙肝”和“乙型肝炎”同时命中假设两者都有的时候需要按长度排序保留最长的匹配结果。代码里我没有写这段实际使用时建议加上“优先最长匹配”的规则。有些项目里还会加一步把匹配到的实体跟用户问题上下文做一次打分判断“这个实体是不是问题真正关心的对象”。比如“乙肝患者的转氨酶偏高”中图谱可能同时匹配到“乙肝”和“转氨酶”两个实体都有用但一个是疾病主语一个是检查指标宾语。这类消歧在后续生成查询语句时需要结合实体类型来决定谁是起点、谁是条件。3.3 模板到 Cypher把“意图实体”组合成可执行查询意图告诉系统“查什么关系”实体告诉系统“从哪个节点出发”。两者组合以后根据意图类型选定关系方向和目标实体类型然后拼出 Cypher 语句。这一步不建议直接用字符串拼接而是维护一个“意图模板字典”。cypher_templates { 治疗: MATCH (d:Disease {{name: $name}})-[:推荐用药]-(n:Drug) RETURN n.name AS answer, 症状: MATCH (d:Disease {{name: $name}})-[:表现症状]-(n:Symptom) RETURN n.name AS answer, 检查: MATCH (d:Disease {{name: $name}})-[:推荐检查]-(n:CheckItem) RETURN n.name AS answer, 饮食: MATCH (d:Disease {{name: $name}})-[:饮食推荐]-(n:Diet) RETURN n.name AS answer, } def generate_query(intent, entity_name): try: template cypher_templates[intent] except KeyError: return None # 这里用参数化查询而不是直接 format避免 Cypher 注入 return template.format(nameentity_name), {name: entity_name}模板的局限在于组合问句“乙肝患者转氨酶高需要做什么检查”同时包含疾病和检查指标两个实体单一模板处理不了。常见做法是把这类复杂问句拆成两个子问题分别查再把结果合并。例如先查“乙肝”-推荐检查得到候选检查列表再查“转氨酶”关联的指标名称两者取交集得到更精确的答案。执行查询时要注意统一走参数化接口不要直接把实体名字嵌入字符串。等你上线以后会发现用户问题里什么符号都有——括号、引号、斜杠参数化能挡掉一大部分异常。返回结果后统一只取第一列作为答案并且保留来源属性来回答“为什么是这个答案”。4. 把 master 分支的代码跑起来依赖、环境初始化与启动顺序拿到源码包之后最忌讳的是直接python main.py然后面对一整屏报错。正确顺序是先隔离 Python 环境再确认 Neo4j 实例前置要求接着初始化图谱数据最后才启动问答服务。4.1 环境隔离与依赖检查先用虚拟环境兜底绝大多数这类项目在 README 里会写依赖清单但拿到手后直接装全局环境很容易跟系统里已有的包版本打架。我一般会在项目根目录下新建虚拟环境再统一安装依赖。unzip QASystemOnHepatopathyKG-master.zip cd QASystemOnHepatopathyKG-master # 创建并激活虚拟环境Windows 用 venv\Scripts\activate python -m venv venv source venv/bin/activate # 如果有 requirements.txt 就安装没有就手动装下面几个核心包 pip install neo4j pyahocorasick flask jieba关于依赖有一个经常踩坑的点项目可能是在 Python 3.6 时代写的而你现在用的是 3.10。neo4j 官方驱动的 API 在 4.x 版本里发生过一次大的变更最常见的是graph.run()方法被移除。如果代码里调用的是session.run()这种新 API 就没有问题如果用的是旧 API会直接报 AttributeError。遇到这种情况最快的处理方式是查看项目里 import 的是哪个驱动包如果是py2neo那么很多 connect 参数需要按 py2neo 的写法来跟官方驱动不互通。还有jieba这类包在 Python 3.10 下通常没问题但ahocorasick很可能需要编译或安装pyahocorasick这个改名后的版本。安装报错时先检查是不是 pip 源里找不到再检查是不是缺少 Visual C 编译环境。4.2 准备 Neo4j 并完成图谱初始化Neo4j 推荐使用 4.x 社区版下载并启动后先修改初始密码。源码包里的配置文件或者环境变量里一般会写明连接的 URI、用户名、密码三个参数不同作者习惯不同。常见做法是通过.env文件或者项目根目录的config.py来统一管理。# config.py 示例实际操作时改成项目里实际使用的配置文件 NEO4J_URI bolt://localhost:7687 NEO4J_USER neo4j NEO4J_PASSWORD your_password改完配置后先不要急着启动问答服务先确认网络能连通 Neo4j写一段最小脚本测试连接。这一步能帮你把“配置写错”和“代码跑错”这两类问题分开避免后面定位问题时花掉半天时间。from neo4j import GraphDatabase driver GraphDatabase.driver( bolt://localhost:7687, auth(neo4j, your_password) ) with driver.session() as session: result session.run(RETURN 1 AS ok) print(result.single()[ok]) # 输出 1 表示连接成功 driver.close()如果返回 1说明连接没问题。接着运行图谱初始化脚本。具体入口文件名可能叫build_graph.py或init_kg.py这类脚本通常做的事是清空已有数据、导入实体和关系、创建图谱索引。运行完以后用 2.2 节里的抽样查询做验证确认节点数和关系数不为零。4.3 用最小样例跑通问答链路图谱数据就位后启动问答服务。项目的入口可能是 Flask 应用也可能是一个命令行交互脚本。先用手动调用问答函数的方式验证链路再决定要不要启动 HTTP 服务。# 常见方式直接调用问答核心函数 python -c from qa_engine import answer; print(answer(乙肝吃什么药))如果看到返回了一串药物名称说明核心链路已经通了。这一步不用纠结服务响应快慢先确认逻辑正常。如果返回了空结果先不要怀疑代码按第五章的排查步骤去检查图谱数据和实体对齐配置。最后再启动 Web 服务。常见框架是 Flask启动方式一般是python app.py然后打开http://localhost:5000输入问题做交互测试。验证时不要只测个“你好”要按照“疾病推荐药、疾病查症状、指标查检查组合”这几个意图各测一遍确保不是只对某一个写死的问句有效。5. 避坑肝病问答源码包落地时的五个常见问题源码包能跑通只是起点真正麻烦的是跑通以后的各种边界情况。下面这些坑都是这类项目里反复出现的按“现象 → 原因 → 解决”记下来遇到的时候能少走很多弯路。5.1 问句里的疾病名对不上图谱节点现象用户问“乙肝吃什么药”系统回答“没有找到相关答案”但图谱里明明有乙肝节点。原因问句表达的疾病名是别名“乙肝”图谱节点名存的是标准名“慢性乙型病毒性肝炎”。实体链接阶段没有把两者映射起来关键词直接落空。解决建立同义词表或者在链接前先做一步别名归一化。写一个normalize_entity()函数把常见别名映射到标准名并把它放在实体链接流程最前面。做完之后重新跑一遍测试问句确认命中。5.2 Neo4j 连接失败但代码没问题现象问答服务启动时报ServiceUnavailable或AuthError项目代码一行没改。原因最常见的是 Neo4j 的访问协议版本不匹配。4.x 默认用 bolt 协议但有的项目配置里写的是http://localhost:7474这是浏览器界面的地址不是驱动程序连接地址。另外密码里如果带有或#没有做 URL 编码也会导致鉴权失败。解决检查配置文件里的连接串是否以bolt://开头同时确认密码与 Neo4j 实际设置的密码一致。修改密码后Neo4j 有时不会立刻在旧连接池上生效重启一下问答服务再试。如果用了 Docker 部署 Neo4j还要确认宿主机端口是否真的映射到容器了。5.3 master 分支代码与本地 Python 版本不兼容现象跑初始化脚本时报AttributeError: module time has no attribute clock或者某处 import 直接失败。原因这类项目代码可能在旧版 Python 上开发项目名里的 master 分支往往保留了最早一批代码没有做过新版本兼容。例如 Python 3.8 移除了time.clockPython 3.10 开始对collections模块的导入方式有变化。解决先用python --version确认本地版本。如果发现不兼容优先建一个指定版本的环境来跑而不是改源码。比如用 conda 创建一个 Python 3.7 的环境把这套代码放在里面运行改造成本最低。修改旧代码的时候顺手用 Git 在本地新建一个分支再改不要直接动 master 分支本体——revert 的时候就知道后悔药在哪里了。5.4 中文分词把实体切碎导致匹配失败现象问句是“乙型肝炎患者的乏力症状明显”实体链接后识别出来的是“型肝”之类的不存在的词或者干脆一个实体都没匹配到。原因jieba 默认词典是通用语料训练出来的把它放在垂直领域里“乙型肝炎”被切成“乙型”和“肝炎”都算正常因为通用词典里不一定有这个词。解决在程序启动时把领域内所有标准名和别名加入 jieba 词典用jieba.add_word(乙型肝炎)逐个添加或者把整张同义词表一次加载进来。更省事的方案是跳过 jieba 分词直接用 AC 自动机做词典匹配绕开分词这个环节。5.5 生成了 Cypher 语句但查询结果为空现象日志显示意图识别正确、实体链接命中、Cypher 语句也执行了但返回列表为空。原因图谱中关系方向存反了。比如数据源里写的是“药物-治疗-疾病”而查询模板用的是“疾病-推荐用药-药物”方向相反就查不到。这种问题不是程序逻辑错而是数据建模和模板不一致。解决写一个数据一致性检查脚本遍历模板里用到的每一种关系随机抽样一条路径打印出来检查方向。确认后要么改模板方向要么改数据导入脚本保持两者统一。还有一个容易被忽略的原因节点类型标签不对。图谱里节点标签叫Drug模板里写的却是Medicine大小写不一致也会查不到。6. 进阶把问答系统做成可量化的检索服务问答系统能回答几个固定问题不代表可用。真正要上线或者作为项目交付需要用测试集评估再通过 HTTP 接口对外提供服务。6.1 用测试集量化意图识别准确率规则版意图识别的优点是可控缺点是没法凭感觉说“它到底准不准”。我建议花二十分钟整理 30 到 50 条测试问句覆盖四类意图和若干长尾说法然后一键跑出分类报告。# 测试集样例每条是问句和期望意图 test_cases [ (乙肝患者能吃什么药, 治疗), (乙肝的早期症状有哪些, 症状), (查乙肝应该做哪些检查, 检查), (饮食上要注意什么, 饮食), (恩替卡韦有什么副作用, 其他), # 这个用例用来暴露规则盲区 ] correct 0 for question, gold_intent in test_cases: pred_intent detect_intent(question) is_right pred_intent gold_intent correct int(is_right) if not is_right: print(f误判: {question} - 预测{pred_intent}, 期望{gold_intent}) print(f准确率: {correct / len(test_cases):.2f})只看总准确率还不够要把预测错误的问题逐条看一遍判断是关键词缺失还是优先级设计有问题。上面测试集里最后一条“恩替卡韦有什么副作用”如果被误判成“治疗”说明副作用这个意图完全没有被覆盖。要不要加新意图取决于你的业务是否需要回答副作用类问题不要为了提升测试集分数而硬塞规则。6.2 在本地封装成 HTTP 接口并加上兜底返回命令行问答适合调试但想给别人用必须封装成接口。用 Flask 包一层注意加超时控制和空结果兜底。from flask import Flask, request, jsonify app Flask(__name__) app.route(/qa, methods[POST]) def qa(): data request.get_json() question data.get(question, ).strip() if not question: return jsonify({error: question 不能为空}), 400 # 调用问答核心链路 try: answer_text answer(question) # 返回字符串或列表 if not answer_text: return jsonify({question: question, answer: 暂时没有找到相关答案}) return jsonify({question: question, answer: answer_text}) except Exception as exc: # 这里记录完整异常到日志返回给用户的是一个友好提示 app.logger.error(fQA failed: {exc}, exc_infoTrue) return jsonify({question: question, answer: 系统繁忙请稍后重试}), 500 if __name__ __main__: app.run(host0.0.0.0, port5000)这里有两个细节值得强调。第一对外返回的答案一定不要直接抛异常堆栈否则用户能看到内部表结构和图谱节点名这在真实环境里是信息泄露隐患。第二启动时加一个“预热”步骤把自动机和词典加载都放在app.run()之前避免第一个请求进来时现场加载造成十几秒的超时假象。做这套系统以后我养成一个习惯每次改动同义词表、实体对齐规则或者意图优先级都要重新跑一遍那三五十条测试问句把准确率数字记下来。改规则之前先记基线改完对照看是提升还是回退。很多问答系统做着做着就不准了不是因为代码坏了而是因为规则越加越乱、新旧规则互相打架。有了基线数据至少能快速发现这种回退不用靠“感觉好像还行”这种玄学来验证。希望这套从图谱到查询的落地思路能帮你在跑通QASystemOnHepatopathyKG之后少踩几个坑。本文还有配套的精品资源点击获取