
简介基于Python语言实现的RAG与大模型医疗问答系统是面向计算机、人工智能及相关专业的高分毕业设计重点解决医疗场景下知识检索与智能问答的落地问题。压缩包共89个文件大小约94.7MB涵盖Python源码、Jupyter Notebook、JSON/YAML配置、文本数据、说明文档及界面截图等既包括数据预处理、NER实体识别、知识图谱构建也包含lora、ptuning、sft等微调脚本与WebUI登录注册模块。系统采用检索增强生成框架结合ChatGLM等大模型并实现nl2cypher自然语言查询转换所有功能均经过完整测试答辩获98分。目前已有99人学习下载读者可参照其RAG流程、微调配置与前后端集成方式进行功能扩展或算法改进对课程设计和工程研发都有实际参考价值。包内文件按功能分类存放包含数据集与备份文件便于对照复现和二次开发。1. 为什么医疗问答必须用RAG大模型幻觉会让毕设答辩当场翻车直接拿大模型做医疗问答是毕业设计里最容易“演示成功、追问失败”的方案。你问“高血压患者能服用布洛芬吗”模型给你一段措辞严谨的回答听起来完全可信但处方依据、药品禁忌、剂量范围全靠模型自己“脑补”——这在医疗场景里是不可接受的。RAG检索增强生成把外部医学知识库接进生成链路先检索出权威资料片段再让大模型基于这些片段作答答案有出处、可溯源。这套“基于Python的RAG与大模型医疗问答系统”正好把知识库构建、文本切分、Embedding、向量检索、Prompt约束、效果评测全链路串起来适合做毕设也更适合想真正落地一个rag知识库问答原型的开发者。下面按我自己的实现路径拆开讲。2. 系统链路设计与技术选型先画清楚数据流向再写代码2.1 RAG系统在医疗问答里的完整链路一个可演示的医疗问答RAG系统至少包含五个环节知识库导入、文本切分与向量化、向量存储、检索召回、增强生成。医疗场景的特殊性在于知识库通常是不规则文本——药品说明书、诊疗指南、医学教材摘录、问答对格式差异大专业术语密集对切分和检索的要求比通用场景高一个档次。我建议的链路顺序是原始文档 → 清洗为标准文本 → 按语义边界切分 → 逐块生成向量 → 写入向量库 → 用户提问时对query生成向量并召回 → 融合关键词检索结果 → 可选重排 → 拼装Prompt → 调用大模型生成带引用的回答。每一步都值得在毕设文档里单独写一节答辩时讲数据流比讲模型参数更让老师信服。链路设计上有一个容易被忽视的决策点检索召回和生成用到的模型是否拆开。常见做法是Embedding模型、重排模型、生成大模型三者分离。Embedding和重排可以完全本地化生成大模型根据预算选API或本地部署。这样做的理由是Embedding模型参数量小、CPU也能跑而生成模型如果也本地跑一个7B模型就要吃掉十几GB显存答辩现场环境不一定有GPU。2.2 向量库、Embedding与生成大模型的选型对比先列一张选型参照表后面讲参数时还会展开。组件推荐选项理由注意点向量库Chroma / FAISS毕设规模无需分布式Chroma自带持久化和metadata过滤Milvus适合十万级以上规模小项目有点重EmbeddingBGE-M3 / M3E中英文混合医学文本效果好支持中文专有名词text2vec在小规模医学语料上也可以但长文档略弱重排bge-reranker-v2-m3基于交叉编码器精度明显高于向量相似度只对top50以内的候选重排别全量跑生成大模型Qwen、DeepSeek、GLM的API医疗场景对中文术语理解要求高国产模型更稳用API注意按量付费和超时设置开发框架LangChain / LlamaIndex 或手动实现毕设建议手动实现核心检索环节方便讲原理框架封装太深答辩问到底层时容易卡壳选型理由说透一点Chroma最适合当前规模因为它把“向量化存储相似度检索metadata过滤”收在一个API里代码量少出图效果好。向量检索的本质是计算余弦相似度Chroma底层用HNSW索引不需要你手写最近邻搜索。BGE-M3是BAAI开源的Embedding模型支持中英文输出维度1024对“肌钙蛋白”“血管紧张素转化酶抑制剂”这类复合医学词能保留语义相关性。如果机器配置一般可以换M3E-base维度768速度更快代价是少量精度损失。生成模型这一层毕设思路是本地部署与API二选一。本地部署推荐Qwen2.5-7B-Instruct用vLLM或Ollama拉起来API则推荐DeepSeek或智谱GLM中文医学问答质量够用价格低。这里务必注意答辩现场网络不一定稳定API方案要提前准备好备用Key和超时重试机制。2.3 构建医疗知识库数据来源、版权风险与清洗思路知识库是RAG质量的天花板。常见的数据来源有公开的医学教材电子版、诊疗指南如高血压防治指南、药品说明书、公开的医学问答对。毕设场景建议收窄到一个垂直病种或一个科室比如只做心血管用药问答知识库控制在几百个文档片段效果更容易做扎实。泛泛地做一个“全科医疗问答”检索噪声会把你淹没。清洗时注意三类问题PDF复制出来的文本常有断行、全角半角混用、数字下标丢失药品说明书里“用法用量”和“不良反应”经常在同一段落里混杂指南里的推荐等级如Ⅰ类推荐和证据等级如A级需要保留这些是检索时的重要线索。清洗后统一转成UTF-8纯文本或CSV每一条记录带“来源”“标题”“正文”字段为后面做metadata过滤做准备。版权处理上毕业设计不是商用但仍建议只保留少量公开资料用于演示完整数据源在论文里注明出处。不要整本复制受版权保护的教材这个问题一旦被答辩老师追问会很尴尬。2.4 最小环境与依赖清单Python环境建议3.10及以上直接按下面文件安装依赖。用国内镜像源能省大量时间。pip install chromadb bge-m3 sentence-transformers langchain-openai --index-url https://pypi.tuna.tsinghua.edu.cn/simple依赖说明chromadb负责向量存储与检索bge-m3是Embedding模型库sentence-transformers用于加载Embedding模型langchain-openai用来接OpenAI兼容的API服务也可以直接requests调用看个人习惯。这里没有把LangChain全家桶装进来核心逻辑自己写避免黑匣子。如果新机器还没装Python先到python官网下一个3.10.x安装包安装时勾选“Add Python to PATH”不然命令行敲python没反应——这是最基础也最常见的翻车点。3. 医疗语料切分与向量化固定字数切分是第一个大坑3.1 为什么医疗文本不能按固定字数硬切很多人第一步就把文档按512字定长切片这是RAG系统里最容易埋雷的操作。医疗文本和新闻文本不一样一个完整的概念常常跨片段出现“急性心肌梗死”如果被切成“急性心肌”和“梗死”两块Embedding后两块向量都没有完整语义检索召回时模型找不到整段描述。更隐蔽的问题是切片边界可能落在“禁忌症”的冒号后面导致生成阶段拿到的上下文缺少关键的否定信息——比如把“不适用于孕妇”切掉模型就敢建议孕妇用药了。正确的切分要同时考虑三个维度语义完整性、重叠窗口、医学实体保护。语义完整性指尽量在句号、分号、段落边界处切断重叠窗口保证切在前一句末尾的内容在后一片段开头再次出现防止信息被切缝吞掉医学实体保护指在切分后检查片段是否包含完整的医学术语发现问题时把切分点向离术语更远的标点偏移。3.2 带重叠窗口的递归切分实现下面的代码用递归思路切分优先按段落切段落太长再按句子切句子还超长才硬切。每个片段保留固定重叠长度。import re def split_text_with_overlap(text, max_len512, overlap80): 按语义边界递归切分带重叠窗口。 优先级段落 句子 硬切。 text re.sub(r\s, , text).strip() if len(text) max_len: return [text] chunks [] paragraphs re.split(r[\n\r], text) current for para in paragraphs: if len(current) len(para) 1 max_len: current para else: if current: chunks.append(current.strip()) current para if len(current) max_len: # 段落超长按句子切 sentences re.split(r(?[。;]), current) buf for sent in sentences: if len(buf) len(sent) max_len - overlap: buf sent else: if buf: chunks.append(buf.strip()) buf sent current if current: chunks.append(current.strip()) # 对长chunk硬切并补重叠 final [] for c in chunks: if len(c) max_len: final.append(c) else: start 0 while start len(c): end start max_len piece c[start:end] final.append(piece) start end - overlap return final切分逻辑说明函数先做空白字符归一化把全角空格、换行统一处理。外层用正则按段落切分保证“药品名称”“禁忌”“用法”这类按段落组织的知识保持完整。段落过长时进入句子层按中文句号、分号、英文分号切切出来的片段再被重叠机制覆盖。max_len512指的是字符数而不是token数对中文编码场景更直观overlap80保证每两个相邻片段有约80字的重叠区域。硬切兜底的部分用滑动窗口结束位置减去overlap是为了让后一片段能看到前一片段的尾部信息防止边界的术语被切断。3.3 向量化与写入向量库分片提交与metadata设计切好的片段统一用BGE-M3生成向量写入Chroma时把来源文档名和章节标题作为metadata一并存进去为后续过滤检索结果做准备。from chromadb import PersistentClient from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-m3) client PersistentClient(path./medical_rag_db) collection client.get_or_create_collection( namemedical_kb, metadata{hnsw:space: cosine}, ) def embed_and_store(chunks, source_name): vectors model.encode(chunks, normalize_embeddingsTrue).tolist() ids [f{source_name}_{i} for i in range(len(chunks))] metadatas [{source: source_name, chunk_index: i} for i in range(len(chunks))] collection.add( idsids, documentschunks, embeddingsvectors, metadatasmetadatas, )参数说明PersistentClient(path./medical_rag_db)创建本地持久化目录第二次运行不需要重新建库hnsw:spacecosine指定余弦距离作为相似度度量对文本向量是标准选择比内积更稳定SentenceTransformer(BAAI/bge-m3)会加载预训练模型权重首次运行需要联网下载大约占2GB磁盘提前下好模型权重再离线跑。批量向量化的规模控制在200条一批一次全量编码容易把内存打爆一个几千片段的医疗知识库分段提交基本几十秒跑完。这里有个细节容易被忽略normalize_embeddingsTrue让所有向量模长为1余弦相似度退化为点积检索速度更快而且和Chroma的余弦度量语义一致。不归一化虽然也能跑但相似度分数会受向量模长干扰检索排序不稳定。3.4 检索效果快速自检写一条query看召回分数向量库建好之后先别急着接大模型做一个检索冒烟测试。query 高血压患者可以服用布洛芬吗 q_vec model.encode([query], normalize_embeddingsTrue).tolist() results collection.query( query_embeddingsq_vec, n_results5, include[documents, metadatas, distances], ) for doc, meta, dist in zip(results[documents][0], results[metadatas][0], results[distances][0]): print(dist, meta[source], doc[:100])这一步的价值在提前暴露问题。如果top5里出现不相关的碎片大概率是切分参数或embedding选型有问题而不是生成层的问题。我习惯把这一步叫“检索层验收”检索不过关后面生成层再改prompt都是治标不治本。4. 召回、重排与生成把准确率从“能用”拉到“能答辩”4.1 混合检索纯向量召回扛不住医学缩写与口语化提问医疗问答场景里用户提问往往口语化知识库里的描述偏书面化。“高血压”和“hypertension”算好处理的真正棘手的是“COPD”“心梗”“阿斯匹林”这类缩写、俗称、错别字。向量召回对同义词有一定容错但对缩写和错别字很敏感。解决思路是混合检索向量召回负责语义相关BM25关键词召回负责精确匹配最后做结果融合。import jieba from rank_bm25 import BM25Okapi def build_bm25_index(chunks): tokenized [list(jieba.cut(c)) for c in chunks] return BM25Okapi(tokenized) def hybrid_search(query, chunks, bm25, collection, top_k10): # 向量召回 q_vec model.encode([query], normalize_embeddingsTrue).tolist() vec_results collection.query(query_embeddingsq_vec, n_resultstop_k) vec_hits {doc: score for doc, score in zip( vec_results[documents][0], vec_results[distances][0])} # 关键词召回 tokenized_q list(jieba.cut(query)) bm25_scores bm25.get_scores(tokenized_q) bm25_hits {chunks[i]: bm25_scores[i] for i in range(len(chunks))} # RRF融合两路结果按排名加权兼顾相关性与多样性 from collections import defaultdict rrf_score defaultdict(float) for rank, doc in enumerate(vec_results[documents][0]): rrf_score[doc] 1 / (60 rank) for rank, doc in sorted(bm25_hits.items(), keylambda x: -x[1])[:top_k]: rrf_score[doc] 1 / (60 rank) ranked sorted(rrf_score.items(), keylambda x: -x[1])[:top_k] return [doc for doc, _ in ranked]实现逻辑拆开讲jieba.cut做中文分词BM25索引在系统启动时构建一次即可不需要每次查询重建向量召回拿余弦距离当作分数距离越小越相关两路结果用RRFReciprocal Rank Fusion融合核心思想是“两路都排在前面的文档优先输出单路排名很高但另一路没召回的文档次之”。RRF公式里的常数60是经验值调大对单路高排名结果更宽容调小更强调两路共识。融合后得到候选集传给重排模型。4.2 重排用交叉编码器把真正相关的片段顶到前面向量召回和BM25都属于双塔结构的粗排速度快但对细粒度语义关系不够敏感。医学问答场景中“高血压患者禁用XX”与“高血压患者慎用XX”字面相近、含义相反向量相似度可能都很高。重排阶段用bge-reranker-v2-m3这类交叉编码器对候选片段逐对计算相关分精度更高代价是速度慢——所以只对top50以内的候选重排。from FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-v2-m3, use_fp16True) def rerank(query, candidates, top_n5): pairs [[query, doc] for doc in candidates] scores reranker.compute_score(pairs) scored sorted(zip(candidates, scores), keylambda x: -x[1]) return [doc for doc, _ in scored[:top_n]]use_fp16True让重排模型以半精度推理速度翻倍但需要GPU支持如果纯CPU环境去掉这个参数。重排模型的分数不是概率不用纠结具体数值只需要关注排序。经验上top5里如果混入了明显不相关的片段先回头查切分参数不要怀疑重排模型。4.3 生成Prompt把“不能编”写进系统指令里生成阶段是RAG系统的最后一公里Prompt设计直接决定输出是否可信。医疗问答的Prompt有三个必写约束只准使用给定的上下文片段作答上下文里没有的内容明确说“知识库中未收录”禁止给出自创的治疗方案或药品剂量。你是一位医疗知识库问答助手。请严格基于下面的参考资料回答用户问题。 回答规则 1. 只允许使用参考资料中出现的信息不得补充外部知识或自行推断。 2. 如果参考资料中没有相关信息请直接回答“当前知识库中未找到相关答案”。 3. 涉及药品用法、用量、禁忌时必须原文引用参考资料中的表述。 4. 回答末尾标注引用的资料片段编号格式为[来源: 片段编号]。 参考资料 {context} 用户问题{query} 回答{context}是重排后选出的top_n片段按编号拼接{query}是用户原始问题。温度参数在调用大模型时设为0.2左右太低会显得机械太高会增加编造风险。还要注意有些大模型API默认不保证输出稳定设置temperature0.2并关闭top_p随机采样能显著减少同一问题两次回答不一致的现象。4.4 生成层接入大模型OpenAI兼容API是通用接法现在市面上绝大多数大模型API都兼容OpenAI格式用同一套代码可以切换不同模型服务商。下面代码同时支持在线API和本地部署的兼容服务。from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlhttps://your-endpoint, # 本地vLLM服务填 http://localhost:8000/v1 ) def generate_answer(query, context_chunks): context \n\n.join( f[来源: {i1}] {chunk} for i, chunk in enumerate(context_chunks) ) prompt f你是一位医疗知识库问答助手。请严格基于下面的参考资料回答用户问题。 回答规则 1. 只允许使用参考资料中出现的信息不得补充外部知识或自行推断。 2. 如果参考资料中没有相关信息请直接回答“当前知识库中未找到相关答案”。 3. 涉及药品用法、用量、禁忌时必须原文引用参考资料中的表述。 4. 回答末尾标注引用的资料片段编号格式为[来源: 片段编号]。 参考资料 {context} 用户问题{query} 回答 resp client.chat.completions.create( modelqwen2.5-7b-instruct, messages[{role: user, content: prompt}], temperature0.2, max_tokens512, ) return resp.choices[0].message.content接入逻辑说明base_url指向兼容OpenAI的服务端点。如果用Ollama本地部署地址是http://localhost:11434/v1如果用DeepSeek或智谱官方API替换成各自平台的endpoint和key就行。模块化设计的好处是答辩演示时能现场切换模型供应商老师问“你们系统能换模型吗”现场演示换一个本地模型再答一遍加分效果明显。5. 医疗问答系统的5个常见坑检索异常、切分丢词与模型乱答5.1 向量检索召回一堆不相关内容分数还很接近现象query是“高血压用药注意事项”召回结果里混入“高血压的诊断标准”和“糖尿病的饮食建议”相似度分数差别不大。原因chunk_size设得太大一个片段里包含多个主题向量被平均成“四不像”另一个常见原因是知识库文档没有按章节拆分整章甚至整篇文档被当成一个片段入库。解决把max_len从512调小到256或384强制片段主题更聚焦切分前先按文档结构目录、章节标题、一级标题拆分再对每个章节做二次切分检查Embedding模型是否适合中英混合如果语料里缩写很多可以考虑换成针对医学语料微调过的向量模型。5.2 问“高血压能吃什么药”召回里全是“诊断标准”现象query意图是寻求用药建议但知识库里相关文档是“药物治疗”小节召回结果排在第一位的是“诊断标准”。原因用户口语表达与知识库书面表述存在词汇鸿沟。“能吃什么药”和“药物治疗”字面没有交集向量语义也没有把这两个概念拉近鞠一个模型没有对query做意图改写。解决在检索前加一步query改写用大模型把口语转成知识库风格的书面查询例如“高血压能吃什么药”改写为“高血压的药物治疗方案”。改写逻辑单独封装成一个函数方便在消融实验里对比效果这也是毕设论文里一个不错的创新点。5.3 大模型照着上下文回答了但引用了不存在的片段编号现象回答末尾标注了“[来源: 3]”但拼接的context里根本没有第3个片段。原因提示词里要求模型标注来源但小模型有时为了“完成格式”编造编号属于生成阶段的幻觉残留。解决生成后做一次引用校验解析回答里的来源编号逐一检查是否在合法范围内。校验失败的答案统一降级为不显示引用标注。这个后处理逻辑代码量不大但能让演示结果可信度提升一截。5.4 首次运行要下载模型答辩现场断网直接卡死现象现场演示时BGE-M3或bge-reranker权重没有提前缓存代码运行到加载模型时卡住最终报超时。原因SentenceTransformer首次运行会从HuggingFace拉取权重现场没有外网或网络极慢。解决提前把模型权重下载到本地目录代码里通过model_path参数指定本地路径加载带一个离线备用的Embedding模型答辩前至少做一次完整的“冷启动流程演练”确认从零开始到出结果不需要访问外网。5.5 本地模型生成中文回答时出现大量重复叠词现象本地7B模型回答时反复输出“高血压患者高血压患者高血压患者”或者整段重复同一句话。原因模型量化精度过低如4bit量化温度参数设置过高对话长度超限。医疗长文本场景下模型容易陷入重复循环。解决把temperature降到0.2以下设置repetition_penalty为1.1到1.3限制max_tokens不超过512如果问题依旧换更高精度的量化方案GGUF Q5或Q8或改用API模型。这个坑在本地部署7B模型时几乎必踩提前准备好“降级到API”的后备方案最稳妥。6. 把答辩从“演示聊天”拉高到“系统验证”评测集与可复现测试毕设答辩时老师最常问的一句话是“你这个系统准确率多少”。如果只演示两三个成功案例这题基本答不上来。正确做法是搭建一个可重复的离线评测流程把检索质量和生成质量分别量化。我实践的评测集构建方法从知识库中抽取30条知识片段每片段手工编写1-2个问题并标注该片段是标准答案的“golden chunk”。问题类型覆盖三类事实型“高血压的诊断标准是什么”、用药型“美托洛尔常见不良反应有哪些”、禁忌型“哪些患者禁用ACEI类药物”。评测集保存成JSON文件每次系统改动后跑一遍回归测试。检索质量看两个数字Recall5——golden chunk出现在前5个召回片段里的比例MRR——golden chunk在排序中的倒数排名均值。生成质量看忠实度我常用一个简化版判断让大模型回答然后人工检查回答里的关键实体药名、疾病名、剂量是否全部能在召回片段里找到任一关键实体没有原文出处就算“不忠实”。30条问题的评测集人工检查半小时能跑完。实际跑下来最常见的分布是检索召回有2-3条不理想生成阶段有1-2条存在“间接推断”。每一个失败case都值得写在论文附录里做案例分析这部分是让答辩分数拉开差距的关键。我还习惯做一个对比实验同一批问题分别用“纯大模型回答”和“RAG回答”把结果打印出来对比。医疗场景下纯大模型回答的错误率会明显更高这个对比结果是论证RAG价值最直观的材料。如果你想让这个毕设有更深的延展性可以在系统里加一层“可溯源的引用高亮”——把回答中引用的片段编号映射回原始文档位置前端点击即可跳转查看原文。另一个方向是把Naive RAG升级成Agentic RAG让模型在知识库检索不到时自主决定是否改写query重新检索或者改用GraphRAG构建医学概念之间的关系图。这些方向代码改动量不大但论文的创新点描述空间会大很多。最后说个我自己的习惯每次改动切分参数或Embedding模型之后我会把同一组评测问题重跑一遍把所有失败case整理成一个文本文件放在项目目录里命名成case_study.md。这些记录既是排错依据也是论文里的分析素材。答辩前一天的最后一件事不是调代码而是把评测集的对比表格打印出来装订好——老师看到你有量化验证过程提问重心就会从“是不是抄的”转向“有没有做大做深的潜力”。希望帮到你。本文还有配套的精品资源点击获取