CQL Workbench文件
文件目錄
核心概念

核心概念

工作台怎麼表示一條條文:IR 裡的出處、原子條件與群組,證據契約怎麼規定每個條件的值,以及產生的 CQL 為什麼只吃參數。全頁沿用 CQL 入門的範例條文。

條文與 IR

一份文件會先拆成一條條給付條文(工作台畫面上稱為「任務」)。每條條文再由 LLM 分析成一份 IR(intermediate representation,中介表示):一份 JSON,記下條文有哪些條件、條件怎麼組合,以及每個條件出自原文哪一句。

IR 是唯一的來源。CQL 由 IR 產生,不手寫;工作台的「決策邏輯」分頁和審核頁上顯示、修改的也是 IR。邏輯一存檔,CQL 就重新產生、驗證重跑,所以畫面上看到的就是會匯出的。

IR 有四個部分:

欄位內容範例條文
source_units出處:從原文逐字抄下的引文,每段一個編號。S1–S4
nodes原子條件:每個是一個能用是或否回答的問題。C1–C4
groups群組:用 AND、OR、NOT 把條件和其他群組組合起來。G1、G2
decision.root根:指向最外層的群組,也就是整條條文的判斷。GROUP:G1

範例條文的四段引文,在原文裡的位置如下:

本品限內科專科醫師處方S1,病人需為成人(年滿十八歲)S2,且 eGFR ≥ 30 mL/min/1.73m²S3;未達標準者,須經腎臟科會診同意S4後使用。

這是範例條文的 IR,先省略 nodes(下一節再看)。每個條件用 source_ids 指向這裡的出處,例如 C3 指向 S3。

{
  "source_units": [
    { "source_id": "S1", "exact_quote": "限內科專科醫師處方" },
    { "source_id": "S2", "exact_quote": "病人需為成人(年滿十八歲)" },
    { "source_id": "S3", "exact_quote": "eGFR ≥ 30 mL/min/1.73m²" },
    { "source_id": "S4", "exact_quote": "須經腎臟科會診同意" }
  ],
  "groups": [
    { "group_id": "G1", "operator": "all", "members": ["C1", "C2", "GROUP:G2"] },
    { "group_id": "G2", "operator": "any", "members": ["C3", "C4"] }
  ],
  "decision": { "root": "GROUP:G1" }
}

exact_quote 必須能在原文裡逐字找到。找不到的引文不會讓分析失敗,但會標成待核,提醒審核者引用它的條件可能建立在模型自行補上的文字上。見引文驗證。

原子條件與群組

原子條件

原子條件(IR 裡的 node)是一個關於病人、能用是或否回答的問題,而且只問一件事。這是 C3 的 node:

{
  "node_id": "C3",
  "label": "eGFR ≥ 30 mL/min/1.73m²",
  "source_ids": ["S3"],
  "clinical_question": "病人的 eGFR 是否 ≥ 30 mL/min/1.73m²?",
  "definition": { "operator": "gte", "value": 30, "unit": "mL/min/1.73m²" }
}
node_id
條件代號,依序編為 C1、C2…。它也是 CQL 參數的名稱,所以必須以英文字母開頭、後面只接英數字或底線,而且不能重複。
label
畫面上顯示的簡短名稱。
source_ids
這個條件出自哪幾段引文。至少一個,而且只能指向 source_units 裡有的出處。
clinical_question
這個條件要回答的是非題,不能留空。
definition
判斷方式(可省略):operator 加上 value 與 unit。operator 只能是 exists、not_exists、eq、ne、gt、gte、lt、lte、in、not_in、between 其中之一;除了 exists、not_exists 都要有 value,只有 in、not_in、between 可以用清單當 value。

一個條件只能問一件事:產生 CQL 時,每個條件只變成一個參數,definition 不會寫進 CQL。如果把「A 且 B」塞進同一個條件,裡面的「且」會在 CQL 裡無聲消失,畫面上卻還像是一個可以審核的條件。所以 operator 不能是 and、or、not 這類組合詞;LLM 這樣寫時,管線會把整份 IR 退回,重新拆成群組。

群組

群組(group)把條件和其他群組組合起來。operator 有三種,產生 CQL 時各對應一個運算子:

operator意思CQL
all成員全部成立(AND)and
any任一成員成立(OR)or
not唯一的成員不成立(NOT),只能有一個成員not (…)

members 裡的條件直接寫代號,例如 C1;子群組要加前綴,寫成 GROUP:G2。decision.root 指向最外層的群組。範例條文的結構是:

  • G1all
    • C1處方醫師為內科專科醫師
    • C2病人年滿 18 歲
    • G2any
      • C3eGFR ≥ 30 mL/min/1.73m²
      • C4經腎臟科會診同意

分析時,每個條件只放進一個群組,每個條件和群組都要從根走得到;走不到的會被當成結構錯誤送回修正。群組之間也不能形成循環。

排除條款要特別注意。「有下列情形之一者不予給付」表示任一項成立就排除:IR 會先把這些項目放進一個 any 群組,再用一個 not 群組包起來,不會在 not 底下放 all。假設條文另有兩項排除情形 C5、C6,產生的 CQL 是:

define "Group G3":
  "Criterion C5"
  or "Criterion C6"

define "Group G4":
  not ("Group G3")

