文章 John Pan · 5 小时 前 30m read

向FHIR存储库提出临床问题,并能核对答案

一个基于FHIR的自然语言代理,完全构建于IRIS for Health之上:数据处理采用FHIR SQL, 术语处理采用向量搜索,并以互操作性生产环境作为代理的运行时环境。 一个真实的问题,通过18条消息的追踪记录,以及该追踪记录揭示了这些 代理在哪些方面真正成功、又在哪些方面失败。


  1. 临床文本到SQL技术的发展历程

这项任务看似已成定局。临床医生用英语提出问题,模型生成SQL语句, 数据库给出答案。相关基准测试已存在多年,该领域通过这些测试取得的进展 值得关注,因为每一步都将难点推向了新的领域。

EHRSQL [5]确立了基准——这是一个基于真实 电子健康记录(EHR)模式的大型实用文本转SQL基准测试,其问题均来自医院工作人员的实际提问。该基准证实, 难点不在于SQL语法,而在于对模式的掌握:只要知道 上百张表中哪一张存储了什么数据,这些问题就能得到解答。

随后,目标转向了FHIR。FHIR-AgentBench [6]针对真实的FHIR资源重新提出了EHRSQL风格的 问题,并报告称,回答每个问题平均需要 14.5个FHIR资源。 难点已从*“该选哪个定制表”转变为“如何遍历一个标准化但深度嵌套的模型”*。 而EHR-ChatQA [7]则实现了多轮交互,同时指出了那些 SQL 风格的基准测试 一直以来默默隐藏的难点:术语不匹配。其典型示例是用户说“Lopressor”, 而记录中却写着“酒石酸美托洛尔”。

这是最新研究所探讨的线索,而它指向了一个令人不安的方向。

当被要求写出定义某个临床概念的代码时,一种“冻结前沿”模型返回的 代码中,有48.6%在参考词汇表中根本不存在,其值集层面的F1值 仅为0.105[1]。 在一项 OMOP 队列构建研究中,同一类未配备 术语工具的模型在需要概念标识符的地方生成了词汇表代码:其****84 个已执行的队列定义 中有 79个未返回任何患者 [2]。

综合上述内容,得出的结论令人不安,因为这与SQL完全无关。 这些问题所需的查询是一个带有IN列表的COUNT(DISTINCT …),或者是一个带有 派生标志的列表——而模型能够可靠地生成这些内容。**但它们无法可靠地决定什么 应该被放入 IN 列表中。**代码调用本质上是死记硬背,而死记硬背的失败往往悄无声息: 查询经过解析、执行,返回了一个看似合理的数值,而输出结果中没有任何迹象表明 这些代码是凭空捏造的。

因此,该领域的重心已从查询生成转向术语****解析,后者现已被视为一个独立的研究问题。当前最优的表述方式 是“检索增强型集合补全”:从 经过精心整理的语料库中检索出最相似的现有值集,然后将每个候选代码分类为“属于”或“不属于”。 一个在 SapBERT上微调的跨编码器,在11,803个公开的VSAC值集上 [1]达到了0.852的AUROC和0.298的集合级F1值——这是零样本任务的三倍,但没人会说这个问题已经解决了。


  1. 这些论文最终得出的架构

不同研究团队、不同数据库,但结论大致相同:不要再让模型 承载临床知识,而应将这些知识融入其周边的工具中。

ML4H 框架[2]最直接地阐述了这一原则。词汇检索、 查询规范、验证和执行都成为模型之外的 工具、指令模块和检查机制。 模型在类型化的中间 表示上执行有限的语义任务,且每个步骤都会被记录下来。随着术语工具的投入使用,与参考队列的 中位数一致性从“79个空队列”基线水平上升至0.91。

值集研究[1]为解析步骤本身提供了框架: **先检索,再分类。**检索以召回率为导向且范围广泛;第二个独立的 阶段则决定归属关系。这两项任务并不相同,单一的相似度得分无法 同时满足这两项需求。

而 SapBERT[3]则提供了检索器。其自对齐目标将同一 UMLS 概念的 同义词聚合在一起,这正是“heart attack”能够检索到*“myocardial**infarction”的原因*,尽管子字符串匹配永远无法实现这一点。

综合来看:有界模型任务、作为外部工具的术语、用于****解析的 **“先检索后重新排序”机制、各阶段之间的类型化中间表示,以及记录的执行轨迹。**这 正是本文所实现的架构。参考文献并非点缀——下文中的每个阶段 都旨在回应上述发现之一。


  1. 针对 FHIR SQL 的实现

