多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于RAG与大模型的Python医疗问答系统实战:从向量检索到引用溯源

基于RAG与大模型的Python医疗问答系统实战:从向量检索到引用溯源 简介这份资源是面向计算机相关专业学生与项目实战学习者的高分毕业设计资料主题为基于RAG与大模型技术的Python医疗问答系统评审分达99分代码完整可运行适合用作毕业设计、课程设计或期末大作业。压缩包共75个文件约84.66MB包含10个py源码、7个ipynb实验笔记、7个json数据、3个yaml配置、3个md说明及19个txt文本另有png、jpg界面截图与csv、npy等数据文件覆盖模型微调、命名实体识别、知识图谱构建与Web交互等模块。已有120人学习下载。读者可获得完整项目源码、微调与推理脚本、数据处理流程、界面截图及文档说明便于快速理解RAG检索增强生成与医疗问答的落地思路并在此基础上完成二次开发与答辩准备。1. 从一份「高分毕设」说起RAG 医疗问答系统到底在解决什么问题医疗问答这个场景最怕的不是模型不会答而是它一本正经地胡说。你问「二甲双胍能不能和造影剂一起用」通用大模型张口就来一段看似专业的建议但剂量、禁忌、适用人群全是编的——这在医疗场景里是致命的。RAG检索增强生成就是冲着这个痛点来的先从可信的医学知识库里检索出相关片段再让大模型基于这些片段组织答案把「凭记忆瞎编」变成「看着资料回答」。这套「基于 RAG 与大模型技术的 Python 医疗问答系统」本质是一个可本地跑通的最小闭环医学文档入库 → 向量检索 → 拼装 Prompt → 大模型生成 → 返回带出处的答案。它适合三类人做毕设需要完整可复现工程的学生、想入门 RAG 实战的 Python 开发者、以及需要给内部知识库加问答能力的工程师。下面我按自己搭这套东西的顺序把选型、代码、参数和踩过的坑讲清楚。2. 拆解 RAG 医疗问答的四层架构与选型理由2.1 为什么医疗场景必须用 RAG 而不是纯微调很多人第一反应是「微调一个大模型不就行了」。微调确实能让模型学会医学术语的表达风格但它有两个硬伤一是知识更新成本高药品说明书改一版你就得重新训二是无法给出处模型说错了你根本不知道它从哪学的。RAG 把「知识」和「推理」解耦——知识放在可随时增删的向量库里推理交给大模型。医疗这种对时效性和可追溯性要求极高的领域RAG 是更务实的选择。这里要区分两个容易混的概念rag 知识库和结构化知识库kg 知识库。向量知识库擅长「语义相似」的模糊召回比如你问「降压药有哪些副作用」它能召回语义相近的段落而结构化知识库知识图谱擅长「关系推理」比如「A 药 → 禁忌 → B 疾病」这种确定性链路。医疗问答里两者常常互补向量库负责找相关文档图谱负责校验药物相互作用这类硬约束。毕设规模下先把向量库做扎实图谱作为进阶方向。2.2 四层架构文档层、检索层、生成层、服务层一套能跑的 RAG 系统拆开就是四层每层职责清晰出问题也好定位层级职责常见技术选型文档层医学文档解析、切分、清洗PyPDF2、python-docx、LangChain Splitter检索层向量化、存储、相似度召回sentence-transformers、FAISS、Chroma生成层Prompt 拼装、大模型调用OpenAI 兼容 API、本地 Ollama服务层接口封装、会话管理FastAPI、Flask选型上我一般这样定嵌入模型用text2vec-base-chinese或bge-small-zh中文医学语料上表现稳、体积小本地 CPU 也能跑向量库毕设阶段用 FAISS零依赖、单文件、够快大模型优先走本地 Ollama 部署避免 API 费用和网络问题实在跑不动再考虑云端 API。这套组合的好处是全离线可复现答辩时不怕断网。2.3 文档切分策略医疗文本不能按固定字数硬切通用 RAG 教程喜欢用chunk_size500, overlap50一刀切但医学文档结构特殊——药品说明书有「适应症/用法用量/不良反应/禁忌」这种固定小节硬切会把一个完整的禁忌说明拦腰截断检索出来半截信息反而误导模型。我的做法是按语义边界切优先按标题层级切其次按段落最后才按字数兜底。切分粒度控制在 300500 字overlap 保留 10%15%保证跨块的上下文不断裂。3. 用 Python 把医疗文档灌进向量库从解析到入库3.1 环境准备与依赖安装先把环境搭起来。Python 建议 3.10 以上依赖用虚拟环境隔离避免和系统包打架# 创建虚拟环境 python -m venv venv # Linux/Mac 激活 source venv/bin/activate # Windows 激活 venv\Scripts\activate # 安装核心依赖 pip install langchain langchain-community faiss-cpu pip install sentence-transformers pip install pypdf python-docx pip install fastapi uvicorn这里faiss-cpu是 CPU 版向量检索库毕设数据量几千到几万条完全够用不用折腾 GPU 版。sentence-transformers负责把文本转成向量。装完先跑一句python -c import faiss, sentence_transformers验证没报错再往下走。3.2 医学文档解析与语义切分把 PDF、Word 里的医学资料读出来再按语义切块。下面这段是核心逻辑from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. 加载 PDF 医学文档 loader PyPDFLoader(data/药品说明书.pdf) docs loader.load() # 2. 语义优先的切分器 # separators 顺序决定优先级先按段落再按句号最后才按字数 splitter RecursiveCharacterTextSplitter( chunk_size400, # 每块目标字数 chunk_overlap60, # 块间重叠防止上下文断裂 separators[\n\n, \n, 。, , , , ], length_functionlen, ) chunks splitter.split_documents(docs) print(f切分后共 {len(chunks)} 块)RecursiveCharacterTextSplitter的关键在separators列表它会按顺序尝试用分隔符切先用\n\n段落切切出来的块还太大就用。句号切逐级降级。医疗文本里一个完整的「禁忌」段落往往就是一个语义单元这样切能最大程度保住完整性。chunk_overlap60是血泪经验——不设重叠时跨块的句子被切断检索出来经常缺主语模型理解就跑偏。3.3 向量化与 FAISS 入库切好的块要转成向量存起来。嵌入模型选中文优化的别用英文模型硬套from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS # 1. 加载中文嵌入模型首次运行会自动下载 embedding HuggingFaceEmbeddings( model_nameshibing624/text2vec-base-chinese, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True}, # 归一化配合余弦相似度 ) # 2. 构建向量库并持久化 vectorstore FAISS.from_documents(chunks, embedding) vectorstore.save_local(vector_store/medical_faiss) print(向量库已保存)normalize_embeddingsTrue这行别省——归一化后向量点积等价于余弦相似度检索排序更稳定。save_local把索引落盘下次启动直接FAISS.load_local加载不用重新算向量省几分钟。数据量大时这一步会慢可以加个进度条或分批add_documents。3.4 检索召回测试先验证再往下接大模型入库完别急着接大模型先单独测检索质量这是最容易被跳过、也最容易翻车的一步# 加载已保存的向量库 vectorstore FAISS.load_local( vector_store/medical_faiss, embedding, allow_dangerous_deserializationTrue, # 本地可信文件才开 ) # 检索 Top-3 相关片段 query 二甲双胍的禁忌症有哪些 results vectorstore.similarity_search_with_score(query, k3) for doc, score in results: print(f相似度得分: {score:.4f}) print(f内容: {doc.page_content[:120]}...) print(- * 40)similarity_search_with_score返回的 score 是距离值越小越相关FAISS 默认 L2 距离。如果 Top-3 里混进了完全不相关的段落说明切分或嵌入模型有问题先调这两个别急着怪大模型。allow_dangerous_deserializationTrue只在加载自己生成的本地文件时开加载来路不明的索引文件有反序列化风险。4. 接上大模型Prompt 拼装与生成链路调优4.1 检索结果如何拼进 Prompt检索出来的片段不能直接丢给模型得用模板框住明确告诉它「只根据以下资料回答」def build_prompt(query, context_docs): # 把检索到的片段拼成上下文 context \n\n.join([d.page_content for d in context_docs]) prompt f你是一名严谨的医疗助手。请严格根据下面提供的资料回答问题 如果资料中没有相关信息请直接回答资料中未提及不要编造。 【参考资料】 {context} 【用户问题】 {query} 【回答要求】 1. 只使用参考资料中的信息 2. 涉及剂量、禁忌必须原文引用 3. 回答末尾标注信息来源段落 return prompt这个模板里最关键的是「资料中未提及就直说」这句约束。不加这句模型遇到检索不到的问题会习惯性用预训练知识补全等于 RAG 白做了。医疗场景宁可答「不知道」也不能编。4.2 调用本地大模型Ollama 方案本地部署大模型用 Ollama 最省事装好后拉个中文能力尚可的模型# 安装 Ollama 后拉取模型以 7B 量化版为例显存友好 ollama pull qwen2.5:7b # 启动服务默认监听 11434 ollama servePython 侧通过 OpenAI 兼容接口调用这样以后换云端 API 也不用改代码from openai import OpenAI client OpenAI( base_urlhttp://localhost:11434/v1, # Ollama 的兼容端点 api_keyollama, # 本地占位非真实密钥 ) def ask_llm(prompt): resp client.chat.completions.create( modelqwen2.5:7b, messages[{role: user, content: prompt}], temperature0.2, # 医疗问答要低温度减少发挥 max_tokens800, ) return resp.choices[0].message.contenttemperature0.2是医疗场景的硬要求——温度越高模型越发散越容易脱离资料自由发挥。max_tokens800控制回答长度避免模型啰嗦。如果本地显存不够跑 7B可以换更小的量化模型或者把生成层切到云端 API检索层保持不变。4.3 完整问答链路串起来把检索和生成接成一条链def medical_qa(query): # 1. 检索相关片段 docs vectorstore.similarity_search(query, k3) # 2. 拼装 Prompt prompt build_prompt(query, docs) # 3. 调用大模型 answer ask_llm(prompt) # 4. 附带出处返回 sources [d.metadata.get(source, 未知) for d in docs] return {answer: answer, sources: sources} # 测试 result medical_qa(阿司匹林肠溶片应该饭前还是饭后吃) print(result[answer]) print(出处:, result[sources])k3是召回数量太小信息不够太大噪声多还费 token。实践中 35 是甜点区可以按知识库密度调。返回sources让答案可追溯答辩时这是加分项也是医疗场景的合规底线。4.4 用 FastAPI 封装成可调用服务毕设通常要演示接口用 FastAPI 几行搞定from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class Query(BaseModel): question: str app.post(/qa) def qa_endpoint(q: Query): return medical_qa(q.question) # 启动: uvicorn main:app --host 0.0.0.0 --port 8000启动后访问/docs就有自动生成的接口文档前端或 Postman 直接调。--host 0.0.0.0让局域网内其他设备也能访问方便演示。5. 避坑指南RAG 医疗问答最常见的五个翻车点5.1 检索召回一堆不相关内容现象问「糖尿病用药」召回的全是「糖尿病饮食」段落答非所问。原因嵌入模型对中文医学语义区分度不够或切分粒度过粗导致一块里混了多个主题。解决换bge-large-zh这类更强的中文模型把 chunk_size 从 500 降到 300引入重排序rerank模型对 Top-10 结果二次精排只留最相关的 3 条。5.2 模型无视资料自己编答案现象检索到的资料明明没有某药剂量模型还是给了一个数字。原因Prompt 约束不够强或 temperature 太高。解决Prompt 里加「资料未提及必须回答不知道」的硬约束temperature 压到 0.10.2在生成后加一层校验检测答案里的数字是否出现在检索片段中不在就拦截。5.3 中文嵌入模型首次下载卡住现象HuggingFaceEmbeddings初始化时卡在下载或报连接超时。原因模型文件从境外源拉取网络不稳。解决提前手动下载模型文件放到本地目录用model_name/本地路径/text2vec-base-chinese指定本地加载或配置国内镜像源加速。5.4 向量库加载报反序列化错误现象FAISS.load_local抛allow_dangerous_deserialization相关异常。原因新版 LangChain 出于安全默认禁止 pickle 反序列化。解决加载自己生成的索引时显式传allow_dangerous_deserializationTrue但绝不要加载来源不明的索引文件pickle 反序列化可执行任意代码。5.5 回答太长、答非所问现象模型把检索到的整段资料复述一遍没有针对性回答。原因Prompt 没限定回答格式或 max_tokens 给太大。解决Prompt 里明确「用 3 句话以内回答」max_tokens 收到 500 以内要求模型先给结论再给依据避免它绕圈子。6. 让答案更可信引用溯源与检索质量自检的两个技巧毕设答辩时评委最爱问的一句是「你怎么保证它没瞎编」。光靠 Prompt 约束是软约束真正能拿出手的是引用溯源——让每个答案都能指回原文。做法是在切分时给每块打上来源标记生成后把答案和检索片段做一次对齐def answer_with_citation(query): docs vectorstore.similarity_search(query, k3) prompt build_prompt(query, docs) answer ask_llm(prompt) # 把检索片段编号让模型在答案里引用编号 citations [] for i, d in enumerate(docs, 1): src d.metadata.get(source, 未知) page d.metadata.get(page, ?) citations.append(f[{i}] {src} 第{page}页) return answer, citations配合 Prompt 里加一句「引用资料时标注 [编号]」模型输出就会带上[1][2]这样的标记前端渲染时点开就能看到原文。这套机制在医疗场景不是锦上添花而是刚需——医生和患者都需要知道这个结论从哪来。第二个技巧是检索质量自检。上线前我会准备一组「标准问答对」跑一遍看召回率测试问题期望召回片段实际 Top-1是否命中阿司匹林禁忌说明书禁忌段禁忌段是二甲双胍剂量用法用量段不良反应段否命中率低于 80% 就说明切分或嵌入模型要调别急着优化生成层——检索是地基地基歪了上面盖什么都白搭。我一般会攒 3050 条这样的测试问题每次改完切分参数就重跑一遍用数据说话而不是凭感觉。最后说个我自己的习惯这套系统我从来不敢直接给非专业人士用哪怕它答得再像样。RAG 能大幅降低幻觉但降不到零医疗场景里那 1% 的错误可能就是事故。所以我的做法永远是——答案必须带出处界面上必须挂免责声明把它定位成「辅助查资料的工具」而不是「替代医生的诊断」。做技术要有边界感这条线不能越。希望帮到你。本文还有配套的精品资源点击获取
返回列表