
简介这是一套面向高校学生与开发者的医疗问答系统完整项目资源基于RAG与大模型技术构建融合DiseaseKG数据集与Neo4j知识图谱结合BERT命名实体识别和34b大模型意图识别通过精确知识检索与问答生成提升医疗咨询性能着力解决大模型在医疗领域应用的可靠性问题。资源包共75个文件约84.65MB涵盖Python源码、Jupyter Notebook实验记录、JSON与CSV数据文件、PNG/JPG界面截图、YAML配置及Markdown说明文档覆盖知识图谱构建、模型微调、NER训练与Web交互等模块。已有157人学习关注。项目经过严格测试运行功能完整可复现答辩评审平均分达96分适用于毕业设计、课程设计、大作业、工程实训及学科竞赛等场景也可作为学习练手或二次开发的基础设计报告同样具备借鉴价值。使用中遇到问题可联系作者获取解答与相关开发资料。1. 医疗问答系统为什么不能只靠大模型硬答从一次答错用药剂量说起医疗问答系统最怕的不是答不上来而是答得理直气壮却错了。我最早做的一个版本直接把大模型接进对话框问它「二甲双胍肾功能不全怎么调剂量」它给了一段看起来非常专业的回答剂量、注意事项、禁忌都齐了但拿去和说明书一对照关键阈值是错的。问题不在模型不够大而在于它是在用参数里的模糊记忆回答一个必须精确的问题。医疗场景对错误几乎零容忍这就是为什么现在做医疗问答主流方案都是 RAG 加知识图谱再叠加大模型先用检索把权威资料找出来再让模型基于资料组织语言而不是让它凭记忆自由发挥。这套思路适合做毕设、课设、实训和大作业的同学也适合想入门 RAG 实战的工程师因为它把检索、知识图谱、大模型调用三条线都串起来了做完能讲清楚每一环为什么存在。下面我按自己搭过的一版方案把选型、建库、检索、生成和踩坑完整讲一遍。2. 医疗 RAG 的架构选型为什么是 Neo4j 加 BERT 加大模型三件套2.1 纯向量 RAG 在医疗场景的三个硬伤最常见的 RAG 做法是「文档切块 → 向量化 → 相似度检索 → 塞给大模型」。这套流程在通用问答里够用但放到医疗领域会暴露三个问题。第一是切块会切断语义一份用药说明里「禁忌」和「慎用」往往隔了好几段切完以后检索到的块可能只有半句条件模型补全时就容易编。第二是向量相似不等于医学相关「糖尿病」和「低血糖」在向量空间里可能很近但一个是病一个是并发症检索时容易混进来。第三是医疗问题经常是多跳的比如「这个药对孕妇的甲状腺功能有没有影响」需要先定位药再定位成分再定位对甲状腺的作用单次向量检索覆盖不了这种链路。我一般会在这套流程上加一层知识图谱用 Neo4j 存实体和关系把「药—成分—适应症—禁忌—人群」这些结构化关系显式建出来。检索时先走图谱拿到确定性的关联再用向量检索补充非结构化描述最后交给大模型做归纳。这样做的代价是建库成本高但换来的是可解释和可追溯答辩时能指着图谱说清楚答案从哪来。2.2 BERT 在链路里到底干什么很多人以为有了大模型就不需要 BERT 了其实在医疗 RAG 里 BERT 主要干两件脏活累活。一是做查询理解把用户口语化的提问映射到标准医学术语比如「吃了降糖药还是高」要能识别出「降糖药」和「血糖控制不佳」这两个意图再拿去检索。二是做实体识别从用户问题里抽出药品名、疾病名、检查指标这些实体是查 Neo4j 的入口。大模型当然也能做这些但每次调用成本高、延迟大而 BERT 类模型可以本地部署推理快适合放在检索前置环节。选型上中文医疗场景常用的是基于 BERT 的预训练模型做微调或者直接用现成的医疗 NER 模型。如果只是做课设用 HuggingFace 上公开的中文 BERT 加少量标注数据微调就能跑起来。要注意的是 BERT 的输出是向量和标签不是自然语言所以它和大模型之间需要一个适配层把实体和意图转成 Neo4j 的查询语句和向量检索的 query。2.3 大模型放在哪一环怎么选大模型在这套架构里只负责最后一步拿到检索回来的结构化事实和文本片段组织成通顺、有依据的回答。它不负责判断事实对错那是检索和知识库的事。选型上如果做毕设或实训优先考虑能本地部署或能稳定调用的开源模型比如常见的 7B 到 13B 级别中文能力较好的模型用 Ollama 或类似工具拉起来就能用。如果实验室有显卡本地跑推理最省心不用担心接口限流。如果没有用公开的推理 API 也可以但要注意医疗数据不要往外传脱敏后再调用。这里有个容易翻车的点很多人把大模型当成万能入口用户问什么就直接丢给模型模型答不上来再补检索。正确顺序反过来先检索再生成模型只做「基于以下资料回答」的题。提示词里要明确写「如果资料中没有相关信息回答不知道」否则模型会习惯性编造。2.4 最小可跑通的架构长什么样把上面几块拼起来一个能跑通的最小架构是用户提问 → BERT 做实体识别和意图分类 → 实体查 Neo4j 拿结构化关系 → 原始问题做向量检索拿文本片段 → 两路结果合并成上下文 → 大模型生成回答。数据层需要三样东西一份医疗知识图谱可以先用公开的医药数据集导入 Neo4j一份文档库药品说明书、诊疗指南等做向量化一个 BERT 模型做查询理解。这套东西用 Python 串起来几百行代码能跑通适合作为 rag 实战的起点。3. 用 Neo4j 建医疗知识图谱从安装到导入数据的完整命令3.1 Neo4j 社区版安装与内存配置Neo4j 社区版是免费的做课设够用。安装方式按系统分Linux 和 Mac 常用 tar 包或包管理器Windows 用安装包。装完第一件事是改内存配置默认配置往往偏小导入数据时容易卡死。配置文件在 conf/neo4j.conf重点改这几项# 堆内存初始值和最大值按机器内存调整8G 机器给 2G 左右 dbms.memory.heap.initial_size2G dbms.memory.heap.max_size2G # 页面缓存用于缓存图数据建议给到可用内存的 50% dbms.memory.pagecache.size2G # 监听地址本地开发用 localhost 即可 dbms.default_listen_addresslocalhost改完重启服务。如果启动报内存不足先确认机器实际可用内存再把 heap 调小。有个常见坑是只改了 heap 没改 pagecache导入几万节点后查询变慢就是因为图数据没进缓存。启动后用cypher-shell连进去或者浏览器打开 7474 端口用 Neo4j Browser后者对新手更友好能直接看图谱。3.2 医疗图谱的节点和关系怎么设计医疗知识图谱不需要一上来就建全先覆盖核心实体药品、成分、疾病、症状、人群、检查指标。关系按问答需要设计常见的有「药品—治疗—疾病」「药品—含有—成分」「药品—禁忌—人群」「疾病—表现—症状」。设计原则是用户问题里会问到的关系才建问不到的先不建否则图会变得又大又难维护。下面是一段 Cypher 建节点和关系的示例用 MERGE 而不是 CREATE避免重复导入// 建药品节点name 作为唯一约束 CREATE CONSTRAINT drug_name IF NOT EXISTS FOR (d:Drug) REQUIRE d.name IS UNIQUE; // 建疾病节点 CREATE CONSTRAINT disease_name IF NOT EXISTS FOR (dis:Disease) REQUIRE dis.name IS UNIQUE; // 插入一个药品和疾病并建立治疗关系 MERGE (d:Drug {name: 二甲双胍}) SET d.category 降糖药, d.usage 口服 MERGE (dis:Disease {name: 2型糖尿病}) MERGE (d)-[:TREATS {evidence: 一线用药}]-(dis); // 插入禁忌人群关系 MERGE (p:Population {name: 严重肾功能不全}) MERGE (d)-[:CONTRAINDICATED_FOR {reason: 乳酸酸中毒风险}]-(p);这段代码先建唯一约束保证同名节点不会重复。MERGE 的语义是「有则匹配无则创建」适合反复导入。关系上的属性用来存证据和原因检索时可以带出来给大模型做依据。参数说明evidence和reason是自定义属性按你的数据源填没有就省略。3.3 从 CSV 批量导入药品和疾病数据手工插几条可以真做项目得批量导入。Neo4j 提供LOAD CSV命令适合导入几万到几十万行。准备两个 CSVdrug.csv 和 disease.csv再加一个关系文件 drug_disease.csv。导入命令// 导入药品节点 LOAD CSV WITH HEADERS FROM file:///drug.csv AS row MERGE (d:Drug {name: row.name}) SET d.category row.category, d.usage row.usage; // 导入疾病节点 LOAD CSV WITH HEADERS FROM file:///disease.csv AS row MERGE (dis:Disease {name: row.name}) SET dis.description row.description; // 导入治疗关系 LOAD CSV WITH HEADERS FROM file:///drug_disease.csv AS row MATCH (d:Drug {name: row.drug}) MATCH (dis:Disease {name: row.disease}) MERGE (d)-[:TREATS {evidence: row.evidence}]-(dis);CSV 文件要放在 Neo4j 的 import 目录下路径写相对路径。WITH HEADERS表示第一行是列名。导入关系时用 MATCH 先找到两端节点再 MERGE 关系如果某个节点不存在这条关系会被跳过不会报错所以导入后要检查关系数量对不对。常见坑是 CSV 编码不是 UTF-8中文会乱码导入前用编辑器确认编码。3.4 从一个节点出发查多条路径的写法热词里有人问「neo4j 查询从一个节点出发如何查询多条」这在医疗问答里很常用比如从药品出发查它的成分、适应症、禁忌、相互作用。Cypher 用MATCH配合多条路径模式就能做到// 从药品出发查它的成分、治疗的疾病、禁忌人群 MATCH (d:Drug {name: 二甲双胍}) OPTIONAL MATCH (d)-[:CONTAINS]-(ing:Ingredient) OPTIONAL MATCH (d)-[:TREATS]-(dis:Disease) OPTIONAL MATCH (d)-[:CONTRAINDICATED_FOR]-(p:Population) RETURN d.name AS drug, collect(DISTINCT ing.name) AS ingredients, collect(DISTINCT dis.name) AS diseases, collect(DISTINCT p.name) AS contraindications;这里用 OPTIONAL MATCH 而不是 MATCH是因为某个关系可能不存在用 MATCH 会导致整行结果消失。collect(DISTINCT ...)把多条结果聚合成列表方便后续传给大模型。如果关系层级更深比如药品到成分再到靶点可以继续在后面接 MATCH但要注意路径爆炸查询前先用 LIMIT 试跑。4. BERT 查询理解与向量检索把用户问题变成可查的实体和向量4.1 BERT 模型下载与本地部署BERT 类模型从 HuggingFace 或国内镜像下载做中文医疗 NER 一般选中文预训练模型再微调。下载后本地加载用 transformers 库几行代码就能跑from transformers import AutoTokenizer, AutoModelForTokenClassification import torch # 模型路径换成你下载或微调后的目录 model_path ./bert-medical-ner tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForTokenClassification.from_pretrained(model_path) model.eval() def extract_entities(text): inputs tokenizer(text, return_tensorspt, truncationTrue, max_length128) with torch.no_grad(): outputs model(**inputs) preds torch.argmax(outputs.logits, dim-1)[0].tolist() tokens tokenizer.convert_ids_to_tokens(inputs[input_ids][0]) # 这里按你的标签体系解析 preds 和 tokens拼出实体 return list(zip(tokens, preds)) print(extract_entities(二甲双胍对肾功能不全的人能用吗))这段代码加载模型并对输入做推理输出每个 token 的标签。实际使用时需要把标签映射回实体类型比如 B-DRUG、I-DRUG 拼成药品名。参数说明max_length按你的文本长度调医疗问题一般 128 够用truncationTrue保证超长文本不报错。如果没微调过模型输出的标签是随机的必须先用自己的标注数据微调或者直接用公开的医疗 NER 模型。4.2 把实体转成 Neo4j 查询拿到实体后要拼成 Cypher 查询。不要直接字符串拼接用参数化查询防注入from neo4j import GraphDatabase driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, 你的密码)) def query_drug_info(drug_name): cypher MATCH (d:Drug {name: $name}) OPTIONAL MATCH (d)-[:TREATS]-(dis:Disease) OPTIONAL MATCH (d)-[:CONTRAINDICATED_FOR]-(p:Population) RETURN d.name AS drug, collect(DISTINCT dis.name) AS diseases, collect(DISTINCT p.name) AS contraindications with driver.session() as session: result session.run(cypher, namedrug_name) return [record.data() for record in result]参数$name由驱动做转义避免 Cypher 注入。返回的 record.data() 是字典直接可以塞进上下文。如果实体识别没抽准这里查不到结果所以 BERT 的准确率直接影响整条链路这也是为什么查询理解不能省。4.3 文档向量化与检索非结构化的药品说明书、指南文本需要向量化。用 sentence-transformers 或类似工具把文档切块后编码存进向量库。检索时把用户问题也编码算相似度取 top-kfrom sentence_transformers import SentenceTransformer import numpy as np model SentenceTransformer(你的向量模型路径) # 假设 docs 是切好的文档块列表 doc_embeddings model.encode(docs, normalize_embeddingsTrue) def retrieve(query, top_k3): q_emb model.encode([query], normalize_embeddingsTrue)[0] scores np.dot(doc_embeddings, q_emb) idx np.argsort(scores)[::-1][:top_k] return [(docs[i], float(scores[i])) for i in idx]normalize_embeddingsTrue让向量归一化点积就等于余弦相似度。top_k 一般取 3 到 5太多会超出大模型上下文太少可能漏掉关键信息。医疗场景建议把相似度阈值也加上低于阈值的片段不返回宁可说不知道也不要塞无关内容。4.4 两路结果合并成上下文图谱结果和向量结果要合并成一段结构化文本再给大模型。合并时标注来源方便模型引用def build_context(graph_result, vector_result): lines [【知识图谱结果】] for item in graph_result: lines.append(f药品{item[drug]}适应症{item[diseases]}禁忌{item[contraindications]}) lines.append(【文档检索结果】) for text, score in vector_result: lines.append(f相似度{score:.2f}{text}) return \n.join(lines)这样拼出来的上下文既有结构化事实又有原文描述大模型生成时能兼顾准确和通顺。注意控制总长度超出模型上下文窗口要截断优先保留图谱结果因为它的确定性更高。5. 避坑与排查医疗 RAG 上线前必须过的五道坎5.1 检索命中率低答非所问现象用户问「孕妇能不能吃某药」系统返回的是该药的一般说明没有禁忌信息。原因通常是切块时把禁忌段落切散了或者向量模型对「孕妇」和「妊娠」这类同义词不敏感。解决方法是切块时按语义段落切不要固定字数硬切同时在检索前做同义词扩展把「孕妇」扩展成「孕妇、妊娠、哺乳期」再检索。另外可以给图谱里的禁忌关系加权重检索时优先返回。5.2 大模型无视检索结果自己编现象检索回来的资料里没有某个信息模型还是答了出来。原因是提示词没约束好或者上下文太长模型没注意到关键句。解决方法是提示词里明确写「只根据以下资料回答资料没有的写不知道」并把资料放在问题前面问题放最后。如果还不行降低 temperature让输出更保守。医疗场景 temperature 建议 0.1 到 0.3。5.3 Neo4j 导入中文乱码现象CSV 导入后节点名字变成问号或乱码。原因是 CSV 文件编码不是 UTF-8或者 Neo4j 启动时 JVM 编码不对。解决方法是导入前用file -i或编辑器确认 CSV 是 UTF-8如果是 GBK 先转码。Neo4j 4.x 以上默认 UTF-8一般不用改 JVM 参数问题多出在 CSV 本身。5.4 BERT 实体识别漏抽关键实体现象用户问「二甲双胍和格列齐特能一起吃吗」只抽出了「二甲双胍」漏了「格列齐特」。原因是模型训练数据里并列药品的样本少。解决方法是标注数据里补充并列实体样本或者用规则兜底比如按「和」「与」「、」切分后再分别识别。另外可以在后处理阶段用药品词典做匹配把模型漏掉的补上。5.5 响应太慢答辩时卡住现象一个问答要等十几秒。原因通常是 BERT 和大模型都在 CPU 上跑或者 Neo4j 查询没走索引。解决方法是给 Neo4j 的 name 字段建索引前面建的唯一约束自带索引查询时用 EXPLAIN 看执行计划BERT 模型如果太大可以换小一号的大模型如果本地跑不动考虑用量化版本或减少 max_tokens。答辩前把常见问题预热一遍把检索结果缓存起来。6. 让回答可追溯给每条结论加上来源标注的一个技巧医疗问答和通用问答最大的区别是用户和评委都会问「你这个结论哪来的」。所以最后一章讲一个我实际用过的技巧在生成阶段强制模型给每条结论标注来源编号前端再把编号映射回具体文档或图谱路径。做法是在上下文里给每个片段编号提示词里要求模型引用编号prompt 你是一个医疗问答助手只根据以下资料回答。 资料 [1] 药品二甲双胍禁忌严重肾功能不全 [2] 文档片段二甲双胍在 eGFR 低于 30 时禁用…… 问题肾功能不全能吃二甲双胍吗 要求回答中每个结论后用 [编号] 标注来源资料没有的写不知道。模型输出类似「严重肾功能不全患者禁用二甲双胍 [1][2]」。前端拿到后把 [1][2] 渲染成可点击的角标点开显示原始资料。这个技巧的好处是即使模型某句话组织得不够好只要来源标对了用户也能自己判断。答辩时评委问「你怎么保证不瞎编」直接演示点开来源比说一堆架构图管用。我自己的习惯是每加一个新数据源先跑一遍固定测试集看来源标注的准确率低于 90% 就先修检索不急着调模型。医疗 RAG 这方向值得做但前提是接受它是个系统工程检索、图谱、模型三块哪块偷懒都会在回答里露馅。希望帮到你。本文还有配套的精品资源点击获取