医疗问答系统实战:Neo4j知识图谱+BERT向量检索+RAG
2026/9/23 15:07:05 网站建设 项目流程

简介:本资源为基于RAG与大模型技术的医疗问答系统完整项目工程,面向计算机、人工智能相关专业的毕业设计、课程设计、大作业、工程实训及学科竞赛参赛者。项目利用DiseaseKG数据集与Neo4j构建知识图谱,结合BERT命名实体识别与34b大模型意图识别,通过精确知识检索与问答生成提升医疗咨询性能,着力解决大模型在医疗领域应用的可靠性问题。压缩包共75个文件,约84.65MB,涵盖Python源码、Jupyter Notebook实验记录、JSON与CSV数据集、YAML配置、Markdown说明文档及PNG/JPG界面截图等,覆盖知识图谱构建、模型微调、NER训练与Web交互等模块。已有157人学习下载。项目代码经过测试运行,功能完整,可实现复现复刻,设计报告亦可借鉴,适合在此基础上扩展开发新功能,遇到使用问题可联系作者获得解答与学习资料支持。

1. 医疗问答系统为什么不能只靠大模型硬答:从一次答错药名说起

患者问“二甲双胍能不能和碘造影剂一起用”,通用大模型很可能给出一个语气笃定、细节含糊的答案。问题不在模型不够大,而在它没有把“医院内部用药规范”和“最新说明书”当成事实来源。医疗问答系统真正要解决的是可溯源、可更新、可约束:答案必须能指回某段指南或某条药品说明,知识库更新后不用重新训练模型,遇到超纲问题要敢说“不确定”。这正是 RAG(检索增强生成)加知识图谱的价值所在。这套方案适合做毕设、课设、实训或竞赛的同学:用 Neo4j 存结构化医学关系,用 BERT 做语义向量,用大模型做最终生成,把“背答案”变成“查资料再回答”。下面按我实际搭过的一版流程拆开讲,重点放在能复现的步骤和容易翻车的地方。

2. 把医疗知识拆成 Neo4j 图谱和向量库:两条腿走路的检索架构

2.1 为什么医疗问答需要图谱检索和向量检索并存

纯向量检索擅长“语义相似”,比如把“心梗”和“心肌梗死”匹配上,但它不擅长多跳推理。患者问“服用华法林期间不能吃哪些抗生素”,向量库可能召回一堆含“华法林”的段落,却漏掉“华法林—相互作用—抗生素”这条关系链。Neo4j 这类图数据库正好补上:节点是疾病、药品、症状、检查项,边是“治疗”“禁忌”“表现为”“相互作用”。常见做法是双路召回——向量路负责模糊语义,图谱路负责精确关系,最后合并去重再交给大模型。

选型上,BERT 系列做中文医疗语义编码是稳妥的,因为公开的中文医疗语料上它有成熟权重可用;大模型侧本地部署可选 Ollama 拉取量化模型,竞赛或课设环境用免费 API 也能跑通。这里不展开具体模型榜单,只强调一点:生成模型只负责组织语言,事实必须来自检索结果,否则系统就退化成普通聊天机器人。

2.2 用 Neo4j 建医疗知识图谱的最小步骤

先装 Neo4j。社区版够用,Windows 用 Neo4j Desktop,Linux 用 tar 包或 apt 源。装完确认 7474 和 7687 端口通,浏览器打开http://localhost:7474能进 Bloom 界面即可。常见坑是远程访问被拒,需要在neo4j.conf里把dbms.default_listen_address设为0.0.0.0并配置连接白名单,否则只能本机连。

建图用 Cypher。下面这段把“疾病—症状—药品”三类节点和关系写进去,可直接在 Neo4j Browser 里执行:

// 创建疾病节点 MERGE (d:Disease {name: '2型糖尿病'}) SET d.desc = '一种以高血糖为特征的代谢性疾病' // 创建症状节点并建立关系 MERGE (s:Symptom {name: '多饮'}) MERGE (d)-[:HAS_SYMPTOM]->(s) // 创建药品节点并建立治疗关系 MERGE (m:Drug {name: '二甲双胍'}) MERGE (d)-[:TREATED_BY]->(m) // 建立药品相互作用关系 MERGE (m2:Drug {name: '碘造影剂'}) MERGE (m)-[:INTERACTS_WITH {level: '慎用', note: '可能增加乳酸酸中毒风险'}]->(m2)

