
简介基于检索增强生成与大模型技术的Python医疗问答系统毕业设计完整资料包面向人工智能、通信工程、自动化、电子信息、物联网等计算机相关专业的学生与从业者既适合毕业设计、课程设计、作业提交或项目初期演示也适合编程初学者作为进阶实战项目学习。压缩包共1667个文件大小约140.19MB主要包含Python源码、前端TypeScript代码、JSON配置、Markdown文档、pickle数据文件、图片资源以及Shell脚本等从项目代码到配套文档一应俱全目录结构清晰方便按模块查阅和二次开发。系统完整覆盖医疗问答场景的数据准备、向量检索、模型调用与前端展示流程并附带详细设计报告、Docker部署配置、Excel表格等资料可帮助读者理解RAG与大模型技术在实际项目中的落地方法与调优思路。源码经过全面测试且运行稳定已有55人学习下载提供远程指导支持可直接支撑毕业设计或课程设计展示也方便在现有基础上扩展新功能。1. 医疗问答为什么绕不开RAG大模型裸答的风险边界如果把大模型直接接到医疗问答场景里第一版demo很快就能跑通真正让项目停下来的是“大模型会一本正经地胡说”。同样是“我最近经常头晕”这个问题裸模型可能从低血糖一路猜到脑瘤既不看血压也不考虑年龄。RAG检索增强生成要解决的就是这件事先在被筛选过的医疗资料里检索证据再让大模型只基于证据作答没证据就明说不知道。标题里这个毕设项目的可写之处就在这用Python把RAG、大模型和医疗知识库串成完整问答系统代码量不大但把“答案有出处”这件事做扎实。适合正在选题的学生也适合想在业务里落地RAG的工程师前者看整体实现后者看检索和prompt边界怎么控制。2. 医疗RAG系统的技术构成与组件选型Embedding、向量库、本地大模型通用RAG链路大家都不陌生但医疗场景会单独逼你把每个环节想清楚资料格式混乱、专业名词太多、一句话答案可能决定一个患者下一步做什么。所以这一章先讲链路再给组件选型最后讨论一个比较热门但容易做过头的话题agentic rag在医疗场景里到底要不要上。2.1 医疗RAG的检索增强生成流程与通用问答的本质区别最常见的实现方式是把问答拆成五个动作医疗文档清洗、按语义切块、向量化写入向量库用户提问时把query也向量化在库里做相似度检索取回top_k个知识块最后把知识块拼进prompt交给大模型生成答案。整套流程用Python串起来核心链路可以用下面这段伪代码表示# 传统“一次注入”问答模型凭内部记忆回答 answer llm.generate(question) # 医疗场景的RAG链路伪代码 def medical_rag(question): chunks vector_db.search(embed(question), top_k30) # 召回阶段 chunks rerank(question, chunks)[:5] # 重排阶段 evidence format_evidence(chunks) # 带编号与来源 answer llm.generate(build_prompt(evidence, question)) return answer, evidence和通用问答最大的区别在“证据”两个字上。通用RAG检索结果不理想时用户最多觉得回答不聪明医疗场景里检索结果不理想大模型会把不相关的内容也编进回答里看起来很有道理实际上换了药名或剂量。所以在医疗RAG里检索质量决定了整个系统可信度的上限。这也是为什么医疗RAG的检索阶段要比通用场景多一个动作先多召回一些候选比如top_k30经过重排后再挑出5个真正相关的片段给大模型。召回和重排分离是这类项目常见的做法毕设报告里写这个点也容易拿分。2.2 Embedding、向量库与本地大模型的选型理由在Python生态里三个核心组件的选型直接决定后面代码怎么写。我一般给出的组合是BGE系列Embedding模型、Chroma或Milvus作为向量库、Qwen2.5系经Ollama或vLLM本地部署。组件推荐选型选型理由Embedding模型BAAI/bge-m3 或 bge-large-zh-v1.5中文效果靠前bge-m3支持8192长度归一化后可以做余弦相似度向量数据库Chroma毕设/小规模、Milvus生产Chroma零运维够用Milvus支持标量过滤和分区适合按科室、药品维度过滤大模型Qwen2.5-14BOllama或vLLM部署开源权重、中文对话能力强、显存要求可控且不会像云端API那样受网络影响重排模型BAAI/bge-reranker-v2-m3在top30里挑top5能救回不少被Embedding忽略的专业名词补充一点选型心得。医疗问答系统不建议一上来就用医疗垂直大模型比如某些基于医学语料微调的开源模型。这类模型专业名词背得多但同样会幻觉而且因为训练语料偏向教科书遇到院内手册、药品说明书里的最新信息反而答不准。通用模型加RAG把最新资料喂进去可控性更强也更容易解释答案来源。2.3 进阶形态的取舍agentic rag在医疗场景要不要上最近agentic rag的概念很热核心思想是让大模型自己决定“要不要再检索一次”“要不要改写query”“先查A还是先查B”把单次RAG变成多轮推理。这个方向在大模型公开课和技术文章里很常见但对医疗毕设来说需要克制。医疗问答的大多数问题属于“单跳问答”高血压患者能不能吃布洛芬、这个药一天几次、某检查前的注意事项一次检索加一次生成就能回答。只有遇到“患者同时吃A和B会不会冲突”这类需要多步推理的问题才值得让模型拆解步骤。我的建议是毕设主体做扎实的单次RAG答辩时把agentic rag作为“后续演进方向”来谈比直接把系统做成多轮决策更稳妥因为多轮检索会让答案的可解释性和延迟都变差。3. Python实现医疗RAG问答系统从加载模型到跑通一轮问答这一章直接给可复现的最小实现。整体思路是用Ollama管理本地大模型用HuggingFace BGE模型做向量化用Chroma存向量业务代码只写检索、拼prompt、调模型三件事。整套代码在CPU环境下也能跑通只是Embedding和生成会慢一些。3.1 环境准备与依赖安装建议用虚拟环境隔离项目依赖Python 3.10及以上版本。需要安装的核心库包括LangChain社区包、Chroma向量库、sentence-transformers以及Ollama的Python客户端。python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate pip install langchain langchain-community chromadb sentence-transformers ollama # 本地大模型运行时 ollama pull qwen2.5:14b ollama run qwen2.5:14b两个说明。第一venv不是可选项LangChain和Chroma的依赖版本经常打架隔离环境能省很多排错时间。第二ollama pull会拉取模型权重到本地之后每次启动都由Ollama进程托管模型Python代码通过HTTP接口或ollama包访问。如果机器显存不够跑14B模型先换成qwen2.5:7b验证链路代码不用改。3.2 医疗文档向量化的最小实现向量化的目标是把切好的文本块转成浮点向量。这里用LangChain封装的HuggingFaceBgeEmbeddings加载bge-m3它能识别GPU并自动分配设备。from langchain_community.embeddings import HuggingFaceBgeEmbeddings model_name BAAI/bge-m3 model_kwargs {device: cuda} # 没有GPU就改成 cpu encode_kwargs {normalize_embeddings: True} embeddings HuggingFaceBgeEmbeddings( model_namemodel_name, model_kwargsmodel_kwargs, encode_kwargsencode_kwargs, )这个模型输出1024维向量支持中文、英文以及中文医学名词中常见的夹写形式。normalize_embeddings设为True让向量在做余弦相似度时只需要算内积Chroma内部的检索效率会更高。如果从HuggingFace下载权重较慢可以从ModelScope渠道下载同名模型文件再把model_name改成本地路径。向量化之后写入Chroma注意collection_metadata里指定余弦距离from langchain_community.vectorstores import Chroma vectorstore Chroma.from_documents( documentssplit_docs, embeddingembeddings, persist_directory./medical_rag_db, collection_metadata{hnsw:space: cosine}, )persist_directory指定数据库落盘目录医疗文档量不大时整个知识库就是本地一个目录复制到别的机器就能迁移。3.3 检索与大模型生成的衔接代码检索阶段用top_k控制传给大模型的资料条数。医疗场景不要把top_k设成3以下因为一个知识点往往分散在多段资料里太少会漏关键信息。retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 5}, ) # 调用示例 docs retriever.invoke(高血压患者可以服用布洛芬吗) for i, doc in enumerate(docs, 1): print(f[{i}] {doc.page_content[:100]})生成阶段通过ollama包调用本地模型。prompt的写法对医疗问答效果影响很大需要明确告诉模型“只依据资料回答”“信息缺失时明说”并对回答格式做约束。import ollama def answer_with_rag(question): docs retriever.invoke(question) context \n\n.join( [f[资料{i}] {d.page_content} for i, d in enumerate(docs, 1)] ) prompt f你是一名谨慎的医疗问答助手。请只依据下方资料回答不要使用资料之外的知识。 回答顺序先给结论再列依据资料里没有的信息明确回答“资料未提及”。禁止给出诊断结论。 资料 {context} 问题{question} 回答 res ollama.chat( modelqwen2.5:14b, messages[{role: user, content: prompt}], options{temperature: 0.2, top_p: 0.9}, ) return res[message][content]temperature在医疗问答里建议压在0.1到0.3之间top_p可以保留0.9。这个组合下模型更倾向于复述资料内容而不是自由发挥。如果体验下来回答过于机械优先提高temperature不要动top_p。3.4 一轮完整问答的效果与关键参数速查跑通上面的代码后回答会明显出现“裸答”和“RAG回答”的差异。裸答时模型会写“布洛芬可能升高血压建议监测”RAG回答则会引用具体说明书条目例如“资料[2]提到布洛芬说明书注意事项第4条高血压患者用药期间应监测血压”。后者在毕设答辩里展示出来非常直观。参数建议范围设置说明temperature0.1 - 0.3越低越忠实于资料太高容易在回答里夹带未验证信息top_p0.85 - 0.95控制生成候选集范围一般不用频繁调整top_k5 - 10检索返回条数取决于知识块切分粒度chunk_size256 - 512文本切块长度医疗场景建议偏小4. 医疗知识库的构建与检索调参chunk切分、top_k与重排很多RAG项目效果不好问题不在大模型而在知识库侧。医疗资料的来源复杂清洗、切分、向量化每一步都可能引入噪声。这一章把医疗语料的处理链路拆开讲并给出一套可以复用的调参方法。4.1 医疗数据清洗的常见问题与处理顺序医疗RAG的知识来源通常是三类药品说明书PDF居多、临床指南长文档、医院内部手册Word或Excel。清洗时要先处理几个高频问题PDF里的页眉页脚和页码会被切进正文表格内容按行读取后缺乏上下文药品剂量写法不统一比如“mg/kg”被错误换行指南里的推荐等级标识如“A级推荐”会打断句子。常见的清洗做法是用PyMuPDF读取PDF后先按页面抽文本再用正则去掉页眉页脚和参考文献编号Word文档用python-docx按段落读表格数据单独转成“字段名值”的文本块。清洗结果统一成纯文本或Markdown格式再进入切分环节。代码层面最容易被忽略的是剂量单位的保护。切分器如果按固定窗口硬切可能会把“5mg/kg”拆成“5mg / kg”语义就变了。处理办法是把剂量和单位之间的空格先替换成占位符切完再还原import re def protect_dose(text: str) - str: # 把5mg/kg中的空格替换成占位符避免切分器切断 text re.sub(r(\d(?:\.\d)?)\s*(mg|g|ml|μg|ug)\s*/\s*kg, r\1\2/kg, text) return text4.2 chunk切分参数与结构化切分思路切分是检索质量的直接决定因素。通用场景喜欢大chunk保证语义完整但医疗场景我建议chunk_size控制在256到512之间。原因很实际一个chunk里包含的疾病信息越杂检索命中后大模型就越难判断哪句话才是回答依据回答时容易把同段跟主题无关的内容也带出来。用LangChain的RecursiveCharacterTextSplitter时要注意把中文句号加进separators默认分隔符是按英文句号设计的from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap128, separators[\n\n, \n, 。, , ], ) split_docs text_splitter.create_documents([cleaned_text])这里overlap设为chunk_size的四分之一左右目的是让同一句话在两个相邻chunk里都完整出现避免切分恰好把关键信息切断。但overlap不是越大越好过大会导致同一个知识点在多条记录中重复命中检索结果高度雷同浪费上下文窗口。更进阶的做法是按标题层级做结构化切分先用正则把“第X章”“适应症”“禁忌”“用法用量”这类标题抽出来再在标题树内做切分并把标题作为metadata存进向量库。这样回答时能明确指出“这个结论来自‘禁忌’章节”在医疗场景里非常有说服力。4.3 top_k、相似度阈值与重排参数的组合试验检索参数之间会相互影响单个参数调优意义不大。建议用一组固定的20个医疗问题做测试集对比不同参数组合下的回答效果。这组问题不要只选简单题要包含“症状描述不精确”和“多个疾病相似”两类难例。参数参考值值偏小值偏大chunk_size256 - 512检索精准但上下文碎片化上下文完整但噪声变大chunk_overlap64 - 128段落衔接信息丢失向量库膨胀检索结果重复top_k5 - 10响应快但容易漏资料上下文过长无关信息干扰模型重排候选数召回30重排后取5重排效果不明显增加延迟但收益递减实际测试时我会先固定chunk_size512、overlap128跑完20问看回答的引用质量再把chunk_size压到256对比。如果检索返回的内容里频繁出现“答非所问”先不要调模型回到切分环节查chunk里是不是混进了多个主题。重排是对Embedding检索结果的有力补充。常见做法是用bge-reranker-v2-m3对召回的前30条重新打分from FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-v2-m3, use_fp16True) candidates vectorstore.similarity_search(question, k30) scores reranker.compute_score( [(question, doc.page_content) for doc in candidates] )重排模型本身学习过“问题和文档的相关性”排序对专业术语的敏感度比向量相似度更高。注意重排得分不归一只用来排序不要硬套阈值。4.4 检索效果差时的排查路径检索效果差通常表现为三种情况返回内容完全不相关、相关信息散落在多个chunk里、返回内容相关但缺少关键细节。排查时按顺序做三步检查。第一步打印出检索到的原始页面片段确认question经过Embedding后是否抓住了核心实体。“高血压患者可以服用布洛芬吗”如果检索到的都是感冒用药先怀疑Embedding模型对“高血压”和“布洛芬”这两个实体关注不足应改用bge-m3这类在中文实体上表现更好的模型。第二步检查chunk的metadata。如果命中的chunk没有记录资料来源和章节即使内容相关大模型也无法给出可信的引用。第三步观察重排前后排序的差异。若重排后top1变化很大说明Embedding召回的候选质量本身不稳定优先扩充召回数量而不是调整阈值。5. 让答案可追溯相似度阈值兜底与20问评估集验证最后这章不再加新模块而是给前面搭好的系统加上“不知道时怎么办”和“效果怎么证明”两个关键约束这也是毕设答辩里最容易被追问的地方。5.1 相似度阈值兜底宁可拒答不可乱答检索返回的每条结果都有一个相似度分数在Chroma中可通过similarity_search_with_relevance_scores拿到。医疗场景必须在生成前检查这个分数低于阈值的直接拒答。阈值建议用一批真实问题跑出来而不是拍脑袋设0.5。results vectorstore.similarity_search_with_relevance_scores(question, k5) filtered [(doc, score) for doc, score in results if score 0.35] if not filtered: return 目前知识库中没有找到与您问题匹配的资料建议尽快咨询线下医生。 context \n\n.join( [f[资料{i}] {doc.page_content} for i, (doc, _) in enumerate(filtered, 1)] )阈值0.35怎么定准备一批“知识库内一定有答案”的问题和一批“知识库内没有答案”的问题分别计算最相近chunk的相似度分数正例的最低分和负例的最高分之间取中间值。实际操作中bge-m3在正常问答对上的相似度通常在0.5以上低于0.35的内容基本不可信。5.2 用20问测试集锁住系统回归建议在项目里建一个questions.json手工准备20条覆盖不同难度的医疗问答对格式如下[ { question: 高血压患者可以服用布洛芬吗, expected: 资料中布洛芬说明书提示高血压患者慎用需监测血压, source_ref: 布洛芬说明书-注意事项 }, { question: 甘草片和降压药能一起吃吗, expected: 资料未提及相互作用需咨询医生, source_ref: 院内药物手册-相互作用 } ]每次调整切分参数、换Embedding模型或改prompt后跑一遍这20个问题把回答按照下表打分评估维度评分标准得1分的情况忠实度回答是否完全来自资料出现资料中没有的药品剂量完整性是否覆盖资料中的关键信息只回答了适应症没提禁忌引用可溯回答能否定位到具体文档无法说明依据来自哪个章节拒答正确性资料不足时是否拒答知识库没有相关材料但仍作答打分时不看大模型的生成流畅度重点看“证据”链路是否完整。每轮迭代后记录总分数低于上一轮就回滚参数。5.3 矛盾资料的处理技巧医疗知识库里经常出现同一药物在不同指南里的建议不一致比如某药在旧版说明书标注“肝肾功能不全者禁用”在新版指南改为“减量慎用”。如果不处理大模型可能在一次回答里同时输出两个结论。处理办法是在prompt里增加一句“多份资料存在矛盾时优先采用版本日期最新的资料并在回答中注明信息冲突”。同时在资料metadata里保存来源文件的更新时间让检索结果可以按时间排序。一个更简单的兜底是把矛盾的chunk直接标记为“低优先级”生成时最后才考虑。本文还有配套的精品资源点击获取