triAIge 架構說明(給團隊成員)
救護車上的救護員說「男性,胸痛半小時,血壓一百六十、九十,心跳一百一」,幾秒後,救護紀錄表上的性別、主訴與生命徵象就填好了,每一格都標明是 AI 填入、等人複查;病人送到急診時,檢傷護理師打開同一個案件,看到的是預先填好、附上建議檢傷級數的評估表。triAIge 做的就是這件事。前端是救護端、急診端與醫師端的網頁;後端是一個 FastAPI 服務,在一台 DGX Spark 上用 Nemotron 3 Super 擷取欄位,臨床判斷交給確定性規則與護理師。這份文件說明系統現在的組成:程式分成哪幾層、一句話經過哪些層、每一類規則放在哪裡,以及新增提示詞、新增檢查或換模型時要改哪裡。
1. 這份文件給誰、怎麼讀
讀者有兩種:開發者會用到全部章節;臨床組員只需要讀第 2 章(系統全貌)、第 4 章(一句話的處理過程)與第 12 章(已知限制),第 5 到第 10 章講程式內部結構與操作,可以跳過。
這份文件遵守 eval/README.md 的誠實規則:評測用的逐字稿是模型寫的草稿,案例卡來自 MIMIC,還在等醫師審閱,所以這裡不說「已通過臨床驗證」。
2. 系統全貌
flowchart LR
subgraph Browser["前端(iPad/瀏覽器)"]
WEB["/ 正式介面(真後端)"]
MOCK["/internal 模擬版(瀏覽器內 mock)"]
end
FUNNEL["Tailscale Funnel"]
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"]
VLLM[("vLLM:Nemotron 3 Super<br/>:8000")]
PG[("PostgreSQL :5433<br/>cases JSONB、kb_passages")]
end
EVAL["eval/(離線評測)"]
WEB --> FUNNEL --> 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檢查。網頁部署在 Vercel。 - 後端是單一 FastAPI 服務(
server/src/triaige_server/),NAT 內嵌在同一個行程裡,不另外起服務。 - 資料存在 PostgreSQL:案件以 JSONB 存放;
kb_passages表已經先建好,留給階段 2 的 RAG(server/src/triaige_server/migrations/001_init.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"]
RT["NAT 執行期黏合<br/>extractor.py、nat_*.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、self_check"]
ADP["output.py、wire.py、choice_scoring.py"]
end
INFRA["infra/ 部署手冊與 group_vars"]
VLLM[("vLLM")]
API --> SVC
API --> ENGN
SVC --> ENGN
SVC --> TRN
SVC --> RT
SVC --> PROF
RT --> PROF
RT -- "OpenAI 相容 HTTP" --> VLLM
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 黏合、寫入閘門 | engine、task_rules、llm |
| 任務 | 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
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 會取出 {…},其他地方再寫一份,會被架構測試擋下。
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,ai/extractor.py 的 build_config_dict 呼叫它 | 模型 |
| 關閉推理的開關 | 同一個設定檔(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) | 尚未建立:Confidence.thresholds 已預留檔案路徑,目前是 None;校準(C2)後放在 llm/profiles/<id>/confidence_thresholds.json,見 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 | 任務 |
| 急救責任醫院分級建議:輸入欄位、臨床下限(到院前檢傷 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,各正式任務的腳本都經由它讀取模型與取樣參數 | 評測 |
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
self_check
}
ModelProfile --> Serving
ModelProfile --> "4" Sampling : extraction、route、translate、rail
ModelProfile --> Confidence
ModelProfile --> PromptSet應用層有三條固定規則,不由設定檔決定:max_retries 一律是 0(失敗就送出 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(第 3 條串流曾觸發 CUDA 錯誤) | 設定檔的 max_streams、eval/README.md |
| 取樣 | 擷取 300、路由 48、翻譯 96、護欄 8 個 token;temperature 0.0;enable_thinking=false | llm/profiles/nemotron_3_super/__init__.py |
| 輸出修補 | json_span(取第一個 { 到最後一個 }) | llm/output.py 的 json_span |
| 對數機率 | top_logprobs=5 | 設定檔的 Confidence.top_logprobs |
| 信心門檻 | 尚未校準(thresholds=None) | inline-review 計畫 §4.4 |
| 證據 | 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/;eval/complaint_zh/ | 設定檔的 evidence |
這些值由 server/tests/unit/test_model_profile.py 固定,設定檔裡列出的每個路徑也要存在。
候選設定檔:nemotron_3_5_super_vl(評估中,不是預設)
Nemotron 3.5 Super VL NVFP4,只用文字輸入。為了讓評估只比較模型本身,它和 Super 只差在服務名稱 nemotron-3.5-super-vl;取樣、token 上限、提示詞集、關閉推理的開關、json_span、top_logprobs=5、逾時與 2 條串流都和 Super 相同,由同一個測試固定。它有自己的信心門檻檔,還沒校準(enabled=false,每個欄位的 tau 都是 null)。對數機率測試資料(fixture)要在評估窗口錄製。vLLM 啟動參數的出處、切換與回滾,見 infra/README.md 的「評估 Nemotron 3.5 Super VL」。在保留集上不比 Super 差,才把 triaige_model_profile 換成它。
第二個設定檔要填什麼
下面是示意,不是會上線的程式碼:
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={...}), ... },
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會在啟動時直接失敗,不會默默產生沒有信心值的結果。 - 啟動時遇到不認得的設定檔 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 | 任務 | 對任何翻譯者都成立的顯示規則 |
自我檢查護欄(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 | 執行期 | 傳輸;失敗就失敗,不重試 |
連續失敗門檻(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)、授權。資料來源見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)、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;標準答案有疑問就交給臨床組員。不能加只針對個案的字眼。
- 信心校準。 執行
calibrate_choice_confidence.py --profile <id>(尚未實作,屬於 C2 工作),寫入該設定檔的門檻檔。達到 inline-review 計畫 的 D7 目標之前維持enabled=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 主機"]
TS["Tailscale Funnel"]
subgraph Docker
SRV["triaige-server :8100<br/>記憶體上限 1536m"]
DB[("PostgreSQL :5433<br/>記憶體上限 512m")]
VLLM[("vLLM Nemotron 3 Super :8000")]
end
end
TS --> SRV
SRV --> DB
SRV -- "TRIAIGE_LLM_BASE_URL" --> VLLM部署由 infra/playbooks/deploy-triaige-server.yml、deploy-nemotron-super.yml 與 infra/playbooks/templates/triaige/docker-compose.yml.j2 負責,參數在 infra/inventory/group_vars/servers.yml。後端映像在 Spark 上原生建置,標籤是 git revision。正式環境目前執行映像 triaige-server:03c2ed7c0f49,設定檔是 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、不重試(level_llm 的 NAT 重試次數降為 1) |
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 項(開推理,約 2–5 分鐘);設定檔沒有 differential 取樣時啟動失敗;TRIAIGE_DIFFERENTIAL_TIMEOUT_S(預設取自設定檔)、TRIAIGE_DIFFERENTIAL_STALE_S(預設 60:等這麼久還沒輪到閒置時段就升成一般背景工作)。知識庫匯入見 infra/README.md「知識庫」 |
TRIAIGE_LLM_BASE_URL、TRIAIGE_LLM_MODEL、TRIAIGE_LLM_TIMEOUT_S、TRIAIGE_AI_CONCURRENCY | 取自設定檔 | 部署時可覆蓋 |
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/。 - 評測標準的 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,因為第 3 條串流曾觸發 CUDA 錯誤(原因不明,間歇發生)。
- 網頁測試的字面值與註解裡出現的
nemotron字串沒有作用,不需要改。