简介:这是一套基于RAG与大模型技术的医疗问答系统完整资源包,包含源代码、文档说明及全部配套资料,专为计算机、人工智能、自动化等专业学生与从业者设计,适用于毕业设计、课程设计及进阶学习。资源共75个文件,集合了Python功能脚本、Jupyter分析笔记、JSON/YAML配置、Markdown说明、CSV数据集以及系统截图和知识图谱图片,整体压缩包约84.65MB。包内覆盖从语料处理、命名实体识别、关系抽取、知识图谱构建到低秩适配微调、推理问答和Web界面展示的完整链路;还提供环境配置说明、训练参数配置和运行结果分析笔记,便于逐步复现。现有378人学习下载,代码均经过调试保证可运行,项目曾获98分高分,适合从小白到进阶的读者参考,亦可在其基础上扩展新功能,具有很高的学习借鉴价值。
1. 医疗问答系统为什么绕不开 RAG:它解决的不是“智能”,而是“可信”
过去做大模型问答,最常见的方式是把病历、用药指南、诊断路径直接拼进 Prompt 扔给大模型。结果大家都清楚:参数一多就截断,回答似是而非,最要命的是查无实据——模型一本正经地编出“板蓝根治疗心梗”这种话,你还找不到是哪句话带偏的。基于 RAG 与大模型技术的医疗问答系统,就是把外部医学资料先切块、向量化、存进检索库,再在大模型回答前把相关片段拉出来作为依据。它的核心价值不是让模型更聪明,而是让每个答案都能被回溯到原始文档里,这在医疗场景里比“聪明”重要得多。这篇笔记适合正在做毕业设计、或者准备做医疗领域知识问答的从业者,我会把源码结构、关键脚本、参数选型和踩坑记录一次性讲透,让你照着就能把系统落起来。
2. 搭建可复现的 RAG 医疗问答系统:核心链路与源码骨架
很多同学拿到一个“RAG+大模型医疗问答”的方向,第一时间就去调大模型 API,结果做了两周才发现:真正决定系统可用性的不是模型,而是知识库的处理方式。RAG 全称 Retrieval-Augmented Generation,也就是检索增强生成,它把整个链路拆成“文档加载 → 文本切分 → 向量化 → 检索→ 生成”五个环节。医疗领域的特殊性在于:文档来源杂、专业术语多、答案对来源要求高,所以每个环节都和通用聊天机器人不一样。
我先给你看一个我在类似项目里常用的源码目录结构,这个结构也可以直接作为毕业设计的“系统实现”章节骨架:
medical_qa_system/ ├── data/ # 原始医学文档(PDF、Word、TXT) │ ├── raw/ # 未处理的原始资料 │ ├── chunked/ # 切分后的文本片段(JSON格式) │ └── test_set.json # 人工标注的测试问答对 ├── src/ │ ├── loader.py # 文档加载器,支持 PDF/Word/HTML │ ├── splitter.py # 文本切分器,负责 chunk 划分 │ ├── embedder.py # 向量化封装,集成 embedding 模型 │ ├── retriever.py # 检索器,实现向量召回 + 重排序 │ ├── generator.py # 大模型生成,集成 prompt 模板 │ └── pipeline.py # 把上面串成完整的 RAG 流程 ├── app.py # FastAPI 服务入口 ├── config.yaml # 所有可调参数(chunk_size、模型名、阈值) ├── build_vectorstore.py # 一键构建知识库脚本 ├── query_service.py # 本地测试问答脚本 ├── requirements.txt # 依赖清单 └── docs/ # 毕业设计文档/答辩PPT ├── 01_需求分析.md ├── 02_系统设计.md ├── 03_实验对比.md └── 04_使用说明.md这个目录设计背后有三个考量。第一是“数据与代码分离”,医疗原始资料动辄几千万字,切分出来的 chunk 可能有几万条,如果混在代码目录里,git 提交会非常痛苦,而且向量库一旦构建失败,原始数据也不容易被误删。第二是“所有参数集中到 config.yaml”,切分大小、检索条数、模型 temperature 这些参数,在复盘答辩时一定会被老师反复追问,集中管理方便你反复试验并截图对比。第三是“测试集单独存放”,没有人工标注的测试集就没法量化系统效果,后面我专门讲怎么建这个测试集。
2.1 加载与切分医疗文档:chunk_size 和 overlap 怎么定
医疗文档处理和通用文本最大的差异在于“语义边界不清晰”。一份《药品说明书》里,同一段可能既讲适应症又讲不良反应;而《临床指南》往往以“推荐意见”为段落单元。所以第一步不能只按固定字符硬切,而是要结合文档结构。
我会先写一个 loader,用 LangChain 库读取 PDF 并保留标题信息:
# loader.py from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter def load_pdfs(data_dir: str): docs = [] pdf_files = list(Path(data_dir).glob("*.pdf")) for pdf_path in pdf_files: loader = PyPDFLoader(str(pdf_path)) pages = loader.load() for i, page in enumerate(pages): page.metadata["source"] = f"{pdf_path.stem}_第{i+1}页" docs.extend(pages) return docs这里之所以按页保留 metadata 里的 source,是因为后续回答需要定位到“哪份文档哪一页”。很多初学者只保留文件名,等到做引用溯源时发现同一页多个答案混在一起,很难向答辩老师交代。
切分我推荐用RecursiveCharacterTextSplitter,它支持按天然分隔符(比如标题、换行、句号)逐级切分,比硬按字数切更符合语义。但医疗文档有其特殊坑:药品名称、医学单位、缩写容易正好被切在边界上,比如“阿司匹林肠溶片”被切成“阿司匹林”和“肠溶片”,检索时“肠溶片”会单独命中,答案就可能张冠李戴。
我的做法是:先采用较大的chunk_size配合较小的chunk_overlap,再结合标题段落合并。参数设置逻辑如下:
# config.yaml chunk_size: 512 chunk_overlap: 64为什么是这些值?医疗文本以陈述句为主,512 个字符大概能覆盖 8~12 句临床描述,既能保证一个完整知识点被装进一个 chunk,又不会因为过长导致向量化之后语义被稀释。chunk_overlap设置为 64,是为了让相邻 chunk 之间保留上下文交接区,避免“阿司匹林”这样的词在边界处丢失。如果你处理的文档是《临床路径》这类句式结构更强的,可以尝试chunk_size: 768, chunk_overlap: 96,但要通过检索命中率来回调。
2.2 向量化与检索:选 bge-large-zh 还是 text-embedding-3-small
向量化是整个系统的关键。医疗领域有很多专业词汇,比如“呋塞米”“肺栓塞”“甲泼尼龙”,如果 embedding 模型不认识,检索召回率就会很差。我在选型时通常对比两类:
第一类是你本地部署的国产开源模型,典型代表是BAAI/bge-large-zh-v1.5。它的优势是中文语义理解好,医疗语料表现稳定,完全离线,答辩时不会因为网络问题翻车。但需要一块 6GB 以上显存的显卡,CPU 跑也很慢,构建大规模知识库要等很久。
第二类是云端 API 模型,比如 OpenAI 的text-embedding-3-small。它的服务稳定,而且维度可以压缩到 256,内存占用小,但每次调用都有费用,而且医疗数据涉及患者隐私或医院内部资料时,线上 API 可能会带来合规问题。学院的毕设如果用的是公开教材,问题不大;如果手上是真实病历,务必考虑数据脱敏。
我最常用的是 bge-large-zh-v1.5。这里给出封装代码:
# embedder.py from langchain_community.embeddings import HuggingFaceEmbeddings def build_embeddings(model_name: str = "BAAI/bge-large-zh-v1.5"): return HuggingFaceEmbeddings( model_name=model_name, encode_kwargs={"normalize_embeddings": True}, )注意normalize_embeddings必须设置为 True。因为检索时计算的是向量余弦相似度,归一化之后点积就等于余弦相似度,可以简化计算并避免某些索引库的精度问题。这一点在答辩时经常被问到。
检索器我建议加上“混合检索”。纯向量检索会漏掉那些“关键词存在但语义被掩埋”的文档。我的方案是向量检索 + 关键词召回,再用一个重排序模型做二次排序。这里用BM25做关键词召回,它不需要训练,用 Elasticsearch 或rank_bm25库都能实现。再使用bge-reranker-base对两路召回的候选做精排。具体调参在第三节代码里体现。
2.3 大模型生成:限定答案来源,依赖 prompt 约束
生成阶段不是单纯把问题丢给大模型,而是要“先给依据,再让模型作答”。我常用的 prompt 模板如下:
# generator.py prompt_template = """你是一个医疗领域知识助手。请仅根据下面给出的资料片段回答问题。 资料片段: {context} 问题:{question} 要求: 1. 如果资料中没有提到答案,请直接回答“资料中没有相关信息”。 2. 回答时要引用资料片段编号,格式如[1][2]。 3. 不得自行发挥或使用资料之外的医学知识。 """这里的context是检索器返回的 top-k 个 chunk,你需要在代码里给每个 chunk 编号,并拼接进去。让模型引用编号是为了后续做答案溯源。很多人嫌麻烦会省略编号,等到论文里没法展示“答案支持性审计”时再补就晚了。
温度参数,医疗问答建议设成 0.1 到 0.3,绝不要用默认的 0.7。医疗场景容不得“创造性回答”,温度低可以压制随机性。同时设置max_tokens或者max_new_tokens来限制回答长度,防止模型在长文本生成中途跑偏。
3. 跑通最小闭环:从原始 PDF 到问答接口的完整命令
这一章我会带你亲手搭建一套最小可运行系统。我们使用 LangChain 作为 RAG 框架、Chroma 作为向量库、FastAPI 作为服务层。理论上你可以替换成其他组件,但这一节要确保你能在原封不动的情况下跑通。
3.1 环境准备与依赖安装
建议使用 Python 3.10 和虚拟环境,避免系统 Python 里包冲突。创建环境并安装核心依赖:
python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install langchain langchain-community chromadb rank_bm25 pip install fastapi uvicorn pypdf这里chromadb是向量数据库,pypdf用来解析 PDF,rank_bm25实现关键词检索。如果你要本地跑 bge-large-zh 向量模型,额外安装sentence-transformers和torch:
pip install sentence-transformers torch --index-url https://download.pytorch.org/whl/cpu注意 CPU 版 torch 只能跑,速度偏慢。构建小规模知识库(几千个 chunk)问题不大,如果要上十万级数据,建议去申请带 GPU 的机器或者直接用云端 API。
3.2 构建知识库脚本 build_vectorstore.py
这是整个系统最耗时的部分,也是出错最多的部分。我把完整脚本贴出来,然后逐段说明参数改动的影响。
# build_vectorstore.py import yaml from pathlib import Path from src.loader import load_pdfs from langchain.text_splitter import RecursiveCharacterTextSplitter from src.embedder import build_embeddings from langchain_community.vectorstores import Chroma # 读取配置 config = yaml.safe_load(open("config.yaml")) # 1. 加载原始 PDF docs = load_pdfs("data/raw") # 2. 切分文档 splitter = RecursiveCharacterTextSplitter( chunk_size=config["chunk_size"], chunk_overlap=config["chunk_overlap"], separators=["\n\n", "\n", "。", ";", "", " "], keep_separator="end", ) chunks = splitter.split_documents(docs) # 3. 为每个 chunk 添加全局编号和来源信息 for idx, chunk in enumerate(chunks): chunk.metadata["chunk_id"] = idx chunk.metadata["source"] = chunk.metadata.get("source", "unknown") # 4. 向量化并存入 Chroma embeddings = build_embeddings() vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="db/medical_db", ) print(f"成功构建知识库,共 {len(chunks)} 个文本块")这里的separators参数值得解释。默认的RecursiveCharacterTextSplitter分隔符顺序是先长后短,我把中文句号。和分号;加在末尾,这意味着代码会优先按段落切,如果段落太长再按句号切,尽可能保证一个 chunk 内是完整句子。keep_separator="end"会把分隔符保留在前一个 chunk 的末尾,避免“阿司匹林。”被切成“阿司匹林”和“。”导致语义缺失。
另一个容易踩坑的是persist_directory路径。Chroma 默认会想用PersistentClient写入磁盘,如果你在 Windows 上遇到路径过长报错,可以把目录放到项目根目录下的db目录。建议每次重新构建知识库之前,先删除旧的db目录,否则会出现“旧 chunk 残留 + 新 chunk 插入”导致重复检索。
3.3 后端问答服务 query_service.py
构建好知识库后,我们就写一个命令行问答脚本,方便本地验证。
# query_service.py import yaml from langchain_community.vectorstores import Chroma from src.embedder import build_embeddings from langchain_community.llms import HuggingFacePipeline from langchain.prompts import PromptTemplate from langchain.chains import RetrievalQA # 加载配置和已有向量库 config = yaml.safe_load(open("config.yaml")) embeddings = build_embeddings() vectorstore = Chroma( persist_directory="db/medical_db", embedding_function=embeddings, ) # 初始化大模型(此处以本地模型为例,也可以用 OpenAI API) llm = HuggingFacePipeline.from_pretrained( "Qwen/Qwen2-7B-Instruct", max_new_tokens=512, temperature=0.1, device_map="auto", ) # 构建检索器 retriever = vectorstore.as_retriever( search_type="similarity_score_threshold", search_kwargs={"score_threshold": 0.5, "k": 4}, ) # 拼接 prompt 模板 prompt = PromptTemplate.from_template( """请根据以下资料片段回答问题,注意引用片段编号[1][2]。 资料片段: {context} 问题:{question} 回答:""" ) qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=retriever, return_source_documents=True, chain_type_kwargs={"prompt": prompt}, )这里我把score_threshold设置为 0.5,k设置为 4。score_threshold表示相似度阈值,低于 0.5 的 chunk 会被丢弃,避免检索到一堆不相关的内容;但这不能一刀切。对于bge-large-zh-v1.5这种模型,语义相似度经常落在 0.4~0.7 之间,如果你发现答案总是“资料中没有相关信息”,很可能是阈值设高了,要调低到 0.4;反之,如果答案满天飞但引用乱七八糟,则调高到 0.6。
运行问答:
# query_service.py 主流程 if __name__ == "__main__": while True: question = input("你的问题:") if question.strip() == "": break result = qa_chain({"query": question}) print("\n回答:", result["result"]) print("\n参考来源:") for doc in result["source_documents"]: print(doc.metadata["source"], "——", doc.page_content[:30])这是一个可持续运行的最小闭环。你可以在终端看到模型回答了哪个问题,依据来自哪些文档片段。如果引用的来源明显不对,就根据来源定位到原始文档,去调整切分方式或检索阈值。
3.4 启动与演示
如果你要做一个可演示的 Web 页面,用 FastAPI 包一层接口即可。最简单的是写一个/answer接口:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Query(BaseModel): question: str @app.post("/answer") def answer(query: Query): result = qa_chain({"query": query.question}) return { "answer": result["result"], "sources": [ {"source": doc.metadata["source"], "snippet": doc.page_content[:50]} for doc in result["source_documents"] ] }用uvicorn app:app --host 0.0.0.0 --port 8000启动后,你就能用 Postman 或请求工具直接调用。在答辩演示时,我个人建议浏览器打开 Swagger 文档(FastAPI 自带的/docs页面),在那里输入问题时导师能看到系统返回的结构化字段,比黑乎乎的命令行有说服力得多。
4. 把“高分毕设”做扎实:文档体系与效果验证
源码能跑只是第一步,毕设能否拿高分,关键在于文档和实验的说服力。很多同学过度关注代码,却忽略了评审最看重的“完整闭环”和“量化指标”,这部分我专门展开。
4.1 论文文档要写什么:从系统设计到实验对比
毕设文档不是软件说明书,它需要讲清楚“为什么用 RAG”和“RAG 比传统微调好在哪里”。我建议按这样的章节组织文档:
- 需求分析:明确系统服务对象(患者自诊?医生辅助?),收集 10 份以上医疗资料作为知识库,列举 20 个典型问题作为验收标准。
- 系统设计:画架构图(离线构建流程与在线问答流程),标注每层选用的组件和技术指标。
- 关键技术实现:贴 loader、splitter、retriever、generator 的核心代码,配合参数表说明为什么选择
chunk_size=512而不是 256 或 1024。 - 实验与评价:这是最容易拉开差距的地方。要跑三组对比实验:
- 纯大模型(不接 RAG)在测试集上的表现
- 基础 RAG 在测试集上的表现
- 加入重排序的 RAG 在测试集上的表现
每组实验都要给出“准确率、召回率、答案完整性”三个维度,你可以用人工评判,也可以用 GPT-4 做自动评价,但至少要用表展示 3 组结果。
4.2 评测指标与测试集
评测是 RAG 项目里的黑匣子,很多人不知道自己的系统到底改进在哪。医疗问答系统评测通常看两个层面:一是检索层面,二是生成层面。检索层面看“检索命中率”和“召回率”,生成层面人工评价“答案是否与资料一致、是否完整覆盖问题”。
我的建议是准备 100 条带标准答案的问答对,每条都要包含“问题、标准答案、应引用的文档来源”。然后用 recall@k 来评估检索质量:
# evaluate_retrieval.py def recall_at_k(retrieved_chunk_ids, gold_chunk_ids, k): hit = set(retrieved_chunk_ids[:k]) & set(gold_chunk_ids) return len(hit) / len(gold_chunk_ids)这个脚本不需要集成到主服务里,单独跑就行。比如设置 k=4,统计 100 条测试题的平均 recall@4,如果在 0.7 以下,说明检索还需要调。我把测试集放在data/test_set.json,格式很简单:
[ { "question": "阿司匹林禁用于哪些患者?", "answer": "对阿司匹林过敏者、活动性消化道溃疡患者、出血体质者禁用。", "gold_source": "药理_第12章_第3页" } ]这个测试集是我用来倒逼系统迭代的。你在写文档时直接展示这种结构,导师一眼就能看出你有“工程验证”的意识,而不是把代码凑出来就交差。
另一件值得做的事是“错误案例分析”。挑出 5 个系统回答错误的例子,分别分析是检索错了、上下文不够、还是模型没有遵循 prompt。这个部分放在论文“不足与改进”里,反而是加分项。
5. 医疗问答系统常见问题避坑:这 5 个坑我当年都踩过
RAG 系统的开发看起来链路清晰,但实际调试时会遇到各种反直觉的问题。我这里按“现象 → 原因 → 解决”的格式记录 5 个高频踩坑点,都是医疗问答场景里特有的,通用项目里可能遇不到。
5.1 现象:检索出来的内容不相关,答案驴唇不对马嘴
原因分析:我最初用默认的Chroma.as_retriever(),它采用similarity检索,返回向量距离最近的 top-k。但医疗文档中存在大量同义词和上下级概念,比如“高血压”和“血压升高”语义相似但向量距离可能很远,更常见的问题是chunk_size过大导致一个 chunk 里混合了多个主题,检索时距离被平均,命中不了真正的答案。
解决方案:切换为similarity_score_threshold搜索,并配合重排序模型。核心调参技巧:
retriever = vectorstore.as_retriever( search_type="similarity_score_threshold", search_kwargs={"score_threshold": 0.55, "k": 6}, )同时把top_k从 4 提升到 6,召回更多的候选片段,交给重排序模型精排。重排序模型可以用bge-reranker-base:
from langchain_community.cross_encoders import HuggingFaceCrossEncoder reranker = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base")在获得向量检索结果后,再让 reranker 对候选按“问题和片段的相关程度”打分,取前 3 个作为最终上下文。这一层多加的耗时大约 100ms,但准确率提升非常明显。
5.2 现象:答案引用了正确的文档,但来源标注张冠李戴
原因分析:这是医疗问答中很严重的信任问题。我的一个教训是在最初的loader里没有正确保留页码信息,多个 PDF 的 metadata 都是文件名,检索结果打印出来全是一个来源,答辩时被问“回答里的高血压内容到底来自哪一份文档”,我当场无法区分。
解决方案:在构建知识库时,把“文档名 + 章节名 + 页码”拼成完整 source 写入 metadata,并且在构建之前打印抽样 chunk 验证一下。修改后的 loader:
from langchain_community.document_loaders import PyPDFLoader, TextLoader def load_pdfs(data_dir): docs = [] for pdf_path in Path(data_dir).glob("*.pdf"): loader = PyPDFLoader(str(pdf_path)) pages = loader.load() for page, doc in enumerate(pages, start=1): doc.metadata["source"] = f"{pdf_path.stem}_第{page}页" docs.append(doc) return docs这个看似简单的改动,直接决定你的系统能不能做“引用溯源”。没有精确来源的 RAG 在医疗场景里基本等于废物。
5.3 现象:向量库占用内存过大,构建时直接内存溢出
原因分析:Chroma 在persist_directory模式下会把所有向量加载到内存,如果知识库有几万个 chunk,每一条向量 768 维(bge-large 是 1024 维),内存很快就告急。医疗文本动辄几百万字,切分出来十万级 chunk 非常常见。
解决方案:如果是面向毕设的中小型数据,控制在 2 万 chunk 以内不会有大问题。如果数据量实在大,第一考虑降维:使用text-embedding-3-small的 256 维;第二改用FAISS向量库,它支持索引文件和内存映射,资源占用比 Chroma 更可控;第三是分批写入,每处理 1000 个 chunk 持久化一次,避免一次性构建的峰值内存。我在处理《新编药物学》第 17 版时,用 FAISS 把构建时内存峰值降了一半。
如果你用 Chroma,不要重复运行from_documents往同一个目录追加,否则历史向量不会被清理,每次追加后目录越来越大。
5.4 现象:大模型回答与检索内容相矛盾
原因分析:大模型在生成时并不可靠,即使你给了它明确的资料片段,它可能还是会“发挥”出资料之外的知识。这在医疗领域是致命的。现象是系统明明没检索到“禁用阿司匹林”的信息,模型却回答“阿司匹林可以用于儿童退热”,这是常识性错误,但模型不会自知。
解决方案:除了把温度调低到 0.1,更有效的是在 prompt 里强制加入“资料中没有相关内容时,必须回答未知”。我用的强约束表达是:
如果资料片段中没有明确提及该问题的答案,请只回答“根据现有资料无法回答”,不要补充任何额外知识。同时,在代码层做一个“矛盾检测”:检查模型回答中的关键医学实体(药物名、症状、剂量)是否出现在检索到的上下文中。如果没有出现,就丢弃那部分回答或直接返回“无法回答”。这个逻辑我写在pipeline.py里,算是保险丝。
5.5 现象:API 调用超时导致前端频闪失败
原因分析:医疗问答系统的用户场景往往是在浏览器里等待答案,而大模型 API 的响应时间普遍在 3~10 秒,如果前端没有设置合适的超时,就会频繁报错。
解决方案:在 FastAPI 层把问答接口设为异步,并把前端超时拉到 30 秒。同时给接口加一个简单的缓存,相同问题在 5 分钟内直接返回结果。这里单独抽一个小函数:
from functools import lru_cache @lru_cache(maxsize=100) def get_answer_cached(question: str): # 调用 qa_chain return qa_chain({"query": question})但要注意lru_cache缓存的是同一个进程内的结果,如果部署多 worker,需要改用 Redis。对于毕设演示,单进程缓存完全够用。另外,要把超时时间从默认的 60 秒适当调低,否则用户反复点击会导致系统连锁阻塞。
6. 进阶:把单轮问答改成可追溯的 Agentic RAG
如果毕业设计想冲刺高分,单轮 RAG 已经不够看了。现在的热点是 Agentic RAG,也就是让大模型具备“自主决定检索什么、如何重试、如何整合”的能力。在医疗场景里,这一步不是炫技,而是实打实地解决单轮检索容易漏掉关键证据的问题。
6.1 引入查询改写与意图路由
先说查询改写。用户问“高血压患者能用布洛芬吗?”,这个 query 直接检索到的文本可能同时涉及“高血压”和“布洛芬”两个实体,但文档中这两个词的关联性并不强。Agentic 的常见做法是让大模型自动拆解子问题,比如先检索“布洛芬禁忌症”,再检索“高血压用药注意事项”,最后综合答案。代码层可以用 LangChain 的create_react_agent或AgentExecutor,也可以手动实现一个简单的两步路由:
def rewrite_query(question: str) -> list[str]: prompt = f"针对医疗问题,拆成 1-3 个子检索词,用逗号分隔:{question}" resp = llm.invoke(prompt) return [q.strip() for q in resp.split(",") if q.strip()]然后对每个子检索词分别从向量库检索,合并结果后去重,再交给生成模型。这种小改造让系统在“多条件禁忌”问题上明显更有条理。
6.2 用“引用溯源”让答案可验证
我在生成 prompt 里要求模型按编号引用资料片段,最终答案呈现在界面上时,可以把这些编号映射成可点击的来源卡片。实现并不复杂——返回source_documents时附带每条的chunk_id,前端用 Tooltip 展示来源文档名和原文片段。毕设答辩时,导师点开引用卡片能看到原文档内容,信任感完全不一样。这一步在源码里体现为:
retriever_result = retriever.invoke(question) sorted_chunks = reranker.rerank(question, retriever_result) context = "\n\n".join(f"[{i+1}] {chunk.page_content}" for i, chunk in enumerate(sorted_chunks))千万记得把[1]的编号与实际传入的 chunk 顺序保持一致,这比 prompt 本身更影响“引用正确性”。我有一次忘了重排序后重新编号,结果答案引用 [2] 但实际对应的是另一段内容,这种行为败好感度极重。
6.3 我的收尾习惯
我对 RAG 项目唯一的习惯是:每次改完参数,立刻重跑测试集并记录结果,不要凭感觉说“好多了”。医疗问答系统里“感觉”是最不靠谱的,没有量化指标,你连自己系统有没有退化都不知道。毕设做到最后,最值钱的不是那几行代码,而是一套能说服人的数据表和完整的排错记录。希望这篇文章能帮你在医疗 RAG 这条路上少走几个跟头,祝你的知识库目标命中率和答案可信度都能调到最优。
本文还有配套的精品资源,点击获取