资讯详情

RAG医疗问答系统实战:知识图谱与大模型全链路拆解

📅 2026/10/25 9:09:31 | 华诺云谱 👁 阅读
RAG医疗问答系统实战:知识图谱与大模型全链路拆解
简介这是一套以RAG与大模型技术为核心的医疗问答系统项目资源面向人工智能、健康医疗方向的研究者与开发者适合希望掌握知识图谱构建、实体识别及大模型意图识别落地方法的读者。资源包含75个文件涵盖Python脚本、Jupyter Notebook示例、JSON与YAML配置、文本数据及示意图涉及Neo4j图谱搭建、BERT命名实体识别、34b大模型意图识别、知识检索与问答生成等关键环节压缩包整体约84.65MB目录按数据、模型、训练配置等模块划分便于对照实践。目前已有840人学习下载。通过该项目可了解从DiseaseKG数据集处理、NER数据增强到微调训练与WebUI部署的完整流程获得可直接参考的实验代码、说明文档与界面示意有助于降低医疗大模型应用落地的探索成本。1. RAG 医疗问答系统为什么先检索后生成才是医疗 AI 的及格线直接拿通用大模型回答医疗问题表面流畅实际上你没法信它它会把某种罕见病的病因说得头头是道但追问一句这个结论是哪条文献来的就立刻露馅。这个基于 RAG 与大模型技术的医疗问答系统把回答拆成两个可靠步骤——先从 DiseaseKG 数据集和 Neo4j 知识图谱里把事实精确检索出来再让大模型基于检索结果生成回答等于给大模型配了一份可校验的医学笔记而不是让它凭记忆裸答。系统里没有用一套模型包打天下BERT 负责把疾病、药品、症状、检查项目这些实体从问句里抠出来34b 大模型负责判断用户到底想要什么类型的答案最后由 langchain 把图谱查询结果拼进 prompt交给 chatglm 生成答案。适合谁正在落地 RAG 知识库的开发者、做医疗或垂直领域问答的产品团队以及想看清一条完整 RAG 链路长什么样的人。带问题来读这篇直接教你拆这个工程。2. 知识图谱底座从 medical.json 洗数据到 Neo4j 三元组落地2.1 解压后先分三类文件原始数据、实体词典、图谱脚本拿到 zip 先别急着看代码第一件事是分清文件位置。这个工程里数据相关的文件大概分四组medical.json 和 medical_new_2.json 是原始医疗数据与清洗后版本科目.txt、检查项目.txt、药品商.txt、疾病.txt、治疗方法.txt、食物.txt、药品.txt、疾病症状.txt 是一组实体词典文件它们定义了知识图谱中节点的类型和名称范围ent_aug 和 ner_data_aug.txt、rel_aug.txt 是实体和关系的增强数据用于 NER 模型训练nl2cyhper_data.txt、nl2cyhper_data_test.txt、nl2cyhper_data-Copy1.txt 是自然语言到 Cypher的配对语料给后面的查询生成环节用。这四组文件对应了 RAG 系统的四个环节原始数据做清洗实体词典定义图谱 schema增强数据训练实体识别nl2cypher 语料做查询生成。所以流程不是数据进模型这么简单而是先有图谱再有能查图谱的中间件最后才轮到生成答案。我一般建议先读 medical_new_2.json 里的一条记录把字段结构记下来后面所有脚本都以这个结构为准。字段大概长这样以下按常见规划说明不同批次数据字段略有差异{ disease: 高血压, symptom: [头痛, 眩晕, 耳鸣], medicine: [硝苯地平, 卡托普利], check: [血压测量, 心电图], treatment: [药物治疗, 生活方式干预] }这个 JSON 结构直接决定了 build_up_graph.py 里怎么建节点和关系疾病是主节点症状、药品、检查项目、治疗方法是它的邻接节点。参数上注意一组疾病对应多个症状和药品所以建关系时要循环遍历而不是一对一赋值。关系名称要统一比如有症状使用药品需要检查采用治疗后面 nl2cypher 生成查询时才能对上。2.2 build_up_graph.py连库、清库、merge 三件套图谱构建脚本大概是这个工程的入口。它的核心动作只有三件事连上 Neo4j、清空旧库、按三元组写入节点和关系。别小看清空旧库这一步如果你反复调试脚本Neo4j 里会累积大量重复节点后面查出来的结果全是重影你会以为是检索逻辑写错了其实是脏数据。常见的写入方式是这样的from py2neo import Graph, Node, Relationship import json # 连接 Neo4juri 和账号按本地部署环境改 graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) # 清库调试期每次跑脚本前执行避免重复写入 graph.run(MATCH (n) DETACH DELETE n) # 读取清洗后的数据 with open(medical_new_2.json, r, encodingutf-8) as f: data json.load(f) for item in data: disease Node(疾病, nameitem[disease]) graph.merge(disease, 疾病, name) for s in item.get(symptom, []): sym Node(症状, names) graph.merge(sym, 症状, name) graph.merge(Relationship(disease, 有症状, sym))这里 merge 而不是 create 是重点merge 会先按疾病 name去重存在就直接返回已有节点不存在才创建。create 每次执行都会新建跑第二次脚本图谱就翻倍。批量写入时我习惯用 graph.begin() 开一个事务攒几百条 merge 一次提交否则数据量大时单条 commit 会让 Neo4j 的压力集中爆发。参数上注意 uri 是 bolt 协议不是 httppy2neo 走 bolt 端口 7687浏览器管理界面的 7474 只是页面端口这两个端口容易搞混很多连接失败都来自于端口选错。事实上节点 Label 用中文是可以的Neo4j 支持 UTF-8 的 Label但要注意后续所有查询脚本里的 Label 字符串必须一字不差。我见过最典型的翻车是治疗_方法和治疗方法这种版本不一致图谱里建的是 A生成查询用的是 B结果永远查不到。这一点后面避坑章还会再展开这里先把命名规范定死全部用无下划线的中文名词一个文件里只出现一套写法。图谱构建完成后还有一个容易被新手忽略的步骤给查询字段建索引。比如每次查询都按 name 匹配疾病节点那就应该建CREATE INDEX disease_name IF NOT EXISTS FOR (d:疾病) ON (d.name)。没有索引时Neo4j 会全库扫描节点数据量到几万条之后查询时间会从毫秒级跳到秒级。这个工程里数据量不大索引可以先不加但如果你把它换成真实医院的百万级数据这条必须补上。2.3 nl2cypher 语料把图查询变成翻译任务图谱建好之后难点就从建图变成让模型知道怎么问图。这是 Text-to-Cypher 问题这个工程里的解法是用 nl2cypher 数据。看文件名你就能懂它的意思nl2cyhper_data.txt 把用户问句和对应 Cypher 语句配对存放格式大致是高血压有哪些症状 MATCH (d:疾病 {name:高血压})-[:有症状]-(s:症状) RETURN s.name 感冒了吃什么药 MATCH (d:疾病 {name:感冒})-[:使用药品]-(m:药品) RETURN m.name这个配对语料的用途有两个方向。第一个是微调拿它去做小模型的 Text-to-Cypher 微调让模型学会把问句映射成图查询nl2cyhper.py 就是干这个的第二个是给大模型当 few-shot 示例在 prompt 里放几条 nl2cypher 样例让 34b 模型照着样例输出 cypher。工程里两个文件都有说明作者两种路线都试过。要和文件型 RAG 区分开文件型 RAG 知识库检索的是文档切片本质是语义相似度知识图谱是结构化 RAG 知识库检索出来的是某疾病关联某症状这种精确三元组。前者擅长回答这个文档里有没有提到某个话题后者擅长回答A 和 B 之间是什么关系。医疗问答恰恰大量需要后者比如高血压的并发症有哪些这种问题用文档切片检索很容易漏用图查询是唯一确定答案。这个选择是这个医疗问答系统最大的设计决策也是为什么这个工程值得拆开看的核心原因。3. 双引擎配合BERT 负责实体、34b 负责意图、拼出可执行 Cypher3.1 NER 不是难点但标签体系要对齐实体识别环节在这个工程里落在 BERT 上理由是性价比医疗问句短、实体类型固定用 BERT-base 微调一个 token 分类模型推理速度远快于大模型识别精度在常见实体上也足够。工程里 ner_data_aug.txt 是增强后的训练语料ner_model.py 负责训练和预测tag2idx.npy 保存 label 到 id 的映射。预测流程分三步加载分词器、加载模型、把问句转成 token 序列后取每个 token 的 logits。一个常见写法是import torch import numpy as np from transformers import BertTokenizer, BertForTokenClassification tokenizer BertTokenizer.from_pretrained(bert-base-chinese) tag2idx np.load(tag2idx.npy, allow_pickleTrue).item() idx2tag {v: k for k, v in tag2idx.items()} model BertForTokenClassification.from_pretrained( ner_model/, num_labelslen(tag2idx) ) model.eval() def predict_ner(text, max_len128): tokens tokenizer.tokenize(text) ids tokenizer.convert_tokens_to_ids(tokens) with torch.no_grad(): output model(torch.tensor([ids])) preds torch.argmax(output.logits, dim-1)[0].tolist() entities [] cur None for tok, idx in zip(tokens, preds): tag idx2tag[idx] if tag.startswith(B-): cur {type: tag[2:], text: tok} elif tag.startswith(I-) and cur: cur[text] tok elif tag O: if cur: entities.append(cur) cur None return entitiesmax_len 参数决定了长问句会被截断到多长医疗问句一般不超过 50 个 token128 是安全值。如果你在真实业务里遇到描述一段症状后问应该挂什么科这种长句建议把 max_len 提到 256但代价是推理变慢。返回的实体是类型加文本的字典比如 [{type: 疾病, text: 高血压}, {type: 症状, text: 头痛}]这些实体在后面拼 Cypher 时直接作为查询条件。NER 是黑匣子里最容易翻车的一环模型不会报错只是安静地输出一堆O标签。这种时候先别怀疑代码先检查 tag2idx 的顺序是不是和训练时一致——这个坑很多人在联调后期才踩到排查起来非常耗时间。3.2 意图识别为什么要用 34b 而不是小模型有了实体还得知道用户问的是症状还是药这就轮到意图识别。工程里选择 34b 大模型做意图识别而不是用一个小的分类模型是因为医疗问句天然多意图混杂我最近咳嗽发烧应该做什么检查吃什么药这句话同时包含检查意图和药品意图。小意图分类模型通常输出单标签遇到这种复合问句就废了34b 模型可以被要求输出意图列表而且能理解口语化的表达。我见过一个做工整的做法prompt 里给定固定的意图枚举——症状查询、治疗查询、药品查询、检查项目查询、统计查询、闲聊要求模型以 JSON 数组返回命中的意图和关键信息意图枚举症状查询、治疗查询、药品查询、检查项目查询、统计查询、闲聊 请判断下面问题属于哪些意图输出 JSON 数组例如 [症状查询,药品查询]。 问题咳嗽发烧应该做什么检查吃什么药这个 prompt 模板可以放在工程里的配置文件里调用 34b 模型时用 transformers 或 vllm 的 generate 接口temperature 设低一点0.2 左右避免意图识别结果随意漂移。需要提醒的是34b 这个说法对应的是模型参数量实际模型权重并没有随压缩包一起提供部署时按你本地的大模型环境来对齐路径和加载方式。不要觉得用大模型做意图识别是杀鸡用牛刀。在这个工程里BERT 是快刀负责精确到 token 的实体抽取34b 是慢脑子负责把模糊的口语问句归类。两者的速度差反而构成一个优势90% 的简单问句走 BERT 路径秒回只有复杂多意图问句才需要调用 34b。这一层分流在真实部署里能省下大量算力。3.3 把实体和意图粘起来nl2cyhper.py 的查询拼装实体和意图都有了剩下一件事把它变成 Neo4j 能执行的 Cypher。工程里 nl2cyhper.py 承担这个拼装任务。核心逻辑可以这样描述根据意图确定关系类型根据 NER 实体确定查询起点然后把两者拼成固定模板。REL_MAP { 症状查询: (疾病, 有症状, 症状), 药品查询: (疾病, 使用药品, 药品), 检查查询: (疾病, 需要检查, 检查项目), 治疗查询: (疾病, 采用治疗, 治疗方法), } def build_query(intent, entities): if intent not in REL_MAP: return None start_label, rel, end_label REL_MAP[intent] entity_name entities.get(start_label) if not entity_name: return None return ( fMATCH (d:{start_label} {{name:{entity_name}}}) f-[r:{rel}]-(e:{end_label}) RETURN e.name )看起来简单但注意两个细节。第一个是实体名的转义真实数据里难免有特殊字符拼接前最好做一次引号过滤不然用户输入一个带单引号的问句会把 Cypher 整个炸掉。第二个是生成失败时的兜底如果 NER 没抓到任何实体直接返回 None让上层去重新调用大模型做一次宽松抽取而不是硬生生拼出一条错误的查询。宁可查不到不要查错这是医疗场景的底线。这一章的思路概括起来就两个字分工。BERT 管字34b 管意拼装脚本管型。任何一层单独看都不复杂但它们合在一起才构成精确的知识检索这六个字。4. 避坑Neo4j 连接、NER 标签、34b 显存与 lora 合并的五个现场4.1 现象py2neo 连不上 Neo4j报 The client is unauthorized 或 Direct connection is not allowed原因大多数人把 Neo4j 装成最新版 5.x而工程里的旧脚本用的 py2neo 还停留在 2021 年左右两者握手协议不兼容。Neo4j 5.x 默认只接受 neo4j:// 协议py2neo 走 bolt:// 连不进去。解决两个方案二选一。要么锁版本Neo4j 4.4.x 配 py2neo 2021.2.4依赖关系最稳很多老工程都在这套组合上跑要么换官方驱动用 neo4j 官方 python driver 替代 py2neo连接串改成 Neo4j 5.x 支持的路由格式。我实际跑这类工程时习惯先看工程建设用的 py2neo 版本再决定 Neo4j 版本不要让 Neo4j 版本反过来绑架脚本。注意如果你的工程里 py2neo 版本已经锁死优先锁定 Neo4j 4.4.x这是最少改动的方案。4.2 现象NER 模型推理结果全是 O一个实体都抽不到原因这种不是模型训废了多半是推理时加载的 tag2idx.npy 和训练时的 label 顺序不一致。训练时标签顺序是 [O, B-症状, I-症状, B-疾病, I-疾病...]推理时加载的却是按拼音排序后的另一套argmax 得到的索引对应的标签就全偏了。解决训练完成后把 id2label 单独持久化一份而不是只存 model 权重推理侧严格加载同一份映射文件。我在工程里见过最稳妥的写法是把 idx2tag 也存成 npy和 tag2idx 成对保存每次推理前先验证 len 和首尾元素不一致就直接报错不让模型静默跑下去。这个检查成本极低但能省掉大把调试时间。4.3 现象34b 模型一加载就 OOM还没到推理原因34b 参数量的模型即使 fp16 也需要约 68GB 显存单卡 A100 40G 都撑不住更别说消费级显卡。工程代码里如果写死 torch_dtypetorch.float16 满精度加载OOM 几乎是必然。解决用 4bit 量化加载常见做法是 transformers 配合 bitsandbytes 设 load_in_4bitTrue或直接用 GPTQ 量化版模型实测量化后占用可以压到 20GB 上下。如果你用的是 vllm 部署可以进一步限制 max-model-len 缩短 KV cache用长上下文换显存。这里最忌讳的是开 swap 硬撑性能会掉到没法用的程度属于典型的看着能跑实际白跑。4.4 现象lora 微调完推理结果和原模型没有任何区别原因微调产物只是低秩 adapter推理脚本只加载了 base model没有加载 adapter 权重等于没微调。这个现象理论上说过无数次实操中几乎每个人都会踩一遍。解决加载时用 PeftModel.from_pretrained(base_model, lora_data) 再 merge_and_unload()或者推理时显式指定 peft 配置加载 adapter。工程里 finetune_hf.py 训练出 lora 权重后inference_hf.py 里对应加载步骤千万别省。验证是否生效的办法很简单同一个 prompt 在微调前后各跑一次对比输出差异没差异就是 adapter 没挂上。4.5 现象nl2cypher 生成的查询报 Invalid input (或者查出来是空原因大模型生成的节点名、关系名和 Neo4j 库里的实际 Label 对不上。常见于治疗_方法和治疗方法这种版本不一致以及疾病名里有空格或繁体差异。空结果尤其坑不见得是查错了可能只是 Label 拼错了但看起来像库里没数据。解决把图谱 Label 白名单化拼 cypher 时用固定模板加从白名单里选 Label而不是让模型直接输出整条语句。可靠的做法是让模型只输出实体槽位值Cypher 由本地模板拼接这样模型永远没机会写错 Label。我在联调时习惯把所有 Label 同步出一份 JSON 清单传给生成端校验用两边各存一份同步靠脚本不靠人肉复制。5. RAG 链路组装与 webui 联调从提问到答案展示的完整走法5.1 RAG 组装顺序检索结果先压缩再进 prompt前面两章把检索链路拆开了现在到组装环节。整个工程里 langchain 是胶水层真正的核心链路是一条固定流水线webui 拿到用户问题 → 34b 意图识别 → BERT NER → nl2cyhper 拼 Cypher → Neo4j 查询 → 把结果文本化 → 拼进 prompt → chatglm 生成答案。组装顺序有一点很多人容易搞反不是把整个图谱查询结果直接塞给大模型。从 Neo4j 查出来可能有一长串症状和药品此时需要做压缩和过滤选取前 5~8 条作为限定上下文做成一段事实清单再接用户问题。from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser prompt ChatPromptTemplate.from_messages([ (system, 你是医疗助手。只能根据给定事实作答不得编造。\n事实{context}), (human, {question}) ]) def answer(question, fact_list): context .join(fact_list[:8]) chain prompt | llm | StrOutputParser() return chain.invoke({context: context, question: question})fact_list[:8] 这个截断很重要大模型上下文长度有限context 越长越容易把关键信息挤出注意力窗口。实际医疗场景里我把上限调成 6 条宁可少给不要给到不相关的内容——因为大模型的通病是给什么就引用什么一条不相关的检索结果会带跑整段生成。temperature 这个参数在这种检索增强场景建议压到 0.2 以下。意图识别要稳定答案生成更要稳0.2 足够让表达自然又不会让事实发生突变。如果发现输出还是偶尔乱跑先看是不是 temperature 忘了改回低值。5.2 登录与用户数据落库login.py user_data_storage.py有了链路还要有壳。工程里 webui.py 是入口login.py 处理登录user_data_storage.py 负责把对话历史写进本地文件。这个设计在真实部署里非常重要医疗问答需要对同一个用户连续多轮追问如果不把上一次的完整问答落盘多轮对话就是破碎的。user_data_storage.py 的读写逻辑简单但关键按用户名分文件存储每个文件追加一条记录格式是时间戳 用户问题 系统答案 检索上下文。为什么连检索上下文也要存因为这是后期复盘唯一的依据。如果只存问题和答案出问题时你根本不知道是检索错还是生成错。我见过太多生产事故因为没存检索上下文而只能靠猜。import json, time, os DATA_DIR tmp_data def save_record(username, question, answer, fact_list): path os.path.join(DATA_DIR, f{username}.json) records [] if os.path.exists(path): with open(path, r, encodingutf-8) as f: records json.load(f) records.append({ ts: time.strftime(%Y-%m-%d %H:%M:%S), question: question, answer: answer, facts: fact_list }) with open(path, w, encodingutf-8) as f: json.dump(records, f, ensure_asciiFalse, indent2)注意 append 语义读整个文件再写整个文件数据量大了会慢但医疗问答场景一天几千条完全够用。如果将来 QPS 上来再迁到 sqlite 或 MySQL核心字段还是这三样问题、答案、事实列表——这个数据结构会一直沿用。5.3 微调配置怎么选lora.yaml、ptuning_v2.yaml、ds_zero_3.json工程里 finetune_hf.py 直接支持多种微调方式configs 目录下的 sft.yaml、lora.yaml、ptuning_v2.yaml、ds_zero_3.json 分别是全参微调、LoRA、P-Tuning v2 和 ZeRO-3 数据并行配置。这套布局说明作者在同一份代码里兼容了三种规模的算力条件。我给个配置选择的参考表配置适用场景显存要求建议lora.yaml单卡消费级显卡微调 34b 以下模型显存省8~24G 可跑首选参数改动小ptuning_v2.yaml只微调 prompt embedding不想动主干更低适合轻量场景效果上限低ds_zero_3.json多卡全参微调 34b多卡并行有集群才考虑lora.yaml 里最需要调的是 lora_rank 和 target_modules。我的经验是领域适配需求越强rank 越大rank 从 8 起步最大到 64 基本覆盖绝大多数场景。target_modules 默认抓 q、k、v、o 投影即可不必强行加 MLP。下面这段是 lora.yaml 的关键部分样例注意学习率要明显小于全参微调因为 adapter 在极小参数空间里更新太快容易过拟合model_name_or_path: /path/to/34b_model lora_rank: 32 lora_alpha: 64 lora_dropout: 0.05 target_modules: [q_proj, k_proj, v_proj, o_proj] learning_rate: 2.0e-4 num_train_epochs: 3 per_device_train_batch_size: 1 gradient_accumulation_steps: 8lora_alpha 通常取 rank 的两倍这个比例是 LoRA 论文里的经验值不是玄学是防止缩放因子失真。gradient_accumulation_steps8 配合 batch_size1 相当于等效 batch 8显存受限时的标准做法。跑微调之前先把 train.json 和 dev.json 各抽一条肉眼过一遍确认输入输出格式和 tokenizer 的模板匹配这一步能避免大量无效训练时间。提示单卡 24G 环境优先走 lora.yaml别碰 ds_zero_3.json多卡配置在单机上跑还会引入额外的通信开销。5.4 前后端联调时最容易漏掉的一环缓存如果某个问题在前后端联调时反复出现同样答案不要先怀疑模型先看是不是 webui 层做了缓存。工程里 user_data_storage.py 只是存储但有些演示版会在内存里做 question-to-answer 的简单缓存。医疗问答场景的缓存要格外小心病情的表述千差万别即便两个问题语义接近答案也应该基于不同上下文生成而不是返回同一段缓存文本。所以前后端联调验证时建议把缓存先关闭验证链路本身是否正确再决定哪类高频问题开缓存。缓存一旦设计不当反而会成为整个 RAG 链路的瓶颈。6. 进阶用 log 回放法判断 RAG 是检索失败还是生成失败工程里带 log.txt 和一堆结果 ipynb结果2.ipynb、结果3.ipynb、NER结果保存1.ipynb这些不是摆设是复盘的关键资产。我在用这类 RAG 工程时养成一个固定习惯每跑一条问答就把六个字段落一次 log——原始问题、BERT 抽取的实体、34b 判定的意图、拼出的 Cypher、图谱返回的事实清单、大模型生成的答案。一份最小的落盘函数长这样def append_log(question, entities, intent, cypher, facts, answer): with open(log.txt, a, encodingutf-8) as f: f.write(json.dumps({ q: question, entities: entities, intent: intent, cypher: cypher, facts: facts, answer: answer }, ensure_asciiFalse) \n)复盘方法其实是一套排除法。答案错先打开 log 看 facts 对不对facts 对但答案错问题在生成阶段去调 prompt 或 temperaturefacts 错再看 cypher 拼得对不对cypher 错再往前查是 NER 抽错了实体还是意图分类错了。这样一层层往回找两分钟就能定位根因而不是对着模型输出干瞪眼。这个工程里现成的几个结果 ipynb 就是这种回放法的产物每块 cell 记录一个环节的中间结果跑完一轮把 NER 结果和查询结果对照着看很容易发现NER 把感冒识别成了症状而不是疾病这类问题。从那以后我每次跑医疗问答联调都强制先走一遍 log 回放先怀疑检索再怀疑模型——这个习惯替我省了至少三次返工。需要下载这套工程的话压缩包里 README_en.md 和 requirements.txt 是对齐环境的第一站先按它锁好版本再动代码。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