共五个阶段。其中四个已投入运行并进行了跟踪记录;第五个阶段已构建完成,第9节对此进行了如实说明。

    "Provide a list of Type 2 Diabetis patients showing who
     had a routine HbA1c check last year"
                      │
   ┌──────────────────▼──────────────────┐
   │ 1. STEM + CONTEXT                   │  the question's words -> clinical
   │    phrase · domain · synonyms ·     │  vocabulary, read in context.
   │    specific_terms                   │  NO codes: the model is told it
   │                                     │  does not know them.
   └──────────────────┬──────────────────┘
   ┌──────────────────▼──────────────────┐
   │ 2. TERMINOLOGY RESOLUTION           │  SapBERT embedding -> VECTOR_COSINE
   │    embed -> vector search           │  over the concept table. Recall-
   │                                     │  oriented, deliberately over-fetches.
   └──────────────────┬──────────────────┘
   ┌──────────────────▼──────────────────┐
   │ 3. EXPANSION                        │  $expand is-a · $subsumes · RxNav
   │    hierarchy + brand->ingredient    │  brand->ingredient.  BUILT, NOT YET
   │                                     │  WIRED IN -- see §9.
   └──────────────────┬──────────────────┘
   ┌──────────────────▼──────────────────┐
   │ 4. RE-RANKING                       │  sees the WHOLE question + every
   │    keep / drop, with a reason       │  candidate. This is where precision
   │    per code                         │  comes from.
   └──────────────────┬──────────────────┘
   ┌──────────────────▼──────────────────┐
   │ 5. FHIR SQL                         │  resolved codes as literals, live
   │    guard -> execute                 │  schema from INFORMATION_SCHEMA,
   │                                     │  read-only validator, then IRIS runs it.
   └──────────────────┬──────────────────┘
                      ▼
            rows  +  the resolution trail

关于该链有三个方面值得探讨,因为正是这些方面使其偏离了 其构建所依据的论文。

词干提取是在上下文仍保留的情况下进行的,而非在上下文丢失之后。第一阶段不会 孤立地对单词进行规范化处理——它会先读取整个问题。 因此,“Type 2 Diabetis”会被处理为 2型糖尿病 / T2DM / NIDDM,尽管其中存在拼写错误;而在一个 关于阑尾炎的问题中,“surgery”一词会生成“阑尾切除术”和“阑尾切除”,而 在其他语境下,同一个词则不会产生这些结果。 在问题已被简化为 关键词之后放置的术语规范化器无法做到这一点;因为上下文已经不复存在。这一阶段也是模型 最有用且风险最低的阶段:它被要求进行改写,这是它擅长的,并且 被明确禁止生成代码,这是它不擅长的。

*解析和扩展是两个独立的阶段,因为它们的失败机制不同。向量搜索 会找到听起来像该短语的概念。层次扩展则会找到属于该概念*的子类——这是一种不同的关系,由本体而非距离来确定。 若将它们合并为一个检索步骤,则永远不会查询到第二种关系。§5.2 准确展示了 这种合并的代价:一个“同级”条件得分高于真正的“同组”成员,且没有任何 阈值能够将它们区分开来。

**重新排序是一个阶段,而非截断点。**廉价的设计方案是相似度阈值。下文的 测量结果表明为何这种方法行不通:排序本身就是错误的,因此没有任何截断点 能纳入正确的代码并排除错误的代码。 精确度必须来自能够 解读问题并给出理由的机制——正因为它能给出理由,输出结果才 可供审查,而这实际上比准确度更为重要。


  1. 为何 IRIS for Health 是实现这一目标的理想平台

若将其作为基础设施来描述,该处理流程需要:一个 FHIR 存储库;其 可供 SQL 操作的关系型投影;针对嵌入式术语的近似最近邻索引; 一个能够按阶段配置、对模型和工具调用进行排序的协调器; 每个步骤的持久化且可查询的记录;一个凭证存储库;以及一个Web层。

传统的架构由七个产品组成,且每两款产品之间都需要集成。而在 IRIS for Health 上,这一切仅需一个实例即可实现。

