ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

医疗问答系统实战:RAG+Neo4j+BERT融合检索与图谱推理

医疗问答系统实战:RAG+Neo4j+BERT融合检索与图谱推理 简介本资源为基于RAG与大模型技术的医疗问答系统完整项目工程面向计算机、人工智能相关专业的毕业设计、课程设计、大作业、工程实训及学科竞赛参赛者。项目利用DiseaseKG数据集与Neo4j构建知识图谱结合BERT命名实体识别与34b大模型意图识别通过精确知识检索与问答生成提升医疗咨询性能着力解决大模型在医疗领域应用的可靠性问题。压缩包共75个文件约84.65MB涵盖Python源码、Jupyter Notebook实验记录、JSON与CSV数据集、YAML配置、PNG/JPG界面截图及Markdown说明文档覆盖知识图谱构建、模型微调、NER训练与Web交互等模块。已有157人学习关注。项目代码经过测试运行功能完整可实现复现复刻设计报告亦可借鉴适合在此基础上扩展开发新功能遇到使用问题可联系作者获取解答与学习资料支持。1. 医疗问答系统为什么不能只靠大模型硬答做过通用问答的都知道把知识塞进提示词里让大模型直接回答在开放域闲聊里能糊弄过去但一进医疗场景就原形毕露。患者问「二甲双胍和格列齐特能不能一起吃」模型如果只靠预训练里那点模糊记忆很可能给出一个语气笃定、细节错误的答案。医疗问答的容错率极低答错一句可能影响用药判断所以这套系统的核心不是「让模型更会说」而是「让模型只根据可信资料说」。这就是 RAG检索增强生成在医疗问答里必须存在的理由。RAG 的思路是先把权威医学资料切块、向量化、存进知识库用户提问时先检索出最相关的若干片段再把这些片段作为上下文交给大模型组织答案。模型不再凭记忆作答而是「看着资料答题」可追溯、可更新、可审计。再叠加 Neo4j 这类图数据库做结构化知识疾病—症状—药物—禁忌关系以及 BERT 类模型做医学实体识别和意图分类整套系统就从「聊天机器人」升级成「有据可查的问答助手」。这套方案适合谁做毕设、课设、实训、大作业、竞赛的学生以及想在企业内部搭私有化医疗知识问答的工程师。它不需要你从头训练大模型核心工作量在数据清洗、检索链路调优和图谱构建上门槛可控、成果可展示。下面按「先跑通最小闭环再逐层加固」的顺序讲清楚。2. 把 RAG 医疗问答的最小闭环先跑通2.1 技术选型为什么是 RAG Neo4j BERT 三件套单用向量检索的 RAG 有个明显短板它对「关系型问题」不敏感。比如「对青霉素过敏的糖尿病患者能用哪些降糖药」向量检索可能召回一堆讲糖尿病的段落却漏掉「过敏禁忌」这条关键约束。Neo4j 存的是实体和关系能精确回答「A 和 B 之间有没有禁忌边」这类问题。BERT 则负责在检索前把用户口语化的提问归一化——「我血糖高吃啥药」要能映射到「高血糖 药物治疗」这样的标准表达同时识别出「血糖」「药」这些医学实体用于图谱查询。三者分工是BERT 做理解和实体抽取Neo4j 做结构化关系推理向量库做非结构化文本召回大模型做最终答案组织。常见做法是用 LangChain 或类似框架把这几段串起来避免自己造轮子。组件职责常见选型大模型答案生成本地部署开源模型或调用合规 API向量库文本片段召回FAISS / Chroma / Milvus图数据库实体关系推理Neo4j理解模型实体识别、意图分类BERT 医学微调版编排框架链路串联LangChain选型时要注意向量库和 Neo4j 不是二选一而是互补。竞赛或毕设里如果只做向量 RAG答辩时很容易被问「关系型问题怎么办」补上图谱这条线会稳很多。2.2 环境搭建与依赖安装先把基础环境跑起来。Python 建议 3.10 以上Neo4j 用 Desktop 版最省事向量库先用 FAISS 本地跑避免一上来就折腾分布式。# 创建虚拟环境 python -m venv medrag source medrag/bin/activate # Windows 用 medrag\Scripts\activate # 核心依赖 pip install langchain langchain-community faiss-cpu pip install neo4j sentence-transformers transformers pip install fastapi uvicorn # 后面做接口用Neo4j Desktop 装好后新建一个本地数据库记下 bolt 地址一般是bolt://localhost:7687和密码。启动数据库后浏览器打开 Neo4j Browser能连上就说明图库就绪。提示Neo4j 版本差异会影响 Cypher 语法和驱动兼容性装之前先确认你的驱动版本和数据库版本匹配否则连接阶段就会报协议错误。2.3 医疗语料切块与向量化入库医疗文本切块和通用文本不一样。药品说明书、诊疗指南里一个「用法用量」段落被从中间切断检索出来就是废的。所以切块要按语义边界走而不是死磕固定字数。from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import FAISS from langchain_community.embeddings import HuggingFaceEmbeddings # 用中文医学语料微调过的 embedding 效果更好这里先用通用中文模型起步 embedding HuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5) splitter RecursiveCharacterTextSplitter( chunk_size500, # 医疗片段信息密度高500 字左右较稳 chunk_overlap80, # 重叠防止边界语义丢失 separators[\n\n, \n, 。, , ] # 中文标点优先切 ) docs splitter.split_text(raw_medical_text) vector_store FAISS.from_texts(docs, embedding) vector_store.save_local(med_faiss_index)chunk_size是关键参数太小则单块信息不完整太大则检索精度下降、噪声变多。医疗场景我一般从 400 到 600 之间试用一批真实问题测召回率再定。chunk_overlap给 60 到 100保证跨块的句子不被割裂。separators里把中文句号、分号放前面能让切块尽量落在完整句子上。2.4 用 Neo4j 建医学实体关系图谱图谱的价值在于回答「关系」。先定义节点和边疾病、症状、药物、检查、禁忌。用 Cypher 建几个示例节点和关系感受一下查询方式。// 建疾病和药物节点 CREATE (d:Disease {name: 2型糖尿病}) CREATE (m1:Drug {name: 二甲双胍}) CREATE (m2:Drug {name: 格列齐特}) // 建治疗关系 CREATE (m1)-[:TREATS]-(d) CREATE (m2)-[:TREATS]-(d) // 建禁忌关系 CREATE (m1)-[:CONTRAINDICATED_WITH {reason: 严重肾功能不全}]-(d) // 查询治疗2型糖尿病且无特定禁忌的药 MATCH (drug:Drug)-[:TREATS]-(d:Disease {name: 2型糖尿病}) WHERE NOT (drug)-[:CONTRAINDICATED_WITH]-(d) RETURN drug.name节点属性里name要统一命名规范否则「2型糖尿病」和「II型糖尿病」会变成两个节点查询直接漏结果。实际项目里图谱数据一般从公开医学知识库或结构化表格导入用LOAD CSV批量建边比手写 Cypher 高效得多。2.5 检索链路向量召回 图谱查询的融合真正好用的检索不是二选一而是先并行跑两条路再合并结果喂给大模型。def hybrid_retrieve(question, vector_store, neo4j_driver, top_k4): # 路径一向量召回非结构化片段 vec_docs vector_store.similarity_search(question, ktop_k) vec_context \n.join([d.page_content for d in vec_docs]) # 路径二图谱查询结构化关系实体需先用BERT抽取这里简化 with neo4j_driver.session() as session: result session.run( MATCH (d:Disease)-[r]-(n) WHERE d.name CONTAINS $kw RETURN d.name, type(r), n.name LIMIT 5, kwextract_keyword(question) ) graph_context \n.join([str(record) for record in result]) return vec_context \n【图谱关系】\n graph_contexttop_k控制召回片段数给 3 到 5 比较合适太多会稀释关键信息、拉长上下文。图谱查询里的关键词抽取就是 BERT 的用武之地——用医学 NER 模型把问题里的疾病、药物实体抠出来再拿去查图比整句模糊匹配准得多。两条路的上下文拼在一起交给大模型答案既有原文依据又有关系约束。3. BERT 在医疗问答里到底干什么活3.1 实体识别把口语提问翻译成图谱能懂的词用户不会说「2型糖尿病」他说「我爸血糖老高吃药也不管用」。BERT 微调后的医学 NER 模型要做的是从这句话里抽出「血糖高」这个症状实体映射到标准术语「高血糖」再去图谱里找关联。没有这一步图谱查询基本查不出东西。from transformers import pipeline # 加载医学NER模型需替换为你微调或选用的模型 ner pipeline(ner, modelyour-medical-bert-ner, aggregation_strategysimple) text 我爸血糖老高还查出有糖尿病 entities ner(text) # 输出形如 [{entity_group: SYMPTOM, word: 血糖高}, ...]aggregation_strategysimple会把被拆散的子词合并成完整实体否则 BERT 的 tokenizer 会把「糖尿病」切成好几个片段。实体识别准不准直接决定图谱那条路能不能走通这是整条链路里最值得花时间调的一环。3.2 意图分类先判断用户想问什么医疗问题分几类问症状、问用药、问检查、问禁忌、问预后。意图不同检索策略也不同。问用药就该优先查图谱的药物关系问症状就该优先向量召回科普段落。用一个 BERT 分类头做意图判断成本低、收益明显。from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch tokenizer AutoTokenizer.from_pretrained(your-intent-bert) model AutoModelForSequenceClassification.from_pretrained(your-intent-bert) def classify_intent(question): inputs tokenizer(question, return_tensorspt, truncationTrue) with torch.no_grad(): logits model(**inputs).logits return torch.argmax(logits, dim1).item() # 映射到意图标签意图标签体系要提前定好一般 5 到 8 类够用。标签太细会导致每类样本不足、训练不收敛太粗又起不到分流作用。这个分类模型不需要很大BERT-base 级别在几千条标注数据上就能有不错效果。3.3 什么时候该微调什么时候用现成的就够不是所有环节都要微调。向量检索用的 embedding 模型如果通用中文模型召回效果还行就不必急着微调但医学 NER 和意图分类通用模型对医学术语的识别率往往不够微调收益很大。微调数据可以来自公开医学标注集也可以自己从问答日志里人工标几百条起步。注意微调 BERT 时学习率要给小通常 2e-5 到 5e-5轮数 3 到 5 轮就够医疗标注数据量小训多了直接过拟合验证集掉点就是信号。4. 避坑与排查这套系统最容易翻车的地方4.1 检索召回一堆相关但不解决问题的片段现象用户问具体用药禁忌检索出来的全是疾病科普答案答非所问。原因切块太粗一个块里混了多个主题向量被平均掉了。解决把chunk_size调小到 300 到 400并按语义段落切同时给每个块加上来源标题作为元数据检索时按元数据过滤。4.2 图谱查询返回空结果现象Cypher 查询明明写了却一条记录都不返回。原因实体命名不统一用户输入「糖尿病」而图谱里存的是「2型糖尿病」CONTAINS匹配不上。解决建图谱时统一术语表查询前先用 BERT 做实体归一化把口语词映射到标准词再查。4.3 大模型无视检索内容自己编现象检索片段里明明没有某个药模型还是把它写进答案。原因提示词没约束住模型倾向用预训练记忆补全。解决在系统提示里明确写「只允许根据提供的资料回答资料中没有的信息必须回答不知道」并在后处理阶段做一次答案与检索片段的比对校验。4.4 上下文太长导致响应慢、成本高现象召回片段一多提示词几千字推理变慢。原因top_k给太大或片段没做去重。解决top_k控制在 3 到 5召回后做相似度阈值过滤低于阈值的片段直接丢图谱上下文只保留命中的关系不要把整个子图塞进去。4.5 中文医学分词和标点处理不当现象切块把「每日三次饭后服用」切成「每日三次」和「饭后服用」两块检索时只召回半句。原因分隔符顺序没把中文标点放前面。解决separators里中文句号、分号、逗号按优先级排好并适当加大chunk_overlap兜底。5. 让答案可追溯引用标注与效果验证的实操技巧医疗问答和普通问答最大的区别是答案必须能指回原文。用户看到「二甲双胍可能引起胃肠道反应」得能点开看到这句话出自哪份说明书。实现上不难但很能体现项目完整度。做法是在向量入库时给每个片段带上来源元数据文档名、章节、页码检索返回时把元数据一起带出来生成答案后让模型在句末标注引用编号。# 入库时附加元数据 vector_store FAISS.from_texts( docs, embedding, metadatas[{source: fdoc_{i}, section: sec} for i, sec in enumerate(sections)] ) # 检索时保留元数据 results vector_store.similarity_search_with_score(question, k4) for doc, score in results: print(doc.metadata[source], score, doc.page_content[:50])similarity_search_with_score返回的分数能帮你设阈值——分数越低越不相关低于某个值就别往上下文里塞了。这个阈值没有通用值得拿一批真实问题跑一遍看分数分布再定。效果验证别只看「答得像不像」要建一个小评测集准备 50 到 100 条带标准答案的问题分三类——事实型某药适应症、关系型两药能否同服、开放型某病日常注意。分别统计检索命中率和答案正确率。检索命中率低问题在切块和 embedding检索命中但答案错问题在提示词和模型。这样一拆调优方向就清楚了。我自己的习惯是每改一次切块参数或提示词都重跑一遍评测集把命中率和正确率记在表格里别凭感觉说「好像变好了」。医疗场景里感觉最不靠谱数据才靠谱。这套 RAG 图谱 BERT 的组合跑通最小闭环大概两三天真正拉开差距的是检索调优和评测这一环值得多花时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表