
从接触医疗知识图谱问答系统这个方向开始我陆陆续续看过不少开源项目但这个仓库给我的印象特别深。它没有堆砌复杂的模型而是用一套完整但门槛不高的工程链路把医学知识结构化、自然语言问句理解、图数据库查询这三件事串了起来。这篇文章就是我对 QASystemOnMedicalKG-master 的探索记录包含代码阅读思路、知识图谱构建细节、问答链路拆解、复现时踩过的坑以及我基于这套系统做的改进实验。如果你想快速入门医疗垂直领域的问答系统或者正在纠结知识图谱到底怎么落地这篇记录应该能给你一个比较完整的参考。我先把话说在前面这个项目不是那种开箱即用、放上生产环境就能直接服役的成品它是很好的教学样板和学习骨架。你需要理解它的设计逻辑再根据业务数据去做二次改造。下文我会按照我自己的阅读顺序来展开——先看整体架构再看图谱构建然后是问答引擎核心最后聊查询性能和踩坑经验。1. 先摸清楚项目到底在做什么三大模块与一条主链路1.1 从目录结构反推系统架构拿到一个开源项目我一般不会急着去跑代码而是先看目录结构和入口脚本。这套系统最直观的划分是三个部分数据准备、图谱构建与导入、问答服务。数据准备负责把原始的医学资料整理成结构化三元组图谱构建部分负责把这些三元组清洗、去重、关联最终导入图数据库问答服务则接收用户输入的中文问句完成意图识别、实体抽取、查询语句生成和答案返回。这种分层非常清晰也符合大多数知识图谱问答项目的基本套路。我建议你在看任何类似项目时都先问自己一个问题这个项目的数据源头是什么数据是怎么变成图的用户输入是怎么被路由到图查询的把这三个问题的答案找出来整个项目的主干脉络就清楚了。1.2 数据是医疗问答系统的血液医疗知识图谱问答系统最核心的资产不是算法而是数据。这个项目里的数据主要由疾病、症状、药物、食物、检查项目等实体类型以及它们之间的语义关系组成。比如“高血压”和“头晕”之间是“表现为”的关系“糖尿病”和“胰岛素”之间是“推荐用药”的关系。为什么要用知识图谱而不是传统的表格因为医疗知识本身是网状结构。一个疾病关联多种症状一种症状又可能指向多种疾病一种药物有适用症也可能有禁忌症。这种多对多、跨层级、带语义标签的关系用二维表表达非常吃力但用图来表达就非常自然。图数据库里节点是实体边是关系用户可以顺着某条路径去检索“从疾病出发经过症状节点再到达相关检查项目”这类多跳问题。这个项目把这一整套流程都串了起来。我在探索时的体会是它的价值不在某个单点算法有多强而在让你看到一条完整的、可以被复刻的落地链路。这也是我推荐从它入手的原因。2. 知识图谱构建疾病、症状、药物之间如何被结构化2.1 实体类型与关系类型的设计先聊实体类型。这套系统把医疗领域信息抽象成六个左右的实体类别包括疾病、症状、药物、食物、检查项目、身体部位等。每种实体在图中都有对应的节点标签节点属性一般是名称、别名、所属科室、常见程度等。关系类型则围绕疾病展开比如疾病和症状之间的“表现为”、疾病和药物之间的“推荐用药”、疾病和检查项目之间的“适合检查”、疾病和食物之间的“宜吃”和“忌吃”等。这个设计看起来不复杂但它是后续所有问答逻辑的基础。因为问答系统生成查询语句时本质上就是把用户问句里的实体和意图映射为图上的节点和关系。比如用户问“感冒应该吃什么药”系统要做两件事识别出“感冒”这个疾病实体再判断出意图是“推荐用药”然后生成一条从疾病节点到药物节点的查询路径。如果关系类型设计得不够明确这一步就会非常难受。我特别想强调命名规范的重要性。这个项目里关系类型大多采用统一的动词短语比如“HAS_SYMPTOM”“RECOMMEND_DRUG”。我建议你在自己的项目里也保持这种清晰的命名习惯因为在写问答模板时这些关系名会被直接拼进查询语句命名越一致模板维护成本越低。2.2 数据清洗与导入Neo4j的细节图谱构建里最花时间的往往不是设计而是清洗。原始数据可能来自医疗百科、结构化表格或人工整理基本都会包含重复实体、同义名称、空属性、乱码字符等问题。比如同一个药物在不同数据源里可能写作“阿司匹林”和“乙酰水杨酸”如果不做归一化图谱里就会出现两个孤立的节点查询时用户说哪个都只能查到一半。我当时复现这个项目时数据处理部分主要做了三类操作。第一是去重按实体名称归一化后合并重复节点。第二是补全关系只保留两个节点都存在的三元组避免产生悬挂节点。第三是字段清洗把科室、药品剂量等属性中的空格、换行、特殊符号去掉统一格式。导入Neo4j这一步也有讲究。这个项目的数据规模不大常规做法是用Cypher的LOAD CSV或者直接通过Python驱动批量写入。批量写入时要注意把节点先创建完再创建边否则会出现“关系引用了不存在的节点”这类错误。如果数据量更大比如几十万以上节点就得考虑使用Neo4j Admin Import这样的离线导入工具把数据转成CSV头文件再导入速度快好几个数量级。3. 问答引擎核心链路从自然语言到Cypher查询3.1 中文问句的识别与实体链接问答链路是整个系统最让人兴奋的部分。用户输入是一句口语化的中文问句比如“高血压病人平时吃什么降压药比较好”而系统内部需要把它转成一条可执行的图查询。这个转换过程大体分为两步先抽取实体再判断意图。实体抽取这一层项目采用了词典与分词工具结合的方式。思路是准备一个包含疾病名、症状名、药物名的自定义词典在分词阶段就尽量让专业术语完整切分。比如“高血压”这个词如果按普通分词可能被切出“血压”有了词典约束才能把它作为一个完整实体返回。我实测下来这个方案对常见的疾病名、药物名覆盖效果不错遇到不认识的专有名词就会失效所以后期又补充了基于规则的回退机制当词典匹配不到时尝试从原文中提取包含“疾病”“药物”等后缀词的片段再和图谱里的节点模糊匹配。实体链接则是把抽取出的实体名映射到图谱中的具体节点。这里有个隐藏问题一个实体可能有多个名字。我在改进时加了一张同义词映射表把凡是节点属性中包含别名的表述都指向主节点例如“高血压”和“essential hypertension”这样的中外文表述。就这一个改动让问答命中率提升了不止一点点。3.2 模板解析的多个分支意图识别的作用是决定用户到底要问什么。这个项目把意图划分为若干类常见的包括疾病症状查询、疾病治疗用药查询、疾病宜吃忌吃查询、疾病检查项目查询、科室查询等。每个意图类型对应至少一个查询模板模板里预留实体槽位比如“{疾病}吃什么药”对应“推荐用药”。模板匹配的具体思路是把问句和预定义好的句式规则做比对。这里不是靠深度学习模型而是靠规则加关键词权重。比如句子中出现了“症状”“表现”“什么感觉”等词系统判断意图偏向症状查询出现“吃什么药”“治疗”“用药”等词则偏向用药查询。这种做法的优点是实现简单、完全可控缺点是覆盖不全换一种没见过的问法就可能误判。我在自己的改进实验里给每个意图类型都增加了正例和反例句子通过小规模分类模型做增强效果比纯规则稳定了一些。3.3 一个完整问答示例我拿“糖尿病有哪些常见并发症”这个问句走了一遍完整链路。分词阶段自定义词典把“糖尿病”和“并发症”都识别为词条实体抽取阶段系统从图谱中找到疾病节点“糖尿病”意图判断阶段句子中出现“并发症”这个关键词系统把它归类到并发症查询模板紧接着模板实例化生成一条查询语句从糖尿病节点出发沿着表示并发关系的边找到相关联的疾病节点返回这些节点的名称。这条查询在数据库里执行后返回的结果可能是“糖尿病肾病”“糖尿病视网膜病变”等。系统再把节点名称包装成一句自然语言答案返回给用户。整个过程从输入到响应在本地环境上大概只需要几百毫秒。这个速度对于教学演示完全够用也说明在数据量可控时规则加图查询的方案远比想象中高效。4. 图数据库的选型观察与查询性能体验4.1 为什么不能直接用关系型数据库很多人会问医疗数据实体不算多用MySQL也能存为什么非要引入图数据库我自己探索后的理解是图数据库的核心优势不在基础查询而在多跳关联和路径检索。医疗领域最常问的问题之一就是“这种症状可能是哪些疾病引起的”这个查询本质上是从症状节点出发反查所有连到它的疾病节点可能还要继续跳到对应的检查项目或用药建议。这种查询如果用关系表实现通常需要设计多张关联表写一串JOIN语句而且关系层级越深SQL越复杂。在图数据库里这只是一个沿边遍历的操作。这套项目使用的是图数据库领域最主流的Neo4j它的查询语言Cypher对路径遍历的表达非常直接比如匹配一条关系链就是简单地描述节点和关系的连接模式。我个人的观察是对于关系类型固定、查询模式清晰的垂直领域图数据库的思维模型天然更匹配。它逼迫你把业务关系建模成图这个建模过程本身就是对领域知识的一次梳理。4.2 索引、参数化与查询延迟实际跑通问答之后我开始关注性能问题。在这个项目的小规模图谱上直接查询几乎没有压力但不代表可以忽略索引。医疗图谱虽然在单个实体类型上节点数量有限但如果以后扩展到几十万实体、上百万关系没有索引的查询性能会迅速恶化。我在验证时给疾病名、症状名、药物名的名称属性都加了索引。加索引前某些模糊匹配查询能明显感觉到延迟加索引后即便是全库扫描类的匹配也顺滑很多。这个经验可以通用到所有图数据库项目先确认哪些实体属性会被当成查询入口再针对这些属性建立索引千万不要等用户来吐槽慢。另外我还建议查询语句里尽量使用参数化写法不要把所有实体值硬拼到查询字符串里。参数化不仅能让数据库做执行计划缓存更重要的是能避免查询注入和特殊字符引发的解析错误。这一点很多做知识图谱问答的初学者容易忽略觉得数据是自己导进去的没必要防注入。但等到实体名里出现引号、括号、特殊符号时你就知道参数化有多重要了。5. 复现过程中踩过的坑与排查思路5.1 CSV导入时报错的根因第一次按文档复现时我在数据导入环节就卡住了。现象是CSV文件里明明有数据但导入报错说找不到字段或者节点冲突。排查链路从文件编码开始最后定位到两个问题一个是文件是含BOM的UTF-8编码解析时把首列列名带上了不可见字符另一个是同一实体在CSV里出现了多行重复创建后触发了唯一性冲突。这两个问题在真实项目中非常典型。我的解决方法是先统一文件编码为无BOM的UTF-8再在写入前对实体名做一次归一化校验保证同一主键只写入一次。如果你想避开同类问题这里有个实用经验写导入脚本之前先单独把数据读出来看一眼列名和数据类型不要默认源文件干净。5.2 分词与实体匹配失败问答链路里我遇到最影响体验的问题是分词。比如“阿司匹林肠溶片”这种带剂型的药物名标准词典可能只收录“阿司匹林”分词后“肠溶片”变成普通词导致实体抽取只能抽到半个名字。更麻烦的是“小儿感冒颗粒”这类包含修饰语的长词条词典不维护完整词表的话几乎百分百切错。我当时的排错思路是把问答模块单独做日志输出把每个问句的分词结果、候选实体和最终匹配节点全部打印出来。当答案错误时往回看分词结果基本就能定位是词典问题、实体链接问题还是模板问题。用这个办法我一次性发现了二十多个需要补充进自定义词典的药物和疾病词条。后续迭代时我建议你把分词结果和匹配节点也加入监控日志这比单看最终答案好不好用要高效得多。5.3 模板覆盖不足的应对规则模板的天然短板是覆盖不全。这个项目内置了一些问法模板但换成口语化表达就可能匹配失败。比如“血压高的人该注意什么”这句话系统可能无法直接判断它等价于“高血压的注意事项”。最初我选择不断加模板后来发现模板越加越多维护成本急剧上升。后来我换了一个思路先对问句做标准化改写把口语说法映射成核心句式再做意图判断。比如“血压高”“血压偏高”先归一化为“高血压”“该注意什么”“要当心什么”归一化为“注意事项”。这样模板数量不需要膨胀意图识别准确率反而提高了。这个经验我觉得很值得分享在垂直领域问答里与其堆模板不如先做问句改写。6. 项目能怎么改进把Demo做成可用产品的方向6.1 同义词与实体链接增强在探索这个项目的基础上我做了一轮增强实验核心是补全同义词映射。医疗领域的同义词非常多包括中文别名、英文缩写、商品名和通用名。比如不同品牌的降压药商品名可能完全不同但本质成分是同一种药。如果不做同义词归一用户在问答时就会频繁遭遇“查不到”。我的实现方式是在图谱中给实体节点增加“别名”属性在实体链接阶段先按标准名精准匹配匹配不到再按别名模糊匹配。这一层补充并不复杂但对问答体验的提升非常明显。如果你也打算在医疗垂直领域做产品建议在一开始就把同义词表设计成独立可维护的数据文件让医学人员可以持续补充。6.2 从单轮到多轮与澄清原始项目的交互基本限定在单轮问答用户问一句系统答一句如果实体有歧义会直接选第一个。这在真实医疗场景里风险很大。一个“心脏疼”的用户可能指的是心绞痛也可能指的是胃食管反流引起的胸痛系统不应该直接给答案而应该反问澄清。我在这套系统上增加了一个很轻量的澄清机制当候选实体超过一个并且各候选实体之间的答案差异较大时先把候选实体列给用户让用户选择后再继续查询。虽然这只是多轮交互的雏形但已经把体验从“命令式查询”往前推了一大步。医疗问答涉及生命安全宁可多问一句也不要武断给结论。6.3 评估方式与个人体会最后聊聊评估。这类项目的效果不能只看几个例子跑通了就算完。我建议整理一份一百到两百条的真实问句集覆盖各个意图类型人工标注正确答案再批量跑系统统计准确率和无答案率。我对原系统做了一轮实测统计结果显示在模板覆盖范围内准确率很高但出了覆盖范围无答案率明显上升。这正是规则系统的分水岭。改进之后我用同义词、问句改写和澄清机制把无答案率往下压了不少。整个过程让我对医疗知识图谱问答有了更深的理解这类系统的天花板其实不取决于算法多先进而取决于知识库的完整度和业务规则的合理设计。如果后续要进入生产环境我会考虑引入更强的实体识别模型同时保留规则系统作为兜底让两种方案互相补位。如果你正在研究这个项目我的建议很直接先别急着换模型把知识图谱的数据质量打磨到位把提问模板吃透再用真实问句做一轮完整的评估。这个项目最好的地方就是给了你一个完整的基线让你能在上面快速验证自己的想法。等这一轮跑完你对医疗问答里那些“看似简单但藏满细节”的问题心里就有底了。