CONVENTIONAL ASSEMBLY                        IRIS FOR HEALTH
7 products, 8 integrations                   1 instance, 1 sidecar

  FHIR server                              ┌──────────────────────────────────┐
      │ sync                               │  FHIR R4 repository              │
      ▼                                    │  FHIR SQL projection             │
  ETL job                                  │  VECTOR(DOUBLE,768) + HNSW       │
      │                                    │                                  │
      ▼                                    │  ┌────────────────────────────┐  │
  warehouse ──────┐                        │  │ Interoperability           │  │
      │           │                        │  │ production = agent runtime │  │
      ▼           ▼                        │  │ service · process ·        │  │
  vector DB ──► orchestration              │  │ 5 traced operations        │  │
                 framework                 │  │ settings · pool · retry ·  │  │
      ▲           │                        │  │ resend                     │  │
      │           ▼                        │  └────────────────────────────┘  │
  secret store   trace /                   │                                  │
      │          observability             │  message bank = the trace        │
      │           ▲                        │  Credentials = the key store     │
      └──────► web tier                    │  CSP page    = the web tier      │
                                           └──────────────────────────────────┘
                                                        ▲
                                                        │ loopback, no exposed port
                                           ┌────────────┴─────────────┐
                                           │ sidecar: SapBERT 109M,   │
                                           │ CPU + vendor SDK         │
                                           └──────────────────────────┘

IRIS 的三项核心能力支撑着这一架构,每项能力都满足架构中不可或缺的 一项要求。

互操作性已经是代理运行时的一部分

这是评估 LangGraph 的团队始料未及的。 此类框架 旨在提供的功能,几乎与互操作性生产环境二十年来所具备的功能 完全一致:组件间类型化消息的序列化、按组件配置、池 大小与队列管理、重试和故障超时、通过*“重发”实现*的重放,以及持久化消息日志。

question ──► Agent.Service ──► Agent.Process ──┬──► LLM.Extract   stage 1
 (CSP page)   business          orchestrator    │
              service                           ├──► Embed.Op      stage 2  ◄─┐
                                                │                             │ once per
                                                ├──► Vector.Op     stage 2    │ concept
                                                │                             │
                                                ├──► LLM.Rerank    stage 4  ◄─┘
                                                │
                                                └──► LLM.SQL       stage 5
                                                         │
                                               SqlGuard ─┴─► IRIS executes ──► rows + trail

七项生产级功能。Embed.Op和Vector.Op是业务操作,而非内联 方法调用**,这正是为了**让嵌入的往返过程和向量搜索各自以 独立的请求和响应形式出现在跟踪记录中。 若采用内联方式,这些操作将不可见——而该设计的 核心理念正是确保每个决策都可供审查。

额外收获:ML4H 的作者将他们的可观察性跟踪列为一项明确的 贡献[2];而在这里,它仅仅是消息原本所在的位置。 按阶段选择 模型是生产环境中的设置,因此低成本的第 1 阶段处理和高成本的第 4 阶段处理可以 在不修改代码的情况下运行不同的模型,而跟踪记录了由哪个模型给出了响应。 添加一个阶段 ——即第9节中的 Expand.Op,即入站路径上的PHI守护——是一个新项和一条新路径,而非 图定义的重构。 而且由于聊天页面调用了业务服务,REST 客户端或 HL7 数据源也会以相同的方式进入并生成相同的跟踪记录。

FHIR SQL 为该模型提供了实际可定位的目标

通过同一命名空间中的 FHIR SQL 投影对存储库进行查询——无需导出、无需 ETL 计划、也无需额外副本来保持同步。

在此基础上还有一项刻意设计。FHIR SQL Builder 将搜索参数投影到 HSFHIR_X0001_S.*,但其中的令牌值采用**$LIST- 编码,不包含显示文本**。 若查询这些参数以获取代码,结果会返回 0 行,尽管数据中显然包含 32 个匹配项——结构 正确,比较方式错误,却未报错。因此,本演示将一个扁平的事实表直接从 资源 JSON 中投影出来:

INT_FHIRSQLLLM_Projection.Fact
  FactType · PatientId · ResourceId · Code · CodeSystem · Display
  FactDate · FactDay · ValueQuantity · ValueUnit · Status

每行对应一个 (资源, 编码) 组合。扁平化、纯文本类型,仅供 SQL 阶段处理的一张表——且 模式在运行时从 INFORMATION_SCHEMA 读取,因此提示信息始终描述的是 实际部署情况,而非其快照。

对于术语解析而言,向量搜索并非可选项

嵌入模型不包含任何代码。SapBERT 是一个文本编码器:输入文本,输出向量。 术语解析需要一个存储的 (code, system, display, vector) 表,先进行一次嵌入,然后 进行搜索。该表即术语库,这里是一个普通的 IRIS SQL 表, 带有额外的一列:

Property Embedding As %Vector(DATATYPE = "DOUBLE", LEN = 768);
CREATE INDEX ConceptVecIdx ON INT_FHIRSQLLLM_Terminology.Concept (Embedding)
  AS HNSW(Distance='Cosine')

