简介:检索增强生成(RAG)是当前大模型落地应用中的核心技术范式。它通过先从知识库检索相关资料,再让大模型基于这些资料生成答案,有效解决了通用大模型在专业领域“一本正经地胡说八道”的幻觉问题。在医疗、法律、企业知识库等高准确性要求的场景中,RAG架构正成为标配方案。本文以一个医疗问答系统毕业设计为例,系统讲解从知识库构建、文本切块、向量化存储到检索重排序、提示词设计、效果评测的完整工程链路,并对比了FAISS、Milvus等向量数据库选型,同时分享了优化准确率、降低幻觉率的实战踩坑记录。无论你是准备毕设选题,还是想深入了解RAG与医疗问答的工程实现,这篇内容都能提供有价值的参考。 去年这个时候,我还在为毕业设计选题焦头烂额。那会儿大模型的概念已经火到不行,但多数同学的毕设还停留在“调一个API做个聊天机器人”的程度,答辩时很难讲出深度。我最后选的方向是——基于 RAG 与大模型技术的医疗问答系统。RAG(Retrieval-Augmented Generation,检索增强生成)的思路并不复杂:不直接让大模型凭空回答,而是先从本地知识库里检索出相关内容,再把检索结果作为上下文交给大模型生成最终答案。这个套路在今天的企业级知识库问答、智能客服、私有化部署场景里几乎是标配。如果你正在准备计算机方向的毕设,或者想系统搞懂一个 RAG 项目从文档处理到最终部署的完整链路,这篇内容应该能给你不少参考。
需要先说明一点:这套系统的定位不是“AI医生”,而是“医学知识问答助手”。它的核心价值在于把权威医学资料沉淀成可检索、可回答的知识库,用户提问时能基于这些资料给出有出处的回答。这个定位差异很重要——它决定了知识库构建、检索策略、答案生成方式的设计方向,也决定了你项目的技术深度能做到什么程度。
1. 为什么医疗问答不能直接甩给通用大模型
1.1 “一本正经地胡说八道”在医疗场景里是致命的
如果你直接把问题丢给通用大模型,比如问“高血压患者可以吃柚子吗”,模型确实能给你一段看起来像模像样的回答。但问题在于——这段回答可能是模型从训练语料里“猜”出来的,它既不知道你问的是哪个指南、哪个药品说明书,也无法为回答给出确切出处。如果用户在某个小众药物上追问一句,它可能就编出根本不存在的相互作用机制。
这在医疗领域是完全不可接受的。普通知识问答答错了无非是闹笑话,医疗问答答错了轻则误导用户,重则导致误服、延误治疗。所以我从一开始就确定了一个原则:系统必须基于给定的医学资料回答,不能靠模型的“记忆”自由发挥。这也是这个项目选择 RAG 而不是直接微调模型的核心原因。
1.2 开卷考试比闭卷考试更适合专业场景
可以把大模型理解成一个“闭卷考试”的考生,它的知识截止到训练数据那天,而且记忆会模糊、会混淆。但 RAG 模式下,大模型变成了“开卷考试”——你先把知识库里的相关段落翻出来摆在它面前,它只需要基于这些材料组织语言回答。
举个直观例子:
- 闭卷模式:用户问“布洛芬和萘普生能否同时服用”,模型可能给出一个泛泛的答案。
- 开卷模式:系统先检索药品说明书或药物相互作用数据库,把两药的代谢途径、相互作用记录返回给模型,模型再基于这些记录给出“不建议同时服用,请咨询医生”的回答,同时可以附上资料来源。
用户看到的不再是“某条神经网络里挖出来的信息”,而是“某份药品说明书、某篇指南里明确写过的内容”。这种模式天然具备可追溯性,对医疗场景来说非常重要。
1.3 为什么这个选题适合做成毕业设计
从毕设角度,RAG 医疗问答系统的技术栈非常完整:中文自然语言处理、文本向量化、向量数据库、检索算法、大模型调用与提示词工程、前后端联调,每个环节都能写出实质性的设计内容。它既不是纯调 API 的“空壳项目”,也不是需要从零训练大模型的“无底洞项目”,难度和工程量刚好卡在一个良性位置——认真做能做出深度,混日子也能跑通流程。
从评分角度,这个选题有几个天然的加分项:痛点真实(医疗问答的准确性诉求)、技术新颖(RAG 是当前大模型应用的热门方向)、成果可演示(做成网页或命令行工具都能演示)。这几个点在后文答辩章节会展开讲。
2. 系统整体架构:一套能跑通、能答辩的完整链路
2.1 四层架构设计
整个系统我按数据流分成四层,每一层的职责边界必须清晰,否则后期排查问题会非常痛苦。
数据层负责的是医学知识的接入与存储。原始资料可能是 PDF、Word、TXT 或网页文本,这一层要把它们解析成干净的纯文本,再切分成固定大小的文本块(chunk),最后通过 embedding 模型把每个文本块转成向量,连同原文一起存入向量数据库。这一层输出的核心产物是“可检索的知识库”。
增强层是 RAG 的核心,负责在用户提问时从知识库中检索相关内容。这个“相关内容”不一定只有一个文本块,通常会取 Top-K 个候选块,然后用重排序(rerank)模型对候选结果做二次精排,把最可能解决问题的那几个文本块选出来。
生成层负责把“用户问题 + 检索到的参考资料”封装成结构化的提示词(Prompt),交给大模型生成答案。这一层需要精细设计提示词模板,明确告诉模型“只能参考给定资料”“资料不足时回答不知道”,从源头抑制幻觉。
交互层是用户看到的部分,包含 Web 界面或命令行工具、流式输出、历史记录等功能。对毕设来说,这一层用一个轻量级的 Web 界面即可,重点在于演示效果。
2.2 技术选型:为什么选这些组件
我做了大量的对比实验之后,最终选用了下面这套组合:
| 模块 | 选型 | 理由 |
|---|---|---|
| 大模型 | Qwen2-7B-Instruct | 中文能力强,开源可本地部署,也有 API 可用 |
| Embedding 模型 | BGE-M3 | 中文语义理解优秀,支持多种粒度的检索 |
| Rerank 模型 | BGE-Reranker-v2 | 对检索结果做精排,效果提升明显 |
| 向量数据库 | Milvus | 支持百万级向量检索,项目可扩展性强 |
| 知识库处理 | LangChain + 自研切块逻辑 | LangChain 提供文档加载与向量化流水线,切块逻辑自己做 |
| 服务框架 | FastAPI | 轻量、性能好、接口优雅 |
| 前端界面 | Streamlit | 用 Python 写界面,开发速度快,演示效果好 |
如果拿不到 GPU 资源做本地推理,也可以把大模型换成 API 服务(比如通义千问、DeepSeek 等),向量数据库可以用 FAISS,牺牲一部分可扩展性换来更低的部署门槛。这类替代方案不影响整个系统的架构设计与核心逻辑,对毕设可能反而更友好。
2.3 源码目录结构
项目源码按照“数据、检索、生成、交互”四层组织,答辩时讲解会非常清晰:
medical_rag_system/ ├── data/ │ ├── raw_docs/ # 原始医学资料 │ ├── processed/ # 清洗后的纯文本 │ └── vector_store/ # 向量数据库持久化目录 ├── src/ │ ├── ingestion/ │ │ ├── loader.py # 文档加载与解析 │ │ ├── splitter.py # 文本切块 │ │ └── embedder.py # 向量化与写入向量库 │ ├── retrieval/ │ │ ├── retriever.py # 检索入口 │ │ └── reranker.py # 重排序 │ ├── generation/ │ │ ├── prompt.py # 提示词模板 │ │ └── llm.py # 大模型调用封装 │ └── api/ │ └── main.py # FastAPI 接口 ├── web/ │ └── app.py # Streamlit 前端 ├── docs/ │ ├── 需求文档.md │ ├── 设计文档.md │ ├── 数据库设计说明.md │ └── 答辩PPT提纲.md └── requirements.txt这套结构最大的好处是“按层解耦”:想换向量数据库,只需要改 retrieval 模块;想换大模型,只需要改 generation 模块;知识库重建不碰代码,只要重新跑 ingestion 流程。答辩老师问“你这里为什么不用 XXX”,你可以直接回答“该模块是接口化的,替换成本很低”。
3. 知识库构建:切块、向量化与存储的细节取舍
3.1 数据来源与预处理
医学知识库的数据来源非常关键,我主要用了三类公开资料:权威医学教材的电子版、临床诊疗指南、药物说明书。这些资料来自公开渠道,版权和使用场景比较安全,也能保证回答内容的专业性。
拿到原始文档之后,第一步是统一格式。PDF 要用 PyMuPDF 提取文本,Word 用 python-docx,网页文本用 BeautifulSoup 清洗。清洗时注意处理页眉页脚、公式、表格、乱码等干扰信息。这一步骤看着不起眼,但如果不做好,后面切块和向量化的质量会大打折扣。
3.2 切块策略:固定长度还是语义切分
文本切块是整个知识库构建里最容易被低估的环节。切得太粗,一个块可能混杂多个主题,检索时精确度下降;切得太细,单个块信息量不足,模型拿到手里也回答不完整。
我使用的是带重叠的固定窗口切分,参数设置:
- chunk_size = 300 字符
- chunk_overlap = 50 字符
这里有个经验:中文文本按字符数切分比按 token 数切分更直观,因为一个汉字大致等于一个语义单元。50 字符的重叠是为了避免关键信息恰好落在两个块的边界上,这个参数经验性很强,建议在真实数据集上多次试验后确定。
如果文档里有明显的标题结构,还可以考虑“按标题层级切分”,即每个二级标题下的一部分作为完整语义单元。这一点在 LangChain 的 MarkdownHeaderTextSplitter 里已经实现,但需要资料本身有规范的结构标注,只对某些来源的文档有效。
3.3 Embedding 模型:中文医学语料是个坑
Embedding 模型的选择对检索效果影响极大。通用领域表现好的模型,在医学专业术语上可能并不理想。我对比过 OpenAI text-embedding-3-small、text2vec-large-chinese、BGE-M3 几个模型,用一组医学问题做召回测试,结果发现 BGE-M3 在中文医学语料上的综合表现最稳定。
原因是 BGE 系列在训练时用了大量中文语料,对“心肌梗死”“反流性食管炎”这类专业表述的语义理解更准确。此外 BGE-M3 支持稀疏检索和稠密检索的混合模式,可以搭配做混合召回,这个特性在后面的检索优化中非常有用。
3.4 向量数据库的选型对比
向量数据库这一步可选方案很多,常见的几种我都实际跑过:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| FAISS | 轻量、无需部署服务、内存向量检索 | 不支持分布式、没有可用的过滤功能 | 毕设、演示、小数据量 |
| Chroma | 开箱即用,元数据过滤方便 | 大规模数据性能一般 | 中等规模项目 |
| Milvus | 支持百万级向量、具备标量过滤与混合检索 | 需要 Docker / 服务器部署,运维成本较高 | 生产级、强调可扩展性的项目 |
| Elasticsearch | 全文检索成熟,可同时做关键词与向量检索 | 配置复杂,中文分词需要额外插件 | 文本检索与向量检索结合的场景 |
我选了 Milvus,主要是考虑到答辩时可以展示出“面向工程的系统设计能力”——强调它支持向量检索、标量过滤、混合查询,不是玩具级 demo。如果你不打算部署 Docker,FAISS + pickle 持久化也能把整个项目跑起来,代价是后期换数据库时检索模块的抽象层要设计好。
3.5 元数据设计
很多初学者会忽略元数据的设计,但这恰恰是知识库可用性的关键。我存储每个文本块时都保留了三类元数据:
- 来源:文档名称、章节、页码,用于回答后附上引用出处
- 结构:标题层级、段落类型,方便后续做过滤或优先级排序
- 权限:数据类别(指南/教材/说明书),便于后续扩展
例如用户问到“布洛芬的用法用量”,系统返回答案后可以附上来源说明:“以上内容参考自《XX药品说明书》第 3 章,页码 12”。这种“有据可查”的用户体验和纯聊天机器人的体验完全不在一个量级。
4. 核心实现:从检索到生成的代码串联
4.1 文档加载与切块
入口是src/ingestion/splitter.py,核心逻辑如下:
def split_text(text: str, chunk_size: int = 300, overlap: int = 50) -> list[dict]: chunks = [] start = 0 while start < len(text): end = start + chunk_size chunk = text[start:end] chunks.append({ "text": chunk, "metadata": { "char_start": start, "char_end": end, "seq_id": len(chunks) } }) if end >= len(text): break start = end - overlap return chunks这段代码看着简单,但有几个细节值得注意。切分后的文本块要记录原始字符位置,用于溯源;seq_id保留前后顺序,避免后续跨块拼接时出现逻辑错位。实际项目中我还会在切块前先清理空行、去掉页眉、规范化中英文标点。
4.2 向量化与入库
向量化入口是src/ingestion/embedder.py:
from langchain_community.embeddings import HuggingFaceBgeEmbeddings model_name = "BAAI/bge-m3" model_kwargs = {"device": "cuda"} encode_kwargs = {"normalize_embeddings": True} embedding_model = HuggingFaceBgeEmbeddings( model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_kwargs, ) vectors = embedding_model.embed_documents([chunk["text"] for chunk in chunks])normalize_embeddings=True很重要,归一化之后计算余弦相似度等价于计算内积,查询时可以直接用点积结果,逻辑更清晰。向量化后的向量写入一个映射结构:chunk_id -> vector,再把整个映射和 chunk 内容同步写入 Milvus。
4.3 检索与重排序
检索模块是src/retrieval/retriever.py,查询时做两步召回:
def retrieve(query: str, top_k: int = 20, rerank_top_n: int = 5): query_vector = embedding_model.embed_query(query) # 第一步:向量召回 raw_results = collection.query( data=[query_vector], output_fields=["text", "metadata"], limit=top_k ) # 第二步:重排序 rerank_results = reranker.rerank( query=query, documents=[r["text"] for r in raw_results], top_n=rerank_top_n ) return [raw_results[i] for i in rerank_results]为什么先取 20 个粗召回再重排序取前 5?因为向量检索的 top-1 不一定最准,粗召回集合越大,重排序就越有发挥空间。直接 top-5 容易漏掉真正命中的答案。这个参数选择是从几百个测试问题里统计出来的,粗召回数取 20 的时候,重排序后的准确率提升最明显。
重排序用到的是 BGE-Reranker-v2,它本质上是一个交叉编码器,把“问题+候选文档”拼接起来做相关性打分,比纯向量相似度更精准,缺点是速度较慢,所以只能用于精排阶段。
4.4 提示词模板设计
生成层是整个系统里“技术含量最隐形但也最关键”的部分。提示词模板不是随便写两句“请回答用户问题”就行的,我实测下来的几个核心约束如下:
PROMPT_TEMPLATE = """你是一个专业的医学知识助手。请严格基于以下参考资料回答用户问题。 参考资料: {context} 回答要求: 1. 只依据参考资料回答问题,不得使用自己记忆中的知识。 2. 如果参考资料无法回答,请直接说“资料中未找到相关内容”,不要编造答案。 3. 回答时在末尾标注引用来源编号,如[1]、[2]。 4. 回答尽量条理清晰,控制在300字以内。 用户问题:{question} 回答:"""这里第 2 条“允许模型说不知道”是抑制幻觉最有效的手段。很多模型滥用是因为提示词里隐含了“你必须回答”的压力,一旦给它一条“不知道”的合法出路,胡编乱造的概率会大幅下降。
4.5 大模型调用与流式输出
我封装了一个统一的大模型接口src/generation/llm.py,支持两种模式:
- API 模式:调用 OpenAI 兼容接口,填入 base_url 和 api_key
- 本地模式:通过 vLLM 或 Ollama 加载本地模型
Streamlit 前端调用这个接口并实现流式输出。流式输出的好处是用户等待时能看到文字逐字出现,体验远远好于等全部生成完再一次性显示。对毕设答辩来说,流式输出也是一个小亮点,能体现出工程细节的用心。
4.6 接口设计
FastAPI 只暴露两个核心接口:
POST /api/upload_doc:上传文档,同步触发解析、切块、向量化、入库POST /api/ask:接收问题,返回检索结果与生成的答案GET /api/source/{chunk_id}:获取指定答案的引用原文
这个设计把文档管理和问答逻辑解耦,前端也可以在不调用任何 Python 库的情况下用 HTTP 请求完成全部交互。
5. 效果评测与踩坑记录:真实调试过程中遇到的坑
5.1 评测维度
做毕设不只是“把功能跑通”,至少要有一个能说明问题的评测。我建立了一个 200 条医疗问答对的测试集,从准确率、完整率、幻觉率三个维度进行评测:
- 准确率:答案与人工标注的标准答案是否一致
- 完整率:答案是否覆盖问题中所有关键信息点,比如药品问答必须覆盖用法、用量、禁忌、相互作用
- 幻觉率:答案中是否出现参考资料中不存在的信息
评测方式是人工打标,因为大模型生成的答案无法用简单规则自动评判,人工打标虽然慢,但对毕设来说足够有说服力。
5.2 踩坑一:文本切块导致关键信息断裂
第一次跑通后,我发现用户问“阿莫西林的剂量”时,系统返回的答案总是在“每日三次,每次0.5g”这个关键信息上缺失。排查后定位到问题:原始资料中“阿莫西林胶囊说明书”里,用法用量这个章节有大量前置描述,导致切块时“剂量”被切到了前一个块,而“服用方法”在下一个块,两个块都只覆盖了问题的一半信息。
解决办法是调整切块参数,同时在切块前对文档的表格和段落结构做特判——如果一段文本里包含“用法用量”“禁忌”等关键词,就强制保持整段不切割,而不是机械地按长度切。
5.3 踩坑二:Embedding 模型对专业术语不友好
早期测试阶段,我用 text2vec-large-chinese 做向量化,发现“心肌梗死”和“冠心病”在医学场景下高度相关,但向量相似度却很低。同样的还有“高血压”和“血压升高”,在通用语料里语义距离较远,在医学场景下却常常是同一个意思。
换用 BGE-M3 之后,这类同义表述的召回情况明显改善。这个坑给到我的教训是:评估 embedding 模型时,不能只看整体指标,一定要拿自己领域的专业词汇做测试。
5.4 踩坑三:向量检索召回不全
只做向量召回,会出现一个问题:包含确切关键词但语义上“不相似”的文档不会被召回。例如用户问“不能与华法林同服的药物有哪些”,如果知识库里某个包含“华法林”关键词的文档和问句的向量相似度不高,就会被排在很后面,最终漏召回。
解决方式是引入混合检索:向量召回 + BM25 关键词召回,两种结果做合并去重后再进行重排序。这个策略在多个 RAG 项目里都被验证有效,也是 Milvus 本身支持的能力之一。
5.5 踩坑四:长文本导致 Token 溢出
最初配置的粗召回数为 30,精排后保留 10 个块,结果大模型接口直接报 Token 超限。原因是一个块 300 字,10 个块加上问题本身,已经远超模型上下文长度。
最终调整为粗召回 20 个块、精排保留 5 个块,并且每个块控制在 300 字以内,最终上下文大约 2K Token,在 Qwen2-7B 的上下文范围内非常安全,推理速度也快了很多。
5.6 效果数据与资源占用
经过上述几轮优化,最终的测试结果大致如下:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 准确率 | 62% | 84% |
| 完整率 | 51% | 79% |
| 幻觉率 | 33% | 10% |
同时记录一下资源占用:本地部署 Qwen2-7B 时,单卡 16GB 显存可支撑推理,加载向量库进程占用内存约 2GB。如果换成 Ollama 量化版模型,资源占用还能进一步降低,适合在无 GPU 环境下演示。
6. 如果毕设答辩,我会把亮点讲成这三件事
6.1 第一件事:讲清楚“为什么做”和“为什么这么做”
答辩时很多同学上来就讲技术细节、讲代码实现,其实评委老师更想先听到的是“选题动机”。你要能一句话讲清楚:医疗问答场景对准确性要求极高,通用大模型会产生幻觉无法直接使用,因此我采用 RAG 架构,把权威医学知识构建为可检索的知识库,让大模型在回答时基于给定资料生成内容,从机制上解决幻觉问题。
这段话要背下来,它就是整个项目的第一个亮点。同时要准备好应对追问:“那你为什么不直接用微调模型?”——这个问题我在前面的章节已经回答过,核心逻辑是微调成本高、更新慢、仍然无法保证资料来源可追溯,RAG 是更适合专业场景的方案。
6.2 第二件事:展示量化对比与真实案例
空口说“我的系统效果好”没有用,要把评测过程展示出来。我在答辩 PPT 中放了三个东西:
- 200 条测试集上的准确率、完整率、幻觉率数据表
- 两个典型问题的检索结果对比截图(优化前 vs 优化后)
- 一个真实的问答示例,同时展示系统返回的引用来源
评委看到“幻觉率从 33% 降到 10%”这个数据时,通常会比较感兴趣,说明这不是一个纯粹的演示项目,而是有评测意识、有迭代过程的工程实践。
6.3 第三件事:体现工程完整度
很多毕设败在“看起来像个 demo”,而不是像一套系统。要避免这个印象,有几个加分项:
- 项目有完整的文档目录(需求文档、设计文档、数据库设计、部署文档)
- 代码有清晰的模块分层和组织结构
- 提供了 Docker 镜像或一键部署脚本,答辩现场直接演示
- 前端界面设计了合理的交互流程,包括历史记录、引用来源展示、文档上传入口
我最后演示时只用一条命令就把系统跑起来了,评委当场看到了上传文档、提问、带引用的回答完整流程。这个演示效果比讲十页 PPT 都有说服力。
6.4 最后分享一个答辩前的小技巧
正式答辩前,一定把“断网演示”和“本地缓存”准备好。很多现场演示翻车是因为网络波动或者服务没启动,我的做法是在答辩现场用本地模式跑整个链路——模型加载好后断网也能完成检索和回答,这样即便现场环境不稳定,你的演示也不会被打断。
这个项目做下来,我最大的体会是:RAG 系统的技术深度不在于某一个环节有多难,而在于每个环节之间如何衔接、如何取舍、如何用数据说明你的判断是合理的。医疗知识问答只是其中一个应用场景,同样的架构换一批文档,就能变成企业知识库问答、法律文书问答、教育辅导问答。把这条链路吃透,你收获的不只是一份毕业设计,而是一套在大模型落地场景里真正能用的方法论。
本文还有配套的精品资源,点击获取