简介:这份资源是肝病知识图谱问答系统的完整源码包,面向具备Python基础、希望入门知识图谱与智能问答开发的开发者与学习者。项目以肝病领域为场景,将疾病、症状、治疗、药物等实体及其关联组织为结构化图谱,并在此基础上实现自然语言提问到答案检索的完整链路,适合作为医疗知识图谱与NLP方向的实战练手项目。压缩包共28个文件,约9.31MB,以9个Python脚本为核心,配合8个txt词典与数据文件、5个xml配置、3个json数据及说明文档,覆盖图谱构建、问句分类、问句解析、答案搜索与对话入口等模块,目录结构清晰,便于按流程阅读与二次开发。目前已有115人学习下载。通过该资源可掌握知识图谱的构建与查询思路、自然语言问句到图谱查询的映射方法,以及问答系统的整体工程组织方式,对提升Python开发与NLP应用能力有实际帮助。
1. 拿到 QASystemOnHepatopathyKG-master.zip 之后:肝病知识图谱问答到底怎么跑起来
很多人第一次看到 QASystemOnHepatopathyKG-master.zip 这个包名,第一反应是「又一个知识图谱 demo」,解压完发现里面既有 Neo4j 的导入脚本,又有前端页面,还有一堆没见过的实体词典,直接卡在第一步。这个项目解决的是一个很具体的问题:把肝病领域的实体、关系、属性组织成图结构,再让用户用自然语言提问,系统把问题解析成图查询语句,从 Neo4j 里捞出答案返回。它适合两类人:一类是想找一个完整 KGQA 链路练手的中级开发者,另一类是做医疗信息化、想把科室里的诊疗知识做成可查询系统的工程师。整条链路的核心不是模型多强,而是「实体识别准不准、意图映射对不对、Cypher 拼得对不对」这三件事。跑通它不需要 GPU,一台能装 JDK 和 Neo4j 的机器就够,但坑基本都藏在数据清洗和问句模板的边界上。
2. 肝病知识图谱的数据从哪来、怎么进 Neo4j
2.1 先看清数据文件的结构再动手
解压之后不要急着启动服务,先把数据目录翻一遍。这类 KGQA 项目的数据通常分三块:实体文件(disease、drug、symptom、food 等)、关系文件(三元组)、以及问答模板文件。实体文件一般是「实体名 + 实体类型」两列,关系文件是「头实体 + 关系 + 尾实体」三列,分隔符可能是逗号、制表符或者竖线,这一步必须先确认,否则后面导入全是脏数据。
我一般会先写一个探查脚本,把每个文件的编码、分隔符、行数、字段数打出来,避免直接导入后才发现某列错位。
# inspect_data.py import csv, os DATA_DIR = "./data" # 按实际解压路径改 for fname in os.listdir(DATA_DIR): path = os.path.join(DATA_DIR, fname) if not os.path.isfile(path): continue with open(path, "r", encoding="utf-8") as f: # 先读前 5 行看结构,不假设分隔符 head = [next(f).rstrip("\n") for _ in range(5)] print("=" * 40) print("file:", fname) for line in head: print(repr(line))逻辑说明:这段脚本不做任何解析假设,只把原始行 repr 出来,让你肉眼判断分隔符到底是\t还是,,以及有没有表头。参数上DATA_DIR指向解压后的数据目录,range(5)控制预览行数,数据量大时不要一次全读进内存。
确认结构后,把实体和关系整理成 Neo4j 能吃的 CSV。常见做法是统一成entity.csv(name, type)和relation.csv(head, relation, tail),编码统一 UTF-8 无 BOM。
2.2 Neo4j 导入:LOAD CSV 的写法与索引
Neo4j 导入有两种路子:LOAD CSV适合中小规模、可反复调试;neo4j-admin import适合千万级、要求停机。肝病知识图谱这种体量,LOAD CSV完全够用,而且改起来快。
// 建唯一约束,避免重复导入产生重复节点 CREATE CONSTRAINT entity_name IF NOT EXISTS FOR (n:Entity) REQUIRE n.name IS UNIQUE; // 导入实体 LOAD CSV WITH HEADERS FROM 'file:///entity.csv' AS row MERGE (n:Entity {name: row.name}) SET n.type = row.type; // 导入关系,先匹配两端节点再建边 LOAD CSV WITH HEADERS FROM 'file:///relation.csv' AS row MATCH (h:Entity {name: row.head}) MATCH (t:Entity {name: row.tail}) MERGE (h)-[r:REL {type: row.relation}]->(t);逻辑说明:MERGE而不是CREATE,是为了幂等——重复执行不会产生重复节点。CREATE CONSTRAINT那行是性能关键,没有唯一约束时MERGE会全表扫描,几万行数据就能让你等到怀疑人生。参数上,CSV 文件要放到 Neo4j 的import目录下,file:///后面跟文件名,路径写错会直接报找不到文件。
导入完做一次校验,确认节点数和关系数对得上:
MATCH (n:Entity) RETURN count(n) AS node_count; MATCH ()-[r:REL]->() RETURN count(r) AS rel_count;如果 node_count 明显小于源文件行数,八成是实体名里有空格或特殊字符导致 MERGE 合并了,回去查数据清洗环节。
2.3 实体类型和关系的设计取舍
很多新手会纠结「症状」到底算实体还是算疾病的属性。我的经验是:只要它需要被单独查询、或者会和其他实体产生关系,就建成节点;如果只是描述性文本、永远不参与匹配,就放属性。肝病场景里,症状、药品、检查项、科室都值得建节点,而「疾病简介」这种长文本放属性更合适。
关系命名也要统一,别一会儿has_symptom一会儿症状,后面拼 Cypher 时大小写和命名风格不一致,是排查起来最烦的一类问题。建议全部小写下划线,关系类型在代码里用常量管理。
3. 问句怎么变成 Cypher:意图识别与模板映射
3.1 意图分类的最小可用方案
KGQA 的核心难点是「用户问法千变万化,但图查询只有那么几种」。这个项目里常见的意图有:查疾病的症状、查症状对应的疾病、查疾病的用药、查药品的适应症、查疾病的检查项。最省事的做法不是上大模型,而是「关键词 + 模板」先跑通。
# intent.py INTENT_RULES = [ (["症状", "表现"], "disease_to_symptom"), (["吃什么药", "用药", "药物"], "disease_to_drug"), (["什么病", "哪些病", "会导致"], "symptom_to_disease"), (["检查", "化验"], "disease_to_check"), ] def detect_intent(question: str): for keywords, intent in INTENT_RULES: if any(kw in question for kw in keywords): return intent return "unknown"逻辑说明:按关键词优先级从上到下匹配,先命中先返回。参数上INTENT_RULES的顺序很重要,比如「症状」和「什么病」可能同时出现,谁先谁后决定结果。这套方案召回率高但精确率一般,适合先跑通链路,后面再换成分类模型。
3.2 实体抽取:词典匹配够不够用
意图定了,还得知道用户问的是哪个病、哪个药。最直接的办法是拿实体词典做匹配,把问句里出现的实体名捞出来。
# ner.py def extract_entities(question: str, entity_dict: dict): hits = [] for name, etype in entity_dict.items(): if name in question: hits.append((name, etype)) # 长实体优先,避免「肝炎」把「乙型肝炎」切碎 hits.sort(key=lambda x: len(x[0]), reverse=True) return hits逻辑说明:entity_dict是「实体名 -> 类型」的映射,从 Neo4j 或本地文件加载。排序那行是关键——如果词典里同时有「肝炎」和「乙型肝炎」,不按长度排序就会先匹配到短的,导致实体识别错误。参数上,词典越大匹配越慢,实体过万时建议换成 Aho-Corasick 或前缀树。
3.3 模板填充与 Cypher 生成
意图和实体都有了,接下来就是拼查询。每种意图对应一个 Cypher 模板,把实体名填进去。
# cypher_builder.py TEMPLATES = { "disease_to_symptom": ( "MATCH (d:Entity {name: $name})-[:REL {type: 'has_symptom'}]->(s) " "RETURN s.name AS answer" ), "disease_to_drug": ( "MATCH (d:Entity {name: $name})-[:REL {type: 'has_drug'}]->(s) " "RETURN s.name AS answer" ), } def build_cypher(intent: str, entity_name: str): tpl = TEMPLATES.get(intent) if not tpl: return None, None return tpl, {"name": entity_name}逻辑说明:用参数化查询$name而不是字符串拼接,一是防注入,二是 Neo4j 能缓存执行计划。参数上entity_name必须和 Neo4j 里存的完全一致,差一个空格都查不到,所以前面实体抽取时最好做一次去空格和全半角归一化。
3.4 把链路串起来跑一次
# qa_pipeline.py from intent import detect_intent from ner import extract_entities from cypher_builder import build_cypher def answer(question, entity_dict, run_cypher): intent = detect_intent(question) ents = extract_entities(question, entity_dict) if not ents: return "没识别到实体" name, _ = ents[0] cypher, params = build_cypher(intent, name) if not cypher: return "没匹配到意图" return run_cypher(cypher, params)逻辑说明:run_cypher是外部传入的执行函数,方便替换成真实 Neo4j 驱动或测试桩。参数上ents[0]只取第一个实体,多实体问句需要额外处理,这是这套最小方案的边界。
4. 避坑与排查:跑不通时先看这几处
4.1 导入后查不到数据
现象:Cypher 明明写对了,返回空。原因:CSV 里有 BOM 或首行表头被当成数据,导致实体名带了不可见字符。解决:用LOAD CSV WITH HEADERS并确认文件是 UTF-8 无 BOM,必要时在导入前用脚本 strip 掉\ufeff。
4.2 实体匹配总是匹配到短的
现象:问「乙型肝炎的症状」,结果查的是「肝炎」。原因:词典遍历顺序不确定,短实体先命中。解决:按实体名长度降序排序后再匹配,或者用最大正向匹配算法。
4.3 关系类型大小写不一致
现象:模板里写has_symptom,数据里存的是Has_Symptom,查询永远为空。原因:导入时没统一命名规范。解决:导入前统一转小写,代码里关系类型用常量,别手写字符串。
4.4 Neo4j 内存不够导致导入中断
现象:导入到一半报堆内存溢出。原因:LOAD CSV默认批量提交,大文件会撑爆堆。解决:在neo4j.conf里调大dbms.memory.heap.max_size,或者用CALL {} IN TRANSACTIONS分批提交。
4.5 问句里实体带别名查不到
现象:用户问「乙肝」,词典里只有「乙型肝炎」。原因:没有别名映射。解决:建一张别名表,实体抽取前先把别名归一化成标准名,这一步在医疗场景里几乎是必须的。
5. 让问答更稳的两个进阶技巧
第一个技巧是给意图识别加一层兜底。关键词匹配在遇到「这个病平时要注意啥」这种没有明显意图词的问句时会返回 unknown,我的做法是维护一个「高频未知问句」日志,每周把新出现的问法补进规则或模板,跑上一个月,覆盖率能明显上去。第二个技巧是给 Cypher 查询加超时和结果上限,避免某个实体关系特别多时把前端卡死。
# 给查询加超时和上限 def safe_query(driver, cypher, params, timeout=3.0, limit=50): with driver.session() as session: result = session.run(cypher + f" LIMIT {limit}", params, timeout=timeout) return [record["answer"] for record in result]逻辑说明:timeout是服务端执行超时,limit防止返回过多结果。参数上超时别设太短,Neo4j 冷启动第一次查询会慢,3 秒是个比较稳的起点。
验证这套系统好不好用,别只看它答对了多少,要看它答错时的表现——是返回空、返回无关答案,还是直接报错。返回空说明实体或意图没匹配上,返回无关答案说明模板映射错了,报错说明 Cypher 拼错了,三种情况对应三处不同的排查方向。我自己踩过的最大坑是早期没做实体归一化,用户换个说法就查不到,后来加了别名表才稳定下来。做这类项目,数据清洗和边界处理的时间永远比写查询多,别指望一次跑通,留出调试的耐心。希望帮到你。
本文还有配套的精品资源,点击获取