医疗知识RAG架构与边界

从零设计医疗知识 RAG:先划清边界,再谈技术选型

摘要:医疗知识问答最容易犯的错误,是把“模型能回答”误认为“系统可信”。本文从数据、检索、生成和审计四个层面,拆解一套可落地的 RAG 架构,并说明哪些问题不应该交给 RAG。

标签:RAG 医疗信息化 大模型 知识库 系统架构

一、为什么医疗知识场景更需要 RAG

通用大模型拥有较强的语言理解能力,但它的训练数据存在时间边界,也无法天然掌握机构内部制度、药品目录、编码规则和本地业务口径。RAG(Retrieval-Augmented Generation,检索增强生成)的核心思路,是在生成答案前先从可信知识库检索相关材料,再让模型依据材料组织答案。

它解决的不是“让模型知道更多”,而是三个更实际的问题:

  1. 知识可更新:指南或制度变化时,更新知识库即可,不必重新训练模型。
  2. 答案可追溯:回答能够附带出处、版本和页码,便于复核。
  3. 权限可控制:不同角色只检索自己有权查看的内容。

但要注意,RAG 不能自动保证答案正确。检索错误、文档过期、切分不合理或模型忽略证据,都会形成“有引用的错误答案”。

二、一套实用的四层架构

1. 数据层:先治理,再入库

知识来源可以包括诊疗指南、医保政策、院内制度、编码规则和产品说明书。入库时至少保存以下元数据:

{
  "title": "文档标题",
  "source": "发布机构",
  "version": "版本号",
  "effective_date": "生效日期",
  "page": 12,
  "department": "适用部门",
  "security_level": "权限级别"
}

正文只是知识的一部分,版本、时效和权限同样重要。若只保存纯文本,后期很难回答“这条结论来自哪个版本”“是否仍然有效”。

2. 检索层:关键词与语义检索并用

医疗术语中有大量缩写、编码和近义表达。例如同一个概念可能同时出现中文名、英文名、缩写和编码。单纯依赖向量相似度,容易漏掉精确编码;单纯依赖关键词,又难以理解自然语言问题。

比较稳妥的方式是混合检索:

  • BM25 负责精确术语、药名、编码和数字;
  • 向量检索负责语义相近的表达;
  • 重排模型对候选片段再次排序;
  • 元数据过滤负责版本、时间、科室和权限。

3. 生成层:把“依据证据回答”写进流程

提示词不应只写“请准确回答”,而要明确约束:只能依据给定材料;材料不足时必须说明;重要结论需要引用编号;不同材料冲突时展示冲突而不是强行合并。

推荐让模型输出结构化结果:

{
  "answer": "回答正文",
  "citations": ["doc-12#page-8"],
  "confidence": "medium",
  "missing_information": []
}

结构化输出便于前端展示,也方便服务端校验“引用是否真实存在”。

4. 审计层:记录一次回答是怎样产生的

一次完整调用应记录问题、用户角色、检索条件、候选片段、重排结果、提示词版本、模型版本和最终输出。审计日志不等于无限期保存原始敏感内容,应根据数据分级做脱敏、加密和保留周期控制。

三、知识入库并不是一次性的“导文件”

不少 RAG 项目的第一版会写一个脚本,把 PDF 转成文本、切块、计算向量,然后批量写入向量库。这个流程可以完成演示,但离可维护系统还有很大距离。生产环境中的知识入库至少应包含以下阶段:

文件接收
  → 格式与病毒检查
  → 文本/表格/图片解析
  → 结构恢复与章节识别
  → 元数据补全
  → 内容切分
  → 质量校验
  → 向量与关键词索引
  → 抽样复核
  → 发布生效

每一个阶段都可能失败,而且失败后应该能够从当前阶段重试,而不是重新处理全部文件。可以为文档设置 receivedparsedindexedpublishedfailed 等状态,并记录失败原因。

表格是医疗知识解析中的高风险区域。若解析器把表头与单元格拆散,模型看到的数字虽然都存在,却无法知道它们之间的对应关系。对于关键表格,建议同时保存结构化表示和适合阅读的 Markdown 表格;对于扫描版文档,则需要将 OCR 置信度纳入质量检查。

文档更新时也不要直接覆盖旧版本。更稳妥的做法是先建立新版本索引,完成抽样和回归测试后再切换“当前生效版本”。旧版本保留只读状态,用于解释历史回答,但默认检索只查询最新有效版本。

四、查询理解:不是所有问题都走同一条链路

用户问题可以先做轻量分类,再选择不同策略:

  • 精确查询:包含编码、药名或明确条款编号,增加关键词检索权重;
  • 解释查询:询问概念、原因或流程,增加语义检索权重;
  • 比较查询:需要同时检索多个对象并分别保留证据;
  • 时效查询:强制过滤生效时间,并优先展示版本信息;
  • 越权或高风险查询:不进入普通生成链路,直接触发安全策略;
  • 闲聊或知识库外问题:明确告知系统能力边界。

查询改写可以帮助补全缩写或同义词,但必须保留原问题。错误改写有可能改变用户意图,因此检索日志中应同时记录原问题、改写结果和最终使用的查询条件。

例如用户询问“某编码今年还能不能用”,系统真正需要的不是生成一段编码介绍,而是识别出三个条件:具体编码、当前年度和有效状态。只有完成条件提取,后续检索才有意义。

五、冲突知识如何处理

知识库中经常同时存在国家规范、地区政策、院内制度和历史版本。简单地选择相似度最高的片段,会把“适用范围”问题变成随机排序问题。

