我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

基于RAG与ChatGLM的医疗问答系统:从CSV知识库到Docker部署实战

基于RAG与ChatGLM的医疗问答系统:从CSV知识库到Docker部署实战 简介基于RAG与大模型技术的Python医疗问答系统完整毕设项目面向计算机相关专业学生、教师及科研人员覆盖AI、通信、自动化、电子信息、物联网等方向适用于毕业设计、课程设计、作业提交及初期项目演示。压缩包共1667个文件整体约140.19MB其中513个ts与355个tsx构成前端交互界面225个py实现RAG检索、大模型调用与后端服务逻辑156个md和76个mdx提供项目文档与说明另有JSON配置文件、pickle模型数据、Dockerfile/Shell脚本等结构清晰便于按模块查阅和二次开发。内含详细设计报告docx、LangChain-ChatGLM相关CSV数据、前端页面源码、后端服务代码以及完整的Docker部署配置与远程技术支持读者既能直接复现医疗问答全流程又能在源码基础上扩展功能深入理解RAG与大模型在垂直领域应用的具体工程实现。目前已有56人学习浏览适合需要完整毕设参考、进阶学习及动手实践的学生与开发者。1. 基于RAG与大模型的医疗问答系统为什么这个毕设先吃数据而不是先堆模型用RAG和大模型技术做医疗问答系统很多人第一次接触时会把注意力全放在ChatGLM的模型参数上真正决定系统能不能用的往往是一份看起来不起眼的CSV文件。这个项目包里的langchain-ChatGLM_open.csv和langchain-ChatGLM_closed.csv承载了问答系统的主要语料一个负责开放场景的知识覆盖一个负责封闭场景的精准回答。两份数据经过清洗、切块、向量化配合LangChain的检索链路最终由大模型生成用户能直接读的答案。项目自带完整源码与详细设计报告运行稳定、便于复现适合AI、通信工程、自动化、电子信息、物联网等方向的毕业设计和课程设计也适合想真正上手RAG的Python开发者。下面从数据文件、部署步骤到常见踩坑按实操顺序完整拆一遍。2. 核心链路拆解ChatGLM、LangChain与两份CSV的真实分工2.1 先搞清open.csv与closed.csv的数据边界这个资源包最容易被低估的文件就是langchain-ChatGLM_open.csv和langchain-ChatGLM_closed.csv。很多人下载后第一件事就是找模型加载代码实际上项目的关键在数据组织方式上。两份文件都是问答对但定位完全不同。open表示开放域覆盖常见病、多发病、用药和检查指标这类通用问题数据量大、覆盖面广适合回答“感冒反复发烧怎么处理”这种日常场景。closed表示封闭域通常是经过整理的专科问答比如心内科、内分泌科、术后护理这类定向内容答案更规范也更适合作为测试集和演示场景。实际使用中这两份文件不应直接拼接。常见做法是先分别读取检查字段是否一致再按项目需要的字段做去重和空值处理。如果字段不一致就强行concat后面检索返回的上下文会错得离谱。这是一个很经典的隐性坑第4章专门给了排查流程。这里给一个读取并检查字段的脚本判断数据编码和列名是否对齐。因为判断一个下载来的CSV能不能直接进入RAG流程第一步永远是列名和编码而不是检索和生成。import pandas as pd def try_read_csv(file_path): 依次尝试常见编码返回读取成功的DataFrame。 for enc in [utf-8-sig, utf-8, gbk, gb18030]: try: df pd.read_csv(file_path, encodingenc) print(f{file_path} - {enc} 读取成功) return df except UnicodeDecodeError: continue raise ValueError(f{file_path} 所有编码均失败) df_open try_read_csv(langchain-ChatGLM_open.csv) df_closed try_read_csv(langchain-ChatGLM_closed.csv) print(open列, df_open.columns.tolist()) print(closed列, df_closed.columns.tolist()) print(df_open.head(3).to_string())这段代码的实际意义是把“能不能读”这个问题前置。utf-8-sig会正确处理带BOM的文件gb18030能覆盖中文环境的绝大多数来源文件。打印列名之后你要做的第一件事就是对比这两份数据有没有公共字段比如question、answer、department、source。如果没有就要在合并前先写一个列重命名逻辑否则后面切块、向量化全部白做。2.2 医疗问答的RAG链路切块、向量化、检索与生成整个系统的链路可以拆成四个环节语料切块、向量化入库、检索召回、大模型生成。其中切块是最容易被新手跳过、但最决定效果上限的环节。医疗答案经常是一整段连续文本包含“主诉—诊断—用药—注意事项”的完整因果链。如果按固定200字符硬切一个完整答案会被切成好几个碎块检索时只召回其中一块模型拿到的上下文缺失了前半段生成结果自然答非所问。解决办法是按段落边界切块并让相邻块保留50到100字符的重叠。import pandas as pd from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS def split_text(text, chunk_size300, overlap50): 按长度切块保留重叠窗口避免切碎语义。 if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) start end - overlap return chunks df_open pd.read_csv(langchain-ChatGLM_open.csv, encodingutf-8-sig) df_closed pd.read_csv(langchain-ChatGLM_closed.csv, encodingutf-8-sig) df pd.concat([df_open, df_closed], ignore_indexTrue) chunk_list [] for answer in df[answer].dropna(): chunk_list.extend(split_text(answer)) embeddings HuggingFaceEmbeddings(model_nameshibing624/text2vec-base-chinese) vectorstore FAISS.from_texts(chunk_list, embeddings) vectorstore.save_local(./medical_faiss_index)逻辑说明split_text函数先按长度切分再通过overlap参数让相邻块之间保留一部分重复文本这样模型在召回时不会丢掉段落的起承转合。chunk_size放300左右比较合适太小容易割断语义太大又会让向量检索精度下降因为余弦相似度对长文本会趋向平均。overlap取50到100足够覆盖句子的衔接部分。参数说明中有个容易被忽略的地方HuggingFaceEmbeddings默认使用text2vec-base-chinese模型对中文医疗问句的效果稳定而且不需要联网申请API完全本地运行。如果后续换成了OpenAI或其他厂商的embedding接口还要注意batch_size和max_seq_length的取值长文本会被截断从而明显拉低召回率。向量库选的是FAISS而不是Chroma或Milvus原因很简单毕设和课程设计场景下FAISS不需要单独起服务本地文件即可持久化save_local就能把索引存到磁盘。Chroma在LangChain里集成也方便但多一个后台进程就多一个排查点。等系统规模大了再迁移到Milvus也不迟。提示检索时用户输入会先向量化再在FAISS索引里做相似度检索取出top_k个文档块。top_k这个参数直接决定生成质量设太小召回内容不够设太大则引入噪声。医疗场景我一般取3到5。2.3 源码包与设计报告文件清单和边界资源包内文件不算多但每一项都有明确用途整理成一张清单文件用途关键点langchain-ChatGLM_open.csv开放域医疗问答语料覆盖常见病症、用药、检验指标langchain-ChatGLM_closed.csv封闭域专科问答语料适合评测与演示答案更可控Dockerfile构建服务镜像统一Python与大模型依赖环境.dockerignore控制镜像内文件范围排除本地缓存、日志和无关目录详细设计报告.docx设计与答辩支撑技术选型、模块划分、测试方案源码目录主程序与数据处理脚本问答链路、配置、启动入口对毕业设计来说设计报告往往是拉分项。很多学生代码能跑但文档写不出来这份docx基本覆盖了答辩会被问到的问题为什么选RAG而不是纯微调、为什么用CSV做知识库而不是直接灌给模型、检索失败时怎么兜底。你可以拿它当框架来改写不要直接提交因为答辩老师会追问里面的细节。有一点边界要明确这是一个教学和演示性质的项目不是完整的临床辅助诊断系统。它没有经过医院真实数据验证也没有过敏史、既往史、药品相互作用的实体识别所以不要在真实诊疗场景里直接依赖它的输出。它的价值在于把RAG链路完整跑通把“数据怎么加工、模型怎么调用、接口怎么暴露”这条主线讲清楚。3. 环境准备与Docker化部署跑通ChatGLM的最短路径3.1 为什么必须用Docker固定大模型环境手动部署ChatGLM类项目的痛点几乎都集中在环境上先装PyTorch再对着CUDA版本挑torch版本然后装transformers、accelerate、langchain最后还要处理protobuf和tokenizers的兼容问题。这些依赖彼此锁版本一次装错就是半天以上。项目自带Dockerfile说明作者已经考虑到了这个问题。Docker把Python版本、CUDA驱动、依赖包都锁在同一个镜像里换机器只需要重新build一次。我拿到这个资源包后没有在宿主机上直接配环境而是优先看Dockerfile和requirements.txt这个顺序能少走很多弯路。3.2 构建镜像与启动服务Dockerfile逐行说明先看一份典型Dockerfile的内容对应本项目常见的运行方式FROM python:3.10-slim WORKDIR /app RUN apt-get update apt-get install -y --no-install-recommends gcc \ rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . EXPOSE 8000 CMD [python, main.py]逐行解释FROM python:3.10-slim选的是轻量级Python基础镜像比ubuntu完整版小很多构建和传输都更快。RUN里装gcc是因为一部分依赖包在安装时需要现场编译比如tokenizers和pydantic的相关扩展不装gcc会在构建阶段报编译错误。pip install指定清华镜像源是为了国内网络环境如果你的机器不在国内或习惯用其他镜像可以把地址替换掉。EXPOSE只是声明容器内端口真正让外部访问要靠run命令里的-p映射。启动命令如下docker build -t medical-rag:latest . docker run -d --name medical-rag --gpus all -p 8000:8000 medical-rag:latest docker logs -f medical-rag参数说明-t给镜像命名最后一个.表示使用当前目录下的Dockerfile。run的-d表示后台启动--name指定容器名--gpus all让容器能访问宿主机GPU这行只有在显卡和nvidia-container-toolkit都就绪时才能用。如果环境没有GPU就删掉--gpus all换成CPU镜像或降级模型方案。最后的-p 8000:8000把容器内8000端口暴露到宿主机。启动后不要马上调接口先看日志。日志里出现“Running on http://0.0.0.0:8000”才算真正起来。如果进程运行几秒就退出大概率是模型路径不对或显存不足可以翻第4章的排错方法。3.3 没有GPU怎么办CPU推理与量化降级医疗问答的毕设环境不一定有显卡。这里以ChatGLM系列6B模型为例float16下加载需要大约12到14GB显存很多学生机器只有6到8GB。降级方案有三种按代价从低到高模型量化、换小模型、纯CPU推理。量化是最省事的在加载模型时加load_in_4bitTrue可以显著压缩显存占用from transformers import AutoModel, AutoTokenizer tokenizer AutoTokenizer.from_pretrained( THUDM/chatglm3-6b, trust_remote_codeTrue ) model AutoModel.from_pretrained( THUDM/chatglm3-6b, load_in_4bitTrue, device_mapauto, trust_remote_codeTrue )解释一下load_in_4bit这是transformers结合bitsandbytes提供的量化加载方式把模型权重量化到4bit显存占用能从12GB以上降到6GB左右。device_mapauto让模型自动分配到可用的GPU或CPU上避免手动指定设备时报错。需要提前安装bitsandbytes并且在加载4bit模型时不要同时设置torch_dtypetorch.float16这两者会冲突。如果机器连4bit都跑不动另一个思路是换成更小的模型比如1.5B到3B的开源对话模型。效果上会比6B弱一些但对课程设计演示完全够用。纯CPU推理是最后选择速度会比较慢单轮问答可能到二三十秒现场演示时容易出现卡顿。4. 避坑与排查这个项目最常见的五个踩坑现场4.1 CSV编码与列错位检索结果全乱的根源现象服务能启动但问“高血压的饮食禁忌”返回的答案完全是另一段内容甚至出现KeyError: question这类报错。原因下载的CSV可能是utf-8-sig或gbk编码直接用utf-8读入后字段名带着BOM比如第一列显示为“question”前面有一个不可见字符。另一种更隐蔽的情况是两份CSV字段不一致pd.concat合并后列错位模型拿到的根本不是问题字段。解决先用脚本探测编码再打印列名做一致性检查import pandas as pd def read_csv_smart(path): for enc in [utf-8-sig, utf-8, gbk, gb18030]: try: df pd.read_csv(path, encodingenc) print(f{path} 编码{enc}列{df.columns.tolist()}) return df except UnicodeDecodeError: continue raise ValueError(f所有编码尝试均失败: {path}) df_open read_csv_smart(langchain-ChatGLM_open.csv) df_closed read_csv_smart(langchain-ChatGLM_closed.csv) if df_open.columns.tolist() ! df_closed.columns.tolist(): print(警告两份数据列名不一致需要先对齐字段)注意代码末尾的对齐操作我刻意没有写死因为只有确认列顺序完全一致时才能直接赋值列名。否则应该用rename做显式映射。这个坑最费时间的地方不是代码本身而是“看似成功”的静默错位所以先打印列名做判断再执行合并。4.2 显存OOM加载模型时进程被直接杀掉现象启动时日志打印到一半进程退出终端出现CUDA out of memory或者Killed。原因ChatGLM这类大模型参数规模大float16加载时显存占用是参数量两倍以上。而且向量库检索结果会把上下文拼到Prompt里Prompt越长显存占用越高。有的机器显存本来就小刚加载完模型就已经到极限。解决加载模型时不手动指定device用device_mapauto让库自己分配from transformers import AutoModel, AutoTokenizer import torch model AutoModel.from_pretrained( THUDM/chatglm3-6b, torch_dtypetorch.float16, device_mapauto, trust_remote_codeTrue )参数解释torch_dtypetorch.float16把权重精度降到半精度显存立刻减半device_mapauto会检测可用显存自动切分模型层放到多张卡或CPU上避免单卡直接爆掉。在此基础上还OOM就走第3章的量化方案改load_in_4bit。需要注意load_in_4bit和device_map可以同时使用量化后显存占用会更可控。4.3 切块粒度不对答案答非所问现象问题是“糖尿病人能不能吃西瓜”模型回答内容却像用药说明和水果没有任何关系。原因固定长度切块把一句话拦腰截断检索到的文档块里没有完整上下文。医疗答案的文本结构很长关键结论往往在段尾切块只切出了前半段背景信息召回内容自然丢失。解决按段落或句子边界切块并加overlap窗口import re def split_text_keep_sentences(text, max_len300, overlap80): sentences re.split(r(?[。.!?]), text) chunks, current [], for sent in sentences: if not sent.strip(): continue if len(current) len(sent) max_len: current sent else: if current: chunks.append(current) current sent[-overlap:] sent if current: chunks.append(current) return chunks这段代码先按中文句号、叹号、问号以及英文标点切句再按最大长度组合句子同时把上一块结尾的overlap字符拼到下一块开头。效果是文档块保持了完整句子检索召回后上下文更连贯。医疗场景下切块的优先级我认为比向量模型选择还要高数据切碎了大模型再强也拼不回来。4.4 多轮对话丢失上下文追问变成“失忆”现象用户先问“高血压能喝茶吗”再问“那绿茶呢”第二次回答完全没有结合第一轮的高血压背景。原因系统没有维护历史消息。很多教程只演示单轮问答到了多轮场景直接把用户问题单独扔给检索器和模型没有把上一轮结论放进上下文。解决用LangChain的ConversationBufferWindowMemory维护最近若干轮对话from langchain.memory import ConversationBufferWindowMemory memory ConversationBufferWindowMemory(k3, return_messagesTrue) # 每轮问答后更新记忆 memory.chat_memory.add_user_message(user_question) memory.chat_memory.add_ai_message(model_answer) # 构造下一轮输入时载入历史 history memory.load_memory_variables({})[history]说明k3表示只保留最近三轮对话防止累计过长时间导致Prompt超长和显存升高。return_messagesTrue让记忆以消息对象形式返回方便拼接到LangChain的消息列表中。这个设计的另一个好处是控制成本因为长对话会让每次推理的token量增加限制记忆窗口是常见取舍。4.5 Docker端口映射失败宿主机访问不了服务现象容器日志显示服务启动成功但在宿主机上curl http://localhost:8000没有响应。原因最常见原因是服务监听了127.0.0.1而不是0.0.0.0容器内只有回环地址可以访问。另外docker run时-p参数写错也会导致端口未映射。解决启动入口显式监听0.0.0.0uvicorn main:app --host 0.0.0.0 --port 8000补充一个排查顺序先看容器是否在运行再看端口映射是否生效docker ps docker exec medical-rag curl http://127.0.0.1:8000/health如果容器内访问正常但宿主机不通去检查-p参数如果容器内也不通就是服务本身没有监听正确地址。这个顺序能把“网络”和“应用”两个层面的问题分开避免瞎猜。5. 进阶验证把问答系统封装成API服务并量化评估效果5.1 用FastAPI暴露统一接口项目源码里的调用方式多数还是命令行或简单脚本。要把系统扩展成可被外部调用的服务常见做法是封装一个FastAPI层。它自带/docs调试页对答辩演示很友好前端demo也可以直接对接。from fastapi import FastAPI, Query from langchain_community.vectorstores import FAISS from langchain_community.embeddings import HuggingFaceEmbeddings from transformers import AutoModel, AutoTokenizer import torch app FastAPI(titleMedical RAG Service) embeddings HuggingFaceEmbeddings(model_nameshibing624/text2vec-base-chinese) vectorstore FAISS.load_local(./medical_faiss_index, embeddings, allow_dangerous_deserializationTrue) retriever vectorstore.as_retriever(search_kwargs{k: 4}) tokenizer AutoTokenizer.from_pretrained(THUDM/chatglm3-6b, trust_remote_codeTrue) model AutoModel.from_pretrained(THUDM/chatglm3-6b, torch_dtypetorch.float16, device_mapauto, trust_remote_codeTrue) app.get(/health) def health(): return {status: ok} app.post(/ask) def ask(question: str Query(..., min_length2)): docs retriever.get_relevant_documents(question) context \n.join([d.page_content for d in docs]) prompt ( 请根据以下医疗资料回答问题。 f\n资料\n{context}\n问题{question} ) response, _ model.chat(tokenizer, prompt) return {answer: response, source: [d.page_content[:80] for d in docs]}代码解释Query的min_length2用于过滤空问题和单个字符的无效输入。retriever的search_kwargs设k4回到前面讲过的top_k思路。model.chat是ChatGLM自带的多轮生成接口第二个返回值是历史记录这里用不到就丢弃。接口返回里除了答案还带source也就是命中的原始文档片段这一步对后续排查非常重要。5.2 效果验证的四个维度接口封装好以后还需要验证质量。我一般从以下四个维度检查维度通过标准测试方式召回准确率问题对应的知识块出现在返回docs中打印docs内容人工判断生成一致性答案与原文关键事实一致对比检索块的原文片段单轮延迟GPU环境3秒内CPU环境10秒以内用time计时统计多轮稳定性连续5轮追问不出现明显跑题按4.4配置记忆后连续对话做验证前先用第2章的脚本确认索引和模型都加载成功再逐项测。项目报告里可以配上测试样例表和输出截图比单纯贴代码更有说服力。我最早动手复现这个项目时花了一个下午处理CSV编码问题最后发现只是多了一个看不见的BOM字符。从那以后我拿到任何RAG项目都会强制走一套顺序先检查数据文件和字段再定切块策略然后才碰模型和检索参数。这条顺序让我避开了很多意外。希望帮到你。本文还有配套的精品资源点击获取
返回列表