索引采用 HNSW[4]—— 这与专用向量数据库使用的结构相同 —— 因此 无需在检索质量上做出妥协。将其置于数据库中的优势在于 概念表与患者事实表可以进行关联。 代理考虑的每个候选项 都会附带其实际将贡献的患者行数,该数据来自计算余弦得分时 使用的同一语句:

SELECT TOP 25 c.Code, c.Display, c.OccurrenceCount,
       VECTOR_COSINE(c.Embedding, TO_VECTOR(?, DOUBLE)) AS Similarity
FROM   INT_FHIRSQLLLM_Terminology.Concept c
WHERE  c.Embedding IS NOT NULL
ORDER  BY Similarity DESC

如果使用独立的向量存储,该行数需要通过应用程序代码处理,这会增加一次往返通信,并引发 一致性问题。而在这里,它被设计为一个列——第5.2节将展示其存在的价值。

概念表由存储库 中实际存在的 不同编码值构建而成,因此代理能够解析的每个代码都保证存在于数据中。当向量和事实共享同一个数据库时, 维护起来非常简单;若不共享,则需要进行同步操作。

本次部署:来自Synthea[10]的337名合成患者(马萨诸塞州,种子 20261101,10年病史)→ 8种资源类型共68,956个FHIR资源 → 199,267 预测事实 → 涵盖 SNOMED CT、ICD-10、LOINC、RxNorm、CVX 和 CDT 的1,220 个术语概念,其中 33 个(2.7%)存在词汇歧义。

Synthea 对结果至关重要,而不仅仅关乎许可问题:它通过逐个模块模拟每位患者的生活, 而非抽样行数据,因此诊断之后会紧随 相应的检查。这正是“护理缺口”问题能得出结果的唯一原因。


  1. 一个问题,端到端

以上内容均为论述。以下是一个真实的对话片段,取自消息库 ,完全按照记录原样呈现,该对话来自聊天页面。

这是一条质量报告请求,措辞完全按照实际收到的原样呈现,包括其中的拼写错误:

Provide a list of Type 2 Diabetis patients showing who had a routine HbA1c check last year

它生成了18条消息——9组请求/响应对,因为有两个临床 概念,且每个概念都有各自的解决周期:

\#会话 发自 → 发往 阶段
282 Agent.Service → Agent.Process `Agent.Question`
283/284 Agent.Process ↔ LLM.Extract 1 — 词干 + 上下文
285/286 Agent.Process ↔ Embed.Op 2 — 概念 1
287/288 Agent.Process ↔ Vector.Op 2 — 概念 1
289/290 Agent.Process ↔ LLM.Rerank 4 — 概念 1
291/292 Agent.Process ↔ Embed.Op 2 — 概念 2
293/294 Agent.Process ↔ Vector.Op 2 — 概念 2
295/296 Agent.Process ↔ LLM.Rerank 4 — 概念 2
297/298 Agent.Process ↔ LLM.SQL 5 — FHIR SQL
299 Agent.Process → Agent.Service `Agent.Answer`

5.1 第一阶段 — 词干与语境

model=claude-sonnet-5 · prompt 702 tok · completion 402 tok · 4,434 ms

{
  "entities": [
    { "phrase": "Type 2 Diabetis",
      "domain": "condition",
      "synonyms": ["Type 2 Diabetes Mellitus", "T2DM",
                   "non-insulin-dependent diabetes mellitus", "NIDDM"],
      "specific_terms": [] },
    { "phrase": "routine HbA1c check",
      "domain": "observation",
      "synonyms": ["hemoglobin A1c test", "glycated hemoglobin test", "HbA1c measurement"],
      "specific_terms": ["HbA1c level", "hemoglobin A1c lab result"] }
  ],
  "dimensions": {
    "aggregation": "list_patients",
    "temporal": { "type": "window", "before": null, "after": null, "window_days": 365 },
    "group_by": null
  },
  "answerable": true,
  "reason_if_not": ""
}

两个概念,每个都有其应用领域和临床释义。错别字已修正。且没有一个 代码——提示语禁止使用代码。

5.2 第二阶段——解决方案,以及决定架构的结果

Embed.Response   model=SapBERT  dim=768  latency_ms=45
Vector.Response  count=10  top_similarity=0.8309  latency_ms=7

CPU 嵌入耗时 45 毫秒,搜索耗时 7 毫秒。候选列表直接摘自跟踪日志,并 添加了正确的判定结果:

