「下一步走向 platform」這句話,我看過的版本多半被實作成加東西:加一個 model gateway、加一個 workflow engine、加一個治理平台。加完之後常見的下場是多了三個要維運的元件,而原本綁在應用裡的決策一個都沒鬆開。
換個動詞:從單一 process 走出去,做的其實是搬走——把某條邊界移出這個 process,而不是在旁邊長出新元件。讀完你能對自己的服務畫出同一條線:哪幾條邊界已經站在可以動手的起點,以及在動手之前先問清楚,這條邊界圈住的是哪個問題的答案,搬走之後誰來回答。
先驗收,再談下一步。因為這 30 天的產出,正好就是那些答案。
Day 1 的說法是:LLM 一次打破了確定性輸出、毫秒級延遲、按次計費、輸出可信這四個後端工程賴以工作的假設,而「這個系列就是把這四個洞一個一個補回來的過程」。29 天後該對帳了。
我把每個洞對到它的答案,以及那個答案今天住在哪裡:
| Day 1 的失真 | 系列給的答案 | 答案住的邊界 | 驗收結論 |
|---|---|---|---|
| ① 相同輸入,不同輸出 | 假/真 adapter 藏在同一個 Protocol 後(Day 4 起)、Day 28 的兩層評測 | provider adapter 合約+tools/ 的評測 harness |
緩解,非關閉 |
| ② 延遲以秒計 | Day 6 的兩段式串流錯誤邊界、Day 27 的串流三時間量 | api/ 串流邊界+telemetry |
緩解,非關閉 |
| ③ 按 token 計費 | Day 9 的計量不估算、雙重 guardrail、Day 14 的 prompt 位元組上限 | 預算權威(設定/准入/帳本/投影) | 緩解,非關閉 |
| ④ 輸出本身就是攻擊面 | Day 19 驗簽、Day 21 每請求隨機圍欄、Day 22 audit | api/+prompts/+audit 權威 |
轉移,且有一塊是開著的 |
四個格子沒有一個是「關閉」。這不是謙虛——限制多半是公開文件自己寫下的,只有一處來自本系列自己的量測紀錄,我在那一條標出來:
① 的限制在 docs/evaluation.md:judge 的穩定性一律寫成「這一輪沒有觀察到翻轉」,從不寫「穩定」;沒跑重複時直接印 NOT MEASURED。而 Day 28 那一輪從頭到尾一次 fail 都沒出現——失敗路徑沒有被走過。
② 的限制是量出來的:httpx 的 span 在收到 response headers 就結束。docs/observability.md 公開的那次量測是 1.067 秒對 2.568 秒;另一次跑出更懸殊的比例,但那次只在本系列自己的量測紀錄裡,沒有進公開文件。兩次的 prompt 與 reasoning token 都不同,所以只主張方向不主張比例:那條 span 代表不了完整的生成延遲,不能拿來當生成延遲用。
③ 有兩個誠實缺口。失敗的 turn 上游可能已經產生計費處理,但不進帳本(安全清單 T9);而 Day 29 又補了一個更難看的:loopback 探測實測到一次邏輯呼叫三次抵達,而且沒有 Idempotency-Key——所以成功的 turn 也可能有沒記到的錢。
④ 是四個裡唯一連文件標題都寫著 open 的:docs/prompt-injection.md 的 G3 節標題是「exfiltration through rendered output (open, and it belongs to the client)」。後端原樣回傳模型文字,不剝 markdown 圖片、不改寫 URL;文件自己把這件事定性為風險轉移,不是緩解,並且明說轉移只有在告知接收方時才正當,所以客戶端該做的四條被逐條列出來。
同一節還有兩條同樣不該被當成覆蓋:請求路徑上沒有任何 jailbreak 偵測器(Prompt Shields 是外部探針,不是 /chat 的依賴),以及唯讀工具是「今天的工具集」而不是被強制的不變量。
這張表真正的資訊在答案的位置,不在分數。四個洞的答案沒有散落各處,它們聚居在少數幾條可以指認的邊界上。而下一步要評估的,正是這些邊界。
盤這張表的時候撞到一件事:docs/architecture.md 寫的分層,跟程式樹已經不一樣了。
文件說編排在 core/。實際上 conversation.py、rag.py、agent_turn.py 全都在 services/,跟 adapter 同一個套件;而 core/ 裡面是 audit schema、設定、correlation、錯誤契約、logging、telemetry 組裝、keyed lock、租戶 context——橫切的基礎元件,不是請求路徑上的一層。
第二處同時是畫得太寬跟漏畫。舊文件在 API 層寫的是「輸入長度上限住在這裡——第一道成本閘門」,但那句其實只對 /rag 成立(API 層擋 2,000 字元):agent 的 4,000 位元組上限在 service 裡,而 /chat 在應用層沒有顯式的 message 長度上限。真正整條路徑都不在圖上的,是 Day 9 之後才長出來的 conversation token 預算。
而它有四個擁有者:政策在 core/config.py,准入與「什麼時候提交」的決定在編排服務的 _check_budget(只讀已提交總量),帳本狀態與它的原子提交在 ConversationStore,api/ 只負責把 TokenBudgetExceededError 轉成 429 與一筆 audit event。
這不是找碴。一份說錯自己邊界在哪的架構文件,在需要判斷「哪條邊界搬得動」的那一天會直接把人帶偏——你會去搬一個文件說在那裡、但實際不在那裡的東西。所以 Day 30 的第一件事是把地圖修回領土,順便把 Day 20 之後長出來、但文件裡完全沒有的東西補進去:audit plane、telemetry 的單一組裝點、agent adapter、住在 tools/ 而非 src/ 的評測、以及部署邊界。
修地圖本身就是回顧的一部分。系列的最後一天不該只有結論,也該有一次對帳。
現在回到主題。從單一 process 走向 platform,三件常被提到的事(model gateway、agent workflow、governance)各自對應一條今天已經存在的邊界。它們要的不是新增,是搬出。

