尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于知识图谱与RAG的医疗问答系统全链路解析

基于知识图谱与RAG的医疗问答系统全链路解析 简介基于 RAG 与大模型技术的医疗问答系统项目包面向人工智能、计算机等相关专业的学生与开发者可作为毕业设计、课程设计或项目案例。项目以 DiseaseKG 数据集为基础通过 Neo4j 构建知识图谱结合 BERT 完成命名实体识别、34B 大模型进行意图识别配合精确知识检索与生成式问答解决大模型在医疗咨询场景下可靠性不足的问题并配有 Web 交互界面与详细文档。资源共 76 个文件涵盖 Python 源码、Jupyter Notebook 分析脚本、JSON/CSV/NPY 数据文件、YAML 配置文件、Markdown 说明及项目截图等整体压缩包约 84.65MB目录结构清晰便于定位与二次开发。已有 212 人学习/下载。除完整可运行代码外还包含实体识别与关系增强数据、nl2cypher 转换脚本、lora 微调示例、知识图谱构建流程、登录注册模块及结果展示图既适合快速复现实验效果也便于按需改造是医疗 RAG 方向学习与实践的优质参考。1. 为什么医疗问答必须换一种做法医疗领域的问答系统和通用聊天机器人有本质区别用户问「头疼三天伴有呕吐」时要的不是一段流畅的安慰话术而是能落到具体疾病、检查项目和用药禁忌上的确定性回答。通用大模型虽然能生成通顺文本但在医学这类高专业门槛场景里存在两个致命问题——知识截止日期导致的时效性缺失以及无约束生成带来的幻觉。一个 34B 参数的模型可以凭印象编造一种不存在的并发症这在医疗场景是不可接受的。这套基于 RAG 与大模型技术的医疗问答系统给出了一个值得拆解的解决思路先把 DiseaseKG 数据集导入 Neo4j 构建知识图谱用 BERT 做命名实体识别再用 34B 大模型做意图识别最后通过 Cypher 查询知识图谱获取事实依据让大模型基于检索结果生成回答。整个链路本质上是把「记忆」从模型参数中剥离出来放到可验证的知识图谱里模型只负责理解和表达。这个项目特别适合两类人一类是做毕业设计或课程设计、需要完整可跑通系统的学生另一类是已经在做 RAG 应用、想看看如何用知识图谱替代向量数据库做精确检索的工程师。接下来按数据层、算法层、检索层、应用层的顺序逐步拆解。2. 知识图谱构建从 DiseaseKG 数据集到 Neo4j 图数据库2.1 DiseaseKG 数据集为什么比纯文本更适合医疗问答DiseaseKG 是一个面向疾病知识的结构化数据集覆盖了疾病、症状、药物、检查项目等实体及它们之间的语义关系。和常见的中文医疗问答对文本相比它的核心优势在于关系是显式的——比如「肺炎」和「发热」之间的「表现为」关系在纯文本里需要模型去隐式学习在知识图谱里则是一条明确的边。项目里medical_new_2.json和rel_aug.txt这两个文件分别对应实体和关系的原始数据。build_up_graph.py脚本负责把这两份数据解析成 Neo4j 的节点和关系。实体被映射为具有name和type属性的节点类型包括疾病、症状、药物、检查项关系则定义了疾病到症状的HAS_SYMPTOM、疾病到药物的DRUG_FOR等类型。这种设计带来的直接好处是查询路径变得可控。用户问「感冒应该吃什么药」系统不需要在几十万字里做语义相似度匹配而是先在图谱中找到「感冒」节点再沿DRUG_FOR边找到所有关联药物节点。查询的每一步都是确定性的返回结果可以被追踪和验证。对于医疗场景这种可解释性是纯向量检索无法提供的。2.2 图谱构建脚本的参数与执行build_up_graph.py的核心逻辑可以简化为数据读取、实体去重、关系建立三个步骤。参考项目中的实现通常需要先配置 Neo4j 连接参数然后批量写入。常见的写入方式是逐条执行 Cypher 的MERGE语句MERGE能避免重复创建同名节点from neo4j import GraphDatabase driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, your_password)) def create_entity(tx, name, entity_type): query MERGE (n:Entity {name: $name}) SET n.type $type tx.run(query, namename, typeentity_type) def create_relationship(tx, src, dst, rel_type): query ( MATCH (a:Entity {name: $src}) MATCH (b:Entity {name: $dst}) MERGE (a)-[r:REL {type: $rel_type}]-(b) SET r.type $rel_type ) tx.run(query, srcsrc, dstdst, rel_typerel_type) with driver.session() as session: for entity in entity_list: session.execute_write(create_entity, entity.name, entity.type) for rel in relation_list: session.execute_write(create_relationship, rel.src, rel.dst, rel.rel_type)这里有几个关键点需要说明。第一MERGE和CREATE的区别在于MERGE会先检查节点是否存在重复执行脚本时不会产生脏数据这在反复调试图谱时非常重要。第二给所有实体统一使用Entity标签通过type属性区分疾病、症状、药物而不是为每种类型单独建标签这样做的目的是让查询语句更统一避免动态拼接标签名带来的 Cypher 注入风险。第三关系上也挂了一个type属性虽然 Neo4j 本身支持带类型的关系边但把类型同时冗余到属性里在某些 ORM 场景下查询过滤会更方便。实际跑这个脚本时需要注意两个坑。一是数据量较大时逐条MERGE的速度会很慢可以改成UNWIND批量提交二是可视化调试时不要试图一次导入全部数据先用LIMIT 100抽样子集验证关系方向是否正确否则关系方向画反了要回头重新清洗数据。2.3 知识图谱在 RAG 中的角色定位把知识图谱引入 RAG 体系并不是为了取代向量数据库而是补上精确召回这块短板。传统的 Embedding 检索擅长处理语义相近但表述不同的情况比如「肚子疼」和「腹痛」但遇到「糖尿病患者是否可以接种某类疫苗」这种涉及多跳关系的问题时向量检索经常给出语义相关但逻辑不完整的片段。知识图谱的图遍历能力恰恰擅长这种多跳查询。在这套系统里图谱承担的是事实层角色——实体识别的结果被转换为 Cypher 查询从图谱中取出确定的事实三元组这些三元组连同用户原始问题一起被送入大模型。nl2cyhper.py脚本的作用就是把 BERT 识别出的实体和意图转换为可执行的 Cypher 查询语句。这一步可以理解为给知识图谱装了一个自然语言接口用户不需要懂图查询语言系统内部自动完成翻译。需要明确边界的是知识图谱的覆盖范围取决于 Dataset 的规模DiseaseKG 无法覆盖所有医学问题超出图谱范围的查询最终仍然需要回落到大模型自身的知识。所以这套系统的设计哲学是图谱能答的用图谱答图谱答不了的再靠模型兜底。理解这个边界才能在生产环境中合理设置检索策略和兜底逻辑。3. 双层 NLUBERT 完成命名实体识别34B 大模型锁定用户意图3.1 为什么需要实体识别和意图识别两层结构医疗问答场景中「我最近总是失眠还伴有心慌」和「失眠需要挂哪个科室」这两句话的实体是相同的失眠但用户的意图完全不同——前者是寻求诊断建议后者是寻求就诊指引。如果只做实体识别无法区分这两种需求如果只做意图识别又拿不到具体的实体值用于图谱查询。双层 NLU 结构解决的就是这个「实体 意图」的二维解析问题。项目里的方案是BERT 模型负责命名实体识别NER识别出文本中的疾病名、症状名、药物名等实体34B 大模型负责意图识别判定用户的问题是咨询病因、查询用药、还是了解检查项目。两个模型串联工作先抽实体再定意图最后根据意图决定查询图谱的方式。这种分工还带来一个工程上的好处——NER 模型可以做得小且快ner_model.py基于 BERT 微调推理速度足够支撑实时交互意图识别虽然用了 34B 大模型但因为只是做分类任务输入和输出都非常简短实际推理开销远小于生成一段完整回答。如果反过来用大模型做实体抽取虽然效果可能更灵活但延迟和成本都会显著上升。3.2 NER 模型的训练与推理细节项目目录里的ner_data_aug.txt是实体识别训练语料tag2idx.npy保存了标签到索引的映射关系。NER 任务的标注方案使用 BIO 标注体系B 表示实体开始位置I 表示实体内部O 表示非实体。例如「患者出现发热和咳嗽」会被标注为「患/O 者/O 出/O 现/O 发/B-SYMPTOM 热/I-SYMPTOM 和/O 咳/B-SYMPTOM 嗽/I-SYMPTOM」。训练部分的实现可以参考 Hugging Face 的 Transformers 库加载预训练的 BERT 中文模型在输出层接一个线性分类层每个 token 预测一个标签。模型的损失函数使用交叉熵但需注意忽略标签为 -100 的位置——这是 PyTorch 中常用的 padding 处理技巧。推理阶段的核心代码如下import torch from transformers import BertTokenizer, BertForTokenClassification tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForTokenClassification.from_pretrained(./ner_model_output) def extract_entities(text): inputs tokenizer(text, return_tensorspt, truncationTrue, max_length128) with torch.no_grad(): logits model(**inputs).logits predictions torch.argmax(logits, dim2)[0] tokens tokenizer.convert_ids_to_tokens(inputs[input_ids][0]) entities [] current_entity None for token, label_id in zip(tokens, predictions): label index2tag[label_id.item()] if label.startswith(B-): if current_entity: entities.append(current_entity) current_entity {type: label[2:], text: token} elif label.startswith(I-) and current_entity: current_entity[text] token.lstrip(##) else: if current_entity: entities.append(current_entity) current_entity None if current_entity: entities.append(current_entity) return entities这段推理代码的关键在于标签序列到实体列表的转换逻辑。B-开头的标签开启一个新实体I-标签追加到当前实体遇到O则结束当前实体。注意 BERT 的 WordPiece 分词会把一个词拆成多个 token中文场景下##前缀表示该 token 是前一个词的延续拼接时需要去掉这个前缀。训练这类医疗 NER 模型时我一般会重点关注样本均衡问题。疾病名和症状名在语料中占比可能悬殊如果不做处理模型会对高频类别过拟合、对低频类别召回不足。一种常见做法是在 Loss 中按类别频率施加权重或者对低频实体类型做简单的过采样。此外医疗实体中存在大量嵌套结构比如「急性支气管炎」既是一个完整疾病名其中又包含「支气管炎」这个子实体BIO 标注无法表达嵌套关系实践中通常只取最外层或最完整的实体这个取舍要在标注规范阶段就明确下来。3.3 34B 大模型的意图识别策略意图识别采用 34B 大模型而不是直接用 BERT 做分类主要原因是医疗问诊的意图类别边界模糊且经常出现复合意图——一句话里既问了病因又问了治疗建议。传统分类模型面对类别重叠和复合意图时表现不佳而大模型可以通过 prompt 中给出的指令和示例灵活处理。参考项目中finetune_hf.py和inference_hf.py的文件结构意图识别有两种实现路径一是直接使用基座模型通过 few-shot 推理完成分类二是先对模型做 LoRA 微调再推理。微调需要的训练数据可以从nl2cyhper_data.txt中提取这个文件包含了自然语言问句到 Cypher 查询的映射对其中每条问句的意图类型已经隐含在对应的查询结构中——涉及MATCH疾病节点的查询意图是病因咨询涉及OPTIONAL MATCH药物关系的是用药查询。实际的 prompt 构造可以参考下面的模式你是一个医疗问答系统的意图分类器。用户的问题属于以下类型之一 1. symptom_query: 查询症状对应的可能疾病 2. drug_query: 查询疾病的用药建议 3. department_query: 查询就诊科室 4. cause_query: 查询疾病病因 5. general_query: 一般性医疗咨询 请只输出意图类型名称不要输出其他内容。 用户问题{user_question} 意图类型这里的关键是「只输出意图类型名称」这个约束。如果不加这个约束34B 模型可能会输出一长串解释文字下游代码解析起来非常麻烦。即使加了约束实际使用中也可能出现模型输出多余内容在代码里还需要对输出做正则匹配或字符串包含判断来兜底。选择 34B 模型而不是 7B 或更小的模型是因为意图识别的准确率直接决定了后续图谱查询的正确性。如果意图判断错了实体抽取再准确也只会产生南辕北辙的查询。在医疗场景下意图错误的代价比实体错误的代价更高——实体错了顶多查不到内容意图错了可能把病因咨询回答成用药建议这在某些情况下是有风险的。34B 模型的指令跟随能力比 7B 明显更强对于「只输出分类结果」这类约束的执行更可靠。4. RAG 检索链路从 nl2cypher 到多路召回的知识获取4.1 自然语言到 Cypher 查询的转换逻辑nl2cyhper.py是整套系统里最核心的工程模块它承担的任务是将「用户问题 实体 意图」转换为可执行的 Cypher 语句。这个转换不能靠简单的字符串模板拼接因为同一意图在不同实体上的查询结构可能不同。比如「感冒有什么症状」和「高血压应该挂什么科」虽然意图分别是 symptom_query 和 department_query但涉及的节点标签和返回字段完全不同。常见的做法是维护一个「意图 → Cypher 模板」的映射表然后把实体值填充进模板。模板示例如下意图类型Cypher 模板symptom_queryMATCH (d:Entity {name: $disease})-[:HAS_SYMPTOM]-(s) RETURN s.namedrug_queryMATCH (d:Entity {name: $disease})-[:DRUG_FOR]-(m) RETURN m.namedepartment_queryMATCH (d:Entity {name: $disease})-[:DEPARTMENT]-(dep) RETURN dep.namecause_queryMATCH (d:Entity {name: $disease})-[:CAUSE]-(c) RETURN c.name模板中的$disease参数通过参数化查询传递给 Neo4j 驱动而不是直接拼接进查询字符串。原因有两个一是防止 Cypher 注入——实体文本来自用户输入可能包含恶意构造的内容二是利用 Neo4j 的查询缓存——参数化查询的查询计划可以被复用性能更好。模板方案的问题在于覆盖度有限遇到模板中没有的意图类型时会查询失败。生产环境中我倾向于增加一个回退机制当模板匹配失败时将用户问题和已识别实体打包发送给大模型让大模型直接生成 Cypher 语句再用正则校验查询语句中是否包含MATCH、RETURN等关键字校验通过后再执行。这种方法牺牲了一点延迟但显著提升了查询的覆盖范围。4.2 混合检索策略知识图谱与向量召回如何配合整套系统在检索层面实际上采用了混合架构并不是单一的知识图谱查询。RAG.png架构图中展示的链路也印证了这一点用户问题经过意图识别后分流需要精确事实的走图谱查询需要开放知识的走向量召回。这种设计在 RAG 领域被称为多路召回——多个检索器并行工作结果合并后统一送入大模型。data目录中的medical_new_2.json除了用于构建图谱也被处理成向量库的语料来源。具体的实现路径是将图谱中的实体描述、关系说明等文本内容切块通过 Embedding 模型编码后存入向量数据库。当图谱查询返回结果为空或结果数量不足时触发向量召回作为补充。多路召回的结果合并有一个需要注意的细节图谱查询返回的是结构化三元组向量召回返回的是文本片段两者格式不统一。直接拼接会导致上下文混乱需要在送入大模型前对结果做统一格式化。我一般会定义一个format_context函数把三元组序列化为疾病A - 表现为 - 症状B的字符串形式这样大模型读取时不会产生歧义。4.3 上下文窗口管理与提示词约束检索结果可能超出大模型的上下文窗口限制特别是图谱查询返回大量关联节点时。34B 模型通常有较长的上下文窗口但在实际部署时过长的上下文会导致生成变慢、成本上升而且无关内容会干扰生成质量。因此检索结果在送入模型前需要做截断和排序。排序策略可以参考以下优先级先按意图类型过滤——drug_query 只保留药物相关的结果再按实体匹配度排序——与用户问题中实体完全匹配的结果排在前面最后做数量限制——最多保留 10 到 15 条结果。这个数量不是拍脑袋定的而是综合了窗口大小和回答质量的经验值太少信息不足太多干扰生成。Prompt 的最终结构遵循 RAG 应用的标准模式系统指令、检索上下文、用户问题。关键约束在系统指令中要明确写出包括「只能基于提供的上下文回答不能使用模型内部知识」「如果上下文包含答案请给出结论并说明依据如果上下文不包含答案请直接回答未知」等。在医疗场景下「承认不知道」远比「编造一个答案」安全。补充说明一下只用图谱查询结果驱动的回答难免显得生硬所以系统设计上允许模型用用户问题来做适度的语言润色前提是不偏离检索事实。5. 前端与用户体系WebUI 交互、登录注册与数据存储实现5.1 基于 WebUI 的对话界面与图谱可视化webui.py是系统的前端入口提供了对话交互界面和知识图谱可视化功能。从img目录的截图可以看出界面分为左右两栏左侧是对话窗口用户输入问题后显示回答文本右侧是图谱可视化面板展示当前问题相关的实体和关系子图。对话界面的实现通常基于 Gradio 或 Streamlit 这类快速开发框架配合 Neo4j 的 Browser API 或 ECharts 关系图实现图谱展示。e3.png、e7.png等截图展示的即是不同查询场景下的图谱渲染效果。可视化不是为了炫技而是让用户能够直观地看到回答背后的事实依据——这是医疗问答系统建立信任感的重要方式。在实现上对话接口和后端服务的通信采用同步请求-响应模式因为图谱查询的延迟通常在百毫秒级别不需要额外的异步处理。但如果并发量上来了WebUI 层需要加一层缓存——把常见的「疾病-症状-药物」三元组查询结果缓存在 Redis 中避免每次对话都穿透到 Neo4j。5.2 登录、注册与用户数据隔离login.py、user_data_storage.py和user_credentials.json组成了系统的用户体系。登录注册功能虽然看起来是 web 应用的标配但在 RAG 项目中有一个特殊作用记录用户的问答历史和反馈数据这些数据可以作为后续微调模型的语料来源。user_credentials.json采用简单的 JSON 文件存储用户名和密码哈希这在演示项目中可以接受但生产环境应该替换为数据库存储。user_data_storage.py负责将用户问答记录写入文件或数据库每条记录包含用户 ID、问题、回答、检索上下文、用户反馈等字段。需要特别处理的是用户的隐私保护。医疗问答数据比普通对话数据敏感得多在存储用户问题时至少要做到两点一是脱敏处理去除姓名、身份证号、联系方式等个人信息二是访问控制用户只能查看自己的历史记录管理员查看全部记录时需要审计日志。项目中的admin.png截图表明系统包含管理员界面这部分权限控制要单独实现。5.3 从交互日志收集微调数据用户在实际使用中提出的问题往往比原始训练语料更自然、更口语化这是宝贵的真实数据。当用户对某个回答点击「有帮助」或「无帮助」时系统将这组「问题-回答-反馈」记录下来。过滤掉低质量样本后可以作为后续指令微调的训练数据也可以用来评估当前系统在真实场景下的表现。lora_data目录中保存的应该就是这类处理后的训练数据。跑微调时一个经验是不要直接全量微调 34B 模型而是使用 LoRA 或 QLoRA 技术只训练低秩适配器可以大幅降低显存需求。finetune_hf.py中已经实现了 LoRA 微调的完整流程加载模型后冻结基础参数仅训练注入的 LoRA 参数矩阵。医疗问答系统的持续优化是典型的「数据飞轮」线上使用产生数据数据筛选后微调模型更好的模型产生更好的回答进而提升用户信任度。6. 本地部署与排错Neo4j 连接、显存控制、微调与测试验证的实用技巧6.1 Neo4j 连接失败与数据导入的常见排查方法部署过程中最容易出问题的环节就是 Neo4j 的连接与初始化。初次启动系统时如果build_up_graph.py报连接错误优先检查三个位置Neo4j 服务是否已启动、bolt://localhost:7687端口是否能连通、认证账号密码是否正确。Docker 部署时需要额外注意容器的端口映射宿主机访问容器内的 Neo4j 服务需要映射7687Bolt 协议和7474HTTP 管理界面两个端口。批量导入数据时的性能问题也很常见。逐条MERGE写入数千条数据时速度可能慢到无法接受。推荐做法是使用UNWIND批量处理UNWIND $batch as row MERGE (n:Entity {name: row.name}) SET n.type row.typePython 侧对应地将数据组装为列表一次会话提交数百条记录。如果导入速度仍然不理想检查是否为每条语句都单独开启了事务——事务提交的开销往往比语句执行本身还大正确的做法是一个事务里包含多条语句。6.2 34B 模型的显存优化与量化方案34B 参数模型推理需要的显存远超过普通消费级显卡的容量即使使用 FP16 精度模型权重也需要大约 68GB 显存。项目能在普通设备上运行必然采用了量化和分布式推理方案。常见的做法是使用 4-bit 量化将模型压到约 20GB例如通过 bitsandbytes 库或 GGUF 格式实现from transformers import AutoModelForCausalLM, BitsAndBytesConfig quant_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_compute_dtypefloat16, bnb_4bit_quant_typenf4 ) model AutoModelForCausalLM.from_pretrained( model_path, quantization_configquant_config, device_mapauto )load_in_4bitTrue启用 4-bit 量化bnb_4bit_compute_dtypefloat16表示计算时反量化为 FP16保持数值稳定性device_mapauto让框架自动分配模型层到可用的 GPU 和 CPU 显存。如果显存仍然不足可以再加一条将模型放在 CPU 上使用较小的 batch size 推理。延迟会上升但至少能跑起来做功能验证。如果只是做开发和测试推荐另外一个实用策略先用较小的模型比如 7B 或 14B跑通整个流程验证 NER、意图识别、图谱查询、答案生成这条链路的正确性再替换成 34B 模型做最终效果评估。这样调试阶段的迭代成本会低很多。6.3 微调数据的质量控制与训练参数参考意图识别和回答生成的质量很大程度上取决于微调数据的质量。lora_data目录中的训练数据如果直接使用需要注意检查三种问题一是答案与问题的相关性低相关样本会误导模型生成东拉西扯的回答二是答案的准确性医学内容需要人工抽检错误知识一旦被微调进模型就很难清除三是数据格式的一致性指令和回答的分隔符、角色标记必须统一。LoRA 微调的核心参数通常在configs目录的配置文件中关键参数包括lora_r、lora_alpha、lora_dropout、learning_rate和num_epochs。lora_r决定低秩矩阵的维度一般设为 8 到 16 之间太小的秩表达力不足太大的秩又失去了参数高效微调的意义lora_alpha控制 LoRA 权重与基础模型权重的融合比例经验值设为lora_r的两倍learning_rate使用1e-4到3e-4比全量微调常用的5e-5稍高因为 LoRA 只需要更新极少量的参数。微调完成后需要手动验证训练是否有效即在测试集上做推理对比检查微调前后的回答差异是否符合预期不要只看训练 Loss 下降就认为完事。一个实用的验证方法是准备一组「未见过」的医疗问题微调前跑一遍记录回答微调后再跑一遍比较两个版本的回答确认修改了哪些行为。如果模型在微调后开始用训练集中的措辞回答所有问题说明出现了过拟合需要增加数据多样性或降低训练轮数。6.4 性能评估与优化收尾整套系统跑通之后还需要关注回答延迟、缓存策略和并发的处理。图谱访问要加缓存大模型推理要从输入输出长度上做限制。最后的调优顺序是先查检索是否准确再调生成质量最后才是性能和界面顺序反了容易白费功夫。本文还有配套的精品资源点击获取
返回列表