下拉重新整理

20 年老 HIS 逐步轉 SMART on FHIR 的可行建議書:用絞殺榕策略,一個人也推得動

我們手上已經有三張牌:一套跑了超過 20 年的傳統 HIS、一台已經架起來的 FHIR Server,還有用 Rails 加 AI coding 開發 SMART on FHIR App 的能力。這篇是一份完整、可執行的轉換建議書,主張不要整套換、也不要無限期共存,而是用「絞殺榕(strangler-fig)」策略:在老 HIS 外面包一層 FHIR 門面,新功能一律長在 FHIR 這邊,舊功能一塊一塊被標準介面接管,老 HIS 慢慢退成純資料來源直到能下線。內容涵蓋為什麼漸進、目標分層架構、五階段路線圖、你這週就能動手的第一個 PoC(把某一個唯讀資源用 FHIR façade 端出來)、PHI 資安與治理、還有 AI coding 在關鍵醫療系統上該有的把關紀律(Spectra 加臨界性光譜)。核心主張是:遷移的難點從來不是技術,是既有投資、停機風險和廠商動機,所以路線要設計成每一步都能單獨上線、單獨回滾、單獨產生價值。2026-07-27 加了一節修正:絞殺的對象是資料主權,不是使用者的操作習慣。不能無限期共存的是資料主權,使用者介面可以

| 4,788 字 | 12 分鐘閱讀 | 330 次閱讀 |

strangler-fig-cover-v2.jpg

這份建議書要回答一個很具體的問題:我們院內已經有一套跑了超過 20 年的傳統 HIS,也已經架起一台 FHIR Server,而且有用 Rails 加 AI coding 開發 SMART on FHIR App 的能力,那接下來該怎麼一步一步把它轉過去?我不談「明年上線一套新系統」那種簡報式路線,我談的是每一步都能單獨上線、單獨回滾、單獨產生價值的實作路線。

先講結論

三件事先講在前面,後面都是展開:

  1. 不要整套換,也不要無限期共存(這句在 2026-07-27 被修正了,見下面那一節:不能無限期共存的是資料主權,使用者介面可以)。整套換的風險醫院扛不起,無限期共存則永遠享受不到標準化的好處。中間那條路叫絞殺榕(strangler-fig):在老 HIS 外面包一層 FHIR 門面,新功能一律長在 FHIR 這邊,舊功能一塊一塊被標準介面接管,老 HIS 慢慢退成純資料來源,直到某天它只剩幾張表還在被讀、可以安排下線。
  2. 遷移的難點不是技術,是風險與動機。老 HIS 不能停機、不能出錯、原廠沒有主動開放的誘因。所以路線的每一步都要設計成「加一塊、不動舊的」,先做唯讀、再做回寫,把不可逆的動作往後排、往小切。
  3. 我們手上的三張牌剛好湊成一套絞殺榕的最小裝備:FHIR Server 是門面要站的地方,Rails 是門面跟轉譯層的實作骨架,AI coding 加上 Spectra 紀律讓一個人也維持得住關鍵系統該有的把關密度。

2026-07-27 修正:絞殺的是資料主權,不是使用者的按鈕

來源是 DHH 在 David Senra 節目上講印表機的那一段(我寫成了看片筆記)。他說他不在買更好產品的市場裡,他在買他已經買的那個產品的市場裡。37signals 2004 年的第一版 Basecamp 2010 年就停售,到今天還開著、還幾乎是純利;Highrise 凍結不再開發,還在做好幾百萬美金的生意。他們給客戶的承諾是這些東西會活到網際網路結束的那一天。

這一段推翻了這份建議書裡的兩個講法。原文我留著不動,因為它記錄了我當時怎麼想。

推翻的第一個:「老 HIS 退到能下線」不該當成功指標。

下面階段 4 的寫法是把下線當終點,把「還有多少表還在被讀」當進度。但下線這件事對使用者沒有任何價值,它只對我有價值,因為它讓我的維護面變小。護理站沒有人在等老 HIS 死掉。他們在等的是那個畫面明年還在原地。

