资讯详情

资讯详情

Python医疗RAG问答系统实战:从知识库构建到检索生成全链路拆解

简介本资源为基于Python的RAG与大模型医疗问答系统完整实现面向计算机、人工智能、自动化等专业的高校师生及行业技术人员可作为毕业设计参考或教学研究案例。系统融合检索增强生成框架与前沿大语言模型覆盖知识图谱构建、命名实体识别、模型微调与Web交互等核心模块答辩环节获98分评价。压缩包共89个文件约94.7MB包含10个py脚本、7个ipynb实验笔记、7个json配置与数据文件、3个yaml训练配置、19个txt语料及18张png界面截图另附模型权重与备份文件结构清晰便于按模块查阅。已有99人学习下载。读者可从中获取完整的医疗问答技术方案、可运行的源码工程、微调与推理脚本、图谱构建流程及界面设计参考适合具备一定专业基础的用户进行功能扩展与二次开发。1. 从一份能跑通的医疗 RAG 源码说起它到底解决了什么问题医疗问答这个场景跟通用闲聊完全不是一回事。你问「感冒吃什么药」通用大模型能给你扯一堆但真到「二甲双胍和格列齐特能不能一起吃」这种问题它敢编你敢信吗这就是为什么基于 Python 的 RAG检索增强生成加大模型方案在医疗问答里几乎是标配——不是因为它时髦而是因为纯靠模型参数里那点记忆根本兜不住医学知识的严谨性和时效性。这份毕业设计源码包核心就是一套完整的医疗问答系统用 Python 把知识库构建、向量检索、大模型调用、前端交互串成一条链路。它适合两类人——正在找毕业设计选题、需要一份能讲清楚原理又能跑起来的学生以及想快速摸清 RAG 工程落地套路的开发者。你拿到的不只是几个 .py 文件而是一条从「医学文档进」到「自然语言答案出」的完整管道。下面我按实际拆包和复现的顺序把这条管道拆开讲。2. RAG 医疗问答的骨架知识库、检索器、生成器怎么串2.1 为什么医疗场景非 RAG 不可先把这个逻辑说透不然后面调参你都不知道在调什么。大模型的知识来自训练语料训练完就冻结了。医学指南每年更新药品说明书隔三差五修订模型不可能知道。更麻烦的是模型在不确定的时候会「自信地胡说」这在医疗场景是致命的。RAG 的思路很直接不让模型凭记忆答题而是先从可信知识库里检索出相关段落把原文塞进提示词让模型基于给定材料回答。这样答案有出处知识库更新了系统就更新了不用重新训练模型。医疗问答对「可追溯」的要求极高RAG 天然满足这一点。常见做法是把医学文档切成块用嵌入模型转成向量存进向量库查询时把问题也转成向量算相似度取 Top-K 段落。这里的关键参数是 chunk_size 和 top_k后面会细说。2.2 源码目录结构与模块职责拿到包先别急着跑花五分钟看清结构能省后面半小时的瞎找。典型目录长这样medical_rag_qa/ ├── data/ # 医学知识原始文档 │ ├── raw/ # 未处理的 txt/pdf │ └── processed/ # 切块后的 json ├── vectorstore/ # 向量库持久化目录 ├── src/ │ ├── config.py # 路径、模型名、超参配置 │ ├── document_loader.py # 文档加载与清洗 │ ├── text_splitter.py # 文本切块 │ ├── embedding.py # 向量化封装 │ ├── retriever.py # 检索逻辑 │ ├── llm_client.py # 大模型调用封装 │ └── qa_chain.py # 检索生成主流程 ├── app.py # 交互入口 ├── requirements.txt └── README.md这个划分的好处是每个环节可单独替换。比如你想把向量库从 FAISS 换成 Chroma只动 embedding.py 和 retriever.py想换大模型只动 llm_client.py。毕业设计答辩时老师问「你这系统哪部分是自己写的」你能指着模块说清楚比一坨代码强得多。2.3 环境搭建与依赖安装Python 版本建议 3.9 到 3.11太新太旧都可能踩依赖坑。先建虚拟环境别往全局环境里装# 创建虚拟环境 python -m venv venv # 激活Windows venv\Scripts\activate # 激活macOS/Linux source venv/bin/activate # 安装依赖 pip install -r requirements.txtrequirements.txt 里通常包含这些核心包langchain langchain-community faiss-cpu sentence-transformers openai pypdf streamlit这里有个血泪经验faiss-cpu 在 Windows 上偶尔装不上报编译错误。解决办法是先pip install faiss-cpu --no-cache-dir还不行就换 conda 装。另外 sentence-transformers 第一次运行会下载嵌入模型几百 MB网络不稳就卡住建议提前手动下载放到缓存目录。提示如果 requirements.txt 里锁了版本号别手贱去升级RAG 相关库版本兼容性很玄学能跑就别动。3. 知识库构建实战从医学文档到向量索引3.1 文档加载与清洗医疗知识来源可能是 txt、pdf 甚至网页导出。先统一转成纯文本去掉页眉页脚、乱码和多余空行。document_loader.py 的核心逻辑import os from pypdf import PdfReader def load_documents(raw_dir): 加载目录下所有文档返回 {文件名: 文本内容} 字典 docs {} for fname in os.listdir(raw_dir): path os.path.join(raw_dir, fname) if fname.endswith(.txt): with open(path, r, encodingutf-8) as f: docs[fname] f.read() elif fname.endswith(.pdf): reader PdfReader(path) # 逐页提取并拼接跳过空页 text \n.join( page.extract_text() for page in reader.pages if page.extract_text() ) docs[fname] text return docs逻辑说明txt 直接读pdf 用 pypdf 逐页提取。参数上注意 encoding 必须指定 utf-8否则中文文档会乱码。pdf 提取质量取决于原文件扫描件提取出来是空的这种情况得先做 OCR源码包一般不含 OCR 模块遇到扫描版说明书要自己补。清洗环节建议加一步正则把连续空行、页码残留去掉import re def clean_text(text): text re.sub(r\n{3,}, \n\n, text) # 多个空行压成一个 text re.sub(r第\s*\d\s*页, , text) # 去页码 text re.sub(r[ \t]{2,}, , text) # 多空格压一个 return text.strip()3.2 文本切块chunk_size 和 overlap 怎么定切块是 RAG 里最容易被忽视、又最影响效果的一步。切太大检索出来的段落包含无关信息模型容易被干扰切太小一个完整医学概念被切断检索到了也答不全。常见做法是按字符数切chunk_size 设 500 到 800overlap 设 50 到 100。overlap 的作用是让相邻块有重叠避免关键句正好卡在边界上被切碎。def split_text(text, chunk_size600, overlap80): 按字符切块带重叠 chunks [] start 0 while start len(text): end start chunk_size chunk text[start:end] if chunk.strip(): # 跳过纯空白块 chunks.append(chunk) start chunk_size - overlap # 前进时回退 overlap return chunks参数说明chunk_size600 是医疗文本的经验值一段药品说明或诊疗要点基本能装下。overlap80 保证边界语义连续。如果你处理的是结构化很强的内容比如一条条药品记录别用固定切块按记录切更合理。切完的块存成 json带上来源文件名方便后面答案溯源。3.3 向量化与索引持久化切好的块要转成向量。用 sentence-transformers 加载中文嵌入模型常见的是shibing624/text2vec-base-chinese或BAAI/bge-small-zh。代码from sentence_transformers import SentenceTransformer import faiss import numpy as np import pickle def build_index(chunks, model_nameBAAI/bge-small-zh): model SentenceTransformer(model_name) # 批量编码normalize 后内积等价余弦相似度 embeddings model.encode( chunks, normalize_embeddingsTrue, show_progress_barTrue ) embeddings np.array(embeddings).astype(float32) dim embeddings.shape[1] index faiss.IndexFlatIP(dim) # IP 内积 index.add(embeddings) # 持久化索引和原始文本 faiss.write_index(index, vectorstore/med.index) with open(vectorstore/chunks.pkl, wb) as f: pickle.dump(chunks, f) return index逻辑说明normalize_embeddingsTrue 把向量归一化这样内积就等于余弦相似度检索更准。IndexFlatIP 是精确检索数据量小的时候够用如果知识库上万条可以换 IndexIVFFlat 加速但要先训练索引。持久化分两部分——FAISS 索引存向量pickle 存原始文本块检索时靠下标对应。注意嵌入模型换了索引必须重建。不同模型的向量空间不通用混用会检索出乱七八糟的结果这是新手最容易翻车的地方。4. 检索与生成链路把问题变成有出处的答案4.1 相似度检索与 Top-K 选择检索环节就是把用户问题向量化去索引里找最相似的 K 个块。retriever.pyfrom sentence_transformers import SentenceTransformer import faiss import pickle import numpy as np class Retriever: def __init__(self, index_path, chunks_path, model_nameBAAI/bge-small-zh): self.model SentenceTransformer(model_name) self.index faiss.read_index(index_path) with open(chunks_path, rb) as f: self.chunks pickle.load(f) def search(self, query, top_k4): q_vec self.model.encode( [query], normalize_embeddingsTrue ).astype(float32) scores, indices self.index.search(q_vec, top_k) results [] for score, idx in zip(scores[0], indices[0]): if idx -1: # FAISS 找不到时返回 -1 continue results.append({ text: self.chunks[idx], score: float(score) }) return results参数说明top_k4 是医疗问答的常用值。太小可能漏掉关键信息太大则塞进太多噪声还会撑爆大模型上下文。score 是余弦相似度一般 0.5 以上算相关低于 0.3 基本是硬凑的可以在代码里加个阈值过滤避免把不相关段落喂给模型。4.2 提示词模板让模型基于材料回答检索到材料后要拼成提示词。医疗场景的提示词必须强调「只依据给定材料回答不确定就说不知道」否则模型又开始自由发挥。PROMPT_TEMPLATE 你是一个严谨的医疗问答助手。请仅根据下面提供的资料回答问题。 如果资料中没有相关信息直接回答根据现有资料无法回答不要编造。 【参考资料】 {context} 【问题】 {question} 【回答】 def build_prompt(question, retrieved): context \n\n.join( f[资料{i1}] {r[text]} for i, r in enumerate(retrieved) ) return PROMPT_TEMPLATE.format(contextcontext, questionquestion)逻辑说明把检索结果编号拼进 context方便模型引用也方便你后面做溯源展示。那句「不要编造」不是客套实测能明显降低幻觉率。参数上如果 top_k 调大了context 会变长注意别超过模型的上下文长度限制超了要么截断要么减 top_k。4.3 大模型调用与流式输出llm_client.py 封装模型调用。如果你用在线 API注意把 key 放环境变量别硬编码进源码——毕业设计代码是要交的泄露 key 是低级错误。import os from openai import OpenAI client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL) # 兼容本地部署的模型服务 ) def generate(prompt, streamTrue): resp client.chat.completions.create( modelyour-model-name, messages[{role: user, content: prompt}], temperature0.2, # 医疗场景要低温度减少随机性 streamstream ) if stream: for chunk in resp: delta chunk.choices[0].delta.content if delta: yield delta else: return resp.choices[0].message.content参数说明temperature0.2 是关键医疗问答要的是稳定和准确不是创意。base_url 留成环境变量方便你切换到本地部署的模型服务很多学校要求私有化部署这套封装不用改代码就能切。streamTrue 让答案逐字输出交互体验好很多。4.4 主流程串联与交互入口qa_chain.py 把检索和生成串起来def answer(question, retriever, top_k4): retrieved retriever.search(question, top_ktop_k) if not retrieved: return 未检索到相关资料。, [] prompt build_prompt(question, retrieved) answer_text .join(generate(prompt, streamFalse)) return answer_text, retrievedapp.py 用 streamlit 做个简单界面输入框加答案展示区再把检索到的资料折叠展示体现「有出处」。这套链路跑通一个能演示、能答辩的医疗问答系统就成型了。5. 避坑与排查那些让系统「看起来能跑但答得离谱」的问题5.1 检索结果全是无关内容现象问「高血压用药」检索出来的却是糖尿病段落相似度还都不低。原因多半是嵌入模型和知识库语言不匹配或者索引是用旧模型建的、查询用了新模型。也可能是文档清洗没做好噪声太多拉偏了向量。解决确认建索引和查询用的是同一个模型名检查清洗后的文本是否干净把 top_k 调大看前几条如果第一条就不相关基本是模型或数据问题不是检索逻辑问题。5.2 答案里出现资料中没有的药名现象模型回答里冒出一个检索材料里根本没有的药还说得头头是道。原因提示词约束不够强或者 temperature 太高模型开始自由发挥。也可能是 top_k 太小没检索到正确资料模型只能靠记忆补。解决把提示词里「不要编造」的约束写死temperature 降到 0.1 到 0.2适当增大 top_k。如果还不行在生成后加一步校验检查答案里的关键实体是否出现在检索材料中。5.3 中文乱码或切块切出半句话现象检索出来的段落开头结尾是断的或者显示成乱码。原因读取文件时没指定 utf-8 编码切块时没考虑句子边界硬按字符数切。解决所有文件读取强制 encodingutf-8切块前先按句号、换行做一次粗分尽量在句子边界切。如果对质量要求高用 langchain 的 RecursiveCharacterTextSplitter它会优先在段落、句子边界切。5.4 首次运行卡在下载模型现象跑 embedding 那步卡住不动或者报连接超时。原因sentence-transformers 首次使用要从远端拉模型权重。解决提前手动下载模型放到本地缓存目录代码里把 model_name 改成模型在本地的绝对路径。这样离线也能跑答辩现场不怕断网。5.5 换了知识库但答案没变现象往 data/raw 里加了新文档重新提问答案还是老的。原因只加了原始文档没重新跑切块和建索引向量库里还是旧数据。解决知识库更新后必须重跑「加载→切块→向量化→建索引」整条流水线。建议把这几步写成一个 build_knowledge_base.py 脚本一键重建别手动一步步来容易漏。6. 进阶技巧把检索质量再往上提一档基础链路跑通后你会发现有些问题答得还是不够准。这时候可以上几个进阶手段成本不高但效果明显。第一个是重排序rerank。向量检索是粗筛取回 top_k 后用一个交叉编码器对「问题-段落」逐对打分重新排序把最相关的顶上来。常见做法是用 bge-reranker 系列模型代码上就是在 retriever.search 之后加一层from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-base) def rerank(query, candidates, top_n3): pairs [[query, c[text]] for c in candidates] scores reranker.predict(pairs) ranked sorted( zip(candidates, scores), keylambda x: x[1], reverseTrue ) return [c for c, _ in ranked[:top_n]]逻辑说明先向量检索取 top_k10再重排序取 top_n3 喂给模型。这样既保证召回又保证精度。代价是多一次模型推理速度慢一点但医疗问答对准确性的要求值得这个开销。第二个是查询改写。用户问「吃了降压药头晕咋回事」直接检索可能效果一般。可以先让大模型把问题改写成几个更规范的检索查询分别检索再合并结果。这一步能明显提升召回率尤其是口语化提问。第三个是给答案加引用标注。生成时让模型在答案里标出引用了哪条资料前端展示时把对应原文高亮。这在答辩时是加分项体现系统的可追溯性。最后一个习惯每次改完参数别凭感觉说「好像好点了」。准备一组 20 到 30 条的测试问题覆盖常见病、用药、禁忌几类改完跑一遍记录准确率。我一般会把测试问题和期望答案存成 json写个小脚本自动跑分。从那以后我每次调完 chunk_size 或 top_k都强制走一遍这个测试集不然改着改着就不知道哪版好了。希望这套拆解帮到你把这份源码真正跑成自己的东西。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →