昨天 Day 13 我們把「可抽換」拆到了底——標記、分類表、名冊三樣東西,外加一條結論:名冊握著所有零件,執行期報出名字就能挑一個。文末我留了個鉤子,說「逐請求覆寫」這整篇都建立在那本名冊上。今天就來兌現。問題很具體:設定檔的 provider 是「全站此刻的預設」,改它會影響所有人;但有時候我只想對「這一筆」請求臨時換一家供應商,而且換完就還原,不碰任何人。怎麼做到?答案短得有點不講道理:加一個 HTTP header。
本篇結構:
先說清楚這個需求從哪來,不然會覺得多此一舉:既然設定檔改一行就換好了,為什麼還要逐筆?
因為設定檔那條路是「全域、要重載、影響每一個人」的。它適合「我決定全站從今天起改用某家模型」這種長期決策,但有兩個場景它幫不上忙。
openai-http 跟 gemini-http 對同一批問題的答題品質差多少。最乾淨的做法是讓同一套流程、同一個入口,按某種規則把請求分流到兩家——A 組打一家,B 組打另一家,其餘條件完全一致。這件事如果靠改設定檔,根本做不到「同時存在兩組」;它天生就是個逐請求的決策。這兩個場景的共同點是:切換的粒度是「一筆請求」,不是「一次部署」。Day 13 講名冊有兩種選法——執行期查表、開機定下——逐請求覆寫就是把「執行期查表」這條路用到盡頭:既然所有零件啟動時都還握在名冊裡,那「這一筆挑哪個」當然可以細到每一筆都不一樣。
實作上幾乎沒有玄機。請求進來時,Portal 在挑供應商的那個點,多看一眼有沒有帶覆寫用的 header;有就用 header 指定的名字去名冊查,沒有就退回設定檔的預設。一筆附帶覆寫的 /chat 請求長這樣:
POST /chat
Content-Type: application/json
X-LLM-Provider: openai-http
X-Guardrail-Provider: openai-moderation
{ "message": "我的員編是 123456,AD 帳號被鎖了,順便想查內湖分行的營業時間。", "history": [] }
這筆請求做的事:LLM 這個插槽,臨時改用 openai-http;入境守門這個插槽,臨時改用 openai-moderation。沒帶的插槽(例如地圖)照舊走設定檔。請求一結束,什麼都沒留下——下一筆沒帶 header 的請求,又回到全站預設。它就是 Day 13 那本名冊的一次臨時查詢,差別只在「用哪個名字查」這次來自 header 而不是 YAML。
選擇的優先序也很直白,記住這個順序就懂它的行為:
| 順序 | 條件 | 採用的供應商 | 生效範圍 |
|---|---|---|---|
| 1 | 請求帶了 X-…-Provider header |
header 指定的那個 | 只這一筆 |
| 2 | 沒帶 | 設定檔 portal.<slot>.provider 的值 |
全站此刻的預設 |
| 3 | 連設定都沒給 | 該類插槽的內建預設(通常是 mock) |
全站此刻的預設 |
例外:header 指定了一個名冊裡不存在的名字,該筆請求直接拒絕,不會默默退回預設(避免你以為換了、其實沒換)。
最後那條是刻意的:打錯供應商名字應該要立刻報錯,而不是「靜靜地用回舊的」。線上除錯時最怕的就是「我明明換了啊」其實根本沒換成——所以寧可這一筆失敗,把錯誤攤在你面前。
放回 Day 9 鋪過的 correlation id(追蹤碼)來看,這件事在日誌裡會留下清楚的痕跡,A/B 對帳時很有用:
2026-06-22 09:15:03.412 INFO [req=a1b2c3d4] ChatController : dispatch llm=openai-http (override) guardrail=openai-moderation (override)
2026-06-22 09:15:03.661 INFO [req=a1b2c3d4] AuditLog : emit status=success llm=openai-http durationMs=249
(override) 這個標註不是裝飾——它告訴你這一筆走的不是全站預設,是被 header 蓋過的。出事時循 req=a1b2c3d4 回看,你一眼就知道這題當時實際打的是哪一家,不會跟全域設定混淆。
這是今天最該講清楚的一條界線,也是 Day 13 那個「兩種選法」最實際的後果。不是所有插槽都接受 header 覆寫——能逐請求覆寫的,只有走「執行期查表」那一類;開機定下的那一類,一律無視 header。
能逐筆換的,都是查表型的能力:
| 插槽 | 覆寫 header | 為什麼可以逐筆換 |
|---|---|---|
| LLM | X-LLM-Provider |
換一家模型答這一題,是最常見的覆寫,也是 A/B 測試的主角。 |
| 地圖 | (查表型;機制上可換,PoC 尚未開出專屬 header) | 性質跟 LLM 一樣,純粹是「找一個外部來源回答」,換哪一家不影響流程正確性。 |
| 入境守門 | X-Guardrail-Provider |
想比較 regex 跟 openai-moderation 對同一段話的攔截差異,逐筆換最方便。 |
不能逐筆換的,是開機定下的那一類,列出來你會發現它們有個共同氣味:
| 插槽 | 為什麼焊死 |
|---|---|
| 登入/身分驗證 | 身分規則在一次部署裡本來就該全站一致。讓它能逐請求換,等於開了一個「這一筆我自己挑驗證方式」的後門——這不是彈性,是漏洞。 |
| 稽核(audit 寫到哪裡) | 法遵留痕的去向是營運決策,不該被單筆請求左右;要是能逐筆換寫入目標,稽核的完整性立刻可疑。 |
| 送 LLM 前的預檢、MCP 認證、檢索器 | 同理,都屬於「一次部署裡該保持一致」的東西。 |
判準其實很簡單:這個選擇換掉,會不會動搖安全或一致性的根基? 會的,就只能開機定死,連 header 都不給它看;不會的——純粹是「找個來源回答問題」的查表型能力——才開放逐筆覆寫。LLM、地圖、入境守門屬於「換了頂多答案風格不同,不會破壞規則」的那種,所以放行;身分和稽核這種「換了就動搖信任地基」的,焊死。
逐請求覆寫的開放範圍
可逐筆換(執行期查表) 開機定死(不看 header)
────────────────────── ───────────────────────
LLM X-LLM-Provider 登入 / 身分驗證
地圖 (查表型) 稽核 audit
輸入守門 X-Guardrail- 送 LLM 前預檢
Provider MCP 認證 / 檢索器
判準:換掉它會動搖安全或一致性嗎?會 → 焊死;不會 → 可逐筆
逐請求覆寫好用,但你應該已經察覺——一個能「指定這題用哪家供應商、用哪套守門」的 header,落在不對的人手上是很危險的。最該警惕的是守門:要是任何人都能在請求裡塞 X-Guardrail-Provider: mock,等於遞給外部一把「這一筆我不要過濾」的鑰匙,把入境守門整個關掉。這顯然不能對所有來源開放。
所以這個能力在定位上應該是內部工具,不是面向終端行員的公開功能。但這裡得把 PoC(概念驗證)的實況誠實攤開,不能只講「應該」:目前 /chat 對這個 header 完全沒有把關——不管帶不帶有效身分,任何呼叫端都能塞 X-Guardrail-Provider: mock 把守門關掉,沒有管理權限檢查,也沒有來源白名單。而且稽核這側還有一道缺口:dispatch 那行應用日誌有 (override) 標註,但稽核(audit)那筆沒有這個維度。它記下這筆最後用了哪套守門(provider 名),卻不會標記「這是被 header 覆寫的」,也沒有把「誰下令降級守門」當成一個獨立事件記下來。事後單看稽核紀錄,你看得到「這筆用了 mock」,卻難以一眼斷定它是設定檔的預設,還是有人臨時把守門關掉的。
這條線跟 Day 8 講身分時那個底層原則是同一個:能改變系統行為的輸入,得先確認它有沒有資格這麼做。逐請求覆寫只是把這個原則套到「換供應商」這件事上。正式版至少要補三件:把覆寫能力收在受控呼叫端或要求管理權限;對「降級/關閉守門」這種高風險覆寫留下 actor 維度的稽核(誰、何時、把哪一筆的守門換成了什麼);以及在 production profile 直接禁掉 guardrail=mock 這類危險組合(這點 Day 18 會再從縱深防禦的角度談一次)。header 可以很方便,但它現在還是一個無人看管的方便。
還有三個現實邊界順帶講清楚,免得高估它。
第三點多說兩句。各家供應商的實作是套在同一個最大公約數介面上的,介面本身不帶能力資訊——它不標自己支不支援串流、看不看得懂圖片、能不能 function calling。所以你若把某筆請求覆寫到一個能力不匹配的供應商(比方要串流,卻指定一個還沒實作串流的 openai-http),系統不會在挑選的當下擋你:它只核對「這名字在名冊裡、沒被停用」就放行,真正執行到那一步才炸(Day 23 的串流正是這樣:不支援的供應商直接回報錯誤,而非假裝退化成整段一次吐)。能力協商(呼叫前先確認對方支不支援這招)目前是沒有的,逐筆覆寫到能力不匹配的供應商,會在呼叫當下失敗,這是「純靠名稱抽換」要自己留意的邊界。更嚴謹的做法是把這道檢查提前到「挑供應商的當下」:名冊除了名字,還帶一張能力表(支不支援串流、多模態、function calling,是語意守門還是規則遮罩),挑的時候就比對這題需要什麼,不匹配就當場回一個明確的 4xx 業務錯誤,而不是執行到中段才炸。這樣「可抽換」才從「選得到」走到「選得對」。
明天 Day 15,我們把鏡頭從「這一筆挑誰」拉到 Portal 的管理面——看它怎麼在不重啟的前提下查現況、熱重載、停用某一家供應商(控制面),以及後台怎麼透過它讀寫知識庫(KM 反向代理)。