單元測全綠、Review 全過,功能卻沒上場:agentic 開發的三層驗證

前言:綠燈不等於對 我用一條 agentic 開發流水線,從零做完一個後台的「服務目錄」CRUD(Create/Read/Update/Delete,增刪查改)功能:一個內容管理系統(CMS)裡,管理員可以建立「服務」與「服務分類」,讓服務出現在使用者端 App 的列表裡。 結束時帳面很漂亮:單元測 772 全綠、build 通過、每個任務都過了兩段式 code review、最後還有一次整體 review。然後我把它部署到測試環境、用真實資料和真 App 一驗 —— 使用者 App 裡根本看不到剛建好的服務。 這篇要講的不是「怎麼修這個 bug」,而是一個更值得記下來的觀察:單元測、code review、端到端驗證,三者各自只抓得到「不同層級」的錯,缺一層就會放掉一整類問題。 開發流水線:每個任務都派新的 subagent 我用的是 subagent-driven development:先 brainstorm 出設計、寫成 spec、再拆成一份逐任務的 plan,然後每個任務派一個全新、無歷史包袱的 subagent 去實作。它做完後,再派兩個獨立 reviewer —— 先審「spec 合規」(有沒有照規格、有沒有多做少做),再審「程式品質」。 為什麼要派新的 subagent 關鍵在 context 乾淨。實作者只拿到「這個任務的完整描述 + 周邊脈絡」,不繼承我的對話歷史,所以不會被先前的決策慣性帶偏。reviewer 同理:它不知道實作者「想做什麼」,只能讀「實際寫出來的 code」對照規格 —— 這正是抓得到落差的前提。 主控者(orchestrator)唯一的工作是「精準地餵 context」與「把關 review loop」,不自己寫 code。這保住了主控者的 context,也讓每個 agent 都聚焦。 第一層:兩段式 review 擋下的真 bug 下面這些全部躲過了單元測(因為 mock 餵的是理想資料),是被 reviewer 讀 code 抓出來的。 雷 1:跟 API 要「只給名字」,結果連 ID 都不給了 存一筆「服務」時,要記錄它屬於哪個負責人,所以需要負責人的 ID。我為了省流量,跟後端說「負責人那欄,只給我名字就好」。後端很聽話 —— 只給了名字,把 ID 一起省掉了。 ...

June 19, 2026 · 2 分鐘 · Peter

Combining Strategic Cutover Docs with Tactical Headless Orchestrators

缺口:戰略協調有了,戰術執行還在手動驅動 上一篇講了一份 CUTOVER.md 怎麼當跨 session、跨 repo 的控制平面 — 用 8 個 pattern 解決「AI agent 有遺忘症、任務卻沒結束」的協調問題。那套機制解的是戰略層:哪些高階決策被做過、哪個方案被否決、誰改完了等誰配合。 但它有個明顯缺口。Cutover doc 追蹤的是「高階任務」這個粒度 — 例如「後端 dual-write regression 要修」是一個 row。可是這個 row 底下,真正的執行還是我一個 session、一個 session 手動驅動:開 session、貼 pickup prompt、看它跑、它卡住我再 nudge、跑完我再回來 push doc。 換句話說,戰略層自動化了(doc 自己會被各 session pickup),但每個高階任務內部的戰術 loop — 寫測試、跑、看結果、修、再跑 — 還是人在當 orchestrator。 Anthropic 自家在 Building effective agents 這篇 engineering 文章裡定義過一個叫 orchestrator-workers(orchestrator–工人 pattern — 一個中央 LLM 動態拆解任務、派發給 worker LLM、再彙總結果)的工作流:原文寫「a central LLM dynamically breaks down tasks, delegates them to worker LLMs, and synthesizes their results」。配上 Claude Code 的 subagents(子代理 — 由主 agent spawn 的獨立 agent,每個都有自己 isolated context window)跟 headless mode(非互動模式 — 用 claude -p 把 prompt 餵進去、跑完吐結果,不開互動對話),就能補上 cutover doc 的戰術缺口 — 也就是「單一高階任務底下,怎麼讓 AI 自己跑數小時的 autonomous loop(自主迴圈 — 不需要人在中間 nudge,loop 自己 plan / execute / evaluate 直到條件達成)而不撞牆」。 ...

May 22, 2026 · 11 分鐘 · Peter

Designing a Control-Plane Document for Async Multi-Session AI Agents

