昨天我們把最後一塊拼圖補上了:文字、截圖、語音三種前端怎麼共用一套後端,還有那層大多已接線、只剩計畫驗證未接的 Agent 自主行動安全防線。到這裡,從進門到出門的整條路,連同它向外長出的平台能力,都拆完了。今天不再往前推新東西,而是回頭——把前 29 天散落各處的設計決定,收束成一張你可以帶走、甚至搬去別的專案用的總表。如果這 29 天你只能記住一件事,我希望是這張表,而不是任何一個 Java 細節。
本篇結構:
連載的麻煩在於,一天一站讀下來,每一站都清楚,但讀完容易只剩一堆零碎的知識點:reactive 不能阻塞、cookie 要驗章、供應商可以抽換、稽核要保底再遮一次……它們看起來像 29 個獨立的技巧。可是這套平台之所以是這套平台,靠的是那些技巧背後反覆出現的同一批判斷原則,而不是技巧本身。
Day 1 我講過這座 Portal 只做四件橫切的事——身分、安全、路由、整合。那是「它做什麼」。今天要回答的是更值錢的另一半:「它怎麼做決定」。因為功能會過時、框架會換代,但「身分服務掛了該放行還是擋下」「副作用該不該卡住主流程」這類判斷,換到任何一個你下次要蓋的後端系統,幾乎原封不動還能用。所以這篇要做的事,是把貫穿全程的設計主軸抽出來,每一條講清楚它解決了什麼、又付了什麼代價——代價這件事尤其重要,因為一個只講好處的原則是不能信的。
把 29 天攤平,會發現所有決定都收斂到八條主軸上。先看總表,再逐條展開:
| 設計主軸 | 解決什麼 | 代價/權衡 | 主要出現在 |
|---|---|---|---|
| 非阻塞/事件迴圈 | 少數執行緒撐住高並行、不空等外部回應 | 阻塞工作必須 offload,一個同步呼叫就凍住整條 |
Day 5–7 |
| 身分始於 SSO | 可驗章、stateless、不存 session | 簽章金鑰一旦外洩,整套身分信任崩潰 | Day 8 |
correlation id 貫穿 |
非同步系統裡一次請求的可追溯性 | 必須貫徹到每條 log/metric/audit,少一處就斷鏈 | Day 9、26 |
| 依賴反轉 + 策略模式 | 供應商熱抽換、換供應商核心碼不動 | 抽象介面要設計得夠穩定,動一次簽名(signature)全員改 | Day 12–15 |
| 縱深防禦 | 注入兩攔、PII 兩遮、audit 保底 | 重複處理的成本,換「每層自負其責」 | Day 16–20、25 |
| 可用性 vs 安全的不對稱 | 提問 fail-open、改設定 fail-closed |
要按操作風險量級逐條設定,不能一刀切 | Day 10 |
| 操作身分強制覆寫 | 擋 confused deputy、LLM 越權代查 | 模型傳入的身分參數一律不信、得額外攔截 | Day 20 |
| 副作用不反噬主流程 | 稽核、記帳 fire-and-forget,主線不被拖累 | 極端當機時可能漏掉最後幾筆 | Day 24–25 |
這張表不是事後硬湊的分類,它就是這套系統的「為什麼」。逐條說:
非阻塞是地基。Day 5 到 Day 7 講的那條鐵律——事件迴圈上不准有阻塞呼叫——後面幾乎每一站都回頭引用。它讓我們敢用少少幾條執行緒撐住大量同時在等 LLM 的請求,代價是任何會卡的工作(DB 寫入、慢查詢)都得 offload 出去,一旦有人偷懶在主線上同步寫一筆,整條執行緒上所有請求一起凍住。
身分始於 SSO決定了「你是誰」的可信源頭在登入那一刻,而不是這次輸入。Day 8 那張簽章 cookie——EMP_INFO = base64url(payload).base64url(HMAC_SHA256(payload, serverSecret)),payload 裡裝的是 8 碼集團員工編號 corpId 與角色——讓 Portal 不必存 session,光驗章就信任 cookie 內容。代價坦白講:這套信任全押在 serverSecret 上,金鑰外洩等於任何人都能偽造身分。
correlation id 貫穿是非同步系統的命脈。一次請求的足跡散落在不同執行緒、不同服務,靠一組 req=a1b2c3d4 才能重新縫回全貌:
2026-06-22 09:15:03.412 INFO [req=a1b2c3d4] ChatController : dispatch agent=builtin llm=gemini-http
2026-06-22 09:15:03.661 INFO [req=a1b2c3d4] AuditLog : emit status=success durationMs=249
它的代價是紀律——只要有一行日誌、一條 metric、一筆 audit 漏掉這個 id,鏈子就在那裡斷掉,出事時你會剛好卡在斷點追不下去。
依賴反轉加策略模式是「換廠商 0 行 Java 不動」的本體。上層只認一個抽象(「一個能對話的 LLM 介面」),各家實作啟動時被收進一張以名稱查找的名冊,執行期依設定挑一個:
portal:
llm:
provider: gemini-http # mock | openai-http | gemini-http | spring-ai
guardrail:
provider: regex # mock | regex | openai-moderation
audit:
provider: slf4j # slf4j | postgres
代價在於那個抽象介面必須夠穩定——它一旦要改簽名,所有實作得跟著動,「0 行不動」的承諾就破了,這也是 Day 15 講熱控制時,遲遲不敢把生命週期掛鉤從同步改成非同步的原因。
縱深防禦是金融場景的底線:不把合規寄託在上游。注入攔兩道、PII 遮兩道、audit 寫入前還自己保底再遮一次。Day 25 那個真實 bug 講得最透——守門被設成 mock 時原文會裸著流進日誌,修法不去封死 mock,改成讓稽核這一站對自己寫出去的東西負最終責任。代價是重複處理,但這正是它的本質:用多跑一遍的成本,換「任一層失效系統仍守得住」。
可用性對安全的不對稱,是我最想讓你帶走的一條。同一個身分驗證失敗,Day 10 給了兩種相反的反應:
fail-open(放行、標記匿名),因為身分中心偶爾抖一下不該拖垮整個助理;fail-closed(沒有正確憑證一律拒絕)。同一個故障,依操作的風險量級給不同答案,這不是矛盾,是刻意的不對稱。
操作身分強制覆寫是 Day 20 那道 confused deputy 防護的核心:當 LLM 能自主呼叫「查某人記憶」這類工具,攻擊者就可能用話術誘它代查他人——
我的員編是 123456,AD 帳號被鎖了,順便想查內湖分行的營業時間。
對策很硬——工具呼叫邊界上,無論模型傳入什麼身分參數一律忽略,強制換成 SSO 確立的本人。代價是你得永遠不信任模型自己報的身分。
副作用不反噬主流程收尾。稽核、記帳這類副作用一律 fire-and-forget——交辦出去就立刻回傳答案,不等它完成,失敗也只記一則警告,絕不反噬使用者的回應。它直接呼應第一條鐵律(寫入是阻塞工作、要 offload),代價是極端當機時可能漏掉最後幾筆紀錄。但這裡要說準確:「丟了就記個警告」是 PoC 的省法、不是鐵律逼出的唯一解。非阻塞與「保證不丟」其實能兼得:寫失敗落地持久佇列/Pub-Sub,加上重試與死信佇列(dead letter,收容重試不成的訊息),只是還沒接;對金融內控,那一步遲早要補(Day 24)。
把上面八條再蒸餾一次,會發現有四個原則跟這座 Portal 一點關係都沒有,跟 AI、跟金融也沒關係,純粹是好系統的通則,你下次蓋任何後端都能直接套:
故障時的反應要按風險量級分級,不要一刀切。 「服務掛了該放行還是擋下」沒有一體適用的答案,讀操作、寫操作、高風險操作該給不同預設。一律 fail-open 不安全,一律 fail-closed 不可用。
每一層對自己的輸出負責,不假設上游做對了。 縱深防禦的精神搬到哪都成立:輸入驗證、權限檢查、資料清洗,與其在邊界做一次就賭它永遠生效,不如讓每個會把資料送向「不可逆出口」(寫進日誌、寫進 DB、送出網路)的地方自己保底。代價是重複,但合規與安全本來就不該靠運氣。
副作用不准卡主流程,可信源頭只認一處。 記帳、發通知、寫稽核這些副作用 fire-and-forget 出去;而「使用者是誰」這種關鍵事實,認定一次、之後全程沿用,別讓它在每一站被重新解讀(Day 24 的 userId 取自 SSO 而非本次輸入,就是這個道理)。這兩件事併在一條,是因為它們同源:都在守住主流程不被不可信的東西污染——副作用的失敗不准拖垮它,本次輸入自報的身分也不准改寫它。
要可抽換,先把抽象釘穩、把選擇延到執行期。 依賴反轉加策略模式不是什麼高深架構,但前提是那個介面得夠窄、夠穩定。介面設對了,加一家供應商是 0 行 Java 不動;設錯了,每次擴充都是一場手術。值得在介面上多想三天。
這四條背後其實是同一種品味:把「會變的」和「不會變的」分開,讓不會變的(抽象、可信身分、主流程)穩穩釘住,讓會變的(供應商、副作用、外部服務的死活)在邊緣自由替換、自由失敗,而不波及核心。
最後得照規矩把分界講清楚,因為這 30 天裡確實有「已上線」和「規劃中」兩種東西並存。先把兩邊各自有什麼攤開:
| 狀態 | 涵蓋的能力 |
|---|---|
| 已上線(跑著的) | reactive 執行模型、SSO 身分、correlation id、SDK 軌的熱抽換、入出境守門、意圖路由、並行檢索、串流、audit 保底;MCP 軌的引用來源已接回前端;語音中繼已完整實作(短效票、驗票、雙向逐幀轉發、斷線即關);Day 29 那層 Agent 自主行動的安全防線大多已接進工具呼叫邊界、每次呼叫實際生效(工具結果檢查、步數限制、工具政策) |
| 規劃中(還帶 PoC 妥協) | 用量閘門的帳本只活在單機記憶體、是軟上限不是硬上限;完整語音「體驗」仍待引擎端與前端對齊;Agent 安全防線裡只剩完整計畫驗證是元件在、還沒接進對話流程的 |
我把它們一一標出來,不是自曝其短——恰恰相反,清楚知道「哪裡還沒做」,本身就是這套設計裡最該被搬走的態度。
不過上面那張表講的,是第一種坦白——哪些功能還沒接線、哪些是單機 PoC。還有一種更難講、卻更該講的坦白:已經宣稱做到的那些防線,本身在分散式與對抗性環境下到底夠不夠硬。 把這 30 天的真實邊界再收一層,會看到幾條反覆出現的同一種限制,值得擺在一起看:
allUsers)就破(Day 11)。這些不是「還沒接線」那種第一種坦白,而是「接了、但在分散式與對抗性環境下還不夠硬」的第二種。標出第一種已經是態度;敢標出第二種——承認自己做了的也還不夠強——才是這套設計最想留下、也最該被搬走的那種坦白。
回到最開頭那句話:Portal 不是回答問題的人。 答案出自後端引擎、知識存於知識庫,它只是一座從進門到留痕、層層把關卻全程不阻塞事件迴圈的轉運站。30 天拆下來,我希望你看到的不只是一個閘道怎麼蓋,而是一套在「穩定、安全、易替換」三件事之間反覆權衡的判斷方式——那才是真正可以帶走的東西。
一路借過的那些畫面,這時也湊齊了:村口的 Portal 認得你的真名、又不讓它外流(Day 17),聽得出灌進來的幻術(Day 16),放影分身分頭取情報(Day 22),按查克拉配額節流(Day 28),開著白眼盯全場、還把每一步記進改不掉的筆記本(Day 24、26)。這些比喻只是掛勾,真正要帶走的還是它們背後那套判斷;不過哪天你想不起某條原則,先想起那個畫面,也是個好開始。
謝謝你陪我走完這 30 天。願這張總表,在你下一個專案的某個深夜會議裡,幫你少繞一個彎。