分類: 💻 技術實踐

  • 我的家庭照顧 14 個 HTML 工具:dementia-care monorepo + 實戰使用指南

    重點摘要

    • dementia-care monorepo 是 14 個 HTML/Python 工具的 personal toolkit:圍繞「家人照顧」這件事,自家用為主,朋友(藥師)+ 社區照護者也用得上。
    • 起源:照失智媽媽 2 年急速進展 + 育兒 2023 年出生女兒,前面有 15 年照爸爸 stroke 失能 secondary 經驗。每天遇到 friction 就寫一個小工具。
    • 上位 mission:「讓整個家忙起來」(ambient engagement,Marx & Cohen-Mansfield 2010 循證概念)—— 工具不是治療,是降低照護者啟動 friction。
    • 共用 spec:純前端、單檔 HTML、0 CDN、0 build、離線可用、繁體中文、大字大按鈕、SVG emoji favicon、GitHub Pages 部署。
    • 5 條自我約束(2026-04-28 後):凍結新 sub-project 60 天 / commit cap 20/天 / 每週一個下午全關 / 0 CDN 機器化守門 / 不主動推工具只分享方法。
    • 跨專案 lessons:純黑白對白內障是錯的 frame、圖書館 ≠ 景點 precision contract、Google Maps fallback default pin 陷阱、Selenium 驗證 self-use 例外、物件 literal 尾逗號讓整頁 blank。

    dementia-care monorepo 是 14 個圍繞家庭照顧的 HTML/Python 工具集,過去半年累積。為什麼會有這個 repo?照失智媽媽,順便做給太太、藥師朋友的太太、社區照護者用 ——「自家工具集 + 經驗分享」,不是 community product。

    這篇是這個 monorepo 的全面整理:14 個工具的分工、起源故事、共用設計原則、5 條自我約束、5 條跨專案踩坑 lessons。給想做類似 personal toolkit 的人參考,也給之後的我自己回頭看為什麼這樣設計。

    為什麼會有這個 monorepo?

    結構上設計給:白天只有你跟長輩、家屬 backup 0、商業 app 都假設你有時間引導。我家 timeline:

    • 15 年照父親 stroke 失能(secondary,媽媽是 primary,2024 過世)
    • 2024 起媽媽輕度失智 → 2 年急速進展到中重度
    • 2023 出生女兒進入 toddler 階段
    • 太太是藥師、有正職,白天無法 backup
    • 過去半年我變 primary caregiver

    每天遇到 friction(媽媽問同樣問題第 50 次 / 居服員交班沒交集 / 回診忘記想問什麼 / 女兒週末沒地方去) —— 直接寫一個小工具,不寫 spec、不開會、不等 backlog。半年累積 14 個。

    上位 mission 是「讓整個家忙起來」。對應的循證概念是 ambient engagement(Marx & Cohen-Mansfield 2010 “Engagement of Persons with Dementia”):環境引發的活動 vs 人引發的活動,失智長輩參與有意義活動的「量」與認知衰退斜率負相關。工具不是治療,是降低照護者啟動 friction —— 讓 engagement 在沒人陪伴的疲累照護日實際發生。

    14 個工具分四大類

    🧠 失智照護生態系(主線資料閉環)

    工具 類型 說明
    陪伴小幫手 v1 互動遊戲 15 個認知訓練遊戲,8 級難度滑桿自動選 10 個顯示
    陪伴小幫手 v2 試驗版 推薦式首頁 + 照護者陪伴指南 + 今日摘要分享
    白板 OCR Bot Telegram Bot 每日白板照 → Gemini 3 Flash(box-index 策略,磁鐵在第幾格)→ iDempiere Z_momSystem
    就診小幫手 Web App 回診前 prep:抓 iDempiere 異常、症狀分類、診間錄音

    👶 兒童 + 親子

    工具 類型 說明
    小朋友學習樂園 學齡前互動 1-6 歲 23 個活動,4 年齡層自適應深度
    週末出遊地圖 親子景點 全台 22 縣市 850+ 景點 + 多點路線規劃
    托嬰幼兒園手冊 方法論 0-6 歲托嬰中心 + 幼兒園選擇方法論(4 種類型對照,評估 SOP)

    📖 生活方法論手冊

    手冊 主題
    care-handbook 長照手冊:找居服員 / 日照 / 喘息 / 機構 / 看護工 / 失智專屬支援系統
    home-handbook 家中照護手冊:13 項物理改動 + 早午晚 SOP + 援手系統 awareness
    home-handbook-classic 家中照護手冊舊版(保留作對照,新版改用 home-handbook)
    garden-handbook 家庭園藝手冊(可食 + 觀葉 + 觀花 + 根莖類,陽台/窗邊/室內)— 對失智長輩是 behavior redirection 工具
    pet-handbook 家中寵物生活手冊(貓/狗/兔/鳥/魚/烏龜)—「會主動靠近的家人」,失智長輩 + 小孩雙受惠
    mindset 心法手冊 — 6 條視角(失智不是失能 / 用加法對抗減法 / 躁動不是無聊 / 動線是功能可及性 / 誠實版優於善意包裝 / 手冊是網狀不是清單)

    🧃 飲食 / 健康

    • health-drinks:**19 款醫療營養品 + 嬰幼兒配方 + 高蛋白補充品**並排比較(180-280ml 容量範圍。三重使用者:Tom 自家、藥師朋友、藥局客戶)

    失智照護主線:資料閉環長什麼樣?

    14 個工具裡 4 個是失智照護核心,串成一條 daily/weekly 閉環:

    [居服員 / 我]
        ↓ 拍白板照傳 Telegram
    whiteboard-ocr-bot
        ↓ Gemini 3 OCR + 人工 confirm
    iDempiere Z_momSystem(每天一筆紀錄)
        ↓ REST API
    mom-clinic-companion
        → 回診前產出「今天要問醫生的 3 件事」

    白板每天記錄媽媽 餐食 / 用藥 / 行為事件 / 睡眠時數。OCR Bot 把照片轉成結構化資料寫進 iDempiere(自家 ERP-as-PHR,Z_momSystem 表 12 欄位)。回診前 mom-clinic-companion 比對「上次回診 → 今天」vs「上上次回診 → 上次回診」兩個 window,抓變化顯著項目給神經科醫師看具體事件,不是「最近狀況不太好」這種模糊描述。

    這條主線運轉半年,媽媽每月回診從「我也不知道要說什麼」變成「上週三晚上她突然找不到回家的路」這種具體 evidence。神經科醫師根據具體事件調藥,比根據家屬情緒描述準確很多。

    共用設計原則 — 為什麼都是單檔 HTML?

    14 個工具共用同一套 spec:

    • 單檔 HTML:HTML + CSS + JS 全塞 `index.html`,不拆檔。對 backup / 修改 / 分享(直接寄一份)極友善。
    • 離線可用:所有資源 inline(SVG favicon、base64 圖、不依賴 CDN)。山區農場、長輩家 wifi 弱 都能跑。
    • 0 CDN 強制:禁止任何 fonts.googleapis / cdnjs / unpkg / jsdelivr / Google Analytics / 外部 script。原因:使用者打開網頁時瀏覽器會從使用者裝置直接發請求到 CDN 伺服器,洩漏 IP + User-Agent + 時間給第三方,違反 COPPA(兒童)/ GDPR-K / 個資法。有專屬 CI workflow `monorepo-cdn-ban.yml` 機器化守門(2026-04 加),違反即擋 push。**Caveat**:`health-drinks` 因為較早期建置仍用 Chart.js + Google Fonts CDN,是 monorepo 唯一例外,計畫之後遷移成 inline。
    • 0 build step:不進 npm、不進 webpack。寫完開瀏覽器看,改完 commit push 30-60 秒 GitHub Pages 部署。
    • 繁體中文 + 大字大按鈕:觸控友善,最小點擊區 48×48px。
    • SVG data URL emoji favicon:離線也有 tab icon。

    低視力對比:原本以為「純黑底 + 純白字 = 最大對比 = 最適合低視力長輩」,後來發現對中度白內障(monorepo 主場景)會因 forward light scatter 引發 halation/glare —— 21:1 是 over-shoot,不是越高越好。Evidence-based 替代:米白底深字(#222 on #f5f5f5)/ 深底淺字(#e8e8e8 on #1a1a1a)/ 字級 ≥ 24px / line-height ≥ 1.6。維持 WCAG AAA(7:1+)但消除極端 glare。

    5 條自我約束(2026-04-28 後加)

    跨 6 domain expert review 共識項。寫進 CLAUDE.md 變強制規則,我自己做時也守:

    1. 凍結新 sub-project 60 天(到 2026-06-30):14 個已過載,主軸佔比 < 35%。6 個月 450+ commits(`git log –since=’2025-11-01’`),「持續產出 = 情緒迴避」紅旗。例外:只允許 archive / 整併 / 刪除。解凍後若仍想開新工具,先在 blog 寫一篇「為什麼這值得開一個工具」,沒寫完不開。
    2. Commit cap 20 / 天 (soft warning):超過 20:pre-commit hook 跳對話框問「今天你媽吃幾餐、女兒抱了你幾次、你笑了幾次」。失智照護者 burnout 是 monorepo 結構性 SPOF —— Tom 倒下 = mom-clinic 朋友家屬可能誤診 = 整個生態系凍結
    3. 每週一個下午全關:不寫 code、不寫 blog、不規劃 feature。陪女兒、發呆、睡覺都行。「讓整個家忙起來」mission 不該包含 Tom 自己一刻不停。
    4. 0 CDN 機器化守門:`.github/workflows/monorepo-cdn-ban.yml` 自動掃所有 *.html。違反即擋 push。不再靠人記得。
    5. 不主動推工具,只分享方法:工具是 personal toolkit,維護優先順序看當下需要。不主動推給陌生使用者(避免讓人對「會被維護」產生錯誤期待)。朋友(藥師)是「資訊交流」不是「使用者」,不發配工具帳號。方法寫成 blog,blog 才是擴散渠道(這篇就是)。

    5 條跨專案踩坑 lessons

    1. 純黑白對白內障是錯的 frame

    「最大對比 = 最適合低視力」是錯的。對白內障(monorepo 主場景)forward light scatter 引發 glare,21:1 反而降可讀性。WCAG 21:1 是 物理可量化(luminance ratio),「適合低視力」是 臨床效果(可讀 + 不疲勞 + 不誘發 glare),兩者不是同一件事。Evidence-based:米白底深字 / 深底淺字 / 字級 24px+ / line-height 1.6。

    2. 圖書館 ≠ 景點:precision contract

    不同 place category 對 precision 容忍度不同。景點(公園 / 山林 / 老街)area centroid 1-2 km 噪音 OK(本身就大);圖書館 = 單一建築,1 km 偏差 = 不同樓棟 = 錯,不能套同一條 fallback 規則。對「single building」類,寧可 entry 數量少(精確) 也不要 area centroid(誤導 user)。

    3. Google Maps fallback default pin 陷阱

    Google Maps geocoding 找不到地址時不會回 error,而是 silent fallback 回最後一個成功 query 的 cached coord。kids-weekend 跑 23 筆 Playwright 有 3 筆全部 fallback 到同一個 [24.9790, 121.4579]。必跑 sanity gate:縣市 bounding box 比對。任何 geocoding pipeline 必須有 downstream sanity check —— 不能信「200 OK + 有 @lat,lng」就當作對的。

    4. Selenium 驗證 self-use 例外 ≠ photo scraping hard violation

    Google Maps Platform ToS 是合約不是法律,enforcement profile 對「自用 / 非商業 / 中等規模」幾乎為零。判斷不是 binary —— 看(1) self-use vs 商業化 (2) 一次性 vs 持續性 (3) 文字 vs 照片 三維度。kids-weekend 圖書館 geocode 落在「self-use + 一次性 + 文字座標」三個都 OK 的格子,屬技術性 ToS issue。但 photo extraction + redistribute 仍是 hard violation(著作權法 §91 §92),商業化應升級付費 API。

    5. 物件 literal 尾逗號讓整頁 blank

    2026-04-21 kids-companion 整頁 blank 事故:新增 IMG 物件忘記前一筆尾逗號,SyntaxError 整頁掛。單檔 HTML 沒 webpack 兜住,JS 一錯整頁 die。寫 lint script (`scripts/lint_places.py`)機器化檢查每筆 entry 結尾。

    為什麼不對外推銷?

    因為這是 tier 2 personal toolkit 不是 community product。三個原因:

    1. 維護順序看自家當下需要:某天我家 schedule 變、媽媽病程變、女兒入學,工具優先順序就變。對外推銷會建立「會被維護」期待,實際上我可能突然停某個工具 —— 對 user 不公平。
    2. SPOF 問題:14 個工具只有我一個 maintainer,我倒下整個 stop。對外推 = 對外承諾,承諾兌現不了 = 信譽損傷。
    3. 方法 vs 工具:方法可以用文字傳承,擴散性高;工具 binding 在我家 schedule + 我家硬體 + 我家數據格式。方法寫成 blog,工具留給有能力 fork 改的人。

    朋友(藥師)是「資訊交流者」不是「使用者」 —— 我跟她討論失智用藥,她跟我討論病人家屬,但我不發給她工具帳號讓她依賴我的伺服器。她想用就 fork,改成她家用。

    給想做類似 personal toolkit 的人的建議

    1. 明確 scope = self-use:這個 frame 鎖死所有設計決定 —— 不做 user account、不做 paywall、不對外推銷、不收 review。Scope 一變,所有 compliance / privacy / 合規 trade-off 全變。
    2. 單檔 HTML 是 personal toolkit 的甜蜜點:0 build / 0 server / 直接寄一份檔案 / 純前端 / 離線 / GitHub Pages 免費部署。技術簡單到不會卡住做事的人。
    3. 0 CDN 是隱私底線:任何外部 fonts / analytics 都洩漏 user IP。家庭應用(兒童 / 醫療 / 失智)不能讓 Google 知道你家在訪問什麼工具。寫 CI workflow 機器化守門,不靠人記得。
    4. 每個工具對應一個 friction:不要先想「我要做什麼工具」,要想「我每天卡在什麼」。卡在交班 → OCR Bot;卡在回診 → mom-clinic;卡在週末 → kids-weekend。
    5. 政府開放資料 > 商業 API scraping:libstat / data.gov.tw / TGOS 對台灣應用是乾淨 source,合規且持久。每個 domain 找對應的政府 source(圖書館 / 醫院 / 學校 / 古蹟)。
    6. self-use 工具不是免責令:Photo extraction、持續性 production scraping、商業化 仍是 hard violation。判斷 framework 看 (1) self-use vs 商業 (2) 一次性 vs 持續性 (3) 文字 vs 照片 三維度。
    7. 給自己自我約束:照護者 burnout 是結構性 SPOF。commit cap、每週全關一下午、凍結新 sub-project,這些不是「強制症」是「對抗持續產出 = 情緒迴避」的 hard rule。

    —— 工具實戰判斷與用法 ——

    三層分工:Handbook、工具、專業

    使用 monorepo 之前,先建立三層分工的觀念:

    作用 對應 monorepo 頻率
    Handbook 認識系統、申請流程、SOP 學習 care-handbook / home-handbook / childcare-handbook 階段性(剛確診、要申請居服員)
    工具 每日操作、降低啟動 friction 陪伴小幫手 / OCR Bot / mom-clinic / kids-weekend 每日 / 每週
    專業 診斷、用藥、急救、法律 神經科 / 精神科 / 1966 / 1925 / 119 / 共照中心個管師 回診 + 突發事件

    實戰原則:剛確診先讀 handbook(2-3 天密集吸收),3 個月後 handbook 慢慢變參考書,工具變每日操作,專業在事件發生時 escalate。

    7 個照顧場景對應工具速查

    場景 主要工具 配合 handbook
    失智剛確診那週 無工具(handbook 為主) home-handbook P0 + care-handbook 申請流程
    居服員每天交班 白板 OCR Bot home-handbook 居家日 SOP
    媽媽情緒不好想做點事 陪伴小幫手 v2(推薦)/ v1(自選)
    回診前一晚 mom-clinic-companion
    週末帶孩子去哪 kids-weekend(住哪 → 路線)
    孩子在家想做活動 kids-companion(2-6 歲 4 年齡層自適應) — (kids-companion 自己分齡)
    第一次申請長照 無工具(handbook 為主) care-handbook 7 步 SOP + 援手系統

    場景 1:失智剛確診那週 — handbook 為主

    剛確診那週不要急著上工具。先做 5 件事(home-handbook P0 章節):

    1. 法律 P0:輔助宣告 / 監護宣告 / 預立醫療決定 — 趁失智長輩仍有意思能力時辦,過了會難辦
    2. 醫療 P0:確認確診醫院 + 預約共照中心 + 申請重大傷病卡
    3. 防走失:愛心手鍊 / GPS / 鄰里通報網
    4. 物理改動 13 項(home-handbook 列清單):防滑地墊 / 衛浴扶手 / 廚房瓦斯總開關 / 大門感應…
    5. 申請長照:1966 → 照管專員到府評估 → 失能等級 → 服務媒合(care-handbook 7 步 SOP)

    這週不要碰工具。一邊吸收 handbook,一邊處理法律醫療 P0,一邊跟家人討論分工。工具是後面 2-3 個月開始有居服員 / 日照排班後才會用得上。

    場景 2:居服員每天交班 — 白板 OCR Bot

    白板 OCR Bot 解決交班斷層:居服員早上來、下午走;家屬晚上接手 — 中間 8 小時居服員紀錄不傳給家屬,神經科回診也說不清楚。

    實際流程:

    1. 家裡放一塊白板,居服員每天用筆寫紀錄(餐食 / 用藥 / 行為事件 / 睡眠)+ 用磁鐵標記固定欄位的選項
    2. 居服員下班前 / 我經過時拍照傳 Telegram bot(`telegram_bot.py`)
    3. Bot 走 `ocr_pipeline.py` → Gemini 3 Flash Preview API,**用 box-index 策略**(磁鐵在第幾格)避開手寫中文字元 OCR — human-as-verifier 設計
    4. 手機收到 bot push 訊息「請確認」,3 秒過目即可。錯了直接改文字回傳
    5. 自動寫入 iDempiere Z_momSystem(每天一筆 row,12 個欄位)
    6. 家屬晚上看 iDempiere 知道白天發生什麼,銜接夜間照護

    另也支援 📝 文字訊息 → 加時間戳(早 / 晚 / 大夜班)append 到當天備註欄;`/status` `/today` 指令查當天/累積狀態。`whiteboard-ocr-bot/index.html` 是純前端 demo 版本(模擬對話流程,不連網不傳資料),正式版要 Python + iDempiere REST API 跑在自家 server。

    關鍵 design:box-index 策略 + human-as-verifier 不是全自動。Gemini OCR 對手寫長輩字跡不一定 100%(尤其用藥名稱),所以白板用固定欄位 + 磁鐵 — Gemini 只要識別「第幾格有磁鐵」這種離散 boolean,不用識別自由手寫,大幅降低錯誤率。

    場景 3:媽媽情緒不好想做點事 — 陪伴小幫手 v2

    陪伴小幫手有 v1 / v2 兩版本並存:

    情境 用 v1 還是 v2?
    媽媽心情好,自己想挑 v1(10 格遊戲卡讓她挑)
    媽媽疲累 / 不擅選擇 / 照護者也累 v2(一鍵推薦「今天一起做這個好嗎」)
    想知道對長輩說什麼話帶遊戲 v2(每遊戲 3 條陪伴指南)
    想記錄今天玩了什麼貼 LINE 給家人 v2(每日摘要一鍵複製)

    v2 推薦邏輯:依時段(早 / 午 / 晚 / 深夜)+ 最近玩過避免重複 + 當前難度自動挑一個遊戲。次要選項「換一個 / 我想自己選」字級縮小、無底色,降低照護者選擇癱瘓 — 是給疲累照護者的設計。

    難度滑桿 1-8 對應方式(`v1 CLAUDE.md` 規格):

    • 1-2 輕度:遊戲篩選後較難(辨識變化小、選項多),語音只朗讀題目
    • 3-6 中度:遊戲難度中等,語音朗讀題目 + hover 選項唸出
    • 7-8 重度:遊戲篩選後最簡單(選項少、對比大),語音朗讀題目 + 所有選項,重複一次

    遊戲庫(`GAME_LIBRARY`)15 個遊戲每個有 `min`/`max` 範圍,`selectGames(level)` 從符合範圍的遊戲中隨機抽 10 個顯示首頁。選擇難度時看當週實際狀況試,不是越難越好 — 失智長輩做不到會挫折,做太簡單會無聊

    場景 4:回診前一晚 — mom-clinic-companion

    失智回診最大的問題是「**最近狀況不太好**」這種家屬模糊描述,神經科沒辦法調藥。mom-clinic-companion 讀 iDempiere `Z_momSystem` 表(白板 OCR Bot 寫入的),把 raw data 歸納成照護者可以記住、可以問的內容。

    實際 4 個 sections(`mom-clinic-companion/index.html` H2):

    1. 🚨 值得跟醫生討論的 — 規則引擎自動比對「上次回診 → 今天」vs「上上次回診 → 上次回診」兩個 window,找變化顯著項目
    2. 💭 這一個月媽媽發生過 — 從白板 OCR Description 欄抓症狀關鍵字
    3. 📋 這次要問醫生 — 從上面兩區一鍵「加進問題」,手動補其他
    4. 🎙️ 診間錄音 — 醫師講話 STT 轉字,回家同步回 iDempiere

    核心設計是回診日期感知:`APP.visits` 是回診日期 array,`computeWindows()` 產生最新兩段比對區間 — 因為實際回診頻率不規則(通常每月,有時兩週),按日曆切會不準。

    場景 5:週末帶孩子去哪 — kids-weekend(MAP 工具)

    kids-weekend 是 monorepo 裡最大的 MAP 工具:全台 22 縣市 851 筆親子景點 + 路線規劃(`STATE_VERSION` 3,單檔 HTML ~700KB)。解決的具體問題是「從家出發 X 公里範圍內,有什麼順路可以串成一日」 —— 媽媽社團、IG 收藏、Google Maps 我的清單,都做不到「按距離 + 順路 + 一鍵多點導航」這件事。

    1 題 wizard 是 deliberate 砍簡 — 從 4 題到 1 題

    2026-04 砍簡前,wizard 有 4 題:住哪 + 天氣 + 年齡 + 範圍。實測發現 user(太太、藥師朋友)80% 都直接 next 跳過後 3 題:

    • 天氣晴雨:家長自己抬頭看,問了多餘
    • 年齡:每筆景點 entry 已有 `ages: [‘0-2′,’2-3′,’3-6’]` 標籤,filter UI 之後再勾就好
    • 範圍:**家庭多在 20 km 內**(自家 + 朋友家實測 4 個月),預設 20 km 一拉就動

    所以剩 1 題:**「住哪?」**(GPS 一鍵 / 縣市 + 區下拉)。設完直接看推薦清單,範圍可拉滑桿改 5-400 km。砍題目比加 feature 更重要 —— personal toolkit 的設計訓練。

    實際 6 步使用流程

    1. 設家 — 第一次選,之後 `localStorage[kidsWeekendState]` 記住。GPS 一鍵會 reverse geocode 成「📍 新北土城」(不顯示醜的經緯度)
    2. 看推薦清單 — 預設 20 km 內景點,室內/戶外/觀光工廠/博物館/公園/海邊/廟宇/老街全混合
    3. 篩 filter — 晴天/雨天/0-2 歲/室內/避開人多/避開假日塞車黑名單
    4. 勾比較籃 — 1-3 個目的地加入比較籃
    5. 看路線預覽 — corridor 演算法找順路景點(預設 5 km corridor,`STATE.preferences.corridorWidthKm`)+ 計算多點總公里 + 拖 ⋮⋮ 換站順序
    6. 一鍵 Google Maps 多段導航 — 直接丟 Maps app(`maps.google.com/dir/?…` URL),不用手動複製貼

    實例(README 範例):「搜尋觀音山設目的地 → IKEA 新莊距路線 0.44 km ➕ 加入 → 五股 ➕ 加入 → 想加林口三井但 5.13 km 擦邊 → corridor 拉到 6 km → 拖 ⋮⋮ 把林口移到第 1 站 → 一鍵 Google Maps」。

    Place precision contract:景點 vs 圖書館 fallback 策略

    kids-weekend 的距離計算 priority(`getPlaceCoords()` line 2354):

    1. 先查 `PRECISE_COORDS[place.id]` — 有就用
    2. 沒有 → fallback `getCoordsFromArea(place.area)` 取「縣市 + 區」中心點(`TAIWAN_DISTRICTS` lookup,286 個區 centroid)
    3. 都沒有 → null,distance filter 排除

    但圖書館不能 fallback 區中心(本文上方「跨專案 lesson 2」)。圖書館是單一建築,1 km 偏差 = 不同樓棟 = 錯。所以 2026-04-30 圖書館 wave A→C 擴點時,**沒有精確 coord 的圖書館直接 drop,不入庫**(reject 連江縣文化處 / 桃園兒童文學館 / 高雄新興閱覽室 3 筆,因為 Google Maps fallback 到 default pin)。

    Metadata flag 系統 — 怕感冒 / 怕塞 / 失智注意

    每筆景點除了基本欄位(name / area / ages / types / hours / website),還帶 metadata flag 給推薦邏輯參考:

    Flag 含意 影響
    `elder_friendly: true` 長輩友善(平緩、無障礙、有座位) 失智 + 親子混合家庭優先推薦
    `weekend_jam: true` 假日塞車黑名單(陽明山 / 九份 / 淡水) 勾「避開塞車」會排除
    `dementia_caution: true` 失智長輩慎入(動物園 / 大型賣場 / 高 crowd 易迷路) 混合家庭手動觀察
    `crowd: ‘low|mid|high’` 人潮 enum UI「🤧 避開人多景點」剔除 high

    851 筆景點怎麼累積 — 5 wave + 圖書館 wave A→C

    景點不是一次抓進來的,分 wave 增量。每 wave 對應「家附近還缺什麼類型」的 surface gap:

    Wave 主題 +景點
    v3.11 base 親子館 / 公園 / 觀光工廠 主軸 ~770
    v3.12 0-2 歲友善 補嬰幼兒場域(托嬰中心 / 兒童美術館) +8
    v3.13 觀光工廠 DIY 體驗 + 雨天首選 +12
    v3.14 露營區 Glamping + 渡假農場 +10
    v3.15 雲嘉南高屏 南台灣覆蓋補齊 +14
    圖書館 wave A→C(2026-04-30) 縣市總館 + 親子閱覽室 + 文學/繪本 +33

    圖書館 wave 走獨立 pipeline:libstat 國家圖書館抓政府公開資料 → OSM Overpass strict verify(`amenity=library` POI 全台 853 個)→ Google Maps Selenium geocode 補不足 → 縣市 bbox sanity 過濾 → 通過才入庫。三條 source 各補不同覆蓋率,合規(政府開放資料 0 ToS)+ 精確(building-level coord)。

    每個 wave 的關鍵 lesson:**對應實際 user gap,不盲目擴量**。盲目擴點會稀釋信號(user 搜「週末去哪」跳出一堆樓上閱覽室,反而難用)。圖書館 wave 砍 46 筆閱覽室因為「樓上小空間 ≠ destination」,只進 33 筆 high-value。

    場景 6:孩子在家想做活動 — kids-companion

    kids-companion 是 2-6 歲兒童平板學習 app,核心設計是「自適應深度 (Adaptive Depth)」 —— 同一內容,不同年齡,不同深度。4 個年齡層各有版本:

    • toddler(幼幼班 1-2 歲):選項 2、語音題目+選項重複、目標字 140px、無拖拉
    • small(小班 3-4 歲):選項 2-3、語音題目+選項、目標字 120px
    • middle(中班 4-5 歲):選項 3-4、語音題目+hover、目標字 100px、可拖拉
    • large(大班 5-6 歲):選項 4-6、只朗讀題目、目標字 80px

    使用流程:設角色 emoji + 年齡層(toddler/small/middle/large) → 首頁活動卡 → 系統依年齡層自動調選項數 / 字級 / 互動方式。設計哲學在 brain memory `project_kids_companion_philosophy.md`。

    跟陪伴小幫手 v1/v2 關鍵差異:Web Speech API pitch 設定不同。kids-companion `pitch: 1.2`(高頻活潑感吸引兒童);陪伴小幫手 v1/v2 都已校正成 `pitch: 0.95`(老年聽力 presbycusis 高頻損失,低頻反而清楚)。同 API 不同設定,因為使用者生理不一樣。

    場景 7:第一次申請長照 — care-handbook 7 步 SOP

    長照申請新手最大的痛點:不知道要打給誰、要準備什麼、評估會問什麼。care-handbook 直接給 7 步 SOP:

    1. 打 1966(長照專線,免費)→ 報長輩姓名 + 縣市 + 失智症
    2. 等照管專員到府評估(通常 1-2 週內)
    3. 準備評估會問的:長輩 ADL/IADL 量表自評 + 失智診斷書 + 重大傷病卡(如有)
    4. 失能等級判定(2-8 級)→ 對應給付額度
    5. 服務媒合(居服員 / 日照 / 喘息)→ 排面試
    6. 居服員 / 日照面試 SOP:重點問題、紅旗信號、簽約注意事項(handbook 列詳細 checklist)
    7. 失智專屬支援:申請共照中心 + GA07 家屬訓練諮商給付碼 + 巷弄失智據點(這條在 care-handbook 獨立章節,大多家屬不知道)

    handbook 不取代 1966,是讓你打 1966 之前知道要問什麼。直接打 1966 也可以,但你會發現照管專員講的有些聽不懂(BA / CA / GA01-07 給付碼),先看 handbook 心裡有底再打。

    場景 8:選托嬰中心 / 幼兒園 — childcare-handbook

    childcare-handbook 是托嬰中心 + 幼兒園選擇方法論手冊(0-6 歲),不是「找最便宜」「找最近」這種比價工具,而是教**怎麼系統性評估** —— 在報名截止前知道「該看哪些點、該準備哪些東西、該追蹤哪些時間」。

    4 種托育類型(0-2 / 2-6 歲分流)

    • 托嬰(0-2 歲)3 類型:公托 / 準公共 / 私立
    • 幼兒園(2-6 歲)4 類型:公幼 / 非營利 / 準公共 / 私立

    5W1H 評估方法論(Who / What / When / Where / Why / How)

    Handbook H1「🎯 5W1H 評估方法論」拆 5 個 H2:你家是誰 / 選哪一類 / 什麼時候動 / 距離與動線 / 決策框架。每個維度有具體 checklist。

    核心 family mode:`sandwich`(三明治世代) — 全域狀態 `STATE.familyMode` 有 4 個值:`normal` / `sandwich`(同時照顧失智長輩 + 幼兒)/ `dual_income` / `single_parent`。**Tom 家是 sandwich**,工具設計 deliberately 給三明治世代設想:選離長輩家近 vs 離公司近的 trade-off、晚接送對失智長輩晚餐 SOP 的影響、公幼接送時間跟長輩日照時間怎麼對齊。

    state(`localStorage[childcareHandbookState]`):`childAge` enum + `familyMode` enum + `favorites` 收藏園所類型 + `checklist` 文件 / 行動完成度。可匯出匯入 JSON。

    場景 9:陽台種菜 / 觀葉 / 觀花 — garden-handbook

    garden-handbook 是家庭可食 + 觀葉 + 觀花 + 根莖類種植手冊,涵蓋陽台、窗邊、室內盆栽。每個 plant 一個 entry,**統一 10-section 結構 + 5W1H 總覽 + 購物籃匯入**。

    為什麼 garden 在失智照護 monorepo 裡?

    因為 repo 上位 mission「讓整個家忙起來」(ambient engagement)—— 用植物讓家裡有生長中的東西、每天互動的動作、可採可賞的成果。對失智長輩,種植是 behavior redirection 的有效工具:她躁動時遞她澆水器,「來,我們去澆花」是個比「坐下休息」更不抗拒的指令,因為她可以「做有意義的事」。

    10-section 結構 + 5W1H + 規劃模式

    每個 plant entry 統一格式:

    • 5W1H 5 秒總覽:`renderPlantOverview()` 從 meta + sections 自動抽 6 行摘要(怎麼種 / 多久收成 / 適合哪些家)
    • 10-section 結構:選盆 / 選土 / 種法 / 澆水 / 採光 / 病蟲害 / 採收 / 留種 / 失敗訊號 / 跟其他植物搭配 — 每株都這 10 段
    • 規劃模式:設定頁 toggle 開啟,filter 按「家庭模式 / 季節 / 採光」推薦對應 plant
    • 購物籃匯入:一鍵把該 plant 需要的盆/土/工具加入購物清單(連到 brain memory `feedback_starter_kit_ordering.md`「基礎設施先,活體後」原則)

    state:`localStorage[gardenHandbookState]` — 最愛 / 購物籃 / 規劃模式 / 照護級數 / memos,可匯出匯入 JSON。

    場景 10:讓家有「會主動靠近的成員」 — pet-handbook

    pet-handbook 是家中寵物生活手冊,按 6 個物種分類:**貓 / 狗 / 兔 / 鳥 / 魚 / 烏龜**。每物種下有多個 topics(親訓 / 飼料 / 醫療 / 訓練)。

    跟 garden 的 frame 區別 — 寵物是會主動靠近的家人

    核心定位:寵物是會主動靠近你的家人。跟植物不一樣 —— 他會自己跳上你腿、用頭頂你手、發出呼嚕聲。他不是被動被照顧的對象,**是家庭成員**。

    對失智長輩尤其重要 —— 當她大腦退化、溝通困難,一個會自己靠近的生命,是「她還被需要、她還存在」的證明。garden 是 ambient(植物在那裡),pet 是 active(寵物來找你)—— 兩者並用補不同層次的 family vitality。

    對小孩:寵物從小同住的孩子發展(empathy / 責任感 / motor skills)有 evidence-based 好處。混合家庭(失智長輩 + 幼兒 + 寵物)是 monorepo 主場景之一。

    場景 11:照護判斷迷茫時 — mindset(6 條視角)

    mindset 是「心法手冊」 — 不是 SOP、不是 checklist,是一位失智照護者面對新狀況時,腦袋裡跑的判斷視角。中英雙語(zh + en),開源歡迎其他家屬補充。

    6 條視角

    • 視角 1:失智不是失能 — 她不是壞了,是用不一樣的方式運轉
    • 視角 2:用加法對抗減法 — 認知衰退是減法,加新刺激抵銷
    • 視角 3:躁動不是無聊 — 行為背後通常有具體 trigger(尿急、肚子餓、燈太亮)
    • 視角 4:動線是功能可及性 — 她做不到不一定是失能,可能是動線設計
    • 視角 5:誠實版優於善意包裝 — 不要編故事騙她,她感受得到
    • 視角 6:手冊是網狀不是清單 — 沒有「按順序做完」這種事

    caveat 自註:Handbook 自己寫了「⚠️ 不要把任何視角當絕對」 —— 「這 6 條視角是我寫家中照護手冊時用的判斷方式,不是失智照護的真理」。比如「視角 2 加法對抗減法」跟主流的「懷舊治療(Reminiscence Therapy)」(用過去記憶喚起情感連結)有衝突。Tom 在文章明文承認這個衝突。

    這個工具不解決具體問題,而是在其他工具給不了答案時,**回頭問:我用什麼 frame 在想這件事?** Handbook 是網狀的,在失智路上某些時刻會用上。

    場景 12:長輩 / 病人 / 嬰兒選營養品 — health-drinks

    health-drinks 是19 款醫療營養品 + 嬰幼兒配方 + 高蛋白補充品的並排比較工具(180-280ml 容量範圍)。**不是市售飲料**,是給病人 / 長輩 / 嬰兒喝的營養補充品(葡勝納 / 完膳 / 安素 / S-26 / 倍速益 / 康健等)。

    三重使用者

    • Tom 自家 — 失智媽媽吞嚥退化階段選低糖高蛋白配方
    • 藥師朋友 — 跟客戶解釋「葡勝納原味跟菁選香草」差別時要快速比成分
    • 藥局客戶 — 拿出手機看比較表自己挑(藥師沒空講)

    資料流:拍包裝 → AI 擷取 → 手填 drinks.js

    每加新飲品的流程:

    1. 藥局買回來拍包裝側標籤
    2. AI(Gemini / Claude)從圖片擷取營養成分
    3. **手填**到 `data/drinks.js`(brain memory `health-drinks-data-entry.md` 「拍標籤說『加進去』就全填,包含微量元素,不可自行省略」原則 — `feedback_nutrition_label_fulldata.md` lesson)
    4. UI 並排比熱量 / 糖 / 鈉 / 蛋白質 / 脂肪 / 維生素 / 礦物質

    monorepo 唯一 CDN 例外:health-drinks 較早建置仍用 Chart.js + Google Fonts CDN(Noto Sans TC)。圖表呈現用 Chart.js 4,字體用 Noto Sans TC。違反 monorepo 0 CDN 強制規定 —— 計畫之後遷移成 inline,但目前留著作為「歷史包袱」example。新工具不准走這條路。

    何時不該用工具,直接找專業

    工具看到「預期外的 pattern」就停用,改找專業。具體訊號:

    訊號 該找誰 為什麼
    突然拒食 ≥ 3 天 神經科 / 急診 可能是 UTI / 吞嚥退化 / 譫妄,不是「沒胃口」
    行為驟變 24 小時內 急診先排除 UTI 中老年 UTI 常表現為譫妄不是發燒
    跌倒 119 / 急診 頭部撞擊、髖關節骨折看似輕微會延遲爆發
    照顧者撐不住 1925 安心專線 / 共照中心個管師 Burnout / 預期性悲傷有專業 evidence-based 支援
    小孩發燒 ≥ 39°C 連 2 天 兒科 / 急診 tools 不會幫你看川崎 / 流感 / 腸病毒

    原則:工具是 baseline 操作,異常事件外溢就 escalate。不要用「我家工具上看了沒問題」當不去看醫生的理由。

    工具互鎖:資料閉環怎麼運作

    14 個工具裡不是每個都獨立。失智照護核心 4 個工具串成 daily/weekly 閉環:

    每天:
    [居服員 / 我] 寫白板 → 拍照傳 Telegram
        ↓
    [白板 OCR Bot] Gemini OCR + confirm
        ↓
    [iDempiere Z_momSystem] 每天一筆紀錄
    
    每月:
    [mom-clinic-companion] 拉一個月紀錄
        ↓
    產出回診清單 → 神經科看具體 evidence
        ↓
    醫師調藥決定寫回 iDempiere
    
    每月共照中心月聚:
    [care-handbook 援手系統] 個管師有資料看可以諮詢
        ↓
    社工 / 物理治療 介入決定

    關鍵設計:資料只進 iDempiere 一次,各工具讀同一個 source。不是 OCR Bot 一個資料庫、mom-clinic 另一個資料庫 — 那會分裂。所有寫入收斂到 iDempiere,所有讀取從 iDempiere 出來。

    iDempiere 是台灣 open source ERP,我把它當 PHR (Personal Health Record) 用。對家庭規模 overkill,但有 Z_table 自訂 + REST API + audit log 的 enterprise 級資料管理,這條主線運轉半年沒掉資料。

    不是處方,是 invitation

    這套工具不是處方。每家狀況不同 — 有的家有外籍看護工、有的家三代同堂家屬 backup 充足、有的家在偏鄉服務不完整。我家是「白天 solo + 配偶藥師有正職 + 兒女幼小 + 父母 secondary 經驗 15 年」這個極窄 niche,所以工具 design 偏向「降低 solo 啟動 friction」。

    如果你家狀況跟我接近,這套工具開箱可用;不接近,把它當「一個照護者怎麼設計自家工具」的 reference,fork 改成你家用的。原始碼在 GitHub,部署在 tm731531.github.io/dementia-care

    有相關問題歡迎在 blog 留言 — 工具不主動推,但方法 / 經驗 / 踩過的坑歡迎交流。

  • 我該選哪個 AI 模型?從硬體到能力的白話判斷指南

    選 AI 模型不是看誰最厲害,而是看哪個「裝得進你的機器、會做你的工作、跟你脾氣合」。這篇用白話講清楚怎麼判斷,不需要工程背景就能讀懂。最後附上三種人的實際範例和一張機器等級對照表,讓你看完知道下一步該做什麼。

    重點摘要

    • AI 模型有四關:可不可用、裝不裝得下、擅不擅長、合不合作
    • 「總參數」決定要多少記憶體,「激活參數」決定跑多快 — 兩個不一樣
    • 32GB 記憶體的家用電腦能跑 30B 級的本地模型(壓縮後),別被 700B 的數字嚇到
    • 判斷模型擅長什麼,看 第三方榜單 + 自己跑題目,不要相信廠商海報
    • 不會微調、不寫程式的一般人:雲端 ChatGPT/Claude 比地端模型實用 10 倍

    為什麼這篇不是工程師專用

    很多 AI 模型介紹一上來就講 Transformer、MoE、量化、KV cache,看不懂的人乾脆直接問 ChatGPT 算了。但這樣你永遠不會知道:為什麼有時候 ChatGPT 答得不如另一個工具好?為什麼有人在自己電腦上跑 AI?那台主機要花多少錢?

    這篇用「買菜選菜」的角度切入。選模型就像選餐廳:有的店招牌大但你家附近沒有(雲端 vs 地端)、有的菜單大但你只吃其中三道(總參數 vs 激活參數)、有的師傅擅長熱炒但你想吃日料(文字強但圖片弱)。看懂這幾個對應,後面什麼術語都不會卡住你。

    第一步:先問自己這四個問題

    判斷一個模型「對你來說可不可用」,不是看它在排行榜第幾名,而是先答這四個問題:

    1. 我要它做什麼? — 寫文案、翻譯、寫程式、看 PDF、看圖,選的方向完全不同
    2. 我要在哪裡用? — 雲端服務 (ChatGPT 網站、Claude 網站) 還是裝在自己電腦 (地端)
    3. 我有多少預算? — 雲端是月費或按量計費,地端是一次性硬體投資
    4. 資料能不能上雲? — 病歷、客戶資料、未公開的營業資料,丟雲端要先看公司規範

    這四題答完之後,80% 的人會發現:地端模型不是給你的。雲端 ChatGPT/Claude/Gemini 月費 600-800 台幣,你買得到的「能力」遠超過你自己組一台機器跑 Llama 3。地端只有在「資料不能外流」、「想要客製化微調」、「想當興趣研究」這三種情況才划算。

    第二步:看模型「裝不裝得進」你的機器

    如果你決定走地端,這一關是硬門檻。模型有兩個關鍵數字,廠商常常混在一起講,你要會分:

    總參數 vs 激活參數

    名詞 白話解釋 決定什麼
    總參數 模型「整本書」有多厚 要多少記憶體裝它
    激活參數 每次思考翻幾頁 跑得快不快

    普通模型(像 Gemma 3、Llama 3)「整本翻完」才回答你 — 總參數 = 激活參數。MoE 模型(像 Mixtral、DeepSeek、Gemma 4)「只翻其中幾頁」 — 總參數遠大於激活參數,跑得快但還是要把整本書放進記憶體

    記憶體簡易公式

    用「壓縮過」(技術上叫 Q4 量化) 的模型,大致這樣估:

    • 模型體積 ≈ 總參數 ÷ 2 GB
    • 實際記憶體需求 ≈ 模型體積 + 2~4 GB(系統開銷 + 上下文)

    例子:Gemma 3 27B 模型 → 27 ÷ 2 ≈ 14 GB 模型 + 4 GB 開銷 = 18 GB 記憶體需求。一台 16GB 筆電裝不下,32GB 桌機剛好。

    第三步:看模型擅長什麼

    模型不是萬能。同樣 27B 大小,有的會寫程式但不會看圖,有的中文很爛但英文很強。怎麼判斷?

    看第三方榜單(快速篩選)

    你的需求 看哪個榜單分數 大概及格線
    寫文案、知識問答 MMLU-Pro >65 算強
    寫程式 LiveCodeBench、HumanEval LiveCodeBench >30 可用
    中文能力 C-Eval、CMMLU >70 算強
    看圖、讀 PDF MMMU、DocVQA MMMU >60 算強
    數學、邏輯 MATH、GPQA MATH >70 算強
    綜合(一般使用) LMArena Leaderboard 看 Elo 排名

    自己跑題目(最關鍵)

    榜單分數會作弊,你必須跑「自己工作會碰到的題目」。流程:

    1. 準備 20-30 題你實際工作會碰到的問題(不是從網路抄題目)
    2. 同樣的題目餵給 2~3 個候選模型
    3. 蒙著看答案、給分(別看是誰寫的)
    4. 看「一次到位率」、「中文是否流暢」、「會不會瞎掰」

    這比看任何排行榜都準。我之前在本地 AI 模型完整實測那篇就是用這個方法,五款模型結果完全不照 leaderboard 排名走。

    第四步:確認模型「合作說明書」

    同樣強的兩個模型,「會不會聽指令」差很多。這部分用戶最容易踩雷,要看四件事:

    • 支不支援系統指令(System Prompt) — 例如 Gemma 系列不支援系統指令,你只能塞在第一句話。Llama、Qwen、Claude 都支援。不知道這點會發現「怎麼叫它扮演什麼角色都沒效」。
    • 能不能呼叫工具(Function Calling / Tool Use) — 想做 AI Agent、自動查資料、操作 Excel,模型必須會「呼叫工具」。Claude、GPT、Qwen 強;Gemma 弱要靠土法煉鋼。
    • 能不能吃圖、吃 PDF — 多模態能力。Gemma 3 / GPT-4o / Claude 都吃圖;純文字模型(像 Llama 3 base)看不到圖。
    • 上下文(Context)能裝多少 — 4K 只能聊天、32K 可以吃一份合約、128K 可以吃一本書、1M 可以吃整個資料夾。看你的工作內容多長。

    三種人的實例:看完直接套

    實例一:上班族 Mary(行銷專員)

    需求:寫社群貼文、改履歷、翻譯英文信、整理會議紀錄。一台 8GB 筆電,沒在管什麼資料外流。

    判斷:

    • 地端?不要,8GB 連最小可用模型都裝不下
    • 能力需求:文字 + 中文,不需要寫程式
    • 預算:能接受月費 600-800 元

    建議:直接訂 ChatGPT Plus 或 Claude Pro。免費版偶爾用也夠,只是會在尖峰時段斷線。不要花一分錢買硬體,投資報酬率最差的選擇。

    實例二:工程師阿志(後端開發者)

    需求:寫程式、Code Review、技術文件查詢、自動化腳本。32GB 桌機,公司規定客戶資料不能上雲。

    判斷:

    • 分流:涉及客戶資料的問題用地端,公開資料用雲端
    • 地端能力:寫程式 → Qwen2.5-Coder-32B(壓縮後 ~20GB,塞得進)
    • 雲端:Claude Sonnet 寫程式最強,搭配 Cursor 或 Claude Code

    建議:Ollama 裝 Qwen2.5-Coder-32B 跑地端,Claude Pro 月費跑雲端。兩邊加起來月支出 ~800 元 + 一次性 0 元(用現有電腦)。

    實例三:創作者小凱(YouTuber + 部落客)

    需求:腳本撰寫、字幕翻譯、看 PDF 整理重點、看圖寫敘述、長影片轉文字稿。16GB 筆電 + 一張 RTX 4060(8GB VRAM)。

    判斷:

    • 多模態(吃圖)是剛需 → 模型必須支援
    • 16GB 筆電能跑 8B-12B 級的多模態小模型(像 Gemma 3 12B)
    • 長影片字稿要長 context → 32K 起跳

    建議:雲端為主(ChatGPT 或 Gemini 看圖最強),地端裝 Gemma 3 12B 當備援(網路斷或想離線時用)。腳本創意給雲端、批量字幕翻譯給地端省錢。

    機器等級對照表:你的硬體能跑到哪

    下表用 Q4 壓縮後的本地模型尺寸估算。「速度感受」是 CPU 推理(沒獨顯)的體感:

    機器等級 能跑的最大模型 速度感受 適合
    8GB RAM 筆電 放棄,直接走雲端 N/A Mary(上班族)
    16GB RAM 筆電 7B-12B 模型(Gemma 3 12B、Llama 3 8B) CPU 跑會卡,有獨顯才順 輕度離線備援
    32GB RAM 桌機 26B-32B 模型(Gemma 4 26B MoE、Qwen2.5-32B) MoE 模型可用,Dense 慢但能跑 阿志(開發者)
    64GB RAM 工作站 70B 模型(Llama 3.3 70B) CPU 很慢,要獨顯才實用 研究、實驗
    RTX 3090/4090 (24GB VRAM) 27B-32B 模型,速度飛起 流暢,接近 ChatGPT 體驗 認真做地端 AI 的甜蜜點
    2x RTX 4090 / A100 70B 模型流暢、Mixtral 等大 MoE 商用級 公司部署、付費服務

    關鍵原則:沒有獨立顯示卡的話,別買貴的桌機去跑大模型。CPU 推理 30B 級模型每秒只吐 2-5 個字,寫一封信要等三分鐘,用一次就會放棄。要嘛走雲端,要嘛投資一張二手 RTX 3090(約 2 萬台幣)。我之前測過 AMD 內顯 (iGPU) 跑 Gemma 4 也會踩坑,內顯不是省錢方案。

    最後:我該問自己的 7 個問題

    把這份清單存起來,下次有人推薦你「某某新模型超強」時,逐題核對一遍:

    1. 我用 AI 來解什麼具體問題?(模糊回答 = 還沒想清楚,先別買硬體)
    2. 這些問題,雲端的 ChatGPT/Claude 已經能解嗎?(能 → 直接訂閱,不要繞遠路)
    3. 我的資料能不能上雲?(不能 → 才考慮地端)
    4. 我的機器有多少記憶體和顯卡?(對照本文那張表)
    5. 這個模型在我關心的能力(寫程式 / 中文 / 看圖)分數怎樣?(看榜單再自己跑題)
    6. 這個模型的「合作說明書」有什麼坑?(系統指令、工具呼叫、上下文長度)
    7. 用一個月後,我真的有用嗎?(很多人裝完地端模型用三天就涼了,雲端訂閱反而沒浪費)

    結論一句話:普通人選雲端、有資料安全顧慮的選地端、想當興趣研究的選地端 + 雲端混用。買硬體之前,先把第 1、2、7 題答清楚,就會省下七成不必要的開銷。

  • 失智照顧半年的三個誤會:從菠蘿麵包、日照失敗到白內障

    重點摘要

    • 誤會一:早餐要吃什麼 — 我給菠蘿麵包是因為我自己這樣吃,後來才發現要換成燕麥、雞蛋、香蕉。
    • 誤會二:日照送不進去就算了 — 送日照失敗三次後我差點放棄,後來才發現真正要偷的是「日照在做什麼」。
    • 誤會三:她在廁所地上拉大便 = 失智退化 — 我煩惱了快三個月,最後真相是白內障,她看不到白色馬桶。
    • 半年 11 項環境改造,底層只有一條規則:她大腦掉的不是動作,是「下一步去哪」的能力

    2025 年 10 月,我開始介入家裡長輩的失智照顧。那時候家裡給藥很亂,「什麼藥都早上一次給」,我看不下去就分成早、晚、睡前。這個小小的介入起頭之後,後面半年我改了家裡大概 11 項硬體、定了兩套 SOP、試過失敗過十幾個活動。

    但這篇不是講 SOP,是講我誤會過的三件事。失智照顧這半年,我發現最需要被寫下來的,不是我做對了什麼,是我以為是 X、其實是 Y 的那三個轉彎。每個轉彎都讓我重新理解這個病。

    誤會一:以為她就跟我吃一樣的早餐

    最早我給她的早餐是菠蘿麵包。原因很簡單 —— 因為我自己就這樣吃。我甚至沒想過這是個決定。

    但失智的人需要的營養不是「什麼都沒有的澱粉」。我後來改成:

    • 無糖豆漿
    • 雞蛋(用煮蛋器前一晚設定 P3、定時 9.5 小時,隔天早上自動煮熟)
    • 燕麥片(麥片盒上的圓蓋一滿匙)

    這個誤會之所以重要,不是因為菠蘿麵包「不營養」。而是這是我半年下來第一個、也最小的那個轉彎 —— 我發現照顧失智的人,不能用「我習慣這樣」當標準。她的需求跟我不一樣,我必須先承認這件事。

    後面所有比較大的誤會,其實都是這個誤會的延伸版本。

    誤會二:送日照送不進去,就算了

    2025 年 12 月中。那時候我最怕的事情是她一整天躺著 —— 怕她越躺著失智越快。

    我嘗試送她去日照中心,失敗了。家屬帶去她就回頭、大吵,根本進不去。我差一點就結論:「日照不適合她,這條路斷了。」

    但我停下來想一個問題:日照中心到底在做什麼?

    我去看了幾家、觀察他們的日常。我發現他們在做的事其實不特別:

    • 看影片
    • 做手作
    • 重新認識一些東西
    • 一直詢問每個個案的背景,想從中找出蛛絲馬跡讓這位長輩多做一點

    最後這一條是我偷到的方法論 ——日照中心其實在做的,就是「找出這個長輩一輩子做過最多的動作,然後讓她在這裡做」

    我回家重新看她。她這輩子做了很多年家務,洗衣、疊衣服是幾十年的日常。我做了個實驗 —— 給她一籃衣服讓她疊。

    五分鐘之內疊好,大小、尺寸、方向跟以前一模一樣。

    所以我沒有放棄日照,我把日照的邏輯搬回家。我的做法不是什麼厲害的概念 —— 就是分散她的注意力、讓她的身體有點事情做。這個做法後來叫出了一整套「在家可以做的事」:

    我試過的活動 結果 失敗的樣子
    剝蒜頭 ✓ 主力
    剝豆芽
    疊衣服
    洗碗、洗鍋子 自動放棄
    澆花 找不到水龍頭,拿著澆花器到處走
    擦桌子 拿著抹布到處走
    分豆子(綠豆+黃豆) 最早一次成功,後來都撒在一起當結束

    失敗的那些,每一個畫面都幾乎一樣 ——她手上拿著工具,但不知道下一步去哪。成功的那些,每一個畫面也都一樣 ——坐著、東西就在眼前、手自己會動

    這裡藏了一個規則,等等會用到。

    誤會三:她在廁所地上拉大便 = 失智退化

    2026 年 1 月開始,最嚇人的事情發生了。

    她開始到處拉大便。甚至已經在廁所裡了,她還會拉在地板上。我煩惱了快三個月。那三個月我懷疑過:

    • 她是不是失智退化更快了?
    • 她是不是在抗議什麼?
    • 是不是腸胃出問題?

    那段時間很多人告訴我應該要加藥。我幾乎要聽進去了。如果我去加藥,我現在可能還在誤會裡。

    長照人員那句話,把我從醫療路線拉回來

    轉折是我家的長照人員的一句話。她跟我說:

    可以帶她去日照試試看。

    我立刻回:「她之前去日照失敗。」

    她說:「可以讓長照人員來幫忙帶,不用家屬帶 —— 因為之前是家屬帶失敗的,你們要區分清楚。

    這句話把問題整個翻過來 —— 我以為失敗的是「日照這條路」,她指出失敗的其實是「家屬帶的這個方式」。剛好家附近今年新開一家日照,我們決定試。

    日照入照前要體檢。

    體檢當天,0.3 的視力

    體檢當天,視力測出 0.3。是誰都看得出來視力差。我當天馬上掛眼科。眼科醫生看了,確認白內障

    所有的拼圖這一刻合起來:

    • 她在廁所地上拉大便,不是她失禁、不是退化、不是抗議。是她看不到白色馬桶
    • 她去陽台、廚房小便,是她找不到廁所。白色的家具她分不出輪廓。
    • 白色水煮蛋放在白色衛生紙上,她不吃;紅殼蛋她會吃。看不到 = 不存在

    前三個月我的方向全錯。真相是體檢意外撞到的。如果不是長照人員那句 reframe,我可能還在加藥的錯誤路徑上。

    白內障之後的環境改造

    確認白內障之後,我做了一系列視覺改造:

    • 馬桶加上彩色圈(同顏色她看不到)
    • 廁所門拆掉改成布簾(看到布簾後面的輪廓,她直接進去,不用「推開門」這個步驟)
    • 白色水煮蛋改放在有顏色的盤子上

    這三個小動作做下去之後,她廁所拉地板的問題大幅改善。

    半年學到的那條規則

    回頭看,這三個誤會背後都指向同一件事 ——她大腦掉的不是動作,是「下一步」的能力

    這個「下一步」有兩層:

    1. 感知層的下一步:她看不到馬桶、看不到白色蛋 —— 看不到 = 不存在 = 沒有下一步可走。
    2. 認知層的下一步:她拿著抹布找不到「去哪擦」,拿著澆花器找不到「水龍頭在哪」,疊完衣服不知道「放進哪個衣櫃」 —— 所以她就抱著疊好的衣服到處走

    這半年的 11 項環境改造,底層其實是同一件事:把她該做的決定,從她大腦外包到物理空間

    改動 外包了哪個「下一步」
    廁所門拆成布簾 不用決定「要不要開門」
    馬桶彩色圈 「坐這裡」直接看得到
    家具擺設改成迴圈 不用決定「往哪走」
    定時器開燈/風扇 不用決定「現在該開了嗎」
    故意打散衣服在沙發邊 「這裡要整理」的訊號讓環境去發
    紅殼蛋取代白殼蛋 「這裡有食物」的視覺訊號

    如果我早點知道這條規則,前三個月的廁所問題可能一個月就解了。但沒辦法,規則是被我誤會三次之後才長出來的。

    給其他照顧者的三個提醒

    1. 不要用「我習慣這樣」當起點 —— 菠蘿麵包給她,是因為我自己這樣吃。這是誤會的源頭。
    2. 路徑失敗不代表方向失敗 —— 家屬帶日照失敗,不代表日照不能用。這個 reframe 我是從長照人員那邊聽到的,我自己想不出來。
    3. 行為異常(尤其是大小便)先排除感官問題 —— 不要第一時間想「失智退化」或「加藥」。先帶去眼科、耳科體檢。我花了三個月才做這件事。

    🔧 同一條規則,我用在自己身上

    這條「把決定外包」的規則,對照顧者自己也一樣 —— 甚至更關鍵。

    照護這件事最難的不是體力,是精神力。一天裡真正能好好做決定的時間有限,所以我寫工具的核心邏輯就這三句:

    1. 我要拯救為數不多的精神力、專注力、時間 — 每個決定都不該讓大腦再跑一次
    2. 紀錄是我的習慣,統一跟一致化才能修正 — 沒有結構化資料就沒有 pattern
    3. 最花精神的不是執行,是「每天要想話題」 — 陪伴不是體力活,是創意活

    這三句話各自長出了三組工具。

    動機 1:精神力有限 → 每個工具省掉一個決定

    當下會耗掉精神力的事 我的外包方案
    每天跟她玩什麼? 陪伴小幫手 v1 — 15 遊戲 × 8 難度,不用自己設計今天玩哪個
    每天要跟她聊什麼? 陪伴小幫手 v2 — 一鍵推薦 + 45 條陪伴指南
    居服員白板寫了什麼要人工歸檔? 白板 OCR Bot — 拍照 → Gemini OCR → 自動寫進系統
    回診前要熬夜翻月報? 就診小幫手 — 規則引擎跑異常,5 分鐘前打開看「這次要問醫生的 3 件事」
    在藥局當場查營養品成分? 健康飲品分析 — 21 款並排、per 100ml 自動換算
    想用植物讓她每天有事做? 但我不懂園藝 家庭園藝手冊 — 可食植物 + 觀葉植物,每個 entry 附失智照護 3 級應用(地瓜葉為什麼適合、虎尾蘭給照顧者喘息)
    想養寵物陪她,但不知道會不會變負擔 寵物手冊 — 6 物種(貓/狗/兔/鳥/魚/龜)× 每種 7 topics + 失智照護 × 寵物選擇 matrix(含哪些物種不建議失智家庭新養)

    動機 2:統一跟一致化 → 為什麼背後要有一個 iDempiere

    很多人看到「Telegram Bot + Gemini OCR + 規則引擎 + iDempiere」會覺得是技術炫技。真正的理由很單純:

    我的記性不夠,系統必須替我記 —— 而且要記成「事後找得到 pattern」的形式。

    紀錄要「統一、一致」的意思不是「我每天記得寫」,是「欄位格式永遠一樣,系統才能跨天比對」。這件事 Excel 做不到(欄位亂加)、Notes App 做不到(算不了)、Notion 做不到(沒 schema 強制)。必須是結構化資料庫。

    為什麼我選 iDempiere,不是 Firebase / Supabase / Airtable?

    iDempiere 本質是開源 ERP,拿來當失智照護後端聽起來怪,但三個點全部打中我要的:

    1. AD(Application Dictionary)設計 —— 擴充像加 Excel 欄一樣快

    新增欄位不用寫 SQL、不用跑 migration。在 AD 裡拉一個欄位,UI、REST API、報表全部自動生出來。失智照護的追蹤項目會一直變動:這個月發現血壓要追、下個月發現排便規律要量化、再下個月發現情緒要分早晚。AD 讓我加欄位就像加一欄 Excel,但拿到的是完整的資料庫 + API + 權限系統。

    2. 內建 REST API —— 我不用寫 backend

    加完欄位 GET /api/v1/models/Z_momSystem?$filter=... 立刻可用。不用管驗證、不用管 serialization、不用管 CORS(裝個 filter 就好)。我的工具裡沒有一行 backend 程式碼是我寫的 —— 前端直接 call iDempiere API。這讓我的精神力可以花在真正的問題上(規則引擎、UI、使用情境),而不是重新發明一個 backend。

    3. 開源 + 自架 —— 資料不出家

    整個系統跑在我自己的伺服器,沒有任何 SaaS 在中間,沒有「你的資料被拿去訓練 AI」的疑慮。這一條對我來說比效能還重要。前面提過,家人的資訊不能丟到我不知道的雲端去 —— 這不是偏執,是第一原則。Firebase/Supabase 方便,但資料在別人家。

    這三點組合起來讓擴充變得很快。每發現一個新痛點,通常半天內就能多一個 iDempiere 欄位 + 一個前端小工具連上去。這是為什麼這條工具鏈在半年內可以長出四個工具,而不是我每做一個新工具就要自己刻一套 backend。

    資料流(閉環長這樣)

    居服員每天在廚房白板寫 12 個追蹤項目
    (夜間活動 / 睡眠 / 早/午/晚餐 / 活動 / 外出 / 陪伴 / 排泄 / 洗澡 / 情緒 / 異常事件)
             ↓
    我用手機拍白板 → Telegram Bot → Gemini OCR 辨識磁鐵位置
             ↓
    寫進 iDempiere Z_momSystem(每天一筆 × 12 個結構化欄位)
             ↓
    30 天累積 → 規則引擎跑 4 種異常偵測:
      · window comparison(當前窗口 vs 上窗口差異)
      · co-occurrence(同日多症狀同時出現)
      · streak(連續 N 天某狀態)
      · chronic(本月累積超過門檻)
             ↓
    回診前 5 分鐘打開就診小幫手 → 「這次要問醫生的 3 件事」

    這個閉環其實在前面的白內障故事裡出現過 —— 如果沒有 iDempiere 裡 30 天的結構化資料,體檢撞到 0.3 視力之後,我在眼科問不出什麼具體問題。規則引擎讓那些「感覺她最近怪怪的」的模糊印象,變成「她這兩週排泄時間比上個月晚了 2 小時」這種醫生能用的資訊。

    動機 3:最花精神的是每天要想話題 → 為什麼 v2 長那樣

    這一條是我半年下來最深的自我反省。

    我寫 v1 的時候假設「照護者會從 10 格遊戲裡挑一個」。半年後我發現 —— 我自己就是那個不想挑的人

    「每天要想新話題、新遊戲、要怎麼陪她」這件事,會把一天剩下的精神力全部吃光。不是陪伴本身累,是每天從零開始想要怎麼陪這件事累。

    所以 v2 不是新遊戲,是「系統替你挑」:

    • 依時段 + 當前難度 + 最近玩過的 → 直接推薦一個
    • 每個遊戲配 3 條陪伴指南(進入時說什麼、答對時說什麼、完成時說什麼)= 45 條可說的話
    • 完成後自動產生 LINE 可貼的每日摘要(給沒在現場的家人看)

    v2 的核心不是新功能,是對 v1 假設錯誤的反省 —— v1 假設大腦有能量挑,v2 假設大腦沒能量挑。

    這個 v1 → v2 的演化,跟我在家把衣服「打散在沙發邊讓她自己去整理」是同一個動作 —— 把「要做什麼」的訊號從大腦拿出來,放到環境/系統裡。

    底線:為什麼全部都是純前端 + 自架後端

    • 純前端 HTML + JavaScript,手機打開就用,沒 App 要裝
    • 沒帳號、沒廣告、沒追蹤
    • 資料不會上傳到任何地方(就診小幫手的 iDempiere 是跑在我自己的伺服器上,不是雲端 SaaS)

    家人的資訊不能丟到我不知道的雲端去。這是第一原則。

    工具總集(持續擴充):https://tm731531.github.io/dementia-care/
    原始碼:https://github.com/tm731531/dementia-care

    這篇是上篇,講我在照護這件事上的學習曲線。下篇《失智長輩為什麼一直到處走:她在找的不是這個家》講的是這半年我慢慢發現,她為什麼一直「到處走」,以及她到底在找什麼。那條線會把這個故事帶到一個我一開始完全沒預期的地方。

    相關文章:我之前另一篇失智照護相關文章《日落症候群》

  • Claude Design 實戰拆解:brainstorming/spec/plan 三層流程的必要性與代價

    重點摘要

    • Claude Design 不是一個工具,是 Claude Code 裡由 brainstormingwriting-planssubagent-driven-development 三個技能組成的結構化開發流程,把「想 → 寫規格 → 拆任務 → 做 → 審查」變成可追蹤的步驟。
    • 最大價值是「砍掉不該做的事」——在我的失智症陪伴專案裡,這個流程幫我在寫 code 前砍掉了背景音樂、獨立話題卡、每日心情打卡三個功能,省下至少 3-4 小時重工。
    • 設計原則會在對話中浮出來——使用者一句「照顧者腦子清晰的,可以有期待的方式知道」變成整個系統的「可預期」原則,驅動 45 條提示語都用同一個模板。
    • 但它不適合所有情境——單檔 HTML 原型用 subagent-driven-development 會被 39 次 subagent dispatch 拖到超慢,實測比 inline 慢 3-5 倍。
    • 判斷標準:想事情時用 brainstorming、寫一致內容時用 spec、多檔複雜時用 subagent;單檔改一個函式就直接 inline。
    • 最深的反省:Claude Design 其實就是把 SDD「菜譜化」。人人可以照做,但如果不懂菜譜背後的「為什麼」,遇到菜譜涵蓋不了的變化就卡住。AI 時代最危險的是「永遠繞著菜譜轉、卻以為自己會了」——你的能力上限會被 AI 給的菜譜深度封頂。
    • 用中華一番「大魔術熊貓豆腐」案例說明:對手下黑手、大豆意外變納豆、把汙染當靈感——這三個 AI 結構上做不到的元素。文末附 7 個對抗菜譜化的具體做法,最關鍵是「保留一塊 AI 絕不介入的創作領域」。

    「Claude Design」這個名字其實沒有官方定義,它是 Claude Code 裡一組設計驅動的工作流程技能(superpowers:brainstorming / writing-plans / subagent-driven-development)的總和。我最近用它做了一個失智症陪伴 APP 的重大功能迭代,這篇文章把它在實戰中「什麼時候救了我」跟「什麼時候反而拖慢我」完整拆給你看。

    Claude Design 是什麼?三層流程一次看懂

    Claude Design 是一套讓 AI 助手在動手寫 code 前先「想清楚 → 寫規格 → 拆任務」的結構化流程。它不是任何新工具,而是 Claude Code 內建的三個技能組合使用:

    階段 技能名稱 產出 目的
    1. 發想 brainstorming 設計 spec(Markdown) 用多輪一問一答釐清需求、砍掉不該做的、浮出設計原則
    2. 規劃 writing-plans 實作計畫(含驗證步驟) 把 spec 拆成 10-20 個可個別 commit 的小任務
    3. 執行 subagent-driven-development 每個 task 一個 commit 派 subagent 做、派 subagent 審 spec、派 subagent 審 code quality

    重點不是「三個步驟」這個形式本身,而是每一步強迫你跟 AI 在不同粒度上達成共識:brainstorming 達成「做什麼」、spec 達成「做成什麼樣」、plan 達成「怎麼拆」、subagent 執行時還能雙重審查。

    沒用 Claude Design vs 用 Claude Design 的實際差異

    同樣是「加一個照顧者支援功能」的需求,兩種做法的差別在哪?這是我實測後的對照:

    面向 直接叫 AI 寫 走 Claude Design 流程
    需求釐清 憑 AI 猜,常常做錯方向 多輪問答,需求明確後才動手
    功能範圍 容易做太多(AI 傾向加功能) 在 brainstorming 階段就砍掉不必要項目
    一致性 做到一半風格會漂移 spec 用表格固定內容模板
    可追蹤 一個大 commit 或多個散亂 commit 每 task 一個 commit,可個別 revert
    重工成本 常常寫完才發現方向錯 方向錯在 spec 階段就發現
    速度 單點快、整體容易重工 前期慢、後期穩;不適合微任務

    實戰案例一:砍功能,省下 3-4 小時重工

    Claude Design 最值錢的貢獻不是寫 code,是阻止你寫不該寫的 code。在我的失智症陪伴 APP 迭代過程中,brainstorming 流程實際上幫我砍掉了三個功能:

    砍掉案例 1:背景音樂

    我一開始提議加背景音樂(失智症音樂療法有根據),brainstorming 過程中討論到「單檔 HTML 沒辦法帶 MP3、base64 嵌入會讓檔案爆到 30MB、用 File API 每次要重選」三個技術方案的取捨後,使用者自己說「先不要」。如果沒經過討論直接開寫,我大概會先做 File API + ducking 邏輯寫個兩小時,結果做完發現整個方向不對。

    砍掉案例 2:獨立話題卡

    原本規劃「話題卡」+「遊戲中陪伴提示」是兩個獨立功能。討論到一半,使用者界定:「這個 APP 是玩樂的時候用,真的要純聊天另外開 APP」。這句話直接把「獨立話題卡」功能從 spec 刪除,只保留遊戲情境下的提示。沒有這個界定,我會把話題卡塞進首頁,未來做下一個 APP 會有功能衝突。

    砍掉案例 3:每日心情打卡

    我在 brainstorming 時列過 5 個功能候選,其中「每日狀態簡記」(3 秒打卡心情/精神/食慾)看起來很有價值。使用者沒選這個,理由是「會把 v2 從『打開就用』變成『每天要記得打卡』,失智照顧者已經很累了」——這是使用者比我更懂他的使用情境的典型例子。AI 單獨設計是想不到這層的。

    這三個砍掉的功能合計估計省了 3-4 小時的 coding + 之後的 code review + git 回改。這才是 Claude Design 最值錢的部分——它讓「沒寫到 code 就砍掉」變成可能。

    實戰案例二:「可預期」這個原則如何救了 45 條提示語

    brainstorming 過程中,使用者往往會不經意說出一句話,變成整個系統的設計支柱。這次是這一句:

    「我希望對於照顧者而言,他是腦子清晰的,他可以有期待的方式知道,我可以看到該說什麼。」

    這句話裡面的「可預期」被提煉成整個功能的設計原則,寫進了 spec。然後這個原則驅動了三個具體決策:

    1. 提示固定模板:15 個遊戲 × 3 個時機(開場/答對/完成)= 45 條提示,全部用「情境 → 可以直接說的話」這個同一個格式,照顧者看一次就學會。
    2. 摘要強制預覽:不是一按就複製,而是開 modal 預覽、確認內容,才複製。照顧者對輸出「可預期」。
    3. 按鈕只在完成活動後才出現:有資料才有按鈕,避免使用者按下去才發現「沒東西可傳」。

    沒有 brainstorming 把這個原則浮出來、沒有 spec 把它寫下來,我會寫 45 條有個性的不同句型(AI 本能會追求多樣性)。結果照顧者每次要重新解讀 UI,反而更累。「可預期」這個字沒有出現在原始需求,是對話中自然生成的,但它比任何功能清單更重要。

    實戰案例三:Spec 表格讓語氣不漂移

    Spec 文件裡有一張表格,把 15 個遊戲 × 3 個時機 × 實際內容全部列出來:

    遊戲 開場 答對時 完成時
    顏色辨識 哇,好多顏色,你以前最愛穿什麼顏色? 對!你好厲害 今天陪你看顏色,我也覺得很漂亮
    認字遊戲 這個字你以前就認得吧 對啦,這個字你小時候就會了 今天還認這麼多字,真棒
    呼吸練習 我們一起慢慢呼吸 (改成中段顯示)吸氣…吐氣…不急 輕輕摸摸他的手,這樣就好

    如果不是在 spec 階段一次寫完這張表,而是「做到顏色遊戲時寫顏色的 3 條 → 做到形狀遊戲時寫形狀的 3 條」這樣推進,語氣一定會漂移。寫到第 10 個遊戲的時候我會忘記前面的語氣,變成有些句子很溫暖、有些很乾。表格逼我一次寫完,才有辦法保持「你年輕時、你很厲害」這類一致的溫度。

    這是表格化內容的威力——它讓「一致性」不是靠意志力維持,而是靠資料結構強制。

    反例:Claude Design 不適合的時候

    這次實戰也讓我踩到一個很明顯的坑:subagent-driven-development 對單檔 HTML 原型是殺雞用牛刀

    我一開始對 13 個 task 全部套用標準流程:每個 task 派 1 個 implementer subagent + 1 個 spec reviewer + 1 個 code reviewer,總共 39 次 subagent dispatch,每次 20-130 秒。前幾個 task 都只是「加一個 const、改一個函式」這種改動,triple review 完全是過殺。

    使用者實測後直接問我:「你寫程式變慢很多你用什麼模型」——這是一個誠實的信號。我當下檢討並切換到 inline 執行模式(我自己在 session 裡直接做),後半段 8 個 task 的速度回到正常的 3-5 倍。

    這個教訓很值:流程的 overhead 必須跟 task 粒度匹配。如果你的 task 只是改 20 行 code、不會影響其他模組,triple review 沒有價值,還會讓每個 task 多花 3-5 分鐘在 subagent 啟動/總結上。

    更重要的反省:Claude Design 是「菜譜」,不是基本功

    讀到這裡,你可能會問:「Claude Design 其實不就是 Spec-Driven Development(SDD)嗎?」 沒錯。brainstorming + spec + plan + implement 這四步是 SDD 老方法,不是 Claude Code 發明的。Claude Design 做的事其實是把 SDD 從「一千個人有一千種做法」方法化成「人人可以順暢工具化」的標準流程

    這是好事,也是很危險的事。

    菜譜出現前、出現後

    想像一下麻婆豆腐這道菜:

    • 菜譜出現前:只有師傅會做。每個師傅的麻婆豆腐都不一樣,因為他們從「為什麼要用豆瓣醬、為什麼花椒要後下」這種基本功思考過。客人要求「少辣多麻、加竹筍」這種變化,師傅能即時調整。
    • 菜譜出現後:人人可以照做,麻婆豆腐變得普及。但只會照菜譜的人,遇到「客人對豆瓣醬過敏,要用別的替代」這種問題就卡住了——因為他從來沒問過「為什麼要用豆瓣醬」。

    Claude Design 就是出現了菜譜。它把 SDD 的步驟變成 Skill、把「設計審批」變成 <HARD-GATE>、把 subagent 審查變成標準流程。你不需要知道為什麼要分離「設計」和「實作」,照著 skill 走就有規範的輸出。

    為什麼 AI 時代這件事特別危險

    菜譜時代的廚師,至少還能從菜譜回推原理——多做幾次就知道花椒晚下是為了香氣不揮發。但 AI 時代:

    1. 你問 AI「怎麼做需求分析」→ AI 給你 Claude Design 流程
    2. 你照流程做,產出看起來很漂亮的 spec 和 plan
    3. 你以為你會做需求分析了
    4. 遇到「這個需求太模糊、brainstorming 走 10 輪還是卡住」這種菜譜沒涵蓋的情況
    5. 你再問 AI → AI 給你另一個菜譜
    6. 你永遠繞著菜譜轉,沒有真的建立「為什麼需求會模糊、要怎麼引出使用者的隱藏前提」這種基本功

    最可怕的結果:你的能力上限永遠等於 AI 提供的菜譜深度。菜譜涵蓋不了的變化,你就做不到。而且因為你沒痛過傳統 SDD 的坑,你連「現在這個情境是菜譜解不了的」都判斷不出來——你不知道自己不知道

    SDD 真正的基本功是什麼

    Claude Design 的 skill 不會教你這些,但這些才是真正讓你能判斷、變通、甚至挑戰流程的基本功:

    菜譜只教你「做什麼」 基本功讓你懂「為什麼」
    brainstorming 要一次問一個問題 人的認知頻寬有限,多問題並排會讓使用者選擇癱瘓而隨便回答
    spec 要寫 45 條提示的表格 分批生成會有語氣漂移,但為什麼會漂移?因為 LLM 每次 context 都重新「感覺」一次
    實作前要設計審批 因為修改 code 的成本遠高於修改文字,越晚發現方向錯越貴
    subagent 要 triple review 多視角審查降低單一 agent 的盲點,但前提是 task 複雜到值得這個 overhead
    每 task 一個 commit Git bisect 的粒度等於 debug 速度,太粗的 commit 追 bug 要多花 10 倍時間

    懂了「為什麼」,你才能在菜譜斷裂的地方自己接住。例如:brainstorming 走不下去,你會知道這是因為使用者自己也沒想清楚,你要換一個方式(給他看 mockup 而不是繼續問字)而不是換另一個菜譜。

    給讀者的反省題

    每次用 Claude Design 之前,自問一句:「如果沒有這個菜譜,我會怎麼做這件事?」

    • 如果你答得出來 → 你用 Claude Design 是省時間
    • 如果你答不出來 → 你該先花時間讀 SDD 的原理,而不是依賴 skill 的自動化

    這不是反對 Claude Design。菜譜是好事,讓一道菜普及化。我想提醒的是:Claude Design 方便到你可能永遠不會問「為什麼」。而一個工程師的上限,取決於他問過多少個「為什麼」

    什麼時候用、什麼時候跳過?判斷決策表

    情境 建議流程 理由
    有多個可能方向、還在猶豫 brainstorming 避免選錯方向重工
    要改 15+ 個相似位置、要寫一致的內容 brainstorming + spec 用表格強制一致性
    多檔複雜後端、有 test framework、多人協作 完整三層 + subagent triple review 能防止衝突跟回歸 bug
    單檔 HTML、改一個函式、加一個按鈕 直接 inline,跳過流程 overhead 大於價值
    修 bug 已經知道怎麼修 直接改,不派流程 問題明確時流程是拖累
    重構一個模組的架構 brainstorming + spec(可能跳過 subagent) 要想清楚新架構但執行可以快

    3 個給讀者的實作建議

    1. 開工前先問自己一句話:「這個 task 的粒度適不適合走流程?」如果是「修一個 bug」「改一個顏色」,直接做;如果是「加一個新功能」「重構一個模組」,走 brainstorming。
    2. brainstorming 時刻意引導 AI 砍東西,不是加東西。AI 本能傾向加功能,你要主動問「這個是不是另一個 APP 的事?」「這個能不能先不要做?」。砍掉的功能是最值錢的產出。
    3. Spec 一定要用表格呈現重複性內容。如果你有 10+ 個相似的東西(15 個遊戲的 3 條提示、20 個 API 端點的 error message、12 個表單欄位的 placeholder),強制用表格一次寫完,不要分批生成。

    中華一番案例:AI 結構上做不到的創新

    講到這裡都還是抽象論述。讓我用一個具體案例把「AI 為什麼結構上做不到某些創新」講清楚——1997 年日本動畫《中華一番》第 43 集「大魔術熊貓豆腐」。

    劇情背景:小當家與黑暗料理界的邵安,在樓麟艦上進行豆腐對決。評委要求不能用現成豆腐,必須從黃豆開始做。比賽中:

    1. 對手下黑手:黑暗料理界的向恩把稻草偷偷放進小當家的大豆裡
    2. 意外發生:稻草上的納豆菌讓大豆發酵,大豆變成了納豆(日本傳統發酵食品)
    3. 認知翻轉:小當家看到黏糊牽絲的納豆,不是當成「比賽被破壞」丟掉,而是把這個意外當成靈感起點
    4. 創新誕生:從納豆的特性衍生出做法與工具,最終做出「大魔術熊貓麻婆豆腐」擊敗邵安

    (資料來源:萌娘百科納豆條目,ACGN 中的納豆相關劇情段落)

    這故事裡有三個 AI 結構上做不到的元素

    劇情元素 AI 為什麼做不到
    對手下黑手破壞材料 AI 的 brainstorming 永遠假設「正常情境」,不會模擬「比賽中有人陷害你」這種社會性干擾
    辨識大豆已變納豆 需要跨文化背景知識在當下湧現。一個四川廚師要有人把日本發酵食品的視角帶進來,AI 知道但不會主動從「邊緣」冒出來
    把失敗當靈感起點 AI 被訓練避免失敗、給成功解。被汙染的食材應該丟掉重做——這是 AI 的強勢模式。把 contamination 翻轉成創新起點是訓練資料裡的反模式

    我把這個對比叫做「紹安路 vs 小當家路」

    • 紹安路:精修技術、重組既有元素、走正統菜譜 → AI 擅長
    • 小當家路:個人記憶 + 意外事件 + 跨文化知識 + 重新定義失敗 → AI 做不到

    紹安在豆腐三重奏對決中輸得更深:他自以為原創的料理,其實 7 年前師父陳邦鈴就做過一樣的。他切斷了有意識的傳承,但無意識的傳承切不斷——他走遍中國、投奔黑暗料理界研發的「自創」菜,結構上還是師承的重組。

    用 Claude Design 久了的人可能遇到比紹安更慘的處境:紹安至少還是在複刻真實的師父,你在複刻 AI 訓練資料——但你不會知道,因為沒人會告訴你「這個東西 AI 在 2023 年就生成過一模一樣的了」。

    7 個對抗紹安化的具體做法(可以從下個 task 開始)

    如果你接受「菜譜化是現實、不會回頭」這個前提,那問題就不是「要不要用 Claude Design」,而是「怎麼用但不被它吃掉」。以下是我經過這次專案後整理的 7 個具體做法:

    1. 先離線想 10 分鐘,再開 AI

    用紙筆或純文字檔,在沒有 AI 的狀態下先寫下你對這題的想法——即使亂七八糟、半句話、只有關鍵字。寫完才來找 AI。這 10 分鐘的差別,是你有沒有在對話開始前建立自己的立場。

    2. 刻意加入 AI 絕對不會建議的約束

    AI 傾向推薦「平均來說最好的解」。你主動加怪條件逼自己走不一樣的路。例:做失智症陪伴工具時,不要說「做陪伴 APP」,說「做一個只能給老花眼、色盲、聽障照護者用的工具」——限制是創新的助產士。

    3. 每週跟一個「四郎」講一次話

    不是讓他們給你解決方案,是把他們的「意外知識」放進你生活。失智照護協會的阿姨、你媽或阿嬤(上一代的照護記憶)、會日文的朋友、幼稚園老師(他們跟認知差異的小孩互動方式跟失智照顧很像)⋯⋯讓他們隨口說的話在你腦裡發酵,像稻草丟進黃豆

    4. 把「bug / 意外 / 失敗」當創作材料,不是要避免的事

    開一個 inspirations-from-failures.md,每次遇到意外事件,先不要問怎麼解,先寫三句話:這個意外讓我看到什麼我原本沒看到的?這個限制如果我接受它,會變成什麼新東西?如果我把這個「bug」當成功能,會是什麼?AI 的訓練資料裡「bug 要修」是強勢模式,你要刻意反抗這個傾向。

    5. 對 AI 使用「反向問法」

    每次 AI 給你答案,下一句問:「這答案的相反是什麼?」「80 歲阿嬤會怎麼想這件事?」「如果這是錯的,錯在哪?」「這答案看起來合理,可是我錯過了什麼?」——這會強迫 AI 跳出訓練資料的中心,去邊緣抓東西。AI 不會主動做這件事,你得逼它。

    6. 保留一塊「AI 絕不介入」的創作領域

    不是「少用 AI」,是完全禁止。具體例子:照顧長輩的手寫日記(絕不給任何 AI 看)、一個嗜好(煮菜、種花、學樂器)、每週一天完全不開 Claude / ChatGPT。這塊領地是你保持「能產生小當家型靈感」的保護區。失去這塊,你永遠只能是紹安。

    7. 跟 AI 約定「小當家模式」信號詞

    當你要創新、不要優化時,直接跟 AI 說:「這次我要小當家模式,不要 Claude Design」。你的 AI 應該立刻做三件事:關閉所有 brainstorming/spec/plan 流程建議、不給業界做法、反過來問你的個人史跟身邊的人。這是你跟 AI 之間的信號詞。沒有它,AI 會把菜譜當成預設。

    優先順序:如果只能選一個開始

    選第 6(AI 禁區)。沒有這個,其他六個遲早會被 Claude 侵蝕回去。禁區是你維持「小當家能力」的物理基礎。

    結論:不是流程多就好,是流程跟 task 匹配才好

    Claude Design 不是一個「你該永遠用」的聖杯,也不是一個「多此一舉」的噱頭。它是一套當你面對真正需要思考的問題時,把你跟 AI 之間的對話規則化的工具。

    我這次失智症陪伴 APP 專案裡,Claude Design 救了我的前半段(brainstorming 砍功能 + spec 寫一致的 45 條提示),拖慢了我的後半段(subagent-driven-development 對單檔 HTML 過殺)。能分清這條線,是下次用它用得更好的關鍵。

    如果你的下一個 task 真的很簡單、你已經知道怎麼做——直接做。但如果你正在「想不清楚這功能該不該做」或「要做 15 個相似的東西怕語氣不一致」,那就值得花 20 分鐘先走 brainstorming。這 20 分鐘會幫你省下 3-4 小時。

    延伸閱讀

  • Claude 4.7 Memory 與 Agent Team 實戰:自建 Brain 系統的真正價值

    重點摘要

    • Claude 4.7 的 memory 改進本質是「檔案使用得更好」,不是新增神奇的跨 session 記憶機制 — session 之間仍然完全隔離,靠 MEMORY.md 等檔案橋接。
    • 自建 Brain 系統 = 精煉版 cross-session memory:機制相同(檔案 + CLAUDE.md 宣告載入),差別在手動 curate、domain 分類、顯式載入,品質遠勝 auto-memory。
    • Named sub-agent 真正的價值在「單一任務多輪延續」,不是 Team A/B/C 那種多工並行 — 兩者是互補的兩個層次。
    • Bug 追查 = PUA 方法論 × Named agent 容器 × Brain 更新,三層缺一不可,且要對「無法中斷的頻道(如 Bot)」具備韌性。
    • 模型版本升級 ≠ 知識管理升級:Brain 系統是 model-agnostic 設計,換任何 LLM 都能搬過去。

    Anthropic 在 2026 年 4 月發布的 Claude Opus 4.7 在 memory 和 Agent Team 兩塊都有顯著改進。但這些「改進」對已經自建知識架構的開發者來說,究竟是關鍵升級還是錦上添花?本文整理一天的深度討論,對比 Claude 原生機制和自建 Brain 系統,並落地一套可跨機器攜帶的 bug 追查框架。

    Part 1:Memory 系統 — 4.7 改了什麼,對我有什麼用

    Claude 4.7 的三個 memory 改進

    Claude Opus 4.7 在 memory 方面的改進可以拆成三個層次:

    1. 檔案式 memory 變「一等公民」:4.7 特別訓練用檔案系統當 memory(scratchpad、notes、structured store),能主動寫筆記且下次對話時知道去讀筆記。
    2. Cross-session 穩定性提升:跨多 session、多小時的工作流程一致性更好,模型會依 brain 和 memory 的指引從上次停下的地方接續。
    3. 1M context 無長 context 溢價:原生百萬 token context window,不另收費。以前 200K+ 要 /compact 的場景現在一口氣吃完。

    Cross-session memory 的真相:沒有魔法,只有檔案

    很多人以為 Claude 4.7 的 cross-session memory 是某種「核心系統」幫你串起過去對話。真相是:session 之間仍然完全隔離,模型本身沒有跨 session 的任何 state。所謂 cross-session,是 Claude Code 在新 session 開啟時自動注入:

    • CLAUDE.md(你寫的指令)
    • MEMORY.md 和 topic files(auto-memory 累積的筆記)
    • 過往 session summary(Claude Code 自動寫的摘要檔)

    這些全是檔案,不是隱藏的 cloud memory。4.7 改進的不是機制,而是對這套檔案機制的使用熟練度:知道什麼該寫、該去哪讀、讀到後如何套用。

    自建 Brain 系統 vs Claude 原生 auto-memory

    如果你已經有自建的 Brain 系統(跨專案的技術 domain 知識庫),對比 Claude 原生 auto-memory 會發現:本質是相同機制,差別在淬煉程度

    面向 Claude 原生 auto-memory 自建 Brain 系統
    範圍 單專案(綁 cwd) 跨專案(按技術 domain 分類)
    載入時機 Claude Code 自動注入 CLAUDE.md 宣告 Domain Brain: 主動載入
    curation 模型自動(容易塞流水帳) 手動規則過濾(Why / How to apply 格式)
    外部知識 僅記錄當前對話 支援 [source: external] 納入社群/文章的坑
    配套 memory 單打獨鬥 Brain + Skill 配對(坑 vs 對的做法)

    4.7 對自建 Brain 的實際收益

    對已經有成熟 Brain 系統的開發者,Claude 4.7 的 memory 改進主要體現在兩個地方:

    • Context 吞更多(70% 的價值):1M context 可以同時載入所有相關 brain + CLAUDE.md + 當前 code,不再被切斷。多個 brain 同時載入 5000+ 行也不爆。
    • 維護判斷力(30% 的價值):叫 Claude 整理 memory 時,4.7 會主動去讀 brain 檢查重複、按規則格式寫,而不是無腦 append。4.6 可能直接塞、不去重。
    • 自動 cross-session 對自建系統無感:已經有 Brain + CLAUDE.md 宣告機制的用戶根本不依賴 auto cross-session,品質比自動版高。

    關鍵洞察:模型版本升級 ≠ 知識管理升級。Anthropic 再怎麼升級內建 memory,都在解決「模型如何維護筆記」。但「什麼知識該存、怎麼結構化、跨專案怎麼轉移」是架構設計問題,不是模型能力問題。

    Part 2:Agent Team — Named sub-agent 真正解決什麼

    4.7 的四個 Agent Team 改進

    • Named sub-agents:spawn 時給名字,後續用 SendMessage({to: name}) 續接,不用重跑 context
    • Permission 繼承修正:user-level permissions 現在會正確傳給 teammate,不再每個 teammate 都跳一堆授權 prompt
    • 個別 teammate 可調 mode:spawn 後可以用 Shift+Tab 單獨切換某個 teammate 的 permission mode
    • /team-onboarding:自動分析使用歷史產生 ONBOARDING.md,幫新隊友快速上手

    前置條件:要啟用 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1,否則 SendMessage 工具根本不存在。

    Named sub-agent 和 Team A/B/C 的差異

    很多人以為 named sub-agent 是在取代自組 Team 的做法,其實兩者是不同層次的機制,互補而非競爭

    面向 Team A/B/C(宏觀) Named sub-agent(微觀)
    解決什麼 多工平行 單工延續
    邊界 不同 task 之間 同 team 內部 agent 之間
    Context 燒法 每個 team 獨立燒一份 同 agent 只燒一次,後續增量
    適用場景 早上開 3 件不相干的事 追一條長 bug / 深入一個模組

    最佳組合是兩者混用:宏觀 Team 隔離不同主題,微觀 named agent 讓每個 teammate 有連續記憶。等於開三家公司並行做不同案子,每家公司裡的同一個員工連續給你服務,不是每次都新人上場。

    已知限制:teammate 之間不能互喊

    目前 Claude Code 有個已知 bug(GitHub issue #48160):subagent 本身不能 originate SendMessage。意思是 teammate A 想主動找 teammate B 協作做不到,所有通訊必須經過 team lead 路由,變成星形拓撲(hub-and-spoke)而非 mesh。

    另外 delegate mode 有個連帶效應:lead 切到 delegate mode 後,它的權限限制會傳給 teammate,導致 teammate 也不能讀檔、跑命令,整個 team 卡住。解法是 spawn teammate 時明確放寬權限。

    Part 3:實戰落地 — Bug 追查框架

    為什麼開發和修正要用不同的 agent 拓撲

    對話中得到一個清晰的洞察:「開發」和「修正」性質不同,應該用不同的 agent 拓撲。

    Case 啟動策略
    開發(新 feature) Team A/B/C 並行,短 context agent,各管一塊
    修正(bug / fix) 單一 named agent + PUA,多輪深挖
    重構(無新功能) 單一 named agent,多檔追蹤 side effect

    認真追 bug 的 agent 應該有的樣子

    一個 bug 不是「修好就算了」。它有根本原因(root cause)、爆炸半徑(blast radius)、和教訓(lesson)。停在第一個看似合理的修補是 bug 改頭換面回來的最快路徑。認真的 bug-hunting agent 應該有三層結構:

    1. 方法論(如何思考):PUA 式修辭壓力 — 從不接受第一個假設、永遠挑戰自己的推理、窮盡所有替代方案。
    2. 延續性(如何記住):Named 長壽 agent 跨輪次攜帶 context,不要每輪重新 spawn 新 agent 失去歷史。
    3. 事後教訓(如何教未來):每個修好的 bug 都要更新 brain,否則知識死在這個專案。

    三層缺一不可:沒方法論 → 停在第一個合理原因、出 bandaid;沒延續性 → 每輪重講 context、走回原路;沒事後教訓 → 同一個 bug 六個月後在別專案又出現。

    頻道韌性:為什麼要假設「無法中斷」

    有些 channel(Bot wrapper、API-based tools、async chat)無法真正中斷正在跑的 turn,用戶的輸入只能排進下一輪。框架設計時要把這當前提:

    • 每個 hunter 步驟要快速 return 控制權,不要 chain 10 個 tool call
    • 長任務用 run_in_background: true 讓 lead 儘快 ready
    • 在自然 checkpoint 回報進度,讓用戶在下一輪調整方向
    • 把用戶輸入當戰略路線修正,不是即時操舵

    如果頻道可中斷(CLI)那是 bonus,不是前提。框架不該依賴可中斷性。

    Skill fallback:沒有 PUA 也要能跑

    如果你在多台機器工作,可能某台有 pua:pua-debugging skill、某台沒有。框架要具備graceful degradation

    • Skill 優先:有 PUA / systematic-debugging skill 時,讓 hunter 載入 — skill 是維護中的、品質更新快
    • Brain 兜底:把 PUA 的核心精神內嵌到 brain 檔,確保沒 skill 的機器也能按內建規則運作

    內嵌的規則例如:「假設你的第一個假設是錯的,立刻列出兩個會產生同樣症狀的替代方案」、「修好了 trigger 下一個問題:為什麼以前會壞?」、「症狀只能『大致』解釋 = 沒被解釋,真正的 root cause 能解釋所有觀察,包含奇怪的那些」。這套 inline rule 讓 brain 成為 skill 缺席時的 safety net。

    結案 checklist

    一個 bug 追查只有在全部滿足以下條件才算完:

    • Bug 可重現(或明確記錄為何無法重現)
    • 根本原因以白話講清楚
    • 修復已驗證(重現原 failure → 套用 fix → 確認 failure 停止)
    • 爆炸半徑評估(同一個 root cause 還可能壞掉什麼?)
    • Brain 檔已更新(domain 對、[source: project] tag 已加)
    • Commit message 以 fix: 開頭,而且 brain 更新發生在開始下一個 task 之前

    為什麼這套設計 model-agnostic

    整套 Brain + Skill + Agent Team 設計最大的優勢是 model-agnostic:未來換 Opus 5.0、換 Gemini 3、換 GPT-5,只要該模型支援讀檔案 + 自訂 instruction 機制,整套可以直接搬過去。

    Auto cross-session memory 綁在 Claude Code 內建機制上,換 tool 就沒了。自建 Brain 是把架構設計前置到模型之外 — 模型只負責執行,架構你自己定。結果是:

    • 4.7 再強,沒 Brain 也是亂塞
    • 4.6 再弱,有 Brain 也能跑
    • 未來換任何 LLM,這套設計直接移植

    這其實很接近個人化 RAG 系統的雛形 — 只是沒用 vector DB,用人類可讀的 markdown + 手動路由。反而更可控、更可維護。

    結論

    Claude 4.7 的 memory 和 Agent Team 改進都是實實在在的能力提升,但對已經有自建知識架構的開發者來說,真正的護城河不在追著升級模型,而在建立 model-agnostic 的架構設計

    • Memory:用 Brain 做跨專案長期記憶、用 MEMORY.md 做專案短期筆記、用 Skill 存對的做法 — 三者分工明確。
    • Agent Team:宏觀 Team A/B/C 並行處理不同主題 + 微觀 Named sub-agent 保持任務延續性 — 互補使用最強。
    • Bug 追查:方法論(PUA)× 容器(Named agent)× 事後教訓(Brain 更新)三層結構,對頻道韌性和 skill 缺席都要有 fallback。

    模型會一代代升級,但你累積的 Brain 不會過時 — 因為它記錄的不是模型能力,是你自己踩過的坑和學到的道理。

  • 你的 AI Agent Team 為什麼越做越小?視角驅動的編制方法論

    重點摘要

    • 你以為的「3 個 Agent 上限」可能是創傷的外推,不是設計 — 來自一次 OOM 事故的過度修正,結果反過來殺死你專案的角色細分工
    • 正確的限制是記憶體預算,不是數量 — 16GB 機器可用 ~11GB agent 預算,混合模型(Opus/Sonnet/Haiku)可以安全跑 8+ 個專職 agent
    • 但更深的問題是:角色不是頭銜,是視角 — 你該問「這個計畫需要哪些思考視角」,不是「我有幾個 slot」
    • 三個永遠不能省的視角:使用者、PM、測試者 — 即使是純技術、只有 Python、只有你一個人的專案,這三個視角的思考必須發生
    • 視角打分模型:每個視角用「風險 × 範圍」打 0-9 分,高分獨立成 agent,低分 fold 到其他 agent 的 prompt
    • 反直覺發現:小任務(3 行 API)比大任務更需要資深視角(架構師、運維、測試),因為實作已經不是風險,「該不該存在」才是
    • 壞規則會透過 template anchoring 自我強化 — 修了 runtime 規則還不夠,必須連 template 一起改

    我的 AGENTS.md template 有一條規則:Agent Team 最多 3 個同時跑。這條規則是對的 — 直到我發現它正在偷偷殺死我的專案設計。

    這篇文章是一趟很誠實的反思紀錄。從一次 16GB 機器 OOM 當機開始,到發現我對 AI Agent Team 的所有規則其實都建立在錯誤的抽象層上。結論不是「解除限制、大膽開 agent」,而是更根本的東西:角色根本不是我以為的那個東西

    第一層:我的規則其實是創傷,不是設計

    事情是這樣開始的。2026 年 3 月 3 日,我嘗試在 16GB 的 Mini PC 上同時跑 9 個 Opus agent 做大型重構,結果系統 OOM 當機。從那一天起,我的 MEMORY.md 多了一條 Iron Rule:「Agent Team 最多 3 個同時跑」。

    這條規則是理性的。9 個 Opus × 1GB ≈ 9GB,加上系統和 Docker 的常態佔用,16GB 當然吃不下。3 個是「經驗上安全」的數字。但我沒注意到一件事:這條規則是在「全 Opus」的情境下歸納出來的。我直接把它當成所有情境的通用上限,沒考慮混合模型的狀況。

    更糟的是,這條規則開始滲透到我的 AGENTS.md template、我的專案計畫、我對每個新專案的初始設計。結果是:即使某個專案明明應該有架構師、後端、前端、Kafka、SQL、Test、PM、QA 八個角色,我也會「收斂」到「全端 + 檢查者 + 架構師」三人組。我不是選擇了簡化設計,是我被一條舊規則綁架了設計想像力

    第二層:記憶體預算制取代數量制

    第一個修正很直覺:限制應該是記憶體總量,不是 agent 數量。16GB 機器扣掉系統和 Docker 的 ~5GB,還剩 ~11GB 可以給 agent 用。而不同模型的記憶體占用差很多:

    模型 記憶體 適合任務類型
    Opus ~1.0 GB 架構決策、跨檔案推理、複雜邏輯、資深思考
    Sonnet ~0.6 GB 實作、API、測試、文件
    Haiku ~0.4 GB 檔案掃描、config 對照、簡單查詢

    換算一下:一個架構師(Opus 1.0)+ 兩個後端(Sonnet × 2 = 1.2)+ 前端(Sonnet 0.6)+ SQL(Sonnet 0.6)+ Kafka(Sonnet 0.6)+ Test(Sonnet 0.6)+ QA + PM(Haiku × 2 = 0.8)= 5.4 GB,8 個 agent。完全在 11GB 預算內,還有 5.6GB 的 headroom。

    所以「3 個 agent」的舊規則其實是錯的。正確的說法是「全 Opus 時 ≤ 3 個;混合模型時可以到 8+ 個,只要總和在預算內」。這一步修完,我理論上可以解鎖原本想要的專職團隊設計。

    但這還不夠:我一直在錯的抽象層解決問題

    我以為修到這裡就結束了。結果真正的問題還在更上面一層。

    記憶體預算制解決了「能不能開 8 個 agent」的技術問題,但沒解決「我該開哪 8 個角色」的設計問題。我仍然在用「頭銜思維」想 Agent Team — 有一個固定的角色清單(架構師、後端、前端、QA…),看看預算能放幾個,從清單挑幾個進來。

    這個思維的錯誤在哪?它把「角色」當成頭銜 / headcount,而角色的本質其實是視角 / perspective — 一種「怎麼思考這件事」的觀點。兩個是完全不同的東西:

    思維 頭銜 Headcount 視角 Perspective
    單位 angular-dev、backend-dev 「使用者會怎麼用」「PM 會怎麼想」
    數量 固定清單 每個 plan 動態決定
    「只有 Python」專案 1 個 Python dev 就好 仍需使用者、PM、測試視角
    小任務(3 行) 給最便宜的 dev 可能需要最資深的運維視角

    三個永遠不能省的視角

    這是最實用的一條發現。不管你的專案是什麼技術、什麼規模、幾個人做,這三個視角永遠必須存在:

    • 使用者視角 — 誰會用這個東西?他們的心智模型是什麼?介面怎麼設計才順?
    • PM 視角 — 這件事符合大方向嗎?是現在最該做的嗎?會影響別的優先級嗎?
    • 測試者視角 — 什麼會壞?邊界 case 在哪?漏掉什麼?失敗模式是什麼?

    這三個視角可以 fold 到其他 agent 的 prompt 裡(例如讓 backend-dev 在設計階段戴 PM 帽子),但絕對不能默默省略。省略它們的結果是:你會產出「技術上正確、實用上無用」的東西 — API 能跑但沒人想用、feature 完成但 PM 說這不是重點、code 過 review 但上線就爆 edge case。

    「只有 Python」不是跳過使用者視角的理由。「只有你一個人」不是跳過 PM 視角的理由。「只是改一行」不是跳過測試者視角的理由。這三個視角的思考必須發生,差別只在「由誰承擔」,不在「要不要做」。

    視角打分模型:風險 × 範圍

    光知道「要列視角」還不夠,還要知道哪些重要、哪些次要。我用的打分模型很簡單:Score = Risk × Scope,兩個維度都是 0-3,總分 0-9。

    • 風險:這個視角漏掉的話多慘?(0 = 無所謂,3 = 災難)
    • 範圍:這個視角涉及多少產出物?(0 = 無,3 = 全部)

    分數算出來後按以下規則分組到 agent:

    • Score ≥ 6:獨立 agent,很可能要用 Opus 或高階 Sonnet
    • Score 3-5:獨立 agent,或跟另一個視角配對(Sonnet)
    • Score 1-2:fold 到其他 agent 的 prompt,明確列入該 agent 的優先順序
    • Score 0:在計畫裡 acknowledge 就好,不花 agent 資源

    最後檢查記憶體預算是否超過 11GB,超過就把分數最低的兩個合併。

    反直覺發現:小任務需要更資深的視角

    這是這整套方法論裡最違反直覺、也最容易忽略的一條。實作越簡單的任務,反而越需要資深視角

    原因很微妙:大任務的風險分散在許多實作細節裡,實作者視角得分最高(因為「寫錯」的機率大)。但小任務的實作幾乎免費 — 風險已經轉移到別的地方了:「這東西該不該存在」(架構師視角)、「會不會在奇怪情境下爆」(測試者視角)、「運維怎麼用這個 endpoint 做決策」(運維視角)

    具體例子:幫一個 Docker 服務加 /health endpoint,回 {"status": "ok"}

    • 實作者視角:風險 1 × 範圍 1 = 1 分(3 行誰都會寫)
    • 運維視角:風險 3 × 範圍 2 = 6 分(這個 endpoint 決定服務要不要被 monitoring 重啟)
    • 架構師視角:風險 3 × 範圍 1 = 3 分(「健康」的定義是最大問題)
    • 測試者視角:風險 2 × 範圍 1 = 2 分
    • 使用者視角:風險 2 × 範圍 1 = 2 分

    結論:這個任務要派一個 Sonnet agent,但 prompt 的優先順序是運維 > 架構 > 測試 > 使用者 > 實作。它不是「後端 dev 寫 3 行」,是「一個有運維腦袋的人剛好以 3 行 code 為產出」。

    這個例子回答了我長久的困惑:「小任務該加一個專職 agent,還是從 tester 出發、還是從 dev 出發?」答案是 — 從得分最高的視角出發,不從頭銜出發。小任務通常是資深視角得分高,所以該派的不是 junior implementer,是帶著資深視角的人。

    三個完整案例

    案例 1:純 Python script(200 行,讀 CSV 做統計)

    視角枚舉:Python 實作者、使用者、PM、測試者、架構師

    打分:實作者 6、測試者 6(資料邊界 case 是真風險)、使用者 2、PM 1、架構師 1

    分組:2 個 Sonnet(~1.2 GB)

    • Agent 1:實作 + 架構 + PM 視角(優先:實作)
    • Agent 2:測試 + 使用者視角(優先:測試 — 資料邊界主導)

    2 個 agent,但 5 個視角都被想過了。要的是「每個視角都有思考發生」,不是「每個視角都有一個人」。

    案例 2:OMS 訂單退款功能(跨前後端/Kafka/SQL/admin)

    視角枚舉:架構師、安全、後端、前端、Kafka、SQL、Admin UI、測試、使用者、PM、合規

    打分:每個都 ≥ 4 分,因為範圍廣且風險高(金流)

    分組:9 個 agent(~6.6 GB,預算內)

    • Architect(Opus)、Security(Opus)
    • Backend-dev、Frontend-dev、Kafka、SQL、Admin-UI、Tester(全部 Sonnet)
    • 使用者 + PM + 合規視角 fold 到一個 Haiku

    這就是大型專案值得完整專職團隊的時候。注意:即使 9 個 agent,使用者和 PM 視角還是被 fold 到一個 Haiku,不是各自獨立。視角沒被省,但不一定要佔獨立 slot

    案例 3:3 行 /health endpoint(已在前面詳細分析)

    分組:1 個 Sonnet,所有 5 個視角都 fold,優先順序運維 > 架構 > 測試 > 使用者 > 實作。

    計畫修訂時必須重算

    一個容易忽略的點:staffing 是 live document,不是一次性決定。當計畫執行到一半發現:

    • 新需求浮現(「喔原來這個也要接 Kafka」)→ 新視角需要打分,決定獨立或 fold
    • 某視角發現其實很關鍵(原本 fold 的安全視角,發現有 PII 暴露)→ 升級為獨立 agent
    • 某視角發現其實不必要(決定不做 admin UI)→ 該 agent 退場或重新分配

    每次 plan 修訂都該觸發 staffing 重算。如果 AGENTS.md 跟當前 plan 不同步,應該先更新 AGENTS.md 再繼續 dispatch。

    壞規則會自我強化 — template anchoring 的陷阱

    最後我想分享一個元層級的教訓,是這整件事最讓我不舒服的部分。

    我一開始以為只需要改 runtime 規則(把「3 個上限」改成「預算制」)就好了。但我後來發現:壞規則會透過 template 自我強化。我的 AGENTS.md template 原本的例子只有 2 個角色(implementer + reviewer),這個「低錨」讓我每次初始化新專案時,都自動往「3-5 個角色」的方向填,幾乎從不主動設計 7-10 個。

    這形成一個 feedback loop:

    • 創傷 → 記憶體規則收緊到「3 個」
    • Template 的例子反映記憶體規則 → 錨在低數量
    • 我用 template 寫新 AGENTS.md → 產出低角色的設計
    • 低角色的設計變成「標準」→ 我的記憶認為「就是這樣」
    • 循環加強

    光改規則不夠,template 也得改。我最後把 template 改成強迫跑完整個視角打分流程的版本:頂部要求先讀「視角驅動 staffing」的 brain,中間有「Perspective Inventory」區塊要你填每個視角的 Risk × Scope 分數,再下面是「Memory Budget」計算區塊。沒跑完流程根本填不完 template。

    這個 template 和配套的方法論 brain 都放在公開的領域腦 repo裡,歡迎 clone 參考。

    跟 VCS / 版本控制的關係

    如果你讀過我之前那篇 Jujutsu vs Git,可能會問:視角驅動 staffing 跟 jj 有什麼關係?

    答案是:沒有關係,而且這個順序很重要。視角驅動 staffing 是計畫期的事 — 你在寫 plan、決定 Agent Team 編制時發生。VCS(git / jj / worktree / workspace)是執行期的事 — Agent Team 已經成立、開始跑、產出變動需要合併的時候才登場。不能倒過來。

    我之前一度把這兩件事混在一起寫(在「VCS + AI 工作流」的 brain 裡討論 Agent Team 編制),後來意識到這是錯的。Staffing 決定「要做什麼」,VCS 決定「做的產出怎麼合併」。兩者相關但獨立,混在一起會讓人以為「切到 jj 就能多開 agent」— 不是的,能多開 agent 是記憶體預算 + 視角分析的事,跟 VCS 無關。

    結論:為什麼這個順序重要

    我從「Agent Team 最多 3 個」走到「視角驅動編制」的這段路程,對我來說最重要的一個 meta-lesson 是:你的限制規則可能不是來自設計,而是來自創傷。一次系統炸掉的記憶,會變成一條 Iron Rule,然後透過 template anchoring 滲透到你所有未來的設計決策。

    修復這種規則不能只修表面。你得:

    1. 檢視規則的原始情境 — 它是在什麼條件下產生的?那些條件還成立嗎?
    2. 把規則轉成可計算的東西 — 不是「最多 3 個」,是「11GB 預算」;不是「寫 3 行 API」,是「運維視角 6 分」
    3. 上升到更對的抽象層 — 從「數量」到「預算」是一次升級,從「頭銜」到「視角」是又一次升級
    4. 同時改 template — 不然新專案會繼承舊錨,規則永遠修不乾淨

    這個流程不只適用 Agent Team 編制,適用任何你「歸納」出來的工程規則。下次你發現自己下意識地拒絕某個選項(「不能這樣做,會爆」),請誠實問自己:這個規則是設計來的,還是創傷來的?如果是後者,它可能正在偷偷殺死你的系統設計,而你根本沒意識到。

  • Jujutsu vs Git:從底層原理到 AI Agent Team 時代的版本控制選擇

    重點摘要

    • Git 的本質是一個 content-addressed key-value 資料庫,所有的 branch、rebase、reflog 都是建構在這個底層上的抽象
    • Jujutsu(jj)不是 Git 的替代品,而是把 Git 的 content-addressed 哲學「再往上推一層」 — 從檔案快照擴展到操作快照
    • Git 的核心限制不是指令不爽,而是它要求人類的同步注意力 — merge 衝突會 block 整個世界等你處理
    • jj 允許異步注意力 — 衝突變成 commit 的一種狀態,rebase 永遠不會停,你可以明天再一次處理完
    • Agent Team 有三種任務類型,jj 的價值差很多:Review(只讀)完全不需要 jj;Debug(試驗假設)是 jj 最被低估的甜蜜點;Dev(平行開發)在合併階段需要 jj
    • 最優的使用模式是 git 為主、jj 只在 Agent Team 合併階段出場 — 由 orchestrator agent 承擔 jj 學習成本,人類使用者和 subagent 全程保持 git 心智
    • 決策準則:默認 Git,三個訊號任一觸發時考慮 jj — 平行 Agent 撞車、review round 批次修正、Agent 實驗成本太高

    最近 GitHub 上 jj-vcs/jj(Jujutsu)越來越熱,社群討論開始出現「Git 要被取代了嗎」的聲音。我花了幾個小時認真研究這個工具,過程中意外地被迫重新理解 Git 的底層設計 — 因為只有懂 Git 真正是什麼,才看得懂 jj 想解決什麼。這篇文章就是這趟理解的完整紀錄,從 Git 底層原理開始,一路推到 AI Agent Team 時代的務實使用模式。

    結論先說:Jujutsu 和 Git 不是互斥關係。jj 的儲存後端其實就是 Git,你可以隨時切回純 Git 指令。真正該問的不是「要不要換」,而是「你的工作流是否已經撞到 Git 的設計假設」。而這個答案,必須先從 Git 的底層長什麼樣講起。

    先搞懂一件事:Git 其實是一個資料庫

    很多人用 Git 很多年,但沒意識到一件事:Git 本身就是一個資料庫,而且是一個設計極其精巧的資料庫。整個 .git/ 目錄就是一個 key-value store,只是它用檔案系統當儲存引擎而已。

    你現在去任何 git repo 跑 ls .git/objects/,會看到一堆兩位數的資料夾:00/ 01/ ... ff/,每個資料夾裡面有一堆 hash 開頭的檔案。這就是 Git 的資料庫。它叫 Object Database,key 是 SHA-1 hash,value 是壓縮過的內容。所有其他概念 — commit、branch、tag、stash、reflog — 全部都是建構在這個 key-value store 之上的抽象。

    四種物件,解構整個 Git

    • Blob:儲存單一檔案的內容,不包含檔名。SHA1(content) = hash,內容相同 hash 必然相同。同樣內容只存一份(自動去重)。
    • Tree:儲存一個資料夾的結構,內容是一張表 — 列出這個資料夾有哪些檔案(指向 blob)和哪些子資料夾(指向另一個 tree)。Tree 可以遞迴指向其他 tree。
    • Commit:儲存一個時間點的快照,內容只有幾行 — 指向根目錄的 tree hash、前一個 commit 的 hash、作者時間、commit message。Commit 本身不含任何檔案內容,它只是一個指標鏈的進入點。
    • Tag:annotated tag 的容器,對理解本質不重要,跳過。

    一個 commit 怎麼「記錄整個資料夾」

    你 commit 時,Git 為每個檔案內容計算 hash 建 blob,為每個資料夾建 tree(一張表,列出那個資料夾裡所有 blob 和 sub-tree 的 hash + 檔名),最後建 commit,只包含 root tree hash + parent hash + 作者 + 訊息。結果是一個指標鏈:commit → tree → sub-tree → blob每個 commit 都完整地、不可變地描述了那一刻整個資料夾的狀態,不是 diff,是完整快照。

    但每次存完整快照不會爆嗎?

    這就是 Git 設計最美的地方。答案是:不會爆,因為 content-addressed 帶來自動去重。你 commit 了 1000 次,但 99% 的檔案沒動,那 99% 的檔案永遠只有一個 blob,因為內容一樣 hash 一樣,儲存只有一份。改一個檔案的成本 = 1 個新 blob + 2-3 個新 tree + 1 個新 commit,其他全部重用。實際效果:Linux kernel 有 100 萬+ commits,整個 .git/ 也就 5 GB 左右。

    Git 從來不儲存 diffgit diff 是每次問的時候走訪兩個 commit 的 tree DAG、現場跑 Myers 演算法算出來的。這個設計的好處是:讀任何一個歷史版本都是 O(1),不用還原一串 diff 才能看到那個版本的樣子。這一切都是「content-addressed immutable store + pointer DAG」這一個設計決策的延伸。Linus 2005 年花兩週寫出 Git 核心,就是這個設計。後來所有東西都是在這個底層上加糖。

    Jujutsu 的核心創新:把 Git 的哲學再推一層

    現在你懂了 Git 的底層,就能看懂 jj 在做什麼。jj 沒有推翻 Git 的設計,它做的事情是:把 Git 的 content-addressed DAG 從「檔案狀態」擴展到「操作狀態」 — 同樣的哲學,應用在不同的層級。

    雙層身份:commit hash + change-id

    Git 只有一個 ID — commit hash,由內容決定。這導致 rebase 後 hash 全變,你找不到「原本那個 commit」。jj 對每個 commit 同時維護兩個 ID:commit hash(跟 Git 一樣)+ change-id(隨機產生的 128-bit ID,只在 commit 誕生時生成一次,rebase/amend/split/squash 都不變)。這個設計的意義是:身份認同不綁在 hash 上,綁在 change-id 上。你可以隨便改歷史,change-id 永遠追蹤得到「同一個概念上的改動」。這就是為什麼 jj 可以把「歷史重寫」當日常操作,而 Git 必須當儀式感操作。

    Op Log:第二層 content-addressed DAG

    jj 的另一個核心是 operation log。每個 jj 操作都產生一個 operation 物件,包含 operation id(hash)、parent operation、執行的指令、操作前後 repo 整體狀態的快照。這些 operation 本身也組成一個 DAG,content-addressed 儲存。注意這裡的模式跟 Git 完全一樣:不可變物件 + 指標 DAG + 內容定址。jj 只是把 Git 用來管檔案狀態的那套機制,再蓋一層管操作狀態。

    結果是 jj undo 變成一步 pointer 移動 — 直接把 repo view 指回前一個 operation 的「操作前快照」就好,不用反向計算任何東西。而且 undo 本身也是一個 operation,可以被 undo,可以無限回退。

    指令層面的六個差異

    面向 Git Jujutsu (jj)
    Staging Area working dir → index → commit(三層) working dir 就是 commit(兩層,無 git add)
    Branch 概念 一等公民,必須 checkout/rebase/merge 可選的 bookmark,每個 commit 有穩定 change-id
    Undo 機制 reflog(補救機制,90 天 TTL) jj undo(第一等操作,可無限回退)
    Conflict 處理 停止世界,當場解完才能繼續 衝突直接存進 commit,可延後解
    Stash 必要指令(stash push/pop/apply) 不存在,jj new 直接開新 commit
    歷史重寫 rebase -i 是儀式感操作 jj split / jj squash 是日常

    最重要的差異:同步注意力 vs 異步注意力

    到這裡為止,都還是「指令差異」和「設計哲學」的層面。但 jj 真正的價值,不在指令爽不爽,而在它如何消耗人類的注意力。這個差異是質變,不是量變。

    Git 是同步注意力模型 — merge/rebase 遇到衝突會停止世界,直到人類出現、看懂、解決、繼續。它要求你在「不可預期的時刻」立刻在場。這個模型在單人或 2-3 人團隊下完全沒問題,因為衝突頻率低、你本來就在鍵盤前。

    jj 是異步注意力模型 — rebase 遇到衝突不停下來,衝突被存進 commit 變成一種有效狀態,rebase 繼續往下跑。你可以明天處理、或一次批次處理所有衝突、或開另一個 Agent 專門解衝突。jj 允許人類「異步在場」 — 系統自己跑,有問題幫你記下來,你有空再來看。

    Git 的設計誕生於 2005 年,當時的假設是「careful human curator」 — 每個 commit 都是人類慎重思考後的決定,每個 merge 都有人類在場。但 Claude Code、Cursor、Copilot Agent 這類 AI 工具改變了「誰在產生 commit」這件事。Agent 產出的是高頻、探索性、非線性、大量並行的中間態。你不可能一邊看 3 個 Agent 平行工作,一邊在它們合併時即時解衝突,一邊決定每個中間態要不要保留。人類同步注意力的頻寬根本跟不上 Agent 的產出速度。

    jj 在這裡的價值不是「比 Git 更爽」,而是它讓 Git 做不到的事變可能 — 讓 Agent Team 真的平行跑到底,明天早上來一次處理所有衝突。這不是量變(爽度提升),是質變(能做的事情不同)。

    Workspace 隔離:平行的真正解法

    要澄清一個常見誤解:jj 本身不能解決「兩個 Agent 同時寫同一個檔案」這種檔案系統 race condition。檔案系統的併發是 OS 管的,任何 VCS 都介入不了。平行 Agent 的正確架構是每個 Agent 給一間房 — Git 用 git worktree add,jj 用 jj workspace add。每個 workspace 是獨立的 working directory,三個 Agent 在不同資料夾各寫各的,完全不會互相污染。

    Git 和 jj 在 workspace 隔離這一步的行為相同。差異出現在下一步 — 把三個 Agent 的輸出合併回 main 時。Git 會序列化 blocking(解一個、繼續、解下一個、繼續),你的注意力被三次阻塞切碎;jj 則是三個動作一氣呵成,最後一次性 batch 處理所有衝突。Workspace 隔離是必要條件,jj 的優勢出現在合併階段的併發模型

    Agent Team 的三種任務類型:jj 的價值差很多

    這是這篇文章最重要的一個區分,也是大部分「jj 推坑文」忽略的。你叫 Agent Team 做不同類型的事,jj 的價值完全不同 — 甚至有些情境 jj 根本沒有用。決定性的問題是:這些 agent 需要寫檔案才能完成任務嗎?

    類型 1:Review(只讀) — 完全不需要 jj

    場景:叫 Agent Team 做 code review、security audit、dead code 掃描、架構審閱。這些 agent 只讀檔案,產出報告,不寫任何東西

    這種場景 jj 沒有價值:沒有 commit、沒有 merge、沒有 undo 需求。三個 agent 同時讀同一個檔案完全沒問題,OS 的 file handle 對讀不是互斥資源。純 git 就好,甚至不需要 workspace 隔離。

    常見誤區:很多人把「開 Agent Team」跟「需要 jj」畫等號,導致在純 review 場景也導入 jj 的學習成本。這是沒必要的。

    類型 2:Debug(試驗假設) — jj 最被低估的甜蜜點

    場景:叫 Agent Team debug 複雜 bug。Debug 的本質是「形成假設 → 驗證 → 推翻 → 再假設」的循環,而驗證通常需要動手改 code — 加 log、改條件、插 assert、暫時跳過某段邏輯。

    Git 下的 debug 痛點:加了 10 個 print() 驗證完要全部刪掉,漏刪一個就污染 production;改了某個函式測試假設,後來發現方向錯,手動改回去容易漏;想試兩個假設 A 和 B 哪個對,兩個互相覆蓋,不能同時存在。

    這是 jj 最強的場景,**比一般開發還強**。原因很微妙:development 的產出是「最終正確的 code」,中間過程 git 可以容忍(最終 squash 成 clean commit 就好)。但 debug 的產出是「對問題的理解」,這個理解存在於中間過程本身,不存在於最終狀態。

    Git 丟掉中間過程 = 丟掉 debug 的精華。jj 保留所有中間過程 — 每個假設是一個 commit、jj undo 可以乾淨撤銷、多個假設可以平行存在於 op log,等於保留 debug 的完整價值。debug agent 可以試 5 個假設、留下完整推理紀錄、報告說「我試了 A/B/C/D,D 可行因為…」,你事後可以 jj op restore 跳回任何一個假設狀態看 agent 那時候看到什麼。

    結論:如果你常叫 AI debug 複雜問題,jj 的價值比你想的大很多。這是 op log 從「檔案歷史」升級為「推理紀錄」的唯一場景。

    類型 3:Dev(平行開發) — jj 在合併階段是必要品

    場景:叫 Agent Team 平行開發多個模組。多個 agent 會產出多個改動分支,最後要合併回 main。這是大家熟悉的場景 — 前面講的「同步 vs 異步注意力」、「workspace 隔離」、「conflict as commit」全部都在這個場景發揮。

    但注意:jj 在這個場景的價值集中在合併階段,不是 agent 工作期間。agent 自己工作時,Git 和 jj 沒差。差異出現在「把 N 個平行輸出 reconcile 回主線」這一步。沒有 jj 的非阻塞合併,你的 Agent Team 上限會被 git 的序列化 blocking 硬生生壓到小規模 — 即使記憶體還有餘裕。

    三種使用模式:該怎麼在日常工作裡擺 jj

    理解完上面的分析,最後的問題是:你該怎麼把 jj 放進你的日常工作?有三種模式,各自適合不同情境。

    模式 1:純 git(現狀)

    適合:單人或「1 RD + 1 Reviewer」工作流、穩定專案、強烈依賴 IDE Git 整合、團隊只認 git。這個模式沒什麼好討論,就是你現在的樣子,對大多數人完全夠用。

    模式 2:jj 為主,git 只是後端

    安裝 jj colocated mode,日常指令都打 jj,幾乎不打 git。這個模式的問題是:邊界仍然是 git 世界。IDE Git panel 顯示的是 jj 上次匯出的狀態,你在本地 jj rebase 一下,push 後 GitHub 還是看到 commit hash 全變,change-id 根本沒傳出去。你本地很爽,對外還是 git 的世界。

    這個模式只適合「單人、沒有協作壓力、不在意 IDE 整合退化」的使用者。對多數人代價太大。

    模式 3:git 為主,jj 只在 Agent Team 合併階段出場

    這是我研究完之後推薦的模式,也是最被低估的一個。玩法是:

    • 你日常全程用 git — 習慣、IDE、CI、PR 流程完全不變
    • Subagent 也用 git — 他們在各自 workspace 裡用 git commit,根本不知道 jj 存在
    • Orchestrator agent(Claude Code)在合併階段用 jj — 只需要學 5 個指令:workspace addgit importrebaselogworkspace forget
    • 一次性設定:jj git init --colocate,之後你完全忘記它存在

    這個模式最精妙的地方:學習成本完全落在 orchestrator agent 身上,不是你身上。你的認知負擔幾乎為零,只需要記住一個 trigger:「要開 Agent Team 時,叫 Claude」。其他時間 jj 完全不存在。

    失敗零成本:任何一個環節出問題,你都可以回退到純 git。subagent 的 commit 是正常 git commit,無論是否用 jj 都能 git cherry-pick;合併失敗就 git merge 手動解;jj 整個壞掉就刪掉 .jj/,git repo 毫髮無傷。沒有 lock-in,沒有 point of no return

    Review Round 批次修正:寫之前切 vs 寫之後切

    另一個 jj 爆發的場景是 review round 工作流。很多團隊的 QA/review 會產出幾十個 issue,你拿到一份清單要一次修完。典型的 commit message 長這樣:fix(fee): R5-12/13/03/04/10/16/17 remaining module items — 塞了 7 個獨立的 issue 修正在一個 commit。

    為什麼塞一起?因為 Git 下的三個選擇都不理想:每個 issue 一個 commit 要 git add -p 切七次太累;全部一個 commit 未來想單獨 backport 做不到;開 7 個 branch 管理成本爆炸。結果大多數人選「全部一個 commit」,歷史變成粗顆粒。

    jj 的解法:打開 editor,一次改完 7 個 issue,不用管 commit 切分。改完後 jj split,互動畫面讓你把每個 hunk 分配到哪個 commit,七次後自動產生 7 個獨立 commit。Git 的 add -p 是「寫之前」切,jj 的 split 是「寫之後」切。這讓你把「commit 切分」這個決策延後到資訊最充足的時候(全部改完後),而不是在你還在思考邏輯的時候被迫處理。

    決策準則:你該用哪個模式?

    純 git 就夠的訊號

    • 單人或「1 RD + 1 Reviewer」工作流 — 沒有平行檔案衝突
    • Agent Team 只做 review / audit(agent 不寫檔案) — jj 沒有價值
    • 穩定專案、強依賴 IDE Git 整合
    • 老 Git 肌肉記憶還在運作,且團隊只認 git

    考慮模式 3(git + jj for Agent Team merge)的訊號

    1. Agent Team 在共享檔案上撞車 — 你為了避免 merge 衝突開始序列化 Agent,生產力被 VCS 綁住
    2. Review round 產生批次修正 — 你寫過大量 fix: XXX-01/02/03/04/05 這種塞一堆 issue 的 commit,事後想拆卻沒工具
    3. AI 常常需要做 debug 試驗 — 你希望 AI 大膽試很多假設,但現在因為回退成本太高,限制了 agent 的自由度

    三個訊號都沒中?純 git 就夠了,別為了新鮮感切工具。

    我的具體建議:安全試用路徑

    1. 選一個個人實驗性專案,執行 jj git init --colocate(一次性,30 秒)
    2. 繼續用 git 做 99% 的事 — 你的肌肉記憶完全不變
    3. 下次真的要開 Agent Team 時,讓 orchestrator agent 用 jj 的 workspace + 非阻塞 rebase 處理合併階段
    4. 如果是 debug 場景,給 agent 「大膽試驗」的權限,利用 op log 保留推理紀錄
    5. 一個月後評估:有沒有真的遇到 jj 幫上忙的場景?沒有就卸載,git repo 完全不受影響

    跨專案的架構原則

    這次研究讓我提煉出一個跨專案適用的原則,寫進我的領域腦系統:

    VCS 的選型要跟「人類注意力的可用模式」匹配,不是跟「指令爽不爽」匹配。Git 假設人類可以同步在場,jj 允許人類異步在場。AI Agent 工作流打破了 Git 的第一個假設,但是否影響到你,取決於你的團隊形狀和工作型態,不取決於你對 AI 的熱情。

    更深一層的 insight:Git 的偉大不在於它的指令設計,而在於 content-addressed DAG 這個設計模式。這個模式你會在 IPFS、Nix、Docker layers、區塊鏈到處看到 — 懂 Git 底層等於懂了這一整類系統的通用結構。jj 的聰明,是看到這個模式可以往上再推一層,應用到操作本身,而不只是檔案狀態。

    AI 時代最大的認知陷阱是「新工具一定比舊工具好」,但真實世界是新舊工具的取捨點會隨場景變化。把場景描述清楚,比把工具吹捧清楚重要得多。下次你看到任何新的版本控制、儲存系統、分散式工具時,問自己這三個問題就好:它的不可變單位是什麼?怎麼定址?DAG 結構長怎樣?答案對上 content-addressed DAG 模式,你就已經懂 80% 了

  • Vibe Coding 協助物聯 AI 開發:從 RTSP 撞牆到 Telegram Bot 上線的完整實戰

    這是一篇關於 Vibe Coding(憑感覺邊聊邊寫)在物聯 AI 開發上的實戰紀錄。主題聽起來很酷,但實情是:我只是想解決一個很平凡的問題 —— 怎麼把家裡長照白板上的磁鐵打點,自動變成醫生回診時能看的資料。

    這篇會誠實記錄這一晚跟 AI 協作的過程、做對的決策、撞到的牆、以及學到的方法論。不是成功故事,中間我還沒跑出一份真正可用的資料。但我覺得過程本身比結果更值得寫下來。

    重點摘要

    • Vibe Coding 不是憑空想像,而是即時驗證 —— 每個假設都在對話中被一條 curl / 一行 ffmpeg 打臉或確認
    • Gemma 4 跟 Gemini 3 Flash Preview 在同一張模糊中文手寫白板上差距巨大:前者一直幻覺亂編項目,後者誠實回 null
    • Schema-constrained Prompt + 信心度分級是物聯 AI 應用的關鍵 —— 寧缺勿錯,比準度更重要
    • 最後撞到的不是模型牆,是光學物理牆 —— RTSP 1080p 遠距離下磁鐵只有 3~5 像素,任何 VLM 都無解
    • 與 AI 協作最大的價值是迭代速度:從想法 → curl 驗證 → 決策 → 下一步,一晚走完通常要一週的探索

    什麼是 Vibe Coding?為什麼它適合物聯 AI 開發

    Vibe Coding 是一種開發風格,核心精神是:不先寫規格、不先畫架構,先跟 AI 對話,在對話中讓問題結構自己浮現。聽起來很鬆散,但在物聯 AI 開發(IoT + AI 結合)的情境下,這種作法反而比傳統瀑布式規劃更有效。

    原因很簡單:物聯 AI 專案有太多無法事先確定的變數 —— 攝影機實際畫質、現場光線、模型對中文手寫的容忍度、API 認證流程的細節、現場使用者的真實習慣。這些東西你寫再多規格也寫不清,只能邊試邊改。

    傳統作法是先買硬體、接好線、寫一堆程式、跑起來才發現 OCR 讀不到。Vibe Coding 作法是:在對話中模擬整條管線,每一步都先驗證再決定下一步。成本極低,迭代極快。

    實戰情境:家母的長照交班白板

    先講背景。家裡照顧阿嬤的居服員會在客廳牆上用一塊大白板做交班記錄:夜間活動、睡前狀況、昨夜睡眠、早/午/晚餐、活動、外出、陪伴狀況、排泄狀況、洗澡,共 12 個項目。每一項下面有幾個選項方格,居服員把一顆彩色磁鐵貼在正確的選項上。

    用意是交班:早班居服員進門就知道夜間情況,中班看早上發生什麼事,晚班接手時看到整天脈絡。白板是「人看的 UI」,極度務實。

    我這邊的需求是:這些資料要每月匯整,讓神經內科醫師看到阿嬤這個月的生活趨勢 —— 是不是睡眠惡化、食慾變差、活動量下降。這關係到藥物調整。原本我每天晚上八點親自去拍照記錄到 iDempiere,但我很清楚一件事:

    靠人不穩定。我不能保證自己每天都拍照,請妹妹拍她都不拍。任何依賴人的環節最終都會斷掉。

    所以目標很明確:零人工依賴的自動化管線。RTSP 攝影機 → 抽幀 → VLM 識別白板狀態 → 寫進 iDempiere。今晚就是要從零對話出這套設計。

    第一步:用 ffmpeg 抓一張給 AI 看

    對話一開始,AI 想直接讀 RTSP 流,但我的 Ubuntu 上沒裝 ffmpeg,只有 VLC,而且 VLC 一直被 satip 模組攔截。試了幾個旗標都失敗。最後決定直接 apt-get install ffmpeg,然後:

    ffmpeg -y -rtsp_transport tcp \
      -i "rtsp://:@192.168.0.53:554/stream1" \
      -frames:v 1 -update 1 -q:v 2 /tmp/tapo/whiteboard_hd.jpg

    一張 1920×1080 的 frame 落地。第一個教訓:TAPO 有 stream1(主碼流,1080p)跟 stream2(子碼流,較低畫質),白板 OCR 一定要用 stream1,子碼流字跡會糊到任何模型都讀不出來。

    把第一張 frame 丟給 AI 看,AI 發現:

    • 畫面是廣角客廳全景,白板只佔左後方約 25%
    • 白板有角度(側視),字有透視變形
    • 阿嬤正躺在沙發上睡 —— 這畫面本身就能當「夜間睡眠」的訊號來源
    • 時間戳在左上角,直接可當記錄時間

    這是 Vibe Coding 的第一個價值:「看到了才知道要問什麼」。事先想像不到的細節(阿嬤就在沙發上、一顆鏡頭可同時驅動多個欄位),第一張實拍 frame 全部揭露。

    Gemma 4 vs Gemini 3 Flash Preview:一張圖的殘酷對比

    我手上的 Gemini API Key 支援 Google AI Studio,我問 AI 能不能用我已經在跑的 gemma-4-26b-a4b-it(我本來拿它做 HN 文章摘要)。AI 坦白說:不確定 Gemma 4 這個 MoE 版本有沒有 vision 能力,最好的方式是直接寫 script 試。

    寫完測下去,兩個結論:

    1. Gemma 4 確實吃圖 —— 這是好消息
    2. 但它嚴重幻覺 —— 它把日期讀成「4月10日」(實際 4/14)、項目名自創「睡眠品質/活動量/食事/盥洗」(白板上根本沒這些)、備註整段編造「各位家屬提醒…」。所有項目都回「綠」,明顯是在套公版答案

    切換到 gemini-3-flash-preview(Gemini 3 系列的 flash preview 版本),同一張圖,結果判若兩人:

    項目Gemma 4Gemini 3 Flash Preview
    項目名自創 5 個通用項目讀出 13 個具體項目
    備註段落純幻覺讀出「為了照護過程順暢請大家配合…飲食攝入小於 500ml 請 Line 通知…」
    品質差距低解析度下直接編造雖有 OCR 誤差但是「在讀真的東西」

    Gemini 3 Flash Preview 讀出的備註段落跟我白板上實際寫的紅字幾乎一致 —— 它不是在猜,是真的看到了。雖然還有誤差(把「外出狀況」讀成「外服藥記」、把「排泄」讀成「排便」),但那些是視覺相似字誤差,不是幻覺。

    這是第二個教訓:「同一張圖丟不同模型」是物聯 AI 應用最該做的前測。同價位的模型在模糊中文手寫這種邊緣任務上差距可以是幾倍級的。不要以為「我手上這把就夠用」。

    iDempiere REST API:從文件陷阱到 10 分鐘跑通

    OCR 能讀不代表能寫。寫入端是我自己建的 iDempiere,一張叫 Z_momSystem 的表。AI 不知道 schema,我也懶得翻 UI,直接讓它打 REST API 取。

    這裡踩第一個坑:iDempiere REST API 的認證流程文件跟實作不一致。我的 brain 筆記寫兩步驟(POST 拿暫時 token → PUT 帶 clientId 換 session token),但 AI 試了之後發現:直接在 POST 時一次帶齊所有 ID 也可以成功:

    POST /api/v1/auth/tokens
    {
      "userName": "",
      "password": "",
      "parameters": {
        "clientId": 1000000,
        "roleId": 1000000,
        "organizationId": 0,
        "warehouseId": 0,
        "language": "zh_TW"
      }
    }
    → 直接回 session token

    四個參數缺一不可,少 roleId 就回 Missing roleId parameter。這種小細節沒人會寫在文件裡,只能撞過才知道。AI 在對話中撞完就直接更新我的 brain file:

    [source: 長照 OCR 專案 2026/4/14] 驗證過 POST 一次帶齊 clientId/roleId/organizationId/warehouseId 可直接拿到可用 token,不用走 PUT 兩步。

    這是 Vibe Coding 第三個價值:踩到的坑立刻回饋到未來。不寫記錄的話,下個專案又會踩一次同樣的坑。

    Schema-Constrained Prompt:讓模型不能亂編

    認證跑通後,AI 直接查 AD_Column + AD_Ref_List,把 Z_momSystem 的 40 個欄位跟 12 個清單型欄位的所有合法選項抓齊:

    欄位合法選項(id → 中文)
    NightActivity 夜間活動C=完成 / N=未完成 / S=抗拒
    BeforeSleepStatus 睡前狀況X=亢奮 / N=正常 / S=疲倦
    LastNightSleep 昨夜睡眠G=良好 / S=斷續 / N=差
    Breakfast/Lunch/DinnerN=正常 / 10000001=少 / 10000009=多 / 10000002=拒食
    ExcretionStatus 排泄狀況10000006=正常 / 10000007=便秘 / 10000008=稀

    拿到完整選項清單後,我跟 AI 共同寫出 Prompt v2,核心設計有四塊:

    1. 嚴格列出 12 個欄位名稱 + 每個欄位的合法 enum,告訴模型「只能從清單裡選,不要自由發揮」
    2. 明確定義磁鐵規則:磁鐵貼在選項上 = 選中;磁鐵在左側待命區 / 沒看到磁鐵 = 未填(null)
    3. 強制輸出信心度:high / medium / low,並規定「寧可標 low 也不要亂猜」
    4. 禁止事項明確化:不要輸出清單外的項目、不要用清單外的值、不要把空白當選中

    這個 Prompt 模式我叫它 Schema-Constrained Prompt:輸出 schema 先於輸入內容確定。模型的自由度被壓縮到只剩「讀」,不能「想」。對資料入庫型應用來說,這是我目前看過最能壓住幻覺的作法。

    信心度機制:為什麼寧缺勿錯比高準度更重要

    我跟 AI 討論一個核心問題:如果 OCR 讀錯一個欄位,後果是什麼?答案是 —— 醫生根據錯誤資料調藥

    這條紅線決定了整個系統的預設行為:

    • High confidence → 直接寫入 iDempiere
    • Medium confidence → 寫入但標記為待確認,月底回診前我掃一次
    • Low confidence → 不寫,保留原圖,等我手動補

    這樣最差情況是「資料不全」而不是「資料錯誤」。對臨床情境來說,null 永遠比錯值安全

    最後的物理牆:磁鐵只有 3 像素

    所有準備工作做完,Prompt v2 在手、schema 對齊、認證跑通。我說「跑」,ffmpeg 抓一張新的 frame、裁切、送 Gemini 3 Flash Preview。結果回來:

    {
      "date": "4月10日",
      "patient_name": "",
      "items": {
        "NightActivity": {"value": null, "confidence": "high"},
        "BeforeSleepStatus": {"value": null, "confidence": "high"},
        ... 12 個項目全部 null ...
      },
      "overall_notes": "白板所有項目的磁鐵均位於最左側待命區,表示尚未填寫狀態。"
    }

    問題是 —— 我明明告訴 AI,最上面兩項的磁鐵是貼在選項上的。模型卻全讀成 null。這代表什麼?

    我們一起算了一下畫素:在當前 TAPO 攝影機位置,1080p 畫面中白板只佔約 25%,每個選項方格大概 40×20 像素,磁鐵大約 3~5 像素寬。

    任何 VLM 都讀不出 3~5 像素的物體位置。不是模型問題,是物理極限。

    這是整晚最重要的一個教訓。我原本以為換到 Gemini 3 就能解決一切,但真正的瓶頸不在模型,在光學。你沒辦法用演算法放大不存在的資訊。

    有意思的是,這個結論不是我預設的,是對話中算出來的。如果我沒跟 AI 一起 debug、一起看 crop、一起數像素,我可能還在抱怨「模型不夠強」,然後浪費錢去升級 model tier。實際上答案是「把鏡頭移近到 1.2 公尺」或「把磁鐵換大到 3 公分以上」—— 兩個都是硬體側的改動。

    Vibe Coding 在物聯 AI 開發上的六個心得

    整晚下來,我想整理出一些可以帶到下個專案的方法論:

    1. 第一張真實 frame 永遠比規格重要

    物聯 AI 專案第一件事不是畫架構圖,是用 ffmpeg / curl 抓一張真實資料給 AI 看。看到之後你才會發現「原來畫面裡還有沙發」、「原來字有角度」、「原來磁鐵只有 3 像素」—— 這些事事先想不到。

    2. 同一任務丟多個模型前測

    不要假設「手上這把就夠」。Gemma 4 跟 Gemini 3 Flash Preview 在同一張圖上差距巨大,如果我沒前測直接開始寫 production code,下游全部要重做。前測成本只有一個 Python script。

    3. Schema 先於 Prompt

    Prompt 不應該從「我想知道什麼」開始寫,應該從「我資料庫裡有什麼欄位、合法值是什麼」開始寫。把 enum 硬塞進 prompt,模型就只能從那個清單選。幻覺空間瞬間縮到 0。

    4. 信心度是一等公民

    每個欄位都要輸出信心度,而且模型要被明確告知「寧缺勿錯」。信心度不是錦上添花,是讓系統可以在「自動化」跟「人工審核」之間無縫切換的基礎設施。

    5. 踩到的坑要立刻寫回知識庫

    iDempiere REST API 的認證細節、TAPO stream1/stream2 的差異、VLM 對 3 像素物體的物理極限 —— 這些東西踩過一次就要寫下來。否則半年後你或你的下一個 AI 助手會原地再踩一次。

    6. 永遠先問「這個瓶頸是模型還是物理」

    當 AI 應用做不出效果時,工程師的直覺是「換更大的模型」。但物聯 AI 應用有一半以上的瓶頸是光學 / 聲學 / 感測器物理,不是模型。在升級 model tier 之前,先算一下原始訊號的解析度夠不夠。

    一晚的進度清單:完成、撞牆、下一步

    分類項目狀態
    環境ffmpeg + venv + google-genai SDK
    模型選型gemini-3-flash-preview 確認為白板 OCR 唯一可用
    API 認證iDempiere REST auth flow + brain 更新
    SchemaZ_momSystem 40 欄位 + 12 清單欄位合法值
    Promptv2 schema-constrained + 信心度
    實拍 OCR12 項全 null(物理極限)
    下一步鏡頭移近至白板前 ~1.2m 或把磁鐵換大🔜

    尾聲:AI 協作不是減少思考,是加速思考

    這一晚最大的收穫不是程式,是幾個原本需要好幾天才能驗證的判斷,在對話中幾個小時就走完了:

    • 「Gemma 4 能不能做 vision OCR」—— 10 分鐘驗證完
    • 「iDempiere REST API 認證怎麼打」—— 15 分鐘跑通
    • 「Z_momSystem schema 長怎樣」—— 5 分鐘 curl 查完
    • 「Prompt 用什麼結構壓住幻覺」—— 30 分鐘寫完 + 測完
    • 「OCR 失敗的根本原因是什麼」—— 從「模型不夠強」修正到「物理像素不夠」

    如果自己慢慢摸,這些決策可能得花一個禮拜,還未必會發現最後那個「像素物理牆」的結論。

    但 Vibe Coding 也不是萬靈丹。它的前提是你自己要會即時判斷 AI 給的答案對不對。如果我不知道「1080p 遠距離下一顆磁鐵只有幾像素」該怎麼算,我不會發現光學才是真兇,可能還會繼續跟 AI 討論要換什麼模型。AI 負責快速生成假設跟驗證工具,但關掉討論的那個人還是我

    下一步很清楚:明天白天我會去把攝影機搬到白板正對面 1.2 公尺處,如果還不夠,就把磁鐵換成 3 公分的大顆粒。然後再跑一次 Prompt v2,看看準度能衝到 8/12 還是 12/12。

    等驗證完整條管線跑通,我會再寫第二篇:零人工依賴的長照自動化 —— 從 OCR 到 iDempiere 到每月回診的完整管線。這一篇先停在「撞到物理牆」這裡,因為這個結論本身就夠值得記下來了。

    後續實戰:從撞到物理牆到 Telegram Bot 上線

    前一篇的結尾停在「撞到物理牆」,隔天白天我接著實戰。這一段記錄從 PTZ 鏡頭控制、模型賽馬、到最後關鍵 pivot 的完整過程。

    後續重點摘要

    • ONVIF PTZ 跑通:TAPO C200 用 RTSP 同帳密就能從 ONVIF 控制 pan/tilt,存 preset 在「看媽媽」跟「看白板」間自動切換
    • 但物理牆還在:C200 沒有 ONVIF zoom,白板在畫面裡的絕對大小被鏡頭距離鎖死
    • 模型賽馬揭露真相:Gemma 4 / Gemini 2.5 Flash 會套預設答案假裝在看;只有 Gemini 3 Flash Preview 跟 2.5 Pro 真的「在看」
    • 關鍵 pivot:放棄 RTSP 自動化,改走「手機拍照 + Telegram Bot」,把 human 設計進 loop 當 verifier
    • 最後上線:Telegram Bot + Gemini 3 Flash Preview + iDempiere REST API + systemd service,免費版每日 20 次額度剛好夠 production 用

    第一回合:ONVIF PTZ 成功,但沒有 zoom

    既然 TAPO C200 是 PTZ 型號(有 pan/tilt 馬達),自然想法就是「平常看媽媽、每天 20:00 轉過去拍白板、拍完轉回來」。代價幾乎為零:不改位置、不買硬體、不影響監護。

    TAPO 的本地 API 很頭痛,新版韌體要另外設定「Camera Account」。試了 pytapo 套件一直認證失敗。最後發現 ONVIF 標準協定可以直接用 RTSP 同帳密,port 2020:

    from onvif import ONVIFCamera
    cam = ONVIFCamera("192.168.0.53", 2020, "", "")
    ptz = cam.create_ptz_service()
    media = cam.create_media_service()
    token = media.GetProfiles()[0].token
    
    # Save current position as preset
    req = ptz.create_type("SetPreset")
    req.ProfileToken = token
    req.PresetName = "mom"
    ptz.SetPreset(req)
    
    # Move to whiteboard preset
    req2 = ptz.create_type("GotoPreset")
    req2.ProfileToken = token
    req2.PresetToken = "2"   # whiteboard preset token
    ptz.GotoPreset(req2)

    30 分鐘內建好兩個 preset:mom (pan=0.165, tilt=-0.714) 看沙發、whiteboard (pan=0.2, tilt=-0.3) 看白板。自動切換、抽幀、切回,全程 10 秒內。

    但查 PTZ 能力的時候發現一個殘酷事實:

    AbsoluteZoomPositionSpace: []

    C200 的數位 zoom 不對 ONVIF 開放。pan/tilt 能改變「看哪裡」,但不能改變「看多近」。白板在畫面裡的絕對大小還是被物理距離鎖死,磁鐵還是只有幾個像素。

    這是今天的第一個重要教訓:即使有 PTZ,沒有 zoom 的 PTZ 解決不了光學採樣率不足的問題。pan/tilt 的價值在於切換主題,不是放大細節。

    第二回合:四個模型的殘酷賽馬

    既然 PTZ 轉過去也只是切換主題、不會變清楚,我決定做一件更有意義的事:在同一張困難的 RTSP 照片上,把所有可能的模型跑一遍,看哪個真的「在看」。

    測試方法:請媽媽的居服員確認白板上 12 個項目的真實狀態(4 個 box 1、2 個 box 2、6 個 null),這是 ground truth。同一張 RTSP 照片丟給四個模型,計算準度。

    模型命中行為模式
    Gemini 2.5 Pro8/12 (67%)6/6 null 正確、off-by-one 錯誤,真的有在讀像素
    Gemini 3 Flash Preview6/12非常保守,看不清就全回 null(這在醫療情境反而安全)
    Gemini 2.5 Flash4/12預設 box 1,剛好對 4 個 box 1 項目,是幻覺而不是識別
    Gemma 4 (26B-A4B-IT)3~4/12兩次跑結果不一樣:第一次全 box 2、第二次全 box 1

    最有意義的發現:跑同一張圖兩次讓偽答案現形。Gemma 4 第一次答「10 項 box 2 + 1 項 box 1 + 1 項 null」,第二次答「12 項 box 1」。兩次完全不同,同一張靜態圖,同一個 prompt。這證明 Gemma 4 根本沒在看磁鐵,只是在套一個預設樣板。

    便宜模型「預設 box 1」這個偏誤特別危險,因為它剛好能騙過不注意的測試者:真實世界裡大部分項目本來就選 box 1(例如「完成/正常/穩定」通常是第一格),所以「全答 box 1」天然就有 30~50% 命中率。

    Gemini 2.5 Pro 的 8/12 表現是最高的,錯誤都是「off-by-one」(看到磁鐵、算錯格子),至少是在做真正的視覺識別。但 2.5 Pro 免費版不開放 vision,要付費才能用。

    Gemini 3 Flash Preview 的 6/12 看起來輸給 2.5 Pro,但它的錯誤類型完全不一樣:它的「錯」是把有磁鐵的項目讀成 null,不會產生錯誤的 value。這在醫療場景反而是最安全的行為 —— 寧缺勿錯永遠優於硬填。

    第三回合:投票沒用、upscale 沒用

    既然 Gemini 2.5 Pro 有 67% 準度,同一張圖跑 3 次取多數票應該能把 67% 拉高吧?

    結果投票後掉到 6/12。為什麼?因為 2.5 Pro 的錯誤不是隨機噪音,是系統性偏誤:某個項目 3 次都答同一個錯的答案。投票對系統性錯誤完全沒用,它只會把多數的錯誤鎖定成最終答案。

    接著試 pre-upscale:先用 ffmpeg 把白板區域裁出來、用 Lanczos 4 倍放大、加 unsharp,再丟 Gemini。直覺上放大應該有用,對吧?

    ffmpeg -i wb.jpg -vf "crop=720:820:480:30,scale=iw*4:ih*4:flags=lanczos,unsharp=7:7:1.5" wb_up.jpg

    結果:2.5 Pro 從 8/12 掉到 6/12,3 Flash Preview 從 6/12 掉到 2/12。放大反而讓準度下降。

    事後想通:Lanczos 是重採樣,不是超解析。它只是讓相鄰像素平滑插值,沒有增加資訊量。原本 3 像素的磁鐵放大成 12 像素,但那 12 像素是模糊的平均值,模型反而更難從中辨認形狀。

    要真的增加資訊量必須用 AI 超解析(Real-ESRGAN 這類),而不是幾何插值。但那會在機器上增加 PyTorch + 模型的負擔。與其走這條,不如直接面對問題:RTSP 這條路本身就有上限

    關鍵 Pivot:把 human 設計進 loop

    這是今天最重要的決策點。

    我原本堅持 RTSP 全自動,理由是「靠人不穩定,請妹妹拍她都不拍」。這個理由本身沒錯 —— 強制依賴他人的流程一定會斷。但我把「靠他人」跟「靠自己」混為一談了。我自己每天可以拍照,不穩定的只是「依賴別人」這一環

    更深層的領悟:為什麼執著於「100% 自動」?因為我下意識想把人完全排除。但對一個給醫生回診參考的月報系統來說,人的角色不該是全被排除,而應該是最後一道驗證。理由:

    • OCR 永遠有錯,醫療資料的錯誤比缺失代價更高
    • 家屬(我)本來就是最終看月報的人,有修正權限跟動機
    • 拍照的動作本身就是「我已經確認白板內容正確」的確認點
    • 沒有必要為了系統 purity 去承擔「誤導醫生」的風險

    這是整個專案最有價值的 design pattern 轉變 —— 從「zero-human automation」到「human-as-verifier」。具體分工:

    角色職責
    我(家屬)看白板、判斷、拍清楚的照片、事後在 iDempiere 修正 OCR 誤判
    Bot + Gemini 3把照片轉成結構化資料寫進 iDempiere、附上原檔
    iDempiere永久保存、提供 UI 讓月底回診時查閱

    最可怕的情境 —— OCR 幻覺亂寫進 DB 誤導醫生 —— 被「我本來就要檢查 iDempiere」這個 policy 擋住了。系統可以不完美,因為人在迴圈裡。

    Telegram Bot 架構

    決定走 human-in-the-loop 後,剩下的工程問題是「怎麼把手機拍的照片快速送到 server」。選項比較:

    方案優點缺點
    Telegram Bot零 app 安裝、即時回饋、從任何地方都能傳要建 bot token
    Syncthing 資料夾同步純本機、無雲端要裝 app + 手動移動檔案
    自架 HTTP 上傳頁不用 app要開 browser、每次登入

    Telegram 明顯最低摩擦,而且我本來就用 Telegram 傳照片 debug。一個 @BotFather 建 bot、5 分鐘拿到 token、寫一個 Python 長輪詢腳本就搞定。

    Bot 的最終能力:

    使用者動作Bot 行為
    📸 傳白板照片Gemini 3 Flash Preview OCR → 找今天紀錄 → update 或 create → 附加照片 → drain 文字佇列
    📝 傳 LINE 居服員回報文字[SHIFT|HH:MM] 前綴 → prepend 到今天紀錄的 Description 欄位
    🕑 今天還沒紀錄時傳文字存 pending 佇列,等照片來時自動塞入
    /pending, /clear, /id管理指令

    Shift 標記:用時段而不是時間點

    iDempiere 既有資料的 Description 欄位有一個我沒注意到的約定格式:

    [NIGHT|20:13]今天把東西往外丟
    [DAY|17:56]攻擊居服員

    格式是 [SHIFT|HH:MM]事件,新的在最上面,\n 分隔。照顧者三班制:

    • DAY(早班):07:00-17:59
    • NIGHT(晚班):18:00-23:59
    • GRAVEYARD(大夜):00:00-06:59

    Bot 根據當下時間自動產生正確的 shift 標記。這個設計好處是醫生看月報時能直接對應到「哪一班回報的事件」,這對失智照護尤其重要 —— 同一個病人,不同班次的表現可能完全不同(例如日落症候群)。

    Find-or-Create:同一天一筆紀錄

    一個設計決策:同一天傳多次照片要建新紀錄還是更新舊的?

    選項:

    • (a) 每次都 create 新的 → iDempiere 一天可能有多筆,報表聚合需要 GROUP BY 日期
    • (b) 同天 find-or-update → 資料乾淨,但要實作 OData filter 查當天紀錄
    • (c) 帶時序保留所有版本 → 最完整但最複雜

    選 (b)。理由是月底回診時醫生看的是「今天的狀態」,不需要看一天之內的修改歷史。實作核心:

    def idempiere_find_today(token):
        date_str = datetime.now().strftime("%Y-%m-%d")
        r = requests.get(
            f"{IDEMPIERE_URL}/models/z_momsystem",
            headers={"Authorization": f"Bearer {token}"},
            params={"$filter": f"DateDoc eq '{date_str}' and Name eq '{PATIENT_NAME}'", "$top": 1},
        )
        recs = r.json().get("records", [])
        return recs[0]["id"] if recs else None

    還有一個巧妙設計:OCR 回 null 的欄位不進 PUT payload。這樣早上拍一次只填前 6 項、晚上拍一次只改後 6 項,早上的資料不會被晚上的 null 洗掉。整個合併邏輯在 Python 端就搞定,iDempiere 端只接受「明確填入」的欄位。

    iDempiere 附件上傳的小坑

    iDempiere REST API 的附件端點是 POST /models/{table}/{id}/attachments,payload 是 {"name": "...", "data": "base64..."}。踩了一個坑:重複檔名會回 409

    解法:上傳時把當下的 HHMMSS 塞進檔名 suffix。同一張照片重複上傳也不會衝突:

    ts = datetime.now().strftime("%H%M%S")
    fn = f"{orig.stem}_up{ts}{orig.suffix}"

    Systemd Service 自動重啟

    最後收尾:bot 用 systemd service 跑,開機自動啟動 + 崩潰自動重啟。關鍵是設定 Restart=on-failureRestartSec=10

    [Unit]
    Description=Tapo Caregiver Telegram Bot
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    Type=simple
    User=tom
    WorkingDirectory=/home/tom/tapo-caregiver
    ExecStart=/home/tom/tapo-caregiver/venv/bin/python telegram_bot.py
    Restart=on-failure
    RestartSec=10
    Environment=PYTHONUNBUFFERED=1
    
    [Install]
    WantedBy=multi-user.target

    成本:免費版剛好夠用

    這個專案我原本因為 debug 爆量升級了 Google AI Studio 付費版。但實際跑 production 的時候算了一下用量:

    • 晚上 8 點拍一次照片 = 1 次 Gemini 3 Flash Preview 呼叫
    • LINE 回報文字 = 0 次(純文字不進 OCR,只是 prepend 到 Description)
    • 一天總用量:1~2 次

    Gemini 3 Flash Preview 的免費 tier 是 每天 20 次GenerateRequestsPerDayPerProjectPerModel-FreeTier, limit: 20, model: gemini-3-flash),實際從先前的 429 錯誤訊息確認過。

    也就是說 完整上線後可以馬上取消付費,免費額度就夠日常跑。debug 階段的付費只是為了不被 quota 卡住開發速度。

    兩天下來的反思

    1. 物理限制不是模型問題,不要用演算法去試

    RTSP 畫面中磁鐵 3~5 像素這件事,不管換什麼模型、不管怎麼放大、不管怎麼 prompt,都解決不了。我浪費了兩輪嘗試才認清:先驗證訊號源的解析度是否足夠,再決定上面要疊什麼演算法

    2. 便宜模型會偽裝在看

    Gemma 4 和 Gemini 2.5 Flash 在困難任務上會套預設答案(全 box 1 或全 box 2),這種偽答剛好能騙過不注意的準度統計。同一張圖跑兩次是揭穿偽答最便宜的方法 —— 真的在看的模型答案應該高度一致,套公版的會變。

    3. 投票救不了系統性偏誤

    Gemini 2.5 Pro 跑 3 次投票準度從 8/12 掉到 6/12,因為它的錯誤是系統性的(3 次都答同一個錯的答案)。投票只對隨機噪音有用,對系統性偏誤沒用。想救系統性偏誤,要用不同模型交叉比對,而不是同一個模型多次跑。

    4. Human-in-the-loop 不是妥協,是正確設計

    我原本想做 100% 自動化是出於「系統純粹」的直覺。但對醫療情境來說,把人排除才是風險來源。正確問題不是「怎麼不靠人」而是「人在哪個環節最有價值」。答案是:人當最終 verifier,機器做勞力密集的 OCR + 結構化 + 儲存。

    5. 工具思維 vs 平台思維

    我原本想把 RTSP 攝影機變成一個「全知白板記錄系統」,這是平台思維,野心大但難。pivot 之後變成「Telegram bot 幫我打字少打一些」,這是工具思維,目標小但能跑。工具思維的上線速度 ≫ 平台思維的完美程度

    6. 讓 AI 自己跑模型賽馬

    過程中我讓 AI 自動跑一圈 Gemma 4 / 2.5 Flash / 2.5 Pro / 3 Flash Preview,把命中率、錯誤類型、各自的 observation 都列出來。我只要看那張表就能判斷用哪個。這種「讓 AI 幫你比較 AI」的 meta-loop 是 vibe coding 很爽的一點 —— 選型決策從「讀文件猜性能」變成「直接實測算分」。

    最終上線狀態

    組件狀態
    Telegram bot(@your_bot_name,白名單限制)✅ LIVE
    Gemini 3 Flash Preview OCR (schema-constrained + box index + layout map)
    iDempiere REST find-or-create + attachment upload
    Description shift tag (DAY/NIGHT/GRAVEYARD) 自動 prepend
    pending notes queue(文字先到、照片後到的情境)
    systemd service 開機自動啟動
    PTZ ONVIF 控制(暫時不用,因為 pivot 到手機路線)✅ 備用

    每日 SOP:

    1. 晚上 8 點我拍一張白板照片
    2. Telegram 分享給 @your_bot_name
    3. Bot 跑 OCR + 寫 iDempiere + 回覆結果(10~20 秒)
    4. 看到有讀錯的,打開 iDempiere 手動改
    5. 居服員白天在 LINE 的回報,我看到時直接貼到 bot,自動 prepend 到今天的 Description
    6. 月底回診時點開 iDempiere 給醫生看趨勢

    總結:這篇文章的「二階 vibe coding」

    這篇文章本身也是 vibe coding 的產物 —— 我叫 AI 把兩天的工作回溯成一篇技術文,自己只負責審核跟指正敏感資料。這種「AI 幫你跟 AI 協作的過程寫回顧」也算是一種 meta-loop:決策的當下有 AI 幫忙驗證,事後有 AI 幫忙紀錄。我負責方向跟價值判斷,剩下的雜活都給 AI。

    兩天下來最重要的一句話:「vibe coding 不是減少思考,是加速思考。」 AI 幫你快速驗證假設、生成原型、寫測試、跑實驗、甚至幫你寫反思文章。但你要知道該往哪個方向走、什麼時候該 pivot、什麼時候該停手。AI 是踏板車,你是駕駛。

  • Ionic 逆向重建 Flutter 完整踩坑紀錄:三套 APP、20+ 個坑、一套方法論

    重點摘要

    • 從 Ionic 1 / AngularJS + WordPress PHP 逆向重建三套 Flutter APP,歷時一週,踩了 20+ 個分類的坑
    • 最毒的方法論錯誤:審查時讀了原始碼,實作時卻只靠缺失清單——導致多輪審查永不收斂
    • PHP Broker API 的 Content-Type 是 text/html、數值欄位全是 String、token 欄位名稱每個子系統不同
    • WordPress Multisite 的 table prefix 陷阱:直接打 plugin 路徑走 main site(wp_),跟 WP Admin 看到的(wp_2_)不同
    • 逆向工程的核心原則:原始碼是唯一 ground truth,不是用戶的記憶,不是截圖,不是 AI 的推測

    這篇文章記錄了一個完整的逆向重建專案:將一套社區管理系統(三套 APP + WordPress 後端)從 Ionic 1 / AngularJS 遷移到 Flutter。不是新開發,是一比一逆向複製——原版有的每一個按鈕、每一個欄位規則、每一個邊角功能,新版都必須完全復現。

    三套 APP 分別是:住戶 APP(社區公告、包裹、費用等)、管理平板(出勤、包裹管理、公設租借、訪客登記等)、門禁保全 APP(簽到簽退、巡邏、QR 掃描)。它們共用同一個 WordPress PHP 後端。

    以下是按主題分類的完整踩坑紀錄和方法論總結。

    一、逆向重建最毒的方法論錯誤是什麼?

    逆向重建最毒的方法論錯誤是「審查讀原始碼、實作不讀」。這聽起來不可能,但在 AI 輔助開發中極其常見:

    1. 審查 Agent:從原版 HTML / JS 逐行讀 → 產出缺失清單 → 每項有行號出處 ✅
    2. 實作 Agent / 新 Session:拿到缺失描述(如「公告無窮捲軸缺失」)→ 直接寫 Flutter → 沒有讀原版邏輯
    3. 結果:修了 A,同頁的 B/C/D 仍缺(因為根本沒讀那頁的完整原始碼)
    4. 下輪審查:再從原版讀 → 再找到 B/C/D → 循環不斷

    我們經歷了 7 輪審查才讓管理平板的缺失清單收斂。前 3 輪每輪都有 10+ 個新發現,根本原因只有一個——方向反了。

    建立的強制規則

    • 拿到缺失 ID 時,必須先讀對應的原始碼(file:line),不得只靠缺失描述動手
    • 修某頁某功能時,如果手上沒有該頁完整 Source Inventory → 必須先做 Source Inventory
    • Review 方向必須是:原版 HTML → 原版 JS → 原版 PHP → 列清單 → 逐項對照新版。禁止從 Flutter 出發猜原版有什麼

    二、Source Inventory 協議:逆向工程的唯一正確開始方式

    Source Inventory 是在動手寫任何程式之前,先把原版的所有功能逐行列出來的過程。每一項都必須標注原始碼來源(檔案 + 行號),沒有行號的項目等於捏造的。

    - [ ] 登出按鈕
          來源:account.html:142 <button ng-click="logout()">
    
    - [ ] 棟別/樓層兩層下拉選單
          來源:visitor-form.html:55-67, controllers.js:9366-9398
    
    - [ ] 黑名單檢查(借用簽名後、API 呼叫前)
          來源:publicequip.js:2040-2052

    清單涵蓋四大類:所有 UI 元素(按鈕、欄位、badge、空狀態提示)、所有互動行為(點擊、滑動、下拉刷新)、所有 API 呼叫及欄位映射所有頁面入口/出口

    最容易漏的永遠是邊角功能:登出藏在 header component、空狀態提示在條件分支、黑名單檢查在簽名驗證之後。主流程之外的東西,AI 不會主動去找。

    三、PHP Broker API 的七大陷阱

    這套系統的 API 層是 WordPress PHP plugin,所有請求都走同一個 Broker endpoint。以下是我們踩過的每一個 API 相關的坑:

    3.1 Content-Type 是 text/html

    PHP Broker 的回應 Content-Type 是 text/html 而非 application/json。Dio(Flutter HTTP 套件)如果設了 ResponseType.json,會解析失敗。必須設 ResponseType.plain 再手動 jsonDecode

    3.2 數值欄位全是 String,除了偶爾不是

    大多數 JSON 數值欄位回傳的是字串("id":"1")。但 $wpdb->get_results() 對 int DB 欄位有時會回傳 PHP native int(JSON 裡沒有引號)。解法:所有 nullable String 欄位一律用 ?.toString(),避免 type 'int' is not a subtype of type 'String?'

    3.3 Token 欄位名稱每個子系統不同

    子系統 Token 放在 例外
    主 Broker(住戶/平板)data.cookie
    Guard Broker(門禁)data.tokenvalidate_authdata.cookie

    搞混了就會被判定為 anonymous user,API 回 "非有效角色,無法使用!",而且沒有任何認證失敗的明確錯誤碼

    3.4 returnCode 不統一

    "OK""0"(package 模組)、"1""ERROR""dup"——全是字串,不要做整數比較。每個模組的成功碼不同,必須逐一確認。

    3.5 Category / Method 命名混亂

    有的是 snake_case(get_ticket_by_member_id),有的是 camelCase(retrivePublicEquipTXN——對,retrive 是拼錯的),有的是完全不同的規則(createTableAccount)。必須從 PHP 程式碼抄出精確字串,不能猜。

    3.6 Response 結構不一致

    • mutual:回 {rows_1:[], rows_2:[]}(自己的 + 社區其他人的)
    • ticket:回 {data: {rows, total, totalPages}}(分頁結構)
    • package_light:returnCode 用 "0" 而非 "OK"
    • getTwCode:空表時回 data: ""(空字串),不是 []

    3.7 磁卡批次 API 是 flat array

    大部分 API 的 data 格式是 {data: {key: value}},但磁卡批次領取是平坦陣列 [{id, card_no, status}]。PHP 端用 is_array($data) 判斷。這意味著 Flutter HTTP client 的 data 參數不能寫死為 Map,必須改成 dynamic

    四、WordPress Multisite 的 Table Prefix 陷阱

    WordPress Multisite table prefix 陷阱是整個專案最令人崩潰的問題之一。直接打 plugin PHP 路徑(如 /wp-content/plugins/.../API_Broker.php)時,WP 識別為 main site (blog_id=1),使用 wp_ 前綴。但在 WP Admin 介面、phpMyAdmin 看到的是 wp_2_(子站表)。

    後果:seed 測試資料時插到 wp_2_ulifeplus_*,但 API 查的是 wp_ulifeplus_*——資料永遠看不到,而且不報錯,就是空陣列。

    另外,改網域必須改 5 個地方wp_options(siteurl + home)、wp_blogs(domain)、wp_site(domain)、wp-config.php(DOMAIN_CURRENT_SITE + WP_HOME + WP_SITEURL)。DOMAIN_CURRENT_SITE必須含 port,否則會無限 302 redirect。

    五、validate_role() 全面封鎖事件

    所有 28 個 PHP endpoint 都在進入主邏輯前呼叫 validate_role()。原版只接受 ulifeplus_api / ulifeplus_admin / administrator / ulifeplus_operator,不含住戶帳號 ulifeplus_subscriber

    原因:原版平板用的是專屬服務帳號(QR code 開通碼流程),不是住戶帳號直接登入。重建版讓住戶直接登入 → 所有 API 一律 ERROR

    修補方案需要雙層:(1) patch PHP 接受 ulifeplus_subscriber;(2) 給測試帳號加 ulifeplus_api role。而且 wp_capabilities 的序列化格式 a:N:{s:LEN:"role_name";b:1;} 的 LEN 必須精確——寫錯一個字元,整個 unserialize 就失效,用戶變成沒有任何 role。

    六、三套 APP 的進化軌跡:從混亂到方法論

    第一套:住戶 APP——摸清後端規則

    住戶 APP 是最先做的,主要踩了後端相關的坑:Content-Type 是 text/html、數值欄位全是 String、member_id vs WP user_id 混淆、validate_role 全面封鎖、WP Multisite table prefix。這些經驗讓我們對 PHP Broker 的行為有了完整的心智模型。

    第二套:管理平板——方法論崩潰與重建

    管理平板是最複雜的(10+ 模組),也是方法論崩潰的戰場。7 輪審查、每輪都有新發現、多個 Agent 交叉審查卻互相誤報(false positive)。最終逼迫我們建立了 Source Inventory 協議、四大卡點規則、和「審查必須從原版出發」的強制流程。

    核心教訓包括:

    • Grid → Card List 轉換會遺失 checkbox column 和 cellTemplate 等「免費」UI 行為
    • 條件式按鈕(enableShowClosed 等)的每個 ng-if 都是業務意圖,不能合併或省略
    • smart_disp.get_hidden_code() 遮名演算法需要完整移植,不能簡化
    • 日期範圍查詢必須含時間(00:00:00 / 23:59:59),否則 BETWEEN 漏掉當天
    • Feature flag 的預設值不一定是 false,必須讀完整條件判斷

    第三套:門禁 APP——方法論成熟

    到第三套門禁 APP 時,Source Inventory 協議已經內化。整個 APP 從原始碼分析到 21/21 測試通過只花了一天。但還是踩了新坑:

    • Guard Broker 的 token 欄位是 data.token(不是主 Broker 的 data.cookie
    • PHP class constant const data = "date" 從不生效(bareword array key 行為)
    • showDialog 的 BuildContext 跨 async 導致確認 dialog 靜默失敗
    • HTTP 部署下相機 API 被瀏覽器封鎖(Secure Context 限制)
    • Flutter Web 的 SPA 狀態在測試間污染(需 full HTTP reload)

    七、Flutter 遷移的技術坑清單

    症狀 修法
    flutter_secure_storage Web 失敗HTTPS 才能用 Web Crypto APIWeb 改用 SharedPreferences
    Platform.isAndroid Web crashdart:io 在 Web 不可用kIsWeb + defaultTargetPlatform
    CORS 重複 headerAccess-Control-Allow-Origin: *, *PHP 已有 CORS,不要加 .htaccess
    Dialog context stale確認後邏輯靜默跳過dialogCtx 不用外層 context
    Release build debugPrint 消失完全無錯誤訊息寫 SharedPreferences breadcrumb
    ConsumerWidget 無 initState需要 async 初始化改用 ConsumerStatefulWidget
    base-href 部署子路徑資源 404--base-href=/app/
    API_BASE_URL dart-defineWeb 連到 localhost--dart-define=API_BASE_URL=http://IP:PORT

    八、AI 輔助逆向工程的認知陷阱

    AI 輔助逆向工程的認知陷阱是這個專案最深層的教訓。AI 在讀原始碼方面能力極強,但在以下場景會系統性犯錯:

    1. 「我以為我知道這頁有什麼」——AI 會基於已讀的程式碼推測未讀的部分,經常猜錯
    2. 改名後誤報缺失——搜尋 scan_addr_code() 找不到,但 Flutter 用了 _scanAddressCode(),同樣邏輯換了名字就被誤報
    3. 函數名稱 ≠ 實際行為——updateUser() 實際上還做 log 寫入、狀態機轉換;getActiveItems() SQL 是 status != 'deleted' 不是 status = 'active'
    4. 「延後功能」不是藉口——AI 傾向把不確定的功能標記為「deferred」,但只要原版存在的就必須實作

    破解方式:建立 Source Inventory 清單,每項附行號。行號是可審計的(可以回去原始碼驗證),AI 的記憶不可審計。

    九、跨專案知識管理:Brain 檔案系統

    為了讓踩過的坑不只活在一個專案裡,我們建立了 Domain Brain 系統:每個技術領域一個 markdown 文件,記錄所有踩過的坑、修法、和來源。

    例如 legacy-code-rebuild.md 記錄了逆向工程的通用方法論;wordpress-broker.md 記錄了 WP Broker API 的所有特殊行為。每次 fix: commit 時,強制要求更新對應的 brain file。

    核心原則:Brain = 上次做的時候踩了什麼坑(經驗)Skill = 正確的做法是什麼(模式)。兩個都要讀。

    十、建議給想做逆向重建的團隊

    1. Source Inventory 先行——不管系統多簡單,先把所有功能列出來(附行號)。簡單的系統往往有最多隱性規則
    2. 後端先跑通——先用 curl 打通每一個 API endpoint,確認 request/response 格式,再寫 Flutter
    3. 每個模組獨立測試——不要等到所有模組做完才整合。我們的 21/21 unit + integration test 在開發過程中抓到了大量問題
    4. 記錄每一個坑——Brain 系統不是可選的,是必要的。第一套 APP 踩的坑如果沒記下來,第三套 APP 會再踩一次
    5. 不要信任 AI 的記憶——AI 會遺忘、會推測、會混淆。只有程式碼行號是可驗證的

    結語

    這個專案讓我深刻體會到:逆向重建的難度不在於「寫新程式」,而在於「完整理解舊系統」。舊系統的每一行 if/else 都是某個真實場景的反映,每一個看似多餘的欄位都有它存在的理由。

    AI 可以加速這個過程,但無法取代「逐行讀原始碼」這個步驟。捷徑是最遠的路——我們前 3 輪審查走了捷徑(從 Flutter 出發找問題),花了 3 倍時間;改用正確方法論(從原版出發列清單)後,第三套 APP 一天就完成了。

    希望這篇記錄能讓正在考慮做逆向重建的團隊少走一些彎路。

    追記:驗收階段又踩的五個大坑(2026-04-13 深夜)

    上面的文章發佈後幾小時,用戶開始實際驗收。結果又炸出一系列問題,每一個都是在開發階段「看了原始碼但沒看完整」造成的。

    坑 10:Dart 的 Map<dynamic, dynamic> 型別推導陷阱

    Flutter 的 API client 在呼叫 api.call(data: {}, token: token) 時,{} 空 Map literal 在 dynamic 參數中被 Dart 推導為 Map<dynamic, dynamic>。原本程式碼檢查 effectiveData is Map<String, dynamic>false → token 永遠不被加到 request 裡。

    影響範圍:27 個 API call。所有傳 data: {} 的頁面全壞(儀表板、帳號開通等),但傳了明確 key 的頁面(如 data: {'address_code': x})正常。這造成「有些頁面好有些壞」的假象,極難排查。

    教訓:原版 JavaScript param.token = value 不做任何型別檢查,直接賦值。重寫到靜態型別語言時,不要加「聰明」的型別守衛。笨方法之所以沒 bug,就是因為它笨。

    坑 11:config.js 常數映射層被完全忽略

    原版 Ionic 有一層 config.js 常數映射:sel → "select"upd → "update"del → "delete"add → "insert"sel2 → "select2"。Flutter 重寫時,有些地方用了短名(config.js 的 key),有些用了長名(PHP 接受的值)。PHP 只認長名。

    影響範圍:19 個 API call。寄放交辦頁面完全打不開(PHP switch/case 不匹配,回 ERROR 但 message 空字串),debug 時完全看不到線索。

    教訓:逆向重建時必須識別原版的每一層抽象。看了 controllers.js(呼叫端)和 factories.js(發送端)不夠——中間的 config.js 映射層不能跳過。每一個字串值都必須追到 PHP handler 的 switch/case 確認一致。

    坑 12:捏造不存在的 API method

    Flutter 程式碼裡出現了 method: 'select_category_detail'(包裹統計)和 category: 'util'(多帳號驗證碼)等原版完全不存在的 API call。追溯原始碼發現:前者在原版是純客戶端計算(groupBy + filter),後者正確的 category 是 'app_member'、method 是 'register_switchsite_validation_code'

    教訓:如果在原始碼裡找不到某個 API endpoint,那它就不存在。不要「合理推測」一個 API 可能存在。

    坑 13:API URL 不應該寫死在 compile time

    Flutter 用 --dart-define=API_BASE_URL=http://192.168.0.48:8010 在 compile time 寫死 API URL。結果 LAN 和 HTTPS 要分別 build,Cloudflare 快取舊 JS 時 URL 就錯。

    原版做法:API_BROKER_URL 在 config.js 是空字串,QR 碼啟動或登入時從 server 回傳 burl(blog_site_url),組合成完整 URL 存入 localStorage。

    修法:Web 版用 Uri.base.origin 自動偵測當前頁面的 origin 作為 API base URL。不管從哪個網域開,API 自動跟著。compile time 的 --dart-define 只作為 fallback。

    坑 14:外部服務走錯路由

    廣告 Banner API 在原版是打一個完全獨立的外部伺服器(vender01.tw),不是走 WP Broker。Flutter 把它包成了 category: 'ad_banner' 送進 WP Broker,當然找不到。

    教訓:原版有多個 factory(brokerFactorycommonServerFactoryomFactoryapi-vender),每個打不同的 server。重寫時必須識別每個 API call 走的是哪個 factory、打的是哪個 server。

    總結:為什麼驗收時才發現這些問題

    以上 5 個坑有一個共通點:「看了原始碼」不等於「看完了原始碼」。每次都是「看了主要的幾個檔案」但漏了中間層(config.js 映射、factory 路由、型別推導規則)。

    逆向重建的正確審查流程應該是:從 PHP handler 的 switch/case 開始,反向追每一個常數值回到 config.js,再追到 controllers.js 的呼叫端。不是從呼叫端出發猜 PHP 接受什麼。方向反了,就永遠追不完。

  • Flutter Web + Selenium E2E 測試踩坑全紀錄:九大實戰教訓

    重點摘要

    • Flutter Web 將所有 UI 渲染到 Canvas,Selenium 必須透過 Tab 鍵啟動語意樹(flt-semantics)才能操作元素
    • showDialog 的按鈕呼叫 Navigator.pop 時,必須使用 dialog 自己的 BuildContext,否則 async 後 context stale 會導致靜默失敗
    • HTTP 部署無法使用相機(非 Secure Context),需實作手動輸入 fallback
    • Release build 的 debugPrint 完全靜默,debug 必須靠 SharedPreferences breadcrumb + Selenium 輪詢 localStorage

    本文記錄了在 Flutter Web 應用上進行 Selenium E2E 自動化測試時踩過的所有坑。這不是教科書式的教學,而是一次完整的實戰復盤——從「為什麼 Selenium 找不到任何元素」到「為什麼確認 dialog 回傳 null」,每一條都是反覆碰壁後才理解的真相。適合正在做 Flutter Web 自動化測試、或考慮將 Ionic/AngularJS 遷移到 Flutter 的開發者。

    一、Flutter Web 語意樹(Semantics Tree)是什麼?為什麼 Selenium 找不到元素?

    Flutter Web 語意樹是 Flutter 為了無障礙功能(Accessibility)而產生的一組 DOM overlay 元素。與 React 或 Vue 不同,Flutter Web 預設將所有 UI 渲染到 <canvas> 元素中,DOM 裡沒有任何可見的文字節點。這意味著 Selenium 的常規定位方式(XPath 文字搜尋、CSS text selector)完全失效。

    如何啟動語意樹?

    語意樹由 flt-semantics 自定義元素組成,但預設是未啟動的。啟動方式只需要一行:

    driver.find_element(By.TAG_NAME, "body").send_keys(Keys.TAB)

    一次 Tab 鍵足以觸發整個語意樹。之後,所有語意元素都會以 <flt-semantics> 出現在 DOM 中。讀取文字的穩定做法:

    text = driver.execute_script("return arguments[0].innerText", element)

    語意角色速查表

    Flutter Widget CSS Selector 備註
    ElevatedButton / TextButtonflt-semantics[role='button']innerText 包含按鈕文字
    BottomNavigationBar Tabflt-semantics[role='tab']innerText 含 “Tab X of Y”
    SwitchListTileflt-semantics[role='switch']title 文字不在 innerText!
    FloatingActionButtonflt-semantics[flt-tappable]tooltip 文字在 innerText
    DropdownButtonflt-semantics[role='button']同按鈕角色

    最大的坑:SwitchListTile 的 title 文字(如「開始巡邏」)完全不會出現在語意樹的 innerText 中。你必須用 role='switch' 去定位元素,而不是搜尋 title 文字。這個坑讓我浪費了整整一小時。

    ScrollView 內容不在語意樹

    AlertDialog 裡的可滾動 ListView 內容(如 RadioListTile 選項列表)全部以 Canvas 渲染,不在 flt-semantics 中。測試時只能驗證「dialog 是否開啟」(確認「關閉」按鈕出現),無法直接驗證列表內容。

    二、Flutter Web SPA 狀態污染:為什麼 driver.get() 沒有重置頁面?

    Flutter Web SPA 狀態污染是指在同一個 Flutter 應用中,前一個測試留下的 widget 狀態(dialog、表單、loading)會影響下一個測試。這是因為 Flutter Web 使用 hash-based routing,driver.get(url + "/#/settings") 如果 hash 沒有變,瀏覽器不會發出新的 HTTP 請求,Flutter widget tree 完全保持原狀。

    # ❌ 錯誤:如果已在 #/settings,不會重置
    driver.get("http://host:port/app/#/settings")
    
    # ✅ 正確:先到無 hash URL(觸發真正的 HTTP reload)
    driver.get("http://host:port/app")         # Step 1: full reload
    time.sleep(3)
    driver.find_element(By.TAG_NAME, "body").send_keys(Keys.TAB)  # Step 2: 啟動語意樹
    time.sleep(1)
    driver.execute_script("window.location.hash = '/settings'")   # Step 3: SPA 導航
    time.sleep(2)

    這個模式在 session-scoped fixture 中尤其重要。當多個測試共享同一個 driver 時,前一個測試的 dialog 可能還開著,下一個測試找不到預期的元素。

    三、BuildContext 跨 async 使用的致命陷阱

    BuildContext 跨 async 陷阱是整個 debug 過程中最花時間的一個坑——3 小時才定位到根因。症狀是:確認 dialog 明明點了「是」,但後續邏輯完全沒執行,沒有任何錯誤訊息。

    問題程式碼

    Future<bool> _confirm(String message) async {
      final result = await showDialog<bool>(
        context: context,           // 外層 widget 的 context
        builder: (_) => AlertDialog(
          actions: [
            TextButton(
              // ❌ 用外層 context 呼叫 Navigator.pop
              onPressed: () => Navigator.pop(context, true),
              child: const Text('是'),
            ),
          ],
        ),
      );
      return result == true;  // null == true → false → 邏輯被跳過!
    }

    發生了什麼?

    1. showDialog() 開啟一個新的 Route(dialog route)
    2. 使用者點擊「是」
    3. Navigator.pop(context, true) 中的 context 是外層 widget 的 BuildContext
    4. 如果外層 widget 在 async 等待期間被 Framework 重建(例如 Provider 狀態更新),該 context 可能 stale
    5. Navigator.pop() 找到錯誤的 Navigator 或 pop 了錯誤的 route
    6. showDialog() 的 Future 得到 null(而非 true
    7. null == truefalse → 確認邏輯認為使用者「取消」了
    8. 所有後續程式碼被跳過,沒有任何錯誤訊息

    正確做法

    builder: (dialogCtx) => AlertDialog(
      actions: [
        TextButton(
          // ✅ 用 dialog 自己的 context
          onPressed: () => Navigator.of(dialogCtx).pop(true),
          child: const Text('是'),
        ),
      ],
    )

    通用規則:showDialog 的 builder 中,永遠使用 builder 參數提供的 context(命名為 dialogCtx),不要使用外層 widget 的 context。這在 Flutter 官方文檔中有提到「Don’t use BuildContext across async gaps」,但在 dialog builder 內部容易忽略。

    四、Release Build 下的靜默失敗:debugPrint 不見了

    Flutter release build 的靜默失敗是另一個時間殺手。flutter build web 產出的是 release build,以下行為與 debug build 完全不同:

    • debugPrint() 完全靜默——不會輸出到 browser console
    • assert() 完全跳過——不會觸發 assertion error
    • 未被 catch 的 async exception 由 Flutter Framework 處理,通常靜默記錄

    Debug Breadcrumb 技巧:寫入 SharedPreferences,Selenium 讀 localStorage

    // Dart side — 在 try/catch 每個步驟寫 breadcrumb
    try {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('flutter.dbg', 'step_A: loading points');
      // ... 業務邏輯 ...
      await prefs.setString('flutter.dbg', 'step_B: saved record');
    } catch (e) {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('flutter.dbg', 'ERROR: $e');
    }
    
    # Selenium side — 輪詢 localStorage
    for i in range(20):
        time.sleep(0.5)
        dbg = driver.execute_script("return localStorage.getItem('flutter.dbg')")
        print(f't={i*0.5}s  dbg={dbg}')
        if dbg and 'ERROR' in dbg: break

    這個技巧讓我在 5 分鐘內定位到「_confirm() 回傳 null」這個根因,而之前靠 console.log 完全看不到任何線索。

    五、Secure Context 限制:HTTP 部署的相機和 Service Worker 全掛

    Secure Context 是瀏覽器對某些敏感 Web API 的安全限制。以下 API 只在 HTTPS 或 localhost 下可用:

    • navigator.mediaDevices.getUserMedia()(相機 / 麥克風)
    • navigator.serviceWorker(Service Worker / PWA)
    • window.crypto.subtle(Web Crypto API,flutter_secure_storage 依賴此 API)

    用 HTTP + IP 位址部署 Flutter Web(如 http://192.168.x.x:8010),嘗試使用 mobile_scanner 掃描 QR code 會得到:

    MobileScannerException(unsupported, This browser does not support displaying video from the camera.)

    解決方案:Web fallback 自動降級

    @override
    void initState() {
      super.initState();
      if (kIsWeb) {
        // Web 版直接跳手動輸入,不嘗試開相機
        WidgetsBinding.instance.addPostFrameCallback((_) {
          _showManualInputDialog();
        });
      }
    }
    
    // 相機版的 errorBuilder 也要處理
    errorBuilder: (context, error, child) {
      if (error.toString().contains('unsupported')) {
        _showManualInputDialog();  // 降級為手動輸入
      }
      return child ?? const SizedBox.shrink();
    }

    生產環境建議架設 HTTPS(Let’s Encrypt / Nginx 反向代理),即可解除所有限制。開發環境用 localhost 也自動視為 Secure Context。

    六、SharedPreferences 在 Flutter Web 的雙重 JSON 編碼

    Flutter Web 的 shared_preferences 套件底層使用 localStorage,但有兩個容易踩坑的特性:

    Key 前綴

    prefs.setString('my_key', 'value');
    // 實際存儲為: localStorage['flutter.my_key']

    值的雙重 JSON 編碼

    prefs.setString('foo', '[{"id":1}]');
    // localStorage 實際內容: '"[{\\"id\\":1}]"'
    // 注意外層多了一組引號 + 轉義
    
    # Selenium 讀取時要解兩層
    import json
    raw = driver.execute_script("return localStorage.getItem('flutter.foo')")
    layer1 = json.loads(raw)     # 解 SharedPreferences 包裝
    parsed = json.loads(layer1)  # 解原始 JSON 內容

    七、非同步快取的時機問題:為什麼清單永遠是空的?

    非同步快取時機問題是一個隱蔽的 bug:App 啟動時以 async 方式從 API 抓取主資料(如巡邏點清單)並快取到 localStorage。但如果用戶在快取完成前就操作,快取為空 → 業務邏輯用空資料建立記錄 → 記錄一旦建立就再也不會重新抓取 → 永遠空白

    解法:On-demand fallback + 恢復修補

    Future<List<PatrolPoint>> _loadPatrolPoints() async {
      var rawPoints = await db.getCachedPatrolPoints();
    
      if (rawPoints.isEmpty) {
        // 快取為空 → 直接打 API 補抓(此 API 不需要 token)
        try {
          final live = await api.getNavPoints(token);
          rawPoints = live.map((item) => {
            'point_id': item['id']?.toString() ?? '',
            'point_name': item['navpoint_name']?.toString() ?? '',
          }).toList();
          if (rawPoints.isNotEmpty) await db.cachePatrolPoints(rawPoints);
        } catch (e) {
          debugPrint('Live fetch failed: $e');
        }
      }
    
      return rawPoints.map((p) => PatrolPoint(...)).toList();
    }
    
    // 恢復進行中任務時也要修補
    if (patrol != null && patrol.patrolPoints.isEmpty) {
      final fresh = await _loadPatrolPoints();
      if (fresh.isNotEmpty) {
        patrol = patrol.copyWith(patrolPoints: fresh);
        await db.updatePatrolRecord(patrol);
      }
    }

    八、Selenium + Firefox geckodriver 實戰注意事項

    Console Log 不可直接取

    Firefox geckodriver 不支援 driver.get_log('browser')。要捕捉 console 輸出,需要在頁面注入攔截器:

    driver.execute_script("""
        window.__logs = [];
        var orig = console.log;
        console.log = function() {
            window.__logs.push(Array.from(arguments).join(' '));
            orig.apply(console, arguments);
        };
    """)
    # 操作後讀取
    logs = driver.execute_script("return window.__logs")

    但在 release build 中,Flutter 的 debugPrint 不呼叫 console.log,所以這招也沒用。前面提到的 SharedPreferences breadcrumb 才是唯一可靠的方式。

    click_text() helper 的完整實作

    封裝一個通用的「找語意元素並點擊」函式,需要同時搜尋 button、tab、tappable 三種角色:

    def click_text(driver, text, timeout=15):
        """Click flt-semantics element whose innerText contains text."""
        end = time.time() + timeout
        while time.time() < end:
            selectors = [
                "flt-semantics[role='button']",
                "flt-semantics[role='tab']",
                "flt-semantics[flt-tappable]",
            ]
            for sel in selectors:
                for el in driver.find_elements(By.CSS_SELECTOR, sel):
                    t = driver.execute_script("return arguments[0].innerText", el) or ""
                    if text in t:
                        driver.execute_script("arguments[0].click()", el)
                        return
            time.sleep(0.5)
        raise AssertionError(f"Clickable element '{text}' not found")

    九、方法論:Flutter Web Debug 的正確排查流程

    當 Selenium 測試與 Flutter Web 出現異常時,依照以下順序排查,可以最快定位問題:

    1. 語意樹是否啟動?按 Tab 後檢查 flt-semantics 元素數量。為 0 → 重按 Tab
    2. localStorage 狀態正確嗎?檢查 flutter.* key,確認 token、快取資料是否存在
    3. HTTP 還是 HTTPS?相機、Service Worker、Web Crypto 都需要 Secure Context
    4. Dialog 回傳值正確嗎?用 dialog 內部的 context 而非外層 context
    5. Release 還是 Debug build?debugPrint 在 release 完全靜默
    6. 寫 debug breadcrumb:try/catch 裡寫 SharedPreferences,Selenium 輪詢 localStorage
    7. SPA 狀態殘留?用 full HTTP reload(無 hash URL)強制重置 Flutter widget tree

    總結:Flutter Web + Selenium 是可行的,但需要正確的心智模型

    Flutter Web 的測試難度不在於「找不到元素」,而在於「找到了但行為不符預期」。Canvas 渲染 + 語意覆層的架構與傳統 HTML DOM 測試完全不同,需要建立新的心智模型。最大的兩個坑——BuildContext 跨 async 靜默失敗release build debugPrint 消失——可以讓你花上數小時 debug 而沒有任何錯誤訊息。

    掌握了這九個教訓後,我們的 Selenium 測試從 0 做到 40/40 全過,涵蓋了登入、導航、簽到/簽退、巡邏流程、設定頁面等完整 E2E 場景。希望這篇記錄能幫到正在走同一條路的開發者。