推翻的第二個:結論第 1 點那句「也不要無限期共存」講得太粗。

它把兩件不同的事綁在一起講死了。要拆開:

  • 不能無限期共存的是資料主權。同一筆資料有兩個主來源,而且兩邊都能寫,這是真正會出人命的那種共存,必須收斂,而且要收斂到明確的一邊。
  • 可以無限期共存的是使用者介面。一個已經被降級成唯讀、只讀單一主來源的舊畫面,繼續活著不會造成任何不一致。它的存在成本是我的,它的消失成本是使用者的。

我原本那句話的錯誤是拿資料主權的標準去要求使用者介面。

因此新增一條全程通用的原則:每一次改動,先算出這次要跟誰收學習成本。

收不起就不要收。「順便把介面整理一下」在這份路線裡不是加分,是自費把成本轉給臨床端。改動要往哪邊倒的判準只有一個:這次改動讓使用者省下什麼,而不是讓我的架構乾淨什麼。

階段 4 因此改義:終點從下線改成凍結。

同一個階段拆成三種處置,依序遞減,不是一路走到最後:

  1. 接管:資料主權移到 FHIR 這側,舊畫面保留、轉唯讀。使用者操作零變化,系統上已經被絞殺完成。這是絕大多數模組的終點。
  2. 凍結:整個模組停止開發,但繼續服務,不設落日日期。這是給那些還有人每天在按、而且沒有任何新需求的模組。凍結不是失敗,凍結是承諾被履行的樣子。
  3. 下線:只有在確認沒有使用者、或法規保存期已過的時候才做。「有新版本了」不是下線條件。

階段 2 第一支 App 的選題標準因此多一條:不要重做使用者已經會按的畫面。

新 App 的價值必須來自老 HIS 結構上答不出來的問題。重做一個已經有人會用的畫面,是要使用者付重新學習的錢,而換不到任何他原本拿不到的東西。這跟我先前挑第一支 App 的判準是同一件事:值得做的是「誰沒來」這類問題,因為老 HIS 的每一個畫面都是從就診紀錄長出來的,沒來的人在它的結構裡不存在。

新增一個反向的進度指標。

原本衡量進度的是老 HIS 還剩幾張表被讀、轉譯層還剩幾行。這個指標只看得到我這一側。要再加一個看使用者那一側的:到目前為止,因為這次遷移而必須重新學習操作的臨床人員有幾位。這個數字的目標是零,不是少。

這條原則的代價我也寫下來,不假裝它免費。

凍結的模組會持續吃資安更新、OS 與相依套件的老化,不是零成本。DHH 有一整間公司養那些舊版本,我只有我自己。所以能凍結的前提是那個模組已經完成第 1 種處置:主權已經移走、只剩唯讀讀取,維護面才小到一個人扛得住。還在寫入、還在長邏輯的模組不能凍結,那不叫凍結,那叫兩套系統同時在跑。

為什麼是漸進,而不是整套換

我在 台灣醫院 HIS 的獨規困局 那篇講過,獨規的成本是隨時間往上長的,方向遲早要走。但「方向對」不等於「一次翻掉」。整套換(big bang)在醫院這種環境幾乎必敗,原因很現實:

  • 停機成本無法承受。門診、住院、急診 24 小時在跑,沒有一個週末大到可以讓你把 20 年的資料與流程一次切過去而不出事。
  • 隱性邏輯藏在老系統裡。跑了 20 年的 HIS,累積的不只是資料,還有無數當年為了某個科、某條健保規則、某次評鑑臨時加的邏輯。這些沒有文件,只在程式碼跟老同事的記憶裡。一次重寫等於一次全部重新發現,風險是指數級的。
  • 驗證面太大。一次換掉,你要在同一時間證明所有東西都對;切成小塊,你每次只要證明這一塊對。

