triAIge 技術導讀:架構、取捨與評審問答
模型為什麼不決定檢傷級數?為什麼不讓模型推理?Guardrails 為什麼關著?F1 0.95 用的是什麼資料?評審追問的多半是這類設計取捨。這份導讀用白話說明現在的系統怎麼組成、每個設計決定的理由與我們放棄的做法,最後整理關鍵數字與評審可能問的問題。
內容以 2026-10-21 為準:正式環境的後端是 commit 70f02968,網頁展示版是 768afcb5。每個數字都附上 repository 裡的出處。繁體中文口述稿由模型寫成,還沒有臨床人員審閱,所以這份文件不說「已通過臨床驗證」(eval/README.md〈誠實規則〉)。程式結構的細節在〈架構說明〉,給評審的正式說明在〈評審報告〉。
1. 整個系統怎麼運作?
救護員在平板的救護端網頁送出一句話,這句話送到醫院端一台 DGX Spark 上的 FastAPI 服務。服務先存下這句話、立刻回應,再在背景透過 NeMo Agent Toolkit 呼叫 vLLM 上的 Nemotron 3 Super,把這句話擷取成救護紀錄表的欄位。模型的回覆先通過一組確定性檢查,再依「人填的值優先」的規則寫進紀錄表,經 SSE 推回畫面,標成「待複查」。救護員送出紀錄後,急診檢傷護理師看到預先填好的檢傷表與規則算出的 TTAS 建議級數,由護理師決定級數;醫師看到的摘要也由規則從紀錄組出。
回答時,有三件事要先講清楚:
- 模型只做擷取與翻譯,臨床判斷由規則與人負責。 TTAS 建議由規則計算,級數由護理師送出,醫師摘要由規則組出,這三步都不呼叫模型。
- AI 填的值都要經過複查,人填的值不會被覆蓋。 AI 的建議直接顯示在欄位上,不用對話框打斷現場人員。
- 系統全部跑在一台 DGX Spark 上,模型可以替換,各層的邊界由測試強制。 推論不使用雲端模型;與模型有關的設定集中在一個
ModelProfile物件裡。
2. 系統由哪些部分組成?
flowchart LR
subgraph Front["前端(Vercel)"]
LIVE["/ 正式介面<br/>救護端、急診檢傷、醫師"]
MOCK["/internal 模擬版<br/>瀏覽器內的模擬後端"]
end
FUNNEL["Tailscale Funnel"]
subgraph Spark["醫院端 DGX Spark"]
API["FastAPI 服務<br/>api、ai、engine、task_rules、llm"]
NAT["NeMo Agent Toolkit<br/>(同一個行程)"]
VLLM[("vLLM<br/>Nemotron 3 Super NVFP4")]
PG[("PostgreSQL<br/>一個案件一列 JSONB")]
end
LIVE --> FUNNEL --> API
API --> NAT --> VLLM
API --> PG
API -- "SSE 推播" --> LIVE- 前端是 Next.js 16 的網頁,部署在 Vercel。同一組畫面掛在兩個路徑下:
/連真後端,/internal連瀏覽器內的模擬後端,沒有 Spark 時也能展示,醫師也在這裡的引導流程裡評述。 - 後端是單一 FastAPI 服務、單一行程,NeMo Agent Toolkit 內嵌在同一個行程裡,不另外起服務。程式分五層:
api/(路由與 SSE)、ai/(排程與 NAT 的銜接)、engine/(案件規則、TTAS)、task_rules/(任何模型都需要的檢查)、llm/(與特定模型有關的一切)。 - 模型由同一台 Spark 上的 vLLM 提供,後端用 OpenAI 相容的 HTTP 介面呼叫它。
- 資料庫是 PostgreSQL,一個案件存成一列 JSONB;
kb_passages表已經建好,留給階段 2 的檢索。 - 對外入口只有一個:經 Tailscale Funnel 連到 FastAPI,設計上每個請求都要帶存取碼。模型端點只開放給本機與 tailnet 介面。
3. 一句話從送出到畫面更新,經過哪些步驟?
sequenceDiagram participant W as 救護端網頁 participant A as FastAPI participant S as 排程與依序 participant M as Nemotron 3 Super participant C as 檢查與寫入規則 W->>A: 送出一句話(帶 clientMessageId) A-->>W: 201,先回應 A->>S: 排進 AI 工作 S->>M: 一次呼叫:這句話+前三句+紀錄表現況 M-->>S: 一行 JSON(附 token 機率) S->>C: 取出 JSON、確定性檢查、計算信心 C->>C: 依來源規則寫入(人填的值優先) A-->>W: SSE:record.updated、assistant.completed S->>M: 背景:主訴翻成繁中
- 前端依序送出。 送出的句子進入先進先出的佇列,一次送一則;前一則還沒送達,後面的就不會先送,所以 AI 讀到的對話順序不會顛倒。每則訊息帶
clientMessageId,重送時伺服器會排除重複。斷線時句子留在畫面上,恢復連線後自動送出(web/src/hooks/outbox-store.ts)。 - 伺服器先存檔再回應。
POST /cases/{id}/messages存下訊息就回 201,AI 在背景執行,所以畫面不必等模型(server/src/triaige_server/ai/service.py)。 - 確定性分流,不呼叫模型。 空白或只有標點的句子直接略過。說話者決定用哪一組提示詞、值上要標記誰說的,但不決定寫不寫:救護員的話用 R1(含生命徵象),病人與旁人的話用 R2。不論誰說的,擷取到的值都可以填進空欄位、標「待複查」,並記下說話者(救護員、病人、旁人,或辨識信心低於 0.6 的救護員),畫面上會標出「家屬說的」這類來源(
engine/routes.py、engine/record.py、docs/api/data-model.md§7)。 - 排程。 送給模型的請求有固定名額,正式環境同時 3 個。工作分優先級:即時的句子最先,說話者修正後的重跑其次,主訴翻譯這類背景工作最後;即時的句子等不到名額時,執行中的背景工作會被取消,之後重新排隊(
ai/scheduler.py)。 - 同一個案件依序處理。 R1 的提示詞要帶上前三句與它們在紀錄表上的內容,模型才知道「不對,是一百三」要更正哪一個值,所以同一個案件的 R1 句子要等前一句寫完才呼叫模型。不同案件之間、病人與旁人的話,可以同時處理(
ai/sequencer.py)。 - 呼叫模型一次。 每句話只呼叫一次模型,不經過智慧體(agent)的多輪迴圈:溫度設 0、關閉推理、最多輸出 300 個 token,並回傳每個 token 前五名候選的機率。輸出是一行精簡的 JSON,涵蓋年齡、性別、主訴、生命徵象、疼痛、病史、用藥與過敏等 12 個欄位(
llm/profiles/nemotron_3_super/)。 - 整理回覆,再嚴格檢查。 模型層先把回覆整理成一個 JSON 物件,任務層再嚴格檢查:未知的欄位、超出合理範圍的生命徵象、沒有原文依據的「無過敏」,都會被丟掉。選項欄位(性別、過敏狀態、病史)另外用 token 機率計算信心分數,分數會存下來,但目前不顯示(見 §4.9)。
- 依來源規則寫入。 空欄位直接寫入並標「待複查」,附上來源句與說話者。還沒有人複查的 AI 值,可以被看過相關句子的新判斷取代;人填的或已確認的值不會被覆蓋,AI 給的不同值以替代選項列在旁邊。人刪掉的 AI 值會被記住,AI 不會再寫回去(
engine/record.py、engine/retroactive.py)。 - 推播。 寫入的交易提交之後,才依序推播
record.updated、assistant.completed等事件,前端據此更新快取(server/src/triaige_server/bus.py、web/src/api/live.ts)。 - 背景翻譯。 英文主訴由最低優先級的背景工作翻成繁體中文,譯文並列顯示、標「AI 翻譯」,紀錄表本身一律存英文。譯文能不能顯示,由一組確定性檢查決定;主訴含數字或明確次數時不翻譯(
engine/complaint_translation.py、task_rules/complaint_guard.py)。
模型呼叫失敗時怎麼辦?每次呼叫的逾時是 15 秒,失敗就推播 assistant.failed,不重試。連續失敗 3 次,心跳訊息會帶出 aiAvailable=false,畫面顯示「AI 暫停」;之後只要成功一次就恢復。手動填表任何時候都能用。
紀錄送出之後的流程不再呼叫模型。急診看到預先填好的檢傷表,救護端沒人確認過的欄位標成「未複查」並附上來源。TTAS 引擎依規則算出建議級數,每一項理由都附上觸發的規則;需要人判斷的條件(例如「收縮壓低於 90 是否伴隨休克?」)會列出來交給護理師,引擎不替人決定。護理師送出的級數和建議不同時,必須填寫理由。醫師只看得到已完成檢傷的案件,摘要與危險訊號都由規則從紀錄組出;危險訊號就是最新一組救護量測裡,所有落在危急範圍的生命徵象(engine/ttas.py、engine/views.py、engine/vital_tint.py)。
4. 為什麼這樣設計?
4.1 為什麼模型只做擷取,不做臨床判斷?
產品的定位是:凡是有公開規則可循的臨床決定,都交給確定性規則,最後由人決定(docs/product/triaige.md §3.3)。TTAS 有公開的判定標準,生命徵象的異常界線也有 TTAS、NEWS2、PALS 等公開來源,所以這些都用規則計算,結果可以逐條查到依據,也不會因為模型換版而改變。
模型擅長把口語整理成結構化資料,這也是現場最花時間的工作,所以模型只負責這一段。這樣分工,模型出錯時最壞的結果是一個待複查的欄位填錯,檢傷級數不會因此算錯。
4.2 為什麼一句話只呼叫一次模型,不用智慧體迴圈,也不推理?
現場擷取的延遲預算以秒計。我們比較過推理強度:關閉推理時,擷取延遲中位數 4.2 秒、F1 0.956;完整推理時 24.9 秒、F1 0.954。在這個任務上,推理沒有提高準確度(eval/README.md〈推理強度〉,舊設定的數字)。智慧體迴圈也一樣,每多一輪,就多一次要花好幾秒的呼叫。
系統仍然有智慧體的行為,只是交給模型外的確定性程式負責:追蹤案件狀態與缺漏欄位;依說話者選擇提示詞,並標記每個值的來源;AI 的新值和人填的值不同時,列成替代值而不覆蓋;出現更正時,判斷要不要回頭重評舊值(必要時送第二次呼叫);依優先級排程。需要多步推理的功能,例如鑑別診斷與給護理師的追問建議,排在階段 2。
4.3 為什麼 AI 的值直接寫進欄位,而不跳出確認卡?
救護員一邊處置病人一邊用平板,任何彈出視窗都會打斷工作。所以 AI 的值直接顯示在要填的欄位上,標「待複查」;救護員要做的是複查,不必在卡片彈出的當下決定接受與否。缺漏的必填欄位與 TTAS 參考級數固定顯示在畫面上的一個區域,送出時還沒複查的欄位,也只在送出列上常駐提示,不用對話框擋住。
為了讓這樣做是安全的,寫入規則很保守:人填的值不會被 AI 覆蓋;AI 的值一律待複查,並標出是誰說的(病人、旁人說的也一樣);刪掉的 AI 值不會再被寫回去;每個欄位都保留來源句、說話者與變更歷程。
4.4 為什麼失敗不重試,也沒有備援模型?
我們認為默默降級比明確失敗更危險。如果 JSON 壞了就重試,或改用比較弱的備援模型,畫面上的值看起來一樣,品質卻可能不同,使用者也無從得知。所以 max_retries 一律是 0:失敗就推播 assistant.failed,連續失敗就顯示「AI 暫停」,手動填表照常可用。啟動時也採同樣的原則:設定了不認得的模型設定檔,或要求不能回傳 token 機率的模型計算信心,伺服器在啟動時就會停止。
4.5 為什麼安全規則寫成程式,而不寫進提示詞?
我們試過只靠提示詞擋下錯誤的「無過敏」:3 次整段口述中有 2 次,模型對一個從沒提到過敏的案例寫了「無」(eval/README.md〈R1 採用標準與重新判定〉)。錯誤的「無過敏」會關掉缺漏過敏的提醒,後果嚴重,所以這條檢查改用程式執行:來源句裡沒有提到過敏的字眼時,「無」與「未知」都會被丟掉。
一條檢查該放在哪一層?我們問的是:如果模型完美無缺,這條檢查還需要嗎?仍然需要的檢查(生命徵象範圍、過敏要有依據、翻譯檢查)放在任務層 task_rules/,只用標準函式庫,換模型也不必改;只是為了修補這個模型輸出習慣的檢查,才放在模型層 llm/(docs/architecture/README.md §7)。
4.6 為什麼要有模型適配層?
模型更新得很快,為某一個模型做的調整,不該分散在整個程式裡。現行模型的所有設定,包括提示詞、取樣參數、怎麼關閉推理、怎麼把回覆整理成 JSON、怎麼讀取 token 機率、逾時與同時請求數,都集中在 server/src/triaige_server/llm/ 的 ModelProfile 物件裡。server/tests/unit/test_architecture.py 強制這條邊界:llm/ 以外不能出現模型名稱、取樣參數與端點字串,下層也不能 import 上層,沒有例外清單。
Nemotron 3.5 Super VL 推出後,我們實際換過一次:只新增一個設定檔、改一個部署變數,就把它換上正式環境評估,再用同一份部署腳本(playbook)換回來,前後約 57 分鐘。評估結果比 3 Super 差,所以正式環境維持 3 Super(eval/results/nemotron-3-5-super-vl-2026-10-18/README.md)。
4.7 為什麼一個案件存成一列 JSONB,而且只有一個行程?
一個案件本身就是一份文件:紀錄表、對話、建議、稽核與檢傷都屬於同一個案件,幾乎每次讀寫都以案件為單位,拆成多張正規化的表,只會讓交易更複雜。每次寫入時,在交易內用 SELECT … FOR UPDATE 鎖住那一列,套用規則後寫回,提交之後才推播。資料庫的鎖在提交時就釋放了,為了讓推播順序與寫入順序一致,同一個案件的寫入另外在行程內排隊,所以後端只有一個行程(server/src/triaige_server/store.py)。
這是依一台 Spark、一家醫院的規模做的選擇。事件不保存、不補送;前端連上 SSE 之後再讀一次快照,所以連線建立後的事件不會遺漏。
4.8 為什麼前端要有兩套後端?
真後端要有 Spark 才能執行,但展示、設計審查與醫師評述不該依賴它。/internal 在瀏覽器內用 TypeScript 實作同一套案件規則,資料存在 IndexedDB。兩邊的規則必須一致,所以有一份共用的測試案例(server/tests/fixtures/mock_parity.json),逐筆比對後端與模擬後端的結果;生命徵象色塊、備忘、回溯更正等規則都包含在內。
4.9 為什麼很多功能做好了卻沒有開啟?
每一項 AI 功能上線前,我們先寫下採用標準與保留集,保留集只跑一次,沒通過就不開啟。下面幾項是沒通過或還沒準備好的,評審問到臨床風險時可以用來說明:
| 功能 | 狀態 | 原因 |
|---|---|---|
| Guardrails 自我檢查護欄 | 已接上,關閉 | 每次擷取要多一次模型呼叫,設計估計多 1–3 秒;安全檢查改由確定性程式負責 |
| 路由分類器(判斷一句話是不是病歷內容) | 關閉 | 召回率 0.981,沒達到 0.99 的目標,而且要多一次呼叫(eval/routing/README.md) |
| 選項欄位的信心等級 | 分數已計算並儲存,等級不顯示 | 校準後發現,模型幾乎對每個值都給很高的機率,包括少數錯誤的值,門檻分不出對錯(eval/results/choice-confidence-2026-10-18/summary.md) |
| AI 寫案件備忘 | 人可以寫備忘,AI 寫備忘的功能關閉 | 兩個版本都沒通過保留集:一版降低了擷取 F1 並增加延遲,另一版寫出太多不需要的備忘(eval/README.md〈M5 的結果〉、〈案件備忘 第 3 版〉) |
5. 用了哪些 <帳號> 技術,用到什麼程度?
| 元件 | 用途 | 狀態 |
|---|---|---|
| DGX Spark(GB10,128 GB 統一記憶體) | 醫院端唯一的主機,模型、後端與資料庫都在上面 | 執行中 |
| Nemotron 3 Super 120B-A12B NVFP4 | 流程中唯一的模型,負責欄位擷取與主訴翻譯 | 執行中 |
| vLLM v0.31.0 | 提供 OpenAI 相容 API;多 token 預測(MTP)的推測解碼一次 3 個 token、前綴快取、fp8 KV cache | 執行中 |
| NeMo Agent Toolkit 1.9.0 | 每一次模型呼叫都是一個 NAT 函式,在 FastAPI 行程內由 WorkflowBuilder 載入;五個 LLM 角色由模型設定檔組成 | 執行中 |
| NeMo Guardrails(透過 NAT 的 guardrails 中介層) | 中介層包在每個函式外面;自我檢查護欄已經寫好 | 已接上,護欄關閉 |
| Nemotron 3.5 Super VL | 換上正式環境評估 | 比 3 Super 差,沒有採用 |
| Nemotron 3.5 Lightning 30B-A3B | 評估能否當作較快的擷取模型 | 準確度較低,也不能和 Super 放在同一台機器上,沒有採用 |
| NeMo Evaluator、Nemotron embedding+pgvector、OpenShell | 階段 2 | 規劃中,資料表已預留 embedding 欄位 |
| NemoClaw | 調查後發現還在 alpha 階段,也沒有說明如何接上自行開發的智慧體 | 沒有整合 |
為什麼用 NVFP4?BF16 的 Super 約 240 GB,一台 Spark 放不下;NVFP4 的權重是 74.8 GiB,載入並配置 KV cache 後約佔 95 GiB,留給主機、後端與資料庫約 16 GiB(docs/product/slides-outline.md 第 10 頁、eval/README.md〈輸出格式與 vLLM 版本〉)。
單一請求的解碼速度約 31 tok/s,已經接近這台機器的上限:Spark 的記憶體頻寬約 273 GB/s,Super 每產生一個 token 要讀約 6 到 7 GB 權重,單一請求的上限約 40 tok/s。所以降低延遲的主要做法是讓模型少寫字:改成精簡的 JSON 輸出後,輸出 token 少約 40%、延遲少約 31%,F1 不變(eval/README.md〈服務參數實測〉)。
6. 前端怎麼組成?
| 項目 | 選擇 |
|---|---|
| 框架 | Next.js 16(App Router)、React 19 |
| 樣式與元件 | Tailwind CSS 4、Base UI |
| 資料存取 | TanStack Query 5;SSE 事件直接更新查詢快取 |
| API 型別 | 由 docs/api/openapi.yaml 產生,前後端共用同一份介面規格(42 個操作);推播事件定義在 asyncapi.yaml |
| 模擬後端 | 瀏覽器內的 TypeScript 規則引擎,資料存在 IndexedDB |
| 醫師評述 | 引導流程裡的評述存在 Neon(PostgreSQL),由 Drizzle 管理 |
| 部署 | GitHub Actions 跑完 bun run check 後部署到 Vercel |
現場畫面有兩條規則。首先,任何操作都不阻塞畫面,AI 的結果經推播逐步出現。其次,救護與急診的現場畫面不用浮動通知,提示放在畫面中固定佔位的提示列,不會蓋住任何按鈕。
7. 關鍵數字速查
| 數字 | 意思 | 出處 |
|---|---|---|
| F1 0.954/0.957 | 整段口述擷取 12 個欄位的宏平均 F1(種子 1 的 132 段/種子 2 的 133 段) | eval/README.md〈輸出格式與 vLLM 版本〉 |
| 幻覺欄位 4/1127、4/1147 | 同上兩個種子中,模型填了口述沒說的欄位數 | 同上 |
| 2.89 秒 | 整段口述擷取的延遲中位數,平均輸出 71 個 token | 同上 |
| F1 1.000、幻覺 0/401 | 透過真後端逐句處理 47 段保留集口述(163 句):F1 是 11 個結構化欄位的結果,幻覺以全部預測欄位計算 | eval/results/r1-context-fix-2026-10-14/ |
| 1.42 秒/2.08 秒 | 同上,每句延遲的中位數/p95 | 同上 |
| 3.89 秒 | 同時 3 個請求、壓力測試 30 分鐘的擷取 p95;CUDA 錯誤、重啟、逾時都是 0 | docs/specs/2026-10-13-ai-routing/soak-results.md |
| 3 | 正式環境同時送給模型的請求數(設定檔預設 2) | infra/inventory/group_vars/servers.yml |
| 4.2 秒對 24.9 秒 | 關閉推理與完整推理的擷取延遲,F1 0.956 對 0.954(舊的完整輸出格式;現行約 2.9 秒) | eval/README.md〈推理強度〉 |
| −31% 延遲、−40% token | 精簡 JSON 輸出與完整格式相比,F1 不變 | eval/README.md〈輸出格式與 vLLM 版本〉 |
| 0.909 → 1.000 | 帶上前文後,回溯更正的召回率(保留集 34 句中的 11 句回溯更正);沒有指名欄位的更正 3/9 → 8/9 | eval/results/r1-context-fix-2026-10-14/ |
| 6/15 → 0/15 | 救護員不是病歷內容的話(問病人、問 AI、閒聊)被寫成欄位的句數(保留集);其中問句 4/5 → 0/5 | eval/README.md〈後續:問句不是陳述〉 |
| 2/3 | 只靠提示詞時,對沒提到過敏的案例寫出「無過敏」的次數 | eval/README.md〈R1 採用標準與重新判定〉 |
| ×1.007 | 要求回傳 token 機率的延遲成本;同樣設定重跑的對照組是 ×1.004 | eval/results/choice-confidence-latency-2026-10-14/README.md |
| 17/163 | 溫度 0 時重跑同一批句子,輸出文字不同的句數 | 同上 |
| 31 tok/s | 單一請求的解碼速度;依頻寬推算的上限約 40 tok/s | eval/README.md〈服務參數實測〉 |
| 74.8 GiB/約 95 GiB | NVFP4 權重大小/載入並配置 KV cache 後的佔用 | docs/product/slides-outline.md、eval/README.md |
| 4.3 倍、F1 0.938 | Lightning 比 Super 快的倍數,以及它的 F1(Super 0.955) | eval/README.md〈服務參數實測〉 |
| 8.7% 對 1.6% | 3.5 Super VL 與 3 Super 的重複填入率 | eval/results/nemotron-3-5-super-vl-2026-10-18/README.md |
| 133 張 | 評測案例卡,來自 MIMIC-IV-ED demo 中 47 位病人的救護車到院紀錄 | eval/README.md〈目前產出〉 |
| 42 | API 操作數 | docs/api/openapi.yaml |
| 約 1,430/1,950/490 | 後端、網頁、評測的測試數(以 def test_、it(、test( 計算定義數) | server/tests/、web/src/、eval/ |
8. 評審問答
回答時注意三件事:不說「臨床驗證過」,只說「在模型寫的繁中口述上量過」;不把規劃中的功能說成已經完成;只引用 repository 裡有出處的數字。
場景與產品
問:這跟「語音轉文字之後交給 ChatGPT 填表」有什麼不同?
差別在模型外的規則。每個 AI 值都標成待複查,附上來源句與說話者(病人、旁人說的會特別標出);人填的值不會被覆蓋;沒有依據的「無過敏」會被丟掉;檢傷級數由規則建議、由護理師決定。另外,整套系統跑在醫院端的一台 DGX Spark 上,病人資料不會送到雲端模型。
依據:§3、§4.3、docs/api/data-model.md §2。
問:AI 填錯了,誰負責?
AI 的值是草稿,標成「待複查」,救護員確認後才算數;送到急診時,沒人確認過的欄位會標成「未複查」。檢傷級數只能由護理師送出,改動建議級數時要附上理由,每一次寫入、複查與送出都有稽核紀錄。責任仍然在人身上,和現在一樣;系統讓人看得到哪些值是 AI 填的、依據的是哪一句話。
依據:docs/api/data-model.md §2、§10。
問:救護員在現場真的有空複查嗎?
我們刻意不要求救護員當下就決定。AI 的值直接出現在欄位上,不跳出視窗;救護員可以在運送途中有空時一次複查,送出列上也會常駐顯示還有哪些欄位沒複查。現場畫面不打斷使用者,是我們定下的介面原則。
依據:§4.3。
問:為什麼不直接串接醫院的 HIS/EMR?
黑客松期間沒有真實的醫院系統可以串接,健保卡查詢與院內既往資料都用虛構病人代替。API 裡已經有查詢與確認身分的操作,導入時再換成院方的介面。
依據:docs/api/README.md §1.3。
智慧體
問:這算智慧體嗎?看起來只是一個擷取 API。
現場擷取刻意做成單次呼叫,因為延遲預算以秒計,而推理並沒有提高準確度。智慧體的行為在系統層:持續追蹤案件狀態與缺漏欄位;依說話者選擇提示詞,並標記每個值的來源;AI 的新值和人填的值不同時,列成替代值而不覆蓋;遇到更正時判斷要不要回頭重評舊值(必要時送第二次呼叫);依優先級排程即時與背景工作。需要多步推理的功能,例如鑑別診斷與給護理師的追問建議,屬於階段 2,目前不在執行中的系統裡。
依據:§4.2、engine/retroactive.py、ai/scheduler.py。
問:為什麼不讓模型多推理一點?
我們量過:完整推理時,擷取延遲中位數 24.9 秒;關閉推理時 4.2 秒,F1 是 0.954 對 0.956(舊的完整輸出格式;現行設定約 2.9 秒)。在這個任務上,開啟推理慢了約六倍,準確度沒有提高。
依據:eval/README.md〈推理強度〉。
問:簡報裡的鑑別診斷呢?
還沒做進執行中的系統。評分流程已經準備好,但要等醫師提供標準答案才能評測,所以目前沒有數字。醫學參考資料的檢索只做到評測階段:在臺北市到院前救護手冊上,45 個查詢的章節層級 recall@5 是 0.978,不過其中 8 個查詢的標準答案,是看過結果之後才更正的。
依據:eval/README.md〈鑑別診斷評測〉、eval/ems_protocol/README.md。
邊緣運算與 <帳號>
問:為什麼一定要在邊緣執行?雲端模型不是更強?
病人的個資、病史與醫院的病歷都在院內,醫院購買的臨床資料庫也多半限院內使用。模型放在資料所在的地方,資料就不必離開醫院。我們也量過,一台 Spark 跑得動 120B 等級的模型:透過真後端,每句擷取的延遲中位數是 1.42 秒。
依據:docs/product/triaige.md §3.2、本文 §7。
問:一台 Spark 能支援幾台救護車?
我們量的是同時 3 個請求、連續 30 分鐘:沒有崩潰、沒有逾時,擷取 p95 是 3.89 秒;同時 4 個請求時,p95 升到 4.70 秒。能支援幾台救護車,取決於每台車說話的頻率,我們沒有實測多台救護車同時口述的情境,所以不提供這個數字。
依據:docs/specs/2026-10-13-ai-routing/soak-results.md。
問:為什麼選 Super,不選更快的 Lightning?
Lightning 快約 4.3 倍,但 F1 是 0.938 對 0.955;口述只講舒張壓時,它有 9 筆把舒張壓留空。它也不能和 Super 同時放在一台 Spark 上,記憶體不夠。
依據:eval/README.md〈服務參數實測〉。
問:Nemotron 3.5 Super 推出了,為什麼不換?
我們換上正式環境評估過。它快約 23%,但用事先宣告的標準來看,整段口述的三項指標都沒有通過;重複填入率是 3 Super 的 5 倍(8.7% 對 1.6%),路由分類的召回率從 0.952 降到 0.857。依照我們的規則,在保留集上不比現行模型差才切換,所以維持 3 Super。
依據:eval/results/nemotron-3-5-super-vl-2026-10-18/README.md。
問:NeMo Agent Toolkit 在這裡做了什麼?只是多包一層嗎?
每一次模型呼叫都是一個 NAT 函式,由 NAT 的 WorkflowBuilder 在 FastAPI 行程內載入,LLM 的設定由模型設定檔組成;Guardrails 以 NAT 中介層的形式包在每個函式外面。我們沒有用 NAT 的智慧體迴圈,原因是 §4.2 說明的延遲。好處是護欄、之後的評測與檢索都接在同一個地方,不必各接一套。
依據:server/src/triaige_server/ai/workflow.yml、ai/extractor.py。
問:Guardrails 為什麼沒有開?
自我檢查護欄已經寫好、接上中介層,但每次擷取要多一次模型呼叫,設計估計多 1 到 3 秒。一直開著的安全檢查是確定性的程式:格式、範圍、過敏要有依據、翻譯檢查,這些不花模型的時間,也不會因為模型換版而改變。
依據:§4.9、infra/inventory/group_vars/servers.yml(triaige_self_check_output: false)。
問:NemoClaw、OpenShell 呢?
我們調查過 NemoClaw,它還在 alpha 階段,也沒有說明如何接上我們自己的智慧體,所以沒有整合。OpenShell 排在階段 2 試裝,構想是把智慧體放進沙箱,只允許連到資料庫與 vLLM。
依據:docs/specs/2026-10-13-backend/decision.md、research-<帳號>-stack.md。
模型與評測
問:F1 0.95 是怎麼量的?資料可信嗎?
案例卡來自公開的 MIMIC-IV-ED demo,是美國急診 47 位病人的 133 次救護車到院紀錄;繁中口述稿由模型依案例卡寫成,經過程式檢查,還沒有臨床人員審閱。所以這個數字說明的是這條流程從乾淨的繁中口述擷取欄位的能力,不代表臨床驗證,也不代表在吵雜語音或台灣案例上的表現。大多數結果只跑過一次,千分之幾的差異是雜訊。
依據:eval/README.md〈誠實規則〉、〈目前產出〉。
問:怎麼防止模型編造口述裡沒說的內容?
有三層。提示詞要求沒提到的欄位不寫;任務層丟掉未知欄位、超出範圍的值與沒有依據的「無過敏」;寫入規則讓每個 AI 值都標成待複查並附上來源句。整段口述的幻覺欄位是 1127 個中 4 個;透過真後端逐句處理的保留集,全部 401 個預測欄位中是 0 個。
依據:§3、§7。
問:病人說的話和救護員說的話怎麼區分?
每句話都帶說話者:救護員、病人或旁人。說話者決定用哪一組提示詞,也記在每個值上,畫面會標出「家屬說的」這類來源,救護員複查時就知道這個值是誰說的。不過,不論誰說的,值都一樣先填進空欄位、標成待複查,已複查或人填的值不會被覆蓋。目前文字輸入的說話者預設是救護員,可以在對話裡修改,改了之後那句話會依新的說話者重新處理;規劃中由語音端的說話者辨識帶入,信心低於 0.6 時標成「說話者不確定」。
依據:engine/routes.py、docs/api/README.md §6。
問:救護員說錯後更正,例如「不對,是一百三」,系統怎麼處理?
擷取時,提示詞會帶上前三句與它們在紀錄表上的內容,模型因此知道要更正哪一個值。保留集 34 句裡有 11 句回溯更正,召回率從 0.909 提高到 1.000;沒有指名欄位的更正,從 9 句對 3 句提高到 9 句對 8 句。如果舊值的依據不在這次看過的句子裡,系統會送第二次呼叫重評,不會直接覆蓋。
依據:eval/results/r1-context-fix-2026-10-14/、engine/retroactive.py。
問:「他有沒有藥物過敏?」這種問句會不會被寫成資料?
以前會,這是我們在後端測試中自己發現的問題。修改提示詞之後,保留集裡被抽出欄位的過敏問句,從 5 句中 4 句降到 0 句;範圍放寬到救護員所有不是病歷內容的話(問病人、問 AI、閒聊),被寫成欄位的從 15 句中 6 句降到 0 句,並用測試固定下來。
依據:eval/README.md〈後續:問句不是陳述〉。
問:為什麼不用 vLLM 的結構化輸出(structured output),以 JSON schema 強制格式?
我們試過,結果更差。不允許 null 時,模型只要寫出一個填不出值的欄位,就只能填數字,口述沒提到的生命徵象變成 0,F1 降到 0.884;允許 null 時 F1 是 0.912,體溫大量漏填。不加 schema 時解析失敗是 0 次,沒有非用不可的理由。
依據:eval/README.md〈輸出格式與 vLLM 版本〉。
問:信心分數呢?為什麼沒有顯示?
分數已經在計算,也存了下來:沿著模型實際寫出的 token,把每個決定點的機率相乘。要求回傳 token 機率幾乎沒有成本(×1.007,對照組 ×1.004)。但校準後發現,模型幾乎對每個值都給很高的機率,包括少數錯誤的值,門檻分不出對錯,所以「高/需留意」的等級不顯示;分不出對錯的信心等級,反而會誤導使用者。
依據:eval/results/choice-confidence-2026-10-18/summary.md、docs/specs/2026-10-14-inline-review/plan.md §4。
問:溫度設 0,為什麼同一句話的結果還會不一樣?
GPU 上的貪婪解碼並不完全確定。同一批 163 句重跑,有 17 句的輸出文字不同;壓力測試時,同一批訊息在單一請求下重送,欄位一致率也只有 97.8%。所以我們以 3 次的平均與範圍當作基準,不根據單次的小差異下結論。
依據:eval/results/choice-confidence-latency-2026-10-14/README.md、docs/specs/2026-10-13-ai-routing/soak-results.md。
安全與可靠度
問:錯誤的「無過敏」怎麼擋?
只靠提示詞擋不住,3 次整段口述中有 2 次寫出錯誤的「無」。所以現在改用程式規則:來源句沒有提到過敏的字眼時,「無過敏」與「未知」都會被丟掉;只提到非藥物過敏的句子,不會勾選「無過敏」。另外,在 24 句的過敏保留集上(只看模型輸出),錯誤的「無」是 0,但這個集合太小,不能算是驗證。
依據:§4.5、task_rules/record_checks.py。
問:模型故障了怎麼辦?
每次呼叫的逾時是 15 秒,失敗就明確告訴使用者,不重試,也不換備援模型。連續失敗 3 次,畫面顯示「AI 暫停」,手動填表照常可用;之後只要成功一次就恢復。
依據:§4.4。
問:資料安全怎麼做?
對外只有一個入口,設計上要帶存取碼(至少 20 個字元);60 秒內失敗 10 次會回 429;登入後拿到有期限的簽章 token(預設 12 小時);請求大小上限 256 KiB;日誌不記錄 query string。模型端點沒有 API 金鑰,所以只開放給本機與 tailnet 介面。API 只接收文字,展示用的病人都是虛構的。
依據:docs/specs/2026-10-13-backend/design.md〈公開上線的加固〉、infra/README.md。
問:主訴翻譯會不會翻錯?
紀錄表一律存英文,譯文只是並列顯示,標成「AI 翻譯」。一組確定性檢查會比對左右側、身體部位、數字與簡體字,沒通過就只顯示英文;含數字或明確次數的主訴不翻譯。在 69 個當時沒用來調整的新主訴上,63 個通過,左右側 18 個全部正確;這是 2026-10-13 那一版檢查的結果,之後的檢查更嚴,擋下的譯文更多。這組檢查由工程師草擬,還在等臨床人員審閱。
依據:eval/complaint_zh/README.md。
工程與部署
問:換一個模型要多久、要改哪裡?
程式只需要在 llm/ 新增一個設定檔,再加上 infra/ 裡那個模型的部署腳本。花時間的是評測:先宣告標準,再跑完保留集,比現行模型差就不換。3.5 Super VL 從換上到換回約 57 分鐘,其中冷啟動 12 分鐘。
依據:docs/architecture/README.md §9、本文 §4.6。
問:怎麼確定展示版和真後端的行為一致?
兩邊共用一份測試案例檔,後端測試逐筆比對 Python 引擎與 TypeScript 模擬後端的結果,規則不一致時測試就會失敗。
依據:server/tests/unit/test_engine_parity.py、test_drift.py。
問:救護車上斷網怎麼辦?
斷線時畫面會顯示提示,已輸入的句子與欄位修改都留在畫面上,恢復連線後依序自動送出,伺服器以 clientMessageId 排除重複。限制是:佇列存在網頁的記憶體裡,換頁還在,關掉分頁就沒了;斷線期間也沒有 AI,因為模型在醫院端。簡報提過「存在裝置上的離線佇列」,這部分還沒做。
依據:web/src/hooks/outbox-store.ts、web/src/screens/ems/CaseBanners.tsx。
問:語音辨識呢?
不在這個 repository 裡。API 已經接受語音訊息的欄位,規劃是在平板的瀏覽器內辨識,音訊不離開平板;展示版用文字操作,評測的也是文字路徑。救護端目前有的是反方向的功能:透過耳機用語音念出新的提醒。
依據:docs/api/README.md §6、web/src/screens/ems/announcer.ts。
問:TTAS 建議準不準?
我們沒有量。規則集是 TTAS-adult@105-draft,涵蓋第一層的生命徵象調節變數與疼痛;依主訴而定的判定標準還沒有資料來源。評測資料裡的檢傷標記是 AI 草擬、還沒審閱的,不能當作標準答案。級數最後由護理師決定。
依據:engine/ttas.py、eval/README.md。
問:怎麼確保程式品質?
後端約有 1,430 個測試、網頁約 1,950 個、評測約 490 個,bun run check 沒通過就不能部署。架構邊界、提示詞的內容、模型設定檔的值,都由測試固定下來;改動伺服器、介面規格或提示詞時要經過獨立審查,只有 P0 到 P2 的問題會阻擋部署。
依據:server/tests/、web/src/、.github/workflows/deploy-demo.yml。
9. 想深入了解時,去哪裡找?
| 想知道 | 看這裡 |
|---|---|
| 程式分層、每個關注點在哪個檔案、換模型的步驟 | 〈架構說明〉(docs/architecture/README.md) |
| 給評審的正式說明與全部評測結果 | 〈評審報告〉(docs/judges/README.zh-TW.md) |
| 每一項評測的採用標準與原始數字 | eval/README.md |
| 欄位複查與信心的設計 | docs/specs/2026-10-14-inline-review/plan.md |
| API 介面規格與資料規則 | docs/api/README.md、openapi.yaml、data-model.md |
| 部署 | infra/README.md |