医疗问答系统实战:RAG与知识图谱源码拆解
简介这份资源是一套基于RAG与大模型技术的医疗问答系统完整项目包面向计算机、人工智能、通信工程等专业的在校学生、教师及企业开发者可用于毕业设计、课程设计、项目立项演示或技术进阶学习。项目以DiseaseKG数据集和Neo4j构建医疗知识图谱融合BERT命名实体识别与34b大模型意图识别通过精确知识检索与问答生成提升医疗咨询的可靠性重点解决大模型在医疗领域落地时的可信度问题。压缩包共76个文件约84.65MB包含Python源码、Jupyter Notebook实验记录、JSON与CSV数据文件、YAML配置、Markdown说明文档以及PNG/JPG界面截图等覆盖数据处理、图谱构建、模型微调与Web交互等环节。该资源为个人高分项目已通过导师评审并获95分答辩成绩代码经测试可运行。目前已有213人学习适合在此基础上二次开发或直接用于毕设、课设与作业场景。1. 医疗问答系统为什么非要 RAG 加知识图谱一份能跑通的毕设级源码拆解医疗问答这个场景直接拿大模型硬答是很容易翻车的。模型会一本正经地编出并不存在的药品剂量、疾病关系而医疗领域对错误答案的容忍度几乎为零。这份资源给出的思路是用 DiseaseKG 数据集和 Neo4j 搭一张知识图谱把疾病、症状、药物、检查项之间的关系固化下来再用 BERT 做命名实体识别把用户问题里的关键实体抽出来配合 34b 大模型做意图识别最后走 RAG 检索增强生成让答案有据可查。它解决的核心问题就是大模型在医疗咨询里的可靠性——不是让模型更聪明而是让它别乱说。适合做毕设、课设、课程作业的计算机相关专业学生也适合想入门 RAG 实战和知识图谱构建的开发者。整套代码经过测试运行文档和资料齐全拿来就能改。2. 环境搭建与数据准备从 requirements 到 Neo4j 图谱落库2.1 依赖安装与目录结构确认拿到压缩包后先别急着跑代码第一步是把目录结构看清楚。根目录下有RAGQnASystem-main作为主工程data目录存放医疗数据model目录放模型权重configs放配置文件img是界面截图。requirements.txt在根目录和子目录各有一份说明不同模块的依赖可能有差异。我一般会先建一个干净的虚拟环境Python 版本建议 3.9 或 3.10太新的版本在装torch和neo4j驱动时容易遇到兼容问题。# 创建虚拟环境 python -m venv med_qa_env source med_qa_env/bin/activate # Windows 用 med_qa_env\Scripts\activate # 安装主工程依赖 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # 单独装 Neo4j 驱动和图谱相关库 pip install neo4j py2neo pandas numpy这里用清华源是因为torch、transformers这些包体积大默认源下载慢。neo4j和py2neo两个驱动都装上是因为项目里不同脚本可能用了不同的连接方式build_up_graph.py大概率用的是py2neo的 Graph 对象而查询接口可能走官方neo4jdriver。装完之后验证一下关键包版本import torch, transformers, neo4j print(torch.__version__) # 建议 1.13 或 2.0 print(transformers.__version__) # 建议 4.30 print(neo4j.__version__) # 建议 5.x如果transformers版本过高导致 BERT 加载报错回退到 4.28 左右通常能解决。这是血泪经验新版库经常改 API。2.2 DiseaseKG 数据结构解析与图谱构建data目录下的medical.json和medical_new_2.json是核心数据文件。medical.json通常是原始疾病知识条目每条记录包含疾病名称、症状、常用药品、检查项目、科室等字段。medical_new_2.json应该是经过清洗和增强后的版本可能补充了实体对齐或关系扩充。build_up_graph.py是图谱构建的入口脚本。它的逻辑一般是读取 JSON → 解析实体和关系 → 通过py2neo写入 Neo4j。在跑这个脚本之前确保 Neo4j 服务已经启动并且知道连接地址、用户名和密码。# 启动 Neo4j如果用的是 Desktop 版直接在界面点 Start # 如果用的是社区版命令行 neo4j start # 默认 Bolt 地址是 bolt://localhost:7687 # 默认用户名 neo4j密码是你首次设置的那个然后检查build_up_graph.py里的连接配置from py2neo import Graph, Node, Relationship # 这三行是常见写法确认地址和密码跟你的环境一致 graph Graph(bolt://localhost:7687, auth(neo4j, 你的密码)) # 清空旧数据避免重复导入 graph.delete_all() # 读取医疗数据 import json with open(data/medical.json, r, encodingutf-8) as f: medical_data json.load(f) # 遍历每条疾病记录创建节点和关系 for item in medical_data: disease_name item.get(name, ) if not disease_name: continue # 创建疾病节点 disease_node Node(Disease, namedisease_name) graph.create(disease_node) # 创建症状节点并建立关系 for symptom in item.get(symptoms, []): symptom_node Node(Symptom, namesymptom) graph.merge(symptom_node, Symptom, name) rel Relationship(disease_node, HAS_SYMPTOM, symptom_node) graph.create(rel) # 创建药品节点并建立关系 for drug in item.get(drugs, []): drug_node Node(Drug, namedrug) graph.merge(drug_node, Drug, name) rel Relationship(disease_node, USES_DRUG, drug_node) graph.create(rel)这段代码的关键点在于graph.merge()和graph.create()的区别。merge会先查再建避免同一个症状节点被重复创建create是直接新建。如果全用create图谱里会出现大量同名重复节点查询时结果会爆炸。另一个坑是delete_all()会清空整个库如果你库里还有别的项目数据千万别在生产环境跑。导入完成后在 Neo4j Browser 里跑一条验证查询MATCH (d:Disease)-[:HAS_SYMPTOM]-(s:Symptom) RETURN d.name, s.name LIMIT 10能看到疾病和症状的对应关系说明图谱构建成功。如果返回空检查 JSON 里的字段名是不是symptoms而不是symptom字段名对不上是新手最常踩的坑。2.3 BERT 命名实体识别模型的数据准备ner_data.py和ner_data_aug.txt是 NER 模块的数据处理脚本和增强数据。tag2idx.npy保存的是标签到索引的映射ner_model.py定义模型结构。roberta.txt可能是预训练模型名称或路径配置。NER 的任务是从用户问题里抽出疾病名、症状名、药品名、检查项等实体。比如用户问“糖尿病吃什么药”模型要能标出“糖尿病”是疾病实体。训练数据格式通常是 BIO 标注糖 B-Disease 尿 I-Disease 病 I-Disease 吃 O 什 O 么 O 药 Oner_data.py里一般会做这几件事读取标注文件、把字符转成 BERT 的 token、对齐标签、生成 DataLoader。跑之前确认bert-base-chinese或roberta的预训练权重已经下载到本地或者网络能访问 HuggingFace。from transformers import BertTokenizer, BertForTokenClassification import torch # 加载 tokenizer 和模型 model_name bert-base-chinese tokenizer BertTokenizer.from_pretrained(model_name) model BertForTokenClassification.from_pretrained(model_name, num_labelslen(tag2idx)) # 编码输入 text 糖尿病吃什么药 inputs tokenizer(text, return_tensorspt, paddingTrue, truncationTrue) # 推理 with torch.no_grad(): outputs model(**inputs) predictions torch.argmax(outputs.logits, dim-1) # 解码标签 idx2tag {v: k for k, v in tag2idx.items()} tokens tokenizer.convert_ids_to_tokens(inputs[input_ids][0]) for token, pred in zip(tokens, predictions[0]): if token not in [[CLS], [SEP], [PAD]]: print(token, idx2tag[pred.item()])num_labels必须跟tag2idx的长度一致否则加载权重时会报维度不匹配。truncationTrue是防止长文本超出 BERT 的 512 token 限制。如果显存不够把batch_size调到 8 甚至 4别硬撑。3. RAG 检索与意图识别把用户问题变成图谱查询3.1 34b 大模型意图识别的接入方式项目里提到用 34b 大模型做意图识别这个规模大概率是本地部署或 API 调用。finetune_demo和finetune_hf.py说明有微调流程lora_finetune.ipynb和微调运行.ipynb是微调的操作记录。意图识别的目标是判断用户问题属于哪一类问症状、问药品、问检查、问禁忌、还是闲聊。常见做法是用 LoRA 做轻量微调冻结大模型主体只训练低秩适配器。这样显存占用小34b 模型在单卡 24G 上也能跑起来。peft_data.txt和lora_data目录就是微调数据。from peft import LoraConfig, get_peft_model from transformers import AutoModelForCausalLM, AutoTokenizer model_name your-34b-model-path tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained(model_name, load_in_8bitTrue, device_mapauto) # LoRA 配置 lora_config LoraConfig( r8, # 秩越小参数越少 lora_alpha32, # 缩放系数 target_modules[q_proj, v_proj], # 注意力层的投影矩阵 lora_dropout0.1, biasnone, task_typeCAUSAL_LM ) model get_peft_model(model, lora_config) model.print_trainable_parameters() # 看看可训练参数占比load_in_8bitTrue是量化加载能把显存需求砍一半。target_modules要跟模型架构匹配LLaMA 系一般是q_proj和v_projChatGLM 系可能是query_key_value。填错了不会报错但训练时参数不更新loss 不降这是最隐蔽的坑。意图识别的输出是一个分类标签比如{intent: query_drug, confidence: 0.92}。这个标签决定后续走哪条图谱查询路径。3.2 nl2cypher把自然语言转成图谱查询语句nl2cyhper.py和nl2cyhper_data.txt是自然语言转 Cypher 查询的模块。用户问“糖尿病有哪些症状”系统要生成MATCH (d:Disease {name:糖尿病})-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name。这个转换有两种做法一种是模板匹配根据意图识别结果套预设模板另一种是训练一个 seq2seq 模型做翻译。项目里nl2cyhper_data_test.txt和nl2cyhper_data-Copy1.txt说明有训练数据可能是用 T5 或 BART 做的微调。# 模板匹配的简化版逻辑 def nl2cypher(intent, entities): if intent query_symptom: disease entities.get(disease, ) return fMATCH (d:Disease {{name:{disease}}})-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name elif intent query_drug: disease entities.get(disease, ) return fMATCH (d:Disease {{name:{disease}}})-[:USES_DRUG]-(dr:Drug) RETURN dr.name elif intent query_check: disease entities.get(disease, ) return fMATCH (d:Disease {{name:{disease}}})-[:NEED_CHECK]-(c:Check) RETURN c.name else: return None模板匹配的优点是可控、可解释缺点是覆盖不了复杂问法。比如“我得了糖尿病平时要注意什么吃什么药好”这里同时有症状咨询和药品咨询模板就处理不了。实际项目里往往是模板加模型兜底简单问题走模板复杂问题走模型生成。nl2cypher生成的语句一定要做参数化处理别直接拼字符串。上面用 f-string 拼{disease}是为了演示生产环境应该用参数query MATCH (d:Disease {name:$name})-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name graph.run(query, namedisease)这样能防止 Cypher 注入也能利用 Neo4j 的查询缓存。3.3 RAG 检索增强生成的完整链路RAG 的核心是“先检索再生成”。检索部分从 Neo4j 拿到结构化知识生成部分把知识塞进大模型的 prompt 里让模型基于事实回答。def rag_qa(user_question): # 第一步NER 抽实体 entities ner_model.predict(user_question) # 第二步意图识别 intent intent_model.predict(user_question) # 第三步生成 Cypher 并查询图谱 cypher nl2cypher(intent, entities) if not cypher: return 抱歉我暂时无法理解这个问题。 results graph.run(cypher).data() # 第四步把检索结果拼成上下文 context \n.join([str(r) for r in results]) # 第五步构造 prompt 让大模型生成回答 prompt f你是一个医疗助手。请根据以下知识回答用户问题不要编造信息。 知识 {context} 用户问题{user_question} 回答 answer llm.generate(prompt) return answer这个链路里context的质量直接决定回答质量。如果图谱里查不到相关实体context为空大模型就会开始自由发挥这时候最好直接返回“知识库中未找到相关信息”而不是让模型硬答。webui.py是界面入口login.py和user_credentials.json处理用户登录。user_data_storage.py负责对话历史存储。整个系统跑起来的顺序是先启动 Neo4j再跑build_up_graph.py建图然后启动webui.py浏览器访问对应端口。4. 避坑与排查跑这套系统时最容易翻车的五个地方4.1 Neo4j 连接超时或认证失败现象跑build_up_graph.py时报ServiceUnavailable或AuthError。原因Neo4j 服务没启动或者密码跟代码里写的不一致。还有一种情况是 Bolt 端口被防火墙拦了。解决先确认 Neo4j 服务状态neo4j status看是否 running。然后检查代码里的auth参数密码是不是你首次登录时改过的那个。如果忘了密码删掉data/dbms/auth文件重启可以重置。端口不通的话确认 7687 和 7474 都放行了。4.2 BERT 模型加载报维度不匹配现象ner_model.py加载权重时抛RuntimeError: size mismatch for classifier.weight。原因tag2idx.npy里的标签数量和模型配置的num_labels对不上。可能是你换了数据集标签体系变了但没重新生成tag2idx。解决先加载tag2idx.npy看实际标签数然后确保BertForTokenClassification.from_pretrained(..., num_labelslen(tag2idx))里的数字一致。如果标签确实变了需要重新训练分类头不能直接加载旧权重。4.3 大模型显存溢出现象加载 34b 模型时CUDA out of memory。原因34b 参数量的模型全精度加载需要 60G 以上显存单卡根本放不下。解决用 8bit 或 4bit 量化加载load_in_8bitTrue或load_in_4bitTrue。如果还不行用device_mapauto让 accelerate 自动分片到多卡。再不行就换小模型7b 或 13b 在医疗问答场景下配合 RAG 也能用别死磕 34b。4.4 nl2cypher 生成的查询返回空结果现象图谱里明明有数据但查询返回空列表。原因实体名没对齐。用户输入“糖尿病”图谱里存的是“2型糖尿病”NER 抽出来的实体跟图谱节点属性值不完全匹配。解决在查询前做实体归一化用模糊匹配或别名表。比如MATCH (d:Disease) WHERE d.name CONTAINS $name用CONTAINS代替精确匹配。但这样会慢数据量大时建议建全文索引。4.5 WebUI 启动后页面空白或接口 500现象webui.py跑起来了但浏览器打开是空白或者提交问题后报 500。原因前端静态文件路径不对或者后端某个模型没加载成功但没抛异常。解决看终端日志500 错误一般有 traceback。如果是静态文件 404检查webui.py里static_folder的路径。如果是模型加载失败确认model目录下的权重文件完整没有下载中断产生的.tmp文件。5. 进阶技巧用 LangChain 串起 RAG 链路并做效果验证项目里langchainchatglm.png这张图暗示了 LangChain 的集成可能。如果你想把检索、意图识别、生成串成更工程化的链路LangChain 的RetrievalQA和GraphCypherQAChain是现成的轮子。from langchain.graphs import Neo4jGraph from langchain.chains import GraphCypherQAChain from langchain.llms import HuggingFacePipeline # 连接 Neo4j graph Neo4jGraph(urlbolt://localhost:7687, usernameneo4j, password你的密码) # 包装本地大模型 llm HuggingFacePipeline.from_model_id( model_idyour-model-path, tasktext-generation, model_kwargs{temperature: 0.1, max_length: 512} ) # 构建图谱问答链 chain GraphCypherQAChain.from_llm( llmllm, graphgraph, verboseTrue, return_intermediate_stepsTrue # 能看到生成的 Cypher ) result chain.run(糖尿病有哪些症状) print(result)return_intermediate_stepsTrue是关键它能打印出模型生成的 Cypher 语句。我一般会拿这个输出来做验证如果 Cypher 写错了说明 nl2cypher 模块有问题如果 Cypher 对了但答案不对说明生成环节的 prompt 需要调。效果验证我习惯用questions.csv里的问题做批量测试对比检索到的知识条目和最终回答是否一致。准确率低于 80% 的话优先查 NER 的实体抽取准确率实体抽错了后面全错。从那以后我每次跑这类 RAG 项目都强制先单独验证 NER 和图谱查询这两步确认检索链路通了再接大模型。不然生成环节一出问题你根本分不清是检索错了还是模型编了。希望帮到你。本文还有配套的精品资源点击获取