排除項目查不到資料時,not null 仍是 null,病人不會因此變成符合。完整規則見 CQL 入門的三值邏輯。

審核頁上,審核者可以切換群組的 AND/OR/NOT、把條件移到別的群組;脫離根群組的條件和群組會列在「未連結」區,可以再加回去。見修改邏輯。

證據契約

CQL 裡每個條件都是一個參數,值要有人填。填值的一方,CQL 檔頭的註解稱為證據轉接器(evidence adapter):可以是讀院內資料的程式、從文字擷取資訊的 NLP 工具,也可以是人工審查。工作台不規定由誰來填,而是為每條條文產生一份證據契約(evidence contract),寫清楚每個值的意思:

值契約裡的意思
true已採信的證據確認條件成立。
false已採信的證據顯示條件不成立。
null證據缺漏、過期、模稜兩可,或還沒對應到資料來源。

契約裡每個條件一筆,內容抄自 IR:參數名稱、臨床問題、definition、出處,以及偏好與備用的證據來源。每一筆的 missing_result 都是 unknown:查不到,就是不知道。逐條件覆核另外會在 IR 的條件上註明哪個 FHIR R4 資源與欄位可以證明它,給實作轉接器的人參考。

契約也規定轉接器回報每個條件時要記錄的欄位:

status
true、false 或 unknown。unknown 傳進 CQL 就是 null。
evidence_refs
依據在哪裡:FHIR 資源 ID、資料庫列 ID,或文件的頁面位置。
observed_at
這份臨床證據對應的時間點。
resolved_by
由誰判定:資料轉接器、NLP 擷取工具或人工審查。
confidence
0.0 到 1.0 的信心分數。
notes
模稜兩可之處,以及不同來源怎麼取捨。

信心分數不能把 unknown 變成 false:契約明訂,信心再低也不能把「不知道」改成「不符合」。資料不足的病人應該得到 insufficient-data,而不是 ineligible。

工作台沒有內建讀取病歷的轉接器。想看某組值會得到什麼結果,可以在工作台的「資格評估」分頁,把每個條件設成「符合」「不符合」或「未知」,再按「執行判定」。CQL 引擎會回傳符合資格、不符合資格或資料不足,並標出結果和 IR 的三值邏輯是否一致。

契約會跟著套件匯出,放在 fixtures 底下,給你的資料管線照著實作轉接器。

Parameter-based CQL

從 IR 產生 CQL 的是 renderCql。它不寫任何 FHIR retrieve(CQL 從病歷查資料的語法,例如 [Observation]),IR 的每個部分各對應一段固定格式的 CQL:

IR產生的 CQL
每個條件,例如 C3parameter "C3" Boolean default nulldefine "Criterion C3"
每個群組,例如 G2(any)define "Group G2"成員之間以 or 連接
decision.rootdefine "Eligibility"
(固定產生)define "Eligibility Status"轉成 eligible、ineligible 或 insufficient-data

產生的完整檔案逐行說明,見 CQL 入門的參數一節。

為什麼用參數

  • 條文邏輯和資料對應分開。條文寫的是「eGFR ≥ 30」,沒寫 eGFR 存在哪個系統、用哪個代碼。分析時,LLM 被要求不替醫院選資料對應,這一步留給轉接器。所以同一份 CQL,值可以來自 FHIR 伺服器、院內資料庫或人工判讀。
  • 每個條件都能單獨驗證。參數可以一個一個覆寫,不必先合成整套 FHIR 測試資料。下面的消融驗證就是這樣做的。
  • 缺資料是明確的 null。參數預設是 null,沒填值的條件就是「不知道」;最後的 Eligibility Status 會把它算成 insufficient-data,而不是 ineligible。見 CQL 入門的結果與 is true 和四種情境。

代價是 CQL 本身不會查病歷:沒人填值,每個參數都是 null,結果一律是 insufficient-data。健保署的官方預檢規則走另一條路,直接在 CQL 裡讀 FHIR 申請資料;兩種寫法的比較見台灣官方 CQL 指引。

消融驗證

因為每個條件都是參數,驗證可以把條件一個一個拿掉來看。工作台先把所有參數設成 true 算一次(baseline),再把每個條件輪流改成 false、改成 null,其他維持 true。CQL 由 cql-engine 編譯成 ELM,再由後端的 CQL 執行引擎(cql-execution)算出 Eligibility;每個案例的結果都要和 IR 用三值邏輯算出的預期值相同,才算通過。範例條文有 4 個條件,共 9 個案例:

案例覆寫預期的 Eligibility
baseline全部 truetrue
C1-falseC1 = false,其他 truefalse
C1-nullC1 = null,其他 truenull
C2-falseC2 = false,其他 truefalse
C2-nullC2 = null,其他 truenull
C3-falseC3 = false,其他 truetrue
C3-nullC3 = null,其他 truetrue
C4-falseC4 = false,其他 truetrue
C4-nullC4 = null,其他 truetrue

C3、C4 的四個案例都還是 true:G2 是 OR,另一個條件仍然成立。如果 CQL 把 G2 錯寫成 and,這四個案例就會失敗。結果顯示在工作台的「驗證報告」分頁。

消融驗證只確認 CQL 和 IR 一致:IR 本身是否忠於條文,要靠引文驗證和醫師審核。