絞殺榕的精神,就是讓新舊系統在同一段時間並存,但新的介面逐步把舊系統的職責一項一項接過來,像絞殺榕包住宿主樹一樣,等新的長成了,舊的自然被架空。這跟我在 Omakase Smart Hospital 講的「新的東西一律照 SMART on FHIR 長,舊的獨規慢慢被標準包起來」是同一件事,這份建議書把它落成可執行的步驟。

目標架構:老 HIS 退居資料來源,FHIR 當唯一新入口

先把終點畫出來,才知道每一步往哪走。目標不是「消滅老 HIS」,而是把它降級成分層架構最底層的一個來源系統,跟 LIS、RIS/PACS 平起平坐(這套分層見 TW Core IG 分層架構)。所有新的存取,不管是 App、AI、跨院,一律走上面的 FHIR 介面,不再直連老 HIS 的資料庫。

flowchart TB
  subgraph NEWAPP[新功能一律長在這層]
    APP1[SMART on FHIR App<br/>OAuth2 / Scopes]
    CDS[臨床決策支援 / CQL]
    XREF[跨院調閱 / BI]
  end
  subgraph FACADE[FHIR 門面 + 轉譯層 Rails]
    FS[FHIR Server<br/>TW Core Profile]
    ACL[轉譯層<br/>老 HIS 語意 to FHIR 資源]
    POL[Policy Gateway<br/>授權 / 稽核 / 最小揭露]
  end
  subgraph SRC[來源系統 逐步退居底層]
    HIS[(20 年老 HIS<br/>先唯讀 later 回寫收斂)]
    LIS[(LIS)]
    RIS[(RIS / PACS)]
  end
  APP1 --> POL
  CDS --> POL
  XREF --> POL
  POL --> FS
  FS <--> ACL
  ACL <-->|讀:抽取對映<br/>寫:後期才開| HIS
  ACL --> LIS
  ACL --> RIS

三個關鍵元件:

  • FHIR Server:資料的統一出入口,套 TW Core IG 的 Profile,新舊資料都以標準資源的樣子被存取。我在 FHIR Server 當 Kamal Accessory 評估過怎麼把它一個人維運起來。
  • 轉譯層(anti-corruption layer):這是絞殺榕的心臟。它用 Rails 寫,一邊講老 HIS 的私有語意(那些只有原廠看得懂的表結構、代碼),一邊吐出乾淨的 FHIR 資源。它把「老系統的髒」擋在門面之外,讓上層永遠只看到標準。
  • Policy Gateway:授權、稽核、最小揭露都集中在這一層。這正是我在 FHIR Policy Gateway 系列拆的那層 Rails,身分授權歸這裡,臨床邏輯歸 CQL(見 HAPI 內建 CQL 引擎),兩件事不混在一起才稽核得動。

五階段路線圖

我把路線切成五階段,每一階段都有明確的「可以停在這裡也有價值」的性質。不是非得五階段全走完才回本,每一階段自己就結案。

flowchart LR
  P0[階段 0<br/>盤點與地基] --> P1[階段 1<br/>唯讀門面]
  P1 --> P2[階段 2<br/>第一支 App]
  P2 --> P3[階段 3<br/>選擇性回寫]
  P3 --> P4[階段 4<br/>絞殺與收斂]

階段 0:盤點與地基(先評估,別急著寫)

  • 資源盤點:列出老 HIS 裡最有價值、又相對獨立的資料域。通常 Patient(病人主檔)、Encounter(就診)、Observation(檢驗/生命徵象)是最好的起點,因為它們是唯讀需求最強、寫入風險最低的。
  • 對映清單:每一個要端出來的 FHIR 資源,對應老 HIS 哪張表、哪些欄位、代碼怎麼轉(檢驗對 LOINC、診斷對 SNOMED CT、藥品對 RxNorm)。這份對映本身就是轉譯層的規格。
  • 地基就位:FHIR Server 掛好、TW Core Profile 載入、Terminology 服務能查(CQL 跟驗證都靠它,漏了條件會靜默失敗)、Keycloak/OAuth2 立起來。

