恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

医疗问答系统实践:知识图谱+RAG+大模型意图识别全链路

  • 首页
  • 资讯中心
  • /
  • 医疗问答系统实践:知识图谱+RAG+大模型意图识别全链路

相关资讯

新手入门谷歌关键词排名优化,3步避开模板站陷阱 2026/9/15 6:05:08
顶盖驱动流LBM模拟:参数换算、边界处理与Python实现 2026/9/15 6:05:08
塞梅普雷斯 如是说 (第二部/7.无尽的忧愁) 2026/9/15 6:05:08

最新资讯

RS485与Modbus本质区别:物理层电线 vs 协议层语言
AI教材生成工具:低查重高效创作指南
Ranger HA场景下为统一访问域名生成Kerberos票据的完整指南
Humanizer技能:降低认知摩擦的表达重构方法论
终端AI编程工具实战:从opencode安装配置到模型切换与工程落地
.NET 10 在 aarch64 服务器上的部署与性能优化指南

今日推荐

GDPR下大数据架构重构与隐私保护实践
多组学数据平台架构设计与优化实践
企业主数据管理系统架构设计与实施全解析

本周热门

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化
Flutter应用改名全指南:从Android到iOS的配置与工具实践

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

医疗问答系统实践:知识图谱+RAG+大模型意图识别全链路