The Problem: AI Agents Forget, but the Task Doesn’t End 技術社群已經習慣「給人類看的 RFC」— Architecture Decision Records、incident postmortem、設計文件,模板都很成熟。但給 AI agent 之間用的非同步協作文件呢?少有公開討論。 最近處理一個跨三個 repo 的 schema migration:後端表結構改、前端 GraphQL 切換、行動端讀寫遷移,估計要 30+ 天,會跨越十多個獨立的 Claude session(不同時段、不同任務切片、不同筆電)。每次 session 結束、下一輪起來,前次的 context window 就消失了。問題不只是「上下文遺忘」,還包括: 上次 session 進度到哪?沒人記得。 之前否決過的方案?新 session 很容易「自信地重新提案」。 跨 repo 的「我改完了、等你那邊配合」訊息,怎麼跨越 session 邊界? 某次踩的坑,下次會不會重蹈? 我們最後長出一份 CUTOVER.md 放在獨立的 metadata repo,作為跨 session、跨 repo 的 out-of-band control plane。實戰一個月後,這份文件結晶出 8 個對 AI agent 特別有效的設計模式 — 它們幾乎都對應到分散式系統的經典概念。 The Architecture flowchart LR subgraph code[Code repositories] R1[backend repo] R2[frontend repo] R3[mobile repo] end subgraph plane[Control plane] DOC[CUTOVER.md<br/>append-only<br/>own repo] end subgraph time[AI sessions over time] S1[t1: backend session] S2[t2: frontend session] S3[t3: mobile session] S4[t4: backend session] end S1 -.modifies.-> R1 S2 -.modifies.-> R2 S3 -.modifies.-> R3 S4 -.modifies.-> R1 S1 -->|pull / push| DOC S2 -->|pull / push| DOC S3 -->|pull / push| DOC S4 -->|pull / push| DOC classDef p fill:#cce5ff,stroke:#007bff,color:#004085 classDef s fill:#d4edda,stroke:#28a745,color:#155724 classDef c fill:#f5f5f5,stroke:#333,color:#333 class DOC p class S1,S2,S3,S4 s class R1,R2,R3 c 每個 AI session 開場先 pull doc → 讀完上下文 → 完成任務 → push update。Doc 自己是獨立 repo,與三個 code repo 解耦,扮演純控制平面角色。 ...

May 22, 2026 · 6 分鐘 · Peter

Schema 主表翻轉的 dual-write 過渡:一場不能 stop-the-world 的搬家

引言:當業務主表需要翻轉 某個 SaaS 系統長期以「客戶資料表」為核心:所有訂單、文件、操作紀錄都用客戶 ID 當外鍵。但這個客戶表的資料來源是外部 POS 系統匯入,每天同步幾百筆,schema 由廠商定義。 業務發展後問題浮現:自己 SaaS 的會員表(users)才是真正的「人」 — 有登入、有偏好、有應用內行為。新功能(個人化推薦、訂閱管理、社交綁定)都需要以 users 為主軸。 於是有了一個經典的 schema migration 需求:主表翻轉(primary table pivot)。把業務邏輯的中心從 customers(外部 POS 匯入)轉到 users(SaaS 自家會員),但歷史資料、新進資料、系統相依、回滾風險全部都要顧到。 名詞解釋 開始拆解之前,定義幾個會反覆出現的詞: 詞 定義 主表翻轉(Primary table pivot) 業務主要 entity 從表 A 改成表 B Dual-write 寫入時同時寫舊欄位 + 新欄位,回滾安全 Partial cut-over 分階段切換,read 跟 write 不同步切 Hard cut-over(stop-the-world) 一次切完,downtime 短但 risk 大 Backfill 歷史資料補齊新欄位的 batch update Idempotent migration 重跑無副作用,cron / retry 安全 DISTINCT ON PostgreSQL 專屬去重 syntax,搭 ORDER BY 取每組第一筆 Pseudo entity 為了統一查詢介面而建的「假」實體 Link table ORM 多對多 join 表(user_id + entity_id) 為什麼選 dual-write 而非 stop-the-world 最直接的搬家方式是「stop-the-world cut-over」:選一個維護窗口,停寫入、跑 batch script、改完所有 reference、開機。但這個 SaaS 的條件不允許: ...

May 15, 2026 · 6 分鐘 · Peter

當授權資料不可信時,我選擇讓系統安靜地退後一步

延續上一篇的問題 在上一篇文章中,我們遇到了一個問題:Link Table 資料遺失導致使用者沒有角色,所有 API 都回傳 401。 文章最後我拋出了一個問題: 如果你的系統把角色資訊 cache 在 Redis、JWT claim、或 BFF 層,當 Link Table 資料不正確時,系統應該: 名詞解釋: Redis:一種高速的記憶體資料庫(In-Memory Database),常用於快取(Cache)熱門資料,避免每次都查詢主資料庫 JWT claim:JWT Token 內的資料欄位。例如把使用者角色直接寫在 Token 裡:{ "sub": "user_123", "role": "admin" },這樣就不用每次都查 DB BFF(Backend For Frontend):一種架構模式,在前端和後端 API 之間多一層「專為前端服務的後端」,常會在這層做權限快取 立即全站拒絕? 繼續相信 cache? 還是進入 degraded mode? 我的選擇是:進入 Degraded Mode。 這篇文章會解釋為什麼,以及如何實作。 一句話定義 Degraded Mode = 系統已知自己「部分不可信」,主動降級功能以維持安全與可用性。 不是壞了、不是裝沒事,而是: 我知道哪裡壞 我知道哪些功能不能給 我知道要保住什麼 為什麼不選另外兩個方案? ❌ 方案 A:立即全站拒絕 所有 API → 401/503 → 業務全掛 問題: 使用者完全無法使用系統 對業務衝擊太大 「寧可錯殺一百」的策略在商業系統中代價過高 適用場景: 金融交易、醫療處方等「錯了比沒有更糟」的場景 ...

