iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0

昨天 Day 13 我們把「可抽換」拆到了底——標記、分類表、名冊三樣東西,外加一條結論:名冊握著所有零件,執行期報出名字就能挑一個。文末我留了個鉤子,說「逐請求覆寫」這整篇都建立在那本名冊上。今天就來兌現。問題很具體:設定檔的 provider 是「全站此刻的預設」,改它會影響所有人;但有時候我只想對「這一筆」請求臨時換一家供應商,而且換完就還原,不碰任何人。怎麼做到?答案短得有點不講道理:加一個 HTTP header。

本篇結構:

為什麼需要「只換這一筆」

先說清楚這個需求從哪來,不然會覺得多此一舉:既然設定檔改一行就換好了,為什麼還要逐筆?

因為設定檔那條路是「全域、要重載、影響每一個人」的。它適合「我決定全站從今天起改用某家模型」這種長期決策,但有兩個場景它幫不上忙。

  1. 線上除錯。某個行員回報,他那題用目前的模型答得怪怪的,你懷疑是模型本身的問題。你總不能為了驗證一題,把全站的 LLM 切過去、讓所有人陪你做實驗,驗完再切回來——這風險太大,節奏也太慢。你要的是:只對你自己重發的那一筆請求,臨時指定另一家模型,其他人毫無感覺。
  2. A/B 測試。你想知道 openai-httpgemini-http 對同一批問題的答題品質差多少。最乾淨的做法是讓同一套流程、同一個入口,按某種規則把請求分流到兩家——A 組打一家,B 組打另一家,其餘條件完全一致。這件事如果靠改設定檔,根本做不到「同時存在兩組」;它天生就是個逐請求的決策。

這兩個場景的共同點是:切換的粒度是「一筆請求」,不是「一次部署」。Day 13 講名冊有兩種選法——執行期查表、開機定下——逐請求覆寫就是把「執行期查表」這條路用到盡頭:既然所有零件啟動時都還握在名冊裡,那「這一筆挑哪個」當然可以細到每一筆都不一樣。

機制:一個 header 蓋過設定

實作上幾乎沒有玄機。請求進來時,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 想比較 regexopenai-moderation 對同一段話的攔截差異,逐筆換最方便。

不能逐筆換的,是開機定下的那一類,列出來你會發現它們有個共同氣味:

插槽 為什麼焊死
登入/身分驗證 身分規則在一次部署裡本來就該全站一致。讓它能逐請求換,等於開了一個「這一筆我自己挑驗證方式」的後門——這不是彈性,是漏洞。
稽核(audit 寫到哪裡) 法遵留痕的去向是營運決策,不該被單筆請求左右;要是能逐筆換寫入目標,稽核的完整性立刻可疑。
送 LLM 前的預檢、MCP 認證、檢索器 同理,都屬於「一次部署裡該保持一致」的東西。

判準其實很簡單:這個選擇換掉,會不會動搖安全或一致性的根基? 會的,就只能開機定死,連 header 都不給它看;不會的——純粹是「找個來源回答問題」的查表型能力——才開放逐筆覆寫。LLM、地圖、入境守門屬於「換了頂多答案風格不同,不會破壞規則」的那種,所以放行;身分和稽核這種「換了就動搖信任地基」的,焊死。

逐請求覆寫的開放範圍

  可逐筆換(執行期查表)            開機定死(不看 header)
  ──────────────────────           ───────────────────────
  LLM            X-LLM-Provider      登入 / 身分驗證
  地圖            (查表型)           稽核 audit
  輸入守門        X-Guardrail-        送 LLM 前預檢
                 Provider            MCP 認證 / 檢索器

  判準:換掉它會動搖安全或一致性嗎?會 → 焊死;不會 → 可逐筆

一個威力很大的 header,得有人看著它

逐請求覆寫好用,但你應該已經察覺——一個能「指定這題用哪家供應商、用哪套守門」的 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 可以很方便,但它現在還是一個無人看管的方便。

還有三個現實邊界順帶講清楚,免得高估它。

  1. 覆寫只在「執行期查表」這條路上有效,這是 Day 13 那套機制的直接後果——名冊得一直握著所有零件,逐筆換才有東西可挑;開機定死的插槽,零件根本沒全留在手上,header 自然無從作用。
  2. 它換的是「這一筆用誰」,不是「系統現在實際接著哪些家,各自還活著嗎」——那是另一個層次的問題:若想在執行期查現況、熱重載、停用某一家,得靠供應商的生命週期管理,不是單筆請求能涵蓋的事。
  3. 它只換「用哪一家」,不保證「那一家做得到這件事」。

第三點多說兩句。各家供應商的實作是套在同一個最大公約數介面上的,介面本身不帶能力資訊——它不標自己支不支援串流、看不看得懂圖片、能不能 function calling。所以你若把某筆請求覆寫到一個能力不匹配的供應商(比方要串流,卻指定一個還沒實作串流的 openai-http),系統不會在挑選的當下擋你:它只核對「這名字在名冊裡、沒被停用」就放行,真正執行到那一步才炸(Day 23 的串流正是這樣:不支援的供應商直接回報錯誤,而非假裝退化成整段一次吐)。能力協商(呼叫前先確認對方支不支援這招)目前是沒有的,逐筆覆寫到能力不匹配的供應商,會在呼叫當下失敗,這是「純靠名稱抽換」要自己留意的邊界。更嚴謹的做法是把這道檢查提前到「挑供應商的當下」:名冊除了名字,還帶一張能力表(支不支援串流、多模態、function calling,是語意守門還是規則遮罩),挑的時候就比對這題需要什麼,不匹配就當場回一個明確的 4xx 業務錯誤,而不是執行到中段才炸。這樣「可抽換」才從「選得到」走到「選得對」。

明天 Day 15,我們把鏡頭從「這一筆挑誰」拉到 Portal管理面——看它怎麼在不重啟的前提下查現況、熱重載、停用某一家供應商(控制面),以及後台怎麼透過它讀寫知識庫(KM 反向代理)。


上一篇
Day 13|插件框架核心
下一篇
Day 15|Portal 的管理面:控制面與 KM 反向代理
系列文
轉生到全端工程師沒多久就要負責公司的大平台??18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言