triAIge 架構說明(給團隊成員)
救護車上的救護員說「男性,胸痛半小時,血壓一百六十、九十,心跳一百一」,幾秒後,救護紀錄表上的性別、主訴與生命徵象就填好了,每一格都標明是 AI 填入、等人複查;病人送到急診時,檢傷護理師打開同一個案件,看到的是預先填好、附上建議檢傷級數的評估表。triAIge 做的就是這件事。前端是救護端、急診端與醫師端的網頁;後端是一個 FastAPI 服務,在一台 DGX Spark 級的主機上用 Nemotron 3 Super 擷取欄位,並在背景整理摘要、追問與分級建議;醫師端還有一個會搜尋知識庫的鑑別診斷 agent。臨床判斷交給確定性規則與護理師。這份文件說明系統現在的組成:程式分成哪幾層、一句話經過哪些層、每一類規則放在哪裡,以及新增提示詞、新增檢查或換模型時要改哪裡。
1. 這份文件給誰、怎麼讀
讀者有兩種:開發者會用到全部章節;臨床組員只需要讀第 2 章(系統全貌)、第 4 章(一句話的處理過程)與第 12 章(已知限制),第 5 到第 10 章講程式內部結構與操作,可以跳過。
這份文件遵守 eval/README.md 的誠實規則:評測用的逐字稿是模型寫的草稿,案例卡來自 MIMIC,還在等醫師審閱,所以這裡不說「已通過臨床驗證」。
2. 系統全貌
flowchart LR
subgraph Browser["前端(iPad/瀏覽器)"]
WEB["/ 正式介面(真後端)"]
MOCK["/internal 模擬版(瀏覽器內 mock)"]
end
TUNNEL["Cloudflare Tunnel"]
subgraph Spark["DGX Spark"]
subgraph SRV["triaige-server(FastAPI,:8100)"]
API["api/"]
AI["ai/"]
ENG["engine/"]
TR["task_rules/"]
LLM["llm/"]
end
NAT["NeMo Agent Toolkit<br/>WorkflowBuilder + guardrails middleware<br/>鑑別診斷 agent"]
VLLM[("vLLM:Nemotron 3 Super<br/>:8000")]
PG[("PostgreSQL :5433<br/>cases JSONB、kb_passages")]
end
EVAL["eval/(離線評測)"]
WEB --> TUNNEL --> API
API --> AI
API --> ENG
AI --> NAT --> VLLM
SRV --> PG
EVAL -.-> VLLM- 前端有兩份:
/連真後端,/internal在瀏覽器內用web/src/mock/模擬後端。兩邊的案件規則必須一致,所以由server/tests/fixtures/mock_parity.json與server/tests/unit/test_engine_parity.py、test_drift.py檢查。網頁在 https://triaige.wayneh.tw 與 https://triaige-demo.vercel.app(infra/inventory/group_vars/servers.yml的 CORS 清單),後端在 https://triaige-api.wayneh.tw。 - 後端是單一 FastAPI 服務(
server/src/triaige_server/),NAT 內嵌在同一個行程裡,不另外起服務。 - 資料存在 PostgreSQL:案件以 JSONB 存放;
kb_passages表放鑑別診斷用的知識庫(3,714 段,PostgreSQL 全文檢索;server/src/triaige_server/migrations/001_init.sql、003_kb_search.sql,來源見第 5 章〈知識庫的來源與檢索〉)。護理師與醫師可以處理的醫院放在user_hospitals表(004_user_hospitals.sql)。 - 模型由同一台 Spark 上的 vLLM 提供,後端用 OpenAI 相容的 HTTP 介面呼叫它。
eval/離線呼叫同一台 vLLM,不經過後端。
決策背景見 docs/specs/2026-10-13-backend/decision.md 與 design.md,部署參數見 infra/inventory/group_vars/servers.yml。
3. 分層與依賴方向
哪些程式碼該知道「現在用的是哪個模型」?我們的答案是只有 llm/。其餘各層分成「任務本身的規則」與「執行任務的程式」,兩者都不能出現模型名稱、取樣參數,或處理模型輸出習慣的程式碼。
flowchart TB
subgraph Interface["介面層"]
API["api/(FastAPI 路由、SSE)"]
end
subgraph Application["應用層 ai/"]
SVC["service.py 編排<br/>scheduler、sequencer、route、gate、recorded、mapping、followups、nurse_notes、call_log"]
RT["NAT 執行期黏合<br/>extractor.py、nat_*.py、kb_binding.py、workflow.yml<br/>(workflow.yml 只有 functions 與 guardrails)"]
end
subgraph Task["任務層(與模型無關)"]
ENGN["engine/(案件規則,與 web mock 對齊)"]
TRN["task_rules/(只用標準函式庫)<br/>詞彙、record/statement/route 檢查、主訴翻譯檢查、摘要/鑑別診斷/分級/追問/護理師輸入/備忘檢查"
end
subgraph Model["模型適配層 llm/(只用標準函式庫)"]
PROF["profiles/nemotron_3_super/<br/>ModelProfile"]
PS["prompt_sets/ems_compact/<br/>record、statement、classify、translate、consolidate、hospital_level、summary、followup、nurse_note、differential、self_check"]
ADP["output.py、wire.py、choice_scoring.py"]
end
KBN["kb.py 知識庫檢索(PostgreSQL 全文檢索)"]
INFRA["infra/ 部署手冊與 group_vars"]
VLLM[("vLLM")]
API --> SVC
API --> ENGN
SVC --> ENGN
SVC --> TRN
SVC --> RT
SVC --> PROF
RT --> PROF
RT -- "OpenAI 相容 HTTP" --> VLLM
RT -- "鑑別診斷 agent 搜尋" --> KBN
ENGN --> TRN
PROF --> PS
PROF --> ADP
PS --> TRN
ADP --> TRN
INFRA -. "served name、port、profile id" .-> RT| 層 | 套件 | 負責什麼 | 可以 import |
|---|---|---|---|
| 介面 | api/ | HTTP 路由、SSE、驗證 | ai、engine |
| 應用 | ai/ | 排程、編排、NAT 黏合、寫入閘門、建議追問、護理師輸入擷取、鑑別診斷 agent、模型呼叫紀錄 | engine、task_rules、llm、kb |
| 知識庫 | kb.py、kb_import.py | KnowledgeBase.search 介面與 PostgreSQL 實作(全文檢索);匯入知識庫段落 | 標準函式庫與 psycopg |
| 任務 | engine/ | 案件規則、寫入來源規則、與網頁模擬後端(mock)對齊的邏輯 | task_rules |
| 任務 | task_rules/ | 「這個 AI 任務的合格答案長什麼樣」 | 只有標準函式庫 |
| 模型 | llm/ | 提示詞、取樣參數、輸出修補、對數機率(logprob)讀取、信心評分 | task_rules 與標準函式庫 |
這些依賴方向由 server/tests/unit/test_architecture.py 強制,沒有例外清單:
engine不能 importai或llm。task_rules不能 importtriaige_server內的其他套件,也不能用第三方套件。llm只能 import 自己與task_rules,也只能用標準函式庫,所以不能引入 NAT、langchain、fastapi。llm/以外的server/src不能出現模型字面值(nemotron、<主機名稱>、enable_thinking、chat_template_kwargs、top_logprobs、max_tokens、/v1),也不能有取出{…}的修補程式;ai/裡不能有提示詞檔或提示詞文字。
每一條掃描都另外用刻意放入的違規範例測試過,確認它確實抓得到。
4. 一句話的處理過程
救護人員說了一句話,從送出到畫面更新,中間經過哪些步驟?
sequenceDiagram
participant U as 前端
participant A as api/(介面)
participant S as ai/ 排程與編排(應用)
participant N as NAT(應用)
participant V as vLLM
participant L as llm/(模型)
participant T as task_rules/(任務)
participant E as engine/(任務)
U->>A: POST /cases/{id}/messages
A->>E: 建立訊息(寫入來源規則)
A-->>U: 201(先回,AI 在背景跑)
A-->>U: SSE message.created、assistant.started
A->>S: schedule(case, message)
S->>S: scheduler 依 P0/P1/P2/閒置排隊,空位數是 `settings.ai_concurrency`,預設取自 profile 的 max_streams(2),Spark 上的 inventory 設 3
S->>S: sequencer 讓同一案件的 R1 句子依序處理
S->>N: 呼叫擷取
N->>V: 提示詞與取樣參數來自 profile
V-->>N: 回覆(含 logprobs)
N-->>S: 回覆文字
S->>L: profile.reply_text 修補輸出
S->>T: task_rules 檢查(範圍、詞彙、過敏接地)
S->>L: choice_scoring 評分選項欄位
S->>E: 寫入 record(人工或已確認的值優先)
A-->>U: SSE record.updated、assistant.completed
S->>S: P2 背景:主訴翻譯圖中每一步都標了所屬的層。要注意修補與檢查的順序:先用模型層的 reply_text 把回覆整理成一個 JSON 物件,再交給任務層做嚴格檢查;評分與稽核紀錄則用沒有修補過的原始回覆(raw)。整個後端只有 llm/output.py 的 json_span 會取出 {…},其他地方再寫一份,會被架構測試擋下。鑑別診斷的 reply_text 之後還有一道格式修訂:repair_reply 只補 sources 陣列裡事實編號少一個的引號,其他不動。
背景工作與優先級
除了上面的即時句子,還有幾種背景工作。每一種都經同一個排程器(ai/scheduler.py),總空位是 settings.ai_concurrency;P2 同時最多佔 1 個空位,沒有 P0、P1 在等才啟動,P0 等不到空位時執行中的 P2 被取消(連線中斷,vLLM 會 abort)、之後重排。「閒置」只在沒有任何工作在跑、也沒有工作在等時才啟動,任何 P0、P1、P2 工作一到就取消它、排回隊伍;被取消 3 次,或等了設定的時間(鑑別診斷預設 60 秒,備忘整理 180 秒),就升成 P2。每一種在設定檔裡都有自己的取樣參數(第 6 章),輸出都先過 task_rules/ 的檢查才寫進案件。
| 任務 | 優先級 | 觸發 | 輸出檢查 | 開關(程式預設/Spark 上) |
|---|---|---|---|---|
| 即時句子的分類、擷取(R1 救護員、R2 病患與旁人) | P0 | 每則救護端訊息 | record_checks.py、statement_checks.py、route_checks.py | 擷取固定開;路由分類器 TRIAIGE_ROUTE_CLASSIFIER 關/關 |
| 修正說話者的重新擷取、第二輪重評 | P1 | 修正說話者;這一句要改的未複查欄位,現值的證據句不在這次判斷看過的句子裡 | 同上 | 同上 |
| 護理師輸入擷取 | P1 | 護理師在檢傷畫面送出一段話 | nurse_note_checks.py | TRIAIGE_NURSE_EXTRACTION 開/開 |
| 主訴翻譯 | P2 | R1 寫入英文主訴之後 | complaint_guard.py | TRIAIGE_COMPLAINT_TRANSLATION 開/開 |
| 建議追問 | P2 | 救護端對話停 8 秒;護理師端送出 | followup_checks.py | TRIAIGE_FOLLOWUP_SUGGESTIONS 開/開 |
| 急救責任醫院分級建議 | P2 | 紀錄表欄位變動後 2 秒 | hospital_level_checks.py,臨床下限在 engine/hospital_level.py | TRIAIGE_HOSPITAL_LEVEL 關/開 |
| 醫師交接摘要的 AI 整理 | P2 | 救護端送出、檢傷送出 | summary_checks.py | TRIAIGE_AI_SUMMARY 開/開 |
| 案件備忘整理 | 閒置 | 對話停 20 秒或送出 | memo_checks.py | TRIAIGE_MEMO_CONSOLIDATE 關/關 |
| 鑑別診斷 agent | 閒置 | 案件送出(再送出也算)、檢傷送出 | differential_checks.py | TRIAIGE_DIFFERENTIALS 關/開 |
鑑別診斷與其他背景工作不同的地方:它是一個會呼叫工具的多步驟迴圈(最多搜尋 4 次,ai/nat_differential.py),不是單次呼叫;模型開推理。送出的交易裡先把狀態寫成 pending(mark_differentials),畫面在送出的那一刻就看得到新的一輪;每搜尋一次,步驟寫進 state["differentials"] 並經 SSE 事件 differentials.updated 只推給醫師(案件完成檢傷之後)。結果一律先過 check_differentials:事實編號要對得到案件欄位、段落 id 要是這一次搜尋回傳過的、不含機率或信心分數;任一份清單沒有合格項目就整次失敗,寫成 failed,不重試、沒有備援模型。一次執行的上限是設定檔的 differential_timeout_s(600 秒),實際約 1–3 分鐘。伺服器重新啟動時,上一個行程留下的 pending 會照實結算成失敗(fail_orphaned)。
5. 各關注點在哪裡
| 關注點 | 位置 | 層 |
|---|---|---|
| 系統提示詞 | llm/prompt_sets/ems_compact/(record.py、statement.py、classify.py、translate.py、consolidate.py、hospital_level.py、self_check.py),由 PromptSet 彙整 | 模型 |
取樣參數(max_tokens、temperature) | llm/profiles/nemotron_3_super/__init__.py 的 sampling;llm/nat_config.py 的 llm_entries 組成 NAT 的 LLM(擷取、路由、翻譯、備忘整理、摘要、追問、分級建議,加上開 Guardrails 自我檢查時的 rail、開鑑別診斷時的 differential_llm),ai/extractor.py 的 build_config_dict 呼叫它;每個 LLM 的 num_retries 都是 1 | 模型 |
| 關閉推理的開關 | 同一個設定檔(profile)裡 Sampling.extra_body | 模型 |
| 取出 JSON | llm/output.py(json_span、strip_think_then_json_span),由 ModelProfile.reply_text 選用;嚴格解析在 task_rules/record_checks.py 的 parse_json_object | 模型/任務 |
| 對數機率讀取 | llm/wire.py 的 reply_logprobs | 模型 |
| 選項信心評分 | llm/choice_scoring.py | 模型 |
| 信心門檻(τ、cuts) | llm/profiles/<id>/confidence_thresholds.json,Confidence.thresholds 指向它。Super 的已校準兩次(C2,2026-10-17、2026-10-18),但 τ 分不出差別,enabled=false;3.5 Super VL 的 τ 都是 null。見 eval/results/choice-confidence-2026-10-18/summary.md、docs/specs/2026-10-14-inline-review/plan.md §4.4 | 模型 |
| 生命徵象合理範圍、過敏「無」要有原文依據 | task_rules/record_checks.py | 任務 |
| 病患與旁人陳述的檢查、路由標籤與問題模式的檢查 | task_rules/statement_checks.py、task_rules/route_checks.py;欄位詞彙在 task_rules/vocabulary.py | 任務 |
| 主訴翻譯檢查 | task_rules/complaint_guard.py、task_rules/complaint_words.json | 任務 |
| 建議追問(救護員、護理師還該問什麼)的提示詞與檢查 | llm/prompt_sets/ems_compact/followup.py;回覆形狀與問句規則(繁體中文問句、不含治療與診斷字眼)在 task_rules/followup_checks.py | 模型/任務 |
| 建議追問的案件規則(欄位已填不問、去重、忽略過不再回來、每頻道最多 3 題開著、舊題結案) | engine/followups.py;時機(對話停下來、送出)與 P2 排程在 ai/followups.py | 任務/應用 |
| 護理師輸入擷取(檢傷畫面打的一段話 → 檢傷欄位提案)的提示詞與檢查 | llm/prompt_sets/ems_compact/nurse_note.py;白名單欄位、值的範圍、數值要在原文、無過敏要有依據在 task_rules/nurse_note_checks.py | 模型/任務 |
| 護理師輸入擷取的案件規則(提案掛在護理師那則訊息、不蓋掉護理師已填的值、套用時再檢查一次) | engine/nurse_notes.py、engine/triage.py 的 apply_proposals;呼叫在 ai/nurse_notes.py(P1) | 任務/應用 |
| 寫入規則(人工值優先、supersedes、拒絕記憶) | engine/record.py、field_rules.py、review.py | 任務 |
| 醫師交接摘要的 AI 整理:提示詞與檢查、狀態 | llm/prompt_sets/ems_compact/summary.py(取樣 sampling["summary"],逾時 60 秒);來源存在且有內容、數字出自引用欄位、不含診斷與處置用語、最多 6 句每句 160 字在 task_rules/summary_checks.py;狀態(aiSummary,ready 或 failed)與稽核在 engine/ai_summary.py;觸發與排程(P2)在 ai/service.py 的 _summarize | 模型/任務/應用 |
| 模型呼叫紀錄(給營運儀表板) | ai/call_log.py:每一次模型請求(含被擠掉後重跑、agent 的每一輪)一行 JSON,TRIAIGE_LLM_CALL_LOG 指到檔案才記;背景執行緒寫入,20 MB × 3 個檔 | 應用 |
| 知識庫檢索 | kb.py(KnowledgeBase.search、PgKnowledgeBase)、ai/kb_binding.py(NAT 設定只能放可序列化的值,所以放一把鑰匙,檢索器登記在這裡)、kb_import.py(匯入)、server/scripts/export_kb.py(匯出);來源見下一節 | 應用 |
| 急救責任醫院分級建議:輸入欄位、臨床下限(到院前檢傷 1–2 級、危急、疑似 STEMI 不建議一般)、寫入規則 | engine/hospital_level.py(mock 在 web/src/mock/engine/hospital-level.ts,mock_parity.json 的 hospitalLevel 比對);回覆形狀與引用已填欄位的檢查在 task_rules/hospital_level_checks.py | 任務 |
| TTAS | engine/ttas.py、engine/ems_ttas.py | 任務 |
| 稽核身分(AI 名稱與版本) | engine/state.py 的預設值是中性的 unset;CaseStore 必須傳入 ai_actor(store.py),app.py 依 settings.llm_model 與提示詞版本組出 | 應用 |
| 逾時與併發 | config.py:預設值取自 profile.serving,環境變數仍可覆蓋 | 模型(值)/應用(機制) |
| Guardrails | ai/workflow.yml 只放 functions、middleware、workflow;政策由 ai/extractor.py 依旗標補上 | 應用 |
| 部署手冊 | infra/playbooks/deploy-nemotron-super.yml。triaige_model_profile、vllm_served_names、llm_port 在 infra/inventory/group_vars/servers.yml,後端與 vLLM 的部署腳本(playbook)共用;vllm_stack 選 vLLM 服務哪一個模型 | 基礎設施 |
| 醫師端鑑別診斷(agent) | 提示詞、搜尋工具定義與工具結果格式:llm/prompt_sets/ems_compact/differential.py;取樣(開推理)與逾時:設定檔的 differential、differential_timeout_s;agent 迴圈(NAT 串流函式 triaige_differential):ai/nat_differential.py;知識庫檢索(可換實作):kb.py;案件事實與狀態:engine/differential_facts.py、engine/differentials.py;輸出檢查:task_rules/differential_checks.py;觸發與排程(IDLE,送出時與檢傷送出後各一次,連續要求合併):ai/service.py 的 differentials_requested | 模型/應用/任務 |
| 評測 | eval/scripts/model_profile.py 提供 --profile,各正式任務的腳本都經由它讀取模型與取樣參數 | 評測 |
知識庫的來源與檢索
鑑別診斷 agent 搜尋的是 PostgreSQL 的 kb_passages,共 3,714 段,來自三個來源。每一段有 id、來源(tintinalli、guidelines、ems_protocol)、語言、標題、出處字串(citation)與內文;畫面與輸出只用出處字串,不用內文。
來源(source) | 版本與出版 | 收錄範圍 | 段落數 | 使用依據 |
|---|---|---|---|---|
tintinalli:Tintinalli's Emergency Medicine Manual | 第 7 版,McGraw-Hill,2012 | 22 篇 188 章中的 167 章;第 9 篇兒科(21 章、255 段)預設不匯出(export_kb.py --include-pediatric 才收),因為示範的病人都是成人 | 1,745 | 示範用。版權頁不允許未經同意存入檢索系統,授權未解決;只顯示來源、章節、頁碼,不顯示原文;全文與索引放 private/。正式使用要換成醫院自行取得授權的知識庫,因為 agent 只依賴 KnowledgeBase.search,換實作不必動 agent 與提示詞 |
guidelines:開放授權的臨床指引與共識(PMC) | 31 份,期刊年份 2013–2025 | 只收 PMC 全文 JATS <license> 是 Creative Commons 的指引,非 CC 的拒收(eval/scripts/guidelines_rag.py fetch)。授權:CC BY 15 份、CC BY-NC 11 份、CC BY-NC-ND 5 份,所以 16 份只能非商業使用、不得改作後散布 | 1,688 | 全文在 private/guidelines/,repo 只放候選來源清單 eval/guidelines/sources.json;入選的 31 份(PMCID、簡稱、授權)在 private/guidelines/fetched.json。出處格式「簡稱(期刊 年份,PMCID,授權),§ 章節」 |
ems_protocol:臺北市政府緊急救護技術員到院前緊急救護作業程序手冊 | 第六版(封面 115 年 4 月),臺北市政府消防局醫療顧問委員會總編輯,臺北市政府消防局出版,PDF 140 頁 | 全書(通用程序 G、非外傷與外傷各程序 M、T、特殊情境 S、技術 A、附錄與前言),依標題與編號步驟切段 | 281 | 臺北市的作業程序,不是全國規範。版權屬臺北市政府消防局,「欲參採或引用請事先書面申請」,沒有公開授權,也還沒有取得書面同意,授權問題未解決(docs/team-discussion-todo.md A16)。使用者 2026-10-20 決定,使用手冊裡的切點數字(呼吸 ≥ 30、血糖 < 60 或 ≥ 500)為生命徵象標色不算引用(docs/references/vital-tint-thresholds.md) |
31 份指引依主題:胸痛 3(ANMCO-SIMEU 2016、Brazilian Chest Pain ED 2025、SIRM-SIC 2025);呼吸、給氧與肺栓塞 5(GOLD 2023、BTS Oxygen 2017、TSANZ Oxygen 2015、BTS Outpatient PE 2018、PERT Consortium PE 2019);心衰竭 1(KSHF 2023);敗血症與感染 2(J-SSCG 2020、SEOM Febrile Neutropenia 2018);神經 3(ED Delirium GRADE 2026、EUSEM Syncope 2024、Acute Vertigo ED Consensus 2024);消化與腹部 9(WSES 腸繫膜缺血/消化性潰瘍/胰臟炎/膽囊炎/高齡膽囊炎、ESGE 兩份出血、Asia-Pacific 上消化道出血、BSG 腹水);內分泌與代謝 4(高血糖危象共識 2024、JBDS HHS 2023、ESE-ES 腎上腺功能不全 2024、ED 高血鉀共識 2021);創傷 4(歐洲創傷出血 2023、WSES 高齡創傷 2023、WSES-AAST 胸部創傷 2025、Scandinavian 頭部外傷 2013)。
建立與匯入的流程:先在開發機用 eval/scripts/tintinalli_rag.py build、guidelines_rag.py build、ems_protocol_rag.py build 各建一份 SQLite FTS5 索引,再由 server/scripts/export_kb.py 合成 private/kb/kb_passages.jsonl(不進 git,三個來源的 id 不能重複),部署時加 -e triaige_kb_import=true,由 python -m triaige_server.kb_import 在後端容器內匯入(可重複執行:檔案裡有的來源先整批刪除再寫入)。
檢索是 PostgreSQL 全文檢索,沒有向量:英文查詢用 tsv(english 設定,標題權重 A、內文 B,查詢字是 OR,五個字母以上的字當前綴),查詢字串含中文字就用 tsv_zh(中文每個連續字串切成相鄰兩字,simple 設定,匯入與查詢用同一個 cjk_terms),依 ts_rank_cd 排序,每次回傳 1–8 段,可限定來源(kb.py、migrations/003_kb_search.sql)。資料庫映像是 pgvector 版,但沒有建立擴充套件,embedding 欄位是空的。線上的檢索品質沒有量過;eval 裡量過的是臺北市手冊的 SQLite 索引(章節層級 recall@5 0.978、MRR 0.924,8 題標準答案是看過結果才更正的,eval/ems_protocol/README.md),那是另一份實作。
6. 模型適配層(llm/)
ModelProfile 是這條邊界的核心:後端要服務一個模型時需要知道的事,都收在這個物件裡。它是不可變的(frozen),建立時就檢查八個取樣角色是否齊全、上限是否為正數。
classDiagram
class ModelProfile {
id
display_name
reply_text(raw) str
evidence
}
class Serving {
api
served_name
default_base_url
timeout_s
max_streams
recipe
}
class Sampling {
max_tokens
temperature
extra_body
}
class Confidence {
top_logprobs
thresholds
fixtures
}
class PromptSet {
id
record
statement
classify
translate
consolidate
hospital_level
summary
followup
nurse_note
differential
self_check
}
ModelProfile --> Serving
ModelProfile --> "8" Sampling : extraction、route、translate、rail、memo、summary、followup、level
ModelProfile --> Sampling : differential(開推理,選填)
ModelProfile --> Confidence
ModelProfile --> PromptSet應用層有三條固定規則,不由設定檔決定:每個 LLM 的 num_retries 一律是 1,也就是總共只嘗試一次(NAT 的 LLM client 預設對 429 與 5xx 自動重試,總共 5 次;num_retries 不能是 0,清空 retry_on_status_codes 也不夠,所以降到 1,llm/nat_config.py),失敗就送出 assistant.failed;沒有備援模型;請求逾時與 asyncio.timeout 用同一個 settings.llm_timeout_s(摘要與鑑別診斷各有自己的較長逾時)。這三條都來自「失敗要明確,不默默降級」的原則。
現行設定檔:nemotron_3_super
| 欄位 | 值 | 依據 |
|---|---|---|
| 模型 | <帳號> Nemotron 3 Super 120B-A12B NVFP4,vLLM v0.31.0,DGX Spark 級主機 | infra/inventory/group_vars/servers.yml |
| 服務名稱(served name) | nemotron-3-super | 設定檔的 serving |
| base URL | http://<主機名稱>:8000/v1 | 同上 |
| 逾時 | 15.0 秒(約 3 秒的 no-think 延遲留出餘裕) | 同上 |
| 同時串流數 | 設定檔預設 2(舊版 vLLM v0.28.0 的第 3 條串流曾觸發 CUDA 錯誤);Spark 上 inventory 設 3(triaige_ai_concurrency),依 2026-10-19 在第一台 Spark 上的 soak:3 條沒有崩潰、逾時或 preemption,擷取 p95 3.89 秒 | 設定檔的 max_streams、docs/specs/2026-10-13-ai-routing/soak-results.md |
| 取樣 | 擷取 300、路由 48、翻譯 96、護欄 8、備忘整理 600、分級建議 160、摘要 900(逾時 60 秒)、追問 400 個 token;temperature 0.0;enable_thinking=false。鑑別診斷自成一組:上限 8192 個 token(每次 LLM 呼叫,含推理)、enable_thinking=true 加 low_effort、一次執行 600 秒 | llm/profiles/nemotron_3_super/__init__.py |
| 輸出修補 | json_span(取第一個 { 到最後一個 }) | llm/output.py 的 json_span |
| 對數機率 | top_logprobs=5 | 設定檔的 Confidence.top_logprobs |
| 信心門檻 | 已校準(c2-2026-10-18-compact-2026-10-14-context-2,τ 0.48846,likelihoodCuts [0.5, 0.02]),但 enabled=false:τ 把校準集的所有值都分成「高」,分不出差別 | llm/profiles/nemotron_3_super/confidence_thresholds.json、eval/results/choice-confidence-2026-10-18/summary.md |
| 證據 | eval/README.md〈服務參數實測〉、〈R1 採用標準〉、〈X1 審查修正後的重新驗收〉;eval/results/serving-2026-10-12/、yardstick-2026-10-13/、r1-context-fix-2026-10-14/、choice-confidence-latency-2026-10-14/、choice-confidence-2026-10-17/;eval/complaint_zh/ | 設定檔的 evidence |
這些值由 server/tests/unit/test_model_profile.py 固定,設定檔裡列出的每個路徑也要存在。
候選設定檔:nemotron_3_5_super_vl(2026-10-18 評估過,沒有採用)
Nemotron 3.5 Super VL NVFP4,只用文字輸入。為了讓評估只比較模型本身,它的服務名稱是 nemotron-3.5-super-vl,取樣、token 上限、提示詞集、關閉推理的開關、json_span、top_logprobs=5、逾時與 2 條串流都和 Super 相同,由 test_model_profile.py 固定;它沒有 differential 取樣,所以和這個設定檔一起開 TRIAIGE_DIFFERENTIALS 會在啟動時失敗。信心門檻檔沒有校準(enabled=false,每個欄位的 tau 都是 null)。
評估的結果是比 Super 差:事先宣告的標準上,11 個結構化欄位 F1、主訴 F1、幻覺欄位三項都沒過;路由分類器的 record 召回從 0.952 掉到 0.857;逐句重播的重複填入率是 Super 的 5 倍(8.7% 對 1.6%)。它比較快(整段口述延遲中位數少 23%)。依使用者的授權條件(保留集不比 Super 差才切換),正式環境維持 Super,triaige_model_profile 沒有改(eval/results/nemotron-3-5-super-vl-2026-10-18/README.md)。這個設定檔留在程式裡,是下一次換模型時的範例;vLLM 啟動參數的出處、切換與回滾見 infra/README.md 的「評估 Nemotron 3.5 Super VL」。
第二個設定檔要填什麼
下面是示意,不是會上線的程式碼:
PROFILE = ModelProfile(
id="<vendor_model_size_quant>",
display_name="…, vLLM vX, DGX Spark",
serving=Serving(api="openai_chat", served_name="<served-name>",
default_base_url="http://<主機名稱>:8000/v1",
timeout_s=<實測 p99 × 餘裕>, max_streams=<實測穩定串流數>,
recipe="infra/playbooks/deploy-<model>.yml"),
prompts=ems_compact.PROMPT_SET, # 先沿用;有保留集證據才分岔
sampling={ "extraction": Sampling(max_tokens=..., extra_body={...}), ... }, # 八個角色都要有
differential=Sampling(max_tokens=..., extra_body={...}), # 選填;要開鑑別診斷才需要
reply_text=output.strip_think_then_json_span, # 模型會輸出推理區塊時才需要
confidence=Confidence(top_logprobs=5 或 None, thresholds=None,
fixtures="server/tests/fixtures/logprobs/<id>"),
evidence=("eval/results/<date>-<id>-…",),
)
- 關閉推理的方式因模型而異:
chat_template_kwargs、reasoning_effort欄位,或根本沒有開關,所以extra_body不限格式。 - 如果伺服器回不了
top_logprobs,填None;這時設定TRIAIGE_CHOICE_CONFIDENCE=true會在啟動時直接失敗,不會默默產生沒有信心值的結果。 sampling要涵蓋八個角色(TASK_ROLES:extraction、route、translate、rail、memo、summary、followup、level),缺一個或多一個都在建立時失敗;differential取樣選填,沒有它又開TRIAIGE_DIFFERENTIALS,啟動時直接失敗。- 啟動時遇到不認得的設定檔 id 也會失敗,錯誤訊息會列出認得的 id(
llm/registry.py)。
提示詞集與稽核版本
提示詞以提示詞集(prompt set)為單位,設定檔以 id 引用(ems_compact)。多個設定檔可以共用同一個提示詞集;只有保留集的證據顯示是模型讀錯時,才另外分出新的提示詞集,並提高 PROMPT_VERSION。稽核紀錄裡的 AI 身分是 name = 服務名稱、version = 提示詞版本,設定檔 id 只寫進啟動日誌與評測結果。
7. 檢查放哪一層
新增一條檢查時,怎麼決定它放在哪裡?先問一個問題:如果模型是完美的,這條檢查還需要嗎?
flowchart TD
Q1{"換成完美的模型,<br/>這條檢查還需要嗎?"}
Q1 -- 需要 --> TASK["任務層:task_rules/ 或 engine/<br/>影響畫面時要同步 web mock"]
Q1 -- 不需要 --> Q2{"它在修補或量測<br/>這個模型或伺服器的輸出習慣嗎?"}
Q2 -- 是 --> MODEL["模型層:profile.reply_text 或信心評分"]
Q2 -- 否 --> RUNTIME["執行期:傳輸、排程、可用性<br/>留在 ai/"]目前主要的檢查分類如下:
| 檢查 | 位置 | 層 | 理由 |
|---|---|---|---|
取出第一個 { 到最後一個 } 之間的內容 | llm/output.py | 模型 | 容許模型在 JSON 外多寫文字。已存的 Super 回覆都是乾淨的 JSON,所以對 Super 只是容錯 |
| JSON 必須是物件 | task_rules/ | 任務 | 每個任務的輸出契約 |
未知欄位拒收、null 代表未陳述 | task_rules/record_checks.py、statement_checks.py | 任務 | 輸出 schema |
| 生命徵象範圍、sex 列舉、過敏狀態與藥名限制 | task_rules/record_checks.py、statement_checks.py | 任務 | 生理與記錄限制 |
| 過敏「無」或「未知」要有原文依據 | task_rules/record_checks.py、statement_checks.py | 任務 | 安全規則,任何模型都需要 |
路由標籤合法、問題模式回退 other | task_rules/route_checks.py | 任務 | 分類器輸出契約 |
| 主訴翻譯:英文字詞白名單、數量、重複片語、中文數字/方位/身體部位比對 | task_rules/complaint_guard.py | 任務 | 對任何翻譯者都成立的顯示規則 |
| AI 整理的摘要:每句附來源、來源是給模型看過且有內容的事實、句子裡的數字出自引用欄位、不含診斷/治療/劑量用語、最多 6 句每句 160 字 | task_rules/summary_checks.py | 任務 | 輸出契約與安全規則;不通過的句子丟掉,不修補 |
| 鑑別診斷:事實編號要對得到填了值的案件欄位;段落 id 只認這一次搜尋回傳過的,並換成出處字串;每組最多 5 項、不重複、不含機率或信心分數;任一組沒有合格項目就整次失敗 | task_rules/differential_checks.py | 任務 | 輸出契約;書中原文不進輸出 |
事實編號少一個引號(f3"、"f3) | llm/prompt_sets/ems_compact/differential.py 的 repair_reply | 模型 | 這個模型偶爾寫壞;只修 sources 陣列裡「f 加數字」的項目,其他不動 |
急救責任醫院分級:只有 level、reason、fields 三個鍵,level 合法,理由 80 字內,引用的欄位必須是送給模型的已填欄位 | task_rules/hospital_level_checks.py | 任務 | 輸出契約 |
| 臨床下限:到院前檢傷 1–2 級、危急、疑似 STEMI 時,一般改成中度 | engine/hospital_level.py | 任務 | 臨床規則,與 mock 對齊 |
| 建議追問:繁體中文問句、5–40 字、結尾「?」、不含治療/劑量/診斷建議、欄位鍵在頻道詞彙內 | task_rules/followup_checks.py(欄位已填、重複、超過上限由 engine/followups.py 管) | 任務 | 輸出契約與案件規則 |
| 護理師輸入擷取:只提案白名單欄位與合法的值,數值要在原文裡 | task_rules/nurse_note_checks.py(不蓋掉護理師已填的值在 engine/nurse_notes.py) | 任務 | 輸出契約與案件規則 |
自我檢查護欄(Yes/No、max_tokens 8) | llm/prompt_sets/ems_compact/self_check.py、設定檔的 rail 取樣 | 模型 | 依賴這個模型一個 token 就答 |
| 對數機率讀取、token 與回覆對不上時不給分數 | llm/wire.py、llm/choice_scoring.py | 模型 | 依賴 vLLM/OpenAI 的對數機率格式與分詞 |
| 寫入閘門、前置路由(空白或標點略過) | ai/gate.py、ai/route.py | 任務(留在 ai/) | 產品規則,確定性路由 |
| 人工值優先、supersedes、拒絕記憶 | engine/ | 任務 | 案件規則,與模擬後端對齊 |
| 逾時、HTTP 錯誤、NAT 回傳型別不明 | ai/extractor.py、llm/nat_config.py(num_retries = 1) | 執行期 | 傳輸;失敗就失敗,不重試 |
連續失敗門檻(aiAvailable=false)、每案件 8 個待辦上限、排程空位 | ai/service.py、ai/scheduler.py | 執行期 | 可用性與排程 |
系統裡沒有「JSON 壞掉就重試」或「改用備援模型」的程式,也不打算加。
8. 新增提示詞或檢查
- 提示詞: 放進提示詞集模組,不放在
ai/;只能用標準函式庫;提高PROMPT_VERSION。提示詞與使用者訊息的位元組由server/tests/unit/test_model_layer_pins.py固定(基準檔在server/tests/fixtures/model_layer/,用server/scripts/record_model_layer_goldens.py產生),重新產生基準檔時必須附上保留集證據。另外要維持 StubLlm 的前綴約定(server/tests/stub_llm.py),不然就要連 StubLlm 一起改。 - 檢查: 用第 7 章的判斷。任務檢查放
task_rules/或engine/,影響畫面的規則,要有對照案例,網頁端也要有相同的實作;模型檢查放llm/output.py或設定檔。 - 每條新規則都要有一個「拿掉它就會失敗」的測試,並附上變異測試的證據。
9. 換模型操作手冊
換模型時要改的是 llm/ 裡新增的設定檔,加上 infra/ 裡那個模型自己的部署手冊;其他層不必改。
flowchart TD
S1["1 可行性<br/>端點、對數機率、推理開關、記憶體、授權"] --> S2["2 部署手冊<br/>部署腳本、模板、group_vars;<br/>bench_serving 與 3 串流壓測決定 max_streams"]
S2 --> S3["3 寫 profile 並登錄 registry"]
S3 --> S4["4 不需模型的本機檢查"]
S4 --> S5["5 錄對數機率測試資料"]
S5 --> G{"6 宣告門檻後,<br/>保留集全部跑完<br/>達標?"}
G -- 否 --> S7["7 在根因那一層修;<br/>不加個案字眼"] --> G
G -- 是 --> S8["8 信心校準"]
S8 --> S9["9 部署(--check --diff)"]
S9 --> S10["10 記錄證據"]- 可行性。 確認端點是 OpenAI 相容、
logprobs與top_logprobs(含bytes)能不能回、推理怎麼關、context 長度、記憶體(Super 現在約 95–103 GiB;後端與資料庫上限預設合計 2 GiB,見triaige_server_mem_limit與triaige_db_mem_limit;借用的那台主機的資料庫是 2 GiB,因為知識庫索引要空間)、授權。資料來源見docs/specs/2026-10-13-backend/research-<帳號>-stack.md與group_vars/servers.yml的註解。 - 部署手冊。 新增
infra/playbooks/deploy-<model>.yml、模板與group_vars區塊,先--check --diff,部署到 Spark,再跑eval/scripts/bench_serving.py與 3 條串流壓測,之後才決定max_streams。 - 寫設定檔。 新增
llm/profiles/<id>/__init__.py,內容:身分與服務名稱、八個角色的Sampling(max_tokens取該角色在校準集上輸出 token 的 p99 × 1.5;要開鑑別診斷再加differential取樣)、reply_text、信心能力、逾時與串流數、prompts(先引用ems_compact)、證據;然後在llm/registry.py登錄。 - 不用模型的本機檢查。 用
TRIAIGE_MODEL_PROFILE=<id>跑後端的 pytest(StubLlm 與設定檔無關)、架構測試、設定檔契約測試(server/tests/unit/test_model_profile.py:每個角色都有取樣、路徑存在)、evalpytest、cd web && bun run check。 - 對數機率測試資料。 執行
server/scripts/record_logprobs_fixtures.py --profile <id>(--profile預設是nemotron_3_super),寫到server/tests/fixtures/logprobs/<id>/。測試資料專屬的預期值要重新推導,性質測試不用改。 - 開跑前先宣告門檻(誠實規則)。以評測標準(yardstick)
cb8bb62為準,全部在保留集:基準是現行設定檔跑 3 次取平均;容忍度是 3 次的最大值減最小值,不設下限;候選模型跑 1 次。要跑的有:eval/scripts/run_extraction.py(整段口述)、eval/scripts/replay_backend.py(逐句,含正常與連發)、run_corrections.py與score_corrections.py、路由 T1/T3、過敏 T2、eval/complaint_zh、延遲 A/B(ab_latency.py、ab_logprobs.py)。一律在lockf ~/.local/state/triaige-qa/super.lock內執行,串流數不超過max_streams,結果寫到eval/results/<date>-<id>/。 - 失敗怎麼修。 在根本原因所在的那一層修正(2026-10-13 的使用者規則):模型讀錯了才改提示詞,而且只寫通則、只在校準集上調整、保留集只跑一次;切句或上下文的問題改擷取架構;合併的問題改 engine;標準答案有疑問就交給臨床組員。不能加只針對個案的字眼。
- 信心校準。 執行
eval/scripts/calibrate_choice_confidence.py:collect在校準集與保留集上收集模型的回覆與 logprobs(要持有super.lock),calibrate寫出報告與該設定檔的門檻檔(C2)。達到 inline-review 計畫的 D7 目標、而且 τ 分得出差別之前維持enabled=false:Super 在 2026-10-18 達到目標,但 τ 分不出差別,所以仍是false。重新產生模擬後端用的副本,並跑漂移測試。 - 部署。 把
triaige_model_profile指向新模型(infra/inventory/group_vars/servers.yml;服務名稱從vllm_served_names查,vLLM 的vllm_stack跟著它),--check --diff後部署,再照infra/README.md跑部署後的煙霧測試,並重播兩個案例。回滾就是回到前一個設定檔 id 與前一個映像標籤。 - 記錄。 在
eval/README.md加一節、更新設定檔的evidence、更新本文第 6 章的表。
10. 部署與設定開關
flowchart LR
subgraph Host["DGX Spark 主機"]
CF["cloudflared(Cloudflare Tunnel)"]
subgraph Docker
SRV["triaige-server :8100<br/>記憶體上限 1536m"]
DB[("PostgreSQL :5433<br/>記憶體上限 2g")]
VLLM[("vLLM Nemotron 3 Super :8000")]
end
end
CF --> SRV
SRV --> DB
SRV -- "TRIAIGE_LLM_BASE_URL" --> VLLM部署由 infra/playbooks/deploy-triaige-server.yml、deploy-nemotron-super.yml、deploy-cloudflared.yml(Cloudflare Tunnel,對外網址 https://triaige-api.wayneh.tw)、deploy-dashboard.yml(維運儀表板)與 infra/playbooks/templates/triaige/docker-compose.yml.j2 負責,參數在 infra/inventory/group_vars/servers.yml,各主機自己的值在 infra/inventory/host_vars/。後端映像在主機上原生建置,標籤是部署當時的 git revision(12 碼)。對外服務跑在比賽現場借用的 ASUS GX10(infra/inventory/host.yml 裡的第二台主機;資料庫上限是 2g,因為知識庫的索引需要空間;Tailscale Funnel 在這台機器沒有取得公開 DNS 記錄,所以用 Cloudflare Tunnel);第一台 Spark 是評測與先前部署用的機器。正式環境的設定檔是 nemotron_3_super。
環境旗標(server/src/triaige_server/config.py):
| 旗標 | 預設 | 作用 |
|---|---|---|
TRIAIGE_MODEL_PROFILE | nemotron_3_super | 選設定檔;未知值啟動失敗 |
TRIAIGE_R1_CONTEXT | 開 | 擷取提示詞帶上前文(X1);關掉時 R1 只收那一句,改用不帶前文的提示詞 |
TRIAIGE_CHOICE_CONFIDENCE | 開 | 要求回傳對數機率並對選項欄位評分;設定檔不支援對數機率時啟動失敗 |
TRIAIGE_COMPLAINT_TRANSLATION | 開 | 啟用主訴英翻中 |
TRIAIGE_FOLLOWUP_SUGGESTIONS、TRIAIGE_FOLLOWUP_IDLE_S | 開、8 秒 | 建議追問:救護端對話停下來 8 秒(擷取都結束)、護理師端送出時,背景請模型列出還該問的事,放進既有的建議清單(type = question,不彈出、不語音)。P2 最低優先級,被即時擷取擠掉後從頭再跑;失敗只記稽核 followup.skipped,不重試 |
TRIAIGE_NURSE_EXTRACTION | 開 | 護理師輸入擷取:護理師在檢傷畫面打的一段話,一次模型呼叫(P1)擷取成檢傷欄位提案,掛在那則訊息的 proposedChanges(message.updated),護理師按「套用」才寫入;只提案白名單欄位、不蓋掉護理師已填的值。關掉時 nurse channel 只儲存並回覆固定文字 |
TRIAIGE_SELF_CHECK_OUTPUT | 關 | 啟用 Yes/No 輸出護欄 |
TRIAIGE_ROUTE_CLASSIFIER | 關 | 啟用路由分類器 |
TRIAIGE_MEMO_CONSOLIDATE | 關 | 啟用備忘整理:對話停下來(TRIAIGE_MEMO_IDLE_S,預設 20 秒;還沒整理的句子不到 TRIAIGE_MEMO_MIN_LINES(4)句時,要等最舊的一句滿 TRIAIGE_MEMO_TAIL_S(60 秒))或送出時,一次讀一段話寫案件備忘;只在 AI 閒置時跑,任何工作一到就讓開,等了 TRIAIGE_MEMO_STALE_S(預設 180 秒)才升成一般背景工作;保留集通過之後才在 infra/ 打開 |
TRIAIGE_HOSPITAL_LEVEL | 關 | 啟用急救責任醫院分級建議:紀錄表欄位變動後(TRIAIGE_HOSPITAL_LEVEL_DELAY_S,預設 2 秒內的變動合成一次),以 P2 背景工作讓模型讀已填欄位,寫 /triage/neededHospitalLevel(來源 ai,待複查);輸入沒變不再問、失敗只記稽核 hospital_level.failed、不重試 |
TRIAIGE_AI_SUMMARY | 開 | 醫師交接摘要的 AI 整理:救護端送出與檢傷送出之後,背景(P2,被即時句子擠掉就重跑)把送出的紀錄表、備忘與檢傷評估整理成幾句附來源代號的繁體中文;提示詞與取樣參數在 llm/prompt_sets/ems_compact/summary.py 與 sampling["summary"],確定性檢查(來源存在、數字出自引用欄位、不含診斷與處置用語)在 task_rules/summary_checks.py,狀態與稽核(ai_summary.generated、ai_summary.failed)在 engine/ai_summary.py;結果放在 PhysicianSummary.aiSummary,規則版摘要照常,失敗時明說、不重試、沒有備援模型 |
TRIAIGE_DIFFERENTIALS | 關 | 醫師端鑑別診斷:案件送出與檢傷送出後,背景 agent 搜尋 kb_passages 並整理常見與最致命各 5 項(開推理,約 1–3 分鐘:實測 70–110 秒,正式機 UAT 一次約 2.5 分鐘;一次執行上限取自設定檔,Super 是 600 秒);設定檔沒有 differential 取樣時啟動失敗;TRIAIGE_DIFFERENTIAL_TIMEOUT_S(預設取自設定檔)、TRIAIGE_DIFFERENTIAL_STALE_S(預設 60:等這麼久還沒輪到閒置時段就升成一般背景工作)。知識庫匯入見 infra/README.md「知識庫」 |
TRIAIGE_LLM_CALL_LOG | 不記錄 | 指到一個檔案才記:每一次模型請求寫一行 JSON(任務、token 數、延遲、結果、輸入輸出節錄),給維運儀表板;ai/call_log.py |
TRIAIGE_LLM_BASE_URL、TRIAIGE_LLM_MODEL、TRIAIGE_LLM_TIMEOUT_S、TRIAIGE_AI_CONCURRENCY | 取自設定檔(並行數預設 2) | 部署時可覆蓋 |
程式預設與 Spark 上不同的有四項(infra/inventory/group_vars/servers.yml,由 docker-compose.yml.j2 帶進容器):TRIAIGE_DIFFERENTIALS 開、TRIAIGE_HOSPITAL_LEVEL 開、TRIAIGE_AI_CONCURRENCY 3(2026-10-19 在第一台 Spark 上的 soak,docs/specs/2026-10-13-ai-routing/soak-results.md)、TRIAIGE_LLM_CALL_LOG 指到 /ops/llm-calls.jsonl。其餘旗標用上表的預設:TRIAIGE_SELF_CHECK_OUTPUT、TRIAIGE_ROUTE_CLASSIFIER、TRIAIGE_MEMO_CONSOLIDATE 關;TRIAIGE_R1_CONTEXT、TRIAIGE_CHOICE_CONFIDENCE、TRIAIGE_COMPLAINT_TRANSLATION、TRIAIGE_AI_SUMMARY、TRIAIGE_FOLLOWUP_SUGGESTIONS、TRIAIGE_NURSE_EXTRACTION 開。
11. 證據索引
eval/README.md:〈服務參數實測(2026-10-12)〉、〈R1 採用標準與重新判定〉、〈R1 上下文(X1)採用標準〉、〈X1 審查修正後的重新驗收〉。- 結果目錄:
eval/results/serving-2026-10-12/、yardstick-2026-10-13/、r1-context-fix-2026-10-14/、routing-2026-10-13/、choice-confidence-latency-2026-10-14/、choice-confidence-2026-10-17/、choice-confidence-2026-10-18/、nemotron-3-5-super-vl-2026-10-18/、soak-2026-10-19/(說明在docs/specs/2026-10-13-ai-routing/soak-results.md)。 - 知識庫:
eval/README.md〈Tintinalli 檢索(RAG)〉、〈開放指南索引〉、〈臺北市到院前救護手冊檢索〉;docs/references/ems-protocol-taipei/README.md;server/scripts/export_kb.py;infra/README.md〈知識庫〉。 - 評測標準的 commit:
cb8bb62。 - 分層與模型邊界的測試:
server/tests/unit/test_architecture.py(依賴方向、模型字面值、JSON 修補位置)、test_model_profile.py(設定檔的值與路徑)、test_model_layer_pins.py(提示詞與解析結果的位元組)、test_reply_adapters.py(輸出修補)、test_choice_confidence_properties.py(信心評分的性質)。 - 設計:
docs/specs/2026-10-14-inline-review/plan.md§4(信心設計)、docs/specs/2026-10-13-backend/design.md。
12. 已知限制
- 信心規則的適用範圍見 inline-review 計畫 §4.3;對數機率測試資料只在 Super 上錄過,換模型要重錄。
- 主訴翻譯檢查只能盡量擋下錯誤(ZH9);主訴含數字或明確次數時,只顯示英文。
- 提示詞是針對 Super 調整的;換成其他模型會怎樣,沒有證據。
- 評測用的逐字稿是模型寫的草稿,案例卡還在等醫師審閱。
- 同時請求的上限,設定檔預設是 2,Spark 上設 3:舊版 vLLM(v0.28.0)的第 3 條串流曾觸發 CUDA 錯誤(原因不明,間歇發生);v0.31.0 的 soak(3 條跑 30 分鐘)沒有再出現,但不能因此排除,而且 soak 在第一台 Spark 上跑,借用的那台主機沒有重跑。
- 信心等級目前不顯示:門檻校準過兩次,τ 仍分不出差別(
enabled=false)。 - 階段 2 的 AI 功能(鑑別診斷、AI 整理、建議追問、護理師輸入擷取、急救責任醫院分級)沒有保留集評測,只有確定性檢查與單元測試。鑑別診斷是最低優先級、約 1–3 分鐘,最長 600 秒;失敗時沒有重試,也沒有備援模型。
- 知識庫共 3,714 段,兩個來源有著作權(Tintinalli、臺北市手冊),授權都還沒有解決;31 份指引中有 16 份的 Creative Commons 授權是非商業或禁止改作。線上的檢索召回率沒有量過。
- 案件備忘整理、Guardrails 自我檢查與路由分類器在 Spark 上關閉。
- 網頁測試的字面值與註解裡出現的
nemotron字串沒有作用,不需要改。