停在這裡的價值:你有了一份到目前為止可能從沒人整理過的、老 HIS 的資料地圖。

階段 1:唯讀門面(只讀不寫,零風險起步)

挑一個資源(建議從 Patient 或某一類 Observation 開始),在轉譯層寫一支唯讀的對映,讓 FHIR Server 能以 GET /Patient/{id}GET /Observation?patient=&code= 這種標準查詢,回傳從老 HIS 即時抽出來、轉成 TW Core 格式的資料。

  • 唯讀是關鍵。這一步完全不碰老 HIS 的寫入路徑,老系統對它無感,出事也只是查詢失敗、不會弄壞任何資料。這是整條路線風險最低、最適合當第一個 PoC 的地方。
  • 抽取方式看老 HIS 開放程度:能開唯讀 view 或 replica 最好;只能走既有介面(HL7 v2、檔案交換)就在轉譯層做轉換。

停在這裡的價值:院內第一次有一個「講標準 FHIR」的病人/檢驗查詢介面可以給人接。

階段 2:第一支 SMART on FHIR App(讓價值被看見)

有了唯讀門面,就用 Rails 加 AI coding 做一支真的有人要用的 SMART App,掛在 OAuth2 授權之上。挑一個「用老 HIS 做很麻煩、用標準 App 做很輕鬆」的情境,例如某個科的檢驗趨勢檢視、或病人端的資料查詢(火線超人那條線的做法)。

  • 這一步的目的是政治性的跟技術性的並重:讓臨床端、管理層親眼看到「不動老 HIS,也能長出新功能」,替後面的階段爭取信任與資源。
  • App 走標準 SMART 掛載,證明「一次寫、將來可攜」不是口號。

停在這裡的價值:一個在線上跑、有使用者的成果,而不是投影片。

階段 3:選擇性回寫(謹慎開啟寫入,一次一個)

到這裡才開始碰寫入,而且極度克制。挑風險可控、流程單純的寫入場景先做,例如某類註記、某個病人回報結果(PRO)、某種預約。經由 FHIR 資源寫進來的資料,由轉譯層決定是寫回老 HIS、還是就存在 FHIR Server 這側,成為這筆資料新的主來源。

  • 一次只開一個寫入路徑,每個都要先講清楚這筆資料以後由誰作主(system of record):老 HIS 還是 FHIR?
  • 寫入是不可逆動作,所以這階段的每一步都要走完整的把關紀律(見下一節),不能 vibe 一把就上。

停在這裡的價值:FHIR 這側開始成為某些資料的主來源,老 HIS 的職責首次真正被接管一塊。

階段 4:絞殺與收斂(讓老 HIS 退到能下線)

(這一階段的終點在 2026-07-27 改了,見上面那一節:終點是凍結,不是下線。下線只在沒有使用者的時候才做。)

當越來越多資料域的主來源移到 FHIR 這側,老 HIS 被讀的表越來越少、被寫的路徑越來越窄,它就從「核心系統」被絞殺成「幾張還在被讀的舊表」。這時候才有條件討論:哪些老模組可以正式下線、哪些資料做歷史封存、整台老 HIS 何時能退役。

停在這裡的價值:這才是傳統意義上「換掉了老 HIS」,但它是前面每一小步累積的結果,不是一次豪賭。

你這週就能動手的第一個 PoC

不用等整份路線圖被核准。這週就可以做階段 1 的一個最小切片:

  1. 選一個資源,建議 Observation(某一類檢驗結果,例如某個常用生化項目),因為它獨立、唯讀、又立刻對臨床有感。
  2. 在老 HIS 開一個唯讀來源(view / replica / 既有介面擇一)。
  3. 用 Rails 寫一支最小的轉譯層:收到 GET /Observation?patient=X&code=Y,去老 HIS 撈、轉成 TW Core Observation Laboratory Result 的 FHIR JSON 回傳,代碼對到 LOINC。
  4. 掛到 FHIR Server 後面,用一個病人的真實(去識別化)資料,證明標準查詢拿得到正確結果。
  5. 收尾:把這支的對映清單、代碼轉換寫成文件,它就是轉譯層的第一份規格,也是下一個資源的模板。

