門診遇到一個有意思的個案,想查一下教科書怎麼講、想跟 AI 對一對自己的鑑別診斷有沒有漏掉什麼。這件事我幾乎每週都在做。
但病歷不能就這樣貼上去。
這跟「我信不信任那家 AI 公司」是兩回事。資料一旦離開這台電腦,它去了哪裡、留多久、被誰看過,就不再是我能決定的事。所以我想要的不是一個更好的承諾,是一個機械上的動作:在資料離開之前,先把身分拿掉。
我把自己在用的那套整理乾淨、開源了,叫 chart-scrub。
- 程式碼:github.com/drpwchen/chart-scrub
- 線上試用:drpwchen.github.io/chart-scrub(整頁在你的瀏覽器裡跑,什麼都不會上傳)
它做什麼
一筆門診紀錄丟進去:
病歷號碼:1234567 姓名:王大明 出生:1971/03/05
主訴:右肩痛三個月,夜間痛醒。王大明表示上個月在李阿嬤介紹的推拿館推過。
電話 0912-345-678,住新北市板橋區文化路一段100號5樓。
出來變成:
[代號 PT-0001,55歲]
病歷號碼:PT-0001 姓名:PT-0001 出生:[55歲]
主訴:右肩痛三個月,夜間痛醒。PT-0001表示上個月在[稱謂]介紹的推拿館推過。
電話 [電話],住[地址]。
臨床內容一個字沒動,身分不見了。
為什麼不是直接遮成「[姓名]」就好
這是整件事我最先想通的地方。
如果只是把名字換成 [姓名],那同一個病人下次回診、下下次回診,三筆紀錄之間就沒有任何東西把他們串起來了。可是「這個人三個月前打過針、當時有效、這次又痛回來」,這種前後對照本來就是臨床判斷的一部分,弄丟了,剩下的資料就沒什麼討論價值。
所以第一層做的是定向抽換:這個人變成 PT-0001,下次回診他還是 PT-0001。代號對回真名的那張表,只存在我自己電腦上的一個 SQLite 檔,不進雲端、不進筆記庫、也不會給任何 AI 讀。
生日也一樣:直接刪掉可惜,換算成年齡剛好。臨床要的是「55 歲」,不是那個日期。
第二層:一張寧可多遮的網子
定向抽換只能處理「我知道他是誰」的那個人。紀錄裡還會有一堆我事先不知道的東西:電話、身分證、地址、順口提到的其他人。
第二層就是一組正規表示式規則,把這些東西通通遮掉。設計取向很單純:寧可多遮。偶爾誤遮一個醫學名詞,代價是我重看一次;漏掉一個名字,代價大得多。
有一個順序上的細節,是實作時才想清楚的:定向抽換一定要在通用規則之前跑。反過來的話,名字已經先被遮成 [姓名] 了,代號就再也接不上去,跨次就診的那條線也就斷了。
機器自己驗自己
寫完之後我最不放心的是:它到底有沒有真的遮乾淨?
所以最後一步是殘留檢查:程式跑完,拿它自己知道的每一個真名、每一個病歷號,回頭搜自己剛產出的那份輸出。只要有一個活著,這筆就判定失敗、離開碼給 2,我不會拿去用。
另外還有兩個訊號是抓「漏網」的形狀:一個中文姓名黏在已經被遮掉的身分證旁邊、以及長得像身分證卻沒被遮的字串。這兩種都不是「我知道他叫什麼」,而是「這個位置本來就該有東西被處理掉」。
這件事的通則
自動化最怕的不是失敗,是安靜地成功。所以每一段自動化的最後,都值得補一個「回頭檢查自己」的動作,而且要讓它有辦法大聲喊失敗。
開源的過程中,抓到自己三個 bug
這部分我覺得比工具本身有意思。為了公開,我補了測試、逐條檢查,結果揪出三個原本在用的版本就有的問題,而且三個都是那種不會報錯的失敗。
第一個:我習慣的手寫格式是「身分證號+姓名」擠在同一行。但實際的紀錄常常長這樣:
陪同者 A123456789 李小華
程式抓到的姓名是 「陪同者」,不是李小華。因為規則寫成「身分證號旁邊的 2 到 4 個中文字」,而「陪同者」剛好也是三個中文字,還排在前面。結果就是:一個角色詞被登記成病人,真正的名字反而漏掉。修法是要求姓名的第一個字必須是常見姓氏。
第二個:醫院系統匯出的文字檔編碼很雜,UTF-8、UTF-16、Big5 都遇過,所以程式會一個一個試。問題是一個 Big5 檔用 UTF-16 去解,不會出錯,會解出一堆亂碼。而亂碼不會命中任何一條規則,所以程式一路跑到底、印出「全數通過」,實際上它什麼都沒遮到。修法是:只有在檔案真的帶著 UTF-16 的標記時才用 UTF-16 去解。
第三個是把修正搬回自己那套的時候測出來的,我覺得這個最陰。程式要把病人的病歷號換成代號,用的是直接字串取代。但病歷號 1234567,剛好是同一段文字裡另一個人身分證 A123456789 的中間那一段。取代下去,那個身分證變成了 APT-000189:它不是被遮掉,是被打壞了。而且它壞掉之後就不再長得像身分證,所以我那個專門抓「有沒有漏掉的身分證」的檢查,也跟著看不見它。同一種撞法還能把電話號碼切成兩半,讓電話規則對剩下的部分不再命中。修法是取代時要求前後不能接著英數字。
這三個都是同一種毛病:流程沒有壞掉,只是沒有在做事。這也是為什麼我後來養成習慣,測試寫完要故意把程式改壞一次,看測試抓不抓得到。抓不到的話,那條測試只是裝飾。第三個 bug 的第二條測試就是這樣補出來的:我把修正改回錯的寫法,第一條測試竟然還是綠的,才發現另一條路徑根本沒被測到。
它抓不到什麼
這段請看兩遍,因為它決定了你能怎麼用這個東西。而且最重要的那一條,不是靠加規則可以解決的。
有些人,拿掉全部識別資訊還是認得出來
兩種情況:
第一種,身分本身只有一個人。 一個全國只有一位的職稱,寫出來就等於指名道姓。但對程式來說那只是個普通名詞,跟「病人」「老師」沒有兩樣,它不會覺得有什麼需要處理。
第二種,事件本身就是識別資訊。 重大災害之後,一句「颱風天被女兒牆砸到」,看到的人就知道是誰了。那句話裡一個名字、一個號碼都沒有。
讓這兩種情況被還原的不是某個字串,而是「這段敘述只會指向一個人」。比對規則看不見這件事,再加一百條規則也看不見。只有你看得見,因為只有你知道這個故事有多罕見。如果同事光看敘述就認得出是哪一位,那它就不是「要不要跑去識別化」的問題,是「這段要不要寫下來」的問題。
其他比較一般的
- 沒有線索的名字會漏。「小明今天回診」沒有稱謂、沒有角色詞、也不是自我介紹,程式不會動它。線上試用頁的第二個範例我故意留了一句這種,你可以自己看它漏在哪裡。
- 姓氏表只有約一百個常見姓,罕見姓氏不會被當成名字。
- 純規則,沒有 NER 模型,它不會理解上下文。
- 就診日期是保留的,因為臨床判讀通常需要。要符合 HIPAA safe harbor 那套日期規則的話,得自己加。
所以它是第一道網子,不是保證
我自己的用法是:程式跑完,我還是會把輸出看過一遍才決定要不要送出去。把「有沒有遮乾淨」整個交給程式判斷,本身就是一個風險。
每家醫院的資料都長不一樣
這裡面所有的格式假設,病歷號在哪一欄、生日寫民國還是西元、一次貼一個病人還是十二個,全都是從我自己的門診流程長出來的。你的一定不一樣。
所以我的建議很直接:fork 過去改成你的版本。README 裡有一張表,寫清楚最可能要動的五個地方分別在哪個函式。授權是 MIT,隨便你改。
如果你改出了更好的做法,或是覺得哪裡想得不對,歡迎到 repo 開 issue 告訴我。這東西還在長,我很清楚它現在離「完美」還有一段距離,但我想與其等它完美,不如先放出來讓大家一起看。
相關的兩篇
- 同樣是「不要把安全全押在 AI 每次都判斷正確」這個想法,只是換到瀏覽器那一層:把密碼存瀏覽器、又讓 AI 開瀏覽器,這樣安全嗎?
- 我怎麼把教科書變成 AI 讀得動、而且每句話都標得出章節出處的筆記:Textbook to Note
