简介:本资源是一套基于RAG与大模型技术的医疗问答系统完整项目工程,面向计算机相关专业的毕业设计、课程设计、大作业、工程实训及学科竞赛参赛者。项目利用DiseaseKG数据集与Neo4j构建医疗知识图谱,结合BERT命名实体识别与34b大模型意图识别,通过精确知识检索与问答生成提升医疗咨询性能,着力解决大模型在医疗领域应用的可靠性问题。压缩包共75个文件,约84.65MB,包含Python源码、Jupyter Notebook实验记录、JSON与CSV数据集、YAML配置、Markdown说明文档及PNG界面截图等,覆盖知识图谱构建、模型微调、NER训练与Web交互等模块。已有157人学习关注。项目代码经过测试运行,功能完整,可实现复现复刻,答辩评审平均分达96分,设计报告亦可借鉴,并支持在此基础上扩展新功能,适合作为学习练手与项目立项的优质参考。
1. 医疗问答系统为什么非要 RAG 加知识图谱:一份能跑通的工程包拆解
医疗问答这个场景,纯靠大模型硬答是行不通的。模型会一本正经地编出并不存在的药物剂量和诊疗建议,这在通用闲聊里顶多算尴尬,放到医疗咨询里就是事故。这份工程包给出的解法是 RAG 检索增强加知识图谱双管齐下:用 DiseaseKG 数据集在 Neo4j 里搭起疾病、症状、药品、检查项目之间的实体关系网,再用 BERT 做命名实体识别把用户口语里的症状词抽出来,用 34b 大模型判断意图,最后把检索到的结构化知识拼进提示词交给大模型生成回答。整套流程解决的核心问题就是让模型的每一句回答都有图谱里的边和节点兜底,而不是凭空生成。它适合正在做毕设、课设、大作业或者竞赛项目的同学,也适合想搞明白 RAG 在垂直领域怎么落地、知识图谱怎么和 LLM 配合的开发者。包里带了源码、工程文件、说明文档和一堆中间数据,拿到手能复现出一样的系统。
2. 从 medical.json 到 Neo4j 图谱:数据清洗与 build_up_graph.py 落地
2.1 先搞清楚数据长什么样再动手
拿到包先别急着跑代码,打开data目录看一眼medical.json。这是整个系统的知识底座,里面存的是疾病条目,每条包含疾病名称、所属科室、症状列表、常用药品、检查项目、治疗周期这些字段。原始数据是嵌套 JSON,字段命名不统一,有的症状用逗号分隔的字符串,有的直接是数组,还有大量重复和空值。processjson.py干的就是把这些脏数据洗成规整的三元组。
我一般会先跑一遍统计,看看有多少条疾病、多少种症状、多少种药品,心里有个数。命令很简单:
python processjson.py --input data/medical.json --output data/medical_clean.json这个脚本的核心逻辑是遍历每条疾病记录,把症状、药品、检查项分别抽出来,和疾病本身建立(疾病)-[有症状]->(症状)、(疾病)-[常用药]->(药品)、(疾病)-[需检查]->(检查)这样的关系。参数--input指定原始文件,--output指定清洗后的输出。跑完之后你会得到一个扁平化的 JSON,每个疾病对应一组标准化的关系列表。
提示:清洗前先备份
medical.json,脚本默认是覆盖式写入,改错了原始数据就没了。
2.2 build_up_graph.py 怎么把三元组灌进 Neo4j
图谱构建脚本是build_up_graph.py,它依赖 Neo4j 的 Python 驱动。跑之前确认两件事:Neo4j 服务已经起来,configs目录里的连接配置和你的实际环境对得上。常见做法是在configs下放一个neo4j_config.json,里面写 URI、用户名、密码。
# build_up_graph.py 核心片段 from neo4j import GraphDatabase import json driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "your_password")) def create_relation(tx, disease, symptom): tx.run("MERGE (d:Disease {name: $disease}) " "MERGE (s:Symptom {name: $symptom}) " "MERGE (d)-[:HAS_SYMPTOM]->(s)", disease=disease, symptom=symptom) with driver.session() as session: data = json.load(open("data/medical_clean.json", encoding="utf-8")) for item in data: for sym in item.get("symptoms", []): session.execute_write(create_relation, item["name"], sym)这里用MERGE而不是CREATE是关键。MERGE会先查再建,避免同一个症状节点被重复创建成几十个同名副本。参数上,bolt://localhost:7687是 Neo4j 默认的 Bolt 协议端口,用户名密码换成你自己设的。如果你的 Neo4j 是 3.5 版本,驱动版本要对应降级,否则会报协议不兼容。
灌完数据后去 Neo4j Browser 里跑一句MATCH (n) RETURN count(n)验证节点总数,再跑MATCH ()-[r]->() RETURN count(r)看关系数。正常情况下疾病节点几百到上千,症状和药品节点会更多。如果数字明显偏少,八成是 JSON 里某个字段名和脚本里取的不一致,回去对一下processjson.py的输出结构。
2.3 图谱 schema 设计里容易忽略的取舍
很多人搭图谱喜欢把所有东西都塞成节点,结果查询时 JOIN 一大堆。这份工程包的做法是只把疾病、症状、药品、检查项建成节点,科室、治疗周期这类属性直接挂在疾病节点上当 property。这样查询「感冒有哪些症状」只需要一跳,性能好很多。
另一个取舍是关系方向。(疾病)-[:HAS_SYMPTOM]->(症状)是有向的,但实际检索时经常需要反向查「这个症状可能是什么病」。Neo4j 里反向查询不用建反向边,直接MATCH (s:Symptom {name:"发热"})<-[:HAS_SYMPTOM]-(d:Disease) RETURN d.name就行,方向在建图时保持语义清晰即可。
3. BERT 命名实体识别与 34b 大模型意图识别:两条链路怎么接
3.1 BERT 做 NER 的微调与推理
用户输入「我最近老咳嗽还发烧,吃什么药好」,系统得先把「咳嗽」「发烧」这两个症状词抽出来,这就是 NER 的活。包里ner_model.py和finetune_hf.py是配套的,前者定义模型结构,后者负责微调。训练数据在ner_data.py和ner_data_aug.txt里,格式是 BIO 标注。
微调命令大致长这样:
python finetune_hf.py \ --model_name bert-base-chinese \ --train_file data/ner_data_aug.txt \ --output_dir model/ner_finetuned \ --num_train_epochs 5 \ --learning_rate 2e-5 \ --batch_size 16bert-base-chinese是基座,医疗领域如果数据量够可以换成医疗预训练模型。learning_rate设 2e-5 是 BERT 微调的常规起点,太大容易把预训练学到的语言知识冲掉,太小收敛慢。num_train_epochs设 5 是因为 NER 数据量通常不大,跑太多会过拟合。训练完模型存在model/ner_finetuned,推理时ner_model.py加载这个目录做实体抽取。
推理阶段输入一句话,输出是每个 token 的标签序列,再把标签拼回实体。比如「咳嗽」被标成 B-SYM、I-SYM,就抽出来作为一个症状实体。这里有个坑:BERT 的 tokenizer 会把中文按字切,实体边界靠标签对齐,如果预处理时没做好 offset 映射,抽出来的实体可能多一个字少一个字。
3.2 34b 大模型意图识别的接入方式
意图识别决定用户这句话是要问症状、问药品、问治疗还是闲聊。包里用的是 34b 参数的大模型,通过 API 或者本地部署调用。webui.py里能看到调用逻辑,核心是把用户输入和一组预定义的意图标签一起塞进 prompt,让模型输出意图类别。
# 意图识别调用示意 import requests def detect_intent(user_input): prompt = f"判断以下医疗问题的意图,只输出类别:症状咨询/药品咨询/治疗咨询/其他。\n问题:{user_input}" resp = requests.post("http://localhost:8000/v1/chat/completions", json={ "model": "34b-model", "messages": [{"role": "user", "content": prompt}], "temperature": 0.1 }) return resp.json()["choices"][0]["message"]["content"].strip()temperature设 0.1 是为了让输出稳定,意图分类不需要创造性。接口地址和模型名换成你实际部署的。34b 模型本地跑需要不小的显存,常见做法是量化后部署,或者直接用兼容 OpenAI 接口的推理服务。
意图识别和 NER 是并行的两条链路:NER 抽实体,意图识别定方向。两者结果一起送给检索模块,决定去图谱里查什么。
3.3 两条链路的结果怎么合并成检索 query
假设 NER 抽出「咳嗽」「发烧」,意图识别判定为「症状咨询」,检索模块就构造 Cypher 查询去图谱里找同时有这两个症状的疾病。如果意图是「药品咨询」,就换成查这些疾病对应的常用药。合并逻辑在user_data_storage.py和webui.py的交互层里。
这里的设计要点是:实体和意图不是简单拼接,而是意图决定查询模板,实体填充模板参数。这样即使 NER 抽出的实体有噪声,意图对了至少能保证查询方向不跑偏。
4. RAG 检索与生成:LangChain 链路里的避坑与排查
4.1 检索环节的 query 构造与图谱查询
RAG 的检索质量直接决定生成质量。这份工程包的检索不是简单的向量相似度,而是图谱结构化查询加文本检索的混合。图谱查询走 Neo4j,文本检索部分用 LangChain 的 retriever 接口。langchain+chatglm.png这张图展示的就是这条链路。
图谱查询的 Cypher 模板大致是:
def build_cypher(intent, entities): if intent == "症状咨询": return f"MATCH (d:Disease)-[:HAS_SYMPTOM]->(s:Symptom) WHERE s.name IN {entities} RETURN d.name, d.department" elif intent == "药品咨询": return f"MATCH (d:Disease)-[:HAS_SYMPTOM]->(s:Symptom), (d)-[:USES_DRUG]->(drug) WHERE s.name IN {entities} RETURN d.name, drug.name"entities是 NER 抽出的实体列表。注意 Cypher 里IN的用法,实体多的时候查询会变慢,可以加索引:CREATE INDEX FOR (s:Symptom) ON (s.name)。
4.2 把检索结果拼进 prompt 的模板设计
检索回来的结构化数据要转成自然语言才能喂给大模型。模板设计的原则是:把图谱里的关系用完整句子表述,减少模型的理解负担。
def format_context(diseases): lines = [] for d in diseases: lines.append(f"疾病:{d['name']},所属科室:{d['department']}," f"常见症状:{'、'.join(d['symptoms'])}," f"常用药品:{'、'.join(d['drugs'])}") return "\n".join(lines) prompt = f"根据以下医学知识回答问题,不要编造信息。\n知识:\n{format_context(results)}\n问题:{user_input}"prompt 里明确写「不要编造信息」是有用的,能显著降低模型自由发挥的概率。但光靠这句话不够,关键还是检索结果本身要准。
4.3 生成阶段的参数与幻觉抑制
生成用的大模型和意图识别可以是同一个,也可以分开。inference_hf.py是本地推理入口。关键参数是temperature和top_p,医疗场景建议temperature=0.3以下,top_p=0.8,让输出偏保守。
幻觉抑制还有一招是后验校验:生成完回答后,把回答里的实体再抽一遍,看是否都能在检索结果里找到对应。找不到的就标记出来,或者直接触发重新检索。这个逻辑包里没有现成的,但ner_model.py可以直接复用做这件事。
5. 部署与复现常见问题排查:从环境到数据的五条血泪经验
5.1 Neo4j 连不上或版本不匹配
现象:跑build_up_graph.py报ServiceUnavailable或者ProtocolError。原因通常是 Neo4j 服务没启动,或者 Python 驱动版本和 Neo4j 服务端版本差太多。Neo4j 3.5 要用 1.7 版本的驱动,4.x 以上用 4.x 或 5.x 驱动。解决:先neo4j status确认服务在跑,再pip show neo4j看驱动版本,对不上就降级或升级。连接串bolt://localhost:7687里的端口也要和neo4j.conf里一致。
5.2 BERT 微调 loss 不降或者实体抽不准
现象:训练几个 epoch 后 loss 卡在 0.5 左右不动,推理时实体边界错乱。原因多半是标注数据格式和 tokenizer 对不齐。BERT 中文 tokenizer 按字切,如果你的标注是按词标的,需要先做字符级对齐。解决:检查ner_data.py里的tag2idx映射,确认标签数量和模型输出维度一致。另外学习率 2e-5 如果 loss 震荡,降到 1e-5 试试。
5.3 大模型接口超时或返回空
现象:意图识别偶尔返回空字符串,或者请求超时。原因是大模型推理服务负载高,或者 prompt 太长超出上下文限制。解决:给请求加超时和重试,temperature别设 0,设 0.1 避免采样到空。prompt 长度控制在模型上下文窗口的 70% 以内,超了就先截断检索结果。
5.4 图谱查询返回空结果
现象:明明图谱里有数据,Cypher 查出来是空的。原因通常是实体名不完全匹配,比如用户说「发烧」,图谱里存的是「发热」。解决:在 NER 后面加一层同义词映射,或者 Cypher 里用CONTAINS做模糊匹配。data目录下的rel_aug.txt就是做关系增强用的,可以拿来扩充同义词。
5.5 前端 webui 起不来或接口 404
现象:webui.py跑起来但页面打不开,或者登录接口报 404。原因是端口被占或者路由没注册。解决:看webui.py里app.run的端口参数,换成没被占用的。login.py和user_credentials.json是登录模块,确认这两个文件路径在启动目录下能找到。img目录里的图片是界面素材,路径错了页面会缺图但不影响功能。
6. 把系统跑稳之后:几个让 RAG 医疗问答更靠谱的进阶技巧
系统能跑通只是第一步,真正要拿去做答辩或者上线演示,还得在几个细节上打磨。第一个技巧是给图谱查询加缓存。同一批症状反复查图谱,Neo4j 压力不小,我一般会在检索层加一层内存缓存,key 用意图加实体列表的哈希,value 存查询结果,命中率能到六七成。第二个技巧是检索结果排序。图谱查出来的疾病可能有好几个,按症状匹配数量降序排,匹配度高的排前面,拼进 prompt 时也按这个顺序,模型会更倾向参考前面的内容。
第三个技巧是回答里强制带出处。生成的时候让模型在回答末尾附上参考的疾病名称,这样用户能自己判断可信度,答辩时也是加分项。实现方式是在 prompt 里加一句「回答末尾列出参考疾病」,然后解析输出。
| 优化点 | 做法 | 效果 |
|---|---|---|
| 查询缓存 | 意图+实体哈希做 key | 降低 Neo4j 负载,响应快 30% 左右 |
| 结果排序 | 按症状匹配数降序 | 生成回答更聚焦 |
| 强制出处 | prompt 要求列出参考疾病 | 可追溯,答辩加分 |
| 同义词映射 | 维护症状别名词典 | 减少查空 |
第四个技巧是定期用ner_model.py对生成回答做实体回抽校验,发现回答里的实体不在检索结果里就标记为可疑。这个校验不用实时做,离线批量跑就行,用来评估系统整体幻觉率。
最后说个我自己的习惯:每次改完图谱 schema 或者换了大模型版本,我都会拿一组固定的测试问题从头到尾跑一遍,对比回答变化。医疗场景经不起玄学,改动必须有回归验证。从那以后我每次动检索链路或者换模型,都强制走一遍这组回归问题,确认没有退化才继续。希望帮到你。
本文还有配套的精品资源,点击获取