余弦 代码 display 行数 结论
0.8309 44054006 2型糖尿病(疾病) 18 ✅ 保留
0.6231 368581000119106 2型糖尿病引起的神经病变 8 ✅ 保留
0.5824 157141000119108 2型糖尿病引起的蛋白尿 14 ✅ 保留
0.5747 90781000119102 2型糖尿病引起的微量白蛋白尿 24 ✅ 保留
**0.5717** **714628002** **糖尿病前期(检查结果)** **118** ❌**其他病症**
0.5549 127013003 糖尿病引起的肾脏疾病 28 ❔ 未指定具体类型
0.5351 237602007 代谢综合征 X(疾病) 41 ❌ 独立疾病
**0.5231** **1551000119108** **由Ⅱ型糖尿病引起的非增生性糖尿病视网膜病变** **3** ✅**保留**
0.5005 395123002 糖尿病尿液筛查(操作) 84 ❌ 仅为检测,非诊断
0.4529 80394007 高血糖(疾病) 6 ❌ 检查结果,而非诊断

这个拼写错误并不重要。“2型糖尿病”仍能匹配正确的代码0.8309——而基于 拼写错误的词汇匹配则完全无法返回任何结果。这正是第二阶段展现其价值之处。

但没有任何阈值是有效的。阈值为0.5717 的糖尿病前期,其优先级高于阈值为0.5231的 II型糖尿病引起的 非增殖性糖尿病**视网膜病变——而视网膜病变代码是该队列的 真正成员,而糖尿病前期则属于不同的临床状态。 若将阈值设为0.55,则会纳入 118名糖尿病前期患者,同时漏诊一名确诊患者。没有任何阈值能够同时保留这四个 并发症代码并排除糖尿病前期,因为排序本身就是错误的。

这并非 SapBERT 的缺陷。这正是相似度度量的本质:“糖尿病前期”和“2型 糖尿病”确实是关于相关生理现象的相似字符串。它们仅仅不属于同一个 队列——而这种区分是范畴性的,而非度量性的。这是第3阶段 ($subsumes)所回答的问题,也是第4阶段所探讨的原因。

第二个概念使问题更难理解:

Embed.Response   model=SapBERT  dim=768  latency_ms=42
Vector.Response  count=8  top_similarity=0.5797  latency_ms=7
余弦 代码 显示 行 结论
**0.5797** 268556000 尿液葡萄糖筛查(操作流程) 83 ❌**错误,且排名第一**
0.5238 395123002 糖尿病尿液筛查(操作流程) 84 ❌ 错误
**0.4971** **4548-4** **血红蛋白A1c/总血红蛋白** **2,079** ✅**正确 — 排名第3**
0.4878 722161008 糖尿病视网膜检查(操作) 93 ❌
0.4873 Z00.00 成人常规体检就诊 1 ❌
0.4816 104091002 全血细胞计数,全自动… (CBC) 167 ❌
0.4662 D0603 龋齿风险评估,高风险*(牙科,CDT)* 108 ❌
0.4510 104375008 丙型肝炎抗体,确认试验 84 ❌

Cosine的Top-1结果完全错误。该Top-1检索器对整个质量指标的回答是 *“尿液葡萄糖筛查”。*正确答案——该 数据仓库中唯一的HbA1c LOINC代码(包含2,079条结果)——却排在第三位。龋齿风险评估也出现在 列表中,这生动地展示了混合词汇表中的相似性会带来什么结果。

请注意“rows”这一列在两张表中都发挥了作用。它与余弦得分源自同一个 SQL 语句, 正是这一列向下一阶段表明:错误记录糖尿病前期会导致 118 名患者,而视网膜病变代码则带来 3 名患者。

5.3 第四阶段——重新排序及理由说明

concept 1: prompt 1,241 tok · completion 1,000 tok · 13,101 ms``concept 2: prompt 1,180 tok · completion 266 tok · 3,820 ms

2型糖尿病— 置信度高

  代码 显示 原因
✓ 44054006 2型糖尿病(疾病) 2型糖尿病诊断
✓ 90781000119102 由2型糖尿病引起的微量白蛋白尿 需伴随该诊断
✓ 157141000119108 2型糖尿病引起的蛋白尿 涉及诊断
✓ 368581000119106 2型糖尿病引起的神经病变 需诊断
✓ 1551000119108 由2型糖尿病引起的非增殖性糖尿病视网膜病变 涉及诊断
✗ 714628002 — 糖尿病前期是一种不同的病症,并非2型糖尿病
✗ 127013003 — 由糖尿病引起的肾脏病变——并非仅见于2型糖尿病,1型糖尿病患者也可能出现
✗ 237602007 — 代谢综合征是一种独立的疾病
✗ 395123002 — 这是手术代码,并非糖尿病的诊断
✗ 80394007 — 高血糖是一种体征/检查结果,并非2型糖尿病的诊断

