
简介这是一套基于RAG与大模型技术的医疗问答系统毕业设计源码包包含完整可运行的项目代码与文档说明适合计算机、人工智能、自动化等专业学生用于毕业设计、课程设计或技术进阶。整个资源包共75个文件压缩包大小84.65MB涵盖Python源码、Jupyter Notebook脚本、JSON数据文件、YAML配置、PNG/JPG界面截图与Markdown说明文档等能够支撑从数据处理、模型微调、知识图谱构建到问答界面展示的全流程。目前已有1111人学习下载项目答辩评审分达到98分代码经过调试测试确保可以运行。资源包含自然语言转Cypher查询、模型微调推理、WebUI界面等核心模块并附有数据增强与NER处理脚本可帮助读者掌握RAG检索、大模型微调、医疗知识问答的落地方法基础较强的学习者还能在此基础上二次开发实现更多功能整体具有较高的学习借鉴价值。 一位患者拿着出院医嘱问“阿司匹林肠溶片能不能和酒石酸美托洛尔一起吃”把这个问题直接丢给通用大模型大概率会得到一个完整但没有出处的答案。做成一个 Python 实现的 RAG 医疗问答系统正确做法是先检索医学知识库再把命中的原文与问题一起交给大模型组织语言——大模型负责表达RAG 负责把回答钉在知识来源上这正是这套系统的核心。做这个方向的毕设比直接微调医疗大模型划算不需要 GPU 集群不需要几万条标注数据工作量集中在知识库清洗、检索调参和评测集设计上。系统跑通后你在答辩现场能演示“提问—检索—生成—溯源”的完整链路评委常问的数据、切分、幻觉问题也能讲到实处。这篇笔记把项目拆开讲从选型到代码到踩坑给你一条能照着落地的路径。2. RAG 在医疗问答里的落地选型检索、切分、向量化的关键决策2.1 医疗领域为什么不能只靠大模型硬答医疗场景对错误成本的容忍度极低。通用大模型在开放域问答里表现不错但面对医学问题有三个硬伤第一是幻觉模型没见过某种药物相互作用也会顺着语境编一个出来第二是知识时效性训练语料截止时间决定了它不可能知道新药和新指南第三是缺少可验证性回答没有出处患者和医生都无法复核。这三个问题叠加让“直接拿大模型答医疗问题”在工程上不可接受。RAGRetrieval-Augmented Generation检索增强生成的思路正好对症根据用户问题先从知识库里检索出相关片段再把片段和问题一起喂给大模型让它只依据这些材料生成回答。这样做有两个直接收益一是回答里的关键信息都能回溯到知识库原文幻觉被结构性地压低二是知识库更新只需要替换文档并重新建索引不用重训模型两周就能换一批新药品资料。做这个毕设还有个容易被忽略的边界要先声明在文档里系统定位是“基于知识库的医学信息检索问答”不是“诊断工具”。把这句话写进 README 和答辩 PPT 的引言页既能挡住“你敢拿它看病吗”这类提问也让评审理解你交付的是一套可验证的检索增强框架不是临床决策系统。这个边界不影响项目工作量但会直接影响答辩评价。2.2 检索方案选型BM25、向量检索还是混合检索检索层是整个 RAG 系统的天花板。三套主流方案——BM25 关键词检索、纯向量检索、混合检索——在医疗问答里的表现差异很大。先用一张表把各自的定位说清楚。方案原理优点缺点适用场景BM25 关键词检索词频加逆文档频率打分精确匹配医学专名不依赖向量模型对同义改写无感召回率低药品名、ICD 编码等精确查询纯向量检索文本嵌入向量空间求相似度能处理同义改写和口语化提问医学专名偶发漂移无关键词命中保障症状描述、患者口语表达混合检索推荐BM25 与向量检索各自召回后融合关键词和语义都覆盖实现稍复杂需要调融合参数RAG 医疗问答主选方案我一般给这个项目用混合检索。原因是医疗问题里“阿司匹林”“华法林”这类专名是强关键词用户问“发烧”时知识库里写的是“发热”向量检索能处理这种改写但专名识别又会飘。两个检索器各自取 top_k再用 RRFReciprocal Rank Fusion融合对同一个文档在两份结果列表里的排名取倒数求和分数高的进最终 top_k。这个融合过程只看排名不看原始分数天然规避了 BM25 分值动辄几百、向量相似度只有 0 到 1 的量纲差异一个脚本就能实现不需要额外训练模型。RRF 的公式是 score(d) 1/(k r_bm25(d)) 1/(k r_dense(d))k 取 60 是该算法论文里的常见经验值r 表示文档在对应检索结果里的排名从 1 开始数。两路各取前 50 条候选融合后再取最终 top_5。这里有个关键细节不能两路都只取 5 条就做融合候选池大一点融合后的排名才有意义只取前 5 等于提前截断把跨路命中的文档都扔掉了。2.3 文档切分粒度按标题切还是按段落切医疗文档结构差异比通用文本大得多。药品说明书有固定章节临床指南是嵌套小节检验单是表格。切分策略必须是“按结构走”不能单纯按字符长度硬切。常见做法是按 Markdown 标题层级切分每个最小标题块作为一个检索单元并给每个块保留一层上级标题作为上下文前缀——检索命中的片段自带标题prompt 里的语境就不会断。切分的粒度控制有一组经验值我直接做成表给你抄作业。文档类型结构特征推荐切分策略chunk_size药品说明书固定章节结论常在段首按标题层级切分保留上级标题前缀400 字临床指南多层小节嵌套按二三级标题切分必要时按窗口合并500 字检验单 / 表格行结构字段化转成“字段:值”文本行后再切分300 字医疗场景宁小勿大单块超过 600 字检索时会把不相关内容一并带入 prompt稀释答案精度低于 150 字又会切断“药物相互作用”这类跨段落信息。表格型文档切分前必须转成文本行否则向量化会把表格内容混成语义噪声。如果知识库里已有清晰的医学本体结构比如按 ICD-10 编码组织的目录可以在普通切分之外再加一层实体关系索引接近 GraphRAG 和 ontology 类 RAG 的思路——这是扩展点不是主线毕设把区块切分做扎实就够用了。3. 医疗问答系统的源码结构从数据预处理到检索管线的完整实现3.1 项目源码的目录组织与模块划分一个能答辩的 RAG 医疗问答系统源码要让人一眼看出“数据层—检索层—生成层”的分层。这类毕设项目拿到手第一件事不是跑代码而是看目录结构和 requirements.txt确认数据清洗、向量建库、检索融合、大模型调用四段逻辑分别落在哪些文件里。以下是常见的组织方式按这个结构维护和写文档都会省力。medical_qa_system/ ├── data/ │ ├── raw/ # 原始PDF/Word/数据库导出文件 │ └── processed/ # 清洗后的txt/markdown语料 ├── src/ │ ├── preprocess/ │ │ ├── pdf_parser.py # PDF解析与清洗 │ │ ├── chunker.py # 文档切分 │ │ └── builder.py # 知识库构建入口 │ ├── retriever/ │ │ ├── bm25_index.py # 关键词索引 │ │ ├── vector_store.py# 向量库封装 │ │ └── fusion.py # RRF结果融合 │ ├── generator/ │ │ ├── prompt.py # prompt模板 │ │ └── llm_client.py # 大模型接口封装 │ └── api/ │ ├── main.py # FastAPI服务入口 │ └── schemas.py # 请求响应模型 ├── tests/ │ ├── test_retriever.py │ └── eval_qa.py # 问答效果评估 ├── requirements.txt └── README.md逻辑说明这个结构的核心是把检索和生成解耦。preprocess 只负责把原始文档变成可检索的 chunk 元数据retriever 只负责召回和融合generator 只负责拼 prompt 和调大模型api 层对外暴露 HTTP 接口。答辩时按这个分层讲评委能快速理解你的系统边界而不是在一坨脚本里找逻辑。参数说明data/processed 是中间产物建议清洗后统一存成 UTF-8 的 markdown 文件避免向量库反复解析原始 PDF。tests/ 目录放评估脚本而不只是单元测试这是毕设拿高分的加分区——有评测数据才能证明系统不是玩具。README 里除了环境安装至少要把“知识库更新流程”和“问答请求示例”写清楚这两块是文档说明里最容易被查重的部分。3.2 医疗知识库的构建PDF/Word/数据库的清洗与入库第一步是把零散资料处理成可切分的纯文本。以一份药品说明书 PDF 为例常见做法是用 PyMuPDFfitz抽取文字层再用正则清洗掉页眉页脚和无效换行。下面这段代码负责把 PDF 目录下的文件批量转成 markdown 文本。import fitz import re from pathlib import Path def pdf_to_markdown(pdf_path: Path) - str: 把单份PDF转成清洗后的markdown文本。 doc fitz.open(pdf_path) lines [] for page in doc: text page.get_text(text) # 去掉页眉页脚按页码结尾和常见页眉关键词过滤 text re.sub(r第\s*\d\s*页, , text) lines.append(text.strip()) doc.close() # 合并连续物理换行避免把同一段落切裂 full_text \n.join(lines) full_text re.sub(r\n{3,}, \n\n, full_text) return full_text if __name__ __main__: raw_dir Path(data/raw) out_dir Path(data/processed) out_dir.mkdir(exist_okTrue) for pdf_file in raw_dir.glob(*.pdf): md_text pdf_to_markdown(pdf_file) out_path out_dir / f{pdf_file.stem}.md out_path.write_text(md_text, encodingutf-8) print(f已处理: {pdf_file.name} - {out_path.name})逻辑说明第 6 行的get_text(text)抽取的是 PDF 的文字层扫描件图片需要先走 OCR这里不展开。第 9 行的正则只去掉“第 3 页”一类页码噪声如果你的资料页眉固定可以再加一个re.sub按关键词整行删除。第 15 行合并连续换行是关键动作PDF 抽取的文本经常一行一个换行不合并会让切分器把完整句子切成碎片后续检索的召回质量直接崩掉。参数说明这是批量清洗的底座实际项目里 PDF 会混有表格。表格区域建议用 pdfplumber 的extract_tables()单独抽取再拼成“字段:值”文本图省事直接全文抽取表格内容会变成乱序文本检索时查得到但语义是断的。清洗完成后记得把所有文件统一转成 UTF-8 编码Windows 默认的 GBK 会在后续向量化时代码全部报错。切分这一步直接复用 2.3 的策略按标题层级切每个块记录三样东西——块 id、文本内容、来源文件名统一写进chunks.json。这个文件就是后面向量库的元数据源。3.3 向量化与检索的 Python 实现从 BGE 嵌入到 FAISS 召回知识库构建完成后下一步是把切分好的 chunk 向量化并写入向量库。中文医疗场景我一般选 BGE 系列的中文向量模型它对医学文本的表示能力比通用英文模型好得多。以下是构建向量索引的代码也是“高分毕设”里最能体现工程能力的一段。from sentence_transformers import SentenceTransformer import faiss import numpy as np import json from pathlib import Path def build_vector_store(processed_dir: Path, model_name: str BAAI/bge-small-zh-v1.5): 读取切分好的chunk列表批量向量化并写入FAISS索引。 model SentenceTransformer(model_name) with open(processed_dir / chunks.json, r, encodingutf-8) as f: chunks json.load(f) # [{id: 0, text: ..., source: xxx.md}] texts [c[text] for c in chunks] # normalize后内积等价于余弦相似度检索时可直接比较 embeddings model.encode(texts, batch_size32, normalize_embeddingsTrue) embeddings np.asarray(embeddings, dtypenp.float32) ids np.array([c[id] for c in chunks], dtypenp.int64) index faiss.IndexIDMap(faiss.IndexFlatIP(embeddings.shape[1])) index.add_with_ids(embeddings, ids) faiss.write_index(index, str(processed_dir / faiss_index.bin)) print(f向量库构建完成: {len(chunks)}个chunk, 维度{embeddings.shape[1]})逻辑说明第 11 行normalize_embeddingsTrue之后向量内积就是余弦相似度检索时拿内积当分数直接比较省去一次归一化计算。第 15 行用IndexFlatIP做精确检索毕设阶段的知识库规模几千到几万 chunk完全够用不需要上 HNSW 图索引等数据量超过 10 万 chunk 再考虑换IndexIVFFlat但那是加分项不是必需品。参数说明batch_size32是按内存折中的经验值CPU 机器跑 BGE-small 每批 32 条比较稳内存小就降到 16。模型选 small 而不是 large主要考虑演示环境small 在无 GPU 的笔记本上也能跑得动large 编码一个小知识库要等很久答辩现场卡住非常尴尬。另外faiss.IndexIDMap的 id 必须是np.int64数组直接把 Python 的 int 列表传进去会报类型错误第 13 行的强制类型转换就是为这个准备的。3.4 大模型生成prompt 模板与流式输出检索层把 top_k 个 chunk 找出来后生成层的任务是把这些 chunk 组织成上下文让大模型只依据上下文回答。这里的 prompt 设计决定了回答的“医疗感”也是抑制幻觉最关键的一道闸门。SYSTEM_PROMPT 你是一名医疗知识问答助手。请严格依据给定的知识库内容回答问题。 规则 1. 只使用知识库内容中提供的事实不得使用模型自身记忆补充。 2. 如果知识库内容不足以回答问题直接回答“知识库中未找到相关信息”。 3. 回答中每个关键结论末尾标注来源编号如[来源1]。 4. 不要给出诊断或用药建议只做信息检索与解释。 知识库内容 {context} 用户问题{question} def build_qa_prompt(question: str, retrieved_chunks: list) - str: 把检索结果拼进prompt保留每个chunk的来源编号。 context_lines [] for i, chunk in enumerate(retrieved_chunks, start1): context_lines.append( f[来源{i}] {chunk[text]}\n(来自: {chunk[source]}) ) context \n\n.join(context_lines) return SYSTEM_PROMPT.format(contextcontext, questionquestion)逻辑说明system prompt 里第 1 到 3 条是“硬约束”而不是“建议”。明确禁止模型使用内部记忆补充知识库之外的事实这条约束能把幻觉从根上压住要求标注来源编号回答就天然带引用答辩演示时可以直接指着出处讲。第 4 条限制模型给诊断建议既是对真实场景的安全考虑也是挡住“你敢看病吗”这类提问的话术之一。参数说明{context}字段的长度必须控制。top_k4、每个 chunk 约 400 字时上下文约 1600 字加 system prompt 和问题一次请求约 2000 字正好在大多数大模型服务的高效区间。如果检索到的 chunk 过长在拼 prompt 前按 800 字截断优先保留 chunk 开头——医疗文档的结论通常写在段首。调用大模型时把temperature调到 0.2 以下生成结果更稳定不会同一个问题每次答案都不一样。4. 在本地跑通医疗问答系统环境配置与最小命令4.1 Python 环境与依赖安装先把环境固定住能省掉大量“在我电脑上能跑”的争议。推荐用 Anaconda 给项目建独立环境Python 选 3.10这个版本对 PyTorch 和 FAISS 的兼容性最稳。以下是依赖清单和环境创建命令。conda create -n med_qa python3.10 -y conda activate med_qa # 核心依赖 pip install torch --index-url https://download.pytorch.org/whl/cpu pip install sentence-transformers faiss-cpu fastapi uvicorn pip install pymupdf pdfplumber python-docx jieba rank-bm25依赖里要特别说两个坑torch 的安装源指定 CPU 版如果你只在笔记本上演示没有独立显卡装 GPU 版会拖入几个 GB 的 CUDA 库纯属浪费faiss-cpu 的包名是faiss-cpu不是faiss后者默认指向 GPU 版CPU 机器装到一半会报找不到 CUDA。rank-bm25 提供 BM25 索引实现比自己手写词频统计靠谱。环境装完后先跑一段冒烟验证import faiss能导入、sentence_transformers能加载模型、fastapi 的 TestClient 能发请求。这三项通过说明环境没问题再往下走。顺便说一句Python 环境安装的教程网上到处都是但 RAG 项目真正吃时间的不是装库而是库版本之间的兼容性一次锁定版本号写进 requirements.txt比每次现装要省心得多。4.2 混合检索的融合脚本BM25 与向量结果合并3.3 建的向量库负责语义召回BM25 负责关键词兜底。现在需要一段融合脚本把两路结果合并成一个列表交给生成层。这是 RAG 管线里仅次于 prompt 的第二重要代码。from rank_bm25 import BM25Okapi import numpy as np def rrf_fusion(bm25_scores: dict, vector_scores: dict, k: int 60) - list: RRF融合对同一doc在两份结果列表中的排名取倒数求和。 merged {} for rank, doc_id in enumerate(bm25_scores.keys(), start1): merged[doc_id] 1.0 / (k rank) for rank, doc_id in enumerate(vector_scores.keys(), start1): merged[doc_id] merged.get(doc_id, 0.0) 1.0 / (k rank) # 按融合分从高到低排序返回doc_id列表 return [doc_id for doc_id, _ in sorted(merged.items(), keylambda x: x[1], reverseTrue)] def hybrid_retrieve(question: str, top_k: int 5): 先分路取top50候选再做RRF融合取最终top_k。 bm25_hits bm25_index.search(question, top_k50) vector_hits vector_store.search(question, top_k50) fused rrf_fusion(bm25_hits, vector_hits) return [chunk_map[i] for i in fused[:top_k]]逻辑说明bm25_scores和vector_scores是各自检索器返回的字典键是 doc_id值是对应的原始分数。第 7 行和第 9 行只看排名不看分数所以 BM25 打分几百的量和向量相似度 0 到 1 的量纲差异被完全规避。第 12 行取融合后前 top_k 名。hybrid_retrieve里两路各取 50 条候选避免提前截断。参数说明k60 是 RRF 的经典取值它控制排名权重的衰减速度k 越小排名靠前的文档优势越大你可以在 30 到 90 之间试但对最终 top_5 影响不大。两路的候选池大小也可以调知识库文档噪声多时把 vector_hits 的候选池降到 30减少低质量语义候选对排名的干扰。4.3 启动服务并验证问答效果系统的最小可运行闭环包含三步构建知识库、启动 FastAPI 服务、用 curl 发请求。先把三条命令串起来再打开一个最小 API 入口看一眼请求响应格式。python src/preprocess/builder.py --input data/raw --output data/processed python src/retriever/vector_store.py --data data/processed/chunks.json uvicorn src.api.main:app --host 0.0.0.0 --port 8000 --reload前两条命令会重建 chunks.json 和 faiss_index.bin。毕设阶段的数据规模不需要做增量更新每次换资料全量重建只要几十秒比维护增量索引简单得多。服务启动后用 curl 验证一次问答请求。curl -X POST http://localhost:8000/qa \ -H Content-Type: application/json \ -d {question: 阿司匹林肠溶片与酒石酸美托洛尔能否同服}正常响应是 JSON 格式包含 answer 字段大模型生成的回答、sources 字段命中的 chunk 列表带来源文件名和相似度分数和 retrieval_time 字段。对应到 FastAPI 入口核心代码类似这样from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class QARequest(BaseModel): question: str app.post(/qa) def qa(req: QARequest): chunks hybrid_retrieve(req.question, top_k4) prompt build_qa_prompt(req.question, chunks) answer llm_client.complete(prompt) return { answer: answer, sources: [ {text: c[text], source: c[source], score: c[score]} for c in chunks ], }逻辑说明hybrid_retrieve和build_qa_prompt就是 4.2 和 3.4 里实现的函数llm_client是对大模型接口的封装可以把 API 密钥放进环境变量而不是写在代码里。第 15 行把每个 chunk 的 score 原样返回方便你调试时看分数分布判断某条回答到底是不是检索到的结果。参数说明top_k4是医疗问答的常见取值。top_k 太小知识库里的关键信息可能召不回太大prompt 变长模型容易抓不住重点。对 400 字左右的 chunk4 条上下文加问题约 2000 字生成速度和效果比较均衡。调接口时如果响应里retrieval_time明显偏大超过几百毫秒先检查是不是向量库还是精确检索而数据量已过万再考虑要不要换近似索引。4.4 参数调优top_k、阈值与 chunk_size 怎么调参数调优是这个项目里真正花时间的地方也是答辩时最有话可讲的部分。下面是系统里最值得调的四个参数按影响程度从高到低排。参数位置作用常见取值调试信号chunk_size切分阶段控制每个检索单元长度300-500 字回答细节过多或过散时调chunk_overlap切分阶段保留相邻段落衔接信息60-100 字回答出现断档时调大top_k检索阶段召回 chunk 数量3-6召回不足时调大回答发散时调小score_threshold检索阶段过滤低相似度 chunk0.3-0.5无关内容混入时调高调参有一个固定顺序先固定 chunk_size再在测试集上逐个调 top_k 和 score_threshold最后回头看 chunk_size。原因在于切分粒度决定检索质量的基线top_k 和阈值只能在这条基线上微调。如果所有问题的检索引申都低问题大概率出在切分太碎或文档清洗质量差这时候调 top_k 是徒劳的。一个实用技巧把每个测试问题对应的正确答案来源文档记下来在检索结果里检查该文档是否出现在 top_k 召回中。没出现就是检索参数问题出现了但回答不对才是生成阶段问题。这个简单的归因方法能帮你快速定位调参方向而不是靠肉眼猜。5. 医疗问答系统避坑指南检索偏差、幻觉与效果评估做 RAG 医疗问答踩坑是必然的。这一章列五个最常见的问题每条按“现象 → 原因 → 解决”讲都是跑通系统后最容易遇到的真实故障。5.1 知识库里没有答案大模型还是强行编造现象用户问的问题知识库根本没有涉及但大模型还是给了一段“看起来合理”的回答甚至编出了药品相互作用。原因两层叠加。一是 score_threshold 设得太低检索器把相似度 0.2 的无关 chunk 也送进了 prompt模型误以为材料相关二是 system prompt 约束不够硬模型默认还是走“尽量回答”的老路。解决把 score_threshold 从 0.3 往上调到 0.4 甚至 0.5低于阈值的检索结果直接丢弃同时把 system prompt 里的第 2 条写成“知识库内容不足时只回答知识库中未找到相关信息不要尝试补全”。最后加一道输出校验回答里出现的关键实体药品名、疾病名必须能在检索到的 chunk 文本里找到找不到就降级为“未找到”。这个校验可以作为装饰器包装在生成函数外面实现成本很低。5.2 医疗术语被切分器拆碎BM25 检索直接失效现象检索“阿司匹林肠溶片”时命中的 chunk 断在“阿司匹林肠溶”和“片能降低心脑血管事件”之间BM25 对“阿司匹林肠溶片”这个词组匹配失败。原因切分器按标点和固定长度硬切没有保护医学专名。“阿司匹林肠溶片”这类复合词被切开后关键词检索的分词结果对不上向量检索也会因为语义碎片化而召回漂移。解决切分前用 jieba 加载自定义医学词典把“阿司匹林肠溶片”“酒石酸美托洛尔”这类专名作为整体词加入user_dict切分逻辑改为优先在句号、换行等句子边界处截断而不是字数字符硬切。如果某类药品名反复出现写个小脚本从知识库抽取高频 n-gram 自动生成词典比手写词表省力得多。5.3 向量模型对中文医学专名效果差现象检索“青霉素过敏史”时向量检索召回的 chunk 里有大量和“过敏”无关的内容比如“青霉素类药物的作用机制”BM25 那一路反而命中更准。原因通用中文向量模型对医学实体语义的敏感度不够。“过敏史”和“药物相互作用”在语义空间里距离较远模型没有医学背景区分不出来。解决换用 BGE 系列的中文模型或直接上 bge-large-zh同时依赖 4.2 的混合检索让 BM25 兜底。如果还想进一步优化可以给医学实体加正则前缀索引把“青霉素”“头孢”这类高危词单独建一个关键词表在融合前把命中的文档加权 1.2 倍。这个加权重的小技巧答辩时可以说成“面向医学实体的检索增强”很加分。5.4 知识库里明明有答案回答却说“未找到”现象检索分数很高sources 里能明显看到正确答案来源但大模型回答“知识库中未找到相关信息”。原因prompt 上下文长度被截断。拼接 context 后 token 数超过大模型服务的最大输入限制服务端把末尾内容截掉而答案刚好在截断区也可能是 context 拼接时某个 chunk 里的特殊字符破坏了模板结构。解决在拼接 prompt 前计算 token 数超过 1800 就按“先到先得”截断并保留每条 chunk 的来源编号同时对 context 做转义处理防止 chunk 文本里的花括号或反斜杠干扰模板。建议加一行日志打印实际发送的字符数排查时直接看日志比猜要快。5.5 FAISS 建索引时报错chunk id 类型不匹配现象add_with_ids抛出ValueError: id must be int64索引构建中断。原因chunks.json 里的 id 被 JSON 解析成 Python 的 int但 Python 的 int 是任意精度FAISS 的 C 接口要求显式传入 int64 数组直接传 list 时类型检查过不去。解决构建索引前做一次显式类型转换也就是 3.3 代码里第 13 行的np.array(ids, dtypenp.int64)。这个错误几乎每个项目都会遇到一次属于“知道就不算坑”的类型。顺带提醒chunks.json 里的 id 一旦写入索引就不要随意改动顺序或类型否则检索结果会和元数据对不上那是更难排查的隐性错位。6. 让毕业设计答辩更稳的验证方法构造测试集与评估指标6.1 用滚雪球法构造医疗问答测试集没有测试集参数调优就是盲调。常见的做法是用滚雪球法从知识库里长出一套测试集先从知识库里挑 10 篇代表性文档覆盖药品、疾病、检验单三类各三分之一每篇提炼 5 到 8 个“问题—答案—来源文档”三元组再用这些答案里提到的关联药物、检查项继续提问滚 5 到 6 轮通常能扩出 60 到 100 条问题。每条测试数据三个字段就够了question 是提问文本gold_answer 是人工整理的标准答案gold_doc_id 是答案对应的来源 chunk 编号。不用追求标准的医疗术语患者怎么问就怎么写——“这个药能掰开吃吗”“发烧三天要不要停药”这类口语化问题反而能测出检索层的真实水平。6.2 三个最该跑的指标检索命中率、回答准确率、引文一致性RAG 系统的效果要从检索和生成两端分开看三个指标就能覆盖核心质量。检索命中率看 gold_doc_id 是否在 top_k 召回中回答准确率看生成答案与标准答案的语义接近程度引文一致性看回答里标注的 [来源N] 是否对得上实际引用的 chunk。def eval_retrieval_hit_rate(qa_data: list, retriever, top_k: int 5) - float: 对每个测试问题检查gold_doc_id是否出现在top_k召回里。 hit 0 for item in qa_data: retrieved retriever.retrieve(item[question], top_ktop_k) retrieved_ids {d[id] for d in retrieved} if item[gold_doc_id] in retrieved_ids: hit 1 return hit / len(qa_data)逻辑说明这是一个最朴素也最直观的评估入口。retrieved是检索器返回的 chunk 列表第 4 行只取 id 做集合判断忽略分数——因为分数受向量模型影响而“正确来源在不在候选里”才是检索层质量的真话。第 6 行直接返回命中率你把 top_k 从 3 调到 5看这个值的变化就能判断召回和精度的取舍方向。回答准确率可以用另一个向量模型比如 bge-large-zh分别编码系统答案和 gold_answer取余弦相似度0.7 以上算及格。引文一致性单独写个脚本解析回答里的 [来源N] 标记逐个检查 N 对应的 chunk 是否包含该关键结论的实体。我自己的习惯是每次调完参数把这三个指标跑一遍记录在 README 里答辩时直接报数据比空口说“效果不错”有说服力得多。这条验证路径价值很高值得你花一天时间做扎实希望帮到你。本文还有配套的精品资源点击获取