昨天 Day 28 講用量閘門,我們把成本與頻率擋在花錢之前——不管請求從哪裡來,進門先過預檢、每次呼叫 LLM 前再查一次帳本。但這裡藏了個沒講白的前提:那道閘門守的是「請求」,可它假設請求都長同一個樣子。實際上行員問問題的方式不只一種——他可能打字,可能截一張錯誤畫面,也可能直接用講的。今天進入第八章的最後一站,先看這三種輸入怎麼共用同一套後端、同一套身分與守門;再坦白往前看一眼那層大多已接線、只剩計畫驗證未接的 Agent 安全層。
本篇結構:
先把問題講清楚:文字、截圖、語音這三種輸入,技術形態差很多。
| 輸入形態 | 傳輸方式 | 特性 |
|---|---|---|
| 文字 | 普通 HTTP request/response |
再普通不過的一次請求 |
| 截圖 | HTTP,圖片以 base64 編碼塞進 body | 本質仍是一次 request/response,只是請求變大 |
| 語音 | 即時雙向長連線(WebSocket) | 邊說邊聽:聲音持續流進來、回應持續流回去 |
如果讓前端各自去煩惱這三種後端怎麼接、怎麼帶身分、怎麼過守門,那會是一團亂——每換一種輸入方式,前端就得重學一次後端的認證與安全規則。Portal 在這裡當的是 BFF(Backend for Frontend):把這些雜事收斂到一個後端聚合點,三種通路用一致的方式接進來,前端只管「我是哪一種輸入」,不必管「身分怎麼驗、個資怎麼遮」。
關鍵是這句:三條路各走各的傳輸,但共用同一套身分驗證與守門。 SSO 解出來的身分(Day 8 講的那張 HMAC 簽章 cookie)、入境守門的注入攔截與 PII 遮罩(Day 16、17),不會因為你換成截圖或語音就漏掉。差的只是傳輸的形狀,不是安全的標準。
行員的三種輸入,落到後端其實走兩條客戶端通路:文字+截圖一起走「多模態 RAG」,語音走「中繼」。(後台管理員讀寫知識庫那條 KM 反向代理,屬於 Portal 的「管理面」,不是行員的前端通路,已在 Day 15 跟控制面一起講過。)
| 通路 | 輸入 | Portal 做什麼 |
守門範圍 |
|---|---|---|---|
| 多模態 RAG | 文字 + 截圖(base64) | 驗身分、對文字做 PII 遮罩與預檢,整包轉發給後端引擎 | 只遮文字,圖片內容交下游 |
| 語音 | WebSocket 長連線 | 純中繼管道,逐幀雙向轉發,不做語音處理 | 票券換發+身分驗證 |
前端把文字和一張或多張截圖(base64 編碼)一起送來,Portal 驗身分、對文字做 PII 遮罩與預檢,然後把整包轉發給後端引擎跑多模態 RAG,引擎的答案原樣透傳回前端。
這裡有一條必須講清楚的邊界:Portal 不碰圖片內容,它沒有 OCR。如果小林截的那張錯誤畫面裡,恰好有人把員編、身分證直接打在螢幕上,Portal 看不到、也判斷不了——它的 PII 遮罩規則讀的是文字,圖裡的像素它解析不了。那這些敏感資訊誰來守?交給後端的多模態管線。Portal 只守它守得住的那一塊(文字),守不住的(圖片內容)如實往下交,而不是假裝守了。
語音需要 WebSocket 長連線,但 Portal 本身不做任何語音處理——它是一條中繼管道。瀏覽器連到 Portal,Portal 再連到後端引擎,兩條連線之間把訊息逐幀雙向轉發,真正的語音辨識與生成在引擎那端。
為什麼要中繼、不讓瀏覽器直接連引擎?因為認證。瀏覽器不該拿到打引擎的憑證——那是服務對服務(s2s)的事(Day 11 講過 Portal 怎麼用 workload identity 向下游證明自己)。所以流程設計成兩段票券交換:
1) POST /api/v1/portal/voice/token ← 帶 SSO 身分 cookie
Portal 驗 SSO → 回 { "token": "...", "expiresIn": 120 }
2) (WS) /api/v1/portal/voice/ws?token=...
Portal 驗票 → 確認是誰 → 代他開一條連到引擎的連線、補上引擎要的憑證
│
瀏覽器 ⇄ Portal ⇄ 引擎 逐幀雙向轉發;任一邊斷,另一邊立刻關
兩步驟拆開看:
Portal 換一張短效語音票,expiresIn 只有 120 秒。Portal 驗票、確認身分後,才代使用者開那條連到引擎的連線。那張語音票是無狀態的:用伺服器金鑰簽章、票面塞進有效期,跟 Day 8 的 SSO cookie 是同一套思路——Portal 不存 session,光憑驗章就信任票面內容。為什麼設計成 120 秒這麼短?因為它一旦從網址外洩(query 參數比 header 更容易被記進日誌、被截圖),傷害時間窗就只有兩分鐘。短效是拿來限制萬一外洩時的損失的。不過得講準確:短效的有效期(TTL)縮的是可重放的時間窗(票被撿到後還能用多久),它縮不掉外洩面本身——query 參數會被記進存取日誌、proxy 日誌、瀏覽器歷史,而那些往往保存很久。所以「兩分鐘」擋得了即時重放,擋不了「token 落進長期日誌」這個曝險。要連曝險一起壓低,得讓憑證根本別走 query(例如改用 WebSocket 子協定 Sec-WebSocket-Protocol 夾帶),TTL 只是其中一道、不是全部。
還有一條跟截圖那段對稱的邊界得講:語音這條路上,Portal 同樣守不到內容。 它是純中繼、逐幀轉發聲音,本身不做語音辨識——也就無從對「講出來的內容」做 PII 遮罩或注入偵測(那些規則讀的是文字,不是音訊波形)。所以實情是:文字那條走了完整的入境守門,但截圖的圖內內容守不到、語音的話音內容也守不到,這兩類的內容安全都壓在後端引擎身上。把這點跟截圖那段並排講,才不會讓讀者誤以為語音這條守得住——兩者其實是同一種邊界,Portal 在這兩條路上守的都只有身分與通道,守不到內容。
這兩條前端通路的形狀不一樣,但在同一個地方匯流——驗身分、過守門這道關,誰都繞不過。這就是 BFF 的價值:把差異吸在 Portal 這層,安全標準統一。(後台那條 KM 反向代理也是「驗完身分才轉發」,同一個精神,細節在 Day 15 管理面那篇。)
這幾條通路都還帶著 PoC(概念驗證)階段的妥協,得攤開講。
| 通路 | 現況妥協 | 正式版方向 | 相關 Day |
|---|---|---|---|
| 語音 | 完整語音體驗尚未對齊(中繼本身已完整實作) | 引擎端與前端對齊後補齊完整體驗 | — |
| 語音長連線 | 撞上雲端閒置逾時(Cloud Run 預設約 300 秒切線),對話可能講更久 | 基礎建設層把 Portal 與引擎之間的連線逾時上限調到一小時以上 |
Day 27 |
語音的中繼這條路已經完整實作,不只是「示意通了」——Portal 這側該做的都做了:HMAC 短效票發得出、WebSocket 連線驗得了票、Portal 與引擎之間雙向逐幀轉發、任一邊一斷線就把另一邊也關掉。還沒到位的是完整的「語音體驗」,那要引擎端和前端再對齊。
長連線會撞上雲端的閒置逾時,這跟 Day 27 MCP 軌踩過的是同一個坑:Cloud Run 預設大約 300 秒就切閒置連線,但一段語音對話可能講得更久。於是逼出一個基礎建設層的設定——得把 Portal 和引擎之間的連線逾時上限調到一小時以上,不然講到一半被切線。
最後留一段給這條軌的安全層現況。Day 27 講 MCP 軌時提過,讓 LLM 自主呼叫工具會擴大攻擊面。針對它,框架裡預留了好幾類守門——而且這裡要把狀態講準:這些大多已經接進 Agent 軌、每次工具呼叫實際生效,唯一還沒接線的是完整的計畫驗證。
換句話說,插件框架(Day 13 講的那套標記+名冊)為它們各自留了插槽,而工具結果守門、步數限制、工具政策這幾類,加上 Day 20 那個 confused deputy 的員編強制覆寫,都已經掛進工具呼叫的邊界、每次呼叫都真的會跑。只有「完整計畫驗證」是元件在、但還沒掛進流程的。挑四類說,順帶標清楚哪個還沒接:
工具結果守門:守的是「間接注入」。攻擊者不直接騙 LLM,而是把惡意指令藏進工具回傳的內容裡——比如一篇 KM 文件、一個外部 MCP 服務的回應,裡頭夾一段 ignore previous instructions。Day 16 的入境守門攔的是使用者輸入,但工具回來的東西沒經過那道關。這層就是在工具結果回到 LLM 之前再守一次——它已經接進這條軌、每次工具回傳都會過。
步數限制:守的是「失控的迴圈」。一個能自主呼叫工具的 agent,理論上可以呼叫一次又一次、停不下來——查了又查、改了再改,把成本和時間燒乾。步數限制給它一個硬上限:這一題最多走幾步,到頂就收。這跟 Day 28 的用量閘門是互補的,閘門擋的是「花多少錢」,步數擋的是「繞多少圈」——這層也已經接進這條軌、每次請求都會數。
計畫驗證:守的是「動手之前先檢查打算做什麼」。比較進階的 agent 會先擬一個多步計畫,再逐步執行。計畫驗證的想法是在執行前先看一眼這份計畫合不合規——有沒有打算去碰它不該碰的工具、有沒有偏離原本的任務。它把守門從「事後擋結果」往前挪到「事前審查計畫」。這是這條軌上唯一還沒接線的一塊:元件本身已經寫好、bean 也在,但還沒掛進對話流程,跑到那裡目前不會真的去呼叫它。
工具政策檢查:守的是「這個身分能不能用這個工具」。不是每個行員都該能呼叫每一個工具——有些工具涉及敏感操作,得按身分或角色放行。這層就是工具層級的存取控制,跟 Day 10 講的 fail-closed 精神一致:沒明確授權的,預設不放行——它也已經接進工具呼叫邊界、每次呼叫實際生效。
把這四個擺在一起看,它們其實是把前面講過的安全原則,平移到「LLM 自主行動」這個更難的場景:
| 既有原則 | 平移到 Agent 場景 | 守的東西 | 接線狀態 |
|---|---|---|---|
| 入境守門(Day 16) | 工具結果守門 | 間接注入 | 已接進 Agent 軌、實際生效 |
| 用量閘門(Day 28) | 步數限制 | 失控的迴圈 | 已接進 Agent 軌、實際生效 |
| —(事前審查) | 計畫驗證 | 動手前檢查打算做什麼 | 元件在、尚未接進流程 |
存取控制(Day 10 fail-closed) |
工具政策檢查 | 這個身分能不能用這個工具 | 已接進 Agent 軌、實際生效 |
原則沒變,戰場換了——而這場仗的防線大多已經架上去,只差計畫驗證那一道還沒接線。
不過要把「沒守的」也一起標出來,這條軌的線才算畫完整。工具結果守門擋的是工具回傳內容,但間接注入還有兩條它碰不到的路(Day 20 也提過)。一條是工具描述被下毒(schema poisoning):模型在決定要不要呼叫工具之前就先讀到了它的名稱與描述,藏在描述裡的指令在結果守門生效之前就影響了模型,而目前對工具描述沒有任何驗證。另一條是 RAG 取回的知識內容——原樣拼進 prompt、沒過注入偵測。
還有一層更底的信任邊界:MCP server 本身是被隱含信任的。 Day 27 把「對方獨立升級、Portal 不必跟著改」當成鬆耦合的優點賣——但同一件事的反面是,那是一個你不受控的遠端信任邊界。目前 Portal 對 MCP server 的識別,只是一個設定好的網址加共享金鑰,沒有憑證 pinning、沒有完整性驗證。對方若被攻破、或自動升級成一個會回傳惡意工具與內容的版本,Portal 不會察覺。鬆耦合省掉的是「跟著改」的成本,沒省掉的是「得信任對方」這件事——而這份信任現在是靠網路隔離與共享金鑰撐著,不是靠驗證。
那為什麼計畫驗證那一道還留著沒接?因為接線的代價不只是「呼叫它」。多一道事前審查就多一次延遲、多一個可能誤殺正常請求的點,而完整的計畫驗證又得先讓 agent 擬出多步計畫、再去審它,工程份量比其他幾道更重。在這個階段,把元件和介面定義穩、讓其餘幾道先實際生效,比急著把這一道半成品接進主流程更務實——等真的需要事前審多步計畫時,bean 已經在那裡等著掛,不必回頭改主流程的形狀。這也呼應了整套插件框架的一貫做法(Day 13、15):先用 default 把能力的空殼留好,要不要填真正的實作、何時掛進流程,各自決定、且不驚動既有的東西。
如實畫出這條線——哪些已上線、哪些是未來設計——本身就是平台該有的態度。一個說得清「我守住了什麼、還沒守什麼」的系統,比一個含糊宣稱「都安全了」的系統可信得多。
寫到這裡,第八章的平台能力就收尾了:SDK 軌接函式庫、Agent 軌接服務、Channels 接三種前端通路,三條路把差異都吸在 Portal 這一層,而成本與風險被閘門和守門關在裡頭。三十天的內容也走到了最後一站前。
明天 Day 30,我們把這三十天攤平了重看一遍,抽出那幾條真正能帶走、能搬到別的專案去的通用設計原則,替整個系列收個尾。