简介:基于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 的原因之一。
生成参数别照搬通用对话的配置。医疗答案要求确定性,我常用的三件套是 temperature=0.1、top_p=0.8、max_tokens=512。温度超过 0.3 后,同一个问题反复问两次,模型可能给出两种给药建议,这种不确定性在医疗场景里是不能接受的。参数这东西有点玄学,但温度调低是唯一一个所有医疗问答项目都适用的共识。
2.3 向量库与嵌入模型怎么选:中文医学场景的配置
嵌入模型选择直接决定召回效果。中文医学文本里有大量剂量、症状、药品名,直接用通用英文嵌入模型往往对不上。常见做法是选中文优化的嵌入模型,比如 BGE 系列和 text2vec 系列。bge-small-zh-v1.5 维度只有 512,占空间小,毕设规模足够;数据量大或对精度有要求,再上 bge-large-zh-v1.5。有一条经验:先定嵌入模型再建库,中途换嵌入模型,旧向量全部作废只能重建,别给自己留后悔药。
| 嵌入模型 | 维度 | 中文医学适用性 | 建议场景 |
|---|---|---|---|
| bge-small-zh-v1.5 | 512 | 好 | 毕设与中小知识库 |
| bge-large-zh-v1.5 | 1024 | 更好 | 数据量大、机器内存充足 |
| text2vec-base-chinese | 768 | 好 | 通用中文文本为主 |
向量库方面,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 + 简单前端或 Gradio |
Python 生态里 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, flags=re.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_size=500, chunk_overlap=100, separators=["\n\n", "\n", "。", ";", ",", " ", ""], keep_separator=False, ) chunks = splitter.split_text(clean_text) embedding_model = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", encode_kwargs={"batch_size": 32}, ) doc_list = [] # 这里把 chunks 包装成 Document 对象,并填充 metadata vectorstore = Chroma.from_documents( documents=doc_list, embedding=embedding_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, k=5) # Chroma 默认 L2 距离,分数越小越相似,1.0 是启动阈值 valid_docs = [(d, s) for d, s in docs if s < 1.0] context = "\n".join(f"[{i+1}] {doc.page_content}" for i, (doc, _) in enumerate(valid_docs)) answer = llm.invoke(prompt.format_messages(context=context, question=query)).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_4bit=True, bnb_4bit_compute_dtype=torch.float16, ) tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained( model_id, quantization_config=quant_config, device_map="auto", )device_map="auto" 让模型自动分布到可用设备,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), key=lambda x: x[0], reverse=True )[: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 个可检索的事实问题,人工写标准答案,分别计算检索 Recall@5 和答案准确率。考试题可以作为“泛化能力”参考保留,但不要作为主评估指标。答辩时把这个逻辑讲清楚,评委反而会认可你理解 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 没有记录、基线没有跑、评测集没有标。评估脚本应该从第一天就跟着主流程走,每调一次参数留一条记录,到答辩前再整理就是水到渠成的事。希望帮到你。
本文还有配套的精品资源,点击获取