逻辑说明:MERGE保证重复执行不会产生重复节点,适合批量导入时反复跑。SET补属性,关系上的levelnote是后面生成答案时要引用的证据字段。参数上,节点名建议统一用标准医学术语,别混用俗称,否则图谱查询会漏。批量导入几万条时不要一条条MERGE,用LOAD CSVapoc.periodic.iterate,否则会慢到怀疑人生。

2.3 用 BERT 把医学文本切块并写入向量库

图谱管关系,向量库管长文本。把药品说明书、诊疗指南按 300 到 500 字切块,重叠 50 字,用 BERT 或同类中文编码模型转成向量存进 FAISS 或 Chroma。切块太大,检索精度掉;太小,上下文断裂。我一般按段落边界切,标题和正文不拆开。

from transformers import AutoTokenizer, AutoModel import torch, faiss, numpy as np tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") model = AutoModel.from_pretrained("bert-base-chinese") model.eval() def embed(texts): inputs = tokenizer(texts, padding=True, truncation=True, max_length=512, return_tensors="pt") with torch.no_grad(): out = model(**inputs) # 取 CLS 向量并归一化,方便用内积算余弦相似度 vecs = out.last_hidden_state[:, 0, :].numpy() return vecs / np.linalg.norm(vecs, axis=1, keepdims=True) chunks = ["二甲双胍常见不良反应包括腹泻、恶心……", "碘造影剂使用前应评估肾功能……"] vecs = embed(chunks).astype("float32") index = faiss.IndexFlatIP(vecs.shape[1]) index.add(vecs) faiss.write_index(index, "medical.index")

逻辑说明:CLS向量是 BERT 对整句的聚合表示,归一化后用内积等价于余弦相似度。max_length=512是 BERT 上限,超长文本必须先切块。参数上,truncation=True防止报错,但会截断,所以切块要控制在 500 字以内。检索时把用户问题同样编码,取 top-k(一般 3 到 5)作为候选证据。

3. 把检索结果喂给大模型:提示词、引用和拒答怎么落地

3.1 双路召回合并与重排的工程做法

用户问题进来后,先并行跑两路:向量库取 top-5 段落,Neo4j 用 Cypher 查相关实体和关系。图谱查询可以先用一个轻量实体识别把问题里的疾病、药品抽出来,再拼 Cypher。比如识别到“二甲双胍”和“碘造影剂”,就查两者之间有没有INTERACTS_WITH边。两路结果合并后按来源打分:图谱关系权重高,向量段落权重稍低,去重后取前 6 条作为上下文。

重排这一步很多人省掉,结果大模型被无关段落带偏。常见做法是用一个交叉编码器对“问题—段落”逐对打分,或者简单点用关键词覆盖率加语义分加权。竞赛里时间紧,我一般先用规则重排,把含问题实体最多的段落提到前面,效果已经够用。

3.2 提示词模板与引用格式的约束

提示词要写死三件事:只依据给定资料回答、每个结论后标注来源编号、资料不足时明确说“现有资料无法回答”。模板如下:

PROMPT = """你是一名严谨的医疗问答助手。请仅根据以下资料回答问题, 不要使用资料之外的知识。每个结论后用[编号]标注来源。 若资料不足以回答,请直接说明“现有资料无法回答该问题”。 资料: {context} 问题:{question} 回答:"""

逻辑说明:{context}里每条资料前加[1] [2]编号,生成时模型会自然引用。参数上,温度调到 0.1 到 0.3,降低发挥;max_tokens控制在 500 以内,避免冗长。拒答机制是医疗场景的底线,宁可少答也不能编。实测中,明确写“不要使用资料之外的知识”比只写“请根据资料”有效得多,这是血泪经验。

3.3 用 Neo4j 查询做答案校验的兜底逻辑

