ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

医疗问答系统实战:Neo4j知识图谱+BERT向量检索+RAG

医疗问答系统实战:Neo4j知识图谱+BERT向量检索+RAG 简介本资源为基于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 DesktopLinux 用 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补属性关系上的level和note是后面生成答案时要引用的证据字段。参数上节点名建议统一用标准医学术语别混用俗称否则图谱查询会漏。批量导入几万条时不要一条条MERGE用LOAD CSV或apoc.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, paddingTrue, truncationTrue, max_length512, return_tensorspt) with torch.no_grad(): out model(**inputs) # 取 CLS 向量并归一化方便用内积算余弦相似度 vecs out.last_hidden_state[:, 0, :].numpy() return vecs / np.linalg.norm(vecs, axis1, keepdimsTrue) chunks [二甲双胍常见不良反应包括腹泻、恶心……, 碘造影剂使用前应评估肾功能……] vecs embed(chunks).astype(float32) index faiss.IndexFlatIP(vecs.shape[1]) index.add(vecs) faiss.write_index(index, medical.index)逻辑说明CLS向量是 BERT 对整句的聚合表示归一化后用内积等价于余弦相似度。max_length512是 BERT 上限超长文本必须先切块。参数上truncationTrue防止报错但会截断所以切块要控制在 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.conf中dbms.default_listen_address0.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_graph0.6, w_vec0.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(), keylambda x: -x[1])[:6]逻辑说明用倒数排名加权排名越靠前贡献越大。w_graph和w_vec是唯一要调的两个参数评测集上跑几轮就能找到较优值。注意别在测试集上反复调留一部分做最终验证。另一个技巧是把用户反馈接进来每次回答后让用户点“有用/没用”没用的记录进待优化池定期人工看是检索漏了还是生成偏了。这个习惯坚持下来系统会越用越准。我自己踩过最大的坑是早期只看生成答案像不像人话忽略了检索召回结果上线后遇到稍微偏一点的问题就胡答。后来把评测集建起来每次改动先跑一遍才敢说系统稳了。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进