January 20, 2026 · 4 分鐘 · Peter

同樣的功能,為什麼 Flutter 比 Vue 難 Debug 十倍?從實戰到架構的六層反思

前言:同一個功能,截然不同的 Debug 體驗 最近在維護一個同時有 Vue 前端和 Flutter App 的專案。兩邊都要實作「關於我們」頁面的選單過濾邏輯——根據不同情境顯示或隱藏特定項目。 Vue 那邊:兩天內改了十幾個 commit,每次都是小幅調整,順順地完成。 Flutter 這邊:卡了一整天,改了一個地方沒效果,懷疑方向錯誤,來回折騰。 同樣的業務邏輯,為什麼 debug 體驗差這麼多? 這篇文章從 debug 實戰出發,一路延伸到架構層面的反思。我們會探討 Domain Model 的防禦能力、Clean Architecture 的責任邊界、扁平架構的取捨、BFF 的可靠性價值,最後揭露這次 debug 困難的真正原因——交接代碼的信任陷阱。 Part 1:Debug 實戰 Vue:問題在 UI 顯示層 Vue 那邊的典型修正長這樣: // Vue - 在 computed 裡加一個 filter const filteredPageTabs = computed(() => { return response.value.pageTabs .filter(item => item.subtitle !== 'Service') .map(item => { if (item.subtitle === 'ABOUT_US') { return { ...item, subTabs: item.subTabs?.filter( subTab => subTab.title !== '醫療團隊' ) || [] } } return item }) }) 問題本質:資料從 API 回來是正確且完整的,只需要決定「哪些要顯示在畫面上」。 Debug 過程:打開 Vue DevTools → 看 store 資料 → 加個 filter → 完成。整個過程不超過 10 分鐘。 ...

January 4, 2026 · 5 分鐘 · Peter

為什麼技術選型 CMS 我要選 Strapi?2024 年中的預算與系統分析決策

引言:一個技術選型的起點 2024 年 6 月,我坐在會議室裡,面對著老闆和行銷總監,準備報告我對公司新系統 CMS 的技術選型建議。這不是一個輕鬆的決定——選錯了,可能浪費數百萬的開發成本;選對了,能為公司省下可觀的人力支出。 經過數週的研究與分析,作為一個架構規劃師同時也是技術決策者,我最終選擇了 Strapi 作為我們的 Headless CMS 解決方案。這篇文章將分享我的決策過程、考量因素,以及最重要的——這個選擇如何為公司省下大筆預算。 什麼是 Headless CMS? 在深入 Strapi 之前,先理解 Headless CMS 的核心概念。 傳統 CMS vs Headless CMS graph TB subgraph Traditional["傳統 CMS(如 WordPress)"] A[後端 + 資料庫] --> B[模板引擎] B --> C[HTML 輸出] C --> D[瀏覽器] end subgraph Headless["Headless CMS(如 Strapi)"] E[後端 + 資料庫] --> F[REST/GraphQL API] F --> G[Web App] F --> H[Mobile App] F --> I[IoT Device] F --> J[其他服務] end 傳統 CMS 將前端與後端緊密耦合,網站的外觀和內容管理綁在一起。而 Headless CMS 則專注於內容管理和 API 提供,讓前端團隊可以使用任何技術框架來消費這些 API。 ...

December 23, 2025 · 4 分鐘 · Peter

Swift Redux 架構完整指南:從 Reducer 到 Middleware 的狀態管理實踐

