
简介基于Python的RAG与大模型医疗问答系统是一套面向计算机、人工智能及相关专业毕业设计的高分完整实现。系统采用检索增强生成RAG与前沿大模型技术覆盖实体识别、知识图谱构建、模型微调与Web问答交互等核心模块所有功能均通过测试可运行性强。压缩包内共89个文件容量约94.7MB以源码脚本.py/.ipynb、配置文件.yaml/.json、数据集.txt/.csv以及界面截图.png/.jpg为主目录划分清晰便于直接运行与扩展改造。已有99人浏览学习。借助这一资源可获得完整的医疗问答系统源码、微调与推理脚本、实体识别与图谱构建数据、训练配置及可视化界面示例适合具备一定Python基础并希望深入RAG问答实践的读者参考。1. 医疗问答为什么要把 RAG 当底座先认清能力和边界基于Python的RAG与大模型医疗问答系统解决的是一个很尖锐的矛盾模型能说会道但医疗回答容不得编造。同样是“头晕挂什么科”通用大模型可能给出三种倾向性不同的回答。这套系统的思路是把医学知识装进外部检索库先检索、再增强、后生成让答案能指向具体原文。它最适合需要可解释、可更新、可控制的垂直问答场景也是毕业设计里性价比较高的实现路径。这篇笔记围绕毕设场景展开覆盖模型与向量库选型、最小Python代码、参数调整、医疗数据专属的坑以及答辩前怎么评估和展示。适合正在做医疗问答方向的毕设学生也适合想快速落地RAG原型的工程师。2. 把系统拆成四层模型选型、向量库与 RAG 流程的三处关键决策把系统拆开看真正影响成败的只有三件事基座模型怎么选向量库和嵌入模型怎么搭检索与生成之间怎么衔接。这三处定下来后面的代码只是把它们串起来。很多毕设翻车都不是代码写不出来而是前期选型没想清楚做到一半发现换了向量库要全部重建或者模型跑不动只能从头再来。2.1 医疗问答为什么非用 RAG 不可与微调路径的对比我刚接触这个方向时也犹豫过直接拿大模型微调不是更省事后来对比发现医疗问答用微调的性价比很低。微调的本质是把知识“背”进参数需要成千上万条高质量问答对而医学问答对的标注成本非常高——药名、剂量、科室错一个字都可能误导患者。更麻烦的是知识更新要重训今天新发布一份临床指南整个模型就要再来一轮。RAG 把知识放在外部库库换文档答案就跟着变不需要重训模型。在毕业设计答辩里这个特性特别好展示你现场往库里加一段新药品说明书再问一次之前答不上来的问题系统立刻能答而且答案能指出这句话出自哪篇文档。评委看到这种“可控的知识更新”比单纯看到模型答对几道题更有说服力。大模型微调不是不能做但在医疗问答这个场景里它更适合作为配套实验而不是主体方案。当然RAG 也有明显边界回答质量上限取决于召回质量。知识库里没有对应段落后面的模型再强也白搭。所以整套系统里最值得花时间的不是基座模型本身而是文档切片、查询改写和重排序这些容易被忽略的环节。这块想清楚后面代码顺序就不会乱。2.2 基座大模型怎么选API 兜底与本地离线的双模式基座模型选择上我建议做成双模式API 调用兜底关键演示走本地。API 方案的好处是效果稳定、部署零成本国内常见的商用或开源模型都能直接调用坏处是答辩现场网络说断就断评委一问“敏感医学数据能不能不出内网”只靠 API 的项目就显得单薄。本地方案优先看 7B 到 14B 量级的开源模型4bit 量化后 6 到 8GB 显存就能跑起来。我习惯把模型加载和问答封装成两个独立模块一个走 API一个走本地启动时用环境变量切换。日常调试用 API答辩演示用本地两不耽误。量化模型的知识记忆会比全量模型弱一些所以它的定位是“读文档的助手”而不是“背书的专家”这也是整个系统坚持走 RAG 的原因之一。生成参数别照搬通用对话的配置。医疗答案要求确定性我常用的三件套是 temperature0.1、top_p0.8、max_tokens512。温度超过 0.3 后同一个问题反复问两次模型可能给出两种给药建议这种不确定性在医疗场景里是不能接受的。参数这东西有点玄学但温度调低是唯一一个所有医疗问答项目都适用的共识。2.3 向量库与嵌入模型怎么选中文医学场景的配置嵌入模型选择直接决定召回效果。中文医学文本里有大量剂量、症状、药品名直接用通用英文嵌入模型往往对不上。常见做法是选中文优化的嵌入模型比如 BGE 系列和 text2vec 系列。bge-small-zh-v1.5 维度只有 512占空间小毕设规模足够数据量大或对精度有要求再上 bge-large-zh-v1.5。有一条经验先定嵌入模型再建库中途换嵌入模型旧向量全部作废只能重建别给自己留后悔药。嵌入模型维度中文医学适用性建议场景bge-small-zh-v1.5512好毕设与中小知识库bge-large-zh-v1.51024更好数据量大、机器内存充足text2vec-base-chinese768好通用中文文本为主向量库方面Chroma、FAISS、Milvus 是三个常见选项。Chroma 把数据持久化在本地目录适合毕设和原型FAISS 纯内存检索最快但重启后不保留数据要自己管落盘Milvus 是服务端架构支持按科室、药品等标量字段过滤适合数据量到几十万条以后。我的默认选择是 Chroma因为答辩时拷贝整个项目目录就能演示不依赖额外服务。向量库运行模式持久化适合阶段Chroma本地文件自动落盘毕设、原型FAISS内存索引手动追求检索速度Milvus服务端服务端管理生产环境、大库再强调一点向量库里每条文档的 metadata 一定要留来源字段至少包含文档名、标题、页码。后续答案输出要标注引用得从检索结果里拿原始出处这个字段越早设计越好。很多人前期只存了文本后期补引用信息要重新入库非常麻烦。2.4 完整数据流与 Python 技术栈清单完整数据流可以分成八个环节文档解析、文本清洗、分块切片、向量化入库、查询改写、向量召回、重排序、增强生成。前面四个环节是“建库”后面四个是“问答”。建库做得好不好直接决定问答效果的底线。组件职责常见选择文档解析从 PDF/Word/TXT 抽取文本pypdf、docx、BeautifulSoup向量存储保存嵌入向量并支持相似度检索Chroma 默认嵌入模型把文本转成向量BGE 中文系列流程编排串联各环节LangChain 或自封装 Pipeline大模型生成最终答案API 本地量化双模式交互界面演示与答辩FastAPI 简单前端或 GradioPython 生态里 LangChain 能省不少胶水代码但 LangChain 升级快、接口经常调整如果只用到核心功能自己封装也并不复杂。我建议把流程拆成函数而不是把代码全堆在 Jupyter 里。毕业设计要交源码和论文清晰的模块边界比炫技重要这也方便你在论文里画架构图。3. 用 Python 搭最小可运行的 RAG 流水线三段核心代码与参数设置下面三段代码按顺序执行就能得到一个最小可运行的 RAG 流水线。环境建议 Python 3.9 以上核心依赖是 langchain、langchain-community、pypdf、sentence-transformers、chromadb。安装时不要盲目追求最新版本遇到接口报错先看对应 release 的 breaking change 说明。3.1 加载医学文档从 PDF 到干净文本的清洗顺序第一步是把原始文档变成干净的纯文本。医学 PDF 来源杂常见问题有页眉页脚混入正文、英文药名被错误断行、半角全角标点混用。下面这段代码做了两件事抽取文本并在清洗前先保护剂量单位组合。# 从 PDF 里抽取文本并做基础清洗 from pypdf import PdfReader import re def extract_pdf_text(pdf_path: str) - str: reader PdfReader(pdf_path) pages [page.extract_text() or for page in reader.pages] raw \n.join(pages) # 去掉孤立行和多余空白但保留段落间的空行 lines [line.strip() for line in raw.splitlines() if len(line.strip()) 1] return \n.join(lines) def clean_medical_text(text: str) - str: # 先保护剂量/单位组合避免后续清洗误伤 text re.sub(r(\d(?:\.\d)?)\s?(mg|g|ml|iu|μg), r【\1\2】, text, flagsre.I) # 合并行内多余空格 text re.sub(r[ \t], , text) return text这段代码里的关键是正则顺序先保护剂量单位再处理空白。如果反过来先合并空格再匹配数字和单位遇到“1.5 m g”这种被错误断行的数据剂量信息就被拆得七零八落。顺序错了后面评估时你会看到各种离谱答案但又很难定位到清洗环节。扫描版 PDF 用 pypdf 抽取出来是空文本需要先接 OCR 或者换用可复制文本的 PDF这一步很容易翻车论文里一定要写清楚数据预处理的范围。3.2 分块切片与向量化入库chunk_size 和 overlap 怎么定切片是医疗 RAG 最容易出问题的环节。chunk_size 太大检索粒度粗一段文本里混了多个知识点太小又把“用法用量”这类完整信息切断。中文医学文本我一般从 500 字符起调最小不低于 300最大不超过 800同时搭配 10% 到 20% 的重叠。from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap100, separators[\n\n, \n, 。, , , , ], keep_separatorFalse, ) chunks splitter.split_text(clean_text) embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, encode_kwargs{batch_size: 32}, ) doc_list [] # 这里把 chunks 包装成 Document 对象并填充 metadata vectorstore Chroma.from_documents( documentsdoc_list, embeddingembedding_model, persist_directory./medical_rag_db, ) vectorstore.persist()separators 的顺序特意把“。”“”排在英文空格前面这是中文文本专用配置。默认的递归切分器是英文习惯遇到中文说明书经常在长句中间硬切。metadata 里至少放“来源文件名”和“标题”后续引用标注全靠这两个字段。doc_list 包装时需要给每段补上来源信息不要只塞一段纯文本。提示换嵌入模型或改 chunk_size 后向量库必须重建旧目录直接删除或另存备份别在同一目录上重复写入。3.3 检索与增强生成带引用编号的答案拼接检索这一步常见做法是用相似度搜索拿回 top_k 候选再拼进 prompt。下面这段代码把检索结果按引用编号排列要求模型回答时标注来源编号这样输出天然带证据链。from langchain_core.prompts import ChatPromptTemplate prompt ChatPromptTemplate.from_messages([ (system, 你是医疗问答助手。只依据资料回答资料中没有的信息回答“当前资料不足无法回答”。引用第n份资料时在句末标注[n]。), (human, 资料\n{context}\n\n问题{question}), ]) docs vectorstore.similarity_search_with_score(query, k5) # Chroma 默认 L2 距离分数越小越相似1.0 是启动阈值 valid_docs [(d, s) for d, s in docs if s 1.0] context \n.join(f[{i1}] {doc.page_content} for i, (doc, _) in enumerate(valid_docs)) answer llm.invoke(prompt.format_messages(contextcontext, questionquery)).content阈值 1.0 不是万能的它只是起步值。不同嵌入模型、不同文档风格分数分布差异很大。我习惯的做法是抽样 50 个问题记录正确召回的最低相似度再下浮 20% 作为阈值。top_k 先设 5粗召回阶段宁可多召回一些重排序阶段再收紧。3.4 显存不够怎么办本地量化模型的最小可用配置毕设机器常见的是 6GB 到 8GB 显存跑 7B 模型全量不现实4bit 量化是常用解法。下面这段代码用 transformers 和 BitsAndBytesConfig 加载量化模型显存占用大约 6 到 8GB。from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig import torch model_id Qwen/Qwen2.5-7B-Instruct # 可换成其它开源模型或本地目录 quant_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_compute_dtypetorch.float16, ) tokenizer AutoTokenizer.from_pretrained(model_id) model AutoModelForCausalLM.from_pretrained( model_id, quantization_configquant_config, device_mapauto, )device_mapauto 让模型自动分布到可用设备GPU 不够时 CPU 也会参与计算。首次加载和推理速度都会明显变慢答辩前一定先把模型进程拉起来预生成几个固定问题的答案避免现场等待。如果你只有 CPU 没有独显也能跑但对话速度会比较难受建议演示时控制提问节奏。4. 从“能答”到“像医疗系统”查询改写、重排序与拒答兜底一个医疗问答系统如果只会“检索能答的瞎编答不了的”那它只是一个 RAG demo不只是医疗系统。要让它在答辩时站得住必须把查询改写、重排序和拒答这三件事做进去它们共同决定系统在边界场景下的表现。4.1 查询改写把口语化长问题转成多条检索查询患者提问往往是一整句话“我妈妈最近总是心慌有时候还喘不上气她之前有高血压该挂什么科”直接拿这句话去检索向量相似度会把重点分散到“妈妈”“最近”“总是”这些词上。常见做法是先让大模型做查询改写注意是改写不是扩写。rewrite_prompt ( 把患者的提问改写成2到3条适合检索的短查询 只保留症状、药物、科室、检查等关键词禁止补充新信息。\n f原问题{question}\n输出格式每行一条 ) rewritten llm.invoke(rewrite_prompt).content.strip().split(\n)改写结果和原问题一起去召回结果合并去重。改写模型和生成模型可以是同一个但 prompt 必须分开因为检索侧要“极简”生成侧要“完整”。改写的风险是引入原本不存在的信息所以 prompt 里要强调“禁止补充新信息”。如果发现改写后检索效果反而变差用一个笨办法兜底只保留原问题召回的结果。4.2 重排序粗召回之后加一道交叉编码器闸门粗召回 top5 到 top20 里通常混着好几段语义相似但不相关的文本比如把“高血压”和“低血压”的段落同时召回。向量相似度是双塔结构问答两段文本各自编码再算相似度速度快但精度有限。重排序用交叉编码器把问题和文档拼成一个序列打分精度更高。from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-base) pairs [(query, doc.page_content) for doc in candidate_docs] scores reranker.predict(pairs) top_docs [doc for _, doc in sorted( zip(scores, candidate_docs), keylambda x: x[0], reverseTrue )[:3]]这里的 candidate_docs 是粗召回结果重排序只看前 20 条左右就够了不要对全库跑交叉编码器。医疗场景下重排序的收益非常明显尤其当库里同时存在“高血压患者慎用”和“高血压患者禁用”这类高度相似的句子时向量相似度区分不了交叉编码器能捕捉更多上下文差异。4.3 拒答与安全兜底医疗场景的“不知道”怎么回答拒答是医疗问答系统最容易漏的一环。很多演示只关心模型答得对不对没人关注它“不该答的时候能不能闭嘴”。实际上答辩评委很吃这一套资料不足时系统明确说不答比硬答一个错误答案更能体现工程意识。if not valid_docs: return 目前的知识库中没有与此问题相关的资料建议咨询线下医生。 if max(retrieval_score) threshold: return 检索到的资料相关度不足为避免误导暂不生成回答。threshold 的标定方法前文提过抽样 50 个问题记录正确召回的最低分数取它的 80% 作为阈值。医疗场景还要加一条免责声明但话术别太生硬像“以上内容仅供健康参考不能替代执业医师当面诊断”这种程度就够了。拒答不是把用户拒之门外而是把系统能力边界写清楚这是医疗问答和普通闲聊系统最大的区别。5. 医疗 RAG 专属避坑指南数据污染、幻觉与评估失真的 4 个现场医疗 RAG 的坑和通用 RAG 不是同一批。通用项目里跑通就能演示医疗项目里跑通只是开始——错误答案一旦涉及剂量和禁忌就不是扣分问题而是安全问题。以下 4 个现场是我在调试中反复遇到的真问题每一条都是按现象、原因、解决的顺序说清楚。5.1 切片把“用法用量”切碎答案漏掉关键半句现象用默认文本切分器处理药品说明书回答“用法用量”时漏掉“一次半片至一片”上下文被切断模型因此给出不完整的数量指示。原因药品说明书经常用“【用法用量】”这种标题结构默认切分器按字符长度硬切把小节标题和正文切到不同 chunk检索时只命中一半内容。解决切片前先按中文标题做结构化预切分。我习惯先按“【……】”标题拆段落再对超长段落按句号细分。metadata 里记录标题路径这样检索结果同时带标题与正文prompt 里上下文更完整。切分器参数要针对你实际的医学文档做一轮验证不要拿通用文档的默认值直接上。5.2 知识库里有冲突描述模型把“对”的错答案召回了现象知识库里既有“高血压患者慎用”也有“高血压患者禁用”两个说法来自不同来源或不同适用条件。模型把不相干那条召回了答案恰好与正确结论相反。原因向量召回只计算语义相似度不校验医学条件。两条文本在语义上高度接近冲突信息在向量空间里甚至可能比正确的条目更接近用户问题。解决metadata 记录适用条件和来源文档生成前对同一实体的冲突片段做简单比对重点检测句子中“禁/慎/忌/不可”这类否定与限制词发现冲突时提示“资料存在不同说法”并把两段原文都列出来。毕业设计做到这一步非常加分因为评委关注的就是这类真实业务风险而不是演示稿里的完美案例。5.3 清洗正则把剂量单位洗错数据污染是隐藏炸弹现象离线评估时发现系统对“1.5mg”相关问题的正确率特别低排查后发现原始文档是“1.5 m g”被错误断行清洗后变成了“1.5m g”剂量单位缺了一个字母。原因正则替换顺序不对先删了空格再匹配数字单位。医学数据里剂量单位被拆行是常态尤其是 PDF 导出文本m、g、m l 都可能被拆到不同行。解决清洗顺序必须是先保护、再清洗、最后还原。用正则先占位匹配数字加单位组合并替换成带占位符的形式常规清洗结束后再解除占位。这条属于血泪经验单看每一条清洗规则都对合在一起就出事最好写一个针对剂量的单元测试把“1.5mg”“1.5 m g”“1.5mg/次”这几个典型样例固定进去每次改清洗逻辑先跑一遍。5.4 评测题和知识库不对齐系统被考试题考糊了现象用医学考试题当评测集系统客观题正确率只有 30%导师一看就皱眉项目差点被否掉。原因考试题与知识库文本不对齐。考题往往需要推理甚至涉及知识库以外的医学常识。RAG 只负责从库里找答案库里面没有对应内容答不对是必然的。解决评测集要从库里出。从指南与说明书里挑 50 个可检索的事实问题人工写标准答案分别计算检索 Recall5 和答案准确率。考试题可以作为“泛化能力”参考保留但不要作为主评估指标。答辩时把这个逻辑讲清楚评委反而会认可你理解 RAG 的能力边界而不是觉得系统效果差。6. 毕业设计答辩视角用可量化的评估把项目做“扎实”毕业设计分数有一半在答辩答辩一半在评估。评委看一个医疗问答系统先看三件事有没有客观指标、答案有没有来源、能不能解释失败案例。这三点都做到项目就从“做了一个 demo”升级成“完成了一个系统”。6.1 用公开医疗 QA 数据跑基线对比客观题可以选用公开的中文医疗问答数据集比如 cMedQA2 这类常见的中文医疗问答集挑选题型为“事实检索”的问题把标准答案关键词做成规则匹配或人工判定。重点对比两组一组不带 RAG直接让大模型回答一组带 RAG。RAG 在事实类问题上的正确率提升就是你论文里最直观的一张图。6.2 人工评分忠实度、相关性、完整性三维表主观题部分找 2 到 3 个人分别评分取均值避免单人主观偏差。维度评分要点分值相关性回答是否对应问题主体1-5忠实度是否严格来自资料有无编造1-5完整性关键信息是否遗漏1-5忠实度是最重要的一档建议在总分里加权到 50%。答辩时拿出 10 条 badcase逐条说清楚是召回失败还是生成失败比报一个 90% 的准确率更让人信服。6.3 证据链与 agentic RAG 的扩展演示进阶设计可以给答案加“证据链”。检索结果带 metadata 来源生成时要求标注[n]输出后把[n]映射回原文名称与页码做成“答案—证据”对。答辩被追问“你怎么知道它答对了”直接点开证据链。如果想再往上升级可以演示一个 agentic RAG 场景首轮检索不到时系统自动改写查询再检索一轮甚至做一次知识库的二次检索这就是 Agent 流程在医疗问答里的自然延伸。GraphRAG、Ontology RAG 这类更结构化的知识组织方式也可以作为论文的后续方向提一句。我自己做这个方向时最后悔的是把评估脚本排在最后两周才补。前面调参数全靠肉眼感觉“差不多”等到真要写论文实验数据才发现 badcase 没有记录、基线没有跑、评测集没有标。评估脚本应该从第一天就跟着主流程走每调一次参数留一条记录到答辩前再整理就是水到渠成的事。希望帮到你。本文还有配套的精品资源点击获取