生成完答案后,可以再做一层校验:把答案里提到的药品关系回查 Neo4j,看是否与图谱一致。比如答案说“二甲双胍与碘造影剂可安全联用”,但图谱里是“慎用”,就触发人工复核标记。这一步不追求全自动,而是给系统一个黑匣子记录,方便排查。

// 校验:查询两药之间是否存在相互作用 MATCH (a:Drug {name: '二甲双胍'})-[r:INTERACTS_WITH]->(b:Drug {name: '碘造影剂'}) RETURN r.level, r.note

逻辑说明:这条查询返回关系属性和说明,直接和生成答案比对。参数上,节点名要和建图时完全一致,大小写、空格都敏感。如果查不到,说明图谱没覆盖,应记录为知识盲区,后续补数据。

4. 医疗问答系统避坑排查:从连不上 Neo4j 到答非所问

4.1 现象:Neo4j 本机能连,远程访问被拒

原因:社区版默认只监听 localhost,且防火墙未放行 7687。解决:改neo4j.confdbms.default_listen_address=0.0.0.0,重启服务,再确认服务器安全组放行 7474 和 7687。注意不要图省事关防火墙,按端口放行即可。

4.2 现象:向量检索召回的全是无关段落

原因:切块太大导致一个块里混了多个主题,或者编码模型没针对医疗语料微调。解决:把块缩到 300 字左右并按段落切;如果条件允许,用医疗问答对做一轮对比学习微调编码模型。临时方案是提高 top-k 再用重排筛。

4.3 现象:大模型回答里出现资料中没有的药名

原因:提示词约束不够,或温度太高。解决:温度降到 0.1,提示词里加“禁止编造资料外的药品名”,并在后处理里用药品词典扫描答案,发现未在上下文出现的药名就拦截重生成。

4.4 现象:图谱查询返回空,但数据明明导入了

原因:节点标签或属性名大小写不一致,或者导入时用了不同名称。解决:先用MATCH (n) RETURN labels(n), count(*)看实际标签,再对齐查询语句。导入前统一做名称标准化,别混用“2型糖尿病”和“二型糖尿病”。

4.5 现象:系统响应慢,一问要等十几秒

原因:向量检索和 Neo4j 查询串行执行,加上大模型生成本身耗时。解决:两路检索改并行;向量索引用 IVF 加速;大模型侧用流式输出,让用户先看到部分答案。竞赛演示时,提前把常见问题缓存,能明显改善体验。

5. 让医疗问答系统更稳的进阶技巧:混合检索权重调优与评测集自建

系统能跑通只是起点,真正拉开差距的是检索质量。我习惯先建一个 50 到 100 条的小评测集,每条包含问题、标准答案要点、应召回的资料来源。没有评测集,调参就是玄学。评测指标看两个:召回率(正确资料有没有被检索到)和忠实度(答案是否只用了检索资料)。召回率低就调切块和 top-k,忠实度低就调提示词和温度。

混合检索的权重可以这样调:给图谱关系结果和向量段落结果各设一个权重,初始 0.6 比 0.4,然后在评测集上网格搜索。下面是一个简单的加权合并示例:

def merge_results(graph_hits, vector_hits, w_graph=0.6, w_vec=0.4): scored = {} for rank, item in enumerate(graph_hits): scored[item["id"]] = scored.get(item["id"], 0) + w_graph / (rank + 1) for rank, item in enumerate(vector_hits): scored[item["id"]] = scored.get(item["id"], 0) + w_vec / (rank + 1) # 按加权分排序,取前 6 条 return sorted(scored.items(), key=lambda x: -x[1])[:6]

逻辑说明:用倒数排名加权,排名越靠前贡献越大。w_graphw_vec是唯一要调的两个参数,评测集上跑几轮就能找到较优值。注意别在测试集上反复调,留一部分做最终验证。

另一个技巧是把用户反馈接进来:每次回答后让用户点“有用/没用”,没用的记录进待优化池,定期人工看是检索漏了还是生成偏了。这个习惯坚持下来,系统会越用越准。我自己踩过最大的坑是早期只看生成答案像不像人话,忽略了检索召回,结果上线后遇到稍微偏一点的问题就胡答。后来把评测集建起来,每次改动先跑一遍,才敢说系统稳了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询