引言:為什麼需要 Redux? 在 iOS 開發中,隨著應用規模擴大,狀態管理逐漸成為最具挑戰性的課題。當多個 View 需要共享狀態、狀態變化難以追蹤時,應用很容易陷入混亂。 Redux 作為一種可預測的狀態容器,最早在 JavaScript 生態系中流行,如今也廣泛應用於 Swift/iOS 專案。本文將深入介紹 Redux 架構的核心觀念,包含: Reducer(減少器):狀態更新的核心邏輯 Store(儲存區):應用的單一狀態來源 Action(動作):描述「發生什麼事」的指令 Middleware(中介層):處理非同步與副作用 Redux 核心架構概覽 架構組成 flowchart TB View[View / SwiftUI] Store[Store<br/>單一狀態來源] Reducer[Reducer<br/>純函數] State[State<br/>應用狀態] Action[Action<br/>動作描述] Middleware[Middleware<br/>非同步處理] View -->|1. dispatch| Action Action -->|2. 觸發| Middleware Middleware -->|3. 可能派發新 Action| Action Action -->|4. 傳遞| Reducer Reducer -->|5. 計算| State State -->|6. 更新| Store Store -->|7. 觀察 @Published| View style Store fill:#4ade80,color:#000 style Reducer fill:#60a5fa,color:#000 style Middleware fill:#f97316,color:#000 架構特性: ...

August 21, 2025 · 12 分鐘 · Peter

RxSwift -bindViewModel

第69行cell.configure一直進不去 RxSwift -bindViewModel 第69行cell.configure一直進不去 原因如下: required init?(coder aDecoder: NSCoder) 是 UIKit 控制器初始化的一部分,尤其是從 Storyboards 或 Xibs 中加載時。如果你正在嘗試從 Storyboard 或 Xib 中加載你的 ZooViewController,那麼就需要實現這個初始化方法。在你的程式碼中, required init?(coder aDecoder: NSCoder) { fatalError(“init(coder:) has not been implemented”) }你用 fatalError("init(coder:) has not been implemented") 拋出了一個執行時期的錯誤。 如果你確實不需要從 Storyboard 或 Xib 加載,你可以避免使用 ZooViewController 作為 Storyboard 或 Xib 的類別,或者避免透過 Storyboard 或 Xib 實例化 ZooViewController。在這種情況下,你的 init(coder:) 方法將永遠不會被呼叫,因此你的 fatalError 也不會觸發。 如果你需要從 Storyboard 或 Xib 加載 ZooViewController,那麼你需要提供 init(coder:) 方法的實現。這可能意味著你需要提供一個預設的 ZooViewModel,或者提供一種方式來設定 ZooViewModel,如下所示: required init?(coder aDecoder: NSCoder) { self.viewModel = ZooViewModel() super.init(coder: aDecoder) }最後從Main.storyboard的viewController冠上我要的ZooViewController, 問題就解決了.

July 21, 2023 · 1 分鐘 · Peter

Flutter 狀態監聽完全指南:Provider、Bloc、Riverpod、GetX 深度比較

前言:Flutter 狀態管理的核心挑戰 在 Flutter 開發中,狀態管理是最重要也最複雜的主題之一。當應用程式規模增長,Widget 樹層級加深,如何讓不同層級的 Widget 能夠正確地監聽和響應狀態變化,成為每個 Flutter 開發者必須面對的挑戰。 選擇錯誤的狀態監聽方式,會導致: ❌ 不必要的重繪:整個 Widget 樹被重建,效能下降 ❌ 記憶體洩漏:忘記釋放監聽器,導致記憶體持續增長 ❌ 狀態不同步:多個 Widget 顯示不一致的資料 ❌ 程式碼難以維護:狀態邏輯散落各處,難以追蹤 本文涵蓋內容 本文將深入探討 Flutter 四大主流狀態管理方案的監聽機制: Provider:Flutter 官方推薦的輕量級狀態管理 Bloc:基於 Stream 的企業級狀態管理 Riverpod:Provider 的改進版本,解決了 Provider 的核心問題 GetX:高效能的響應式狀態管理 我們將從實際問題出發,比較這四種方案的: 🔍 核心原理:底層如何實現狀態監聽 💻 實戰範例:可執行的完整程式碼 ⚡ 效能表現:重繪範圍、記憶體使用 🎯 適用場景:什麼情況下使用哪種方案 ⚠️ 常見陷阱:實戰中容易踩的坑 Provider 狀態監聽機制 Provider 是 Flutter 官方推薦的狀態管理方案,基於 InheritedWidget 實現。它的核心優勢是簡單易用,適合中小型應用。 Provider 核心概念 Provider 使用 依賴注入 (Dependency Injection) 和 作用域 (Scope) 的概念來管理狀態: flowchart TB App[MyApp] --> P1[ChangeNotifierProvider] P1 --> Scope1[作用域 1] Scope1 --> Route1[Route 1] Scope1 --> Route2[Route 2] Route1 --> W1[Widget A<br/>context.watch] Route1 --> W2[Widget B<br/>context.read] Route2 --> W3[Widget C<br/>❌ 無法存取] P1 -.->|提供狀態| W1 P1 -.->|提供狀態| W2 style P1 fill:#4ade80,color:#000 style W1 fill:#60a5fa,color:#000 style W2 fill:#60a5fa,color:#000 style W3 fill:#ef4444,color:#fff 關鍵概念: ...

July 13, 2023 · 18 分鐘 · Peter