病历里有一条华法林记录:MedicationStatement.status=completed,effectivePeriod.end=2024-02-11,note 写着"患者长期口服,需监测 INR"。
半年后,医生在侧栏问医疗 AI:"截至 2024-08-20,患者现在还在用抗凝药吗?"
如果后端把 EHR 当普通文档库处理,路径会很短:切 chunk、做 embedding、相似度召回、交给 LLM 总结。note 里的"长期口服""INR""华法林"和问题高度相关,模型很容易把它写进"当前用药"。
很多错误来自入库阶段:临床时间线被压平。用药记录里的 status、effectivePeriod、encounter、authoredOn、source 一旦丢失,相似度只能证明"文本相近",不能证明"截至今天仍有效"。
图1:一条 MedicationStatement 被拆成 resourceType、status、effectivePeriod、encounter、source 五个标签
FHIR 用药资源不能混成一种"药物文本"
同一个患者可能同时有门诊处方、住院临嘱、出院带药、患者自述、药房发药、护士给药、停药记录。FHIR 把这些事实放在不同 resource 中,RAG 如果只保留药名和 note,后面很难恢复语义。美国 ONC HTI-1、US Core、SMART on FHIR 近两年持续推动结构化 API,医疗 RAG 也开始回到字段、时间和审计证据上。
| FHIR 资源 | 记录含义 | RAG 处理策略 |
|---|---|---|
| MedicationRequest | 医嘱或处方请求,表示曾被要求使用 | 看 status、intent、authoredOn、encounter、dispenseRequest.validityPeriod、dosageInstruction.timing;停嘱、作废、续方链通常依赖院内映射 |
| MedicationStatement | 患者自述或临床记录中的用药状态 | 兼容 effectiveDateTime / effectivePeriod;completed、stopped、not-taken 不能直接归入当前用药 |
| MedicationAdministration | 给药执行事实,多见于住院 | 看 performed[x] 和住院 encounter;一次给药完成不能外推出院后仍在用 |
| MedicationDispense | 药房发药事实 | 用发药时间、数量、daysSupply、退药或取消记录估算覆盖区间 |
最容易出错的是跨资源套用 status。MedicationAdministration.completed 多数表示一次执行完成;MedicationDispense.completed 多指发药流程完成;MedicationRequest.active 还要看有效期、疗程和停嘱链。MedicationStatement.active 如果只有很旧的 effectiveDateTime,也不能直接塞进"当前用药"。
先分桶,再让向量检索进场
医生提问:"截至 2024-08-20,患者是否仍在使用抗凝药?"后端先解析出 patient=Patient/P001、as_of=2024-08-20、药物类别=抗凝药。系统按 resourceType 分桶读取 FHIR,暂时不查向量库。以下 ID 均为构造样例。
结构化层先给每条记录打桶:当前候选、历史候选、否定证据、无法判定。entered-in-error 直接排除并写入审计;MedicationStatement.effectivePeriod.end < as_of 进历史;MedicationDispense 用发药日加 daysSupply 估算覆盖;MedicationAdministration 只作为住院执行证据;缺失时间字段的记录进入无法判定池,等待人工确认。
图2:FHIR 结构化过滤 → 当前/历史/否定/无法判定四个池 → 向量召回 → LLM 证据摘要 → 医生确认
向量召回放在候选池内,用来处理"抗凝药""warfarin""DOAC""血栓预防"等表达差异。LLM 负责合成证据、暴露冲突、生成可读摘要,不能改写字段含义。一个最小实现可以长这样:
json
{
"query": "current_anticoagulant_use",
"patient": "Patient/P001",
"as_of": "2024-08-20",
"filter": {
"exclude_status": ["entered-in-error"],
"required_fields": ["resourceType", "id", "status", "time", "source"]
},
"bucket_rules": {
"MedicationStatement": {
"current": "status=active AND effective.covers(as_of)",
"history": "status IN [completed,stopped] OR effective.end < as_of",
"negated": "status=not-taken",
"unknown": "missing effective OR status IN [unknown,on-hold]"
},
"MedicationRequest": {
"current": "status=active AND validity.covers(as_of) AND local_chain=current",
"history": "status IN [completed,cancelled,stopped] OR local_chain=superseded"
},
"MedicationDispense": {
"coverage": "whenHandedOver + daysSupply - local_returned_days"
}
},
"evidence": [
{
"id": "MedicationStatement/ms-778",
"bucket": "history",
"status": "completed",
"time": {"end": "2024-02-11"}
}
]
}
这里的 local_chain、local_returned_days 不是 FHIR 通用字段,只是示例策略。生产环境要接入院内医嘱状态、续方关系、退药模型和停嘱记录,否则规则看似完整,落地时仍会漏掉本地语义。
写规则前,可以先查公开研究里的字段设计和评测口径,例如 FHIR medication resources、EHR medication reconciliation、clinical timeline extraction、medication safety RAG。我会用 超能文献 做中文检索和文献追踪,整理公开论文中的时间字段、冲突标注和审计方法。它只适合作为证据入口和研究审计工具,不读取患者 EHR,也不能生成诊断、治疗或用药建议。
评测要覆盖会误导模型的样本
用药 RAG 的测试集不能只问"有没有某药"。样本要覆盖停药、换药、重复处方、跨就诊、出院带药、患者自述与处方冲突、同名不同剂型、住院给药但出院未带药。每条样本都要指定 as_of,并标注输出类别:当前用药、历史用药、患者自述、无法确认。
评测指标也不能只看最终回答相似度。至少要看 current/history 分类准确率、旧药误报率、证据字段引用完整率、as_of 覆盖率、无法判定样本的拒答率。日志围绕结论保存:进入当前池的 resource id、进入历史池的 resource id、排除理由、缺失字段、向量分数、LLM 引用字段、医生确认结果。线上出现"旧药当现药",排查人员要能定位错误发生在状态映射、时间规则、语义召回,还是摘要生成。
回到开头那条华法林记录,冲突很具体:note 像当前用药,status=completed 和 effectivePeriod.end=2024-02-11 指向历史用药。半年后医生问"现在还在用抗凝药吗",系统如果先读 note,再生成摘要,旧记录就会被推入当前用药答案。
下一步可以从 50 条用药审计样本做起:覆盖停药、换药、跨就诊、出院带药、患者自述冲突;给每条样本指定 as_of;导出当前池、历史池、否定池、无法判定池和过滤理由;把这些证据接到医生确认界面。完成这层基础工程后,再让 RAG 回答"正在服用"。