ChatService 是我們自己定義的 Protocol,不是 SDK 介面的複製品;SearchClient、EmbeddingClient、RagChatService 是同一個形狀。
這條邊界的證據是它從畫下來那天(2026-07-13,Day 4)就有兩套實作——一套假的、一套真的,在單一組裝點選定,handler 裡從來沒有出現過 if use_fake。假的那套不只是測試道具,它是整條業務邏輯鏈能在毫秒內跑完的原因。一份被兩個實作同時滿足、而且天天被測試跑的合約,就是「外面的東西可以依賴它、而不依賴實作」的證明。
那什麼時候該搬?當平台(不是這個應用)開始擁有模型路由、配額、或跨團隊的花費歸屬。在那之前,gateway 只是多一跳,沒有把任何決策移出去。

忍喵:「什麼時候該搬」的正確答案,很常是「還不用」。多一跳很容易,少一個決策才難。
有意思的是這句話早就寫下了。docs/architecture.md 的 Scope 節,2026-07-09,系列第二天:
"Does not fit: organizations with a platform-provided LLM gateway (the adapter layer moves out of the app)"
寫下的當時,它的功能是免責聲明——劃出這個 lab 不適用的場景。它讀起來像路線圖,純粹是事後的效果。這中間沒有先見之明,只有一個樸素的因果:把 scope 講清楚的文件,日後會替你把路線也講掉一半。
AgentService(services/agent_framework.py:175)是框架的接縫:一個由應用自己擁有的 Protocol,讓 agent runtime 去滿足它。要搬的是這個,而且只有這個。
不搬的東西比較重要。 AgentTurnService 是這個應用對該 Protocol 的消費端;conversation 與 rag 是與它平行的編排服務,不是它下面的一層。對話範圍、預算准入、turn 提交、audit 歸屬——這些留在原地,因為它們是這個應用欠呼叫端的答案,不是 runtime 能提供的能力。這條線畫錯的後果很具體:把 AgentTurnService 一起搬出去,等於把「這一輪算誰的、扣誰的預算、什麼時候算數」交給一個不認識你租戶的東西。
證據面得分三層講:
-> AgentService 的那個邊界)檢查的。tests/unit/test_agent_framework_api_surface.py 釘的——迭代上限、client 建構、per-run 工具、多重工具呼叫。第三層那個檔案,開頭自己就寫著它不是 behavior test。上游動到那幾項,紅的是它,不是 production——但它證明不了我們自己那份 Protocol。合約本身的時間點是 2026-08-02(Day 17),先於任何 runtime 接線。
觸發條件:不只一個應用需要同一個 agent runtime,或者 runtime 的發版節奏跟這個服務對不上。一個應用配一個 agent,需要的是一份 Protocol,不是 workflow 平台。而它已經有了。
治理是最容易被講成「一塊」的東西,但拆開來看是三個權威,各有各的合約形式:
| 權威 | 今天的合約形式 | 證據 |
|---|---|---|
| Prompt 版本 | 封閉的 YAML front matter,啟動時 fail-fast 驗證:只收 name/version/description/changelog,version 必須是 int 且 ≥ 1——而且字面的 true/false 會被擋掉,因為 bool 是 int 的子類別 |
Day 8 畫的;19 個 loader 測試函式(25 個案例,含這輪才補上的 true/false regression),外加把 prompt_name/prompt_version/prompt_sha256 釘在每次上游呼叫的 logging 測試 |
| Token 預算 | 一條准入/帳本不變量:准入只讀已提交的總量;帳本跟著 turn 一起提交,在同一個全有全無的 append(... usage_tokens ...) 裡,所以用量無法跟產生它的那個 turn 漂開 |
Day 9 畫的;13 個預算測試、15 個 store 測試,加上這輪補的可攜合約測試——原子可見性、用量不得為負、總量單調、revision 衝突不提交,任何未來的 store 實作都要照過 |
| Audit | 封閉 schema 加上一個發射邊界:emit_audit_event 先過 AUDIT_EVENT_ADAPTER.validate_python 才輸出——所以型別註記不是邊界,那個呼叫才是 |
Day 22 畫的;20 個 schema 測試、逐 producer 的測試檔,以及帶 CI 漂移檢查的 JSON Schema 匯出 |
這三個沒有一個是 Python Protocol。 這件事有實際後果:如果把判準寫成「有 Protocol 就搬得動」,治理整組會當場不合格——而它明明是三件裡最該有平台版本的東西。
所以判準得寫成別的:一份明確、而且被測試跑過的合約,是安全搬走一條邊界的起點條件。形式可以是 Protocol、封閉 schema、型別驗證,或一條准入/帳本不變量。真正的測試是「外面的東西能不能依賴這份合約、而不依賴實作」,不是「這份合約用哪個 Python 語法寫的」。
它是起點,不是證明。一份在同一個 process 內成立的合約,還沒有回答搬出去之後多出來的那些東西——網路邊界、部分失敗、不再共用 process 的交易、跨信任邊界的授權、延遲、回滾,以及誰在半夜被叫起來。本系列一條都沒有真的搬過,所以它證明得了的只有「這條邊界具備動手的條件」。
反面比正面實用,但也要收窄。一個沒有合約的能力,不會因為別人想要它就變得可搬——在行為被 characterization test 釘住、被寫成合約之前,它不能當成搬過去就好的東西來規劃。那不代表只剩重寫一條路:先補上 characterization test 或 consumer-driven contract,就能走漸進式搬遷。而那份工作,正是搬遷真正需要、卻沒有人會編預算的部分。
邊界搬走,所有權應該跟著走——但那是要談出來的合約,不是搬檔案的副作用。
Provider adapter 進了 gateway,模型路由、配額、跨團隊花費歸屬該由平台團隊承接;AgentService 進了共用 runtime,框架升版的風險該從應用團隊移到 runtime 的維運者;三個治理權威搬出去,prompt 的發版審查、預算的口徑、audit 的保存期限該各自有擁有者。每一個「該」後面都得有人簽字:SLA、決策權、事故責任、相容性承諾。搬程式碼不會自動搬走這四樣。
而且應用端不會因此變乾淨。整合相容性與使用者實際拿到的結果仍然掛在你身上——平台壞了,先接到客訴的還是你。
留下來的那些也一樣清楚。api/ 沒有可搬的合約——它就是這個服務本身。編排服務欠呼叫端的是答案:這一輪算誰的、扣誰的預算、什麼時候算數、記在誰名下。這些不會因為公司多了一個平台團隊就不再是你的責任。
這 30 天我一個人同時扮演了後端、平台、安全、SRE 與資料治理五種角色,所以每個決策都在同一個腦袋裡收斂。真實組織裡它們分屬不同的人,而分界線最好落在合約上——落在別的地方,交接時就會發現有一段沒人認領。
這篇有兩類要交代的東西,形狀不一樣,不能混在一起講。
第一類:今天就存在的缺口。 上面那張驗收表沒有一格是「關閉」。G3 是開著的,而且明寫屬於客戶端;judge 的失敗路徑一次都沒跑過;transport span 代表不了完整的生成延遲;成功的 turn 也可能有沒進帳本的錢。這些不是待辦清單,是這個系統今天真實的形狀,寫在公開文件裡讓抄這套設計的人知道自己抄到了什麼。
第二類:搬出去之後的世界,本系列零證據。 沒有選過任何 gateway,沒有跑過叢集,沒有維運過治理平台。roadmap 上每一條寫的都是起點的證據——合約存在、測試在跑——而不是移動的結果。所以那份文件開頭就寫著:「Nothing below has been built or measured here.」
這兩類的差別是:第一類我知道它壞在哪,第二類我根本沒去過。把第二類寫得像第一類,就是把推測包裝成經驗——這是整個系列從 Day 1 就在避免的事。
主線到這裡結束。沒答完的問題變成了延伸題庫:Agent Framework 與 Semantic Kernel/AutoGen 的技術演進、MCP 在工具整合裡的位置、Azure AI Foundry 在 production lifecycle 的角色、、RAG 的四種失敗、混合檢索的取捨、用 behave 描述品質邊界,以及導入過程的 production lessons。
這裡要講精確一點,不然就變成我剛剛才批評的那種話。這些題目不是整片空白:RAG、混合檢索、Agent Framework、behave、ACA 部署、telemetry 在這 30 天裡都有實作、測試與量測。空白的是每一題最後要下的那個判斷——四種失敗各自的處置、混合檢索的取捨在什麼負載下翻轉。有幾題連底都沒有:MCP、Foundry lifecycle,本系列一次都沒碰過。
所以準確的說法是:每一題至少有一個問題還沒被驗證過。它們不是我還沒寫的題目,是我還沒有資格下結論的題目。
忍喵:30 篇了。值得慶祝的不是寫完,是每一條「沒做到」都還留在文件裡,沒被偷偷刪掉。
用到的 Azure 服務:
本篇無新增雲端資源、零 mutation、零成本:關於這個系統今天的樣子,證據都來自這個 repo 既有的程式碼、測試與前 29 天已記錄的量測,沒有任何一次新的雲端呼叫。搬出去之後的那些目標狀態則相反:它們沒有證據。
如果這 30 天只能留一句話,我會選這句:能搬走的,只有立過合約的那條邊界。
它同時是判準跟警告,而且兩邊都只講到起點——真正搬過去會遇到什麼,本系列沒有資格說。判準的用法是盤點:哪些邊界有被測試跑過的合約,那些就是 platform 化真正的候選。警告的用法是煞車:一個沒有合約的能力,不會因為被需要而變得可搬。
而合約不是為了搬家才立的。它是你在 Day 4 決定不要在 handler 裡寫 if use_fake、在 Day 8 決定 prompt 版本要能 fail-fast、在 Day 9 決定帳本跟 turn 一起提交、在 Day 22 決定型別註記不算邊界的時候,順手立下來的。今天有哪些邊界值得評估,是那些決定在 30 天後替你決定的。
本文由作者規劃與撰寫,AI(Claude)協助草稿整理與程式碼驗證;技術內容與觀點由作者確認並負責。