ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

医疗问答RAG系统构建:从知识库到混合检索的实战指南

医疗问答RAG系统构建:从知识库到混合检索的实战指南 简介这套基于 RAG 与大模型技术的医疗问答系统源码及文档是一份达到高分答辩水准的毕业设计项目主要面向计算机、人工智能、自动化等专业学生与开发者可作为毕设、课程设计或大作业的完整参考。压缩包共75个文件、约84.65MB以Python脚本、Jupyter Notebook、文本与JSON数据及Markdown说明文档为主覆盖问答服务启动、知识图谱构建、数据解析与模型微调等模块。系统将 ChatGLM 微调、Neo4j 图数据库与检索增强生成相结合源码经调试可运行配套的用户登录和管理界面便于演示可帮助读者逐步理解 RAG 医疗问答从数据预处理、模型训练到推理落地的完整链路。已有1114人学习下载附带的 requirements 与说明文档能帮助快速复现环境适合在此基础上继续扩展功能的进阶学习者。1. 医疗问答为什么要绕不开 RAG你拿一个通用大模型问「阿莫西林和头孢的区别」它能给你一条条对比得头头是道但你要是追问一句「信息来源是哪版指南」它就露馅了——要么编一个不存在的文献要么把过期共识当最新结论。医疗场景最大的特点就是知识更新快、表述必须严谨、答错了代价极高。RAG检索增强生成就是来解决这个问题的它让大模型从「背诵记忆」变成「开卷答题」先到知识库里检索相关资料再基于资料生成答案回答的每个结论都能溯源。这套思路不只能做毕设企业里的药品问答、医护辅助查询、健康科普助手本质上都是同一套骨架。适合谁看你手头有这个项目要跑通或者你正在犹豫医疗问答系统的技术选型想搞明白 RAG 和直接 finetune 大模型到底差在哪。2. 医疗知识库的准备为什么 PDF 直出会让 RAG 全军覆没很多人做 RAG 的第一步是下载一批 PDF 说明书和指南然后直接切块塞进向量库结果检索阶段就翻车——查「高血压用药」返回一堆无关段落。问题不在模型在于知识库压根没被处理成「可检索的语义单元」。医疗 PDF 的排版复杂表格多、专业术语密集、层级标题交错直接按字符切块等于把药典撕成纸条再随机捆成一叠检索当然不准。这一章先解决数据侧的问题。2.1 数据源选型与合规边界哪些医疗资料能用作 RAG 知识库常见的做法是优先选三类公开数据源药品说明书NMPA 批准的原版说明书、临床诊疗指南卫健委或各学会发布的公开版本、医学教材的公开章节。这三类内容结构清晰、更新可追溯最关键的是版权风险最低适合毕设和演示场景。这个环节不要贪多也别追求全。我见过不少同学一上来就想把《内科学》整本灌进去最后向量库里一堆重复内容检索时前 20 条结果全是同一段话的变体。更合理的做法是先建一个 200 到 500 份文件的小型知识库覆盖 3 到 5 个常见科室方向比如呼吸科、心内科、消化科、内分泌科每个方向配几份核心指南和常用药品说明书。规模小一点后面做评估时你能人工核实每条答案的准确性。还有一条合规红线需要提前确认知识库只做「医学信息检索与展示」不要碰「诊断建议」和「用药决策」。这不是文字游戏而是系统边界。RAG 的职责是把知识库里的内容检索出来并组织成答案而不是替医生下结论。你可以在系统说明里明确「本系统输出仅供参考不构成诊疗建议」这不是免责甩锅是医疗问答系统的基本边界。2.2 从 PDF 到结构化 Markdown预处理脚本与表格陷阱医疗 PDF 最大的敌人不是扫描件而是「文字版 PDF 的阅读顺序是乱的」。很多药品说明书是双栏排版pdfminer 或 PyMuPDF 直接提取文本时会把左栏读完后跳到右栏造成段落被拆散、表格数据变得完全不可读。所以预处理的第一原则是分栏处理和表格识别。我一般用 PyMuPDF 做第一轮转换先检测页面的文本块坐标再根据坐标排序还原阅读顺序。下面是核心思路你可以直接抄import fitz # PyMuPDF def pdf_to_markdown(pdf_path: str) - str: doc fitz.open(pdf_path) md_lines [] for page in doc: # 提取文本块和坐标 blocks page.get_text(dict)[blocks] line_items [] for b in blocks: if lines in b: for line in b[lines]: text .join(span[text] for span in line[spans]).strip() if text: # 记录每个文本块的 y 坐标和 x 坐标用于还原阅读顺序 line_items.append((line[bbox][1], line[bbox][0], text)) # 按 y 坐标行排序再按 x 坐标列排序 line_items.sort(keylambda item: (item[0], item[1])) # 检测同一行内是否存在跨越半页的间距有则插入分栏标记 for idx, (y, x, text) in enumerate(line_items): if idx 0 and abs(y - line_items[idx - 1][0]) 15: md_lines.append() md_lines.append(text) doc.close() return \n.join(md_lines)这段代码的作用是把 PDF 里的文本块按坐标还原成正确阅读顺序。get_text(dict)返回页面内每个文本块的精确位置按 y 坐标排序后双栏排版的内容会被还原成从上到下、从左到右的正确顺序。abs(y - line_items[idx - 1][0]) 15这个阈值用于判断是否出现了分栏或标题跳变遇到这种跳变就插入一个空行方便后续 Markdown 解析。但这里有个关键坑表格。药品说明书的「用法用量」「不良反应」经常是表格形态PDF 文本提取会把表格拆成一堆散落的数字和文字。遇到这种情况我的建议是不要试图在 PDF 层还原表格而是先初步转成 Markdown再用人工或规则补充的方式把关键表格手动整理成 Markdown 表格。毕设场景里药品说明书的表格总量并不大花半天时间手工整理 30 份核心文件的表格比写一个完美的表格识别器划算得多。这是典型的「算法成本 vs 人工成本」取舍技术方案要服务于交付节奏。2.3 面向语义的切片策略按标题层级切而不是按字数硬切切片是 RAG 里最容易被低估的环节。很多教程让你用 LangChain 的RecursiveCharacterTextSplitter按 500 字切块、重叠 50 字这套参数在通用文档上能用但放到医疗指南上会切出大量「半个结论」——比如把「禁忌症」的完整列表从中间切断前半段说「禁用」后半段全是「慎用人群」检索时模型只看到一半信息生成的答案自然断章取义。正确思路是先识别文档的标题层级再以语义章节为基本单位切片。药品说明书天然有「适应症」「用法用量」「不良反应」「禁忌」这些固定小节指南也有「推荐意见」「证据等级」的结构化分层。切片的逻辑应该是优先保留完整小节其次处理过长的小节。import re def split_by_heading(markdown_text: str, max_chunk_size: int 800, overlap: int 80): # 识别 markdown 标题# 或 ## 开头 headings list(re.finditer(r^#{1,3}\s.$, markdown_text, flagsre.MULTILINE)) chunks [] for i, match in enumerate(headings): start match.start() end headings[i 1].start() if i 1 len(headings) else len(markdown_text) section_text markdown_text[start:end].strip() # 小节内容较短直接整段作为 chunk if len(section_text) max_chunk_size: chunks.append({source: match.group(), text: section_text}) else: # 小节过长滑动窗口切分保留重叠 step max_chunk_size - overlap offset 0 while offset len(section_text): chunk section_text[offset:offset max_chunk_size] chunk chunk.strip() if chunk: chunks.append({source: match.group(), text: chunk}) offset step return chunks这段切片逻辑的关键在于source字段保留了章节标题后面生成阶段做引用时可以直接把这个标题当作信息来源展示给用户。max_chunk_size800经验值来自中文医学文本的特征一个完整的用药说明条目大约 300 到 600 字800 能覆盖大多数情况超过这个阈值再滑动切。overlap80是滑动窗口的重叠区防止句子在切缝处断裂导致语义缺失。切完后记得做一步质检把「禁忌」和「适应症」这两个字段的切片单独拉出来检查如果发现某条切片里只有「禁」或只有「用」说明切得太碎要把这个章节整体作为一个大 chunk 保留哪怕长度超了也比切碎强。RAG 的问题里「信息不完整」对答案准确性的伤害远大于「信息冗余」。3. 向量化与检索链路Embedding 选型到混合检索知识库准备好了下一步是把文本变成向量。很多人以为这一步就是调一个embedding接口的事实际上 Embedding 模型选型、向量库选型、检索策略三个环节每一个都决定最终问答质量的上限。这一章把检索链路的四个关键点拆开讲清楚。3.1 Embedding 模型选型通用中文模型在医学词上的表现差异中文医学文本的 Embedding 有一个特有问题专业术语的语义相似度并不等于文本表面的字面相似度。「阿司匹林」和「乙酰水杨酸」在字面上毫无重叠但在医学语义上完全等价。通用中文 Embedding 模型如常见的 text2vec、m3e在训练时接触的医学语料有限对这类同义词关系的表征能力偏弱。我一般会在两个方向上做测试开源的 BGE 系列bge-large-zh和阿里云的 text-embedding-v2 这类商业化接口。BGE 的优势是本地部署、可微调且在多语言和中文检索基准上有公开数据支撑商业化接口的优势是开箱即用对医疗文本的泛化通常比通用开源模型好一些。选型没有标准答案但有一个验证方法必须做拿 20 组医疗同义词对药品通用名 vs 商品名、疾病名称 vs 俗称、症状描述 vs 医学术语分别计算 cosine 相似度看模型能不能给出合理的高分。from sentence_transformers import SentenceTransformer import numpy as np model SentenceTransformer(BAAI/bge-large-zh-v1.5) pairs [ (阿司匹林, 乙酰水杨酸), (高血压, 血压升高), (心梗, 急性心肌梗死), (糖尿病, 2型糖尿病), ] for text1, text2 in pairs: emb1 model.encode(text1, normalize_embeddingsTrue) emb2 model.encode(text2, normalize_embeddingsTrue) sim np.dot(emb1, emb2) print(f{text1} - {text2}: {sim:.4f})这段代码用余弦相似度检验 Embedding 模型对医学同义词的敏感度。normalize_embeddingsTrue把向量归一化到单位长度这样点积结果就是余弦相似度取值范围在 -1 到 1 之间。如果你的测试结果里「阿司匹林」和「乙酰水杨酸」的相似度在 0.6 以下说明这个 Embedding 模型对医学概念的表征不够你需要换模型或者在检索层做额外补偿比如后面会提到的同义词改写。3.2 向量库选型毕设用 FAISS还是上 Milvus向量数据库这个环节很多人纠结选型实际上完全看你的场景规模。FAISS 是 Meta 开源的向量检索库以库的形式嵌入你的 Python 进程不需要单独部署服务适合单机、小规模十万级向量以内场景。Milvus 是完整的向量数据库系统支持分布式、数据持久化、多租户适合生产环境和数据量持续增长的场景。对毕设或者企业内部的 Demo 系统我的建议是直接用 FAISS本地建索引、内存检索部署成本几乎为零。Milvus 需要单独启动服务、管理集合和索引虽然功能强大但对你验证 RAG 的核心逻辑没有直接帮助。from sentence_transformers import SentenceTransformer import faiss import numpy as np embedder SentenceTransformer(BAAI/bge-large-zh-v1.5) # 假设 chunks 是上一步切片后的列表每个元素有 text 字段 chunk_texts [chunk[text] for chunk in chunks] chunk_vectors embedder.encode(chunk_texts, normalize_embeddingsTrue, show_progress_barTrue) # 建立 FAISS 索引 dim chunk_vectors.shape[1] index faiss.IndexFlatIP(dim) # 内积索引配合归一化向量等价于余弦相似度 index.add(chunk_vectors.astype(np.float32)) # 检索 query 高血压患者能不能服用阿司匹林 query_vec embedder.encode([query], normalize_embeddingsTrue) scores, indices index.search(query_vec.astype(np.float32), k10) for rank, (score, idx) in enumerate(zip(scores[0], indices[0])): print(fTop {rank 1} (score{score:.4f}): {chunk_texts[idx][:80]}...)IndexFlatIP是内积索引配合归一化向量就是余弦相似度检索在十万级数据量下毫秒级返回。这里没有用IndexIVFFlat或IndexHNSW是因为数据量不够大时暴力检索的准确率最高而且速度完全够用。k10先多召回几条后续用重排序模型精排。3.3 混合检索BM25 精确匹配和向量召回怎么互补向量检索擅长语义匹配但医学场景里很多查询是「术语精确匹配」。比如用户问「替诺福韦的肾毒性」向量检索可能因为「替诺福韦」在句子里权重不够召回一堆「抗病毒药物的不良反应」的宽泛内容反而把精确提到替诺福韦的那条说明书埋没了。这时候 BM25经典的关键词匹配算法能直接按词频和逆文档频率把精确匹配的文档顶上来。混合检索的常见做法是向量检索和 BM25 各自取 top N合并后按加权分数排序。BM25 在 Python 里直接用rank_bm25库或者 Elasticsearch 的bm25评分。我一般用rank_bm25轻量、无需额外服务from rank_bm25 import BM25Okapi import jieba # 对知识库切片做分词需要加载医学词典 tokenized_docs [list(jieba.cut(doc[text])) for doc in chunks] bm25 BM25Okapi(tokenized_docs) query_tokens list(jieba.cut(query)) bm25_scores bm25.get_scores(query_tokens) # 合并分数向量分数和 bm25 分数先各自归一化到 0~1 vec_scores scores[0] / max(scores[0]) # 归一化 bm25_norm bm25_scores / max(bm25_scores) combined_scores 0.6 * vec_scores 0.4 * bm25_norm这段混合检索代码的关键是权重配比。0.6给向量语义召回0.4给 BM25 精确匹配这个比例是我在医疗问答场景里的经验值不是一个固定最优解。你可以做一个简单的调参实验拿 30 个查询分别测纯向量、纯 BM25、不同混合比例的召回准确率挑最优的一组。实践里常见的效果差异是纯向量召回时「阿司匹林 vs 乙酰水杨酸」这类同义词问题靠语义能兜住但精确药名查询容易被埋没混入 BM25 后精确匹配的能力立刻体现。分词这步有个前提工作必须做把药品名、疾病名、症状术语加入 jieba 的自定义词典。比如「替诺福韦」如果不加词典会被切成「替诺」「福韦」BM25 匹配必然失败。这个坑极其常见。3.4 重排序把 top_20 收窄到 top_5 的关键一步混合检索召回 top 20 之后直接全部塞给大模型生成效果通常很差——不相关的资料会干扰大模型的注意力导致答案偏离正确方向。正确做法是加一步重排序Rerank用一个专门的交叉编码器模型把 query 和每条召回文本成对打分取 top 3 到 5。交叉编码器Cross-Encoder和双编码器Bi-Encoder的区别在于双编码器把 query 和文档分别编码成向量再算相似度快但精度低交叉编码器把 query 和文档拼接后一起过模型慢但精度高。重排序阶段数据量已经从 20 降下来了用慢一点的交叉编码器完全可接受。from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-base) pairs [(query, chunk_texts[idx]) for idx in indices[0]] rerank_scores reranker.predict(pairs) # 按重排序分数重新排 reranked sorted( zip(rerank_scores, indices[0]), keylambda x: x[0], reverseTrue ) # 取 top 5 作为生成阶段的参考材料 top_results [(idx, score) for score, idx in reranked[:5]] for idx, score in top_results: print(fRerank score{score:.4f}: {chunk_texts[idx][:80]}...)bge-reranker-base是 BGE 系列里的重排序模型输入是「问题 候选文本」的拼接。这里reranked[:5]是最终送入生成阶段的上下文。重排序这步是 RAG 质量的隐形杠杆很多系统向量检索跑得好好的答案质量上不去就是省略了这步。医疗场景尤其需要——同样的一个「注意」条目重排序能把更贴近用户具体病情的片段排到前面。4. 生成环节用 Prompt 约束大模型只做「开卷答题」检索链路通了最后一个环节是让大模型基于检索到的资料生成答案。但如果你直接把资料拼接进 Prompt 然后丢给模型很快会发现两个问题模型还是会自行发挥编造内容或者它引用的内容不在你给它的资料里。这一章讲怎么用 Prompt 塑形和结构化输出来约束生成行为。4.1 模型选型开源本地部署还是 API 调用这一步取决于你的运行环境和预算。API 路线用 Qwen 或 GLM 的中大杯模型上下文长度 32K 以上回答质量和遵循指令的能力都更强本地路线用 Ollama 部署 Qwen2.5-7B-Instruct 这类 7B 级开源模型显存需求在 8 到 16 GB 之间部署简单但回答质量会弱一些尤其在遵循「只使用参考材料」这类约束时容易出现偏差。医疗 RAG 项目里我的建议是7B 的开源模型足够用来验证流程但如果要拿给导师或客户演示API 路线会稳得多。模型能力在这个系统里不是主角RAG 才是但模型的指令遵循能力直接决定 RAG 效果能不能兑现。# 本地部署 Qwen2.5-7B-Instruct需要 16GB 显存或以上 ollama pull qwen2.5:7b # 启动服务 ollama serve上面是本地部署的最小命令。ollama pull拉取模型权重ollama serve启动本地 OpenAI 兼容接口。如果你没有 GPU 环境直接走 API 接口代码里唯一变化就是把base_url和api_key换成对应厂商的配置。4.2 Prompt 塑形系统角色、引用编号与强制拒答生成阶段的 Prompt 设计直接决定模型是「忠实转述」还是「自由发挥」。核心有三条约束明确系统角色是医学信息助手而非医生要求模型只使用参考材料中编号对应的内容遇到材料中没有的信息必须明确说「资料库中未找到相关信息」而不是自己补。下面是我打磨过的 Prompt 模板prompt_template 你是一个医学信息助手。你的任务是基于【参考材料】回答用户问题。 要求 1. 只能使用参考材料中的信息不得使用自身知识补充。 2. 回答中每个关键结论必须标注引用格式为[来源编号]来源编号对应参考材料中每条资料的编号。 3. 如果参考材料中没有相关信息直接回复资料库中未找到相关信息。 4. 不提供诊断结论和用药建议只陈述资料中的既有信息。 【参考材料】 {context} 【用户问题】 {question} 请回答 def build_prompt(query: str, top_results: list) - str: context_parts [] for idx, (chunk_idx, _) in enumerate(top_results): source chunks[chunk_idx][source] text chunks[chunk_idx][text] context_parts.append(f[来源{idx 1}]《{source}》\n{text}) context \n\n.join(context_parts) return prompt_template.format(contextcontext, questionquery)这个模板的巧妙之处在于把引用编号和资料内容绑定模型只要按照格式输出[来源1]你就能在后端准确回溯到知识库里的原始文档位置。context按[来源1]、[来源2]的格式组装一方面给模型清晰的材料边界另一方面让引用回溯成为可能。强制拒答是医疗系统的安全底线这不是为了面试表演是因为在真实场景里「资料库中没有相关信息」这个回答比模型硬编一个结论要安全得多。注意模板里写的是「陈述资料中的既有信息」不是「根据资料回答」。这两者区别很大前者要求模型做信息搬运工后者给了模型发挥的空间而发挥就意味着可能添加资料之外的内容。4.3 结构化输出把答案、来源、免责声明一次解析出来生成阶段如果用纯文本输出后续做前端展示、来源回溯、评估对齐都会很麻烦。更好的做法是要求模型输出 JSON 结构一次性拿到答案正文、引用来源列表和是否命中资料的标志。{ answer: 根据资料阿司匹林可用于高血压患者的二级预防但需注意出血风险。, sources: [1, 2], has_answer: true }这套 JSON 结构的关键是has_answer字段。当模型在检索阶段没有找到相关资料时它应该输出has_answer: false前端就可以直接展示「未找到相关信息」的默认提示而不是渲染一段「资料库中未找到相关信息」的散文。这样处理后端判断逻辑简单前端展示也干净。5. 医疗 RAG 的五个高频坑现象、原因、解决RAG 系统的坑往往不在模型而在工程细节。这一章把我做医疗问答时碰到最多的五个坑按「现象 → 原因 → 解决」列出来每条都是血泪经验。5.1 PDF 提取出的文字顺序错乱切片全切错了现象知识库建好后检索「高血压」召回的是「注意事项」里的杂散文字而不是「适应症」里的内容。原因PDF 双栏排版的文本块坐标还原做漏了。有些 PDF 是表格混排表格单元格的坐标和文本流乱序标记排序后仍然有错位。解决预处理阶段增加表格区域检测把表格用page.find_tables()单独提取并转换成 Markdown 表格然后从文本流中剔除表格区域再排序。这样文本和表格分别处理避免互相干扰。这个事没有捷径你需要把知识库里的 PDF 按版式分类先处理掉大头剩下的边缘案例手工修正。5.2 Embedding 无法识别「阿司匹林 乙酰水杨酸」现象用户问「乙酰水杨酸对胃的刺激」检索结果里没有「阿司匹林」相关的说明书内容。原因通用 Embedding 模型对医学同义词的表征能力弱字面差异大的两个术语在向量空间里距离很远。解决第一道兜底是混合检索BM25 的精确匹配能解决字面一致的问题第二道兜底是建同义词表做查询改写把用户问题的同义词扩展后一起检索。这两层加上同义词问题基本能覆盖绝大多数场景。不要指望 Embedding 模型自己解决这个问题医疗术语的同义词网络太复杂。5.3 表格数据切块后变成散行语义全丢现象检索「头孢类药物的用法用量」返回的切片是「成人一次0.5g」「儿童每日按体重」这样散落的行没有上下文。原因切片逻辑按文本流切分没有感知到「表格是一个完整语义单元」。Markdown 表格的每一行拆开都不是完整信息必须整体保留。解决在切片阶段加一个规则识别|开头和|---分隔符之间的连续行整个表格作为一个 chunk不允许在表格内部切分。如果表格过长超过切片长度限制宁可按行组切比如前 3 行一组、后 3 行一组也要保证每组都有表头。5.4 检索结果越多答案质量反而越差现象top_k 从 5 调到 20 后答案变得冗长且开始出现相互矛盾的内容。原因大模型的注意力在长上下文里会被无关信息稀释。你塞进去 20 条资料只有 2 条有用时模型很难分辨该信哪条。RAG 的核心不是「给更多资料」而是「给最精确的资料」。解决重排序后强制收窄到 5 条以内Prompt 里明确「只使用参考材料中的信息」。如果 5 条里有明显不相关的那是重排序模型的问题需要换更强的 reranker而不是把 top_k 调大。5.5 模型一本正经地编了一个不存在的文献现象问答系统给出了答案但引用的来源编号对应的资料里根本没有那句话说。原因模型的幻觉在 RAG 里依然存在尤其当指令约束不够强时。如果 Prompt 里没明确「只能使用参考材料」模型会调用训练时见过的医学知识来「润色」答案。解决第一是 Prompt 约束见第四章第二是后端加一层校验——把模型输出的核心断言逐个和参考材料里的原文做一致性比对常用的做法是让一个小模型对「断言-原文」对做二分类判断。这一步是医疗问答系统上生产环境的必备检查毕设阶段至少要做人工抽查。6. 评估与继续迭代用 30 道题暴露 RAG 的真实短板最后一个环节是评估。很多 RAG 项目交付后就没人管了问题在于「感觉效果还行」不算验收标准。搭建一个最小评估集能帮你回答两个关键问题当前系统在哪些类型的问题上表现最好哪些问题完全答不了。这决定了下一步优化往哪使劲。6.1 构建评估集三类问题的比例怎么定评估集至少要覆盖三类问题一是「直接抽取型」比如「药品 X 的禁忌症是什么」这类问题答案在知识库里是现成的考验检索准不准二是「多源综合型」比如「高血压患者用阿司匹林需要注意什么」需要从多条资料中综合信息三是「超出知识库型」比如问一个知识库里完全没有的新药考验系统敢不敢拒答。规模不用大30 道题足矣但每道题需要人工标注三个字段标准答案、预期引用的来源文档、是否在知识库范围内。标注工作大概半天时间但这半天价值极高。def evaluate(rag_system, eval_set): results [] for item in eval_set: answer, sources, has_answer rag_system.answer(item[question]) # 检索召回评估答案引用的来源是否出现在预期来源中 retrieval_hit any(str(exp) in sources for exp in item[expected_sources]) # 正确性人工打分1~5 correctness item.get(human_score, 0) # 拒答准确率库外问题应该回答 has_answerfalse correct_reject (item[in_kb] False) and (has_answer False) results.append({ question: item[question], retrieval_hit: retrieval_hit, correctness: correctness, correct_reject: correct_reject }) return results这个评估函数拆开看就三个维度。检索命中评估的是「想要的资料有没有被检索出来」这是 RAG 的地基地基没打牢后面一切都不用谈。正确性需要人工打分因为医疗答案对错不是简单字符串匹配能判定的。拒答准确率专门评估「不知道时敢不敢说不知道」这是医疗系统和普通问答系统的分水岭。6.2 Bad Case 归因先分清楚是检索的锅还是生成的锅评估做完你会得到一批答错的题。这时候最重要的不是急着调参而是先归因。方法很简单看答错的那道题——把它的正确答案对应的资料单独检索一次如果检索结果里没有就是检索端的锅切片、Embedding、重排逐段排查如果检索结果里有但生成的答案还是错的就是生成端的锅Prompt 约束不够、模型能力不足。我见过大量团队在一个难缠的问题上反复调 Prompt最后发现是切片把关键信息切断了方向错了整个白干。归因之后处理路径完全不同检索端问题改切片策略和检索参数生成端问题改 Prompt 模板和模型选型。一次 Bad Case 分析至少能暴露一个具体环节的具体缺陷比盲目调top_k有效得多。6.3 进阶方向HyDE、RAG-Fusion 和知识图谱的边界如果基础 RAG 流程已经稳定想继续往深处做有两个方向值得投入。HyDEHypothetical Document Embeddings是让大模型先为问题生成一个虚构的参考答案再用这个答案去检索——在问题表述和知识库表述差距大的时候能显著提升召回率。RAG-Fusion 是同时用多个改写后的查询去检索再合并结果适合用户提问口语化严重的时候。这里要顺便回应一个检索热词里的常见困惑RAG 知识库和 KG知识图谱知识库怎么选。RAG 适合非结构化的文档检索你说不清楚问题会怎么问但它能从文本里找语义KG 适合实体关系查询比如「哪些药物和 X 药物存在相互作用」这类问题在普通 RAG 里很难通过文本相似度回答但如果你预先建了药物相互作用图谱就能直接查出来。医疗领域两者经常配合使用所谓ontology RAG的思路就是把概念层级结构叠加到检索结果上限制检索范围并过滤歧义。6.4 一个值得投入的验证习惯每次改动后固定跑评估集我最后想分享一个工作习惯任何改动换模型、调参数、加数据之后固定跑一遍评估集把三个维度的数字记录下来。这个习惯会让你很快建立直觉——什么改动真正有效什么改动只是感觉上有效。我做医疗 RAG 项目时最深的教训就是前期总在调 Prompt 和换模型后来发现检索端的效果对答案质量的杠杆更大改用评估集驱动之后系统走向才开始变得可预期、可把控。希望帮到你。本文还有配套的精品资源点击获取
返回列表