**注:**已排除“糖尿病引起的肾脏疾病”,因其未指明具体类型 且可能适用于1型糖尿病患者,这将导致2型糖尿病队列的计数重复。

常规糖化血红蛋白(HbA1c)检测— 可信度高

  代码 显示 原因
✓ 4548-4 血液中糖化血红蛋白A1c/总血红蛋白 糖化血红蛋白(HbA1c)实验室检测
✗ 268556000 — 尿糖筛查,非糖化血红蛋白
✗ 395123002 — 尿液糖尿病筛查,而非糖化血红蛋白(HbA1c)
✗ 722161008 — 糖尿病眼部检查,无关
✗ Z00.00 — 常规体检,非糖化血红蛋白(HbA1c)专项检查
✗ 104091002 — 血常规检查,非糖化血红蛋白检测
✗ D0603 — 龋齿风险,无关
✗ 104375008 — 丙型肝炎检测,无关

判断的两个方向均记录了理由:纳入了四个并发症代码**,因为它们涉及该诊断;排除了糖尿病前期**,因为它属于不同的 临床状态;并且因优先采用第3级而排除了余弦Top-1算法。

5.4 第 5 阶段 — FHIR SQL

prompt 1,329 tok · completion 413 tok · 4,657 ms

SQL 阶段不会接触到词汇表。它将解析后的代码作为字面量获取,并获取动态 模式:

SELECT DISTINCT c.PatientId AS PatientId,
       CASE WHEN EXISTS (
              SELECT 1 FROM INT_FHIRSQLLLM_Projection.Fact o
              WHERE  o.PatientId = c.PatientId
                AND  o.FactType  = 'observation'
                AND  o.Code IN ('4548-4')
                AND  YEAR(o.FactDay) = YEAR(CURRENT_DATE) - 1)
            THEN 'Yes' ELSE 'No' END AS HadRoutineHbA1cLastYear
FROM   INT_FHIRSQLLLM_Projection.Fact c
WHERE  c.FactType = 'condition'
  AND  c.Code IN ('44054006', '90781000119102', '157141000119108',
                  '368581000119106', '1551000119108')

Agent.SqlGuard 首先对其进行验证——仅限单个语句,SELECT 且不包含 DML、DDL、 过程调用或链式调用——随后由 IRIS 执行该语句。

36 行 · 19 赞 / 17 反对 · 26,235 毫秒

PatientId 去年是否进行过常规HbA1c检测
bea259a5-5f7a-fbba-59ed-2a65c333dee5 否
99e4e110-a6d4-40f0-5c29-bc76e91c5e72 否
81459c20-fe55-9452-f9f9-d72961743ffc 是
8e9441b3-1df1-03f9-349f-0bcf7a45c584 是
21dbf5be-f9aa-4762-ad52-8bbe945df48b 是
… *还有 31 行*

17名已登记的糖尿病患者,去年未检测HbA1c:一份待联系的工作清单。

5.5 那26秒去哪儿了

阶段 运行时间 延迟 占比
1 — 词干 + 语境 *模型调用* 4,434 毫秒 16.9%
2 — 嵌入 × 2 **sidecar,回调** **87 毫秒** **0.3%**
2 — 向量搜索 × 2 **IRIS SQL** **14 毫秒** **0.1%**
4 — 重新排序 × 2 *模型调用次数* 16,921 毫秒 64.5%
5 — SQL 生成 *模型调用* 4,657 毫秒 17.8%
保护 + 执行 + 汇编 **IRIS** 约122 毫秒 0.5%
**总计**   **26,235 毫秒** 100%

该平台执行的所有操作——两次嵌入往返、两次向量搜索、守护机制、 执行以及审计记录的**生成——在26.2秒中仅耗时约220毫秒,占比不到1%。**其余 99%的时间用于四次模型调用。

由此产生两个结论。检索机制实际上是“免费”的,因此从性能角度 来看,没有理由在此处偷工减料:扩大候选项集,或添加第 3 阶段的 $subsumes 检查 ,在 17 秒的重新排序过程中仅消耗几毫秒。 而且优化目标 显而易见,且并非数据库——而是重新排序阶段,这恰恰是 生产环境中可替换的组件。


  1. 为何这在三个具体方面优于BI工具

公正的比较不应仅是“该代理更快”。仪表盘一旦构建完成,便能在 几毫秒内给出答案。真正的比较在于获得答案所需的时间, 以及事后可以进行哪些核查。以下三个角度,在上述运行中均可见一斑。

术语角度

