拓冰建站拓冰建站
首页 / 资讯中心 / 正文

医疗RAG调优实录:从知识分段到混合检索与溯源提升21.7%

做过医疗垂直场景 RAG 的朋友应该都有体会同样的框架跑百科类知识库效果还不错一旦换到医学文档各种问题就冒出来了。术语被切碎、检索召回的片段对不上、答案看起来顺滑但找不到出处……我在做医疗问答系统的过程中踩过的坑差不多能写一本手册。这篇文章梳理的是项目 v21.7 迭代中从知识分段、混合检索到重排溯源的全链路调优实践核心成果是把答案溯源覆盖率从 68.3% 提到了 90.0%也就是标题里“21.7”的由来——21.7 个百分点。整个过程涉及文本分段策略、BM25向量混合检索、重排模型选型、引用溯源链路适合正在做医疗 AI、企业知识库或 RAG 应用的同学参考。1. 医疗垂直场景 RAG 的“坑”从哪来1.1 通用 RAG 在医疗场景翻车的原因很多人第一次把通用 RAG 框架迁移到医疗场景时会很困惑embedding 模型没换、向量库没换、Prompt 模板也没换为什么效果断崖式下跌我的经验是问题几乎都出在“文档特性”和“检索目标”的错位上。医疗文档有几个显著特点。第一是术语密集且实体很长比如“急性ST段抬高型心肌梗死”这种实体如果按固定 512 字符切块很容易从中间切断导致语义碎掉embedding 出来的向量既不完整也不稳定。第二是文档权威性分层极其明显临床诊疗指南、专家共识、药品说明书、医学科普文章可信度完全不是一个量级检索系统如果一视同仁低质量内容会把高质量内容挤下去。第三是断言的严谨性要求医疗答案里每一句话都应当有据可查模型不能像闲聊机器人那样说“可能”“大概”就完事。第四是时效性医学指南会定期更新同一主题的旧版本文档如果没被过滤检索时很可能命中过时信息。这四点叠加起来通用 RAG 的“检索一段相似文本 拼进 Prompt 让模型生成”的思路就显得过于简单了它只解决了“有没有相关内容”的问题没解决“内容是否可信、是否完整、是否最新、能不能引用”的问题。1.2 医疗 RAG 必须满足的三个硬性要求在做这个项目之前我和团队花了很长时间梳理需求最后沉淀出三个硬性要求后面所有的技术选型都是围绕它们展开的。硬性要求说明对技术链路的影响可溯源答案中的每个关键断言都能对应到知识库中的原始片段切块时必须保留稳定的文档 ID、章节 ID生成时要输出引用标记可拒答知识库没有足够依据时系统要明确说“不知道”不能硬答检索和重排之后需要置信度判断低分片段直接拦截可审核医生的审核流程中必须能方便地跳转到原文核对引用是否被曲解前端展示引用原文生成后的引用要做一致性校验顺带说一句合规问题这类系统在真实落地时定位通常是“辅助信息检索与证据整理”不输出诊断结论也不替代医生决策。系统设计上要留出“医生审核”这一道人工环节提示语里也要把边界说清楚。这不是保守是医疗场景的基本要求。2. 知识分段医疗文档切块不是“按字数切”那么简单2.1 医学文档的结构特点与切块原则我开始做 v1 版本时采用了最省事的固定字符切块结果被医疗文档狠狠教育了一课。临床指南里最关键的“推荐意见”往往是一个带编号的短段落后面跟着证据等级和参考文献药品说明书里“用法用量”可能是一张表格教材里一个疾病诊断标准会跨好几个段落。固定长度切块的本质问题是切出来的块不是“语义完整的最小单元”而是“字符数量恰好相等的连续文本”两者在检索场景下的表现天差地别。后来我们定下的切块原则只有一条切出来的每个块必须能独立回答某个类型的问题。听起来简单落地时要对文档做结构化解析。对 PDF 格式的指南先用解析工具把版面转成带标题层级的 Markdown对 docx 格式的教材直接读取标题样式。然后以标题层级为锚点一级标题、二级标题作为块的边界块内如果太长再按句号边界切分。切出来的子块如果太短比如只有一两句话就向上合并到父级块避免产生大量碎片信息。2.2 实操中的切块策略与参数选择这套策略在实现时的大概流程是这样先做文档结构解析把 PDF、Word 转成 Markdown保留标题层级。以“二级标题”为默认切块边界二级标题下的内容为一个候选块。候选块长度判断少于 150 字向上并入一级标题块超过 2000 字在句号、问号处再次分割。切块时保留 128 字符的重叠窗口避免跨段的语义承接被切断。每个块生成并存储元数据doc_id、chapter_id、标题、版本号、发布时间、来源类型指南/说明书/教材/科普。这里重点说下重叠窗口的作用。医学描述里经常出现“该病患者应避免剧烈运动尤其是在急性期之后”这种句子前半句和后半句关联很强如果刚好被窗口边界切开后面单独检索到“尤其是在急性期之后”时语义是不完整的。重叠窗口并不能让切块完全避免这种问题但能显著降低边界切错的概率。128 字符是我实测下来成本和收益比较平衡的值重叠太少保护不足重叠太多会引入大量重复块检索时同一内容反复出现白白占向量库容量。2.3 实测对比与避坑在内部 1000 条医疗问答测试集上我们对比过几种切块策略的召回效果口径是“检索到能支撑答案的片段”的命中率切块策略检索命中率备注固定 512 字符无重叠52.6%基线术语切断问题严重固定 1024 字符 128 重叠58.3%有所提升但表格和剂量信息仍会被切断结构感知切块 128 重叠67.1%标题层级锚点完整保留语义单元结构感知 表格单独抽取68.8%表格作为独立块命中率进一步提升表格单独抽取这一点容易被忽视。药品说明书里的“用法用量”表、指南里的“危险分层”表一旦被转成线性文本再切块行列对应关系全乱检索到也读不懂。我们把表格区域单独识别出来按“表头行内容”转换为结构化文本后作为一个独立块存储效果立竿见影。还有一个很隐蔽的坑是引用编号。学术性医疗文档里大量出现“根据相关研究[3]”如果切块时把“相关研究”和“[3]”拆到两个块里溯源时就找不到证据来源。所以切块后我们会单独做一步后处理把段落末尾的引用编号列表连接到对应文本块的元数据里而不是让它散落在文本中间。我后来总结了这样一句话分段是检索的上限如果切块阶段就丢了信息后面接什么检索模型、重排模型都救不回来。这也是为什么我坚持把“知识分段”放在全链路优化的最前面。3. 混合检索与多路召回为什么 BM25 和向量检索必须一起上3.1 单一检索方式在医疗场景的失效案例当我们把切块优化到一定程度后瓶颈自然转移到了检索环节。最开始我们以为向量检索已经够用结果遇到两类典型问题。第一类是术语变体问题。用户问“心梗后多久可以恢复正常工作”文档里写的是“心肌梗死患者出院后建议休息时间”query 里的“心梗”和文档里的“心肌梗死”在字面上不匹配embedding 虽然能把语义拉近但 BM25 这种稀疏检索在这里就无能为力。反过来用户问“阿莫西林一次吃几片”这种带有精确剂量和药名的问题时向量检索对数值的敏感度不够经常把“0.5g”“每日两次”这种关键信息模糊掉而 BM25 能精确命中这些字面信息。第二类是缩略语问题。医疗场景有大量英文缩写ACS、STEMI、NSTEMI、COPD……用户可能用中文提问也可能直接丢缩写还可能把缩写和全称混在一起。单一检索方式很难同时处理精确匹配和语义改写这两类需求。这也是“混合检索”在医疗场景几乎成为必选项的原因。3.2 混合检索与多路召回的实现方案我们的方案是“BM25 稀疏检索 向量稠密检索”双路召回再加一路基于规则的查询改写具体流程如下查询预处理先做分词对医疗缩写做同义词扩展。比如“STEMI”扩展为“ST段抬高型心肌梗死”“心梗”扩展为“心肌梗死”扩展后的多个 query 分别送检索。稀疏检索BM25 在倒排索引上精确召回主要解决药名、剂量、检查指标这类对字面匹配敏感的问题。稠密检索用中文医疗语料微调过的 embedding 模型对 query 和切块向量做余弦相似度召回主要解决同义改写、口语化表述的问题。多路结果合并对 BM25 结果和向量结果分别取 top100然后用 RRFReciprocal Rank Fusion融合。RRF 的公式很简单每个文档打分为所有检索通路中 1/(k rank) 的累加k 通常取 60。关于 RRF 我想多说一句很多人一开始会倾向做分数归一化然后加权求和但实测下来不同检索通路的分数分布差异很大权重很难调而 RRF 只用排名信息天然规避了分数不可比的问题鲁棒性更好。我们在医疗数据上对比过RRF 的稳定性明显优于手工加权求和的方案。在工程集成的层面向量库我们用的是 Milvus它原生支持稠密向量和稀疏向量的混合检索可以比较自然地把 BM25 和向量召回统一到一套 API 里。如果团队技术栈是 Javalangchain4j 和 spring-ai 都对 Milvus 有比较成熟的集成langchain4j 里可以通过 EmbeddingStore 接口对接 Milvus检索链路里串联 DocumentSplitter、QueryTransformer 这些组件实现同样的“多路召回重排”流程。我们部分服务用的是 Java所以这套链路在两种技术栈下都跑通过。3.3 粗召回数量与参数实测这里给出几个我们实测后觉得比较稳的参数值可以作为起步参考粗召回量BM25 top100 向量 top100融合后取 top50 送重排。RRF 参数k60效果稳定k 太小会让排名靠前的文档获得过大权重k 太大则各路召回的区分度被稀释。查询扩展数量每个 query 扩展出 2~3 个变体即可盲目扩太多会把检索质量拉低。采用混合检索后内部测试集的检索命中率从 68.8% 提升到了 73.6%增量主要来自“术语变体”和“缩写查询”这两类 case。这个结果让我们确信在医疗垂直场景混合检索不是锦上添花而是刚需。4. 重排从粗召回 top50 到精排 top54.1 为什么必须有一层重排粗召回阶段无论是 BM25 还是向量检索用的都是“快速但粗粒度”的匹配方式。向量模型为了效率把文档压缩成一个向量天然会损失细节BM25 则是纯字面匹配。两者召回的结果里往往混杂着“主题相关但并不是答案直接依据”的片段。如果直接把 top50 全塞进大模型上下文不仅浪费 token还会因为干扰信息太多拉低答案质量。所以我们在粗召回和生成之间加了重排环节。重排模型采用的是交叉编码器cross-encoder结构把 query 和一个候选片段拼接起来输入模型做深度语义匹配打分。相比向量检索阶段的双编码器bi-encoder交叉编码器能看到 query 和片段之间更细粒度的交互精度高不少代价是速度慢。所以正确的用法是用快速检索做粗召回保证不漏用交叉编码器做精排保证头部结果准。4.2 重排模型选型与实测关于 2b 和 4b 的差距这部分是不少读者私信问得最多的医疗场景重排模型该选多大参数通义 2b 重排模型和 4b 重排模型差距大吗我的回答是差距确实存在但要看你的优化目标是什么。我们同时测试了 2b 和 4b 两个规模的重排模型也在业务测试集上做过对比。结论可以分成三点第一效果上4b 在 nDCG10 指标上普遍比 2b 高出 3~5 个百分点尤其是涉及多条件约束的查询比如“糖尿病患者合并肾功能不全时降压药如何选择”这类需要同时考虑多个医学条件的问题4b 的理解能力明显更强。第二资源消耗上4b 的显存占用和推理延迟差不多是 2b 的两倍。我们线上服务的响应时间预算有限2b 重排生成全链路能压在 3 秒以内换成 4b 之后延迟涨了约 40%。第三端到端影响上在我们的医疗问答集上4b 比 2b 给溯源覆盖率带来的提升大约是 1.2 个百分点。换句话说重排模型加大参数带来的收益是真实存在的但没有检索命中率提升那么显著。基于这些测试我们最终采用的是“线上 2b、离线 4b”的组合线上服务用 2b 保证响应速度关键数据集评测和效果回归时用 4b 得出更准确的上限结论。如果你的业务对响应时间不敏感且 GPU 资源充裕直接上 4b 是完全合理的选择。另外在榜单之外我还建议大家关注重排模型的输入长度限制。医疗文档的切块经常超过 512 token重排时直接截断会丢掉尾部关键信息。我们的做法是把候选片段截断到模型支持的最大长度以内同时在元数据里保留完整文本如果重排发现某个片段的高分信息部分在尾部再由后面生成的溯源校验环节兜底验证。4.3 重排后的截断与置信度判断重排完成后还有个容易被忽视的步骤不要把 top20 全塞给大模型。我们目前只取 top3~5 进入最终上下文。原因有两点一是医疗问题的答案通常集中在少数几个片段里取更多片段会增加信息噪音二是大模型的注意力是有限的上下文越长模型越难聚焦到真正的依据上。同时在重排分数上设定一个动态阈值如果 top1 片段的分数过低说明知识库里可能确实没有能支撑答案的内容这时候系统会走“拒答”分支告诉用户“当前知识库中没有找到足够依据建议咨询专业医生”而不是强行生成一个看起来像模像样的回答。这个“会拒绝”的能力在医疗场景比“答得多”更重要。5. 溯源与可信输出让每个答案都有出处5.1 医疗场景为什么溯源是硬需求做医疗问答系统久了我越来越觉得溯源不是后置的“装饰功能”而是决定医生和患者是否愿意信任整个系统的关键一环。对医生来说一个答案如果拿不出出处基本等同于没有价值遇到复杂病例医生需要快速判断这条信息来自哪版指南、哪个章节、原文怎么说。对责任界定来说一个可回溯到原文的答案即使最终被证明参考了过时资料也能通过溯源链条定位问题出在知识库更新环节而不是模型杜撰。这些要求直接改变了我们对“检索结果”的定义检索返回的不再是一段纯文本而是一份带有完整证据链的“材料包”。每个材料包里包含了原文片段、doc_id、章节路径、版本号、来源类型。所有下游的生成、展示、审核都基于这个材料包展开。5.2 溯源链路的实现细节溯源链路在工程实现上从切块阶段就已经开始了。我们对每个知识块生成一个稳定的 block_id由 doc_id、章节 ID、文本哈希共同计算得出保证同一个位置的文本无论切多少次得到的 ID 都是一致的。这为后面的回溯打下了基础。检索和重排阶段所有候选片段都要携带自己的元数据包括原始标题、发布机构、版本时间、来源类型。到了生成阶段Prompt 里会强制模型按照“依据 回答 引用”的结构输出要求答案中的关键断言必须引用具体的片段编号并且禁止输出记忆中的医学知识。Prompt 里大致会这样约束严格按照“[依据片段] 回答内容”的格式组织输出。每个答案句如果对应某个片段必须在句末标记引用编号。如果片段内容不足以回答问题只能回答“当前知识库中没有足够依据”。禁止总结、推理片段中不存在的医学结论。生成完成之后还有一道“溯源校验”关卡。我们会用一个轻量级的蕴含判断模型检查答案里的每个断言和它引用的片段之间是否存在逻辑蕴含关系。如果答案句子的含义明显超出片段范围或者引用编号对不上就触发后处理要么删除该句要么把该句标记为“需人工核查”。这一步是为了防止模型“强行引用”的情况——生成模型可以为了满足 Prompt 格式而把不相关的片段编号挂在句尾这是我在实际测试里反复遇到过的现象。最后在应用前端用户点击答案中的引用角标可以直接显示对应的知识块原文、来源文档标题、发布章节和版本时间。整个“点击引用 → 跳转原文”的过程就是医生审核体验里最关键的那一步。5.3 21.7 个百分点的提升是怎么拆解的前面提到溯源覆盖率从 68.3% 提升到 90.0%这里给出评测口径和归因拆解方便大家对照复现。评测集是我们内部构建的 800 条医疗问答覆盖术语解释、诊疗流程、用药剂量、禁忌事项四类问题。指标定义是溯源覆盖率 答案中可溯源到知识库依据的断言句数 / 总断言句数。优化阶段溯源覆盖率增量v20.1 基线固定切块纯向量检索top5直出68.3%-结构感知知识分段71.5%3.2%BM25向量混合检索76.3%4.8%交叉编码器重排82.4%6.1%生成后溯源校验与引用约束90.0%7.6%21.7 个百分点就是这四步增量的累加。需要说明的是这些数字是业务场景特定的不同数据集的效果增幅肯定不一样但整体的优化优先级是共通的先保证知识库里能检索到完整依据分段再提高关键依据的召回率混合检索然后把最相关的依据排到最前面重排最后确保生成内容严格依附于依据溯源校验。每一层都是下一层的地基。在实际部署后我还有一个体会最能提升医生信任度的往往不是把模型从 7B 换到 72B而是把溯源链路做扎实。当一个答案点击引用后能精确跳转到指南的对应章节医生对系统的态度会明显不一样。这个项目目前还在继续迭代后续打算把知识库版本自动更新、多模态检查报告文本解析、以及基于 agent 的多轮追问能力逐步加进来但底层“分段 → 混合检索 → 重排 → 溯源”这条主线不会变。回到开头那个问题医疗 RAG 的难度不在于某一个环节有多复杂而在于每个环节都得为“可信”这个目标服务少一环都会漏风。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门