ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于BERT与知识图谱的医药问答系统构建实战

基于BERT与知识图谱的医药问答系统构建实战 简介在自然语言处理与知识图谱技术快速融合的背景下智能问答系统成为垂直领域智能化升级的关键应用。传统关键词检索难以应对医药领域复杂的实体关系与自然语言提问而结合BERT强大的上下文语义理解能力与知识图谱的结构化推理优势能有效解决药品、疾病、症状间的多跳关系查询问题。本文从实体识别、意图分类等基础NLP任务出发讲解如何利用医药词典辅助BERT进行精准实体抽取并通过Neo4j图数据库构建可解释的医药知识图谱。围绕药物相互作用、禁忌症查询等典型场景详细阐述从语料标注、模型微调到图谱存储、问答链路部署的完整工程实践为构建可落地、可复用的垂直行业问答系统提供参考。 我去年接到一个医药行业的自动化咨询需求时最大的困扰就是用户问得最频繁的一类问题比如“这个药什么情况不能吃”“布洛芬和感冒灵能不能一起用”靠纯关键词搜索根本答不准必须把药品、疾病、症状、成分之间的复杂关系理清楚。后来我把Python、BERT和医药词典这三样东西组合在一起做了一套医药知识图谱自动问答系统效果比预期好不少。这套项目从数据处理、模型训练到图谱查询、接口部署每一步都是可复现的我自己用了一周时间从零搭完期间踩了不少坑今天把这套完整方案拆给大家。这个项目适合三类人参考一是想入门知识图谱和问答系统的开发者二是做医疗信息化或者健康类产品的技术团队三是对BERT模型应用和NLP落地感兴趣的算法工程师。我会先把整体设计思路讲清楚然后分别拆解知识图谱构建、BERT模型训练、问答链路打通和项目交付结构最后汇总我在实操中遇到的实际问题和解决办法保证你拿到源码能跑起来、改得动。1. 项目概述与整体设计思路1.1 需求分析医药信息查询为什么需要知识图谱医药信息有三个特点导致它特别适合用知识图谱来组织。第一实体种类多且关系复杂。一个药品至少涉及成分、适应症、禁忌症、不良反应、用法用量、相互作用六类信息而这些信息又分别关联到疾病和症状比如“头孢类抗生素”这个成分关联着“酒精双硫仑反应”这种严重不良反应。这类多跳关系是传统关系型数据库和全文检索很难高效表达的。第二用户提问天然是自然语言。用户不会按字段问“查询阿莫西林的适应症”而是会问“阿莫西林是治什么的”“感冒发烧能不能吃阿莫西林”。这意味着系统必须完成“自然语言→结构化查询→图谱结果→自然语言答案”的完整转换而不是简单做字符串匹配。第三答案必须精准且可解释。医药场景下给出答案时最好能追溯到是哪条知识支撑的比如“这个药不能和酒精同服因为存在双硫仑样反应风险”。知识图谱的每条答案都能回溯到具体实体和关系路径这是黑盒模型给不了的。所以我最终确定的核心思路是用BERT做语言理解和实体识别用医药词典补充领域专业知识用知识图谱存储结构化知识最后把三者串成一条自动问答链路。1.2 技术选型分析为什么是Python BERT 词典 图谱这套选型不是拍脑袋定的我对比过几套方案下面把取舍过程说清楚。政策法规如合规备案等是否办理妥当办理地点、流程和注意事项包括国家法规体系、地方有关规定、备案办理条件、所需材料、周期与费用、常见驳回原因、通过后运营注意事项等方向。选Python而不用Java或GoPython在NLP生态上几乎没有对手HuggingFace的Transformers库开箱即用Py2neo操作Neo4j非常方便。如果非要用Java接BERT光是转ONNX模型再写推理代码就得费很大劲没必要。选BERT而不用传统Word2Vec或RNN医药文本里存在大量长句和复杂从句比如“妊娠期妇女使用本品前应咨询医师该药物可能通过胎盘屏障并在乳汁中排泄”这类句子需要充分理解上下文才能正确抽取实体。BERT的Transformer双向编码结构天然擅长捕捉上下文语义效果明显优于传统词向量。而且国内中文预训练模型已经非常成熟直接加载就行。选BERT词典融合而不用纯BERT我实验过纯BERT在通用文本上效果不错但一旦遇到医药领域的新药名、商品名、简称识别率会明显下降。比如“泰诺”和“对乙酰氨基酚”是同一个药的两种叫法纯BERT在训练语料不足时容易漏识别。这时候医药词典就派上用场了——把药品库、疾病库、症状库做成词典用最大正向匹配算法做第一轮实体候选再让BERT在词典给出的候选框内做精细分类两者互补效果最好。选Neo4j而不用MySQL实体关系查询在Neo4j里是毫秒级的比如“查找与阿莫西林存在相互作用的所有药物”只需一条Cypher语句。如果用MySQL做多表JOIN数据量上来后查询会越来越慢而且图结构天然更贴合实体关系。当然图谱的最终结果也可以导出到ES或向量数据库中供检索这个后面会讲。1.3 系统整体流程拆解整个问答系统跑通一条请求需要经过以下环节我先用文字描述一遍整体流程后面每个环节再展开细讲。用户输入“阿莫西林不能和什么一起吃”系统先经过意图分类模块判断这是“药物相互作用”类问题然后经过实体识别模块从问句中抽取“阿莫西林”这个药品实体接着把意图和实体组装成结构化查询模板生成对应的Cypher语句去Neo4j里查关系最后把查询结果转成自然语言答案返回给用户。这个流程里有两个关键点一是意图识别决定了走哪条查询模板比如“XX是治什么的”对应适应症查询“XX不能和什么一起吃”对应相互作用查询二是实体识别的准确性直接决定查询结果如果“阿莫西林”被识别成“阿莫”或者直接漏掉后面全部白搭。所以我把训练重点放在了这两个环节词典也主要在这两个环节发挥作用。2. 医药知识图谱构建从数据处理到Neo4j落地2.1 多源数据采集与预处理要点知识图谱的源头是数据我这次用了三类数据源分别是药品说明书、疾病百科文本和临床指南片段三者各有侧重。药品说明书是最核心的数据源里面的“适应症”“禁忌”“不良反应”“药物相互作用”字段本身就是天然的实体关系标注比如“禁忌对青霉素类抗生素过敏者禁用”直接就给出了一条“药品-禁忌-疾病/人群”关系。疾病百科文本主要用来抽取“症状-疾病”归属关系和“疾病-病因”关系。临床指南片段我用来补充专业术语和规范化表达比如统一“上呼吸道感染”和“感冒”这类同义术语。数据预处理有个关键步骤统一编码和格式规范化。医药文本里经常出现特殊字符、全半角混用、不同的剂量单位写法比如“0.5g”“500mg”其实是一回事。我会先做全角转半角、去HTML标签、统一单位再按句号、分号切句。切句这步很重要因为后续做BIO标注时是以句子为单位的句子切得不好标注数据质量就差。下面给一段预处理的核心代码示例做编码规范化和切句处理import re def normalize_text(text): # 全角转半角 result [] for char in text: code ord(char) if code 0x3000: code 32 elif 0xFF01 code 0xFF5E: code - 0xFEE0 result.append(chr(code)) text .join(result) # 统一空白字符 text re.sub(r\s, , text).strip() # 剂量单位统一 text text.replace(mg, 毫克).replace(g, 克).replace(ml, 毫升) # 按句末标点切句 sentences re.split(r[。\n], text) sentences [s.strip() for s in sentences if s.strip()] return sentences注意切句之后不要丢掉原句的序号信息后续做实体对齐和关系抽取时经常需要回溯到原文档位置没有位置信息会非常麻烦。2.2 实体体系与关系的设计我把项目里的实体类型分成六类分别用不同的标签表示实体类型标签典型示例药品DRUG阿莫西林、布洛芬、泰诺疾病DISEASE高血压、糖尿病、感冒症状SYMPTOM发热、咳嗽、头痛成分COMPONENT对乙酰氨基酚、头孢氨苄人群POPULATION孕妇、儿童、老年人检查项CHECK_ITEM肝功能、血常规、CT实体类型确定后关系类型就比较容易定了我在项目中定义了七类核心关系覆盖了用户最常见的提问场景关系类型头实体尾实体含义治疗疾病DRUGDISEASE某药用于治疗某病禁忌DRUGDISEASE某病禁用某药产生症状DRUGSYMPTOM某药会导致某症状成分包含DRUGCOMPONENT某药含有某成分成分禁忌COMPONENTDISEASE含某成分的药在某病禁用症状对应SYMPTOMDISEASE某症状可能是某病的表现适用人群DRUGPOPULATION某药适用于某类人群这七类关系基本覆盖了医药问答的常见问题类型比如“治疗”“禁忌”“症状对应”是用户最关心的三类。设计关系时我有个心得宁可关系类型多一点也不要让一个关系表达多种语义。比如“禁忌”和“成分禁忌”我特意分开因为前者是药品级别后者是成分级别如果混在一起查询时容易出歧义。2.3 实体与关系抽取BIO标注和词典回退策略实体和关系的抽取是整个图谱构建中最费时费力的环节我分了两个阶段来做。第一阶段是实体识别。我用BIO标注格式B代表实体开始I代表实体中间O代表非实体配合现有的NER标注工具完成标注。一般每标注1000条句子需要半天时间但医药文本专业性强建议标注人员最好有医药背景否则容易出现“阿莫西林胶囊”被拆成“阿莫西林”和“胶囊”两个实体的情况。为了减少纯人工标注的工作量我实现了一个词典预标注工具逻辑是把词典里的词在文本里跑一遍最大正向匹配自动生成候选标注人工只需要检查和修正。比如词典里有“阿莫西林”文本里出现“阿莫西林胶囊”时自动标注系统会给出B-DRUG和I-DRUG人工只需确认胶囊是否算实体的一部分。关系抽取我用的是基于规则模型的混合方案。先用规则模板从标注好的语料里抽候选三元组比如文本里出现“本品禁用于对本品过敏者”规则能匹配到“DRUG-禁忌-过敏人群”但规则的精确率不高需要再用一个判别模型过滤掉错误三元组。项目里我直接用了BERT的句对分类模式来判断一个三元组是否成立训练数据来自人工标注的正负样本。下面是规则模板抽关系的核心逻辑示例import re relation_rules [ { relation: 禁忌, pattern: r(?:禁用|不得用于|禁止用于), head_type: DRUG, tail_type: DISEASE }, { relation: 治疗疾病, pattern: r(?:用于治疗|适应症为|适用于), head_type: DRUG, tail_type: DISEASE }, ] def extract_relation_by_rules(sentence, entities): results [] for rule in relation_rules: if re.search(rule[pattern], sentence): head next((e for e in entities if e[label] rule[head_type]), None) tail next((e for e in entities if e[label] rule[tail_type]), None) if head and tail: results.append((head[text], rule[relation], tail[text])) return results实操心得规则模板的召回率不会太高但胜在精确率高抽出来的三元组几乎都是对的。先用规则抽一部分高置信度的关系再用这些关系作为训练数据训练BERT关系分类器再用分类器去抽取剩余部分这样能形成良性迭代。2.4 基于Neo4j的图谱存储和索引优化实体和关系整理成三元组之后下一步就是灌入Neo4j。我用的是Py2neo这个库连接Neo4j数据库并批量写入。节点创建时我会把同类型实体的属性统一比如药品节点统一有“name”“category”“manufacturer”属性疾病节点统一有“name”“department”属性。属性统一的好处是后续Cypher查询时可以直接按属性过滤不用判断每个节点有哪些属性。批量写入时有个性能关键点千万不要一条一条地创建节点和关系那样极其慢。正确做法是用Neo4j的批量APOC或直接在Python端先创建所有节点再一次性创建所有关系。我在项目中用了一些优化手段比如先把节点写入并返回ID映射然后拼成批量MERGE语句一次性执行速度提升非常明显几千条三元组大概几秒钟就导完了。为了让查询更快还需要建立索引。我在实体名称字段上建了索引比如CREATE INDEX drug_name_index IF NOT EXISTS FOR (n:DRUG) ON (n.name); CREATE INDEX disease_name_index IF NOT EXISTS FOR (n:DISEASE) ON (n.name);有了索引之后按实体名精确查找的速度就从秒级降到了毫秒级。对于模糊搜索用户问题时解析出来的相似实体名还可以用Neo4j的全文索引做辅助。3. BERT模型与医药词典融合的NLP核心实现3.1 模型选型与微调方案知识图谱构建好之后核心问题就变成了收到一句自然语言问句怎么准确提取实体并判断意图。这里我把任务拆成两个子任务意图分类和命名实体识别两个子任务都基于BERT模型微调。实体识别部分我选用了基于BERT的序列标注模型训练数据就是上一节标注好的BIO格式语料。为了跟词典融合我在BERT的输出层后面加了一个CRF层因为CRF能学习标签之间的转移约束避免出现“I-DRUG跟在B-DISEASE后面”这种离谱的标注序列。下面给一个简单的基于Transformers库的NER模型加载代码from transformers import BertTokenizer, BertForTokenClassification, Trainer, TrainingArguments tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForTokenClassification.from_pretrained( bert-base-chinese, num_labelslen(unique_labels), id2labelid2label, label2idlabel2id ) # 这里后续还要做词典融合所以先保留BERT输出的特征意图分类部分我把它当成一个多分类任务BERT模型最后一层的CLS向量接一个全连接层做分类。项目的意图类型主要有药品用法查询、药品适应症查询、药品禁忌查询、药品相互作用查询、症状疾病查询、通用知识查询这六类在意图分类任务中已经够用了。注意BERT模型本身就是个大规模预训练模型千万别在某一个任务上重复训练太久否则会忘记原来的通用语言知识这叫作“灾难性遗忘”。我的经验是微调2-3个epoch即可学习率控制在2e-5左右。3.2 意图识别模块设计与实践意图识别是问答系统的“导航员”决定了后续要调哪一套查询模板。如果意图分类错了后面即使实体识别百分百正确答案也是错的。我把六个意图类型做了更细的划分并定义了每个意图对应的问题模板意图标识示例问题对应查询模板usage阿莫西林怎么吃查询药品用法用量indication布洛芬治什么病查询药品适应症contraindication高血压患者能吃感冒药吗查询药品禁忌interaction阿莫西林能和酒一起吃吗查询相互作用symptom_disease头晕发热可能是什么病查询症状对应疾病general什么是阿莫西林查询实体基础信息训练意图分类模型时我使用了哈工大讯飞联合发布的预训练模型它在医疗领域的表现比通用中文BERT更好。训练集不仅包括人工标注的真实问句还包括通过模板自动生成的大量样本这两种方式结合让模型对口语化表达的适应性更强。训练完成后用真实用户问句测试时发现一个问题模型偶尔会把“怎么吃”分类成“用法”而不是“用法”在“感冒清热颗粒怎么吃”这种带药品名的句子里模型经常会忽略掉药品名反而抓住“怎么吃”这个关键词来分类。后来我在输入层增加了一个特征把问句里识别出的实体类型标记附加到原文后面比如“感冒清热颗粒怎么吃 ”让模型知道这个句子的核心实体是药品分类准确率明显提升。3.3 实体识别模块的词典融合策略实体识别模块是整个系统里最核心、也最需要调优的部分。我最初直接用纯BERT做NER但在实际测试中发现一个典型问题“阿司匹林肠溶片”这个实体里“阿司匹林”是成分“肠溶片”是剂型纯BERT经常把“肠溶片”漏掉或者把整串识别成一个无类型的实体。解决这个问题的关键就是词典融合。我设计了一套“短语候选召回 BERT边界判断 词典回退”的三步策略。第一步用医药词典做最大正向匹配和最大反向匹配把候选实体从问句里提前捞出来。比如“阿司匹林肠溶片”在词典里如果只有“阿司匹林”那第一步召回的就是“阿司匹林”长度为3同时“肠溶片”如果没在词典里就单独作为一个未知片段。第二步把词典匹配到的候选词送入BERT让BERT判断这个候选词的真实类型同时让BERT独立跑一遍序列标注输出它认为的实体。第三步融合两边结果。如果BERT和词典都认为“阿司匹林”是成分那直接采纳如果BERT认为“阿司匹林”是药品而词典标注为成分就采用词典的结果因为医药词典的专业性远强于通用模型如果BERT找出了词典没召回的新实体比如新上市的药品名就采纳BERT的结果同时把新实体追加进词典实现词典的在线扩充。这个策略用代码表达是这样def merge_entity_results(bert_entities, dict_entities): final_entities [] used_spans set() # 词典结果优先 for entity in dict_entities: final_entities.append(entity) used_spans.add((entity[start], entity[end])) # BERT结果补充进来避免重复 for entity in bert_entities: span (entity[start], entity[end]) if span not in used_spans: final_entities.append(entity) # 按位置排序 final_entities.sort(keylambda x: x[start]) return final_entities实操心得词典优先策略虽然会丢失一部分BERT的泛化能力但在医药这种高专业领域词典的精确率是远高于模型的。不过要注意词典的覆盖范围如果词典太老没有收录新药就会出现实体漏召回的情况。所以项目里我加了一个“新实体在线学习”机制每次BERT识别出词典没有的实体都会自动进入待审核队列人工确认后就能加入词典。3.4 模型训练和评估要点模型训练阶段有几个关键指标必须盯紧。实体识别看的是精确率、召回率和F1值意图分类看的是准确率整体问答系统看的是端到端准确率。我拿BLEU和ROUGE试过评估生成式答案但效果不好因为医药场景需要的是精准答案而不是一段生成文本。所以最终我采用的评估方式是准备500条真实用户问题人工标注标准答案然后看系统返回的答案是否命中标准答案。训练BERT模型时一个容易被忽视的细节是学习率调度。我在前10%的训练步数做warmup把学习率从0线性增加到2e-5后面再用线性衰减降到0。如果不做warmup模型在训练初期很容易震荡反而导致收敛变慢。模型训练完成后我用ONNX Runtime做了推理加速部署到CPU上也能达到单条问句200毫秒以内的响应速度。具体做法是把训练好的PyTorch模型导出为ONNX格式然后用ONNX Runtime替代PyTorch做推理延迟降了一半还多。4. 问答系统完整链路实现4.1 问题解析与槽位设计问答系统的核心是把用户的自然语言问题转换成结构化的查询意图和参数。我设计了一个简单的槽位填充机制类似于对话系统里的Slot Filling只是这里是规则模型混合实现。分析“高血压患者能吃阿司匹林吗”这个例子经过实体识别系统抽取到“高血压”是疾病实体“阿司匹林”是药品实体经过意图识别系统判断这是“禁忌查询”。于是把问题解析成头实体阿司匹林药品尾实体高血压疾病关系禁忌。这个槽位三元组就是图谱查询的基础参数。但如果用户问“阿司匹林能降压吗”系统则会判断这是“治疗疾病”关系如果用户问“阿司匹林和布洛芬能一起吃吗”实体识别会得到两个药品实体意图分类为“相互作用查询”。不同的槽位组合对应不同的查询模板我预先定义了一套Cypher模板比如场景Cypher模板查询药品适应症MATCH (d:DRUG {name:$name})-[r:治疗疾病]-(dis:DISEASE) RETURN dis.name查询药品禁忌MATCH (d:DRUG {name:$name})-[r:禁忌]-(dis:DISEASE) RETURN dis.name查询药物相互作用MATCH (d:DRUG {name:$name})-[r:相互作用]-(d2:DRUG) RETURN d2.name查询症状对应疾病MATCH (s:SYMPTOM {name:$name})-[r:症状对应]-(d:DISEASE) RETURN d.name注意在Cypher里给关系定义了具体语义之后没有为“相互作用”预定义关系类型所以实际建模中需要增加一个“相互作用”关系对应两个药品实体之间双向关联。在实体关系设计时没有把这类关系建进去我在后续迭代中把它加了进来这里提醒大家设计时把关系类型想全。4.2 查询转换与答案生成查询解析完成后系统会执行Cypher查询把结果转换成自然语言答案。对于“阿司匹林和布洛芬能一起吃吗”这种提问系统查询到图谱中存在“阿司匹林”和“布洛芬”之间的“相互作用”关系然后答案模块会返回“阿司匹林与布洛芬存在相互作用联合使用可能增加出血风险请谨慎”。这里有一个重要的答案生成策略优先使用图谱中已有的关系描述文本而不是模板拼接。因为关系属性里我预留了一个“description”字段它是构建图谱时从原始数据中提取的原句描述直接返回比拼接信息更自然、更准确。如果“description”字段为空就用模板拼凑实体名和关系名。答案返回的速度也很关键。Neo4j查询本身很快但FastAPI接收请求、调用模型、拼接答案再到返回整条链路如果超过3秒体验就很差了。我做了两个优化第一个是把BERT模型预热在内存中避免每次请求都重新加载第二个是给重复度较高的问题增加了一个Redis缓存同一个问题若在缓存期内再次出现直接返回缓存答案。4.3 FastAPI服务化部署与调用整个问答系统最终用FastAPI封装成了HTTP服务对外提供两个接口单轮问答接口和图谱管理接口。单轮问答接口的核心逻辑如下from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class QARequest(BaseModel): question: str class QAResponse(BaseModel): answer: str entities: list intent: str app.post(/api/qa, response_modelQAResponse) def qa(request: QARequest): # 意图识别 intent intent_model.predict(request.question) # 实体识别 entities ner_model.predict(request.question) # 拼接查询参数 answer query_graph(intent, entities) return QAResponse( answeranswer, entitiesentities, intentintent )接口部署时需要注意CORS配置前后端分离的项目如果不配CORS浏览器里调用接口会直接报错。另外最好在接口层面加上日志记录和请求ID追踪方便排查线上问题。4.4 多轮对话与兜底策略虽然这个项目核心是单轮问答但用户有时会连续追问比如先问“高血压吃什么药”然后接着问“那这个药有什么副作用”。这种多轮场景需要维护一个对话状态至少要知道上一轮的实体是什么。我实现了一个轻量级的多轮扩展维护一个session_id对应的上下文实体缓存当当前问题中没有识别到实体但意图是“副作用查询”等需要实体的类型时自动从上下文中取上一个实体补充进去。这个方案不复杂但很实用能覆盖80%以上的连续追问场景。同时我还加了一个兜底逻辑如果实体识别结果为空或者图谱查询结果为空系统会返回“抱歉我没有找到相关的医药知识建议咨询专业医师”而不是硬凑答案。医药场景下不知道就说不知道比瞎猜安全得多这也是这个系统相对恪守原则的地方。5. 源码包结构、文档说明与使用教程详解5.1 项目目录结构说明整个项目最终打包交付时我按“源码、文档、教程、数据”四大块组织目录结构大致如下MedicalQA/ ├── README.md # 项目说明和快速开始指引 ├── requirements.txt # Python依赖清单 ├── data/ # 数据目录 │ ├── raw_medical_data/ # 原始医药数据 │ ├── annotated_data/ # 标注好的训练语料 │ ├── dictionary/ # 医药词典 │ └── graph_export/ # 导出图谱数据 ├── docs/ # 文档说明 │ ├── 系统设计文档.md │ ├── 数据集说明文档.md │ └── API接口文档.md ├── models/ # 训练好的模型权重 │ ├── intent_model/ │ └── ner_model/ ├── scripts/ # 脚本目录 │ ├── build_graph.py # 图谱构建脚本 │ ├── train_intent.py # 意图模型训练脚本 │ ├── train_ner.py # 实体识别模型训练脚本 │ └── export_onnx.py # 模型导出脚本 ├── apps/ # 服务代码 │ ├── api.py # FastAPI服务 │ ├── qa_pipeline.py # 问答链路 │ └── model_loader.py # 模型加载封装 └── tutorials/ # 使用教程 ├── 环境配置.md ├── 快速开始.md └── 二次开发指南.md我是刻意把数据、模型、代码分开存放的因为模型文件非常大如果混在代码里会严重拖慢Git仓库的clone和同步速度。数据目录里我放的是标注好的语料和词典原始大文件放的是精简版完整版在网盘。5.2 环境配置与依赖安装项目依赖主要分为三个部分数据处理依赖、模型训练依赖和图谱存储依赖。我全部写在requirements.txt里。数据处理阶段主要依赖pandas、numpy、jieba、re、json等用来做数据清洗和词典匹配预处理。模型训练阶段主要依赖transformers、torch、datasets、onnxruntime。这里的PyTorch版本需要注意GPU版本和CPU版本安装方式不同建议先装对应CUDA版本的PyTorch再装transformers否则会出现依赖冲突。图谱存储阶段需要安装Neo4j社区版版本我用的是4.x系列相对稳定文档也多。Py2neo这个库对Neo4j 4.x支持很好但如果用Neo4j 5.x就需要改用官方的neo4j驱动。这里提醒大家Py2neo和Neo4j的版本匹配问题很容易踩坑动手装之前先查一下对应关系。关于环境依赖我强烈建议用virtualenv或conda建一个独立环境不要直接装在系统全局Python里。因为项目依赖的transformers和torch经常会跟系统里其他项目的依赖冲突隔离环境是最省心的方式。创建环境并安装依赖的完整流程如下# 使用conda创建独立环境 conda create -n medical_qa python3.9 # 激活环境 conda activate medical_qa # 安装PyTorch CPU版 pip install torch2.0.1 # 安装其他依赖 pip install transformers datasets onnxruntime pip install fastapi uvicorn pandas py2neo redis注意目前项目代码没有把模型预训练阶段的GPU训练脚本放进去只放了微调脚本原因是预训练成本太高普通开发者搞不定。我建议直接下载开源的医疗领域预训练模型在此基础上微调效果和成本都更友好。5.3 快速启动指南整个系统跑起来需要四步我把每一步可能遇到的问题提前写好避免用户卡住。第一步是准备Neo4j环境启动Neo4j服务然后修改项目配置里的连接地址、用户名和密码。默认配置是localhost:7687用户名neo4j密码123456如果有改动要同步改配置文件。第二步是准备模型和数据。下载好训练好的模型权重放models目录把data目录下的标注数据解压检查目录是否存在。第三步是构建知识图谱运行以下命令python scripts/build_graph.py这个脚本会读取data目录下的三元组数据批量写入Neo4j并在结束时打印写入统计信息。如果输出的统计信息里关系和节点的数量为0多半是数据格式或连接配置出错重点检查三元组的列名。第四步是启动API服务python apps/api.py服务启动后用浏览器访问http://localhost:8000/docs就能看到Swagger接口文档直接在界面上测试问答接口很方便。也可以在命令行用curl测试curl -X POST http://localhost:8000/api/qa \ -H Content-Type: application/json \ -d {question: 阿莫西林不能和什么一起吃}5.4 数据集说明和二次开发建议数据包里我整理了三份数据分别是原始医疗文本数据、人工标注的训练语料和医药词典。原始医疗文本数据来自公开的药品说明和医学百科总共大概1万多条已经做了脱敏处理只保留了药品和疾病相关的非隐私内容。医药词典包含药品名、商品名、疾病名、症状名、成分名共计两万多个词条每个词条我都标注了实体类型和别名。这份词典在做实体识别时特别有用后续二次开发时也可以直接复用。二次开发这块如果用户想接入其他领域比如法律或金融知识图谱最核心的工作是替换词典和重新标注训练语料。BERT模型的训练框架不用大改意图类型需要重新梳理查询模板需要重新设计。知识图谱的实体关系模式也需要按领域重新设计这部分是整个系统里最耗时的没有捷径可走。6. 常见问题排查与经验总结6.1 模型训练过程中的典型问题与防御方法我在训练BERT实体识别模型时遇到过两个比较典型的问题这里详细展开说说。第一个问题是实体边界不准。症状实体“头痛”在句子“头痛欲裂”里如果模型标注把“痛欲裂”也标进去那检索图谱时完全匹配不到。解决这个问题本质上是训练语料边界标注的规范性不够需要重新审核语料标注。我在项目中加了一个自动校验脚本检查语料中是否有O标签出现在B和I之间以及是否有I标签没有对应的B标签。这类标签错误很隐蔽但会明显拉低模型性能。第二个问题是意图分类过于依赖句式而不是语义。比如“高血压有哪些药不能吃”和“高血压患者吃啥药”句式完全不同但意图一样。单一的分类模型容易把句式特征学到模型里。我在数据增强时用了几种手段同义改写、插入修饰词、换用口语化表达。这样让模型见过更多句式变化泛化能力才有保障。6.2 图谱构建和查询中的坑图谱构建阶段有个坑是关系重复。同一个“阿莫西林-治疗-扁桃体炎”关系可能同时从药品说明书和疾病百科里抽出来如果导入时不做去重图谱里就会存在两条完全相同的边查询时会重复返回结果。解决办法是在导入Neo4j时用MERGE而不是CREATE。MERGE语句会自动判断节点和关系是否已存在如果存在就直接匹配不存在才创建。这个细节虽然简单但能省后期大量去重工作。还有一个坑是关系方向不一致。比如“阿莫西林-治疗-扁桃体炎”和“扁桃体炎-被治疗-阿莫西林”语义一样但方向相反如果不统一方向查“阿莫西林治什么病”时可能查不到。我的解决办法是在关系定义里明确unidirectional方向并在导入前统一转换关系方向。查询性能方面如果图谱里有几万个实体和几十万条关系在Cypher查询时如果没走索引延迟会明显上升。我在前面提过给实体name建索引这里再强调一遍这个动作必须在数据量变大之前做不然后面补索引要等待很久。6.3 问答效果调优心得调优问答系统的时候我按照“意图→实体→查询→答案”的顺序逐层排查定位。如果是意图错了系统返回的是另一类问题的答案比如用户问禁忌系统回答相互作用那问题大概率在意图分类如果意图对但实体错了比如“高血压”识别成“高血”那问题在NER模型如果实体也对但答案不对那问题可能在查询模板。这种分层排查法能快速定位90%以上的错误。还有一个心得是医药领域不是所有问题都适合用知识图谱回答。比如“我头疼三天了要不要去医院”这种开放性问题知识图谱根本答不了这时候要有明确的拒答策略。我在系统里加了置信度判断当意图分类和实体识别的置信度低于阈值时直接返回安全兜底答案而不是硬答。6.4 项目后续可以怎么扩展这个项目做了基础版本后续有几个明确的扩展方向。第一个是引入向量数据库做混合检索。目前纯依靠Neo4j查询图谱遇到图谱里没覆盖的问题就没法答。我后续计划叠加一个向量召回层把所有知识三元组生成向量索引当图谱查不到时用向量检索找相似知识片段作为补充答案。第二个是引入LLM做最终答案整合但要注意幻觉问题。LLM生成的答案可能流畅但错误在医药领域不可接受。安全性做法是让LLM只做答案润色不做知识推理所有事实性内容必须来自图谱查询结果这是当前我用BERT图谱方案最重要的一个底线。第三个是支持语音输入和输出。目前接口是文本输入如果结合语音识别和语音合成就能做成一个陪诊助手类的应用场景价值会更大。这些扩展方向我后续会持续更新到项目的文档和代码中。这套系统目前的版本已经能稳定处理常见的医药知识问答对于想了解知识图谱落地、BERT模型实际应用和NLP工程化的朋友完全值得下载源码跑一遍再根据自己的需求做二次开发收获会很大。本文还有配套的精品资源点击获取
返回列表