triAIge:評審報告
triAIge 是一套在邊緣運算上執行的 AI 交接系統,處理台灣急救最前面的幾分鐘。救護車上的救護員(EMT)把看到的情況說出來或打字輸入,系統就填寫消防局的救護紀錄表,並傳到急診(ED)。檢傷護理師打開的是預先填好、附上建議檢傷級數的評估表,醫師拿到的是交接摘要,以及一份附出處的鑑別診斷與建議檢查。
這份報告的讀者大概只會讀一次,所以每一項說法都附上檔案出處,路徑都相對於 repository 根目錄;repository 裡沒有的數字,報告也不寫。
這是黑客松原型,不是經過臨床驗證的產品。哪些有量測、哪些沒有,第 7 節逐項說明。
1. 目前完成了什麼?
| 狀態 | 位置 | |
|---|---|---|
| 救護端 app、急診檢傷 app、醫師畫面(web) | 已完成;/ 連真後端,/internal 連瀏覽器內的模擬後端(mock) | web/README.md、docs/specs/2026-10-13-frontend-restructure/decision.md |
階段銜接(/) | 救護端送出後播「救護車」轉場,落在急診檢傷台;檢傷送出後播「看診區」轉場,等鑑別診斷整理好再進醫師摘要。轉場可以略過 | web/src/components/flow/stage-advance.tsx、web/src/components/guide/transition/waits.ts |
| 後端:FastAPI、PostgreSQL、42 個 API 操作(含即時推播 SSE) | 已完成 | docs/api/openapi.yaml、docs/specs/2026-10-13-backend/design.md |
| 在 DGX Spark 級主機上用 Nemotron 3 Super(NVFP4)擷取欄位 | 已完成,已評測 | eval/README.md |
| 以規則產生的 TTAS 建議、完整性檢查、規則版交接摘要與醫師摘要 | 已完成。這一份摘要由規則從紀錄組出,不呼叫模型,永遠存在 | server/src/triaige_server/engine/views.py、engine/ttas.py |
| 交接摘要的「AI 整理」(階段 2) | 已完成,預設開啟。救護端送出與檢傷送出之後,Super 在背景把紀錄改寫成最多 6 句附來源的繁體中文;失敗時規則版照常顯示 | server/src/triaige_server/engine/ai_summary.py、task_rules/summary_checks.py |
| 醫師端鑑別診斷(階段 2) | 已完成,Spark 上開啟。一個會搜尋知識庫的 agent,列出常見的前 5 項與最致命的前 5 項,每項附事實與出處。沒有醫師標準答案,所以沒有準確度數字(§7) | server/src/triaige_server/ai/nat_differential.py、kb.py、task_rules/differential_checks.py、infra/inventory/group_vars/servers.yml |
| 建議追問、護理師輸入擷取、急救責任醫院分級建議(階段 2) | 已完成;建議追問與護理師輸入擷取預設開啟,分級建議在 Spark 上開啟。三者都是建議,由人決定 | server/src/triaige_server/ai/followups.py、ai/nurse_notes.py、engine/hospital_level.py |
| 在欄位上複查 AI 的值(沒有阻斷式對話框)、每個欄位的來源與變更歷程、確認/選取/復原操作 | 已完成 | docs/specs/2026-10-14-inline-review/plan.md |
| 由 token 對數機率算出的欄位信心等級 | 分數已經計算並儲存;門檻校準過兩次,但等級還沒有顯示(enabled=false),因為門檻分不出差別,見 §9 | server/src/triaige_server/llm/profiles/nemotron_3_super/confidence_thresholds.json、eval/results/choice-confidence-2026-10-18/summary.md |
| 案件備忘的自動整理 | 程式已寫好,Spark 上關閉,保留集通過後才打開 | infra/inventory/group_vars/servers.yml(triaige_memo_consolidate: false) |
| 語音辨識 | 不在這個 repository 裡。 API 接受語音訊息(inputMode: voice),規劃是在瀏覽器內辨識,但 web app 還沒有辨識器,展示版用文字訊息操作 | docs/api/README.md §6 |
| 真實的醫院系統(HIS/EMR、健保卡) | 用虛構病人代替 | docs/api/README.md §1.3 |
web app 的線上版:https://triaige.wayneh.tw 與 https://triaige-demo.vercel.app(README.md、infra/inventory/group_vars/servers.yml 的 CORS 清單)。展示用的病人都是虛構的;/internal 的資料只存在瀏覽器。
2. 要解決什麼問題?
在台灣,消防局的救護員一邊處置病人,一邊在紙本格式的救護紀錄表上記錄;到了急診,同樣的問題又被問一次。急診檢傷護理師要從台灣急診檢傷與急迫度分級量表(Taiwan Triage and Acuity Scale,TTAS)的五個級數中選一個,醫師則要很快掌握關鍵事實。docs/product/triaige.md §2 列出這個流程出問題的地方:
- 救護員在壓力下打字或手寫,資訊因此晚到,或重複出現。
- 到院時,急診又重問一次主訴、病史、過敏與生命徵象。
- 院前資料、檢傷資料與醫院病歷分散在不同的系統。
- 醫師需要的是主要問題、危險徵象、已給的處置、生命徵象趨勢、病史、用藥、過敏與資訊缺口。
參考表單是救護紀錄表與 TTAS 判定標準,兩者都是公開的政府文件(docs/references/、README.md)。
產品的定位很明確:AI 只負責擷取、整理與提出建議,凡是有公開規則可循的臨床決定,都由確定性規則處理(docs/product/triaige.md §3.3)。AI 不設定檢傷級數;級數由護理師送出,改動建議級數時要附上理由(docs/api/data-model.md §10)。鑑別診斷、急救責任醫院分級與 AI 整理的摘要,都只是供人參考的選項,不是診斷,也不是醫囑(server/src/triaige_server/llm/prompt_sets/ems_compact/differential.py、docs/api/openapi.yaml 的 AiHandoffSummary)。
3. 系統從頭到尾做了哪些事?
flowchart LR
EMT["救護員平板:救護端 app"] -->|"一句話 = 一則訊息"| API["DGX Spark 上的 FastAPI 伺服器"]
API --> SCH["排程器:同時最多 3 個模型呼叫,分 P0、P1、P2、閒置四級"]
SCH --> NAT["NeMo Agent Toolkit 工作流程"]
NAT --> LLM["透過 vLLM 執行的 Nemotron 3 Super"]
LLM --> CHK["確定性檢查"]
CHK --> REC["PostgreSQL 裡的救護紀錄:每個欄位的來源、證據、歷程"]
REC -->|"推播事件"| EMT
REC -->|"送出"| NURSE["急診檢傷護理師:預先填好的評估表"]
NURSE --> TTAS["TTAS 規則引擎:建議級數與理由"]
TTAS -->|"護理師決定並送出"| DOC["醫師端:規則版摘要、AI 整理、鑑別診斷"]
NAT -.->|"鑑別診斷 agent 搜尋"| KB[("知識庫 kb_passages")]
KB --> NAT- 接收輸入。 救護員一次送出一句話,並標上說話者(救護員、病人、旁人)。伺服器先存下這句話、立刻回應,再在背景執行 AI,所以畫面不必等模型(
docs/specs/2026-10-13-backend/design.md〈Extraction flow〉)。 - 擷取欄位。 每一句只呼叫一次模型(現場擷取關閉推理),回傳 12 個紀錄欄位的 JSON:年齡、性別、主訴、體溫、心跳、呼吸速率、SpO₂、收縮壓與舒張壓、疼痛、過去病史、重點用藥類別,以及藥物過敏(
docs/specs/2026-10-13-backend/spec.md§1.3、docs/api/field-mapping.md§6)。每一句的提示詞會帶上最多三句前文,以及紀錄上已有的內容,所以「不對,是一百三」能更正到正確的那個值(eval/README.md〈R1 上下文(X1)〉)。 - 在欄位上複查,不用阻斷式對話框。 救護員、病人與旁人說的內容走同一套寫入規則:寫進空欄位,或取代還沒複查的值。這個欄位會標成「待複查」,附上來源句與說話者,救護員可以確認、修改、改選另一個候選值、清除或復原。人填的值或已確認的值不會被覆蓋,AI 給的不同值以替代選項列在旁邊。說話者辨識信心低的救護員句子也照同一套規則寫入,並標明來源。缺漏的必填欄位與 TTAS 參考級數固定顯示在一個區域,不用彈出視窗(
docs/api/data-model.md§2、server/src/triaige_server/engine/record.py、docs/specs/2026-10-14-inline-review/plan.md)。 - 救護端的背景建議。 兩種建議都是補充:失敗只記稽核,不擋任何操作,也不算「AI 暫停」。
- 建議追問。 對話停了 8 秒、而且擷取都結束,伺服器讀一次案件,請模型列出救護員還該問的事(發病時間、疼痛分數、過去病史、過敏、是否懷孕、血糖)。每個頻道最多同時開 3 題;欄位已有值的不開,忽略過的不會再回來。問題放進既有的建議清單,不彈出、不發聲(
server/src/triaige_server/ai/followups.py、engine/followups.py、task_rules/followup_checks.py)。 - 急救責任醫院分級。 紀錄表欄位變動後,模型讀已填欄位,建議重度、中度或一般急救責任醫院,並附一句理由與引用的欄位;欄位標「待複查」,救護員可以改。臨床下限由規則加上:到院前檢傷 1–2 級、危急個案、心電圖疑似 STEMI 任一成立時,模型建議的「一般」一律改成「中度」。這個欄位只給救護端參考,不進送出的快照,急診看不到(
server/src/triaige_server/engine/hospital_level.py、task_rules/hospital_level_checks.py)。
- 建議追問。 對話停了 8 秒、而且擷取都結束,伺服器讀一次案件,請模型列出救護員還該問的事(發病時間、疼痛分數、過去病史、過敏、是否懷孕、血糖)。每個頻道最多同時開 3 題;欄位已有值的不開,忽略過的不會再回來。問題放進既有的建議清單,不彈出、不發聲(
- 檢傷。 紀錄送出後,護理師看到院前紀錄已預先填入檢傷表;救護端沒人確認過的欄位會標成「未複查」,並附上來源。TTAS 引擎算出建議級數,每一項理由都附上觸發的規則,需要人判斷的條件(例如「有沒有休克?」)會列出來,引擎不替人決定(
docs/api/data-model.md§10、server/src/triaige_server/engine/ttas.py)。護理師可以在檢傷畫面打一段話:一次模型呼叫把它擷取成檢傷欄位的提案,掛在那則訊息上,護理師按「套用」才寫入;只提案白名單欄位與合法的值,數值必須是原文裡寫出的數字,護理師已經填的值不會被蓋掉。護理師端也有建議追問,例如 TOCC 還沒問的題目(server/src/triaige_server/ai/nurse_notes.py、engine/nurse_notes.py、task_rules/nurse_note_checks.py)。 - 醫師摘要。 目前狀況、院前經過與危險徵象,由規則從紀錄與檢傷資料組出,每一句都附上來源欄位的參照(
server/src/triaige_server/engine/views.py)。規則版旁邊多一塊「AI 整理」:救護端送出與檢傷送出之後,模型把送出的紀錄表、備忘與檢傷評估改寫成最多 6 句,每句不超過 160 字,並附來源。確定性檢查會丟掉來源不存在或沒有內容的句子、數字不在引用欄位裡的句子,以及含診斷、治療或劑量用語的句子。呼叫失敗或檢查不過時,畫面照實標示失敗,規則版照常顯示;不重試,也沒有備援模型(server/src/triaige_server/engine/ai_summary.py、task_rules/summary_checks.py)。 - 鑑別診斷。 案件送出(之後內容有變的再送出也算)與檢傷送出之後,後端在背景執行一個會呼叫工具的 agent(NeMo Agent Toolkit 的串流函式,在同一個行程內執行)。它讀的是案件事實:最新送出的紀錄、檢傷評估、已確認的身分與院內既往資料裡填了值的欄位,每個欄位有一個編號(f1、f2…)。模型開推理,最多搜尋 4 次知識庫(工具
search_knowledge,可限定來源,每次回傳 1–8 段,每段給模型最多 1,800 字),讀完段落後回答兩份清單:常見的前 5 項,以及最致命的前 5 項(漏掉會致命或致殘,即使可能性低也要列)。每一項包含診斷名稱、支持的事實、不符合或還缺的資料、2–4 項建議檢查,並用事實編號與段落編號標出依據(server/src/triaige_server/ai/nat_differential.py、llm/prompt_sets/ems_compact/differential.py)。- 確定性檢查(
task_rules/differential_checks.py):事實編號必須對得到案件裡填了值的欄位,對不到的拿掉,一個都不剩的項目整個丟掉;段落編號只認這一次搜尋真的回傳過的,並換成「來源、章節、頁碼」的出處字串,書中原文不會進輸出;每份清單最多 5 項,重複的診斷丟掉;診斷名稱含百分比,或文字裡出現機率、信心分數的項目整個丟掉;任何一份清單沒有合格的項目,整次就失敗。失敗時畫面明說,不重試,也沒有備援模型。 - 排程。 優先級是最低的「閒置」:只在沒有任何其他 AI 工作時才開始,即時句子一到就取消、排回隊伍重跑;等了 60 秒或被取消 3 次,就升成一般背景工作。一次執行上限 600 秒(
server/src/triaige_server/ai/scheduler.py、ai/service.py、llm/profiles/nemotron_3_super/__init__.py)。 - 耗時。 約 1–3 分鐘(推理開啟;實測 70–110 秒,正式機 UAT 一次約 2.5 分鐘)。
- 醫師看到什麼。 每一步(搜尋了什麼、讀到哪些來源、章節與頁碼,最後整理成清單)即時經 SSE 事件
differentials.updated推給醫師,護理師與救護員收不到。「看診區」轉場會逐步列出這些步驟;整理中或失敗時,醫師摘要仍保留上一次通過檢查的結果,並標明狀態(server/src/triaige_server/engine/differentials.py、docs/api/asyncapi.yaml、web/src/hooks/use-differentials-progress.ts)。
- 確定性檢查(
救護端與急診端的畫面共用同一組元件。根路徑連真伺服器,/internal 連瀏覽器內的模擬後端,兩邊的案件規則相同,並由漂移測試確保兩個引擎一致(docs/architecture/README.md §2、server/tests/unit/test_engine_parity.py)。
4. 用了哪些 <帳號> 技術?
| 元件 | 用途 | 狀態 | 位置 |
|---|---|---|---|
| Nemotron 3 Super 120B-A12B,NVFP4 | 流程中唯一的模型。現場擷取與背景的主訴翻譯、AI 整理、建議追問、護理師輸入擷取、急救責任醫院分級都關閉推理;醫師端的鑑別診斷 agent 開推理(low_effort) | 執行中 | infra/inventory/group_vars/servers.yml、server/src/triaige_server/llm/profiles/nemotron_3_super/__init__.py、docs/architecture/README.md §6 |
| vLLM v0.31.0(GB10 的 DGX Spark 級主機) | 以 OpenAI 相容 API 提供模型服務;多 token 預測(multi-token prediction)的推測解碼(3 個 token)、前綴快取,並提供鑑別診斷 agent 用的工具呼叫(--enable-auto-tool-choice --tool-call-parser qwen3_xml) | 執行中 | infra/README.md、infra/playbooks/templates/nemotron/docker-compose.yml.j2 |
| NeMo Agent Toolkit 1.9.0 | 每一次模型呼叫都是在 FastAPI 行程內執行的 NAT 函式,不另外起服務。鑑別診斷是其中一個 NAT 串流函式,自己跑「搜尋、讀、再搜尋」的多步驟迴圈;其他任務都是單次呼叫 | 執行中 | docs/specs/2026-10-13-backend/design.md〈Implementation notes〉、server/src/triaige_server/ai/workflow.yml、ai/nat_differential.py |
NeMo Guardrails(透過 NAT 的 guardrails 中介層) | 中介層已經包在工作流程外面。self check output 護欄已經寫好,但預設關閉,因為每次擷取要多一次模型呼叫(設計文件估計約 1–3 秒)。一直開著的安全檢查是 §6 的確定性檢查 | 已接上,護欄關閉 | docs/specs/2026-10-13-backend/design.md、infra/inventory/group_vars/servers.yml(triaige_self_check_output: false) |
| Nemotron 3.5 Lightning 30B-A3B | 評估過能否當作較快的擷取模型,沒有採用(見 §7) | 已評測 | eval/README.md〈服務參數實測〉 |
| Nemotron 3.5 Super VL 120B-A12B(只用文字) | 評估過能否取代 Super,沒有採用:事先宣告的標準三項都沒過(見 §5) | 已評測 | eval/results/nemotron-3-5-super-vl-2026-10-18/README.md |
| Nemotron embedding 模型加 pgvector | 沒有使用。 知識庫用 PostgreSQL 全文檢索。資料庫映像是 pgvector 版,但沒有建立向量擴充套件,kb_passages.embedding 欄位是空的;要不要加向量檢索,之後用案例比對再決定 | 預留 | docs/specs/2026-10-13-backend/decision.md、server/src/triaige_server/migrations/001_init.sql |
| NeMo Evaluator、OpenShell 沙箱 | 規劃在階段 2 | 規劃中 | docs/specs/2026-10-13-backend/decision.md |
| NemoClaw | 沒有整合。調查後發現它還在 alpha 階段,也沒有文件說明如何接上我們自己的智慧體 | 只在規劃 | docs/specs/2026-10-13-backend/research-<帳號>-stack.md |
模型、伺服器與資料庫都跑在同一台 Spark 上,推論不使用任何雲端模型(docs/product/triaige.md §3.2)。
鑑別診斷用的知識庫
知識庫是 PostgreSQL 的 kb_passages 表,共 3,714 段,來自三個來源。檢索是 PostgreSQL 全文檢索:英文用 english 設定的 tsv(標題權重高於內文),中文沒有空白斷詞,匯入與查詢時都切成相鄰兩字,存進 tsv_zh;查詢字串含中文字就用 tsv_zh,否則用 tsv;排序用 ts_rank_cd。agent 只依賴 KnowledgeBase.search 這個介面,之後換成向量檢索,agent 與提示詞都不用改(server/src/triaige_server/kb.py、migrations/003_kb_search.sql)。三份索引先在開發機用 eval/scripts/*_rag.py 建好,再由 server/scripts/export_kb.py 合成一個檔案,部署時匯入 Spark 上的資料庫(-e triaige_kb_import=true,infra/README.md〈知識庫〉)。檔案含版權內容,只放 private/,不進 git。
| 來源 | 版本與出版 | 收錄範圍 | 段落數 | 使用依據 |
|---|---|---|---|---|
| Tintinalli's Emergency Medicine Manual | 第 7 版,McGraw-Hill,2012 | 22 篇 188 章中的 167 章;第 9 篇兒科 21 章不收,因為示範的病人都是成人 | 1,745(兒科另有 255 段,不匯入) | 示範用。版權頁不允許未經同意存入檢索系統,授權還沒有解決。畫面與輸出只顯示來源、章節、頁碼,不顯示原文。正式使用要換成醫院自行取得授權的知識庫(kb.py 的介面可換實作)。出版年是 2012,提示詞要求與較新的指南不一致時以指南為準 |
| 開放授權的臨床指引與共識(PubMed Central,PMC) | 31 份,期刊年份 2013–2025 | 只收 PMC 全文的 JATS <license> 是 Creative Commons 的指引;非 CC 的拒收。依章節切段,含摘要、內文與表格 | 1,688 | 授權:CC BY 15 份、CC BY-NC 11 份、CC BY-NC-ND 5 份,所以其中 16 份只能非商業使用,不得改作後散布。每段記 PMCID 與授權,出處格式為「簡稱(期刊 年份,PMCID,授權),§ 章節」。全文放 private/,repo 只放來源清單 |
| 臺北市政府緊急救護技術員到院前緊急救護作業程序手冊 | 第六版(封面 115 年 4 月),臺北市政府消防局醫療顧問委員會總編輯,臺北市政府消防局出版;PDF 140 頁 | 全書:通用程序(G)、非外傷與外傷各程序(M、T)、特殊情境(S)、技術(A)、附錄與前言,依標題與編號步驟切段,流程圖與表格盡量整塊 | 281 | 臺北市的作業程序,不是全國規範。版權屬臺北市政府消防局,版權頁寫「欲參採或引用請事先書面申請」,沒有公開授權;我們還沒有取得書面同意,而建索引本身就算參採(eval/README.md),全文現在也在資料庫裡,這個授權問題尚未解決(docs/team-discussion-todo.md A16)。使用手冊裡的切點數字(呼吸 ≥ 30、血糖 < 60 或 ≥ 500)為生命徵象標色,使用者 2026-10-20 決定不算引用 |
31 份指引的清單。 完整清單(PMCID、授權)在 private/guidelines/fetched.json(不進 git);repo 裡有候選來源清單 eval/guidelines/sources.json(簡稱、PMCID、主題)。依主題:
- 胸痛:ANMCO-SIMEU 2016、Brazilian Chest Pain ED 2025、SIRM-SIC 2025
- 呼吸、給氧與肺栓塞:GOLD 2023、BTS Oxygen 2017、TSANZ Oxygen 2015、BTS Outpatient PE 2018、PERT Consortium PE 2019
- 心衰竭:KSHF Acute HF 2023
- 敗血症與感染:J-SSCG 2020、SEOM Febrile Neutropenia 2018
- 神經:ED Delirium GRADE 2026、EUSEM Syncope 2024、Acute Vertigo ED Consensus 2024
- 消化與腹部:WSES 腸繫膜缺血 2022、WSES 消化性潰瘍 2020、ESGE 非靜脈曲張上消化道出血 2018、ESGE 靜脈曲張出血 2020、Asia-Pacific 非靜脈曲張上消化道出血 2018、WSES 胰臟炎 2019、WSES 膽囊炎 2016、WSES-SICG 高齡膽囊炎 2017、BSG 腹水 2021
- 內分泌與代謝:高血糖危象共識 2024(Diabetologia)、JBDS HHS 2023、ESE-ES 腎上腺功能不全 2024、ED 高血鉀共識 2021
- 創傷:歐洲創傷出血指引 2023、WSES 高齡創傷 2023、WSES-AAST 胸部創傷 2025、Scandinavian 頭部外傷 2013
(以上資料來源與數字:eval/README.md〈Tintinalli 檢索(RAG)〉、〈開放指南索引〉、〈臺北市到院前救護手冊檢索〉,docs/references/ems-protocol-taipei/README.md,段落數由 server/scripts/export_kb.py 匯出的檔案逐來源計數。)
5. 模型換版時,程式怎麼不受影響?
模型更新得很快,為某一個模型做的調整,不該分散在整個程式裡。現行模型的所有設定都集中在套件 server/src/triaige_server/llm/ 的 ModelProfile 物件裡,包括提示詞、取樣參數、怎麼關閉推理、怎麼把模型的回覆整理成一個 JSON 物件、怎麼讀取對數機率、逾時,以及同時請求數(docs/architecture/README.md §6)。
flowchart TB API["api:路由、推播事件"] --> AI["ai:排程、NAT 黏合、寫入閘門"] API --> ENG["engine:案件規則、TTAS"] AI --> ENG AI --> TR["task_rules:合格答案長什麼樣"] AI --> LLM["llm:ModelProfile、提示詞、回覆清理、對數機率評分"] AI --> KB["kb:知識庫檢索"] ENG --> TR LLM --> TR INFRA["infra:模型部署手冊與變數"] -.-> LLM
- 換模型只需要改
llm/,加上infra/裡那個模型的部署腳本(playbook)。步驟寫在docs/architecture/README.md§9;infra/inventory/group_vars/servers.yml裡的triaige_model_profile、llm_served_name與llm_port,同時提供給伺服器與 vLLM 的部署腳本使用。 task_rules/放的是任何模型都需要的檢查(數值範圍、允許的詞彙、翻譯檢查,以及各項 AI 輸出的檢查),只用標準函式庫;engine/與llm/都不能 importai/。- 這條邊界由測試強制,不靠團隊慣例:
server/tests/unit/test_architecture.py禁止往上層 import,也禁止llm/以外出現模型名稱、取樣參數與端點字串,而且沒有例外清單(docs/architecture/README.md§3)。 - 這個流程實際跑過一次:2026-10-18 評估 Nemotron 3.5 Super VL。事先宣告的標準上三項都比 Super 差(11 個結構化欄位 F1 0.997 對 0.999,主訴 F1 0.525 對 0.547,幻覺欄位 2/402 對平均 0.33),逐句重播的重複填入率是 Super 的 5 倍(8.7% 對 1.6%);它比較快(整段口述延遲中位數少 23%)。所以正式環境維持 Super,評估前後正式環境的 AI 約 57 分鐘不能用(
eval/results/nemotron-3-5-super-vl-2026-10-18/README.md)。 - 限制:提示詞是針對 Super 調整的,對數機率的測試資料(fixture)也只在 Super 上錄過,換成別的模型會得到什麼分數,目前沒有證據。換模型的步驟規定,換之前要依事先宣告的評測標準(yardstick)完整重新評測。
6. 安全上做了哪些設計?
| 機制 | 做法 | 位置 |
|---|---|---|
| 由人決定 | AI 的值是標成「待複查」的草稿。人填的值或已確認的值不會被覆蓋。最終檢傷級數只能由護理師送出,不採用建議級數時要附上理由。鑑別診斷、AI 整理的摘要、急救責任醫院分級與建議追問,都是供人參考的建議 | docs/api/data-model.md §2、§10 |
| 每個模型回覆都經過確定性檢查 | 每一種 AI 任務有自己的檢查:欄位擷取、AI 整理、鑑別診斷、急救責任醫院分級、建議追問、護理師輸入擷取。輸出必須是 JSON 物件;未知欄位、超出範圍的生命徵象、不合法的選項值、來源不存在的句子都會整個丟掉,不猜測。模型層只做兩件格式上的容錯:取出 JSON 物件,以及鑑別診斷的回覆裡事實編號少一個引號時補上 | server/src/triaige_server/task_rules/(record_checks.py、summary_checks.py、differential_checks.py、hospital_level_checks.py、followup_checks.py、nurse_note_checks.py)、llm/prompt_sets/ems_compact/differential.py(repair_reply)、docs/architecture/README.md §7 |
| 「無過敏」要有依據 | 來源句裡沒有提到過敏的字眼時,「無過敏」與「未知」都會被丟掉,因為錯誤的「無」會關掉缺漏過敏的提醒。只提到非藥物過敏的句子,不會勾選「無過敏」 | server/src/triaige_server/task_rules/record_checks.py(模組 docstring)、eval/README.md〈R1 三項待決〉 |
| 問句不是陳述 | 「他有沒有藥物過敏?」不能變成資料。我們在提示詞裡用範例修正,再用測試固定下來。另外有可選的路由分類器與寫入閘門,把可疑的句子改成待確認,不直接寫入;分類器預設關閉 | eval/README.md〈後續:問句不是陳述〉、eval/routing/README.md、docs/api/README.md §6 |
| 失敗就明確失敗,不默默降級 | 每一個 LLM 角色的 NAT 設定都是 num_retries = 1,也就是總共只嘗試一次(NAT 預設會對 429 與 5xx 自動重試,這裡關掉),沒有備援模型。呼叫失敗會以 assistant.failed 事件結束,手動編輯照常可用;連續失敗後,畫面顯示「AI 暫停」。AI 整理與鑑別診斷失敗時,畫面明說失敗,規則版摘要照常。設定了不認得的 profile id,或向不能回傳對數機率的 profile 要求對數機率,伺服器在啟動時就會停止 | server/src/triaige_server/llm/nat_config.py、docs/architecture/README.md §6、§10、server/src/triaige_server/ai/service.py |
| 鑑別診斷只引用,不複製 | 每一項附事實編號與段落編號,編號換回案件欄位與「來源、章節、頁碼」的出處字串;書中原文不進輸出,不接受機率或信心分數;優先級最低,不會拖慢即時擷取。選項是供醫師考慮的,提示詞明寫「不是診斷或醫囑」 | server/src/triaige_server/task_rules/differential_checks.py、llm/prompt_sets/ems_compact/differential.py |
| 急救責任醫院的臨床下限 | 模型可以建議,規則決定下限:到院前檢傷 1–2 級、危急個案、疑似 STEMI 時,不會停在「一般」。人工或已複查的值,AI 之後只會並列另一個值,不會改 | server/src/triaige_server/engine/hospital_level.py |
| 翻譯檢查 | 紀錄保留英文主訴,繁體中文翻譯只是並列顯示,標成「AI 翻譯」。譯文能不能顯示,由一組確定性檢查決定:左右側、身體部位、沒有多出數字、簡體字。依使用者的決定,主訴只要含數字或明確的次數,就只顯示英文 | server/src/triaige_server/task_rules/complaint_guard.py、docs/api/README.md §6、eval/complaint_zh/README.md |
| 用對數機率計算信心 | 對選項欄位(性別、過敏狀態、病史項目),伺服器沿著模型實際寫出的 token,把機率相乘,再乘上「這個欄位該不該出現」的判斷點。如果 token 拼不回模型的回覆,就不給分數,並記錄這次失敗,不隱藏。分數目前只儲存,不顯示(§9) | docs/specs/2026-10-14-inline-review/plan.md §4.3、§4.7、server/src/triaige_server/llm/choice_scoring.py |
| 稽核紀錄 | 每一次寫入、複查操作、AI 失敗與檢傷送出,都是一筆稽核事件,AI 的動作記 AI 名稱與提示詞版本;鑑別診斷另記搜尋次數與被丟掉的項目。每個欄位保留來源(人工、AI、已確認)、證據句與歷程。急診看得到誰在什麼時候改了哪個救護欄位,但看不到草稿內容 | server/src/triaige_server/engine/views.py(audit_for_ed)、engine/differentials.py、docs/api/data-model.md |
| 看得到什麼 | 救護員看自己建立的案件;護理師看可處理醫院的已送出案件;醫師看已完成檢傷的案件,鑑別診斷的即時事件也只推給醫師。示範用的護理師與醫師可以處理三家醫院 | server/src/triaige_server/auth.py(can_see)、migrations/004_user_hospitals.sql |
| 部署防護 | 有期限的簽章 token(預設 12 小時);請求大小上限(256 KiB);日誌不記錄 query string;模型端點只接受本機與 tailnet 的連線;後端只聽本機,對外只經 Cloudflare Tunnel;維運儀表板的公開版是唯讀的,沒有測試面板,不顯示指令列與案件編號 | docs/specs/2026-10-13-backend/design.md〈公開上線的加固〉、infra/README.md |
| 資料最小化 | API 只接收文字,音訊依設計留在平板上。展示用的病人是虛構的,真實病人資料只能放在 git 忽略的 private/ 資料夾。公開的維運頁會顯示 LLM 輸入輸出的摘錄,所以示範輸入不放真實病患資料 | docs/api/README.md §6、README.md、infra/README.md |
還不能算是安全檢查的部分:Guardrails 自我檢查護欄(關閉),以及信心等級的顯示(關閉)。
7. 評測結果與限制
用的是什麼資料?
請先讀這一段。eval/README.md〈誠實規則〉要求我們這樣描述資料:
- 133 張案例卡來自公開的 MIMIC-IV-ED demo,是美國急診 47 位病人的救護車到院紀錄。生命徵象是檢傷站量的,不是在救護車上量的(
eval/README.md〈目前產出〉)。 - 資料裡沒有院前的文字或語音。繁體中文的口述稿由模型寫成,經過程式檢查;擷取這些口述稿的也是同一個模型家族。沒有臨床人員審閱過案例卡或口述稿。
- 所以下面的數字說明的是這條流程從乾淨、由模型寫成的中文裡擷取欄位的能力,不代表臨床驗證、對吵雜語音的耐受度,也不代表在台灣救護案例上的表現。
- 大多數結果只跑一次,而 temperature 0 的呼叫並不確定:一次 A/A 對照在 163 對句子中,有 17 對文字不同(
eval/results/choice-confidence-latency-2026-10-14/README.md)。千分之幾的差異是雜訊。
量到了什麼?
| 問題 | 結果 | 來源 |
|---|---|---|
| 整段口述擷取,現行設定(vLLM v0.31.0、精簡輸出、關閉推理) | 12 個欄位的 Macro-F1 為 0.954(132 段口述,seed 1)與 0.957(133 段,seed 2)。幻覺欄位是 1127 個中 4 個、1147 個中 4 個。呼叫時間中位數 2.89 秒,平均輸出 71 個 token | eval/README.md〈輸出格式與 vLLM 版本〉;eval/results/serving-2026-10-12/predictions-super-v031-compact-draft.report.json、…-seed2.report.json、…metrics.json |
| 各欄位 | 在 seed 1 的報告裡,11 個結構化欄位落在 0.984 到 1.000。chief_complaint 是 0.493,因為字面比對只是下界,有 78 筆等待人工判讀,它也拉低了 macro 平均 | 同一份 …-draft.report.json、eval/README.md |
| 透過真後端逐句處理,47 段保留集口述(163 句) | 11 個結構化欄位 F1 1.000,幻覺欄位 401 個中 0 個,AI 失敗或逾時 0 次,每句中位數 1.42 秒、p95 2.08 秒 | eval/results/r1-context-fix-2026-10-14/yardstick-replay.txt、replay-holdout-cand.metrics.json、eval/README.md |
| 推理開或關(擷取) | 關閉:中位數 4.2 秒,F1 0.956。完整推理:中位數 24.9 秒,F1 0.954。在擷取這個任務上,推理沒有提高準確度,所以現場擷取關閉推理。鑑別診斷是開放式任務,沒有做開關推理的準確度比較;它開推理,這是使用者 2026-10-22 的決定 | eval/README.md〈推理強度〉、server/src/triaige_server/llm/profiles/nemotron_3_super/__init__.py |
| 精簡 JSON 輸出 | 和完整格式相比,延遲低約 31%、輸出 token 少 40%,F1 與幻覺不變 | eval/README.md〈輸出格式與 vLLM 版本〉 |
| 為了更正帶上前文(70 句的合成資料集,沒有臨床人員審閱) | 保留集 34 句:更正召回率 0.909 → 1.000;沒有指名欄位的更正,3/9 → 8/9;非預期的欄位 7 → 1。這裡評的是模型對最後一句的輸出,不是合併後的紀錄。用事先宣告的 yardstick 來看,47 段保留集口述沒有變差 | eval/results/r1-context-fix-2026-10-14/corrections-holdout-verdict.txt、eval/README.md |
| 藥物過敏與否定(71 句,標準答案由工程師草擬,等待醫師審閱) | 只靠提示詞擋不住錯誤的「無」:3 次整段口述中有 2 次,對一個從沒提到過敏的案例寫了「無」。這說明依據檢查必須用確定性的程式,不能只寫在提示詞裡。在 24 句保留集上:狀態準確率 1.000,錯誤的「無」為零,藥名召回率 1.000。這個資料集太小,不能算是驗證 | eval/README.md〈R1 採用標準與重新判定〉、〈後續:問句不是陳述〉 |
| 問句被寫進紀錄 | 仍產生欄位的非紀錄句:15 句中 6 句 → 15 句中 0 句(保留集)。加上可選的寫入閘門做端到端測試,49 句中 17 句 → 49 句中 1 句 | eval/README.md〈後續:問句不是陳述〉、eval/routing/README.md |
| 路由分類器的召回率(判斷句子屬於紀錄內容) | 0.981(51/52),目標是 0.99,沒有達標。漏掉的那一句被寫入閘門攔下,變成一張待確認卡 | eval/routing/README.md |
| 主訴翻譯檢查(工程師草擬的檢查,等待臨床人員審閱) | 在 69 個從未用於調整的新主訴上,63 個通過(91.3%);左右側 18/18 保留,沒有漏掉數字或項目;luq/rlq 被誤擋 2 次。之後又收緊了好幾輪,已提交的結果中 258 組不同的英文/中文配對,有 206 組通過現行檢查,其餘只顯示英文 | eval/complaint_zh/README.md |
| 要求回傳對數機率的成本 | 保留集 163 句,單一請求:中位數 ×1.007,A/A 對照是 ×1.004,量不到成本 | eval/results/choice-confidence-latency-2026-10-14/README.md |
| 選項欄位的信心門檻校準(2026-10-18) | 事先宣告的精確度目標 0.97。在保留集上,性別 60 個值 0 錯,過敏狀態 45 個「高」中 1 錯,過去病史 88 個「高」中 1 錯,三個欄位都達標。但校準出的門檻 τ 把校準集上所有的值都分成「高」,其中 7 個已知錯誤的值也被分成「高」,等於分不出差別,所以等級維持關閉 | eval/results/choice-confidence-2026-10-18/summary.md |
| 同時模型呼叫數(第一台 Spark,2026-10-19) | 3 條同時呼叫跑 30 分鐘:CUDA 錯誤、逾時、重啟都是 0,擷取 p95 3.89 秒。欄位一致率 97.7%,和同一批訊息在 1 條呼叫時對自己重送的 97.8% 相同,所以不一致來自貪婪解碼本身的不決定性。4 條的 p95 是 4.70 秒,超過 4 秒的門檻。正式環境因此設 3 | docs/specs/2026-10-13-ai-routing/soak-results.md、infra/inventory/group_vars/servers.yml(triaige_ai_concurrency: 3) |
| 服務效能 | 單一請求的解碼速度約 31.1 tok/s(v0.31.0),v0.28.0 是 29.8。Super 在 gpu-memory-utilization 0.77 下佔 95.4 GiB,留給主機、伺服器與資料庫約 16 GiB | eval/README.md〈輸出格式與 vLLM 版本〉 |
| 更快的小模型 | Nemotron 3.5 Lightning 30B-A3B 快約 4.3 倍(中位數 1.01 秒對 4.39 秒),F1 是 0.938 對 0.955;在只說出舒張壓的 9 個案例裡,它把舒張壓留空,所以沒有採用。它也不能和 Super 同時放在一台 Spark 上 | eval/README.md〈服務參數實測〉 |
| 臺北市到院前救護手冊的檢索(只在評測中量過) | 281 個段落、45 個自己寫的查詢:章節層級 recall@5 0.978、MRR 0.924。其中 8 個查詢的標準答案是看過結果後才更正的,所以這不是事先未經調整的分數。線上的是 PostgreSQL 全文檢索,這個分數量的是 eval/ 的 SQLite 索引,兩者不是同一份實作 | eval/ems_protocol/README.md |
哪些還沒有量?
- TTAS 準確度。 規則集是
TTAS-adult@105-draft;eval/labels/裡的草稿標記是ai_draft_unreviewed,不能當作標準答案(eval/README.md)。 - 鑑別診斷。 評分流程已經有了,但還沒有醫師的標準答案,所以沒有數字(
eval/README.md〈鑑別診斷評測〉)。線上的 agent(最多搜尋 4 次、搜尋 PostgreSQL 知識庫)也沒有準確度數字;有單元測試的是確定性檢查,檢查通過只代表來源與格式合格,不代表內容正確。 - AI 整理的摘要、建議追問、護理師輸入擷取、急救責任醫院分級。 這四項階段 2 功能沒有保留集評測,只有確定性檢查與單元測試。
- 檢索的線上品質。 三個來源合併後的全文檢索,沒有量過召回率。
- 真實語音、真實救護車環境、台灣案例、兒童。 資料裡沒有兒童案例(
eval/README.md〈目前產出〉)。 - 真實救護員或護理師使用時的易用性。
8. 怎麼部署?
架構是:平板或瀏覽器連到 web app(https://triaige.wayneh.tw、https://triaige-demo.vercel.app),再透過 Cloudflare Tunnel 連到 API https://triaige-api.wayneh.tw。對外服務跑在比賽現場借用的 ASUS GX10(infra/inventory/host.yml 裡的第二台主機);第一台 Spark 是評測與先前部署用的機器,這份報告的 soak 與評測結果都在那一台量。在借用的這台上,triaige-server(port 8100,記憶體上限 1536 MB)連到 PostgreSQL(port 5433,上限 2 GiB,因為知識庫的索引需要空間,其他主機預設 512 MB),以及執行 Nemotron 3 Super 的 vLLM(port 8000)(infra/inventory/group_vars/servers.yml、docs/architecture/README.md §10)。
- 每個服務一個指令。 Ansible 部署腳本
infra/playbooks/deploy-nemotron-super.yml、deploy-triaige-server.yml、deploy-cloudflared.yml與deploy-dashboard.yml先以--check --diff試跑。架構、GPU、Docker runtime 或可用記憶體(至少 100 GB)不符時,事前檢查會中止這次部署(infra/README.md)。 - 版本固定,可以重現。 vLLM 映像標籤、服務參數、PostgreSQL 映像 digest 與記憶體上限都寫在
infra/inventory/group_vars/servers.yml。換到另一台機器,只要在infra/inventory/host.yml加一台主機、在host_vars/放它的連線資料;借用的這台就是這樣加進來的。第一次啟動會下載約 75 GiB 的權重(infra/README.md)。後端映像在主機上原生建置,標籤是部署當時的 git revision。 - 連線。 模型端點沒有 API 金鑰,所以只開放給本機與 Tailscale 介面(防火牆擋不住 Docker 開放的 port,
infra/README.md記錄了這項測試)。後端只聽本機,從外部只經 Cloudflare Tunnel 連線;這台機器的 Tailscale Funnel 沒有取得公開的 DNS 記錄,所以改用 Tunnel(infra/inventory/host_vars/)。 - 知識庫匯入。 知識庫的匯出檔不進 git;部署時加
-e triaige_kb_import=true,playbook 把檔案複製到主機並在後端容器內匯入,可重複執行,輸出每個來源的段落數(infra/README.md〈知識庫〉)。 - 功能開關。 Spark 上開啟鑑別診斷與急救責任醫院分級,關閉案件備忘整理、Guardrails 自我檢查與路由分類器;同時模型呼叫數是 3;AI 整理、建議追問、護理師輸入擷取、對數機率評分與主訴翻譯用程式的預設值(開啟)(
infra/inventory/group_vars/servers.yml、server/src/triaige_server/config.py)。 - 維運。 後端把每一次模型請求寫成一行 JSON(任務、token 數、延遲、結果、輸入輸出摘錄;
TRIAIGE_LLM_CALL_LOG,3 個檔,每個最多 20 MB),維運儀表板(infra/dashboard/)讀它。公開的唯讀版在 https://triaige-ops.wayneh.tw:沒有測試面板,不顯示行程的指令列與案件編號,但會顯示 LLM 輸入輸出的摘錄(server/src/triaige_server/ai/call_log.py、infra/README.md)。 - Web。
web/在bun run check通過後,由 GitHub Actions 從demo分支部署到 Vercel(README.md)。/連哪個後端,由建置時的NEXT_PUBLIC_API_BASE_URL決定(web/src/api/config.ts)。
9. 已知限制與下一步
限制:
- repository 裡沒有語音辨識器(§1),評測的是文字路徑。
- 評測資料是美國急診的案例卡,加上模型寫的口述稿,沒有臨床人員審閱(§7)。
- TTAS 規則集是草稿,涵蓋第一層的生命徵象調節變數與疼痛;依主訴而定的判定標準還沒有資料來源(
server/src/triaige_server/engine/ttas.py)。每個級數仍由護理師決定。 - 階段 2 的 AI 功能(鑑別診斷、AI 整理、建議追問、護理師輸入擷取、急救責任醫院分級)沒有保留集評測,只有確定性檢查與單元測試(§7)。鑑別診斷要 1–3 分鐘,而且優先級最低:伺服器忙時會往後排,被取消就重跑,最長 600 秒;失敗時醫師看到明確的失敗原因與上一次的結果,不會有備援。
- 信心等級不會顯示給使用者。門檻已在保留集上校準過兩次(2026-10-17、2026-10-18),第一次保留集沒有達到精確度目標,第二次達標,但 τ 分不出差別,所以
enabled=false維持不變(eval/results/choice-confidence-2026-10-17/、choice-confidence-2026-10-18/summary.md)。 - Guardrails 自我檢查與路由分類器目前關閉,因為兩者都要在以秒計的延遲預算裡,多一次模型呼叫;案件備忘整理也關閉,等保留集通過再打開。
- 對外服務只有一台機器,沒有備援。同時模型呼叫上限在 Spark 上是 3(程式預設是 2):舊版 vLLM(v0.28.0)在三條同時呼叫時曾出現一次原因不明的 CUDA 錯誤;v0.31.0 的 soak 沒有再出現,但不能因此排除這個問題,而且 soak 是在第一台 Spark 上跑的,借用的這台沒有重跑(
docs/specs/2026-10-13-ai-routing/soak-results.md、server/src/triaige_server/llm/profiles/nemotron_3_super/__init__.py)。 - 紀錄裡的主訴保留英文。中文顯示採保守做法,像「up and down」這類片語,是已記錄的已知缺口(
eval/complaint_zh/README.md)。 - 知識庫的三個來源,兩個有著作權:Tintinalli's Emergency Medicine Manual(第 7 版)與臺北市到院前救護手冊。它們不進版本控制,但全文現在放在 Spark 的資料庫裡,供鑑別診斷搜尋;兩者的授權問題都還沒有解決(§4)。另有 16 份指引的 Creative Commons 授權是非商業或禁止改作。
- 提示詞與對數機率 fixture 是 Nemotron 3 Super 專用的(§5)。
- 知識庫共 3,714 段。Tintinalli 是 2012 年版,兒科章節沒有收;臺北市手冊是臺北市的作業程序,不是全國規範。
下一步,依優先順序:
- 請臨床人員審閱標準答案、TTAS 規則集與翻譯檢查,並替鑑別診斷與其他階段 2 功能建立標準答案(
docs/team-discussion-todo.md)。 - 處理知識庫的授權:向臺北市消防局取得書面同意,並把 Tintinalli 換成醫院自行取得授權的知識庫(
server/src/triaige_server/kb.py的介面可換實作)。 - 重新校準對數機率門檻,讓分級分得出差別,再顯示「高/需留意」等級(
docs/specs/2026-10-14-inline-review/plan.md§15,C3)。 - 依
docs/api/README.md§6 的規格,在平板的瀏覽器內做語音辨識。 - 階段 2 剩下的項目:是否加向量檢索、NeMo Evaluator、沙箱(
docs/specs/2026-10-13-backend/decision.md)。 - 依
docs/architecture/README.md§9 的步驟評估下一個模型;上一個候選(Nemotron 3.5 Super VL)沒有通過。
10. 想查細節時,去哪裡找?
| 問題 | 檔案 |
|---|---|
| 產品構想與 MVP 範圍 | docs/product/triaige.md |
| 系統分層、換模型的步驟 | docs/architecture/README.md(繁體中文) |
| 後端決策與設計 | docs/specs/2026-10-13-backend/decision.md、design.md |
| 欄位複查與信心設計 | docs/specs/2026-10-14-inline-review/plan.md |
| API 介面規格與資料規則 | docs/api/README.md、openapi.yaml、data-model.md |
| 鑑別診斷 agent 與知識庫檢索 | server/src/triaige_server/ai/nat_differential.py、kb.py、server/scripts/export_kb.py |
| 每一項評測,附事先宣告的通過標準 | eval/README.md |
| 部署與維運儀表板 | infra/README.md |