该查询中的任何内容都与实际输入的数据不符。“Type 2 Diabetis”拼写有误;“routine HbA1c check”在任何显示字符串中均未出现。BI过滤器需要预先由 既了解临床意图又熟悉术语的人选定一组值。

而且,值集的决策同时涉及两个方向,而没有任何过滤器表达式 能对此进行编码:诸如*“2型糖尿病引起的神经病变”之类的并发症代码必须 被包含在内,因为仅携带该代码的患者即为糖尿病患者——而“糖尿病前期”*则必须 被排除在外。第5.2节表明,没有任何相似度阈值能够同时满足这两项要求。

这一决策的价值在于:针对同一数据集的相同查询,若采用正确且 显而易见的简便方法进行处理:

糖尿病队列 患者 是 否 **报告的依从性**
已解析的值集——*代理生成的结果* 36 19 17 **52.8%**
朴素算法 `Display LIKE '%diabet%'` 131 86 45 **65.6%**

这种简单的定义显示依从率为65.6%,而实际数字为52.8%——相差近 13个百分点,误差方向是偏高的。其机制很简单: LIKE '%diabet%' 纳入了92名糖尿病前期患者,由于糖尿病前期患者会接受常规的糖化血红蛋白(HbA1c)监测, 因此他们几乎全都归入“是”一栏,从而虚增了结果。

而且由于该请求会返回一份工作清单,因此该错误不仅仅是一个写在白板上的数字。 这种简单粗放的做法会让糖尿病护士去追踪45名患者,其中28名其实并没有****糖尿病。

现在再对比一下该处理人员自己那个有争议的决定——删除了*“因糖尿病* 导致的肾脏疾病” 这一诊断,理由是该代码未注明具体类型,而请求中明确指明了2型。 审核人员很可能推翻这一决定。这会使研究队列减少3名患者,而报告的 数据则减少不到1个百分点。

这种对比正是关键所在。追踪记录并非证明该操作员正确,而是使 **每个选择的后果可量化。**那个有争议的决定最终被证明无关紧要, 而那个“隐形”的决定却价值十三分,且区分二者只需 一分钟,而非进行一次审计。 对于受监管的指标——如JAWDA、HEDIS或国家糖尿病 审计——那套记录在案的数值,连同每个代码对应的理由,就是可审计的实证依据。

时间维度

“去年”和“显示谁拥有”——这两个时间维度是仪表盘难以处理的。

该时间窗口以当前时刻为基准,且在“上一日历年”与“ 滚动十二个月”之间存在歧义。仪表盘的日期筛选器是绝对的;由某人决定并硬编码, 随后便无人能知晓具体选择了哪一种。 操作员选择了日历年,并在 列名 中注明了这一点——HadRoutineHbA1cLastYear——这一细微之处使得该选择可供审查。

更棘手的是:输出结果需要为每个队列成员生成一行数据,并包含一个派生的“是/否”列,而不是 一个经过筛选的列表。仪表盘只能对已存在的行进行筛选;它很难保留那些 未发生该事件的患者——而在护理缺口分析中,这些患者才是关键所在。这是一种相关 CASE WHEN EXISTS,由系统在查询时动态生成。

这也是同一引擎中 FHIR SQL 发挥作用的地方。时间比较是对 包含真实日期列的扁平事实表进行的自连接,仅需一条语句,一次事务即可完成——而不是 在数据仓库和向量存储之间进行应用层连接,且中间还存在一致性问题。

敏捷性视角

这条数据流的替代方案并非更慢的查询——**而是一张工单。**一位分析师需要整理 两组值,构建一个模型将条件与观察结果进行连接,同时保留未发生该事件的 队列成员,确定时间窗口,并发布仪表盘。这需要数天时间。而最终, 决定该数字的临床判断却消失在仪表盘定义中, 而这个定义再也没有人会去阅读。

下一个问题*——同一组人群,但谁该进行视网膜筛查了?*——又让这个循环 重新开始。新的值集,新的模型,又一张工单。

该工具在26秒内给出答案,无需新的ETL处理、无需新的仪表盘,也无需训练模型,并 反馈了保留了哪些代码、舍弃了哪些代码以及原因。第二个问题的边际成本是26秒,而不是另一个冲刺周期。这就是敏捷性的论据,其价值 远高于延迟问题:临床分析中的瓶颈从来都不是查询执行,而****是分析师面前的待办事项队列。


  1. 受保护健康信息(PHI)的边界

