ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Python的RAG与大模型医疗问答系统实战:从知识库构建到KG校验

2026/10/7 18:41:59 拓冰建站 浏览量
基于Python的RAG与大模型医疗问答系统实战:从知识库构建到KG校验 简介这是一套面向计算机、人工智能及自动化等专业方向的毕业设计级医疗问答系统源码基于检索增强生成RAG框架与前沿大语言模型技术构建可支撑医疗领域智能问答的完整实现。项目为独立完成的毕设成果答辩获98分功能模块均通过测试且可执行适合具备一定专业基础的高校师生与技术人员参考并支持在此基础上做功能扩展与场景定制。资源包共89个文件约94.7MB涵盖Python源码、Jupyter Notebook实验脚本、JSON与CSV医疗数据、YAML训练配置、PNG/JPG界面与流程截图、Markdown说明文档及模型相关文件覆盖数据处理、图谱构建、模型微调与推理等环节。目前已有99人学习。读者可据此获得一套结构完整的RAG医疗问答实现方案理解检索与生成协同的技术路径并借助配置与脚本快速复现、调试与二次开发。1. 医疗问答系统为什么不能直接套通用 RAG从一次答非所问说起你问“二甲双胍肾功能不全能不能用”通用大模型可能给你一段药理机制听起来头头是道但剂量调整阈值、eGFR 分层、禁忌证边界全是模糊的。医疗问答的容错率极低答错一句可能误导用药。这就是基于 Python 的 RAG 与大模型医疗问答系统要解决的核心问题让模型回答前先检索权威医学知识再基于检索到的证据生成答案而不是靠参数记忆硬编。这套方案适合谁计算机专业做毕业设计的学生需要一套能跑通、能演示、有技术深度的完整系统也适合刚接触 RAG 的 Python 开发者想找一个真实场景把检索、重排、生成、评估全链路走一遍。医疗领域的好处是知识边界清晰、评测标准明确比做通用闲聊问答更容易出成果。下面从数据准备到部署把每个环节的参数和坑讲透。2. 医疗 RAG 的数据层知识库怎么建、怎么切、怎么存2.1 医疗知识库的三种来源与取舍做医疗问答知识库质量决定上限。常见来源有三类公开医学指南和药品说明书、教科书结构化章节、以及已有问答对。指南和说明书权威性最高但格式乱PDF 表格多教科书章节连贯适合按段落切问答对可以直接做评测集。我一般这样配比指南和说明书占 60%教科书占 30%问答对占 10% 用于验证。注意不要直接把网上抓的科普文章扔进去来源不可控的内容会污染整个检索层。如果标题里提到 kg 知识库和 rag 知识库的区分这里要明确RAG 知识库存的是文本块和向量KG 知识库存的是实体关系三元组。医疗场景两者可以互补——RAG 负责找证据段落KG 负责校验药物-疾病-禁忌的关系是否冲突。毕业设计里先把 RAG 跑通KG 作为加分项。2.2 文档切分医疗文本的 chunk 策略通用 RAG 教程常建议 chunk_size512但在医疗文本上直接套会出问题。药品说明书里“用法用量”和“禁忌”往往在同一页切太碎会丢失上下文切太大又会让检索精度下降。我的做法是按语义结构切先按标题层级分节再在节内按段落切单块控制在 300500 字。对于表格类内容如剂量调整表整表作为一个 chunk并在块首加一行摘要说明表的内容。from langchain.text_splitter import RecursiveCharacterTextSplitter # 医疗文本专用切分优先按段落和标题切再按字数兜底 splitter RecursiveCharacterTextSplitter( separators[\n## , \n### , \n\n, \n, 。, ], chunk_size450, # 医疗文本信息密度高450字左右较稳 chunk_overlap80, # 重叠80字防止跨段语义断裂 length_functionlen, is_separator_regexFalse, ) chunks splitter.split_text(raw_medical_text)逻辑说明separators 列表按优先级排列先尝试用二级标题切再退到段落最后才按句号切。chunk_overlap 设 80 是为了让相邻块在边界处有重叠避免“用法用量”和“禁忌”被切到两个块后检索只命中一个。参数怎么改如果你的文档表格特别多把 chunk_size 提到 600overlap 提到 120如果检索结果太泛降到 350 和 60。2.3 向量化与存储embedding 模型选型和向量库配置embedding 模型直接决定检索召回率。医疗领域建议用中文医学语料微调过的模型或者至少是中文通用表现好的。常见做法是用 BGE 系列的中文模型本地部署不依赖外部接口。向量库选型上毕业设计推荐 Chroma 或 FAISS。Chroma 自带持久化适合演示FAISS 检索快适合数据量大的场景。下面以 Chroma 为例。import chromadb from chromadb.utils import embedding_functions # 使用本地 embedding 模型避免调用外部 API 的延迟和费用 ef embedding_functions.SentenceTransformerEmbeddingFunction( model_nameBAAI/bge-base-zh-v1.5 # 中文检索表现稳定 ) client chromadb.PersistentClient(path./medical_vectordb) collection client.get_or_create_collection( namemedical_qa, embedding_functionef, metadata{hnsw:space: cosine} # 余弦距离适合文本语义相似度 ) # 批量写入每批不超过 500 条避免内存溢出 batch_size 500 for i in range(0, len(chunks), batch_size): batch chunks[i:ibatch_size] collection.add( documentsbatch, ids[fdoc_{ij} for j in range(len(batch))], metadatas[{source: guideline, chapter: usage}] * len(batch) )逻辑说明embedding_function 指定本地模型首次运行会自动下载。hnsw:space 设为 cosine因为文本向量归一化后余弦距离比欧氏距离更稳定。metadatas 里存来源和章节方便后续按来源过滤检索。参数注意batch_size 不要超过 1000Chroma 在批量写入时内存占用会线性增长。如果报错“duplicate id”检查 ids 是否重复建议用文档哈希加序号生成唯一 id。3. 检索与重排让医疗证据排在前面的四个调参点3.1 混合检索向量召回加关键词召回纯向量检索在医疗场景有个硬伤药物名称、检验指标这类专有名词向量模型可能把它们映射到相近但不精确的位置。比如“二甲双胍”和“格列美脲”向量距离可能很近但它们是不同药物。解决办法是混合检索向量召回负责语义相似BM25 负责关键词精确匹配两路结果合并去重。from rank_bm25 import BM25Okapi import jieba # 构建 BM25 索引中文需要先分词 tokenized_corpus [list(jieba.cut(doc)) for doc in chunks] bm25 BM25Okapi(tokenized_corpus) def hybrid_retrieve(query, top_k10): # 向量召回 vector_results collection.query(query_texts[query], n_resultstop_k) vector_docs vector_results[documents][0] # BM25 召回 tokenized_query list(jieba.cut(query)) bm25_scores bm25.get_scores(tokenized_query) bm25_top_idx sorted(range(len(bm25_scores)), keylambda i: bm25_scores[i], reverseTrue)[:top_k] bm25_docs [chunks[i] for i in bm25_top_idx] # 合并去重保持向量结果优先 seen set() merged [] for doc in vector_docs bm25_docs: if doc not in seen: seen.add(doc) merged.append(doc) return merged[:top_k]逻辑说明jieba 分词对医疗术语支持一般可以加载自定义词典把药物名加进去。合并时向量结果优先因为语义匹配通常更准。参数怎么改top_k 设 10 是召回阶段后面还有重排所以可以放宽。如果检索结果里噪声多把 BM25 的权重降低或者只保留向量结果。3.2 重排模型为什么召回后还要过一遍 reranker召回阶段追求高召回率会引入不相关文档。重排模型reranker对候选文档逐条打分把真正相关的排到前面。医疗场景强烈建议加重排因为“相关”和“不相关”的边界很微妙。常见做法是用交叉编码器做重排比如 BGE-reranker 系列。它把 query 和 document 拼在一起输入模型输出相关性分数比向量内积准得多但速度慢所以只用在召回后的 top 1020 条上。from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-base) def rerank(query, candidates, top_n5): pairs [[query, doc] for doc in candidates] scores reranker.predict(pairs) ranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return [doc for doc, _ in ranked[:top_n]]逻辑说明CrossEncoder 接收 query-doc 对输出一个分数。排序后取 top_n 送入生成阶段。参数注意top_n 设 35 足够太多会稀释关键证据也会增加生成阶段的 token 消耗。如果显存不够把 reranker 换成 small 版本或者用 API 方式调用。3.3 检索结果过滤按来源和时效性筛医疗知识有时效性旧版指南可能被新版替代。检索时可以在 metadata 里加年份和来源等级过滤掉过期或低权威内容。# 检索时加过滤条件只要近5年的指南和说明书 results collection.query( query_texts[query], n_results10, where{ $and: [ {year: {$gte: 2020}}, {source: {$in: [guideline, drug_label]}} ] } )逻辑说明where 条件在向量检索时生效减少无效候选。参数怎么改如果知识库更新频繁把 year 阈值调近如果数据量少放宽来源限制。注意 Chroma 的 where 语法对嵌套条件支持有限复杂过滤建议在应用层做。4. 生成层大模型怎么选、prompt 怎么写、幻觉怎么压4.1 医疗问答的模型选型本地部署还是 API毕业设计常见两种路线本地部署开源模型或者调用大模型 API。本地部署的好处是数据不出域、可离线演示缺点是显存要求高、推理慢。API 的好处是效果好、接入快缺点是有调用成本、依赖网络。如果做企业私有化部署方向本地模型是必选项。常见做法是用 Ollama 部署 7B 或 13B 的中文模型量化后 7B 模型 8GB 显存能跑。如果只是毕业设计演示API 更省事但要注意免费额度限制。提示无论选哪种生成阶段都要把检索到的证据拼进 prompt并明确要求模型“仅根据以下证据回答证据不足时说不知道”。4.2 Prompt 模板医疗问答的约束写法医疗 prompt 的核心是约束模型不要自由发挥。我一般用三段式角色设定、证据注入、输出格式。MEDICAL_PROMPT 你是一个严谨的医疗问答助手。请仅根据以下【证据】回答用户问题。 如果证据不足以回答直接说“现有资料无法回答该问题”不要编造。 回答时先给出结论再列出依据。涉及用药剂量时必须原文引用证据中的数值。 【证据】 {context} 【用户问题】 {question} 【回答】逻辑说明{context} 是重排后的 top 35 个文档块拼接{question} 是用户原始问题。约束“仅根据证据”能显著降低幻觉。参数注意context 拼接时加分隔符和来源标注方便模型区分不同文档。如果模型还是编造把 temperature 降到 0.1 以下。4.3 幻觉压制三个可落地的检查点第一检索为空时不生成。如果重排后最高分低于阈值直接返回“未找到相关医学证据”不调生成模型。第二生成后做关键词校验。把回答里的药物名、数值和证据原文比对不一致的标红。第三多轮追问时重新检索。不要用上一轮的 context 回答新问题医疗问题差一个字结论可能相反。def safe_generate(query, context_docs, min_score0.3): if not context_docs: return 未找到相关医学证据请咨询专业医生。 # 假设 rerank 分数存在取最高分判断 if context_docs[0][1] min_score: return 现有资料不足以回答该问题。 context \n---\n.join([doc for doc, _ in context_docs]) prompt MEDICAL_PROMPT.format(contextcontext, questionquery) # 调用大模型生成 answer llm.generate(prompt, temperature0.1, max_tokens500) return answer逻辑说明min_score 是重排分数阈值低于它说明检索结果不可靠。temperature 设 0.1 让输出更确定。max_tokens 限制 500 防止模型长篇发挥。参数怎么改min_score 需要根据你的 reranker 分数分布调建议先跑 20 条测试集看分布再定。5. 避坑与排查医疗 RAG 落地时最容易翻车的五件事5.1 检索命中率低答非所问现象用户问“孕妇能不能用某药”检索出来的全是该药的一般药理没有妊娠禁忌。原因chunk 切分时把“孕妇及哺乳期妇女用药”单独切出去了或者 embedding 模型对“孕妇”和“妊娠”的语义关联不够。解决检查切分逻辑确保禁忌相关段落完整在 embedding 前对 query 做同义词扩展把“孕妇”扩展为“孕妇 妊娠 哺乳期”。5.2 模型无视证据自己编答案现象证据里写的是“慎用”模型回答成“禁用”。原因prompt 约束不够强或者模型本身指令遵循能力差。解决在 prompt 里加 few-shot 示例展示“证据说慎用回答也必须说慎用”换指令遵循更好的模型生成后做数值和关键词比对不一致就重新生成或降级为“请查阅原文”。5.3 向量库写入慢或内存爆现象导入几千条文档后 Chroma 卡死。原因批量写入时 batch_size 太大或者 embedding 模型在 CPU 上跑。解决batch_size 降到 200embedding 改用 GPU 推理如果数据量超过 10 万条换 FAISS 或 Milvus。5.4 多轮对话后检索漂移现象第一轮问“二甲双胍”第二轮问“那肾功能不全呢”检索结果变成泛泛的肾功能不全丢失了药物上下文。原因第二轮 query 没有带上第一轮的关键实体。解决做 query 改写把历史对话里的关键实体拼进当前 query或者用指代消解把“那”还原成“二甲双胍”。5.5 评测指标好看但实际不能用现象离线评测 recall 很高但人工看回答还是错。原因评测集和真实问题分布不一致或者评测只看了检索没看生成。解决建一个 50100 条的真实问题测试集覆盖常见病、用药、禁忌、剂量四类评测时检索和生成分开打分生成部分人工评估事实一致性。6. 进阶技巧用 KG 校验 RAG 输出把事实一致性再提一档到这一步系统基本能跑了。但医疗问答有个隐藏风险检索到的证据本身可能矛盾比如两份指南对同一药物的推荐等级不同。这时候光靠 RAG 生成模型可能随机选一个。我的做法是引入一个轻量 KG 做交叉校验。具体思路从回答里抽取“药物-疾病-推荐等级”三元组去 KG 里查是否存在冲突。KG 不用大覆盖常见药物和禁忌即可用 Neo4j 或内存图结构都行。下面是一个简化示例。# 假设 KG 用字典模拟drug - disease - recommendation kg { 二甲双胍: { 肾功能不全: {eGFR30: 禁用, eGFR 30-45: 慎用} } } def kg_check(answer, drug, condition): 从回答中提取推荐等级与 KG 比对 if drug not in kg or condition not in kg[drug]: return KG 无记录跳过校验 kg_rec kg[drug][condition] # 简单关键词匹配实际可用 NER 抽取 for level in [禁用, 慎用, 可用]: if level in answer: if level ! list(kg_rec.values())[0]: return f冲突KG 推荐 {kg_rec}回答提到 {level} return 一致逻辑说明这个校验层放在生成之后如果发现冲突就把 KG 的记录作为补充证据重新生成或者直接在回答里标注“注意不同来源推荐等级存在差异”。参数注意KG 的粒度要跟 RAG 知识库对齐否则会出现 KG 有记录但 RAG 没检索到的情况反而增加困惑。验证方法上我习惯用 A/B 对比同一批问题一组纯 RAG一组 RAGKG 校验人工评估事实错误率。通常 KG 校验能把严重错误如禁用说成可用降低一半以上但会增加少量“过度保守”的回答。这个取舍在医疗场景是值得的。最后说个血泪经验别在毕业设计里追求大而全。先把 RAG 链路跑通评测集建好再考虑加 KG 或微调。我见过太多人卡在数据清洗上最后演示都跑不起来。先把最小可用系统做出来再迭代。希望帮到你。本文还有配套的精品资源点击获取