這個 PoC 一個人一週內做得出來,而且完全不碰老 HIS 的寫入,失敗最壞只是查不到,不會弄壞任何東西。它同時驗證了三件事:轉譯層的寫法、TW Core Profile 的套用、Terminology 對映的流程。之後每加一個資源都是重複這個模板。

PHI 資安與治理:從第一步就內建,不是後補

碰的是真實病歷,資安不能等到後面才想。幾個原則從階段 0 就要立:

  • 存取一律走 Policy Gateway,不開資料庫直連。授權(誰能讀誰)、稽核(誰在何時讀了什麼)、最小揭露(只回必要欄位)集中在這一層,不散落在各 App。
  • OAuth2 / Scopes 做細粒度授權。SMART 的 scope 讓「這支 App 只能讀這個病人的用藥」變成標準寫法,而不是各自土法煉鋼。
  • 去識別化資料先行。PoC 與開發階段一律用去識別化資料,真實 PHI 只在通過稽核的正式路徑上流動。
  • 稽核軌跡可回溯。因為身分授權跟臨床邏輯是分層的(Policy Gateway 對 CQL),出事時查得出來是哪一關放行的。這正是我刻意不讓 CQL 碰授權的理由。

AI coding 的把關紀律:越關鍵,人跟規格壓越重

一個人要維護會碰病歷的系統,AI coding 是槓桿,但不能是失控的槓桿。我這套的紀律在 完整 Agentic Workflow 講過,套到這份遷移上,重點是臨界性光譜:同一套流程,依風險調整人與規格的密度。

  • 唯讀門面(階段 1):風險低,AI 可以生得快,但仍走 Spectra 的 discuss/propose/apply/archive,把對映規格版控起來,將來別人接得住。
  • 回寫路徑(階段 3):風險高、不可逆,人要壓得很重。每個寫入場景走完整規格、走 spectra-audit 稽核 sharp edges、有測試、有回滾方案,不因為「只改一點點」就跳過。
  • 共通底線:意圖寫成可版控的工件,agent 就不會漂,你事後也審得動。醫療系統最怕的不是寫得慢,是「看起來會動、其實默默錯」的 vibe coding 上了線。

Kamal 讓每一階段都能一個人零停機佈署、要的話能 rollback,這讓「小步上線、單獨回滾」從口號變成真的做得到。

常見顧慮,我不迴避

  • 「老 HIS 原廠不配合怎麼辦?」 階段 1 是唯讀,很多時候只需要一個 read replica 或既有介面就能起步,不必原廠改他們的系統。絞殺榕的好處就是不需要對方主動開放,你在外面包一層就能開始。
  • 「資料對映對不齊怎麼辦?」 這正是轉譯層存在的理由。對不齊是常態,把不齊擋在轉譯層裡處理,別讓它污染上層標準。每個資源的對映都是一份可以慢慢修正的規格。
  • 「會不會做到一半停在半路?」 會,而且沒關係。這條路線每一階段都設計成「停在這裡也有價值」。停在階段 1 你有標準查詢介面,停在階段 2 你有一支在線上的 App,不是非得走到階段 4 才回本。
  • 「一個人真的扛得動?」 扛得動的前提是每一段複雜度都被壓到可掌握:FHIR Server 用 Kamal Accessory、轉譯層用 Rails 慣例、把關用 Spectra、佈署用 Kamal。這份建議書的每個技術選擇,都是為了「一個人也推得動」而挑的。

相關頁面

medical 公開 smart-on-fhir fhir his legacy-migration strangler-fig tw-core-ig hapi-fhir rails ai-coding spectra criticality-spectrum phi kamal oauth2 interoperability facade-pattern anti-corruption-layer one-person-framework 建議書