阶段 离开实例的内容
1 — 词干 + 语境 仅问题文本
2 — 嵌入 问题中的名词短语 — 发送到回环侧车,不外传
2 — 向量 *无;在IRIS中运行*
4 — 重新排序 候选**概念名称**— 不包含患者数据
5 — SQL 问题 + 模式 + 已解析代码 — 不包含患者数据
执行 *无操作;在IRIS中运行*
结果 **从未发送至任何模型**

第5节中的有效载荷就是证据:唯一离开该实例的临床内容是一份 概念名称列表。这36个患者标识符从未接触过任何模型。

该密钥也未到达任何调用点。它存储在 IRIS凭据条目中;一个方法将其以 模式 600 写入到一个卷中,该卷由 sidecar 自行读取。它未出现在任何源文件、任何生产 环境、任何命令行以及任何进程列表中。

对于真正的个人健康信息(PHI):添加一个入站防护机制,对问题中的标识符进行屏蔽,使用 VerifyPeer = 1 并配置真实的证书颁发机构(CA),并选择一家拥有企业协议且 包含“不得使用您的数据进行训练”条款的供应商。


  •  

这并非为医疗保健领域中的代理式人工智能构建的技术栈,但事实证明,它 一直以来都具备了正确的组件。

代码: https://github.com/adelelsayed/fhirsqlAgent— docker compose up,Synthea数据,不含 个人健康信息(PHI)。


参考文献

*[1]Mukherjee, S., Shu, J., Mazumder, N., Kernell, T., Wheeler, C., Hastings, S., Sidey-Gibbons, C.先检索,再分类:基于语料库的 临床价值集编写 *自动化。arXiv:2604.14616 —https://arxiv.org/abs/2604.14616(RASC:48.6%的不存在代码数据;在11,803个公开的VSAC价值集上,SapBERT跨编码器的AUROC为0.852,集合级F1值为0.298,而零样本学习模型的F1值为0.105。)

[2] 将推理与执行分离:基于OMOP CDM的NL2SQL和队列构建的工具介导的能动性框架。《健康机器学习》(ML4H) 2026,会议论文集专题。(外部化术语查找、类型化中间表示及记录的执行轨迹;在工具支持下,84个队列中有79个为空,与工具的一致性中位数达 0.91。)

**[3]**Liu, F., Shareghi, E., Meng, Z., Basaldella, M., Collier, N. 生物医学实体表示的自对齐预训练。NAACL 2021, 4228–4238.arXiv:2010.11784 —https://arxiv.org/abs/2010.11784· https://github.com/cambridgeltl/sapbert(SapBERT。此处使用的检查点为 cambridgeltl/SapBERT-from-PubMedBERT-fulltext 。)

**[4]Malkov, Y. A., Yashunin, D. A.基于分层可导航小世界图 *的高效且稳健的近似最近邻搜索。*arXiv:1603.09320 — https://arxiv.org/abs/1603.09320 ( IRIS向量搜索中 AS HNSW背后的结构 。)

**[5]李(G.)、黄(H.)、裴(S.)等人。EHRSQL:一个适用于电子健康记录的 *实用文本到SQL基准测试。*arXiv:2301.07695 — https://arxiv.org/abs/2301.07695

[6] FHIR-AgentBench:针对现实场景下**互操作性电子健康记录问答 的大语言模型(LLM)代理基准测试。arXiv:2509.19319 —https://arxiv.org/abs/2509.19319(报告显示,平均每个问题需要14.5个FHIR资源。)

[7] EHR-ChatQA:从对话到查询执行——针对 电子健康记录(EHR)数据库代理的用户与工具交互 基准测试。ICLR 2026。(多轮对话;将术语**不匹配(如“Lopressor”与“酒石酸美托洛尔”)视为首要难点。)

**[8]**HL7 FHIR R4 术语操作: ValueSet/$expand· CodeSystem/$subsumes· CodeSystem/$lookup— 已根据 tx.fhir.org/r4 进行解析。

[9] RxNav / RxNorm API— 商品名到 成分名。

**[10]**Walonoski, J., Kramer, M., Nichols, J., Quina, A., Moesel, C., Hall, D., Duffett, C., Dube, K., Gallagher, T., McLachlan, S.Synthea:一种生成合成患者及合成电子健康 记录的 *方案、方法 和软件机制。《美国医学信息学协会杂志》2018;25(3):230–238. doi:10.1093/jamia/ocx079· https://github.com/synthetichealth/synthea(Apache 2.0) (全文均使用合成人群——任何 环节 均未涉及真实患者数据。*337名患者,马萨诸塞州,种子值20261101,10年病史,由v3.4.0-18-ga07a65555生成。生成脚本及完整运行清单:iris/synthea/ 。)