可以为文档定义优先级,但优先级不应只依赖来源名称,还需要结合问题的业务范围。例如院内操作流程应优先使用当前院内制度,通用医学知识则应优先使用权威指南。若两份同级材料互相冲突,系统应展示来源和版本差异,并提示人工确认,而不是让模型自行裁决。

一种实用做法是让检索结果先按以下维度分组:

  1. 是否在有效期内;
  2. 是否匹配用户所在机构和业务范围;
  3. 来源权威级别;
  4. 文档版本;
  5. 与问题的文本相关度。

相关度不应凌驾于有效性和适用范围之上。

六、索引结构:正文、向量和治理字段要放在一起

下面是一份简化的 Elasticsearch Mapping。重点不是字段名,而是把可检索内容与版本、权限、时效字段同时建模:

PUT medical_knowledge_v1
{
  "mappings": {
    "properties": {
      "chunk_id":       { "type": "keyword" },
      "document_id":    { "type": "keyword" },
      "title":          { "type": "text", "fields": { "raw": { "type": "keyword" } } },
      "heading_path":   { "type": "keyword" },
      "content":        { "type": "text", "analyzer": "standard" },
      "content_vector": { "type": "dense_vector", "dims": 1024, "index": true,
                          "similarity": "cosine" },
      "version":        { "type": "keyword" },
      "effective_from": { "type": "date" },
      "effective_to":   { "type": "date" },
      "tenant_id":      { "type": "keyword" },
      "security_tags":  { "type": "keyword" },
      "source_uri":     { "type": "keyword", "index": false }
    }
  }
}

chunk_id 应稳定且可重建,例如 document_id + version + page + chunk_no。不要直接使用数据库自增 ID 作为业务引用,否则重新索引后引用会整体变化。

向量维度必须与 Embedding 模型一致。更换模型时不要在原字段上混写新旧向量,应新建索引或新字段,完成双写、回归评测和别名切换后再下线旧版本。

七、一个可测试的检索服务接口

检索层最好独立于大模型,先返回可解释的候选:

from dataclasses import dataclass
from datetime import date

@dataclass(frozen=True)
class RetrievalContext:
    tenant_id: str
    security_tags: frozenset[str]
    as_of: date

@dataclass(frozen=True)
class Hit:
    chunk_id: str
    content: str
    title: str
    version: str
    score: float
    retrieval_source: str  # lexical / vector / both

async def retrieve(question: str, ctx: RetrievalContext, top_k: int = 20) -> list[Hit]:
    """先权限过滤,再并行召回,最后融合;不在此处调用生成模型。"""
    lexical, semantic = await run_parallel_retrievers(question, ctx)
    fused = reciprocal_rank_fusion(lexical, semantic, rank_constant=60)
    return fused[:top_k]

这样可以单独为 retrieve() 编写检索回归测试,也能在模型不可用时降级为“只展示相关文档”。生成层只接收已经授权、排序和裁剪后的 Hit

上下文构建时可以给每个片段分配引用序号:

[S1] 标题:……;版本:……;页码:……
片段正文……

[S2] 标题:……;版本:……;页码:……
片段正文……

模型只允许返回 S1S2 这样的局部引用。服务端再将它们映射回真实 chunk_id,并拒绝任何不在候选集合中的引用。

八、哪些问题不应直接交给 RAG

RAG 适合知识检索、制度解释、编码辅助和材料摘要,但不应单独承担以下任务:

  • 无人工复核的诊断或治疗决策;
  • 自动替代医生完成高风险处方;
  • 在证据不足时推断患者个体结论;
  • 绕过权限汇总敏感数据;
  • 对相互冲突的政策给出唯一确定结论。

这些边界最好在产品层面实现,而不是只依赖一句提示词。例如高风险问题触发固定提示、限制输出形式,并进入人工复核流程。

九、如何验证第一版是否真的可用

第一版上线前,可以准备一套小而精的验证集。问题不必很多,但应覆盖不同类型:能够直接找到答案的问题、需要组合两段证据的问题、知识库没有答案的问题、文档版本冲突的问题,以及明确不应回答的高风险问题。

评测时把“检索”和“生成”分开:

  • 如果正确片段没有进入 Top 20,优先改切分、索引或检索;
  • 如果正确片段已进入 Top 5,但答案仍错误,检查提示词、上下文顺序和生成模型;
  • 如果答案正确但引用错误,修复引用生成与服务端校验;
  • 如果无答案问题仍被强行回答,增加拒答判定和最低相关度门槛。

同时观察响应时间和成本。一个答案准确但需要一分钟才能返回的系统,在真实工作流中往往很难被使用。可以将检索、重排和生成耗时分别记录,找到真正的瓶颈。

十、上线顺序比模型大小更重要

建议先选择一个范围清晰、资料稳定、可人工验证的场景,例如院内制度问答。第一阶段只做检索和引用展示;第二阶段再增加答案生成;第三阶段才接入权限、反馈闭环和自动评测。

一个可信的医疗知识 RAG,不是“向量数据库 + 大模型”的简单拼接,而是数据治理、检索策略、生成约束和审计机制共同构成的系统。模型决定表达能力,工程流程决定它能否被安全使用。

说明:本文讨论的是知识系统设计,不构成医疗建议。示例数据均为结构演示,不包含真实患者信息。

posted @ 2026-09-20 15:23  楼主好菜啊  阅读(4)  评论(0)    收藏  举报