核心概念
工作台怎麼表示一條條文: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 |
|---|---|
| 每個條件,例如 C3 | parameter "C3" Boolean default nulldefine "Criterion C3" |
| 每個群組,例如 G2(any) | define "Group G2"成員之間以 or 連接 |
| decision.root | define "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 | 全部 true | true |
| C1-false | C1 = false,其他 true | false |
| C1-null | C1 = null,其他 true | null |
| C2-false | C2 = false,其他 true | false |
| C2-null | C2 = null,其他 true | null |
| C3-false | C3 = false,其他 true | true |
| C3-null | C3 = null,其他 true | true |
| C4-false | C4 = false,其他 true | true |
| C4-null | C4 = null,其他 true | true |
C3、C4 的四個案例都還是 true:G2 是 OR,另一個條件仍然成立。如果 CQL 把 G2 錯寫成 and,這四個案例就會失敗。結果顯示在工作台的「驗證報告」分頁。