发布时间:2026/9/15 6:05:08
医疗问答系统实践:知识图谱+RAG+大模型意图识别全链路 简介基于RAG与大模型技术的医疗问答系统项目资源面向毕业设计、课程设计、工程实训及学科竞赛等场景适合需要快速搭建完整AI应用的开发者。系统以DiseaseKG数据集和Neo4j知识图谱为底座融合BERT命名实体识别与34b大模型意图识别实现精准知识检索与问答生成有效提升医疗咨询可靠性。资源共75个文件压缩包84.65MB包含Python源码、Jupyter Notebook、YAML配置、JSON数据及说明文档等其中py与ipynb覆盖知识图谱构建、NER训练、微调推理等核心流程配图帮助还原界面与系统效果。已有157人学习参考。项目经过严格测试运行功能完好可直接复现提供完整工程文件与设计说明可作为课程设计报告、答辩演示及后续功能扩展的基础同时附有开发环境配置文件便于快速上手。1. 医疗问答不用 RAG34B 裸回答的幻觉率会让项目直接失控医疗问答里最反直觉的一点参数越大裸答越自信犯错越隐蔽。问 34B 大模型“2 型糖尿病患者适合吃哪种水果”回答结构完整但药品学名和剂量错误要逐字核对才能发现。这个项目把“事实”从模型参数里拆了出去DiseaseKG 清洗后导入 Neo4j 知识图谱BERT 抽取疾病、症状、药物实体34B 大模型只做意图识别和答案重写结论必须附带 RAG 检索出的证据节点。整条链路在单机 GPU 上可复现适合做 RAG 项目、大模型应用开发练手也方便改造成毕业设计和课程设计。下面按数据管道、双模型分工、检索链路、系统集成拆开讲。2. DiseaseKG 清洗与 Neo4j 图谱构建从 JSON 到可查询的实体关系2.1 为什么医疗问答要先建图谱而不是纯靠向量库纯向量库做医疗问答有两个硬伤。第一语义近似不等于事实正确“高血压”和“血压偏高”向量接近但关联的用药方案完全不同第二多跳问题推不动比如“同时患高血压和糖尿病的患者首选药物有什么冲突”向量检索只能召回语义片段无法沿着“疾病—症状—药物—禁忌”的关系链走两跳三跳。知识图谱把答案从“找相似的文字”变成“找确定的关系路径”这是医疗场景强约束的必然选择。DiseaseKG 数据集的原始形态是 JSON工程里对应medical_new_2.json。这份数据描述了一个疾病的完整画像疾病名、所属科室、典型症状、常用药物、推荐检查。构建图谱前先要对齐实体口径把“高血压病”“高血压症”这类同义写法归一到一个节点上否则后面 RAG 检索会反复出现“查得到但连不上”的问题。2.2 实体与关系的 Schema 设计先把 ontology 定死图谱不是把所有 JSON 字段无脑变成节点。我一般会先画一个最小的 ontology节点类型控制在五类以内关系类型控制在四类以内。这个项目里比较合理的设计如下节点类型属性示例关系类型方向与语义Diseasename, department, aliasHAS_SYMPTOMDisease → SymptomSymptomnameRECOMMEND_DRUGDisease → DrugDrugname, usageNEED_CHECKDisease → CheckChecknameLOCATED_INDisease → DepartmentDepartmentname——关系类型宁少勿多。有的工程把“禁忌”“慎用”“不良反应”全部拆成独立关系最后查询时路由复杂效果反而差。先用四类核心关系把主链路跑通后续要扩展再补。2.3 build_up_graph.py 的批处理导入逻辑数据量不大时不适合走LOAD CSV因为原数据是嵌套 JSON要先用 Python 做实体对齐再写 Neo4j。常见做法是用py2neo的事务接口批量提交伪代码结构如下from py2neo import Graph, Node, Relationship import json graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) with open(medical_new_2.json, encodingutf-8) as f: records json.load(f) BATCH 500 # 单批写入的节点/关系数量 tx graph.begin() for i, rec in enumerate(records): disease Node(Disease, namerec[疾病]) tx.merge(disease, Disease, name) # merge 而不是 create for symptom in rec.get(症状, []): sym_node Node(Symptom, namesymptom) tx.merge(sym_node, Symptom, name) tx.merge(Relationship(disease, HAS_SYMPTOM, sym_node)) for drug in rec.get(药物, []): drug_node Node(Drug, namedrug) tx.merge(drug_node, Drug, name) tx.merge(Relationship(disease, RECOMMEND_DRUG, drug_node)) if i % BATCH 0: tx.commit() # 分批提交避免大事务 tx graph.begin() tx.commit()这段代码有三个关键点。merge按name属性做唯一匹配所以脚本重复执行不会产生重复节点这是可复现工程的基本要求。BATCH500是经验值Neo4j 单事务写入过多节点时堆内存容易飙高批量提交把内存峰值压住。关系也走merge保证同一条“疾病—症状”关系不会被重复插入后面跑 RAG 检索时不会出现重复证据。2.4 rel_aug.txt 在补什么关系增强与同义词归一rel_aug.txt这个文件看起来像人工整理的关系增强语料它的作用是补全数据集中缺失的关系。例如原始 JSON 只写了“糖尿病—症状—多饮”没有反向关系但用户提问往往是“多饮是什么病的症状”这时就要通过关系增强生成反向三元组或者补充同义实体映射如“口渴”映射到“多饮”。增强后的数据可以喂给后续 NER 数据生成也可以直接用于构建更多图谱路径。导入完成后验证图谱连通性Cypher 如下MATCH (d:Disease)-[r:HAS_SYMPTOM]-(s:Symptom) RETURN d.name AS disease, collect(s.name) AS symptoms LIMIT 10collect把症状聚合到数组里方便一眼看出某疾病的症状覆盖度。验证重点不是查出来多少条而是查一下有没有孤立节点MATCH (n) WHERE NOT (n)--() RETURN count(n)孤立节点意味着实体对不上要回源头查 JSON 和关系增强文件的格式。3. BERT 实体识别与 34B 意图识别医疗场景的双模型分工3.1 为什么实体抽取用小模型意图识别反而用大模型实体抽取是每一条用户请求都要走的环节频率高必须快。BERT 这类百兆级别模型在 CPU 上跑一次几十毫秒精度在医疗命名实体上够用而意图识别要处理的是“我想知道糖尿病平时吃饭注意啥”这类非结构化表达涉及指代和省略小模型容易判错所以交给 34B 大模型做 few-shot 分类更稳。双模型分工后还有一个好处大模型不用每轮都处理全量文本只拿到 BERT 输出的结构化实体和规范化后的问句prompt 更短推理延迟更低。34B 模型 4bit 量化后在 24GB 显存的卡上推理是可行的但如果每轮都让它自己读原文抽实体延迟会翻倍。3.2 NER 数据增强与 BIO 标签体系ner_data_aug.txt 的用法ner_data_aug.txt是 NER 训练语料的增强版本。原始标注数据量有限医疗实体描述多变常见增强手段是实体替换和句式改写例如“糖尿病患者出现多饮多尿”可以改写成“患了糖尿病的人老觉得口渴、上厕所频繁”保持实体标签不变。这样训练出来的 BERT 对口语化表达更鲁棒。标签体系采用标准 BIO 格式tag2idx.npy保存的是标签到索引的映射推理时必须保证它和模型输出维度一致。标注样本大致长这样糖 B-Disease 尿 I-Disease 病 I-Disease 患 O 者 O 多 B-Symptom 饮 I-Symptom3.3 NER 推理代码从 tag2idx 到实体合并import numpy as np import torch from transformers import AutoTokenizer, BertForTokenClassification tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model BertForTokenClassification.from_pretrained( ./model, num_labelslen(tag2idx) ) tag2idx np.load(tag2idx.npy, allow_pickleTrue).item() idx2tag {v: k for k, v in tag2idx.items()} def predict_entities(text: str): tokens tokenizer(text, return_tensorspt, truncationTrue, max_length128) with torch.no_grad(): logits model(**tokens).logits[0] # [seq_len, num_labels] preds logits.argmax(-1).numpy() entities [] cur_tokens, cur_type [], None for tok, pid in zip(tokens[input_ids][0][1:-1], preds[1:-1]): label idx2tag[int(pid)] word tokenizer.convert_ids_to_tokens(int(tok)) if label.startswith(B-): if cur_tokens: entities.append((cur_type, .join(cur_tokens))) cur_tokens, cur_type [word], label[2:] elif label.startswith(I-) and cur_type label[2:]: cur_tokens.append(word) else: if cur_tokens: entities.append((cur_type, .join(cur_tokens))) cur_tokens, cur_type [], None if cur_tokens: entities.append((cur_type, .join(cur_tokens))) return entities代码里[1:-1]是把[CLS]和[SEP]位去掉这两个 token 的预测结果不参与实体合并。max_length128覆盖绝大多数医疗问句但如果问题很长截断会丢掉尾部实体后续可以在预处理阶段加滑窗切分。合并逻辑是“B- 开头同类型 I- 续接”遇到 O 或不同类型就断开最终输出[(类型, 实体词), ...]。3.4 意图识别 prompt 与 LoRA 微调配置意图识别用 34B 大模型跑 few-shot 是性价比最高的方式prompt 只需要固定输出 JSON避免二次解析。模板大致如下你是医疗问答系统的意图路由器只输出 JSON。 问题{question} 实体{entities} 规则 - intentkg_query 表示查询知识图谱 - intentchat 表示闲聊 - intentreject 表示超出医疗范围 输出示例{intent: kg_query, disease: 糖尿病, target: 饮食}结构化输出直接作为下一阶段 RAG 检索的参数不需要再写一套解析规则。工程里的finetune_hf.py和lora_finetune.ipynb做的则是另一件事如果 few-shot 效果不稳定可以对lora_data里的意图语料做 LoRA 微调让模型输出格式收敛。LoRA 微调有几个参数直接影响效果和显存占用我调这个项目时用的配置如下参数推荐值说明lora_r16秩越高表达能力越强显存也越高lora_alpha32通常取2 * lora_rtarget_modulesq_proj, v_projChatGLM/Qwen 系列的注意力投影层lora_dropout0.05防止微调过拟合per_device_train_batch_size134B 模型只能小 batchgradient_accumulation_steps8等效 batch size 8learning_rate2e-4LoRA 微调常用区间量化4bit NF4QLoRA 标配显存降到 1/434B 模型的 LoRA 微调在单卡 A100 40G 上可行24G 显卡建议直接推理不微调或者改用 6B/7B 底座。finetune_hf.py里本质就是把底座模型用bitsandbytes做 4bit 加载再挂peft.LoraModel训练完只保存 adapter 权重推理时合并。注意tag2idx.npy和模型输出头维度要匹配换底座模型时最容易在这里报错。4. RAG 检索链路与 nl2cypher把自然语言编译成 Cypher 查询4.1 混合检索知识图谱查询和向量召回并行RAG 检索层的核心问题是用户表达和知识库表达之间往往存在语义鸿沟。“血压有点高”和图谱里的“高血压”不是同一个字符串所以需要混合检索。一条查询同时走两条路结构化路径从 Neo4j 里按实体名精确匹配非结构化路径把医学文本语料分块后做向量召回最后把两路结果合并送进生成层。向量召回的分块策略直接影响命中率。医疗文本里实体短语经常跨越块边界chunk_overlap太小会把“2 型糖尿病”切碎。我一般用 256 的块大小配合 32 的重叠长度组件参数推荐值文本分块chunk_size256文本分块chunk_overlap32Embedding 模型向量维度768bge-base-zh-v1.5向量召回初筛 top_k20重排序交叉编码器保留 5 条图谱查询查询上限LIMIT 20向量召回用 faiss 内积检索就够数据量超过百万级再考虑迁移 Milvus。重排序这一步不要省初筛 20 条里真正相关的可能只有 3 条交叉编码器逐条打分会显著提升答案质量。4.2 nl2cypher从意图 JSON 到可执行查询工程里的nl2cypher.py、nl2cypher_data.txt、nl2cypher_data_test.txt组成了一套完整的“自然语言转 Cypher”模块。它做的事情是把意图识别输出的 JSON 转成 Neo4j 查询语句。微调数据集里每一行是一对“自然语言问题 对应 Cypher”比如糖尿病早期症状有哪些 MATCH (d:Disease {name:糖尿病})-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name LIMIT 20生成方式可以走 34B 大模型 few-shot也可以对nl2cypher_data.txt做专项微调。我这里展示基于模板的兜底方案适合冷启动def build_cypher(intent: dict) - str: disease intent.get(disease, ) target intent.get(target, ) REL_MAP { 症状: HAS_SYMPTOM, 药物: RECOMMEND_DRUG, 检查: NEED_CHECK, 科室: LOCATED_IN, } rel REL_MAP.get(target) if not rel: raise ValueError(f无法映射目标类型: {target}) return ( fMATCH (d:Disease {{name:$disease}}) f-[:{rel}]-(n) RETURN n.name fLIMIT 20 )这里有个安全细节容易被忽略。Cypher 拼接时用户输入的疾病名称必须走参数绑定$disease不能直接拼字符串否则一个包含引号的恶意输入就能改查询结构。执行前还应该用白名单过滤只允许MATCH、WHERE、RETURN、LIMIT这些只读关键字开头拒绝DELETE、MERGE、CREATE。医疗系统里数据写操作一旦被注入后果不是性能问题而是数据完整性问题。4.3 证据组装与答案生成强迫模型引用来源RAG 生成阶段把图谱查询结果和向量召回片段统一编号拼进 prompt模型只允许基于证据编号作答def build_knowledge_prompt(question, entities, kg_records, doc_chunks): evidence [] for i, (source, content) in enumerate(kg_records doc_chunks, 1): evidence.append(f[{i}] {source}: {content}) return f请仅依据证据回答不要使用模型记忆。 证据不完整时直接回答“依据现有知识无法回答”。 问题{question} 识别实体{entities} 证据 {chr(10).join(evidence)} 回答这个 prompt 的作用是强制模型做“摘要生成”而不是“自由发挥”。每条证据带编号模型回答时可以引用后端也能把答案对应的证据链展示到界面上。没有证据命中时kg_records和doc_chunks都为空此时模型会走到拒答分支这个兜底逻辑是医疗问答系统可靠性的最后一道防线。5. WebUI 集成与系统排错从登录态到一次问答的完整链路5.1 工程文件结构与模块职责webui.py是 Web 服务入口login.py负责登录注册user_data_storage.py管理用户提问记录与会话历史user_credentials.json保存账号数据。分工上登录认证和业务问答分离是很合理的结构user_data_storage.py单独抽出来意味着用户数据的读写逻辑可以被测试脚本直接调用不用启动 Web 服务。一个要注意的点user_credentials.json存账号密码时绝不能明文存储。常见做法是使用werkzeug.security的generate_password_hash每次登录用check_password_hash校验。这个项目的用户数据文件暴露在根目录如果带着这个结构做答辩评审大概率会问密码安全问题提前处理掉值得。5.2 一次完整问答的调用时序用户从前端提交问题后请求依次经过登录态校验 → NER 实体抽取 → 34B 大模型意图识别 → 图谱查询与向量检索 → 证据拼装 → 大模型生成回答 → 答案与证据链返回前端。用 Flask 写路由时核心逻辑如下app.route(/ask, methods[POST]) def ask(): question request.json[question] user session.get(user) if not user: return jsonify({error: 未登录}), 401 entities predict_entities(question) # BERT 实体抽取 intent llm_intent(question, entities) # 34B 意图识别 kg_rows [] if intent[intent] kg_query: cypher build_cypher(intent) # 生成 Cypher kg_rows query_neo4j(cypher) # Neo4j 查询 doc_chunks vector_search(question, top_k20) # 向量召回 prompt build_knowledge_prompt(question, entities, kg_rows, doc_chunks) # 证据拼装 answer llm_generate(prompt) # 答案生成 save_history(user, question, answer, kg_rows[:3]) # 写用户数据 return jsonify({answer: answer, evidence: kg_rows[:3]})session依赖 Flask 的SECRET_KEY不配置的话每次重启服务登录态都会失效这是本地调试常见的“为什么我登录完又跳回登录页”的原因。save_history写用户数据时只存前 3 条证据避免 JSON 文件无限膨胀。5.3 从 log.txt 看常见故障与排查手段log.txt是之前跑通的运行日志里面能看到的几类典型问题现象根因处理方式Cypher 语法错误实体名含引号等特殊字符所有值走参数绑定禁止字符串拼接NER 返回空实体医学名词被分词器拆碎加载自定义词典把常见病名加入tokenizer.add_tokens生成回答为空证据不足触发拒答分支检查图谱覆盖率和分块重叠度显存溢出 OOM34B 全精度推理4bit 量化batch size 设为 1答案质量差向量召回命中率低加交叉编码器重排或换更大的 embedding 模型定位问题的通用手段是把日志结构化。给每个环节记录独立耗时和结果能快速判断瓶颈在哪一层import json, logging, time def log_query(request_id, stage, elapsed_ms, extraNone): logging.info(json.dumps({ request_id: request_id, stage: stage, ms: round(elapsed_ms, 1), **(extra or {}) }, ensure_asciiFalse))用questions.csv里的测试问题做回归时我给每条请求生成一个request_id记录 NER 耗时、意图识别耗时、Neo4j 查询耗时、生成耗时四个指标。如果单条回答超过三秒查看日志定位是查询慢还是模型生成慢。Neo4j 查询慢就检查是否命中索引模型生成慢就考虑换 6B 模型做生成、用 34B 只做意图识别。把日志格式规范好之后再去调整 RAG 分块参数、top_k 或者本地方案下的向量检索配置每次改动都能直接对比日志指标的差异而不是凭感觉反复试。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号