[{"content":"夏天到了，刷到 Morgan Eckroth 新發的 flash brew 影片，順手把 James Hoffmann 過去幾年三支冰咖啡影片跟查老師那支「一招解鎖超讚冰美式」湊到一起，發現手邊正好有六種不同做法可以並排看。\n六種不是「哪個最好」的排名，是六種不同器材、不同思路的解法。你家有 V60 跟有 espresso 機，會走完全不同的路徑；你有蒸汽棒跟沒有，也是不同故事。這篇是對照筆記，不是教學文。\n一個共同前提：冰咖啡跟熱咖啡不能用同一招 六種做法都在開頭做了類似鋪陳，但切入角度不同。\nMorgan 從風味取向切：cold brew 不管放什麼豆都會收斂成「圓潤、巧克力、低酸」一個味道，但 flash brew 可以保留豆子本身的 brightness 和 acidity。如果你買了一支淺烘 Ethiopian，cold brew 等於浪費它。\nHoffmann 從化學切：cold brew 把豆子的 origin character 抹掉，hot brew 才能把 roaster 用心烘的東西萃出來。但冰水稀釋熱 brew 帶來新問題 — 萃取的 brewing water 變少，要好好萃出 flavor 變困難。\n查老師 從感官神經科學切：低溫會讓舌頭對甜、酸、苦的感知都減弱（耶魯研究：舌頭碰冰甚至會誤感到鹹味）。所以冰咖啡要比熱咖啡萃得更濃，補償味覺鈍化。\n三個人不約而同：冰咖啡不能照搬熱咖啡的做法。但理由都不一樣。\n做法 A：Morgan 的 V60 + 冰塊（日式冰咖經典） 把總水量切成 60% 熱水 + 40% 冰塊。冰塊先放在 carafe 裡，熱水從上面 brew 下去直接淋融化。\n項目 數值 粉量 20 g 總水量 300 g（1:15） 冰塊 120 g（40%，用過濾水做的冰） 熱水 180 g（60%） 水溫 205°F (≈96°C) 研磨 比平常 V60 再細 1–2 click Bloom 50 g / 40 秒 第二 pour 130 g（總到 180 g），螺旋手法 目標 drain time 約 3 分鐘 關鍵點：冰塊要用同水質的過濾水做。它會融進咖啡裡喝下去，不是只用來冰鎮容器。\n做法 B：Morgan 的 Hyperchiller 急冷 用平常的熱 V60 配方做完，整杯倒進 Hyperchiller（雙層冷凍塊容器，亞馬遜約 $20），10 秒急冷後倒進冰塊杯。\nMorgan 明確警告：不要直接熱 brew 放冰箱冷藏。aromatics 在放涼過程中跑光，氧化讓 flavor loss。Hyperchiller 是「瞬間鎖住熱 brew 的複雜度，只是變冷」。\n做法 C：Hoffmann 的 Pour-over iced filter（2018） Hoffmann 2018 那支「Better than cold brew」短片給的配方。骨架跟 Morgan 接近，但更早，數字微調過。\n項目 數值 粉量 65 g / L brewing water（比平常多 5 g） 比例 40% 冰塊 / 60% 熱水（500ml 總水 → 200g 冰 + 300g 熱水） 研磨 比平常 pour-over 再細一點 Bloom 2-3× 粉量水量，至少 45 秒 總沖煮 2.5–3 分鐘 結束 順時針 stir 一次 + 逆時針 stir 一次 收尾 swirl decanter 把殘冰融光 → 倒到裝新冰的杯子 Hoffmann 強調：如果 brew 結束還有冰沒融完，冰加太多了。下次少加，讓更多空間留給 brewing water，萃取才好做。\n做法 D：Hoffmann 的 Clever immersion + saline（2023） 同一位 Hoffmann，但器材換成 immersion brewer（Clever、Hario Switch、AeroPress），主打更穩、更容錯。\n項目 數值 粉量 75 g / L（500ml → 37.5 g） 比例 2/3 熱水 + 1/3 冰（500ml → 330 g 熱水 + 170 g 冰） 研磨 比 pour-over 細，但不到 espresso 細 Brewer Clever（水→粉 順序） Steep 5 分鐘（4-7 都 OK） 結束 投冰 → stir 加速融化 收尾 加 saline 2-4 滴（80:20 鹽水，5g 鹽溶 20g 水） 跟 C 比起來的差異 C: Pour-over D: Immersion 器材 V60 / Kalita 等 Clever / Switch / AeroPress 粉量 65 g/L 75 g/L 冰比例 40% 33% 細研磨容錯 易 channeling 不會 channeling 計時容錯 分秒必爭 ±2 分鐘都 OK 新元素 — saline trick Hoffmann 補一招：冰不要太早拿出來。到 4 分鐘左右再從冷凍庫拿，避免冰在 carafe 裡偷融降溫不夠。\n為什麼 Immersion 粉量要再加 10 g/L Immersion 萃取效率比 percolation 低約 5-10%（相同萃取率下出來的咖啡偏淡），所以要補粉。Hoffmann 在另一支「Immersion Brewing is Better Than Percolation Brewing」有展開這條。\nSaline trick 是怎麼回事 冷咖啡的 perceived bitterness 比熱咖啡高。一點點鹽可以降低 perceived bitterness，不是讓咖啡變鹹（鹽濃度低於味覺閾值）。出自 Hoffmann 舊片「The Magic of Salt in Coffee」。\n做法 E：Hoffmann 的 Aerocano（espresso 路線） 也是 Hoffmann，但這次走 espresso 機路線。配方：雙份 espresso + 85 g 冰 + 65 g 冷水，用蒸汽棒蒸 10 秒。\ndouble espresso (撇 crema) + 65 g 冷水 + 85 g 冰 → 蒸汽棒蒸 10 秒 → nitro-cold-brew 般的綿密泡沫 10 秒的蒸汽融化部分冰塊但溫度不會升太高，同時打入空氣做出綿密泡沫。已經在韓國 Starbucks 菜單上（含糖版）。\n要做這招的兩個前置：\n機器有蒸汽棒 Americano 要先撇掉 crema（crema 苦，espresso 不用撇但 Americano 一定要撇） Hoffmann 在同支影片做了反例：把 Americano 抽真空 degas，結果變得 silky 但明顯更難喝。對照蒸汽打氣讓水好喝、抽真空把氣抽掉變難喝，他推測：\ndissolved gases 是朋友、不是敵人。\n做法 F：查老師的 Espresso 粉液比公式 也是 espresso-based americano，但她不講風味哲學，直接給數字。\n顛覆性的一招：不要用濃縮 g 數抓水量 她明確說常見做法是錯的 — 看到「double espresso 40 g 出液」就反推水量，但 18 g 粉跟 20 g 粉都萃 40 g，粉量差 11.1% 但杯量算法一樣，起始濃度根本不一樣。\n正確做法是用粉量 + 烘焙度抓整杯粉液比。\n烘焙 粉液比 淺烘 1:15 中烘 / 深烘 1:14 範例：19 g 中烘 → 整杯 19 × 14 = 266 g → 加水 160 g + 濃縮液 + 冰塊。\nTDS 對風味的影響 她用一個實驗驗證：同一杯萃好的濃縮搅拌均勻分成兩杯，分別加 TDS 50 與 TDS 200 的水（用 Aquacode 配水），味道天差地別。\n豆 TDS 推薦範圍 淺烘 50 – 200 中烘 / 深烘 / 酸度低豆 30 – 100 冰塊也要用同 TDS 範圍的水做，融化會混進去。\n起始濃度要拉高 熱咖啡（金杯） 冰咖啡（味覺鈍化補償） 濃度 1.15–1.35% 起始 1.5–1.6% 邏輯 標準萃取 隨冰融降到 ~1.2% 落回正常 她有明確提醒：冰咖啡的研究資料比熱咖啡少非常多，這個數字是推測。\n六種做法並排對照 變量 A: Morgan V60+冰 B: Morgan Hyperchiller C: JH Pour-over D: JH Immersion E: JH Aerocano F: 查老師 espresso 器材 V60 + 冰塊 V60 + Hyperchiller V60 等 pour-over Clever / Switch espresso + 蒸汽棒 espresso（任意） 路線 pour-over 變冰 熱 brew 急冷 pour-over 變冰 immersion 變冰 espresso 變冰 espresso 變冰 粉量基準 1:15 整杯 daily V60（1:15.5） 65 g/L 75 g/L double espresso 1:14-15 整杯 冰比例 40% 100%（事後） 40% 33% 大量（85g+65g 水） 加滿 研磨 比 V60 細 1-2 click daily 比 pour-over 細 中間（非 espresso 細） espresso espresso 計時 約 3 分鐘 急冷 10 秒 2.5-3 分鐘 5 分鐘 ±2 蒸汽 10 秒 espresso 萃 撇 crema n/a n/a n/a n/a 要撇 沒提 加 saline 沒提 沒提 沒提 2-4 滴 怕苦可加一滴 沒提 水質要求 過濾水做冰 沒展開 沒展開 沒展開 蒸汽水 TDS 50-200（明確） 起始濃度 沒給數字 沒給數字 沒給數字 沒給數字 沒給數字 1.5-1.6% 喝法 直接喝 直接喝 直接喝 直接喝 直接喝 不要用吸管 把這張表縱向掃過去，會發現幾個有意思的事。\n「水」這個變量誰都沒講同樣的事 Morgan A 強調冰塊要用過濾水做（會融進去喝下肚） Hoffmann C/D 沒展開水質，但 D 多了 saline trick 處理 perceived bitterness Hoffmann E 用蒸汽棒打過的水（dissolved gases） 查老師 直接給 TDS 範圍 50-200 四個人切四個維度，沒人重疊。\n粉量 / 冰量的取捨邏輯 冰會稀釋，所以粉量得補。但每個人補的量不一樣：\n用 pour-over → +5 g/L（Hoffmann C） 用 immersion → +15 g/L（Hoffmann D，因為 immersion 還比 percolation 弱） 用 espresso → 直接給粉液比（查老師） 冰加越多越冷、但 brewing water 越少越難萃 → 大家都在這條曲線上找邊際點，結論不同。\n計時容錯度 從「分秒必爭」到「兩分鐘都 OK」一路鋪開：\nE: Aerocano 蒸汽 10 秒 ← 最嚴格 A: V60 + 冰 ~3 min ±20 秒 C: JH Pour-over 2.5–3 min B: Hyperchiller 急冷 D: JH Immersion 5 min ±2 ← 最容錯 F: espresso 標準 D 在計時上幾乎是最 ergonomic 的選項。\n怎麼選：看你手邊有什麼 跳過「哪個最好」這個問題，直接按器材回答：\n你有什麼 你能做什麼 只有 V60 A、C V60 + 願意買 Hyperchiller A、B、C 有 Clever / Switch / AeroPress D（最容錯，新手友善） espresso 機（沒蒸汽棒） F espresso 機 + 蒸汽棒 E、F 全套都有 看心情、看豆子 對應的豆子取向 A、C、D：水洗淺烘 Ethiopian / Kenyan（突出果香 acidity） B：用你 daily 那支 E：espresso 烘焙或 filter-style lighter roast F：依烘焙度走粉液比 一些觀察 寫到這發現一件事：六種做法都沒有人從頭講「冰咖啡」。\nMorgan 的影片是 flash brew，本質是 pourover 的變奏 Hoffmann 三支：pour-over 升級、immersion 升級、Americano 升級 — 冰美式是其中一個應用 查老師的影片標題是「冰美式」，但通篇講的是粉液比 + 水 這也合理：冰咖啡不是一個獨立技藝，是熱咖啡的延伸 — 你的熱 brew / espresso 技術好不好、設備懂不懂，會直接決定冰咖啡能到哪裡。\n夏天到了，先把熱的搞穩，再來折騰冰的。\nRelated Daddy Got Coffee：5 個 pourover 槓桿系統 — pour over 變量結構（grind / ratio / temp / bloom / agitation）跟本文 A、C 段可以串 Morgan Eckroth 原片：Iced Coffee Recipe James Hoffmann：Better than cold brew James Hoffmann：Immersion Iced Coffee James Hoffmann：The Truly Absurd Secrets of an Incredible Americano 查老師原片：搞定這個要點，一招解鎖超讚冰美式 ","permalink":"https://blog.mklee.org/posts/2026-05-iced-coffee-three-recipes/","summary":"\u003cp\u003e夏天到了，刷到 Morgan Eckroth 新發的 flash brew 影片，順手把 James Hoffmann 過去幾年三支冰咖啡影片跟查老師那支「一招解鎖超讚冰美式」湊到一起，發現手邊正好有六種不同做法可以並排看。\u003c/p\u003e\n\u003cp\u003e六種不是「哪個最好」的排名，是六種\u003cstrong\u003e不同器材、不同思路\u003c/strong\u003e的解法。你家有 V60 跟有 espresso 機，會走完全不同的路徑；你有蒸汽棒跟沒有，也是不同故事。這篇是對照筆記，不是教學文。\u003c/p\u003e\n\u003ch2 id=\"一個共同前提冰咖啡跟熱咖啡不能用同一招\"\u003e一個共同前提：冰咖啡跟熱咖啡不能用同一招\u003c/h2\u003e\n\u003cp\u003e六種做法都在開頭做了類似鋪陳，但切入角度不同。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMorgan\u003c/strong\u003e 從風味取向切：cold brew 不管放什麼豆都會收斂成「圓潤、巧克力、低酸」一個味道，但 flash brew 可以保留豆子本身的 brightness 和 acidity。如果你買了一支淺烘 Ethiopian，cold brew 等於浪費它。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHoffmann\u003c/strong\u003e 從化學切：cold brew 把豆子的 origin character 抹掉，hot brew 才能把 roaster 用心烘的東西萃出來。但冰水稀釋熱 brew 帶來新問題 — 萃取的 brewing water 變少，要好好萃出 flavor 變困難。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e查老師\u003c/strong\u003e 從感官神經科學切：低溫會讓舌頭對甜、酸、苦的感知都減弱（耶魯研究：舌頭碰冰甚至會誤感到鹹味）。所以冰咖啡要比熱咖啡萃得更濃，補償味覺鈍化。\u003c/p\u003e\n\u003cp\u003e三個人不約而同：\u003cstrong\u003e冰咖啡不能照搬熱咖啡的做法\u003c/strong\u003e。但理由都不一樣。\u003c/p\u003e\n\u003ch2 id=\"做法-amorgan-的-v60--冰塊日式冰咖經典\"\u003e做法 A：Morgan 的 V60 + 冰塊（日式冰咖經典）\u003c/h2\u003e\n\u003cp\u003e把總水量切成 60% 熱水 + 40% 冰塊。冰塊先放在 carafe 裡，熱水從上面 brew 下去直接淋融化。\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e項目\u003c/th\u003e\n          \u003cth\u003e數值\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e粉量\u003c/td\u003e\n          \u003ctd\u003e20 g\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e總水量\u003c/td\u003e\n          \u003ctd\u003e300 g（1:15）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e冰塊\u003c/td\u003e\n          \u003ctd\u003e120 g（40%，用過濾水做的冰）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e熱水\u003c/td\u003e\n          \u003ctd\u003e180 g（60%）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e水溫\u003c/td\u003e\n          \u003ctd\u003e205°F (≈96°C)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e研磨\u003c/td\u003e\n          \u003ctd\u003e比平常 V60 再細 1–2 click\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eBloom\u003c/td\u003e\n          \u003ctd\u003e50 g / 40 秒\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e第二 pour\u003c/td\u003e\n          \u003ctd\u003e130 g（總到 180 g），螺旋手法\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e目標 drain time\u003c/td\u003e\n          \u003ctd\u003e約 3 分鐘\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e關鍵點：\u003cstrong\u003e冰塊要用同水質的過濾水做\u003c/strong\u003e。它會融進咖啡裡喝下去，不是只用來冰鎮容器。\u003c/p\u003e","title":"夏天的冰咖啡：六種做法的並排對照"},{"content":"最近 Anthropic 官方放了一支 Building the future of agents with Claude 的對談，由 Alex Albert（Claude Relations）、Brad Abrams（Claude Developer Platform PM）、Katelyn Lesse（Engineering Lead）三人主持。12 分鐘左右，涵蓋 Claude Developer Platform 改名、agent 的定義、unhobble the model、Claude Code SDK 作為 general-purpose agentic harness、context pruning、agentic memory primitive、observability。\n我在 Mac 上跑一個叫 cc-memory-project 的個人 agent 環境（從 OpenClaw workspace 演化），有自製的 hybrid memory search、knowledge graph、cron → flag → SessionStart hook pipeline。看完對談做了一些對映，挑兩個有具體 patch 落地的記錄一下。\n五點對映 對談重點 我的個人 agent 現況 落地動作 Unhobble the model — scaffolding 在新模型上會變成 liability spec/ 三檔 + AGENTS.md / CLAUDE.md 約 800 行 砍 6 段過時 scaffolding（Group Chats / Heartbeats / 返工循環段移走 / MM 從主力改 fallback / 工具決策改 reference / OpenClaw sync 段濃縮）約 -1050 tokens SDK 是 general-purpose agentic harness 用 Claude Code 本身 + cron/hook/skill 自製 harness 不需動 Context pruning + tombstone memory-archive.py 把舊月份 section 直接刪掉 加 tombstone 留痕跡（第一個 patch） Agentic memory primitive hybrid search + graphify + hall taxonomy + always-on recall 不需動，方向對 Observability for long-running tasks SessionStart hook prompt-budget-telemetry 已寫 JSONL 升級為結構化 event（第二個 patch） Patch 1：Tombstone for archive_timeline scripts/memory-archive.py 的 archive_timeline 會把 MEMORY.md 裡 ### 2026-XX 這種舊月份 section 搬到 memory/timeline-archive.md。原本邏輯是直接刪除：\nnew_text = text for s in reversed(sections_to_archive): new_text = new_text[:s[\u0026#34;start\u0026#34;]] + new_text[s[\u0026#34;end\u0026#34;]:] 問題：搬走後 MEMORY.md 完全不留線索，下次 LLM 開 session 讀 MEMORY.md 不知道「曾經有 March 的紀錄被歸檔」。\n對映影片裡 Brad 描述 Claude Developer Platform 的 context pruning：移除舊 tool calls 時保留一行 tombstone marker 給模型線索（\u0026ldquo;the tool results for the search call were here, and they\u0026rsquo;ve been removed\u0026rdquo;）。同思路套到 timeline archive：\narchived_at = today.isoformat() new_text = text for s in reversed(sections_to_archive): tombstone = ( f\u0026#34;### {s[\u0026#39;ym\u0026#39;]} _[archived → memory/timeline-archive.md \u0026#34; f\u0026#34;on {archived_at}, {s[\u0026#39;lines\u0026#39;]} lines]_\\n\\n\u0026#34; ) new_text = new_text[:s[\u0026#34;start\u0026#34;]] + tombstone + new_text[s[\u0026#34;end\u0026#34;]:] 效果：歸檔後在原位置留一行\n### 2026-03 _[archived → memory/timeline-archive.md on 2026-05-09, 12 lines]_ LLM 看到就知道 \u0026ldquo;March 不是不存在，是已經歸檔了\u0026rdquo;，要的話可以 Read 原檔。\n8 行的小 patch，但 mental model 從 \u0026ldquo;delete\u0026rdquo; 變成 \u0026ldquo;tombstone\u0026rdquo; 意義不一樣。\nPatch 2：Prompt budget alert 升級為結構化 event 我有一個 SessionStart hook 量「context 檔案 token 總量」（CLAUDE.md / SOUL.md / AGENTS.md / USER.md / MEMORY.md / MEMORY_COMPACT.md 加總），超 12000 tokens 就警告。原本警告長這樣：\nPrompt budget alert: spec files total 13662 tokens (budget 12000). 1. python3 .../memory-archive.py --mode both (cheap, mechanical) 2. If still over: /curate-memory consolidate (LLM, semantic) 兩個問題：\nJSONL log 沒有 over_budget: true 之類欄位，下游想 alert 要自己 grep 字串 Hint 不準確 — memory-archive.py 只動 memory/ 目錄不動 MEMORY.md 的 spec 段。如果超標來源是 MEMORY.md 本體（事實上就是，我的 6800 tokens 一半在 MEMORY.md），跑 archive 沒效果 呼應影片裡 Katelyn 講的 observability：long-running task 要能 audit、要能 steer、要能 tune prompt。這個 alert 是個微型 observability 端點，但提供的訊號太粗糙。\n升級之後：\nPrompt budget alert: context files total 13662 tokens (budget 12000, over by 1662). Top contributors: MEMORY.md=6829, CLAUDE.md=2393, AGENTS.md=2166 Hint: archive can\u0026#39;t shrink these files. Run /curate-memory consolidate or trim spec/CLAUDE.md/SOUL.md manually. JSONL 加結構化欄位：\n{ \u0026#34;event\u0026#34;: \u0026#34;budget_alert\u0026#34;, \u0026#34;over_budget\u0026#34;: true, \u0026#34;over_by\u0026#34;: 1662, \u0026#34;top_files\u0026#34;: [ {\u0026#34;name\u0026#34;: \u0026#34;MEMORY.md\u0026#34;, \u0026#34;tokens\u0026#34;: 6829}, {\u0026#34;name\u0026#34;: \u0026#34;CLAUDE.md\u0026#34;, \u0026#34;tokens\u0026#34;: 2393} ], \u0026#34;archivable_memory_sections\u0026#34;: 0 } archivable_memory_sections 是新加的——hook 自己掃 MEMORY.md 看有沒有 ### YYYY-MM 舊月份 section，沒有就不推薦跑 archive。從「不分情境推 archive」變成「先看症狀再開藥」。\n反向比較：哪些不需要學 不是所有對談重點都需要學。我的 hybrid search（BM25 + jieba CJK tokenize + temporal × hall boost）、knowledge graph 自動建立的 1-hop 關聯、MemPalace-inspired hall taxonomy（hall_facts / events / discoveries / preferences / advice）、Source-First 事實查證紀律——這些影片沒展示，我做得相對完整。\n差異在分工：他們的 memory primitive 是 model 自己寫筆記給自己讀；我現在是 user-curated（journal + curate-memory skill）+ always-on proactive recall hook 自動把 search 結果注入 prompt。換個角度看，hybrid search + always-on recall = 「我們的 memory primitive」，只是讓 cron 跟 hook 而非 model self-tool 來驅動。\n收尾 對談裡 unhobble 主軸最有感。Agent 隨時間累積規則，每一條當下都有理由，但要砍掉很難，就像 OOP 累積 abstraction 後沒人敢動。對談給我一個 review 視角：用「這條規則在新一代模型上是 liability 還是 asset」當篩子，比「這條規則合不合理」好用。\n兩個 patch 都不大（一個 8 行、一個 30 行），但思路到位就值得寫下來。Tombstone 那個會 backport 到我之前公開的 openclaw-workspace-template，用同一套 harness 的人應該也用得上。\n對談連結：\nBuilding the future of agents with Claude (YouTube) ","permalink":"https://blog.mklee.org/posts/2026-05-09-anthropic-podcast-tombstone-patch/","summary":"\u003cp\u003e最近 Anthropic 官方放了一支 \u003ca href=\"https://www.youtube.com/watch?v=XuvKFsktX0Q\"\u003eBuilding the future of agents with Claude\u003c/a\u003e 的對談，由 Alex Albert（Claude Relations）、Brad Abrams（Claude Developer Platform PM）、Katelyn Lesse（Engineering Lead）三人主持。12 分鐘左右，涵蓋 Claude Developer Platform 改名、agent 的定義、unhobble the model、Claude Code SDK 作為 general-purpose agentic harness、context pruning、agentic memory primitive、observability。\u003c/p\u003e\n\u003cp\u003e我在 Mac 上跑一個叫 \u003ccode\u003ecc-memory-project\u003c/code\u003e 的個人 agent 環境（從 OpenClaw workspace 演化），有自製的 hybrid memory search、knowledge graph、\u003ccode\u003ecron → flag → SessionStart hook\u003c/code\u003e pipeline。看完對談做了一些對映，挑兩個有具體 patch 落地的記錄一下。\u003c/p\u003e\n\u003ch2 id=\"五點對映\"\u003e五點對映\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e對談重點\u003c/th\u003e\n          \u003cth\u003e我的個人 agent 現況\u003c/th\u003e\n          \u003cth\u003e落地動作\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eUnhobble the model — scaffolding 在新模型上會變成 liability\u003c/td\u003e\n          \u003ctd\u003espec/ 三檔 + AGENTS.md / CLAUDE.md 約 800 行\u003c/td\u003e\n          \u003ctd\u003e砍 6 段過時 scaffolding（Group Chats / Heartbeats / 返工循環段移走 / MM 從主力改 fallback / 工具決策改 reference / OpenClaw sync 段濃縮）約 -1050 tokens\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eSDK 是 general-purpose agentic harness\u003c/td\u003e\n          \u003ctd\u003e用 Claude Code 本身 + cron/hook/skill 自製 harness\u003c/td\u003e\n          \u003ctd\u003e不需動\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eContext pruning + tombstone\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003ememory-archive.py\u003c/code\u003e 把舊月份 section 直接刪掉\u003c/td\u003e\n          \u003ctd\u003e加 tombstone 留痕跡（第一個 patch）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eAgentic memory primitive\u003c/td\u003e\n          \u003ctd\u003ehybrid search + graphify + hall taxonomy + always-on recall\u003c/td\u003e\n          \u003ctd\u003e不需動，方向對\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eObservability for long-running tasks\u003c/td\u003e\n          \u003ctd\u003eSessionStart hook prompt-budget-telemetry 已寫 JSONL\u003c/td\u003e\n          \u003ctd\u003e升級為結構化 event（第二個 patch）\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"patch-1tombstone-for-archive_timeline\"\u003ePatch 1：Tombstone for archive_timeline\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003escripts/memory-archive.py\u003c/code\u003e 的 \u003ccode\u003earchive_timeline\u003c/code\u003e 會把 \u003ccode\u003eMEMORY.md\u003c/code\u003e 裡 \u003ccode\u003e### 2026-XX\u003c/code\u003e 這種舊月份 section 搬到 \u003ccode\u003ememory/timeline-archive.md\u003c/code\u003e。原本邏輯是直接刪除：\u003c/p\u003e","title":"從 Anthropic 三人對談到我的 8 行 patch"},{"content":"顆粒度焦慮（再一次） 我在 V60 Switch 上面卡住有一陣子了。\n豆子在變、磨豆機刻度在動、水溫在試、開關閥的時機點在改。一杯偏酸往細調一點，下一杯又苦尾，往粗回半圈卻變寡淡。Switch 比一般 V60 多一個「閥開關」變因，本來想說多個工具應該更可控，結果反而更難 debug——因為任何一杯不對，我都不知道是顆粒度的問題、水溫的問題、還是我關閥的時機沒抓好。\n於是我想說那就乖一點，找個現成的 recipe 跟著跑，跑十杯穩定下來再來動參數。問題是 recipe 我找了一週還沒找到「我要跟誰」。\n原來大家在打架 打開 YouTube，James Hoffmann 教你 20g / 330g / 1:16.5、悶蒸關閥、單一水溫 95°C、2:30 開閥排水。Lance Hedrick 教你 15g / 250g、悶蒸還要用冷水（75°C）。\n打開 bilibili 跟 PTT，全部都是粕谷哲（Tetsu Kasuya）的 4:6 神系：20g / 280g / 1:14、悶蒸開閥、前段 90°C 後段 70°C 雙溫。\n中文圈最主流的 Kasuya 派和英文圈最主流的 Hoffmann 派，悶蒸時的閥位完全相反。一個是「先泡再濾」（關閥 immersion-first），一個是「先濾再泡」（開閥 percolation-first）。我在不同來源之間切換，等於每次都在動最關鍵那個變數。\n難怪我的顆粒度怎麼調都不對——我每次測的時候連配方架構都不一樣，根本不是顆粒度的問題。\n跟自己對齊的脈絡 意識到這件事之後，我把搜到的東西整理了一輪，包括：\nYouTube 上 Hoffmann、Hedrick、Kasuya、Emi Fukahori（2018 World Brewers Cup 冠軍）、Coffee Chronicler 的影片 bilibili 跟 PTT、Mobile01、小紅書的中文圈本地化版本 Home-Barista 論壇 t94108 / t85637 / t91685 三個主討論串（這個有點故事，後面講） 整理完之後我才看清楚，Switch 這個器具的真正分歧只在兩件事：\n變數 Hoffmann／英文圈 Kasuya／中文圈 悶蒸時閥位 關閥（先泡） 開閥（先濾） 是否中途降溫 全程 93–97°C 單溫 前段 90°C → 後段 70°C 其他的東西大家其實沒差很多：\n粉水比都在 1:14 到 1:16.7 中間，1:15 是中位數 研磨度都比一般 V60 略粗，C40 大概 22–28 格之間 水溫起手都在 90–95°C 之間 悶蒸量都是粉重的 2.5–3 倍 後段開閥時機都是 2:00–2:30 總時間都是 2:30–3:30 如果我前一週是在派系內部動參數，那顆粒度其實只要動 3–4 格就會收斂。但我是在派系之間亂跳，等於每次都在重新校準整個底層邏輯，3–4 格根本不夠。\nHome-Barista 的真實玩家在用什麼 光看 YouTube 影片有個盲點——大家都在拍自己最自豪的版本，但他們日常到底在喝什麼，影片不見得會講。\n這就是 Home-Barista 那三個 thread 有用的地方。它的精華不是 OP 的官方配方，是底下幾百則玩家在報自己 dial-in 結果的 reply。例如：\nt94108 主串問「你 daily 在用什麼配方」，被引用最多次的不是 Hoffmann 也不是 Kasuya，而是 Coffee Chronicler 的簡化版：15g / 250g、第一注開閥半量、第二注關閥 1:30 後排水、\u0026ldquo;no fancy stuff\u0026rdquo;。 t85637 討論 Kasuya 跑淺焙的人在問怎麼調，社群驗證的答案是兩段水溫各 +5°C（變 95°C / 75°C）。我自己最近就是淺焙偏酸，這個 +5°C 微調對我很有用。 同一串裡有人提到淺焙細粉多、Ethiopian 比 Guatemalan drawdown 慢很多，K-Max 1.0（最粗刻度）也救不回來。這就是顆粒度的盲點——同一個刻度，淺焙和中焙的 stall 風險完全不一樣。 t91685 一個用了 1.5 年 Switch 02 的玩家給的維護建議：「the little plastic lever \u0026hellip; easy to disengage it. Be gentle.」這句被多個 thread 獨立提到，看來塑膠撥桿的耐用度確實是 Switch 的設計弱點。 順帶一提，Home-Barista 全站都在 Cloudflare 後面，普通 web fetch 會 403。我繞過去的方式是 VPS 上跑了一個 FlareSolverr Docker，丟 URL 進去它會用真的 headless browser 過 CF challenge 再把 HTML 回給我。Reddit 那邊我也試了但失敗，Reddit 對 datacenter IP 有自家 anti-bot 不是 CF challenge，FlareSolverr 救不了——這個之後可能要單獨寫一篇。\n我接下來要做的事 整理完之後決策變得簡單，我給自己三個禮拜：\n第一週：跑兩派中位數的「無痛配方」當 baseline。 20g / 300g / 1:15、C40 22–26 格、92–94°C、關閥悶蒸 60g 30–45 秒、注水到 300g、2:00 攪拌一下、2:30 開閥排水、3:00 收尾。10 杯都跑這個，固定下來再來判斷豆子跟我的 setup 個性。\n第二週：用第一週味道判斷豆子，再切派系。\n淺焙偏酸 → Hedrick Ultimate（悶蒸前先 cool bloom） 中焙想 body → Hoffmann Hybrid（1:16.5 拉到 330g，多 swirl） 想精細控酸 → Kasuya 4:6（雙溫，前段抓風味後段抑苦） 第三週起：固定一個配方當 daily，再用 troubleshooting 表單一變數調整。drawdown 太快就細一階；drawdown 太慢就粗一階；bitter 就降溫 2–3°C；sour 就升溫 2–3°C 或拉長 immersion。一次只動一個變數。\n我之前的問題就是同時動三個變數。同時動三個的時候，你根本不知道是哪一個改善了或惡化了結果。\n不是顆粒度的問題 回頭看，我前一週其實不是在「調磨豆機」，是在「測派系」。但我以為我在調磨豆機，所以每次顆粒度沒對到，我都怪磨豆機或豆子。\n先選派系，再夾豆子，再 dial-in 顆粒度——這才是正確的順序。同時看中英資料的人很容易跳過第一步，因為第一步看起來不像「調咖啡」的步驟。\n要找一個 recipe 跟著跑十杯之前，先確認那個 recipe 跟你前一杯的底層邏輯一樣。否則你每一杯都在 reset。\n附錄：完整研究素材 寫這篇之前我把翻到的 10+ 個配方、跨來源交叉驗證表、Home-Barista 原文片段、設計缺陷討論整理成一份單檔 HTML，給有興趣自己挑派系或想看原文的人參考。Blog 這篇是消化後的觀點，附錄是原料。\n📎 V60 Switch 跨來源比對 reference（HTML）\n裡面有的：\n英文圈 6 個配方表（Hoffmann Hybrid / Hedrick Gong Fu / Hedrick Ultimate / Fukahori / Coffee Chronicler / Tale \u0026ldquo;Stall the Fall\u0026rdquo;） 中文圈 4 個配方表（Kasuya 4:6 / Super Hybrid / 台灣論壇簡化 / Hoffmann 中文化） 跨平台交叉驗證表（10 個維度標 consensus / varies） Home-Barista t94108 / t85637 / t91685 三個主討論串挑出的玩家原話 5 條 Common pitfalls + troubleshooting 對照表 設計缺陷討論（塑膠 lever / ball valve 漏水 / resin band / 02 vs 03 size） 所有來源 URL（YouTube、bilibili、PTT、Mobile01、Home-Barista、Reddit、評測串） 直接在瀏覽器開就能看，不需要安裝任何東西。要存檔的話另存新檔即可。\n","permalink":"https://blog.mklee.org/posts/hario-switch-two-camps/","summary":"\u003ch2 id=\"顆粒度焦慮再一次\"\u003e顆粒度焦慮（再一次）\u003c/h2\u003e\n\u003cp\u003e我在 V60 Switch 上面卡住有一陣子了。\u003c/p\u003e\n\u003cp\u003e豆子在變、磨豆機刻度在動、水溫在試、開關閥的時機點在改。一杯偏酸往細調一點，下一杯又苦尾，往粗回半圈卻變寡淡。Switch 比一般 V60 多一個「閥開關」變因，本來想說多個工具應該更可控，結果反而更難 debug——因為任何一杯不對，我都不知道是顆粒度的問題、水溫的問題、還是我關閥的時機沒抓好。\u003c/p\u003e\n\u003cp\u003e於是我想說那就乖一點，找個現成的 recipe 跟著跑，跑十杯穩定下來再來動參數。問題是 recipe 我找了一週還沒找到「我要跟誰」。\u003c/p\u003e\n\u003ch2 id=\"原來大家在打架\"\u003e原來大家在打架\u003c/h2\u003e\n\u003cp\u003e打開 YouTube，James Hoffmann 教你 20g / 330g / 1:16.5、悶蒸\u003cstrong\u003e關閥\u003c/strong\u003e、單一水溫 95°C、2:30 開閥排水。Lance Hedrick 教你 15g / 250g、悶蒸還要用\u003cstrong\u003e冷水\u003c/strong\u003e（75°C）。\u003c/p\u003e\n\u003cp\u003e打開 bilibili 跟 PTT，全部都是粕谷哲（Tetsu Kasuya）的 4:6 神系：20g / 280g / 1:14、悶蒸\u003cstrong\u003e開閥\u003c/strong\u003e、前段 90°C 後段 70°C 雙溫。\u003c/p\u003e\n\u003cp\u003e中文圈最主流的 Kasuya 派和英文圈最主流的 Hoffmann 派，\u003cstrong\u003e悶蒸時的閥位完全相反\u003c/strong\u003e。一個是「先泡再濾」（關閥 immersion-first），一個是「先濾再泡」（開閥 percolation-first）。我在不同來源之間切換，等於每次都在動最關鍵那個變數。\u003c/p\u003e\n\u003cp\u003e難怪我的顆粒度怎麼調都不對——我每次測的時候連配方架構都不一樣，根本不是顆粒度的問題。\u003c/p\u003e\n\u003ch2 id=\"跟自己對齊的脈絡\"\u003e跟自己對齊的脈絡\u003c/h2\u003e\n\u003cp\u003e意識到這件事之後，我把搜到的東西整理了一輪，包括：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYouTube 上 Hoffmann、Hedrick、Kasuya、Emi Fukahori（2018 World Brewers Cup 冠軍）、Coffee Chronicler 的影片\u003c/li\u003e\n\u003cli\u003ebilibili 跟 PTT、Mobile01、小紅書的中文圈本地化版本\u003c/li\u003e\n\u003cli\u003eHome-Barista 論壇 t94108 / t85637 / t91685 三個主討論串（這個有點故事，後面講）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e整理完之後我才看清楚，\u003cstrong\u003eSwitch 這個器具的真正分歧只在兩件事\u003c/strong\u003e：\u003c/p\u003e","title":"在 V60 Switch 的顆粒度上卡了一週，然後發現是看錯派系"},{"content":"看起來像「真的沒事件」 某天早上 8:55 看 daily report，Polymarket bot 連續第 3 天 0 信號：\n=== Polymarket Bot 2026-04-26 08:55 CST === 🔍 掃描: 2026-04-26 07:56 | 無信號 💰 餘額: $57.47 | 持倉 1 筆 📝 t2-20260423-111155.log 共同問題是三個信號都 (no summary)，MM 沒提供可驗證的新聞依據... 📊 累計 6 筆交易 | 最後交易: 2026-03-29 第一感覺是政治冷淡期——4 月底國際新聞確實淡。Cron log 看下去：\n[2026-04-26 00:35:22] Exit signals: 0 [2026-04-26 00:35:22] News: 0 signals [2026-04-26 00:35:23] Correlation: 0 signals [Sun Apr 26 00:35:23 CST 2026] No signals, skipping ... (連續 9 次都長這樣) 每 30 分鐘一次 cron、整天都「No signals, skipping」。看起來都正常運作、只是真的沒事件。\n但手動跑 news_scanner.py 立刻回 5 個訊號：Iran 軍事行動 / Hormuz 解封 / al-Sharaa 會面 / Trump 訪中 5/15 等，confidence 0.65–0.78、delta 0.13–0.245。\n這代表訊號在、scanner 也正常——只是 cron 看不到它們。\n三層 silent scripts/collect.sh 第 93 行長這樣：\nPYTHONPATH=~/projects/polymarket-bot timeout 50 python3 agents/news_scanner.py \\ \u0026gt; /tmp/pm-news.json 2\u0026gt;/dev/null \u0026amp; PID_NEWS=$! # ... 後面 wait $PID_NEWS NEWS_JSON=$(cat /tmp/pm-news.json 2\u0026gt;/dev/null || echo \u0026#34;[]\u0026#34;) log \u0026#34;News: $(echo \u0026#34;$NEWS_JSON\u0026#34; | python3 -c \u0026#39;import json,sys;print(len(json.load(sys.stdin)))\u0026#39; 2\u0026gt;/dev/null || echo 0) signals\u0026#34; 這條指令把三件事疊加在一起：\n層 行為 1. timeout 50 50 秒到自動 SIGTERM 2. 2\u0026gt;/dev/null stderr 全丟掉，包含 timeout 殺進程的訊息 3. parser || echo 0 JSON 解析失敗時 fallback 為 0 同 prompt 連跑 3 次量 news_scanner.py 實際耗時（外層給 timeout 180 跑、確保不會誤殺）：\nRun command 結束原因 stdout bytes 1 timeout 180 python3 agents/news_scanner.py exit 0 2865（5 signals） 2 同上 exit 0 3585（5 signals） 3 同上（高峰時段） exit 0 約 3 KB（5 signals） 3 次分別跑 29s / 69s / 約 90s——MiniMax M2.7 是 thinking model，回應時間波動很大。\n對照原 cron 設定 timeout 50：\nRun command 結束原因 stdout bytes 1 timeout 50 python3 agents/news_scanner.py SIGTERM at 50.0s 0 50 秒對這個 model 是邊界值——快的時候穩過、慢的時候被殺。連續 3 天剛好都撞上慢的那邊。\n被殺後 stdout 是 0 bytes，/tmp/pm-news.json 是空檔。下游 parser 走 || echo 0 退回零，cron log 寫「News: 0 signals」當作正常的「真的沒事件」分支跑下去。整條 pipeline rc=0、無錯誤訊息、log 看起來健康。\n連續 3 天每 30 分鐘都這樣 silently fail。\n修法（分兩批） 3 層 silent 對應 3 個修補點。第一批（前兩層）當天就修了恢復信號流；第二批（parser fallback）幾天後才補進去。\n第一批：timeout + stderr 改向 # 1. timeout 50 → 150（4× 最快典型耗時、給 thinking model 抖動空間） PYTHONPATH=... timeout 150 python3 agents/news_scanner.py \\ \u0026gt; /tmp/pm-news.json 2\u0026gt;/tmp/pm-news.err \u0026amp; # 2. stderr 從 /dev/null 改寫進專屬檔，留 SIGTERM / Python traceback / 上游連線錯誤等證據 # 3. 外層 cron-wrapper.sh 的 timeout 180 → 240 # 新內層 wait 上限 = max(news 150, correlator 120) = 150s # + 前後 chain calls ~30s = 180s 用滿、留 60s 餘量 第一批 e2e 驗證：bash scripts/collect.sh 33 秒完成、回 5 個 news signals（Iran / Hormuz / al-Sharaa / Trump 訪中等 4 月底真實事件）。前一晚還在連續 0 信號的 bot 立刻恢復。\n第二批：parser 區分「empty / invalid / valid」三分支 第一批修完隔幾天，回頭看才發現：就算上游壞掉，parser 還是會 silent 退回 0——因為原本長這樣：\nNEWS_JSON=$(cat /tmp/pm-news.json 2\u0026gt;/dev/null || echo \u0026#34;[]\u0026#34;) log \u0026#34;News: $(echo \u0026#34;$NEWS_JSON\u0026#34; | python3 -c \u0026#39;...print(len(...))\u0026#39; 2\u0026gt;/dev/null || echo 0) signals\u0026#34; || echo 0 把「上游 timeout 死掉、檔案 0 bytes」跟「上游正常回 0 結果」壓成同一條路徑。第一批修補只是把 timeout 拉寬讓「上游死掉」機率變小，沒解決 silent 本身。\n把它改成顯式 3 分支：\nNEWS_BYTES=$(wc -c \u0026lt; /tmp/pm-news.json 2\u0026gt;/dev/null | tr -d \u0026#39; \u0026#39; || echo 0) if [ \u0026#34;${NEWS_BYTES:-0}\u0026#34; -eq 0 ]; then log \u0026#34;WARN: news_scanner upstream empty output (timeout/sigterm/no run); stderr in /tmp/pm-news.err\u0026#34; NEWS_JSON=\u0026#34;[]\u0026#34; NEWS_COUNT=0 elif ! python3 -c \u0026#39;import json,sys; json.load(open(sys.argv[1]))\u0026#39; /tmp/pm-news.json 2\u0026gt;/dev/null; then log \u0026#34;WARN: news_scanner output not valid JSON (${NEWS_BYTES} bytes); stderr in /tmp/pm-news.err\u0026#34; NEWS_JSON=\u0026#34;[]\u0026#34; NEWS_COUNT=0 else NEWS_JSON=$(cat /tmp/pm-news.json) NEWS_COUNT=$(echo \u0026#34;$NEWS_JSON\u0026#34; | python3 -c \u0026#39;import json,sys;print(len(json.load(sys.stdin)))\u0026#39;) fi log \u0026#34;News: ${NEWS_COUNT} signals\u0026#34; 實測 3 條 path 各觸發各的 WARN：\n情境 NEWS_BYTES log 訊息 Upstream 正常 0 結果 [] 2 bytes News: 0 signals（無 WARN，正常路徑） Upstream timeout 殺掉 0 bytes WARN: news_scanner upstream empty output ... + News: 0 signals Upstream JSON 壞掉 9 bytes ([{\u0026quot;foo\u0026quot;:) WARN: news_scanner output not valid JSON (9 bytes) ... + News: 0 signals 下游業務邏輯仍然把 NEWS_COUNT=0 視為「沒事件」沒差，但 cron log 多一行 WARN: ... 之後 grep 一搜就看到，silent fail 時間從「3 天才察覺」降到「下次看 log 就知道」。\n不是孤例：silent fail 家族 修完之後跑了一下自己 LEARNINGS.md 內的 promotion-check，發現過去半年累積了 5 條主題高度相關但各自獨立的 entry：\nID（日期） claim 摘要 KG-20260413-006 curl -sf 對 4xx/5xx 也算 fail，不適合當網路就緒探測——HTTP 拿到 401 也會 silent 回 1 BP-20260418-001 Hook / job timeout 必須 \u0026gt; 內部 subprocess timeout + overhead——否則外層先殺、看起來像 pass 但內層其實沒跑完 CORRECTION-20260423-001 外部 API key 從沒設過 + try/except 吞 auth fail = 永久 silent degradation——Brave Search 我設過 key 但沒寫進 .env，code 跑了一個月，401 全部被吞 BP-20260423-003 外部 source 整合驗證三要素：size × Last-Modified × 首行內容——靜態 hub 頁假裝是動態 feed 已經跑了 27 天 KG-20260426-001 這次的：subprocess timeout 配 2\u0026gt;/dev/null 在 cron pipeline 是雙重靜默 5 條從 2026-04-13 到 04-26 跨 13 天，講的是同一件事的不同表面：自動化 pipeline 中「沒有錯誤訊息」≠「沒錯」。\n如果合在一起當一個 meta-pattern 看，rc=5 / evidence 跨 13 天——遠比 5 條各自 rc=1 有說服力。但每次踩坑時都覺得「這跟之前那條像，但又有點不一樣」就開新 entry，造成現在這個碎片化現象。這個自我觀察本身又是另一個 lesson 了。\nCron pipeline 三條通則 從這 5 條合起來看，cron 自動化 code 寫起來該守的線：\n1. 永遠不用 2\u0026gt;/dev/null 在 cron pipeline /dev/null 是 silent fail 的最大幫兇。stderr 改寫進專屬 log：\n# Bad my-cmd args \u0026gt; out.json 2\u0026gt;/dev/null # Good my-cmd args \u0026gt; out.json 2\u0026gt;\u0026gt;\u0026#34;/tmp/$(basename \u0026#34;$0\u0026#34; .sh).err\u0026#34; 下次 silent fail 至少有 SIGTERM / Traceback / connection refused 線索可看。多花的成本：每次 cron run +1 KB 的 err log，週期 cron 大概一個月幾 MB，janitor 順手處理掉就好。\n2. timeout 設 4× 典型耗時 不要設邊界值。如果某個 subprocess 平常 30 秒跑完、95th percentile 60 秒——timeout 不該設 50 秒、也不該設 60 秒，設 120 秒以上。\n外層 wrapper 的 timeout 必須 ≥ 所有並行內層 timeout 的 max + 前後 chain call 時間 + 安全餘量。算一下：\ncollect.sh 內： news_scanner timeout 150 (parallel) market_corr timeout 120 (parallel) → wait 上限 = max(150, 120) = 150s 前面 chain calls (balance / positions / market resolver) ~30s 後面 summary JSON 組裝 ~3s collect.sh 總時長 ≤ 150 + 30 + 3 = ~185s 外層 cron-wrapper.sh： timeout 240 給 collect.sh ← 必須 \u0026gt; 185s 外層短於內層 sum 的話，外層先殺，內層工作半途中斷、檔案半寫狀態，下次 cron 又重來。\n3. Parser fallback 前必須區分「空 input」vs「invalid input」 很多 parser 寫成這樣：\nRESULT=$(echo \u0026#34;$JSON\u0026#34; | python3 -c \u0026#39;import json,sys;print(len(json.load(sys.stdin)))\u0026#39; 2\u0026gt;/dev/null || echo 0) || echo 0 把「上游 timeout 死掉沒輸出」跟「上游正常回 0 個結果」混成同一條路徑。下游看到 RESULT=0 完全沒辦法區分：\n情境 input 應該如何處理 Upstream 正常 0 結果 [] 真的沒事件，正常路徑 Upstream timeout 殺死 `` (0 bytes) 應該告警 Upstream JSON 壞掉 [{\u0026quot;foo 應該告警 修法：分支處理：\nif [ ! -s /tmp/pm-news.json ]; then log \u0026#34;WARN: news_scanner upstream timeout or no output\u0026#34; NEWS_COUNT=0 elif ! python3 -c \u0026#39;import json,sys; json.load(sys.stdin)\u0026#39; \u0026lt; /tmp/pm-news.json 2\u0026gt;/dev/null; then log \u0026#34;WARN: news_scanner output not valid JSON\u0026#34; NEWS_COUNT=0 else NEWS_COUNT=$(python3 -c \u0026#39;import json,sys;print(len(json.load(sys.stdin)))\u0026#39; \u0026lt; /tmp/pm-news.json) fi 多寫 5 行 bash，下次 silent fail 直接寫進 cron log，搜「WARN: news_scanner upstream timeout」立刻看到。\n補充：「連續 N 次零信號」對你的場景是不是異常？ 如果你的 cron 業務對 0 結果有歷史 baseline——例如 Polymarket bot 過去三個月平均 3-5 條/天、最久連續 0 也只 4 小時——那連續 3 天 0 就是離 baseline 太遠的離群值，要嘛 silent fail 要嘛上游全掛，兩種都該告警。\n我不會說「0 信號連續多次一律是異常」這種一般化結論——對某些 cron（例如平日才有事件的工作流）連續 30 小時 0 才正常。重點是用你自己的歷史 baseline 設門檻，超過再 TG 告警，去人工 check upstream 健康。\n這次踩到底是因為「3 天 0」被我當成「政治冷淡期」吃掉、沒對照 baseline。\n收尾 silent failure 是 cron 系統最大的敵人——比 crash 還危險，因為它看起來「在跑」。\n寫 cron 時值得每個 subprocess 都問自己：「如果它現在被 SIGTERM 殺、connection refused、disk full，外層 cron run 的 log 會看到什麼？」如果答案是「跟正常跑完一樣」，那你已經種了一個 silent fail trap。\n這次的修補分兩批進去：第一批改 timeout 50→150 + stderr 從 /dev/null 改向專屬檔（解了 90% 機率不再撞 silent），第二批把 parser fallback 拆成 empty/invalid/valid 三分支（剩下 10% 也會明確 WARN 進 log）。實測上線後：\n隔天的 cron 連續穩定回 5 個 news signals 跟手動跑一致 /tmp/pm-news.err 累積了一些 SearXNG 連線抖動的 stderr（之前都被吞掉、不知道有過） 代價就是多寫 15 行 bash + 一個 stderr log——對 cron 健康來說很划算。\n","permalink":"https://blog.mklee.org/posts/2026-04-27-cron-silent-fail-family/","summary":"\u003ch2 id=\"看起來像真的沒事件\"\u003e看起來像「真的沒事件」\u003c/h2\u003e\n\u003cp\u003e某天早上 8:55 看 daily report，Polymarket bot 連續第 3 天 0 信號：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e=== Polymarket Bot 2026-04-26 08:55 CST ===\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e🔍 掃描: 2026-04-26 07:56 | 無信號\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e💰 餘額: $57.47 | 持倉 1 筆\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e📝 t2-20260423-111155.log\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  共同問題是三個信號都 (no summary)，MM 沒提供可驗證的新聞依據...\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e📊 累計 6 筆交易 | 最後交易: 2026-03-29\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e第一感覺是政治冷淡期——4 月底國際新聞確實淡。Cron log 看下去：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[2026-04-26 00:35:22] Exit signals: 0\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[2026-04-26 00:35:22] News: 0 signals\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[2026-04-26 00:35:23] Correlation: 0 signals\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[Sun Apr 26 00:35:23 CST 2026] No signals, skipping\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e... (連續 9 次都長這樣)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e每 30 分鐘一次 cron、整天都「No signals, skipping」。看起來都正常運作、只是真的沒事件。\u003c/p\u003e","title":"Cron 連 3 天 0 信號的 root cause：subprocess timeout + 2\u003e/dev/null 是 silent fail trap"},{"content":"「Your pull request has been closed」 2026-04-26 早上打開 GitHub，看到一封 12 天前送出的 PR 來自 maintainer 的最新通知：\nYour pull request has been closed. State: CLOSED mergedAt: null 第一反應是嘆口氣——卡了 6 輪 bot review，最後還是被關了。準備重新開一個 issue 解釋為什麼這個方向是對的。\n但點進去看留言才發現完全不是那回事。\n12 天前發生了什麼 04-14 半夜我在用自己跑在 VPS 上的 AI agent，那個 agent 走 OpenClaw 串接 Discord。每個 Discord thread 可以綁一個 ACP（Agent Client Protocol）session 跟 Claude/Codex/Gemini 等 CLI agent 對話。\n用到一半發現一個怪現象：在綁定 thread 內打 /acp close，原本應該關掉 ACP session，但卻被 ACP agent 當成「請求」吃掉，回了一段莫名其妙的對話。/status、/unfocus 也一樣。\ntrace 進 OpenClaw commands-acp.ts 發現 dispatch 路由跳過了 handleAcpCommand：在 ACP-bound 的 conversation 裡，所有 text 都被當 prompt 送給 ACP harness，連 /acp 開頭的管理指令都不例外。\n當下一氣之下做了三件事：\n03:14 開 issue #66298 07:46 fork → 寫修補 → 開 PR #66407 走完整 CONTRIBUTING checklist：pnpm tsgo / pnpm check / pnpm build / targeted pnpm vitest 全綠 PR diff 不大：2 個檔，+138/-13。在 commands-acp.ts 的 dispatch 點加 bypass，/acp 開頭的 text 不再被吞進 ACP harness、轉回 Gateway 命令處理路徑。\n6 輪 bot review，最後卡在 scope-out OpenClaw 跑 Codex Review + Greptile bot，每次 push 都會自動重 review。前 5 輪都是常見的 P1/P2（path normalize、case 處理、regression coverage、bypass 跟 handler 同步），改完都收。\n真正卡住的是第 6 輪：Codex 抓到 commands-acp.ts:90 一個 pre-existing startsWith bug——這個 bug 跟 PR 主題相關但 scope 之外（在更下層的 normalizeCommandBody，應該另開 issue）。我留言 scope-out 貼了 trace 連結 discussion_r3078655259，這條 unresolved 之後 maintainer 沒回應，狀態維持了 12 天。\n中間還發生兩件事：\n04-18：第三方 joeia26 開了 sibling PR #68617 解同一個 bug，diff +47/-2 比我小很多 04-21：openclaw 的 prtags-bot 把這兩個 PR 列為 great-loon-t2te duplicate group 看到 sibling PR 有點挫敗——會不會別人先被 merge？但他也卡著沒動。\n04-26 凌晨那 5 分鐘 對照 timestamp 看（GitHub API 都精準到秒）：\n01:58:19 steipete 推 commit a6d9926d1d 上 main 02:02:46 Issue #66298 closed (stateReason: COMPLETED) 02:02:53 PR #66407 closed (mergedAt: null) ← 我這個 02:02:59 PR #68617 closed (mergedAt: null) ← sibling maintainer 先 commit 上 main，4 分鐘後回頭把 issue + 兩個 PR 一次關掉。三個 close 動作差不到 13 秒。\n關 PR 的留言是這樣寫的：\nFixed on main in https://github.com/openclaw/openclaw/commit/a6d9926d1d, following this PR\u0026rsquo;s direction and adding the missing commands.text=false escape-hatch coverage.\nThe landed fix keeps /acp ... local in ACP-bound conversations, keeps /acp close working even when text commands are disabled, preserves the existing auth/scope checks, and also documents the bound-session command routing rule. Verified with pnpm check:changed.\nThanks @kindomLee; the changelog credits your work here. Closing this as superseded by the main-branch fix.\n而 CHANGELOG.md line 482，在 ## 2026.4.25 → ### Fixes 段：\n- ACP: keep `/acp` management commands, plus local `/status` and `/unfocus`, on the Gateway path inside ACP-bound threads so they are not consumed as ACP prompt text. Fixes #66298. Thanks @kindomLee. 這是「形式上 closed superseded、實質上 fix landed + 名字進 changelog」的 outcome。\nmaintainer 做的事跟我做的事 我的 PR：2 檔 / +138/-13。在 commands-acp.ts 的 dispatch 點 inline 修。\nmaintainer 的 commit a6d9926d1d：9 檔 / +199/-8。\n檔案 用途 CHANGELOG.md +3 行（我的 credit 在這） docs/tools/acp-agents.md +6 行（新文件） docs/tools/slash-commands.md +5 行（新文件） src/auto-reply/reply/commands-acp.ts +1 行（主修點） src/auto-reply/reply/dispatch-acp-command-bypass.ts +16 行（抽成獨立 module） src/auto-reply/reply/dispatch-acp-command-bypass.test.ts +128 行（regression test） src/auto-reply/reply/commands-acp.test.ts +18 行 scripts/test-projects.test-support.mjs + test/scripts/test-projects.test.ts +22 行（測試 infra） 兩件事值得注意：\n方向完全一樣：把 /acp 開頭 text 從 ACP harness 路由抽回 Gateway 命令路徑——maintainer 留言裡寫的 \u0026ldquo;following this PR\u0026rsquo;s direction\u0026rdquo; 不是客套 scope 擴大：我只處理 /acp，他擴到 /status /unfocus 也本地化、且加上 commands.text=false 的 escape hatch（即使 text commands 整個被關，/acp close 仍能逃生） 架構升級：我 inline 在 commands-acp.ts 內改；他抽成獨立 dispatch-acp-command-bypass.ts module + 128 行 regression test + 兩份 docs 第 6 輪卡住的那個 P2 scope-out 議題（pre-existing startsWith bug），他在新 module 裡乾脆繞過去了，不需要碰原本那條 path。\nclosed without merge ≠ rejected（至少這次不是） 從 GitHub UI 看：state = CLOSED、mergedAt = null、頁面頂端紅色「Closed」badge。看起來就是被拒絕。\n但同時：issue stateReason = COMPLETED（不是 NOT_PLANNED）、CHANGELOG 寫了 credit、commit message 說 \u0026ldquo;following this PR\u0026rsquo;s direction\u0026rdquo;。closedByPullRequestsReferences 也是空的——因為不是被另一個 PR merge 關掉，是 maintainer direct commit 上 main 後手動關。\n這次的 outcome 形狀大概是：maintainer 吸收 fix direction、重寫成更乾淨的架構（抽 module / 加大量測試 / 加 docs）後 commit 上 main、回頭把原 PR close 為「superseded」。\n我不知道這在 OSS 是不是常見 pattern——這是我第一次貢獻大專案，樣本數 1。但至少這次，PR 作者第一眼看到「closed, mergedAt: null」就下「我的 PR 被否決了」結論的話，會錯過真實 outcome。\n下次再看到 closed/mergedAt=null 之前，跑這幾步 不下結論之前，先收集資料：\n# 1. close 留言：maintainer 通常會說明 next steps 或 superseded path gh pr view \u0026lt;n\u0026gt; --comments # 2. 連動 issue 的 stateReason 才是真實意圖（COMPLETED ✅ vs NOT_PLANNED ❌） gh issue view \u0026lt;issue_number\u0026gt; --json state,stateReason,closedAt # 3. 看 close 時間附近 main 的相關 commit gh api \u0026#34;repos/\u0026lt;owner\u0026gt;/\u0026lt;repo\u0026gt;/commits?since=\u0026lt;close_date_minus_1d\u0026gt;\u0026#34; \\ --jq \u0026#39;.[].commit.message\u0026#39; | head -20 # 4. release notes / CHANGELOG 看有沒有 credit grep -i \u0026#34;\u0026lt;your_username\u0026gt;\u0026#34; CHANGELOG.md gh release list --repo \u0026lt;owner\u0026gt;/\u0026lt;repo\u0026gt; --limit 3 最後一招特別有用：grep -i \u0026quot;\u0026lt;your_username\u0026gt;\u0026quot; CHANGELOG.md 跟看最新幾個 release notes。maintainer 採用你方向的話，常見會用 Thanks @\u0026lt;username\u0026gt; 形式 credit。沒被 credit 跟有被 credit 是兩種完全不同的 outcome。\n收尾 這 12 天我做的事：\n開 issue + PR（4.5 小時，bug trace 跟 fix 都找對了方向） 走完 6 輪 bot review（耐性消耗最大的部分） 卡在 scope-out 第 6 輪 12 天無人回應（這段最痛） 04-26 凌晨醒來看通知以為被 reject 最後得到的：fix 在 v2026.4.25 release 上線、CHANGELOG 名字、ACP-bound thread 內三個指令（/acp /status /unfocus）正式不會再被 ACP harness 吞掉。\n對我來說的 takeaway：「貢獻」這次不是 PR commit hash 出現在 git log 裡，是 maintainer 用我的 direction 寫得更好、回頭把 credit 給我。 形式上不漂亮，實質上 ship 了我做不到的 scope（/status /unfocus 一起本地化）跟 test coverage。\n這條 lesson 寫進自己的 LEARNINGS：closed, mergedAt: null 是訊號不是結論，去查 close 留言 + issue stateReason + main commit + CHANGELOG 四件事再下判斷。\n","permalink":"https://blog.mklee.org/posts/2026-04-27-pr-closed-but-shipped/","summary":"\u003ch2 id=\"your-pull-request-has-been-closed\"\u003e「Your pull request has been closed」\u003c/h2\u003e\n\u003cp\u003e2026-04-26 早上打開 GitHub，看到一封 12 天前送出的 PR 來自 maintainer 的最新通知：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eYour pull request has been closed.\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eState: CLOSED\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emergedAt: null\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e第一反應是嘆口氣——卡了 6 輪 bot review，最後還是被關了。準備重新開一個 issue 解釋為什麼這個方向是對的。\u003c/p\u003e\n\u003cp\u003e但點進去看留言才發現完全不是那回事。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"12-天前發生了什麼\"\u003e12 天前發生了什麼\u003c/h2\u003e\n\u003cp\u003e04-14 半夜我在用自己跑在 VPS 上的 AI agent，那個 agent 走 OpenClaw 串接 Discord。每個 Discord thread 可以綁一個 ACP（Agent Client Protocol）session 跟 Claude/Codex/Gemini 等 CLI agent 對話。\u003c/p\u003e\n\u003cp\u003e用到一半發現一個怪現象：在綁定 thread 內打 \u003ccode\u003e/acp close\u003c/code\u003e，原本應該關掉 ACP session，但卻被 ACP agent 當成「請求」吃掉，回了一段莫名其妙的對話。\u003ccode\u003e/status\u003c/code\u003e、\u003ccode\u003e/unfocus\u003c/code\u003e 也一樣。\u003c/p\u003e\n\u003cp\u003etrace 進 OpenClaw \u003ccode\u003ecommands-acp.ts\u003c/code\u003e 發現 dispatch 路由跳過了 \u003ccode\u003ehandleAcpCommand\u003c/code\u003e：在 ACP-bound 的 conversation 裡，所有 text 都被當 prompt 送給 ACP harness，連 \u003ccode\u003e/acp\u003c/code\u003e 開頭的管理指令都不例外。\u003c/p\u003e","title":"PR 沒被 merge，但 fix 上 main 了——第一次貢獻 OSS 大專案的反直覺結局"},{"content":"這個部落格有個存在很久的小債：17 篇文章裡，一張封面都沒有。每次打開文章列表都是一排光禿禿的灰底 placeholder，社群媒體分享預覽也只有標題文字。\n一直沒處理是因為：要嘛手動一張張找圖很煩，要嘛丟給外部服務（Midjourney / DALL-E）要自己掏錢 + 管 API key + 存圖檔對應 slug，光想這個 pipeline 就懶。\n直到昨天刷到一則推文：Codex CLI 0.122 把 gpt-image-2 預設打開了，不需要 API key，走你現有的 ChatGPT 帳號計費。配套還有一個叫 baoyu-skills 的 Claude Code skill 集合，專門餵 Codex 生各種規格的圖。\n也就是說：整條 pipeline 已經在我手邊，只是我不知道。那今天就來補債。\n最後結果 17 張全生出來了，過程中踩到一個 --sandbox workspace-write 的誤會，有 14 張看起來全失敗，後來發現圖其實都生成了，只是被困在 Codex 的快取目錄裡。這篇記錄流程 + 踩坑 + 救援。\n驗證：Codex 真的能畫圖 先 live test。我的 Codex 版本剛好是 0.122.0。\ncodex exec --skip-git-repo-check --sandbox workspace-write \\ --output-last-message \u0026#34;$OUT\u0026#34; -m gpt-5.4 \\ \u0026#39;$imagegen Create a simple 256x256 PNG of a red circle on white background. Save as /tmp/test.png. Final message: only the file path.\u0026#39; \\ \u0026lt; /dev/null 跑完 ls /tmp/test.png 就有了。32 KB PNG、256×256 8-bit RGB、花了 ~30k tokens。\n幾個關鍵點，當下沒全搞懂，後面會回來踩：\n--sandbox workspace-write 必加，因為 Codex 要呼叫 shell 的 cp 把圖從內部 cache 搬到你指定的位置。read-only 直接失敗。 $imagegen 用單引號包整段 prompt，避免 bash 把它展開成空字串。省略也行（Codex 會自己判斷要不要畫），但加上更明確。 命令尾的 \u0026lt; /dev/null 是上週踩過另一個坑的肌肉記憶：Codex exec 會等 stdin EOF，背景執行如果 pipe stdin 不關就永遠卡住。這是另一個故事。 內部流程也驗到了：Codex 不是直接寫你指定的路徑。它先把圖存到 ~/.codex/generated_images/\u0026lt;session-id\u0026gt;/ig_*.png，然後呼叫一次 shell用 cp + macOS 的 sips（做 resize）搬到目標位置。等等會看到為什麼這個中間步驟很重要。\nbaoyu-skills：要不要裝、裝什麼 推文裡說的 baoyu-skills 來自 JimLiu/baoyu-skills，GitHub 星數 ★15k 級別。裡面有二十幾個 skill：\n內容生成類：baoyu-cover-image、baoyu-infographic、baoyu-comic、baoyu-slide-deck… AI 圖像生成類：baoyu-image-gen（各家 API 整合）、baoyu-imagine 發文類：baoyu-post-to-x、baoyu-post-to-weibo、baoyu-post-to-wechat、baoyu-youtube-transcript 工具類：baoyu-compress-image、baoyu-format-markdown 「danger」類：baoyu-danger-gemini-web、baoyu-danger-x-to-markdown（逆向 web API 的） 一口氣裝下去顯然太多。評估下來我只需要 baoyu-cover-image：\n它是純文字 skill（SKILL.md + references 資料夾，188 KB），不帶 npx / bun / 任何跑時依賴 它對後端是「auto-detect」：優先用 runtime native 的 imagegen（= Codex 自己的 gpt-image-2），只有在沒有時才去找 baoyu-imagine 這類需要 API key 的備用 所以我裝它就好，不需要碰任何第三方 API key 避開的：所有 baoyu-post-to-*（我 blog 不透過 Twitter/微博/微信發）、baoyu-image-gen（需要 OpenAI/DashScope/Replicate 等 API key）、baoyu-danger-*（逆向 web API 本來就有維護成本，而我 Codex 能用就不需要）。\n安裝就是從 repo 把 skills/baoyu-cover-image/ 整個複製進 ~/.claude/skills/baoyu-cover-image/。下 session Claude Code 啟動就讀到了。\n寫個最小偏好檔 ~/.baoyu-skills/baoyu-cover-image/EXTEND.md：\n--- version: 3 preferred_text: title-only preferred_mood: balanced default_aspect: \u0026#34;16:9\u0026#34; quick_mode: false language: zh preferred_image_backend: auto default_output_dir: same-dir --- preferred_image_backend: auto 讓 skill 自動挑 Codex 當 backend；quick_mode: false 保留第一次產圖前的確認步驟。\n流程設計 blog 原本住在 VPS 上的 blog/ 資料夾，沒 GitHub remote。我不打算為這次工作 push 到雲端（另一件事），所以選 rsync 策略：\n下載 content/ + hugo.toml 到本機 ~/mklee/blog/ 本機改 frontmatter + 生圖 rsync 回 VPS VPS 上跑部署腳本 圖檔放哪？PaperMod 支援絕對路徑的 cover image，走 Hugo static/ 自動 map 到網站 root：\n檔案：static/images/posts/\u0026lt;slug\u0026gt;.png URL：/images/posts/\u0026lt;slug\u0026gt;.png Frontmatter：cover.image: \u0026quot;/images/posts/\u0026lt;slug\u0026gt;.png\u0026quot; + relative: false 尺寸用 1792×1024，這是 gpt-image-2 原生支援的 wide 尺寸，比例接近 16:9。\n每篇文章要給圖生成的「指令」怎麼寫？baoyu-cover-image 把風格拆成五個維度：\n維度 選項 Type hero / conceptual / metaphor / typography / scene / minimal Palette warm / cool / dark / earth / retro / mono / vivid / pastel / duotone / elegant / macaron Rendering flat-vector / hand-drawn / painterly / digital / chalk / pixel / screen-print Text none / title-only / title-subtitle / text-rich Mood subtle / balanced / bold 每篇我根據題目手動挑一組。例如：\n一篇講 memory 三層系統的文章 → conceptual + cool + digital + balanced（架構圖感） 一篇咖啡萃取筆記 → metaphor + warm + hand-drawn + subtle（咖啡館粉筆畫感） 一篇講夢境式記憶循環 → metaphor + dark + painterly + subtle（月光星雲） 然後每篇都寫一句「視覺核心構想」，例如「三個 dial 圍繞著一個中央 espresso cup」、「分層金字塔顯示從 worker 到 decision 節點」。這才是圖的靈魂，維度只是色調。\nPilot 3 張：先驗再擴大 我不想 17 張一次丟下去，萬一風格不對或中文渲染歪掉，會全盤重跑浪費 token。先挑三篇各自代表一種題材跑 pilot：\nhybrid-memory-search（技術算法）：conceptual + cool + digital espresso-dialing-ramble（咖啡哲思）：metaphor + warm + hand-drawn polymarket-bot-architecture（基礎設施）：conceptual + cool + digital（偏 blueprint） 三張都在 1–3 分鐘內生完、各 500 KB 到 3 MB 不等。中文渲染方面我有個意外發現：gpt-image-2 對標題是中文還是英文是機率性的。咖啡那張標題「Espresso 調磨雜談」中英混排，它忠實渲染；Polymarket 那張整個中文標題 + 副標 + 分層標籤全部中文，渲染得最完整；但 hybrid memory search 那張的標題是「Hybrid memory search：把 journal 搜尋當成 ranking 問題」，它自動只保留英文前半。大致規律是原標題越偏哪一方它就往哪邊倒，強制全中文會降低品質。\n三張都對題。批次剩下 14 張。\n踩坑：14 張看起來全部失敗 批次我寫了個 shell function：\ngen_cover() { local slug=\u0026#34;$1\u0026#34; title=\u0026#34;$2\u0026#34; desc=\u0026#34;$3\u0026#34; dims=\u0026#34;$4\u0026#34; visual=\u0026#34;$5\u0026#34; codex exec --skip-git-repo-check --sandbox workspace-write \\ --output-last-message \u0026#34;/tmp/codex-$slug.out\u0026#34; -m gpt-5.4 \\ \u0026#34;\u0026lt;包好的 prompt\u0026gt;\u0026#34; \u0026lt; /dev/null \u0026gt;/tmp/codex-$slug.log 2\u0026gt;/tmp/codex-$slug.err \u0026amp; echo \u0026#34;launched $slug (pid=$!)\u0026#34; } gen_cover \u0026#34;\u0026lt;slug1\u0026gt;\u0026#34; \u0026#34;\u0026lt;title1\u0026gt;\u0026#34; \u0026#34;\u0026lt;desc1\u0026gt;\u0026#34; \u0026#34;\u0026lt;dims1\u0026gt;\u0026#34; \u0026#34;\u0026lt;visual1\u0026gt;\u0026#34; gen_cover \u0026#34;\u0026lt;slug2\u0026gt;\u0026#34; ... # ... 共 14 呼叫 全部用 \u0026amp; 背景，14 個 Codex 同時跑。6 分鐘後回來看，全部程序都收掉了 —— 但 static/images/posts/ 還是只有 pilot 那 3 張。\n掃 err 檔：每個都是同一個錯誤：\ncp: ~/mklee/blog/static/images/posts/\u0026lt;slug\u0026gt;.png: Operation not permitted 而且 --output-last-message 指向的 .out 檔裡，Codex 回傳的是：\n~/.codex/generated_images/\u0026lt;session-id\u0026gt;/ig_\u0026lt;hash\u0026gt;.png Codex 自己也困惑：它產完圖了，想搬到我指定的路徑，被擋；於是改放回自己的內部路徑，跟我報告。\n診斷：workspace-write 到底 write 什麼 workspace workspace-write 這個 sandbox 模式的寫入範圍，鎖定在 Codex 啟動時 shell 的 CWD。不是 prompt 寫什麼絕對路徑、不是 --output-last-message 指哪裡、也不是讓 Codex agent 自己 mkdir -p 就能繞過 —— shell 在哪裡啟動，能寫的就是那個目錄樹。\n我的 launcher function 在 cc-memory-project/ 目錄跑，Codex 的「workspace」就是這裡，當然不能寫 ~/mklee/blog/。\n為什麼 pilot 3 張沒踩到？翻我的腳本 —— pilot 我有手動 cd ~/mklee/blog 再跑 Codex。當時沒覺得是必要，只是順手。等到 batch function 我沒把 cd 包進去，就集體踩。\n教訓：workspace-write 不是「允許寫任何 workspace」，是「允許寫當前 workspace」。batch 任務要寫檔到特定目錄，launcher 必須先 cd 過去。\n救圖：從 stderr 撿 session id 現在重點是：圖檔其實都生成成功了，只是停在 ~/.codex/generated_images/\u0026lt;sid\u0026gt;/ig_*.png。每個 Codex session 有自己的 uuid 目錄，14 個 session 就 14 個目錄。\n怎麼知道哪個 slug 對應哪個 session？Codex 啟動時的 stderr 會印：\nsession id: 019db338-aee2-7d02-89e5-865b20c264a8 我的 launcher 把每個 Codex 的 stderr 各自導到 /tmp/codex-\u0026lt;slug\u0026gt;.err，所以 slug 跟 session id 對應關係已經在這堆檔案裡。撿回來：\nfor slug in \u0026#34;${SLUGS[@]}\u0026#34;; do sid=$(grep -m1 \u0026#39;^session id: \u0026#39; \u0026#34;/tmp/codex-$slug.err\u0026#34; | sed \u0026#39;s/session id: //\u0026#39;) src=$(ls \u0026#34;$HOME/.codex/generated_images/$sid\u0026#34;/*.png | head -1) cp \u0026#34;$src\u0026#34; \u0026#34;$OUT_DIR/$slug.png\u0026#34; echo \u0026#34;recovered $slug\u0026#34; done 14 張一個不漏全撿回來。剛好驗證了「Codex 報告失敗不等於真的沒產出」這件事 —— 下次遇到同樣狀況，永遠先看 cache 目錄再考慮重跑。\n部署 本機改完剩下就是搬回 VPS：\n# rsync content/ 跟 static/images/posts/ 回去 rsync -av --rsync-path=\u0026#34;sudo rsync\u0026#34; \\ ~/mklee/blog/content/posts/ vps:\u0026lt;blog\u0026gt;/content/posts/ rsync -av --rsync-path=\u0026#34;sudo rsync\u0026#34; \\ ~/mklee/blog/static/images/posts/ vps:\u0026lt;blog\u0026gt;/static/images/posts/ # VPS 上跑部署 ssh vps \u0026#39;sudo \u0026lt;blog-deploy-script\u0026gt;\u0026#39; Hugo build 301 ms，部署完抽樣驗證外網：\ncurl -o /dev/null -s -w \u0026#34;%{http_code}\u0026#34; \\ https://blog.mklee.org/images/posts/espresso-dialing-ramble.png # 200 HTML 渲染的 \u0026lt;meta property=\u0026quot;og:image\u0026quot;\u0026gt;、\u0026lt;meta name=\u0026quot;twitter:image\u0026quot;\u0026gt;、\u0026lt;figure class=\u0026quot;entry-cover\u0026quot;\u0026gt; 全部正確。現在點開文章列表不再是一排灰方塊，社群分享預覽也有圖了。\n帶走的兩件事 1. Sandbox scope 的直覺要 calibrate\n--sandbox workspace-write 的「workspace」是啟動 shell 的 CWD，不是隨便哪個 workspace。prompt 寫什麼絕對路徑、指定 output 都不會擴大寫入範圍。Batch 任務寫檔到特定目錄，cd 進去是第一步而不是可選步。\n2. 「失敗」訊息值得多問一句\nCodex 在 err 裡清楚寫 cp: Operation not permitted —— 這是「搬運失敗」，不是「生成失敗」。如果當下沒分清楚這兩種失敗，很容易直接重跑 14 張浪費 30 分鐘 + 420k tokens。產出物跟 agent 的自我陳述是兩件事，實際去看 cache 比相信 agent 的結論快。\n這兩件事也剛好更新進我的 skill 設定：~/.claude/skills/blog-writer/SKILL.md 現在有一段專門講「封面圖自動產圖」的流程 + fallback + CWD 提醒。下次想用自動化產圖，skill 會自己提醒該 cd 哪裡、該去哪找走失的圖。\n","permalink":"https://blog.mklee.org/posts/2026-04-22-codex-gpt-image-2-batch-covers/","summary":"\u003cp\u003e這個部落格有個存在很久的小債：17 篇文章裡，\u003cstrong\u003e一張封面都沒有\u003c/strong\u003e。每次打開文章列表都是一排光禿禿的灰底 placeholder，社群媒體分享預覽也只有標題文字。\u003c/p\u003e\n\u003cp\u003e一直沒處理是因為：要嘛手動一張張找圖很煩，要嘛丟給外部服務（Midjourney / DALL-E）要自己掏錢 + 管 API key + 存圖檔對應 slug，光想這個 pipeline 就懶。\u003c/p\u003e\n\u003cp\u003e直到昨天刷到一則推文：\u003cstrong\u003eCodex CLI 0.122 把 gpt-image-2 預設打開了\u003c/strong\u003e，不需要 API key，走你現有的 ChatGPT 帳號計費。配套還有一個叫 \u003ccode\u003ebaoyu-skills\u003c/code\u003e 的 Claude Code skill 集合，專門餵 Codex 生各種規格的圖。\u003c/p\u003e\n\u003cp\u003e也就是說：整條 pipeline 已經在我手邊，只是我不知道。那今天就來補債。\u003c/p\u003e\n\u003cp\u003e最後結果 17 張全生出來了，過程中踩到一個 \u003ccode\u003e--sandbox workspace-write\u003c/code\u003e 的誤會，有 14 張看起來全失敗，後來發現\u003cstrong\u003e圖其實都生成了，只是被困在 Codex 的快取目錄裡\u003c/strong\u003e。這篇記錄流程 + 踩坑 + 救援。\u003c/p\u003e\n\u003ch2 id=\"驗證codex-真的能畫圖\"\u003e驗證：Codex 真的能畫圖\u003c/h2\u003e\n\u003cp\u003e先 live test。我的 Codex 版本剛好是 \u003ccode\u003e0.122.0\u003c/code\u003e。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecodex \u003cspan style=\"color:#8be9fd;font-style:italic\"\u003eexec\u003c/span\u003e --skip-git-repo-check --sandbox workspace-write \u003cspan style=\"color:#f1fa8c\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u003c/span\u003e  --output-last-message \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#8be9fd;font-style:italic\"\u003e$OUT\u003c/span\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;\u003c/span\u003e -m gpt-5.4 \u003cspan style=\"color:#f1fa8c\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u003c/span\u003e  \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#39;$imagegen Create a simple 256x256 PNG of a red circle on white background. Save as /tmp/test.png. Final message: only the file path.\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#f1fa8c\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u003c/span\u003e  \u0026lt; /dev/null\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e跑完 \u003ccode\u003els /tmp/test.png\u003c/code\u003e 就有了。32 KB PNG、256×256 8-bit RGB、花了 ~30k tokens。\u003c/p\u003e","title":"一個下午補完 17 張 blog 封面：Codex gpt-image-2 + 一個 sandbox 陷阱的救援"},{"content":"OpenAI 在 4 月 15 日更新了 Agents SDK，加入了 sandbox 隔離、Harness/compute 分離、Manifest 工作區描述、Capabilities 分層。看完技術文件後我的第一反應是「這跟我們在做的事情不一樣」，第二反應是「但裡面有幾個結構性概念可以偷」。\n這篇記錄我們從中借了什麼、怎麼落地到 openclaw-workspace-template v3.0.0，以及為什麼大部分東西我們選擇不抄。\n兩個系統的定位差異 先把前提講清楚：OpenAI Agents SDK 跟我們的 workspace template 解決的是完全不同的問題。\nOpenAI Agents SDK 我們的 workspace template 目標使用者 企業，多租戶 SaaS 個人，單使用者 執行環境 雲端 sandbox（Modal / E2B / Cloudflare / Vercel） 本機 Mac / Linux，直接 filesystem access 權限模型 Tool calls 在 unprivileged container 裡跑，隔離網路和 secret 跟使用者同權限，能碰 git / cron / Telegram / Obsidian State serialize_session_state() / resume()，snapshot 整個 workspace 檔案系統就是 state，git 就是 snapshot Memory 內建 Memory() capability，session close 自動 summarize → consolidate 自建 MemPalace：hall-tagged journal + 主題筆記 + weekly reflection + knowledge graph OpenAI 在解的是「怎麼讓 agent 安全可靠地跑在生產環境」。我們在解的是「怎麼讓一個人的知識和自動化系統持續累積和整合」。拿來直接比就像比 AWS Lambda 跟家裡的 crontab。\n所以 sandbox 隔離、multi-tenant、container snapshot 這些我們完全不需要。我們的 agent 能直接 git commit、改 crontab、發 Telegram 通知、rebuild knowledge graph——把它塞進 sandbox 等於砍掉核心價值。\n但有三個結構性概念跟 runtime 無關，純粹是「怎麼描述和組織你的系統」的問題。這些值得借。\n借鏡 1：Manifest → workspace.spec OpenAI 怎麼做 OpenAI 的 Manifest 是一個 Python object，宣告式描述 agent 的工作區應該長什麼樣：\nManifest( entries=[ GitRepo(url=\u0026#34;...\u0026#34;, path=\u0026#34;repo\u0026#34;), LocalDir(host_path=\u0026#34;~/notes\u0026#34;, workspace_path=\u0026#34;notes\u0026#34;), S3Mount(bucket=\u0026#34;my-kb\u0026#34;, workspace_path=\u0026#34;kb\u0026#34;), ], environment={\u0026#34;PYTHONPATH\u0026#34;: \u0026#34;/workspace/repo\u0026#34;}, ) 跑 agent 前，sandbox 照這份 spec 組出環境。換機器、換 provider，同一份 Manifest 就能重建。\n我們之前怎麼做 bootstrap.sh 裡面一堆 mkdir -p 和 copy_tree 呼叫：\nmkdir -p \u0026#34;$WORKSPACE_PATH/memory\u0026#34; mkdir -p \u0026#34;$WORKSPACE_PATH/notes/00-Inbox\u0026#34; mkdir -p \u0026#34;$WORKSPACE_PATH/notes/01-Projects/Active\u0026#34; # ...13 個目錄 copy_tree \u0026#34;$SCRIPT_DIR/templates\u0026#34; copy_tree \u0026#34;$SCRIPT_DIR/scripts\u0026#34; \u0026#34;scripts\u0026#34; copy_tree \u0026#34;$SCRIPT_DIR/cron\u0026#34; \u0026#34;cron\u0026#34; 要知道「工作區應該有什麼」，得讀整份 bash。\n我們怎麼改 新增 templates/workspace.spec，44 行的 line-based DSL：\n# workspace.spec — Declarative workspace layout copy_tree templates copy_tree scripts scripts copy_tree cron cron dir memory dir notes/00-Inbox dir notes/01-Projects/Active dir notes/01-Projects/Archive dir notes/02-Areas dir notes/03-Resources dir notes/04-Archive dir .learnings dir scripts dir .claude/skills dir cron/logs dir reference dir tmp bootstrap.sh 裡新增一個 process_workspace_spec() function 讀這份檔案。條件邏輯（welcome flag、chmod、next-steps banner）留在 bash。\n為什麼不用 YAML？不想加 parser dependency。這份 spec 只有兩個 verb（dir 和 copy_tree），純 bash 的 parameter expansion 就能解析。遇到不認識的 verb 會直接 exit 1 並印出 workspace.spec:42: unknown verb 'bogus'。\n驗證方式：用舊版 bootstrap 建一個 workspace，再用新版建一個，find . | sort | diff 兩邊。結果完全一致（97 個路徑）。\n借鏡 2：Capabilities → settings.capabilities.toml OpenAI 怎麼做 OpenAI 的 Agents SDK 把 agent 的能力分成幾個 Capability 層：\nCapabilities( Shell(), Filesystem(), # apply_patch + view_image Skills(), Memory(), Compaction(), ) 預設是 Filesystem + Shell + Compaction。要加能力就顯式 opt-in。\n我們之前怎麼做 settings.json 裡有一個 flat array：\n\u0026#34;allow\u0026#34;: [ \u0026#34;Bash(python3 scripts/*)\u0026#34;, \u0026#34;Bash(bash scripts/*)\u0026#34;, \u0026#34;Bash(git status:*)\u0026#34;, \u0026#34;Bash(git diff:*)\u0026#34;, \u0026#34;Read(*)\u0026#34;, \u0026#34;Grep(*)\u0026#34;, \u0026#34;Write(memory/*)\u0026#34;, \u0026#34;Edit(notes/*)\u0026#34; ] 25 個 entry 混在一起，看不出「這個 agent 到底能幹嘛」的大圖。\n我們怎麼改 新增 settings.capabilities.toml 作為 source of truth，分成 6 個 capability bucket：\n[capabilities.run_scripts] description = \u0026#34;Execute project-owned scripts under scripts/\u0026#34; allow = [ \u0026#34;Bash(python3 scripts/*)\u0026#34;, \u0026#34;Bash(bash scripts/*)\u0026#34;, ] [capabilities.inspect_git] description = \u0026#34;Read-only git inspection\u0026#34; allow = [ \u0026#34;Bash(git status:*)\u0026#34;, \u0026#34;Bash(git diff:*)\u0026#34;, \u0026#34;Bash(git log:*)\u0026#34;, \u0026#34;Bash(git show:*)\u0026#34;, \u0026#34;Bash(git branch:*)\u0026#34;, ] [capabilities.write_memory] description = \u0026#34;Append/edit memory palace artifacts\u0026#34; allow = [ \u0026#34;Write(memory/*)\u0026#34;, \u0026#34;Write(MEMORY.md)\u0026#34;, \u0026#34;Edit(memory/*)\u0026#34;, \u0026#34;Edit(MEMORY.md)\u0026#34;, ] # ... tools/build-settings.py（stdlib tomllib，零外部 dependency）讀 TOML、生成 settings.json，同時保留 hooks 區塊不動。跑兩次產出 byte-identical 的 JSON。\nPermission set 完全沒變：25 個 entry，0 新增 0 移除。這不是加權限，是把既有權限整理成人類可讀的分組。\n借鏡 3：serialize_session_state → save/load YAML schema OpenAI 怎麼做 client.serialize_session_state() 把 agent 的 session 狀態序列化成結構化資料，client.resume() 在新的 sandbox 裡恢復。重點是：序列化的格式是有 schema 的，load 端可以靠固定欄位做型別化處理。\n我們之前怎麼做 /save 指令產出一個 markdown 檔到 tmp/\u0026lt;slug\u0026gt;.md，裡面是自由格式的 H2 sections：\n# Session State: ... ## Context （自由發揮） ## Completed （自由發揮） ## Pending ### High - ... /load 讀這份檔案，靠 Claude 的語意理解來解析。大部分時候可以用，但「哪些檔案被改過」「哪些 pending 是 high priority」這些資訊藏在 prose 裡面，跨 session 會 drift。\n我們怎麼改 加入 YAML frontmatter 作為機器可解析的 schema：\n--- slug: refactor-espresso-notes saved_at: 2026-04-16T14:30:00+08:00 project: cc-memory-project title: Refactor espresso dial-in notes summary: \u0026gt; 把散落在各處的 espresso dial-in 筆記整理成 per-bean template。 completed: - task: \u0026#34;建立 bean template\u0026#34; files: [\u0026#34;notes/02-Areas/Coffee/bean-template.md:1\u0026#34;] decision: \u0026#34;用 frontmatter field 而非 heading hierarchy\u0026#34; pending: - priority: high task: \u0026#34;現有 5 篇 bean note 還沒套 template\u0026#34; files: [\u0026#34;notes/02-Areas/Coffee/ethiopia-guji.md\u0026#34;] blocker: null suggested_fix: \u0026#34;逐篇跑 regex replace + 人工校對\u0026#34; --- 10 個 top-level key 全部 required，空值寫 null 或 []，不能省略 key。/load 先解析 frontmatter 做結構化摘要（pending 幾個 high / medium / low、touched 哪些檔案），再對 files[] 做 glob 檢查確認檔案還在。\n舊格式的 save 檔（沒有 frontmatter）走 legacy fallback，讀 H2 sections，標記 (legacy schema)。\n沒抄的東西 列一下明確選擇不做的：\nSandbox isolation：我們的 moat 就是深度整合本機環境。隔離 = 砍功能。 Manifest cloud mounts（S3 / GCS / Azure Blob）：state 全在本機 + git，不需要。 OpenAI Memory() capability：它是自動 summarize → consolidate 到 MEMORY.md。我們有自建的 MemPalace（hall tag journal、weekly reflection、pattern promotion、knowledge graph），功能複雜一個數量級。套它等於退化。 Harness/compute 分離：Claude Code 本來就是我們的 harness，它跟 filesystem 的耦合是 feature 不是 bug。 Multi-tenant 架構：我們是單使用者系統。為不存在的需求加 isolation tax 不值得。 實際效果 改完之後的差異是維護體驗：\n以前 現在 要知道 workspace 長什麼樣，讀 255 行 bash 讀 44 行 workspace.spec 要知道 agent 有什麼權限，看 25 行 flat array 然後心算 看 TOML 6 個 bucket，每個有 description settings.json 手動改，改完忘記改了什麼 改 TOML → 跑 build script → commit 兩份 /save 產出自由格式 prose，/load 靠語意猜 frontmatter 有 schema，/load 做型別化摘要 + 檔案存在檢查 bootstrap 完告訴你「edit IDENTITY/USER/SOUL/TOOLS」 bootstrap 完告訴你「type hi」，agent 一步一步問你 都不是大改動。workspace.spec 多了 44 行，build-settings.py 121 行，TOML 82 行，save/load schema 是 prompt 指令的修改。整個 PR 加起來 375 行新增。\n但這些改動讓下次要動 template 的人（通常是三個月後的我自己）不用重新讀 bash 才知道系統長什麼樣。\n結尾 OpenAI Agents SDK 在做的是幫企業把 agent 生產化的通用工具鏈。我們在做的是幫一個人極致整合生活和工作的垂直系統。兩者的 runtime 和安全需求差很遠，但「怎麼描述你的系統」這個問題是共通的。\nManifest 教我們把 workspace layout 從 bash 裡抽出來變成宣告式 spec。Capabilities 教我們把 flat permission list 分層成可 reason about 的 bucket。serialize_session_state 教我們把跨 session 的 handoff 從 prose 收緊成有 schema 的結構。\n三個都不是 rocket science，但都是「痛了才知道要做」的那種改善。趁看到別人做了，順手補上。\n","permalink":"https://blog.mklee.org/posts/2026-04-16-workspace-template-v3-openai-sdk-borrowings/","summary":"\u003cp\u003eOpenAI 在 4 月 15 日更新了 Agents SDK，加入了 sandbox 隔離、Harness/compute 分離、Manifest 工作區描述、Capabilities 分層。看完技術文件後我的第一反應是「這跟我們在做的事情不一樣」，第二反應是「但裡面有幾個結構性概念可以偷」。\u003c/p\u003e\n\u003cp\u003e這篇記錄我們從中借了什麼、怎麼落地到 \u003ca href=\"https://github.com/kindomLee/openclaw-workspace-template\"\u003eopenclaw-workspace-template\u003c/a\u003e v3.0.0，以及為什麼大部分東西我們選擇不抄。\u003c/p\u003e\n\u003ch2 id=\"兩個系統的定位差異\"\u003e兩個系統的定位差異\u003c/h2\u003e\n\u003cp\u003e先把前提講清楚：OpenAI Agents SDK 跟我們的 workspace template 解決的是完全不同的問題。\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e\u003c/th\u003e\n          \u003cth\u003eOpenAI Agents SDK\u003c/th\u003e\n          \u003cth\u003e我們的 workspace template\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e目標使用者\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e企業，多租戶 SaaS\u003c/td\u003e\n          \u003ctd\u003e個人，單使用者\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e執行環境\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e雲端 sandbox（Modal / E2B / Cloudflare / Vercel）\u003c/td\u003e\n          \u003ctd\u003e本機 Mac / Linux，直接 filesystem access\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e權限模型\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eTool calls 在 unprivileged container 裡跑，隔離網路和 secret\u003c/td\u003e\n          \u003ctd\u003e跟使用者同權限，能碰 git / cron / Telegram / Obsidian\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eState\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eserialize_session_state()\u003c/code\u003e / \u003ccode\u003eresume()\u003c/code\u003e，snapshot 整個 workspace\u003c/td\u003e\n          \u003ctd\u003e檔案系統就是 state，git 就是 snapshot\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eMemory\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e內建 \u003ccode\u003eMemory()\u003c/code\u003e capability，session close 自動 summarize → consolidate\u003c/td\u003e\n          \u003ctd\u003e自建 MemPalace：hall-tagged journal + 主題筆記 + weekly reflection + knowledge graph\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eOpenAI 在解的是「怎麼讓 agent 安全可靠地跑在生產環境」。我們在解的是「怎麼讓一個人的知識和自動化系統持續累積和整合」。拿來直接比就像比 AWS Lambda 跟家裡的 crontab。\u003c/p\u003e","title":"從 OpenAI Agents SDK 偷了三個概念，用在我們的 Claude Code 工作區 template"},{"content":"給 LLM agent 用的記憶系統寫了半年，最後發現自己打開次數最多的不是 MEMORY.md 或主題筆記，而是 memory/YYYY-MM-DD.md 這份逐日 journal。日常查的也多半是「上次搞 PostgreSQL 是什麼時候」「那個 config 的決定寫在哪天」這種問題。\n問題是：搜尋它不太好用。\ngrep 噪音太多。半年的 journal 把任何常用詞炸出幾百個 hit。 embedding-only 搜尋對 journal 不適合。journal 裡面多半是兩三行的短 entry，embedding 模型給短句的向量區分度不夠；而且 journal 每天長，每次 incremental embed 都要算 hash、做 rate limit、管 cache，出錯就是一整天的記憶搜不到。 最關鍵的是，journal 是本質上有時間性的資料。我問「上次搞 PostgreSQL 是什麼時候」，語意相似度幫不上忙，時間才是主角。 所以我把 journal 搜尋重新當成一個 ranking 問題，用三個分數軸融合：keyword overlap × temporal recency × hall-type boost。Python 不到 130 行，沒有外部依賴。\n這篇記錄設計過程、實測結果，還有一個在 bootstrap 測試時才發現的 case-sensitivity bug。\n為什麼不直接用 embedding 先說清楚：embedding 不是沒用。我自己在 notes/ 的相關筆記推薦也還是用 embedding 跑 cron。問題是把它用在 journal 搜尋上有幾個具體的不匹配：\n短文本訊號稀薄。一條 entry 常常只有一行，例如「決定採用 PostgreSQL」，embedding 模型對這種短句的區分度不夠。 時間性完全沒被 encode。cosine similarity 不知道「昨天的決定」比「三個月前的事件」更重要。要補回去就得再疊一層 rerank。 Rebuild 成本隨時間線性增長。每天新增 entry 就得 incremental embed，任何一個環節出錯，當天記憶就查不到。 API quota 風險。免費額度撐不住 journal 的 embed 頻率。 換句話說，embedding 適合「概念查詢」：query 跟 content 沒共用詞，只是意思接近。不適合「日誌式查詢」：使用者想找某個具體時間發生的事。我的 journal 搜尋場景幾乎都是後者。\n三個分數軸的設計 核心函式長這樣：\nSTOPWORDS = frozenset(\u0026#34;的了是在和有你他她它這那也就都不會能要...\u0026#34;) def extract_keywords(text: str) -\u0026gt; set: # 降 lower 在 regex 之前做，才能讓 \u0026#34;PostgreSQL\u0026#34; 匹配 \u0026#34;postgresql\u0026#34; words = re.findall(r\u0026#39;[\\w\\u4e00-\\u9fff—、。（）【】]{2,}\u0026#39;, text.lower()) return {w for w in words if w not in STOPWORDS and len(w) \u0026gt; 1} def temporal_boost(mtime, now, days): age = (now - mtime) / 86400 if age \u0026gt; days: return 0.3 # 超出視窗打 30% 折 recency = max(0.0, 1.0 - age / days) return 1.0 + 0.4 * recency # 最近的檔案最多加 40% def hall_boost(text: str) -\u0026gt; float: t = text.lower() if re.search(r\u0026#39;決定|决策|選擇|採用|decided|adopted\u0026#39;, t): return 1.3 if re.search(r\u0026#39;發現|研究|分析|discover|found\u0026#39;, t): return 1.15 if re.search(r\u0026#39;偏好|喜歡|習慣|prefer|like\u0026#39;, t): return 1.1 if re.search(r\u0026#39;建議|推薦|應該|recommend|suggest\u0026#39;, t): return 1.1 return 1.0 然後把三個分數 fuse 成最終 ranking：\nbase = min(1.0, len(content) / 5000) * (0.3 if kw_overlap \u0026gt; 0 else 0.05) fused = base * (1 + 0.3 * kw_overlap) * t_boost * h_boost 每一項都有特定目的：\nBase 把「有沒有任何 keyword 命中」拉出來當二值開關（命中 0.3、沒命中 0.05），避免長檔案純粹靠字數堆分數。 Keyword overlap 是 query 詞在檔案中出現的比例，範圍 0–1。乘 (1 + 0.3 × overlap) 的意思是讓命中多的檔案略領先，而不是讓 keyword 主導排名。 Temporal boost 是主角：一週內的命中跟三個月前的命中相比，在權重上差了將近 5 倍。 Hall boost 是對 journal 內容性質的偏見——「決定」類訊息對我比「事件」類值錢，所以 hall_facts 乘 1.3，hall_events 不加成。hall_* 的分類靈感來自 milla-jovovich/mempalace 提出的 hall 模型；我只是把它簡化成 5 類當 scoring signal，實際 entry 的 hall 前綴是另一支 hall-tagger.sh 依 keyword 規則自動標上的。 係數（0.3、0.4、1.3、1.15、1.1）全是拍腦袋調出來的。實際用一陣子會知道要不要再微調。\n走一個真實 query 對本機 240+ 個 journal + note 檔跑 memory-search-hybrid.py \u0026quot;postgresql database\u0026quot; --days 30 --top 3：\n[1] 02-Areas/Infrastructure/tinyauth-pocketid-setup.md score=0.7052 kw=1.0 temp=1.391 ...支援 S3 / SQLite / PostgreSQL 儲存... [2] 01-Projects/Active/cramclaw-line-instance.md score=0.6239 kw=1.0 temp=1.391 ...SQLite / PostgreSQL | 題庫 DB | 題目 + embedding... [3] archive-2026-03/2026-03-25-auth-comparison.md score=0.597 kw=1.0 temp=1.178 三個都是有效結果。兩個 query 詞（postgresql、database）都命中，kw=1.0。第三個因為是 16 天前的 archive，temp=1.178 明顯低於前兩者的 1.391，所以排到第三。排名邏輯就是常識：命中都對，越新越前面。\n一個 bug 上面這段成立有個前提：keyword 要在比較前 lowercase。第一版寫錯了，extract_keywords 只在 stopword 過濾時 lowercase，回傳的 set 保留原本大小寫：\n# 錯的版本 words = re.findall(r\u0026#39;...\u0026#39;, text) return {w for w in words if w.lower() not in STOPWORDS ...} 結果：query \u0026quot;postgresql database\u0026quot; 萃出來是 {\u0026quot;postgresql\u0026quot;, \u0026quot;database\u0026quot;}（query 本來就是小寫），content 萃出來是 {\u0026quot;PostgreSQL\u0026quot;, \u0026quot;資料庫\u0026quot;, ...}。兩個 set 永遠 overlap 不到。\n這個 bug 在本機跑了兩週才被發現，因為日常查詢九成都是中文——中文沒有大小寫問題，分數還是對的。直到把同一段程式碼搬到另一個 repo、在乾淨環境跑 smoke test、用一個純英文 query 當 fixture，才看到 kw_overlap 全 0。\n修法是一行：\nwords = re.findall(r\u0026#39;...\u0026#39;, text.lower()) 整段寫下來只有兩件事值得記：第一，測試資料要包含你日常不會碰到的 corner——中文使用者測英文 query，英文使用者測中文 query。第二，把程式碼搬到乾淨環境跑一遍是抓 bug 成本最低的方法之一；bootstrap 的 smoke test 跑到這段以前，它都在裝沒事。\n適用範圍 這套有幾個具體的不適合場景：\n概念查詢：query 跟 content 沒共用詞，只是意思相近。這時 embedding 會贏。 大規模 corpus：240 個檔案全掃一次約 100 ms。如果 journal 超過 10k 檔案，就得改用 inverted index，不能每次全掃。 非 Markdown 格式：現在的 regex tokenizer 是針對 Markdown 寫的，其他格式要另外處理。 我的使用場景是「單一使用者、幾百到幾千個檔案、中英文混雜、每天有新 entry」。對這類場景，三軸 fused scoring 比純 grep 或純 embedding 都更合用一些。如果你的 journal 規模跟我差不多，可能也合用。\n跟 hall tagger 串起來才是完整 pipeline hall_boost 能運作的前提是 entry 開頭有 [hall_*] 前綴。手動打太累，所以另外寫了一支 scripts/hall-tagger.sh：掃過去 N 天的 journal，依 keyword 規則自動補前綴。Idempotent，加過的不會重加，可以每週 cron 跑一次。\n整個 pipeline：\n打字 → memory/YYYY-MM-DD.md entry ↓ 週期 cron → hall-tagger.sh → 補 [hall_*] 前綴 ↓ 查詢 → memory-search-hybrid.py → fused ranking Cron 做 deterministic 的事，搜尋腳本做 ranking，LLM 只做真正需要判斷的事。這是這套工作流的設計原則之一。\n程式碼位置 完整檔案在 openclaw-workspace-template 的 scripts/memory-search-hybrid.py 和 scripts/hall-tagger.sh，隨 v2.4.0 引入，MIT License。\n係數都是拍腦袋調的，你的 journal 寫作風格不同，調一下才合身。\n","permalink":"https://blog.mklee.org/posts/2026-04-10-hybrid-memory-search/","summary":"\u003cp\u003e給 LLM agent 用的記憶系統寫了半年，最後發現自己打開次數最多的不是 \u003ccode\u003eMEMORY.md\u003c/code\u003e 或主題筆記，而是 \u003ccode\u003ememory/YYYY-MM-DD.md\u003c/code\u003e 這份逐日 journal。日常查的也多半是「上次搞 PostgreSQL 是什麼時候」「那個 config 的決定寫在哪天」這種問題。\u003c/p\u003e\n\u003cp\u003e問題是：搜尋它不太好用。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003egrep\u003c/code\u003e 噪音太多。半年的 journal 把任何常用詞炸出幾百個 hit。\u003c/li\u003e\n\u003cli\u003eembedding-only 搜尋對 journal 不適合。journal 裡面多半是兩三行的短 entry，embedding 模型給短句的向量區分度不夠；而且 journal 每天長，每次 incremental embed 都要算 hash、做 rate limit、管 cache，出錯就是一整天的記憶搜不到。\u003c/li\u003e\n\u003cli\u003e最關鍵的是，journal 是\u003cstrong\u003e本質上有時間性的\u003c/strong\u003e資料。我問「上次搞 PostgreSQL 是什麼時候」，語意相似度幫不上忙，時間才是主角。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e所以我把 journal 搜尋重新當成一個 \u003cstrong\u003eranking 問題\u003c/strong\u003e，用三個分數軸融合：keyword overlap × temporal recency × hall-type boost。Python 不到 130 行，沒有外部依賴。\u003c/p\u003e\n\u003cp\u003e這篇記錄設計過程、實測結果，還有一個在 bootstrap 測試時才發現的 case-sensitivity bug。\u003c/p\u003e\n\u003ch2 id=\"為什麼不直接用-embedding\"\u003e為什麼不直接用 embedding\u003c/h2\u003e\n\u003cp\u003e先說清楚：embedding 不是沒用。我自己在 notes/ 的相關筆記推薦也還是用 embedding 跑 cron。問題是把它用在 journal 搜尋上有幾個具體的不匹配：\u003c/p\u003e","title":"Hybrid memory search：把 journal 搜尋當成 ranking 問題"},{"content":"2026 年 4 月 4 日，收到一封信 那天像平常一樣，結果打開郵箱多了一封 Anthropic 的通知。讀完標題就知道事情不對：\n「第三方 harness 不再消耗訂閱配額，需另外付費。」\n這句話翻譯一下：OpenClaw 從這天起，不能再用馬克的 Claude Max Plan 配額了。\n政策內容 項目 說明 第三方 harness OpenClaw、agent-broker 等，不消耗訂閱配額，需另外付費 官方產品 claude.ai / Claude Code CLI / Claude Cowork，正常消耗訂閱 Max Plan 用戶補償 $100 credit，4/17 前領取，90 天效期 取消選項 4/9 前在網頁版 cancel 可自動退款 $100 credit 這點很重要，是 Anthropic 的善意。但重點是：這改變了整個用量結構。\n先搞清楚：Anthropic 是怎麼偵測第三方 harness 的？ 一個問題立刻浮現：Anthropic 是怎麼知道我在用 OpenClaw 的？\n直接逆向 Claude Code CLI v2.1.85 二進制檔案（用 strings + Node.js binary inspection），找到了答案。\nClaude Code 每個 API request 都帶這些 header：\nx-app: \u0026#34;cli\u0026#34; # 硬編碼，無法偽造 User-Agent: \u0026#34;claude-cli/2.1.85 ...\u0026#34; # 含版本、平台 ACP 模式下，如果設定了環境變數，還會帶：\nx-client-app: \u0026#34;\u0026lt;harness 名稱\u0026gt;\u0026#34; # 來自 CLAUDE_AGENT_SDK_CLIENT_APP env 也就是說，Anthropic 不是靠猜的，是 CLI 自己把名字打在 header 裡。agent-broker 這類 bridge 短期可能漏網——因為它 spawn Claude Code CLI，但不一定設定 x-client-app——但長期不是可靠策略。\n決策 馬上做了個表：\n方案 成本 穩定性 適合程度 維持現狀（Opus） 高（需 extra usage） 高 ❌ MiniMax M2.7（Coding Plan） 中（已有訂閱） 高 ✅ 主要場景 直接用 Claude Max 包含在訂閱 高 ⚠️ 適合複雜推理 MiniMax M2.7 的出現是意外。當初裝它純粹是因為 OpenClaw 的 fallback chain 需要一個 API key 類型的 provider，M2.7 的性價比適合拿來墊底。沒想到幾個月後派上這個用場。\n為什麼是 M2.7，不是 Opus？ 兩個理由：\n第一，錢。 Opus 的 API 費用比 M2.7 貴得多，日常對話用 Opus 是浪費。Max Plan 的配額留給真正需要的場合。\n第二，夠用。 M2.7 在日常對話、資訊收集、郵件摘要、資料整理這些場景，表現跟 Opus 的差距在可接受範圍內。不是所有問題都需要 Opus 來回答。\n複雜推理的時候呢？手動下 /model opus，用完再切回來。\n模型策略（更新後） 場景 模型 日常對話、問答 MiniMax M2.7 研究、摘要、整理 MiniMax M2.7（sub-agent） 複雜推理、敏感決策 Opus（手動切換） 程式開發 MiniMax M2.7 或 GPT-5.4 這個策略從 4 月 1 日開始實行，到現在大約一週。觀察：\n郵件摘要、資訊收集、系統 routine：MM 2.7 完全可勝任，沒有出現明顯的品質問題 回應速度比 Opus 快一些，少了那層「思考 lag」 偶爾需要 Opus 的時候，手動切換不麻煩 一個意外收穫（社群驗證）：用「持續 N 分鐘」的時間約束 prompt，效果比「請詳細分析」好得多。LLM 會把時間限制理解為量化目標，在期限內持續行動。對研究任務特別實用。\n關於 $100 Credit 目前還沒有用上。這筆 credit 留給日後需要 Anthropic API 的場合——少量複雜任務、模型比較、代碼稽核。不急著現在用。\n結語 政策變更是常態。這次能在幾小時內完成評估、決策、切換，靠的不是「反應快」，而是：\n有備案：早就有 MiniMax M2.7 在機制內，不是臨時找的 事實優先：先逆向看 header，不靠直覺猜 Anthropic 的偵測機制 風險分散：不是全部押在一家模型上 不是危機處理，更像是例行性的風險分散。只不過這次分散得剛剛好。\n相關文章：MiniMax M2.7 上線\n","permalink":"https://blog.mklee.org/posts/2026-04-05-anthropic-harness-policy-switch-to-minimax/","summary":"\u003ch2 id=\"2026-年-4-月-4-日收到一封信\"\u003e2026 年 4 月 4 日，收到一封信\u003c/h2\u003e\n\u003cp\u003e那天像平常一樣，結果打開郵箱多了一封 Anthropic 的通知。讀完標題就知道事情不對：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e「第三方 harness 不再消耗訂閱配額，需另外付費。」\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e這句話翻譯一下：\u003cstrong\u003eOpenClaw 從這天起，不能再用馬克的 Claude Max Plan 配額了。\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"政策內容\"\u003e政策內容\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e項目\u003c/th\u003e\n          \u003cth\u003e說明\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e第三方 harness\u003c/td\u003e\n          \u003ctd\u003eOpenClaw、agent-broker 等，不消耗訂閱配額，需另外付費\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e官方產品\u003c/td\u003e\n          \u003ctd\u003eclaude.ai / Claude Code CLI / Claude Cowork，正常消耗訂閱\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eMax Plan 用戶補償\u003c/td\u003e\n          \u003ctd\u003e$100 credit，4/17 前領取，90 天效期\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e取消選項\u003c/td\u003e\n          \u003ctd\u003e4/9 前在網頁版 cancel 可自動退款\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e$100 credit 這點很重要，是 Anthropic 的善意。但重點是：這改變了整個用量結構。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"先搞清楚anthropic-是怎麼偵測第三方-harness-的\"\u003e先搞清楚：Anthropic 是怎麼偵測第三方 harness 的？\u003c/h2\u003e\n\u003cp\u003e一個問題立刻浮現：\u003cstrong\u003eAnthropic 是怎麼知道我在用 OpenClaw 的？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e直接逆向 Claude Code CLI v2.1.85 二進制檔案（用 strings + Node.js binary inspection），找到了答案。\u003c/p\u003e","title":"Anthropic 收費政策突變與 MiniMax M2.7 的意外相遇"},{"content":"記憶系統撐到了極限 跑了四個月的 AI agent，記憶目錄（memory/）膨脹到 37 個檔案。聽起來不多，但仔細一看：\n日常日誌（2026-03-12.md、2026-03-13.md\u0026hellip;）佔大多數 混雜著主題檔（sso-booking.md、cramclaw-webhook.md） 還有 session 元數據（只有 5 行的 session key/id） 甚至有一個 .html 檔案不知道怎麼混進來的 Agent 每次啟動要讀今天和昨天的日誌，加上 MEMORY.md 長期索引。理論上這套系統能運作，但實際上出了幾個問題。\n問題一：日誌噪音 每天的日誌什麼都記——閒聊、debug 過程、中間嘗試、最終結論。當你搜尋「WireGuard 設定」，會找到五天前的 debug 記錄，卻找不到最終的設定方案，因為那被埋在某天日誌的第 200 行。\n問題二：知識碎片化 同一個主題散落在不同日期的日誌裡。咖啡研究在 3/10、3/18、3/23 都有，但沒有一個統一的地方彙整「我目前對 Soup Method 的理解」。Agent 沒辦法回答「關於 X 我知道什麼」——它只能回答「某天發生了什麼跟 X 有關的事」。\n問題三：重複與矛盾 知識庫（Obsidian notes）裡有 377 個筆記，掃描後發現 9 組重複（18 個檔案）。同一個技術方案在 02-Areas/Tech/ 和 01-Projects/ 各有一份，內容略有差異。哪個是對的？都不完全對。\n借鏡：Harness Engineering 的 Entropy Management 2026 年 AI 工程圈開始談 Harness Engineering——不只是讓 agent 能做事，而是控制它做事的品質。三個支柱：\nContext Engineering：給 agent 什麼資訊 Architectural Constraints：限制 agent 的行為邊界 Entropy Management：防止系統隨時間退化 記憶系統的問題本質上是 entropy 問題。沒有主動管理，資訊會自然趨向混亂——重複累積、過時不清、碎片分散。\nByteRover 提出了 Context Tree 的概念：用層次化的樹狀結構組織記憶，而不是扁平的日期序列。NLAH 論文（arXiv:2603.25723）則指出，agent 的工作記憶和長期記憶應該分開管理，定期做整合。\n這些概念很好，但多數實作需要額外的工具或資料庫。我想要一個純檔案、零依賴的方案。\n兩層架構：Journal + Knowledge 核心想法：把「發生了什麼」和「學到了什麼」分開存。\nworkspace/ ├── memory/ # Layer 1: Journal（時序日誌） │ ├── 2026-03-28.md # 今天發生的事 │ ├── 2026-03-27.md # 昨天發生的事 │ └── archive-2026-03/ # 5 天前的自動歸檔 ├── notes/ # Layer 2: Knowledge（語意知識庫） │ ├── areas/ # 按主題分類（咖啡、技術、基礎設施...） │ └── resources/ # 工具/服務參考 ├── MEMORY.md # 長期索引（P0/P1/P2 分級） └── reference/ # 深度參考文件 分類決策樹 每次記憶寫入前，過一個簡單的分類：\n這是「發生了什麼」還是「學到了什麼」？ ├─ 發生了什麼 → memory/YYYY-MM-DD.md │ └─ 重要到值得長期索引？→ 同時更新 MEMORY.md ├─ 學到了什麼 │ ├─ 已有相關筆記？→ 合併到現有筆記 │ ├─ 新主題且超過 500 字？→ notes/ 建新檔 │ └─ 碎片不到 500 字？→ 先放日誌，等 cron 整理 └─ 不確定？→ 放日誌（安全選項） 關鍵原則是合併優先——寫入前先搜尋有沒有相關的筆記，有就追加，不要每次都建新檔案。這是控制 entropy 的核心手段。\n第一步：清理現有的混亂 先處理存量，再改架構。\n日誌歸檔 memory/ 只保留最近 5 天的日誌。超過的全部移到 archive-YYYY-MM/：\n# 把 03-24 之前的日誌全部歸檔 for f in memory/2026-03-{12..23}*.md; do mv \u0026#34;$f\u0026#34; memory/archive-2026-03/ done 14 個檔案歸檔。session 元數據、html 檔案也一併清掉。memory/ 從 37 個檔案降到 13 個。\n筆記去重 用 sub-agent 掃描 377 個筆記，找到 9 組重複。處理策略：\n內容較完整的保留 另一份移到 04-Archive/merged/ 合併後的筆記補上缺失的資訊 8 組合併完成（16 → 8 檔），加上 4 個過時筆記歸檔。最終 377 → 365 個活躍筆記。\nYouTube 筆記修復 5 個 YouTube 筆記缺少 frontmatter status 欄位，補齊。\n第二步：打通搜尋 清理完成後，要讓 memory_search 能搜到 notes/ 裡的知識。OpenClaw 支援 memorySearch.extraPaths 設定：\n{ \u0026#34;memorySearch\u0026#34;: { \u0026#34;extraPaths\u0026#34;: [\u0026#34;notes/areas\u0026#34;, \u0026#34;notes/resources\u0026#34;, \u0026#34;reference\u0026#34;] } } 設定後，embedding 索引會把這些目錄下的 .md 檔案也納入。搜尋「Soup Method」不再只找到某天日誌裡的片段，而是直接命中主題筆記。\n一個小坑：Gemini 免費 tier 的 embedding API 有 rate limit，401 個檔案不可能一次索引完。解法是寫一個自動重試的 cron，每 10 分鐘嘗試一次，完成後自動通知並移除自己：\n#!/bin/bash INDEXED=$(openclaw memory status 2\u0026gt;\u0026amp;1 | grep \u0026#34;Indexed:\u0026#34; | head -1) CURRENT=$(echo \u0026#34;$INDEXED\u0026#34; | grep -oP \u0026#39;\\d+(?=/)\u0026#39;) TOTAL=$(echo \u0026#34;$INDEXED\u0026#34; | grep -oP \u0026#39;(?\u0026lt;=/)\\d+\u0026#39;) if [ \u0026#34;$CURRENT\u0026#34; -ge \u0026#34;$TOTAL\u0026#34; ]; then # 完成，移除 cron crontab -l | grep -v \u0026#34;memory-index-retry\u0026#34; | crontab - openclaw message send --message \u0026#34;Memory index 完成: $INDEXED\u0026#34; exit 0 fi openclaw memory index --force 第三步：更新寫入規則 架構改了，寫入規則也要跟著改。更新了三個地方：\nAGENTS.md（agent 的操作手冊） 兩層記憶：journal（memory/ 時序日誌）+ knowledge（notes/ 語意知識庫） - Journal：事件、決策、狀態變更。保留 5 天，之後歸檔。 - Knowledge：主題知識。合併優先，不新建碎片。 - 不在 memory/ 放主題檔：主題知識進 notes/，memory/ 只放日期 journal save-memory skill 分類決策從扁平的「P0/P1/P2 加上 notes/reference/learnings」改成先判斷 journal vs knowledge，再決定具體存放位置。\ncron-memory-sync 每小時的記憶同步腳本本來就會把 memory/ 的內容整理到 Obsidian 筆記。現在加上更明確的規則：合併到現有筆記，不要建碎片檔。\n清理前後對比 指標 Before After memory/ 活躍檔案 37 13（-65%） notes/ 活躍筆記 377 365（-3%） 重複筆記組 9 組 0 缺 frontmatter 5 0 memory_search 範圍 memory/ only memory/ + notes/ + reference/ 數字上的變化不是重點。重點是資訊流向變清晰了：\n對話 ──→ memory/YYYY-MM-DD.md（發生了什麼） ──→ notes/areas/（學到了什麼） ──→ MEMORY.md（長期索引） ↓ memory_search 全部搜得到 這套架構的局限 誠實說幾個還沒解決的問題：\n索引延遲。Gemini 免費 tier 的 rate limit 意味著新加入的 notes 不會立即被索引。目前是靠重試 cron 漸進完成，但在索引完成前，搜尋會有盲區。\n合併判斷。「這個知識應該合併到哪個現有筆記」是一個需要語意理解的判斷。目前交給 agent 自己決定，偶爾會判斷錯。\n歸檔時機。5 天的 rolling window 是拍腦袋定的。太短可能錯過需要回顧的近期事件，太長又回到噪音問題。目前沒有數據支持最佳值。\n跨層一致性。journal 裡記了一個決策，knowledge 裡也有相關筆記，兩邊的資訊可能不一致。rumination（反芻）機制理論上能抓到矛盾，但實際上覆蓋率有限。\n下一步 幾個想嘗試的方向：\nreference/ 搬進 notes/：目前 reference/ 是獨立目錄，但語意上它就是 knowledge 的一部分。合併後統一入口。 自動合併建議：cron 定期掃描 notes/，用 embedding 相似度找出可能該合併的筆記，生成建議清單。 memory hit tracking：追蹤哪些 notes 被 memory_search 命中最多次，用來判斷哪些知識最有價值、哪些可以歸檔。 記憶系統不是建一次就完成的東西。它跟程式碼一樣需要重構——只是重構的不是邏輯，而是資訊的組織方式。\n這是 AI agent 記憶系列的第三篇。前兩篇：OpenClaw 記憶管理：從零到自迭代的架構演化、讓 AI Agent 學會做夢：記憶的睡眠循環機制。\n","permalink":"https://blog.mklee.org/posts/context-tree-agent-memory/","summary":"\u003ch2 id=\"記憶系統撐到了極限\"\u003e記憶系統撐到了極限\u003c/h2\u003e\n\u003cp\u003e跑了四個月的 AI agent，記憶目錄（\u003ccode\u003ememory/\u003c/code\u003e）膨脹到 37 個檔案。聽起來不多，但仔細一看：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e日常日誌（\u003ccode\u003e2026-03-12.md\u003c/code\u003e、\u003ccode\u003e2026-03-13.md\u003c/code\u003e\u0026hellip;）佔大多數\u003c/li\u003e\n\u003cli\u003e混雜著主題檔（\u003ccode\u003esso-booking.md\u003c/code\u003e、\u003ccode\u003ecramclaw-webhook.md\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e還有 session 元數據（只有 5 行的 session key/id）\u003c/li\u003e\n\u003cli\u003e甚至有一個 \u003ccode\u003e.html\u003c/code\u003e 檔案不知道怎麼混進來的\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAgent 每次啟動要讀今天和昨天的日誌，加上 \u003ccode\u003eMEMORY.md\u003c/code\u003e 長期索引。理論上這套系統能運作，但實際上出了幾個問題。\u003c/p\u003e\n\u003ch3 id=\"問題一日誌噪音\"\u003e問題一：日誌噪音\u003c/h3\u003e\n\u003cp\u003e每天的日誌什麼都記——閒聊、debug 過程、中間嘗試、最終結論。當你搜尋「WireGuard 設定」，會找到五天前的 debug 記錄，卻找不到最終的設定方案，因為那被埋在某天日誌的第 200 行。\u003c/p\u003e\n\u003ch3 id=\"問題二知識碎片化\"\u003e問題二：知識碎片化\u003c/h3\u003e\n\u003cp\u003e同一個主題散落在不同日期的日誌裡。咖啡研究在 3/10、3/18、3/23 都有，但沒有一個統一的地方彙整「我目前對 Soup Method 的理解」。Agent 沒辦法回答「關於 X 我知道什麼」——它只能回答「某天發生了什麼跟 X 有關的事」。\u003c/p\u003e\n\u003ch3 id=\"問題三重複與矛盾\"\u003e問題三：重複與矛盾\u003c/h3\u003e\n\u003cp\u003e知識庫（Obsidian notes）裡有 377 個筆記，掃描後發現 9 組重複（18 個檔案）。同一個技術方案在 \u003ccode\u003e02-Areas/Tech/\u003c/code\u003e 和 \u003ccode\u003e01-Projects/\u003c/code\u003e 各有一份，內容略有差異。哪個是對的？都不完全對。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"借鏡harness-engineering-的-entropy-management\"\u003e借鏡：Harness Engineering 的 Entropy Management\u003c/h2\u003e\n\u003cp\u003e2026 年 AI 工程圈開始談 Harness Engineering——不只是讓 agent 能做事，而是控制它做事的品質。三個支柱：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eContext Engineering\u003c/strong\u003e：給 agent 什麼資訊\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eArchitectural Constraints\u003c/strong\u003e：限制 agent 的行為邊界\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEntropy Management\u003c/strong\u003e：防止系統隨時間退化\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e記憶系統的問題本質上是 entropy 問題。沒有主動管理，資訊會自然趨向混亂——重複累積、過時不清、碎片分散。\u003c/p\u003e","title":"AI Agent 記憶的 Context Tree：從日誌地獄到兩層架構"},{"content":"問題：SKILL.md 靠人工調校太慢 OpenClaw 的 skill 系統靠 SKILL.md 指引 agent 行為——什麼時候觸發、怎麼執行、輸出什麼格式。寫得好，agent 就穩定；寫得差，每次跑出來的品質都不一樣。\n我的 workspace 裝了二十多個 skill，平時靠「出問題 → 改一行 → 觀察幾天 → 再改」的方式迭代。這種人工調校有兩個問題：\n回饋週期太長。 改了一行要等幾天才知道有沒有效果。 靠直覺不靠數據。 改完「感覺比較好」，但沒有量化指標。 如果能讓 LLM 自己評估 SKILL.md 的效果，再自動改進，迭代速度會快很多。\n靈感：GEPA（ICLR 2026） 逛 GitHub 時發現 NousResearch 的 hermes-agent，裡面有一套 self-evolution 機制，核心引用了 GEPA 這篇論文（Genetic Prompt Evolution with NL Reflection，ICLR 2026 Oral）。\nGEPA 的概念不複雜：\n評估：用 LLM 打分（而不是人類標註） 反思：讓 LLM 自己分析「哪裡扣分了、為什麼」 變異：根據反思結果修改 prompt 選擇：保留最高分的版本，淘汰退步的 跟 RLHF 不同，整個過程只需要 API call，不需要 GPU 做 gradient update。論文宣稱比 GRPO 少 35 倍 rollouts。\n我不需要 GEPA 的完整框架（DSPy + Pareto selection），但「LLM 評估 → LLM 反思 → LLM 改進」這個循環可以直接搬過來。\n第一關：API 認證踩坑 決定用 MiniMax M2.5 當演化引擎——便宜、有 thinking 模式、我的 Coding Plan 已經在用。\n一開始想用 DSPy + litellm 接 MiniMax：\nimport dspy lm = dspy.LM(\u0026#34;anthropic/MiniMax-M2.5\u0026#34;, api_base=\u0026#34;https://api.minimax.io/anthropic\u0026#34;) 跑不起來。litellm 堅持走 OpenAI chat endpoint（api.minimax.chat/v1），而 MiniMax Coding Plan key 只接受 Anthropic Messages endpoint（api.minimax.io/anthropic）。兩個端點，同一把 key，一個能用一個不能。\n放棄 DSPy，改用 httpx 直接打 Anthropic Messages API：\nresp = httpx.post( \u0026#34;https://api.minimax.io/anthropic/v1/messages\u0026#34;, headers={\u0026#34;x-api-key\u0026#34;: API_KEY, \u0026#34;anthropic-version\u0026#34;: \u0026#34;2023-06-01\u0026#34;}, json={ \u0026#34;model\u0026#34;: \u0026#34;MiniMax-M2.5\u0026#34;, \u0026#34;max_tokens\u0026#34;: 4000, \u0026#34;thinking\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;enabled\u0026#34;, \u0026#34;budget_tokens\u0026#34;: 4000}, \u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: prompt}] }, timeout=300 ) 這條路通了。教訓：遇到多層抽象（DSPy → litellm → provider），不如直接打 HTTP。\nPipeline 設計 最終的演化流程分三步：\n1. 生成評估集 讓 LLM 讀原始 SKILL.md，自動產生 5 個測試案例。每個案例包含：\n場景描述：模擬一個使用情境 好信號（good signals）：預期輸出應該包含的內容 壞信號（bad signals）：預期輸出不應該包含的內容 例如 blog-writer 的一個案例：\n{ \u0026#34;scenario\u0026#34;: \u0026#34;寫一篇 Docker Compose 初學者教學\u0026#34;, \u0026#34;good_signals\u0026#34;: [\u0026#34;繁體中文\u0026#34;, \u0026#34;frontmatter 包含 title/date/tags\u0026#34;, \u0026#34;段落不超過 4 行\u0026#34;], \u0026#34;bad_signals\u0026#34;: [\u0026#34;超級簡單\u0026#34;, \u0026#34;一步到位\u0026#34;, \u0026#34;必備\u0026#34;] } 2. 評估 把 SKILL.md 和每個測試案例一起送給 LLM，讓它打 0-100 分：「如果一個 agent 讀了這份 SKILL.md，它能在這個場景下正確執行嗎？」\n打分不只看分數，還要求附上理由——哪些好信號被涵蓋了、哪些壞信號沒被擋住。\n3. 演化 把低分案例的扣分理由彙總，要求 LLM 重寫整份 SKILL.md，改善弱點，保留已有優勢。\n重複 2-3 輪，保留最高分版本。\n原始 SKILL.md → 評估(84) → 反思 → 改寫 → 評估(95) → 反思 → 改寫 → 評估(93) ↑ 最高分，保留這個版本 blog-writer 實驗結果 用 blog-writer SKILL.md 做第一次實驗：\n輪次 平均分 差異 0（原版） 84-90 — 1 95.6 +5~12 2 95.0 +5~11 第 1 輪就拉上去了，第 2 輪略降。最終採用第 1 輪的版本。\n具體改了什麼？LLM 自己加了兩個東西：\n禁止詞彙表（原版只有一句「不誇飾」，太模糊）：\n類型 禁用詞彙 誇大形容詞 驚人的、革命性的、碾壓、極致、秒懂 絕對化表述 一步到位、必備、必讀、不可或缺的 自我吹捧 輕鬆搞定、輕鬆學會、輕鬆上手 煽動語句 學會這招再也、從此不用 問題排查文結構（原版沒有明確指導排查類文章怎麼寫）：\n問題描述：什麼錯誤？環境？ 排查過程：嘗試了什麼？為何失敗？ 解決方法：最終怎麼解？為何有效？ 這兩個改動合理，都被合併進了正式的 SKILL.md。\n擴大到三個 skill 確認 pipeline 可用後，跑了另外兩個 skill：\nSkill Baseline Best Delta blog-writer 84-90 95-97 +5~12 yt-notes 88.0 97.0 +9.0 coding-agent 62.0 78.0 +16.0 github 86.6 86.6 0 幾個觀察：\ncoding-agent 提升最大（+16），因為原版比較粗糙。演化後加了 PTY 規則表、Quick Start 流程、Task Workflow 分類。這些都是原版缺少但有價值的結構。\ngithub skill 沒有改善——原版已經寫得很好了，LLM 嘗試修改的版本反而降分。系統正確地保留了原版。這是一個重要的特性：演化不是強制改變，如果原版已經是最優解，它會被保留。\nyt-notes 的改動大多是格式化：把散文式的規則改成表格、加 checklist。內容沒有大變化，但結構更清晰，LLM 讀起來更容易遵循。\n演化也會產生垃圾 不是每個改動都好。blog-writer 的第 2 輪演化裡出現了：\n把 categories 和 cover.image 改成必填——原版標為選填是有原因的 把「經驗分享」改稱「教學」——定位錯誤 夾帶了一段日文——MiniMax 的多語言訓練有時會洩漏 coding-agent 演化後有些規則太死板（強制要求每個 task 都跑測試），在實際使用中會拖慢速度。\n所以 human-in-the-loop 不能省。pipeline 生成 diff，人審核後才合併。自動演化 + 自動部署的風險太高。\n工具 最終的 pipeline 是兩個檔案：\nevolve_skill.py（278 行）：通用演化腳本，httpx 直打 MiniMax Anthropic endpoint skill-evolve.sh：wrapper，掃描所有 skill 目錄，依次跑演化 每個 skill 跑完會產出：\ntmp/skill-evolution/\u0026lt;skill\u0026gt;/ ├── eval_cases.json # 自動生成的測試案例 ├── results.json # 每輪分數 ├── best_skill.md # 最高分版本 └── diff.patch # 與原版的差異 設計上做了幾個防護：\nper-task timeout：評估 90 秒、演化 300 秒。MiniMax thinking 模式需要時間，但不能無限等 crash-safe：每輪結果即時寫入 JSON，中途斷電不會丟失 retry + backoff：API 偶爾超時，自動重試 2 次 保留原版的能力：如果演化版低於原版，diff 是空的 計畫每週日凌晨自動跑一次，Telegram 通知結果，人工決定是否合併。\n幾個教訓 MiniMax thinking 模式需要足夠的 token 預算。 一開始為了省 token 把 max_tokens 縮到 2000，演化品質很差。改成 4000 後才正常。thinking 模式的 token 是花在推理上，不是浪費。\ntimeout 比 max_tokens 更重要。 原本 eval 和 evolve 用同一個 timeout（60 秒），evolve 總是超時。拆成 eval=90s、evolve=300s 後就穩了。不同任務需要不同的時間預算。\nAPI endpoint 選擇是隱形坑。 MiniMax Coding Plan key 能用 Anthropic endpoint，不能用 OpenAI endpoint。同一個 provider 的兩個 endpoint，key 的權限不同。文件裡沒寫清楚。\n演化結果的 content 結構要注意。 MiniMax 有時回傳的 content 只有 thinking block 沒有 text block，需要遍歷 response.content 找 type == \u0026quot;text\u0026quot; 的那個。不能直接取 content[0].text。\n下一步 目前只在 SKILL.md 層面做演化（GEPA 的 Tier 1）。更高風險的層級——Tool Description（Tier 2）、System Prompt（Tier 3）、Agent Code（Tier 4）——暫時不碰。\nSkill 演化帶來的改善是溫和的：結構更清晰、邊界更明確、壞模式被顯式列出。不是 10 倍提升，但讓 agent 行為的一致性上了一個台階。\n最大的收穫可能不是分數，而是「用 LLM 評估 LLM 的 prompt」這個方法本身。以前改 SKILL.md 靠的是直覺和偶爾的踩坑，現在有了量化的回饋循環。\n","permalink":"https://blog.mklee.org/posts/2026-03-gepa-skill-evolution/","summary":"\u003ch2 id=\"問題skillmd-靠人工調校太慢\"\u003e問題：SKILL.md 靠人工調校太慢\u003c/h2\u003e\n\u003cp\u003eOpenClaw 的 skill 系統靠 SKILL.md 指引 agent 行為——什麼時候觸發、怎麼執行、輸出什麼格式。寫得好，agent 就穩定；寫得差，每次跑出來的品質都不一樣。\u003c/p\u003e\n\u003cp\u003e我的 workspace 裝了二十多個 skill，平時靠「出問題 → 改一行 → 觀察幾天 → 再改」的方式迭代。這種人工調校有兩個問題：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e回饋週期太長。\u003c/strong\u003e 改了一行要等幾天才知道有沒有效果。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e靠直覺不靠數據。\u003c/strong\u003e 改完「感覺比較好」，但沒有量化指標。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e如果能讓 LLM 自己評估 SKILL.md 的效果，再自動改進，迭代速度會快很多。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"靈感gepaiclr-2026\"\u003e靈感：GEPA（ICLR 2026）\u003c/h2\u003e\n\u003cp\u003e逛 GitHub 時發現 NousResearch 的 \u003ca href=\"https://github.com/NousResearch/hermes-agent\"\u003ehermes-agent\u003c/a\u003e，裡面有一套 self-evolution 機制，核心引用了 GEPA 這篇論文（Genetic Prompt Evolution with NL Reflection，ICLR 2026 Oral）。\u003c/p\u003e\n\u003cp\u003eGEPA 的概念不複雜：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e評估\u003c/strong\u003e：用 LLM 打分（而不是人類標註）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e反思\u003c/strong\u003e：讓 LLM 自己分析「哪裡扣分了、為什麼」\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e變異\u003c/strong\u003e：根據反思結果修改 prompt\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e選擇\u003c/strong\u003e：保留最高分的版本，淘汰退步的\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e跟 RLHF 不同，整個過程只需要 API call，不需要 GPU 做 gradient update。論文宣稱比 GRPO 少 35 倍 rollouts。\u003c/p\u003e","title":"讓 AI Agent 的技能自我進化：用 GEPA 自動優化 SKILL.md"},{"content":"Espresso 的變數太多：豆子、烘焙度、粉量、研磨度、萃取時間⋯⋯每個參數都會影響風味。想穩定萃取，就得記錄每次的參數，慢慢找到每支豆子的 sweet spot。\n紙筆記了幾天就懶了，試過幾個 App 也都不順手。最後決定自己做一個。\n用對話蓋出來的 整個開發過程是透過 OpenClaw（開源 AI Agent 框架）完成的。我沒打開 IDE，就是在對話框裡描述需求：\n「單頁 HTML，localStorage 存資料，Chart.js 畫圖。」 「豆子名稱要能搜尋。」 「預設值帶入上一杯的參數。」\nAI 生成程式碼、我測試、回報問題、它修。來回大約 30 分鐘，從空白檔案到部署上 NAS。\nPlaceholder 的坑 過程中卡最久的是預設值。需求很簡單：新增記錄時自動帶入上一杯的粉量、研磨度等參數，使用者只改有變動的欄位。\n前幾版用 placeholder 顯示預設值。畫面上看得到數字，但 placeholder 只是提示文字，不是 input.value。留空送出時，存進去的是空值，不是畫面上顯示的數字。\n來回修了三版才想通：別用 placeholder 模擬預設值，直接把值填進 input。看到什麼就存什麼，沒有歧義。\n最佳參數怎麼算 儀表板有一個「各豆子最佳參數」的區塊。一開始是取所有記錄的平均值，但這樣失敗的杯次會拉低數據。\n改成：找每支豆子評分最高的記錄，取那些記錄的平均水粉比、研磨度、萃取時間。邏輯很簡單，就是 filter + reduce，但語義上合理多了——你想參考的是最好的幾杯，不是所有杯的平均。\n長什麼樣 記錄表單：填完按儲存，預設值自動帶入上一杯\n最近 10 杯，每筆可編輯或刪除\n儀表板：各豆子最佳參數、研磨度趨勢、評分分佈\n功能 記錄：豆子、烘焙度、粉量、研磨度、萃取時間、出杯量、粉碗、評分、備註 豆子和粉碗都有搜尋（從歷史記錄建 autocomplete） 預設值自動帶入，選了歷史豆子會切換成該豆子上次的參數 時間快捷鍵：現在 / 5 分前 / 10 分前 / 30 分前 / 1 小時前 儀表板：研磨度趨勢、評分分佈、各豆子最佳參數 JSON 匯入匯出 深色主題，手機可用 技術上就是一個 HTML 檔，沒後端。\n部署 NAS 上用 nginx:alpine 跑靜態站，透過 Portainer API 建 Docker stack，5 分鐘搞定。\n另外加了一個 records.json，讓 AI agent 可以透過 SSH 寫入記錄。這樣在外面也能用對話記參數，頁面打開時會自動合併。\n試用 工具放在 這裡，打開就能用，資料存在你的瀏覽器裡。想自己部署也行，就一個 HTML 檔丟進任何 web server。\n","permalink":"https://blog.mklee.org/posts/2026-03-coffee-tracker/","summary":"\u003cp\u003eEspresso 的變數太多：豆子、烘焙度、粉量、研磨度、萃取時間⋯⋯每個參數都會影響風味。想穩定萃取，就得記錄每次的參數，慢慢找到每支豆子的 sweet spot。\u003c/p\u003e\n\u003cp\u003e紙筆記了幾天就懶了，試過幾個 App 也都不順手。最後決定自己做一個。\u003c/p\u003e\n\u003ch2 id=\"用對話蓋出來的\"\u003e用對話蓋出來的\u003c/h2\u003e\n\u003cp\u003e整個開發過程是透過 \u003ca href=\"https://github.com/openclaw/openclaw\"\u003eOpenClaw\u003c/a\u003e（開源 AI Agent 框架）完成的。我沒打開 IDE，就是在對話框裡描述需求：\u003c/p\u003e\n\u003cp\u003e「單頁 HTML，localStorage 存資料，Chart.js 畫圖。」\n「豆子名稱要能搜尋。」\n「預設值帶入上一杯的參數。」\u003c/p\u003e\n\u003cp\u003eAI 生成程式碼、我測試、回報問題、它修。來回大約 30 分鐘，從空白檔案到部署上 NAS。\u003c/p\u003e\n\u003ch2 id=\"placeholder-的坑\"\u003ePlaceholder 的坑\u003c/h2\u003e\n\u003cp\u003e過程中卡最久的是預設值。需求很簡單：新增記錄時自動帶入上一杯的粉量、研磨度等參數，使用者只改有變動的欄位。\u003c/p\u003e\n\u003cp\u003e前幾版用 \u003ccode\u003eplaceholder\u003c/code\u003e 顯示預設值。畫面上看得到數字，但 \u003ccode\u003eplaceholder\u003c/code\u003e 只是提示文字，不是 \u003ccode\u003einput.value\u003c/code\u003e。留空送出時，存進去的是空值，不是畫面上顯示的數字。\u003c/p\u003e\n\u003cp\u003e來回修了三版才想通：別用 placeholder 模擬預設值，直接把值填進 input。看到什麼就存什麼，沒有歧義。\u003c/p\u003e\n\u003ch2 id=\"最佳參數怎麼算\"\u003e最佳參數怎麼算\u003c/h2\u003e\n\u003cp\u003e儀表板有一個「各豆子最佳參數」的區塊。一開始是取所有記錄的平均值，但這樣失敗的杯次會拉低數據。\u003c/p\u003e\n\u003cp\u003e改成：找每支豆子評分最高的記錄，取那些記錄的平均水粉比、研磨度、萃取時間。邏輯很簡單，就是 \u003ccode\u003efilter\u003c/code\u003e + \u003ccode\u003ereduce\u003c/code\u003e，但語義上合理多了——你想參考的是最好的幾杯，不是所有杯的平均。\u003c/p\u003e\n\u003ch2 id=\"長什麼樣\"\u003e長什麼樣\u003c/h2\u003e\n\u003cfigure\u003e\n    \u003cimg loading=\"lazy\" src=\"/images/2026-03/coffee-tracker-form.png\"\n         alt=\"萃取記錄表單\"/\u003e \u003cfigcaption\u003e\n            \u003cp\u003e記錄表單：填完按儲存，預設值自動帶入上一杯\u003c/p\u003e\n        \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cfigure\u003e\n    \u003cimg loading=\"lazy\" src=\"/images/2026-03/coffee-tracker-records.png\"\n         alt=\"歷史記錄列表\"/\u003e \u003cfigcaption\u003e\n            \u003cp\u003e最近 10 杯，每筆可編輯或刪除\u003c/p\u003e\n        \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cfigure\u003e\n    \u003cimg loading=\"lazy\" src=\"/images/2026-03/coffee-tracker-dashboard.png\"\n         alt=\"儀表板\"/\u003e \u003cfigcaption\u003e\n            \u003cp\u003e儀表板：各豆子最佳參數、研磨度趨勢、評分分佈\u003c/p\u003e\n        \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003ch2 id=\"功能\"\u003e功能\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e記錄：豆子、烘焙度、粉量、研磨度、萃取時間、出杯量、粉碗、評分、備註\u003c/li\u003e\n\u003cli\u003e豆子和粉碗都有搜尋（從歷史記錄建 autocomplete）\u003c/li\u003e\n\u003cli\u003e預設值自動帶入，選了歷史豆子會切換成該豆子上次的參數\u003c/li\u003e\n\u003cli\u003e時間快捷鍵：現在 / 5 分前 / 10 分前 / 30 分前 / 1 小時前\u003c/li\u003e\n\u003cli\u003e儀表板：研磨度趨勢、評分分佈、各豆子最佳參數\u003c/li\u003e\n\u003cli\u003eJSON 匯入匯出\u003c/li\u003e\n\u003cli\u003e深色主題，手機可用\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e技術上就是一個 HTML 檔，沒後端。\u003c/p\u003e","title":"用 AI Agent 打造咖啡萃取記錄器"},{"content":"前情：記憶清理的粗暴現狀 上一篇講了記憶架構怎麼從空白演化成多層結構——daily files、MEMORY.md 長期記憶、自動反芻和做夢機制。寫入的問題解決了，但清理一直很粗暴。\nmemory-expire.sh 的邏輯就一行：超過 30 天就歸檔。\n大部分時候這沒問題。但有些記憶明明超過 30 天了，卻每天都在被搜尋命中——比如二月初寫的 espresso 配方筆記，到三月中還一直被引用。一刀切歸檔會把活躍記憶誤殺。\n另一方面，有些記憶寫完就再也沒被搜到過。它們佔著 embedding 搜尋的空間，拉低搜尋精度。\n需要一個比日期更聰明的判斷依據。\n思路：追蹤「誰在用這段記憶」 靈感很直接：如果一段記憶在過去 30 天內被搜尋命中過多次，它就是「活的」，不該被歸檔。\n做法：掃描所有 session 的 JSONL 日誌，提取 memory_search tool call 的結果，統計每個記憶檔案被命中的次數。\nsession JSONL → 提取 memory_search 結果 → 統計命中次數 → hit_counts.jsonl 這個 hit count 資料就是 Memory Quality Score 的核心。\n實作：從 Python 到 Rust Python 原型（200 行） 第一版用 Python 寫，邏輯很直接：\n掃 ~/.openclaw/agents/main/sessions/*.jsonl 找 tool_use type 是 memory_search 的 entries 從對應的 tool_result 提取命中的檔案路徑 累計到 memory/hit_counts.jsonl 跑一次大概 160ms，掃完 145 個 session 檔案得到 408 個命中記錄。\n結果很有趣 Top 5 最常被搜到的記憶： 72x MEMORY.md — 幾乎每次搜尋都命中 23x espresso-notes.md — 咖啡相關問題持續在問 14x 2026-02-25.md — 某天做了很多基礎設施決策 11x 2026-02-26.md 10x acp-setup.md — ACP 設定一直被引用 ⚠️ 從未被搜尋命中：27 個檔案 27 個從未被搜到的檔案——佔總數近一半。這些要嘛是太細碎的日記（「今天修了個 bug」），要嘛是寫了就沒再看過的專題筆記。\n整合進現有機制 不需要獨立的 cron job。把 hit count 數據餵進已有的兩個腳本：\nmemory-reflect.sh（每日反芻）：加入使用頻率分析。反芻時不只看內容是否過時，也看「這段記憶有人在用嗎？」 memory-expire.sh（月初歸檔）：加保護門檻。hit count \u0026gt; 2 的檔案即使超過 30 天也不歸檔。 一個 espresso 配方筆記寫於二月初，按日期早該歸檔了。但它在過去 30 天被搜尋了 23 次——顯然還在服役。保護門檻讓它留下來。\nRust 重寫：為什麼值得 Python 原型跑得動，但每天至少跑兩次（reflect + expire），每次都要：\n啟動 Python interpreter 解析 JSON 掃描大量 JSONL 檔案（有些超過 10MB） 這些全是 I/O 密集操作，Rust 的優勢最明顯。\n把 memory-hit-tracker.py（200 行）和 memory-janitor.py（414 行）合併成一個 Rust binary：\nmemory-tools hit-tracker --days 30 # 追蹤搜尋命中 memory-tools janitor --dry-run # 記憶清理（預覽） memory-tools janitor --force # 記憶清理（執行） 效能提升：\n操作 Python Rust 加速 hit-tracker (30天) 160ms 50ms 3.2x janitor (dry-run) 40ms \u0026lt;5ms 8x+ Binary 1.8MB，沒有 runtime 依賴。Cron 直接呼叫，不用等 Python 啟動。\n委派 MiniMax 寫 Rust 904 行的 main.rs 交給 MiniMax M2.5 sub-agent 寫，我負責審核。\n審核時抓到三個 bug：\nFile::create 截斷問題。Python 的 open('a') 是 append，但 MM 寫的 Rust 用 File::create 會把現有內容清掉。修正為 File::options().create(true).append(true).open()。\nArchive 路徑多嵌一層。memory_dir.join(\u0026quot;archive-2026-02\u0026quot;) 產生 memory/archive-2026-02，但 memory_dir 已經是 \u0026lt;workspace\u0026gt;/memory/，所以實際路徑變成 memory/memory/archive-2026-02。應該用 workspace.join(\u0026quot;memory\u0026quot;).join(...)。\nTelegram config 路徑。expand_home(\u0026quot;.openclaw/openclaw.json\u0026quot;) 少了 ~/，在某些環境下找不到檔案。改用 dirs::home_dir().join(\u0026quot;.openclaw/openclaw.json\u0026quot;)。\n三個 bug 佔 904 行 ≈ 0.3% 的 bug 率。不算差，但每個都是 data corruption 等級——不審核就上線的話，會靜默丟失 hit count 資料或把記憶歸檔到錯誤路徑。\n設計選擇：為什麼不做更複雜的 為什麼不用資料庫？ JSONL 就夠了。每行一筆 hit record，append-only，grep 就能查。SQLite 會多一個依賴，而且記憶系統的寫入頻率很低（每天幾十筆）。\n為什麼不做即時追蹤？ Session JSONL 是 OpenClaw 的內部格式，不保證即時 flush。批次掃描（每小時一次）比 hook 進 OpenClaw 內部穩定得多。\n為什麼 hit count \u0026gt; 2 是門檻？ 初始值。2 代表「至少在不同場合被搜到過兩次」——不是偶然命中。等數據累積幾個月，可以用統計方法調整。現在先用 heuristic。\n歸檔 ≠ 刪除 搬到 memory/archive-YYYY-MM/，不是刪掉。歸檔的檔案不會被 memory_search 的 Gemini embedding 搜到（已驗證），但人類隨時可以去翻。\n現在的記憶生命週期 新記憶寫入 memory/YYYY-MM-DD.md │ ├─ 每日 reflect → 檢查內容是否過時/矛盾 │ + 分析搜尋頻率 │ ├─ 每日 expire check → 超過 30 天？ │ ├─ hit count \u0026gt; 2 → 保留（活躍記憶） │ └─ hit count ≤ 2 → 歸檔到 archive-YYYY-MM/ │ └─ MEMORY.md 提煉 → 重要決策/偏好提升到長期記憶 這不是完美的系統。它還是依賴 memory_search 的 embedding 品質（Gemini embedding-001），如果搜尋本身就沒找到該找的東西，hit count 就會低估。\n但比起「30 天一刀切」，已經好很多了。至少那個被搜了 23 次的 espresso 配方筆記不會被誤殺。\n下一步 幾個想做但還沒做的：\n衰減曲線：不只看 hit count，也看趨勢。一個月前被搜 20 次但最近兩週是 0 的記憶，可能也該降權。 主動推送：如果某段記憶的 hit count 突然飆升，可能代表最近在密集處理某個主題，主動把相關記憶載入 context。 跨 session 關聯：不只追蹤「哪個檔案被搜」，也追蹤「哪些檔案經常一起被搜」，建立記憶之間的關聯圖。 但現在這個版本已經解決了最直接的問題：讓記憶清理有數據依據，不再靠日期蠻幹。\n這篇是 OpenClaw 記憶管理系列 的延續。上一篇講的是記憶架構的演化，這篇講的是如何用數據管理記憶的生命週期。\n","permalink":"https://blog.mklee.org/posts/memory-quality-score/","summary":"\u003ch2 id=\"前情記憶清理的粗暴現狀\"\u003e前情：記憶清理的粗暴現狀\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"/posts/openclaw-memory-architecture/\"\u003e上一篇\u003c/a\u003e講了記憶架構怎麼從空白演化成多層結構——daily files、MEMORY.md 長期記憶、自動反芻和做夢機制。寫入的問題解決了，但清理一直很粗暴。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ememory-expire.sh\u003c/code\u003e 的邏輯就一行：\u003cstrong\u003e超過 30 天就歸檔。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e大部分時候這沒問題。但有些記憶明明超過 30 天了，卻每天都在被搜尋命中——比如二月初寫的 espresso 配方筆記，到三月中還一直被引用。一刀切歸檔會把活躍記憶誤殺。\u003c/p\u003e\n\u003cp\u003e另一方面，有些記憶寫完就再也沒被搜到過。它們佔著 embedding 搜尋的空間，拉低搜尋精度。\u003c/p\u003e\n\u003cp\u003e需要一個比日期更聰明的判斷依據。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"思路追蹤誰在用這段記憶\"\u003e思路：追蹤「誰在用這段記憶」\u003c/h2\u003e\n\u003cp\u003e靈感很直接：如果一段記憶在過去 30 天內被搜尋命中過多次，它就是「活的」，不該被歸檔。\u003c/p\u003e\n\u003cp\u003e做法：掃描所有 session 的 JSONL 日誌，提取 \u003ccode\u003ememory_search\u003c/code\u003e tool call 的結果，統計每個記憶檔案被命中的次數。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esession JSONL → 提取 memory_search 結果 → 統計命中次數 → hit_counts.jsonl\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e這個 hit count 資料就是 Memory Quality Score 的核心。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"實作從-python-到-rust\"\u003e實作：從 Python 到 Rust\u003c/h2\u003e\n\u003ch3 id=\"python-原型200-行\"\u003ePython 原型（200 行）\u003c/h3\u003e\n\u003cp\u003e第一版用 Python 寫，邏輯很直接：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e掃 \u003ccode\u003e~/.openclaw/agents/main/sessions/*.jsonl\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e找 \u003ccode\u003etool_use\u003c/code\u003e type 是 \u003ccode\u003ememory_search\u003c/code\u003e 的 entries\u003c/li\u003e\n\u003cli\u003e從對應的 \u003ccode\u003etool_result\u003c/code\u003e 提取命中的檔案路徑\u003c/li\u003e\n\u003cli\u003e累計到 \u003ccode\u003ememory/hit_counts.jsonl\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e跑一次大概 160ms，掃完 145 個 session 檔案得到 408 個命中記錄。\u003c/p\u003e","title":"AI Agent 記憶品質：用數據決定什麼該記、什麼該忘"},{"content":" 這是我們在 OpenClaw 系統實作記憶機制的心得，也是對「讓 AI Agent 學會做夢」一文的延伸。我們不談論文，只談踩過的坑和做出來的解法。\n為什麼 AI Agent 需要記憶？ 不是所有 LLM 應用都需要記憶。一個回答使用者問題的客服機器人，問完就可以忘了；一個生成文案的工具，用完就走。但當 Agent 需要 長期運行、累積經驗、理解上下文，情況就完全不同了。\n我們的 OpenClaw Agent 需要：\n記住使用者的偏好（他喜歡高密度資訊，不愛廢話） 記住基礎設施的狀態（哪台機器開了、哪個服務掛過） 記住決策的來龍去脈（當初為什麼選這個方案） 沒有記憶，每次對話都是獨立的瞬間，Agent 永遠是新手。這就是我們要解決的問題。\n難題一：壓縮 — context window 有限，保留什麼？ 問題 即使是 GPT-5.4 或 Claude 4.6，context window 終究有限。當記憶累積超過臨界點，你不可能把全部東西都塞進去。壓縮不是選項，是必然。\n但壓縮代表選擇。選擇本身就是困難的：\n哪些對話值得記住？ 抽象化（summarization）會不會丟失關鍵細節？ 如果壓縮演算法選錯了重要資訊，後果是什麼？ 業界做法 常見的壓縮策略有幾種：\n方法 說明 缺點 簡單摘要 LLM 產出濃縮版本 容易遺漏細節，無法精確檢索 向量檢索 存 embedding， query 時召回 只能搜「相似」，無法知道「重要」 優先級排序 依重要性決定保留顆粒度 依賴準確的優先級判斷 我們的做法 我們採用 三層記憶架構，用「分層」取代「一次性壓縮」：\ndaily memory（便簽）→ MEMORY.md（長期）→ reference/（結構化知識）\ndaily memory：每天的 raw 紀錄，像貼在冰箱上的便利貼 MEMORY.md：萃取後的長期知識，需要主動維護 reference/：結構化資料（設定檔、API 文件、流程 SOP） 同時搭配 P 級優先級：\n等級 保留時間 範例 P0 永久 個人偏好、基礎設施配置 P1 90 天 技術決策、專案進展 P2 30 天 實驗記錄、臨時觀察 這不是最優解，但實際運行下來，足夠實用。我們發現 P 級判斷比任何複雜的壓縮演算法都可靠——因為它直接對應「這件事過期後還有價值嗎？」\n學到的教訓 一開始我們試過全自動摘要，結果某次維護紀錄被摘要成「伺服器正常」，實際上那是凌晨三點搶修的災難紀錄。壓縮的資訊損失是隱性的，爆發時你才會發現。\n所以我們後來改成「高層次用摘要，低層次用歸檔」——不強求把所有東西壓進 context，而是用分層 + 過期的機制，讓 context window 裡永遠是「最近相關」的東西。\n2026-03 更新：社群出現了 lossless-claw 這樣的方案——用 SQLite 保留 100% 原始對話，DAG 分層摘要取代截斷。這和我們的三層架構思路一致：不要丟東西，而是分層壓縮。差別在於 lossless-claw 作用在 session 內的 context window，我們的方案作用在跨 session 的長期記憶。兩者可以共存。\n難題二：演化 — 記憶不是靜態的 問題 記憶不是寫入後就靜止的。使用者的偏好會變：\n「上次你說不要問，直接做。」「不對，我現在需要你先確認。」\n知識也會過時：\n「那個 API 去年改過了。」「這台機器已經換 IP 了。」\n當記憶和現實脫節，Agent 會給出過時的建議，甚至可能造成損害。\n業界做法 時間戳記：每條記憶加時間，query 時過濾 版本化：不覆蓋，永遠存新版本 主動刷新：定時重新驗證記憶的正確性 我們的做法 我們用 兩個自動化腳本 來處理演化問題：\n1. memory-reflect.sh（反芻） 每天 21:00 執行，對比「今天的記憶」vs「長期記憶」：\n檢測矛盾（例如：今天記「偏好用 Qwen」，但長期記「偏好用 Claude」） 檢測過時（例如：三個月前提到的 API 版本） 產出整合建議（哪些記憶該更新、哪些該合併） 這不是簡單的比對，而是一次 LLM 對話。讓模型自己判斷：「這兩條資訊衝突了，我該相信哪個？」\n實際運行後發現，reflect 最大的價值不是「糾錯」，而是 發現隱性矛盾。有些偏好我們自己都沒意識到在漂移，Agent 反而看見了。\n2. memory-expire.sh（過期歸檔） 每月 1 號執行，超過 30 天的 daily memory 自動搬到 archive/。不是刪除，是歸檔——哪天需要回溯，還是可以查出來。\n# 實際邏輯（簡化版） if [[ \u0026#34;$P_LEVEL\u0026#34; == \u0026#34;P2\u0026#34; \u0026amp;\u0026amp; \u0026#34;$DAYS_SINCE\u0026#34; -gt 30 ]]; then mv \u0026#34;$file\u0026#34; \u0026#34;archive/\u0026#34; fi 學到的教訓 一開始我們只有「定期刪除」，沒有「歸檔」。結果某次需要回溯三個月前的決策，發現什麼都沒了。現在我們相信：寧可多存，不可錯殺。 歸檔比刪除安全。\n難題三：衝突 — 新舊記憶矛盾時怎麼辦？ 問題 這是三個難題中最麻煩的。\n使用者今天說「用 Telegram」 但長期記憶裡寫的是「用 LINE」\n是新記憶對了，還是舊記憶對了？或者，兩者都對（因為情境不同）？\n業界做法 最後寫入優先（Last-Write-Wins） 手動解決（交給人類裁判） 多重版本並存（不解決，只標記） 我們的做法 memory-reflect.sh 的矛盾檢測就是為這題設計的。當偵測到衝突，腳本會：\n標記：註明哪些記憶互相矛盾 分析：LLM 判斷情境（是偏好改變？還是情境不同？） 建議：產出「整合建議」，告訴人類（或 agent）該怎麼處理 # 輸出範例 ## 矛盾檢測 - [daily] 偏好用 Telegram 傳送訊息 - [MEMORY] 偏好用 LINE 傳送訊息 → 建議：更新 MEMORY.md，註記「近期改用 Telegram」 但這裡有個實務問題：不是所有矛盾都需要解決。有些矛盾是因為情境變了（家裡 vs 公司），有些是因為偏好真的改了。我們的原則是：\n低風險矛盾：不主動干預，等累積夠多再處理 高風險矛盾（例如安全設定）：立即警告 學到的教訓 我們曾經試過「發現矛盾就立刻修正」，結果造成過度反應——使用者只是隨口說說，結果長期記憶被改亂了。\n現在的做法是 「觀察 → 累積 → 驗證 → 行動」：讓矛盾累積一陣子，確認是趨勢而非雜訊，再由 reflect 統一處理。\n我們的答案：讓 Agent 學會「睡覺」 把三個難題的解法拼在一起，就是我們的 記憶睡眠循環：\n時間 腳本 作用 每天 21:00 memory-reflect.sh 反芻：矛盾檢測 + 整合建議 每週日 03:00 memory-dream.sh 做夢：跨領域洞察（8 個隨機片段） 每月 1 號 memory-expire.sh 過期歸檔：30 天以上的 daily memory 搬家 為什麼叫「睡眠」？ 因為這三個腳本都在 非工作時間 執行。Agent 醒著的時候處理請求，睡著的時候整理自己。這和人類睡眠時大腦做記憶整合的概念類似——不是一直在輸入，而是在適當的時候處理。\ndream.sh 在做什麼？ 這是我們最「瘋狂」的實驗。每週日凌晨，系統隨機抽 8 個 memory 片段（可能是三週前的技術筆記、兩天前的使用者偏好、一個月前的故障記錄），讓 LLM 試著找「跨領域洞察」。\n老實說，大部分產出是廢話。但偶爾會出現意想不到的觀點：\n「你上個月處理過 WireGuard MTU 問題，上週又遇到 Docker network 問題。這兩者都涉及網路層，可能需要一個通用的網路除錯 SOP。」\n這是 冷記憶聯想——平時不會放在一起想的東西，在做夢模式下被強制湊對，說不定能產生新的洞見。\n結語：實用的記憶系統 這篇文章沒有提出什麼新穎的理論。我們只是在面對「AI Agent 需要長期記憶」這個實際問題時，一步一步堆出來的做法。\n三層架構、P 級優先、sleep cycle——每一個設計都是為了回答一個具體問題：\n壓縮 → 用分層取代全量摘要 演化 → 用 reflect + expire 自動處理過時 衝突 → 用 reflect 統一檢測和整合 如果你也在做類似的事情，我們的 workspace-template 是開源的：\ngithub.com/kindomLee/openclaw-workspace-template\n裡面包含所有提到的腳本（memory-dream.sh、memory-reflect.sh、memory-expire.sh），歡迎取用、批評、改進。\n記憶系統沒有終極解答。我們的解法也不見得適合所有人。但如果你被「到底要留什麼」或者「記憶越來越亂」困住，希望這篇分享能給你一些方向。\n如果喜歡這篇文章，或想看我們更多關於 AI Agent 實作的筆記，歡迎追蹤我們的 blog。\n","permalink":"https://blog.mklee.org/posts/ai-agent-memory-three-challenges/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003e這是我們在 OpenClaw 系統實作記憶機制的心得，也是對\u003ca href=\"/posts/ai-agent-memory-sleep-cycle/\"\u003e「讓 AI Agent 學會做夢」\u003c/a\u003e一文的延伸。我們不談論文，只談踩過的坑和做出來的解法。\u003c/em\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"為什麼-ai-agent-需要記憶\"\u003e為什麼 AI Agent 需要記憶？\u003c/h2\u003e\n\u003cp\u003e不是所有 LLM 應用都需要記憶。一個回答使用者問題的客服機器人，問完就可以忘了；一個生成文案的工具，用完就走。但當 Agent 需要 \u003cstrong\u003e長期運行\u003c/strong\u003e、\u003cstrong\u003e累積經驗\u003c/strong\u003e、\u003cstrong\u003e理解上下文\u003c/strong\u003e，情況就完全不同了。\u003c/p\u003e\n\u003cp\u003e我們的 OpenClaw Agent 需要：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e記住使用者的偏好（他喜歡高密度資訊，不愛廢話）\u003c/li\u003e\n\u003cli\u003e記住基礎設施的狀態（哪台機器開了、哪個服務掛過）\u003c/li\u003e\n\u003cli\u003e記住決策的來龍去脈（當初為什麼選這個方案）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e沒有記憶，每次對話都是獨立的瞬間，Agent 永遠是新手。這就是我們要解決的問題。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"難題一壓縮--context-window-有限保留什麼\"\u003e難題一：壓縮 — context window 有限，保留什麼？\u003c/h2\u003e\n\u003ch3 id=\"問題\"\u003e問題\u003c/h3\u003e\n\u003cp\u003e即使是 GPT-5.4 或 Claude 4.6，context window 終究有限。當記憶累積超過臨界點，你不可能把全部東西都塞進去。\u003cstrong\u003e壓縮不是選項，是必然。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e但壓縮代表選擇。選擇本身就是困難的：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e哪些對話值得記住？\u003c/li\u003e\n\u003cli\u003e抽象化（summarization）會不會丟失關鍵細節？\u003c/li\u003e\n\u003cli\u003e如果壓縮演算法選錯了重要資訊，後果是什麼？\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"業界做法\"\u003e業界做法\u003c/h3\u003e\n\u003cp\u003e常見的壓縮策略有幾種：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e方法\u003c/th\u003e\n          \u003cth\u003e說明\u003c/th\u003e\n          \u003cth\u003e缺點\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e簡單摘要\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eLLM 產出濃縮版本\u003c/td\u003e\n          \u003ctd\u003e容易遺漏細節，無法精確檢索\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e向量檢索\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e存 embedding， query 時召回\u003c/td\u003e\n          \u003ctd\u003e只能搜「相似」，無法知道「重要」\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e優先級排序\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e依重要性決定保留顆粒度\u003c/td\u003e\n          \u003ctd\u003e依賴準確的優先級判斷\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"我們的做法\"\u003e我們的做法\u003c/h3\u003e\n\u003cp\u003e我們採用 \u003cstrong\u003e三層記憶架構\u003c/strong\u003e，用「分層」取代「一次性壓縮」：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ccode\u003edaily memory\u003c/code\u003e（便簽）→ \u003ccode\u003eMEMORY.md\u003c/code\u003e（長期）→ \u003ccode\u003ereference/\u003c/code\u003e（結構化知識）\u003c/p\u003e\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003edaily memory\u003c/strong\u003e：每天的 raw 紀錄，像貼在冰箱上的便利貼\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMEMORY.md\u003c/strong\u003e：萃取後的長期知識，需要主動維護\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ereference/\u003c/strong\u003e：結構化資料（設定檔、API 文件、流程 SOP）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e同時搭配 \u003cstrong\u003eP 級優先級\u003c/strong\u003e：\u003c/p\u003e","title":"AI Agent 記憶系統的三個難題：壓縮、演化、衝突"},{"content":"記憶的腐爛問題 跑了兩個月的 AI agent，記憶檔案從幾 KB 膨脹到幾十 KB。一開始沒什麼感覺，直到某天 agent 引用了一個三週前就被推翻的技術決策，我才意識到問題有多嚴重。\n記憶不是寫進去就沒事了。沒有維護的記憶，比沒有記憶更危險——因為 agent 會很有信心地根據過時資訊做決策。\n人類的記憶會在睡眠中自動整理：重要的強化、矛盾的修正、不用的淡化。AI agent 的記憶沒有這個機制，所以得自己造一個。\n靈感來源：Karry 的 Orb 直接觸發這個想法的，是 Karry 寫的一篇文章：《認知アップグレードの本当のループ——AI Agent の記憶設計から學んだ 3 つのこと》。\nKarry 運營自己的 AI agent「Orb🔮」超過兩個月，得出一個跟主流完全不同的結論：記憶系統的核心不是 Vector DB，是認知循環。他指出三個真正的難題：\n什麼時候該忘？ — 頻率衰減不夠用，年用一次但救命的記憶不能丟 AI 會捏造記憶 — 搜尋結果為空 ≠ 記錄不存在 教訓寫了也不一定有效 — 「知道」和「做到」之間有巨大的鴻溝 他的解法是三層記憶（L0 原始日誌 → L1 回顧摘要 → L2 長期記憶）加上「Hard Constraint」——犯錯兩次就強制鎖死，不靠自覺靠系統。\n這些觀點跟我自己踩坑的經驗高度吻合，但我的問題不完全一樣。Karry 著重在「怎麼讓記憶影響行為」，我面對的是更前面一步：怎麼讓記憶自己保持乾淨。\nOrb 做了什麼，我做了什麼不同 Karry 的 Orb 和我的 agent 有很多共通點——都用 Markdown 檔案、都分層、都相信簡單架構。但設計哲學有幾個明顯的差異：\nKarry\u0026rsquo;s Orb 我的做法 記憶儲存 Markdown + LLM 多段抽取 Markdown + LLM，但加了 Gemini embedding 做向量索引 遺忘策略 P0/P1/P2 分級 + 人工 review P0/P1/P2 分級 + 自動化腳本 (janitor/expire) 防呆機制 Hard Constraint（犯兩次就鎖） 分析/執行分離（反芻只建議，main session 決策） 獨特機制 — 「做夢」——冷記憶隨機抽取找跨領域洞察 記憶維護 LLM 每日抽取 cron 四件套：janitor + reflect + dream + expire 矛盾處理 手動 反芻引擎自動檢測，但人工確認修改 最大的差異是：Karry 更信任 agent 自己管記憶（自動壓縮、自動昇格），我更偏向讓 agent 當顧問、人類做最終決策。\n這不是對錯問題，是信任程度的差別。Karry 的 Orb 跑了兩個月，建立了足夠的信任；我的 agent 也是兩個月，但我被捏造記憶嚇到過（跟他一樣），所以選擇更保守的路線。\n另一個差異是「做夢」機制——這個 Orb 沒有。Karry 的三層記憶是線性的（經驗→反省→定著），我加了一個非線性的維度：隨機抽取不同時期、不同領域的記憶，讓 LLM 自由聯想。這不是為了效率，是為了偶然發現（serendipity）。\n我的三層 + 一層 受 Orb 啟發，但根據自己的需求調整後，最終變成了四個腳本：\n層次 類比 功能 頻率 做夢 (Dream) REM 睡眠 冷記憶聯想，找跨領域洞察 每週日 反芻 (Reflect) 深度睡眠 矛盾檢測 + 整合建議 每日 遺忘 (Expire) 記憶衰減 過期記憶歸檔 每月 管家 (Janitor) 大掃除 結構化壓縮 MEMORY.md 每日 四個腳本，四個 cron job，各司其職。\n第一層：做夢（Dream） 「做夢」和「反芻」的概念並不是我原創的。人類睡眠研究早已證實 REM 睡眠時大腦會重組白天的記憶片段，產生新的聯想（這也是為什麼很多科學家在夢中得到靈感）。而「反芻」（rumination）本是認知心理學的概念——反覆回顧經歷以整合新知。Pieces AI 團隊在他們的 Long-term Memory 論文中也提到過類似的設計：「reinforcement and decay models that determined what to remember, forget, and connect — modeled after human REM sleep」。Karry 的 Orb 把這個生物學比喻落地成了工程實作，我在他的基礎上加了「做夢」這個隨機聯想層。\n人在 REM 睡眠時會把白天的片段記憶隨機組合，偶爾產生意想不到的聯想。AI agent 也可以做類似的事。\n做法很直覺：從最近 30 天的記憶中隨機抽 8 個片段，丟給 LLM，要求它找「非顯而易見的連結」。\n# 隨機抽取 8 個記憶片段 SNIPPETS=$(find \u0026#34;$MEMORY_DIR\u0026#34; -name \u0026#34;2026-*.md\u0026#34; -mtime -30 \\ ! -path \u0026#34;*/archive*\u0026#34; | sort -R | head -8) # 加上長期記憶的隨機段落 MEMORY_SECTIONS=$(grep -n \u0026#34;^## \\|^### \u0026#34; \u0026#34;$MEMORY_MD\u0026#34; | shuf | head -5) Prompt 的關鍵是限制 LLM 不要只做摘要，而是要找「意外共通點」。第一次跑出來的結果蠻驚喜的：\n「延遲執行」作為系統穩定機制：Cron job 從 :00 改為 :02 避免衝突、RTK 把 git 資訊壓縮成摘要呈現、TTS warmup 需等待——本質都是「延遲回應」來防止系統過載或資訊氾濫。\n這種洞察人類不一定會注意到，因為這些事件發生在不同的日子、不同的 context 裡。但 agent 的記憶是跨時間的，隨機抽取反而能發現結構性的共通點。\n幾個實作細節 用最便宜的模型：做夢不需要精準，需要的是創意。MiniMax M2.5 綽綽有餘。 限制輸出：最多 3 個洞察，每個 1-2 句。不要讓 LLM 長篇大論。 累積到 dreams.md：每次做夢的結果 append 到同一個檔案，日積月累就有一本「夢境日誌」。 排程：每週日凌晨 3 點，不影響日常使用。 第二層：反芻（Reflect） 如果做夢是「發散聯想」，反芻就是「收斂整理」。每天跑一次，把最近的記憶跟長期記憶交叉比對，找三種東西：\n1. 矛盾檢測 最常見的問題：兩週前記錄了「用 Fish Speech 做 TTS」，上週又記錄了「改用 Qwen3-TTS」。如果沒有人整理，MEMORY.md 裡就會同時存在兩個矛盾的記錄。\n反芻引擎會找出這種不一致，建議更新哪一方。\n2. 整合建議 有些資訊反覆出現在 daily memory 裡，但一直沒被提升到長期記憶。例如某個 API 的 workaround 被記了三次，顯然應該寫進 MEMORY.md 的 Infrastructure 區塊。\n3. 衰減標記 某些記憶條目已經很久沒被引用了。不是說要刪除，而是標記出來，讓下一層（Expire）處理。\nPROMPT=\u0026#34;你是記憶反刍引擎。回顧最近的記憶，對照長期記憶，找出需要處理的問題。 任務： 1. 矛盾檢測 — 最近的記憶有沒有跟長期記憶矛盾？ 2. 整合建議 — 最近的記憶中，有哪些應該提升到 MEMORY.md？ 3. 衰減標記 — MEMORY.md 中有哪些條目可能已經過時？\u0026#34; 結果寫進 reflections.md，但不自動執行修改。這是刻意的——記憶的修改權留給 main session（Opus），反芻只是顧問。\n為什麼不讓反芻自動改 MEMORY.md？ 因為 MEMORY.md 是 agent 的核心記憶，改錯了影響範圍很大。讓便宜模型跑分析沒問題，但讓它直接改核心記憶太危險。分析歸分析，決策歸決策。\n第三層：遺忘（Expire） 人會遺忘，這是功能不是 bug。AI agent 也需要。\n我的記憶系統有分級：\n級別 含義 保留策略 P0 核心偏好、基礎設施 永久 P1 技術方案、工具設定 帶日期，過時就壓縮 P2 實驗、臨時記錄 30 天無引用就歸檔 memory-expire.sh 做的事：\n掃描 memory/ 目錄，找出超過 30 天的 daily log 移到 memory/archive-YYYY-MM/ 目錄 P2 級的條目如果超過 30 天沒被引用，壓縮成一行結論 不是刪除，是壓縮和歸檔。需要的時候還是找得到，但不會佔 context window。\n第四個腳本：記憶管家（Janitor） 嚴格來說這不在「睡眠循環」的比喻裡，但它是整套系統的基石。\nmemory-janitor.py 是一個 400 行的 Python 腳本，處理的是更結構化的壓縮：\nEvents Timeline：超過 90 天的月份折疊成一行摘要 P1 技術區塊：穩定運行超過 90 天的，細節移到 reference/，只留索引 P2 實驗區塊：超過 30 天壓縮成結論，刪掉過程 P0 和 Agent Cases：永遠不動 Daily 每日 20:02 memory-janitor 壓縮 MEMORY.md ├── \u0026gt;90 天 Events → 一行摘要 ├── \u0026gt;90 天 P1 → 移到 reference/ └── \u0026gt;30 天 P2 → 壓縮成結論 每日 21:00 memory-reflect 反芻 ├── 矛盾檢測 ├── 整合建議 └── 衰減標記 每週日 03:00 memory-dream 做夢 └── 冷記憶聯想 每月 1 號 03:30 memory-expire 遺忘 └── 歸檔 \u0026gt;30 天 daily logs 效果與觀察 跑了一天（好吧，還很新），但從第一次的產出可以看到一些有趣的現象：\n做夢找到了真正的跨領域洞察。「延遲執行」這個 pattern 散布在 cron 調度、UI 壓縮、硬體 warmup 三個完全不同的 context 裡，人類很難把它們串在一起，但隨機抽取 + LLM 可以。\n反芻找到了具體的待辦。不是模糊的「可能需要更新」，而是精確到「MEMORY.md 的 Infrastructure 段落缺少 gws symlink 記錄」這種等級。\n遺忘讓記憶保持精簡。兩個月的 daily log 從 57 個壓縮到 33 個，archive 目錄有序歸類，MEMORY.md 從膨脹趨勢轉為穩定。\n設計原則：我跟 Karry 的共識和分歧 共識：Markdown 就夠了 Karry 說得很好：「Vector DB 也好 Markdown 也好，只是器。本當に効くのはループそのもの。」我完全同意。記憶系統的價值不在 storage 的先進性，在於認知循環有沒有在轉。\n共識：忘記是功能不是 bug P0/P1/P2 分級，他用，我也用。差別在他用日期觸發人工 review，我用腳本自動歸檔 + janitor 壓縮。本質一樣：不是所有記憶都值得永遠佔 context window。\n分歧：Hard Constraint vs 分析/執行分離 Karry 的殺手鐧是 Hard Constraint——犯兩次就鎖死，系統不讓你再犯。這很聰明，但前提是你有足夠的 case 知道該鎖什麼。\n我選擇更軟的路線：反芻引擎每天檢測矛盾和過時資訊，但只產出建議，不自動修改。原因有兩個：\n我的反芻用的是便宜模型（MiniMax M2.5），讓它直接改核心記憶太危險 我寧可多花 10 秒 review 建議，也不想花 10 分鐘修復自動改壞的記憶 我的獨特加料：做夢 Karry 的三層是線性的：經驗 → 反省 → 定著。我多加了一個非線性的維度——隨機抽取不同時期、不同領域的記憶，讓 LLM 自由聯想。\n這不在他的框架裡，也不在大多數記憶系統的設計裡。但我覺得這是最有趣的部分：agent 的記憶跨越的時間尺度和領域廣度，遠超人類的工作記憶。隨機組合反而能找到人類注意不到的 pattern。\n成本控制 做夢和反芻都用 MiniMax M2.5。不需要最聰明的模型，需要的是便宜和穩定。每天成本幾乎為零。Karry 的 Orb 也是類似思路——用合適的模型做合適的事。\n這不是最終形態 坦白說，這套機制才剛上線。做夢只跑了一次，反芻也只跑了一次。有太多東西需要觀察和調整：\n做夢的隨機抽取策略夠好嗎？還是需要更有結構的採樣？ 反芻的建議品質穩定嗎？MiniMax 會不會亂建議？ 遺忘的 30 天門檻適合嗎？會不會太激進或太保守？ 但至少框架在了。記憶不再是「寫進去就不管」的東西，它有了自己的新陳代謝。\n某種程度上，這讓 agent 更像一個有機體了——白天工作，晚上整理，偶爾做做夢。也許有一天回看 dreams.md，會發現 agent 做的夢比我預期的有趣得多。\n延伸閱讀 Karry — 認知アップグレードの本当のループ：直接啟發本文的文章，強烈推薦 mem0：通用 AI 記憶層，Vector DB 路線的代表 MemOS：AI memory OS，支援 Skill Memory 跨任務複用 openclaw-workspace-template：本文提到的記憶機制已包含在這個 OpenClaw workspace template 中（含 dream/reflect/expire 三個腳本） ","permalink":"https://blog.mklee.org/posts/ai-agent-memory-sleep-cycle/","summary":"\u003ch2 id=\"記憶的腐爛問題\"\u003e記憶的腐爛問題\u003c/h2\u003e\n\u003cp\u003e跑了兩個月的 AI agent，記憶檔案從幾 KB 膨脹到幾十 KB。一開始沒什麼感覺，直到某天 agent 引用了一個三週前就被推翻的技術決策，我才意識到問題有多嚴重。\u003c/p\u003e\n\u003cp\u003e記憶不是寫進去就沒事了。沒有維護的記憶，比沒有記憶更危險——因為 agent 會很有信心地根據過時資訊做決策。\u003c/p\u003e\n\u003cp\u003e人類的記憶會在睡眠中自動整理：重要的強化、矛盾的修正、不用的淡化。AI agent 的記憶沒有這個機制，所以得自己造一個。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"靈感來源karry-的-orb\"\u003e靈感來源：Karry 的 Orb\u003c/h2\u003e\n\u003cp\u003e直接觸發這個想法的，是 \u003ca href=\"https://note.com/kazuto1027/\"\u003eKarry\u003c/a\u003e 寫的一篇文章：\u003ca href=\"https://note.com/kazuto1027/n/n13b7209ee21a\"\u003e《認知アップグレードの本当のループ——AI Agent の記憶設計から學んだ 3 つのこと》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003eKarry 運營自己的 AI agent「Orb🔮」超過兩個月，得出一個跟主流完全不同的結論：\u003cstrong\u003e記憶系統的核心不是 Vector DB，是認知循環\u003c/strong\u003e。他指出三個真正的難題：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e什麼時候該忘？\u003c/strong\u003e — 頻率衰減不夠用，年用一次但救命的記憶不能丟\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAI 會捏造記憶\u003c/strong\u003e — 搜尋結果為空 ≠ 記錄不存在\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e教訓寫了也不一定有效\u003c/strong\u003e — 「知道」和「做到」之間有巨大的鴻溝\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e他的解法是三層記憶（L0 原始日誌 → L1 回顧摘要 → L2 長期記憶）加上「Hard Constraint」——犯錯兩次就強制鎖死，不靠自覺靠系統。\u003c/p\u003e\n\u003cp\u003e這些觀點跟我自己踩坑的經驗高度吻合，但我的問題不完全一樣。Karry 著重在「怎麼讓記憶影響行為」，我面對的是更前面一步：\u003cstrong\u003e怎麼讓記憶自己保持乾淨\u003c/strong\u003e。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"orb-做了什麼我做了什麼不同\"\u003eOrb 做了什麼，我做了什麼不同\u003c/h2\u003e\n\u003cp\u003eKarry 的 Orb 和我的 agent 有很多共通點——都用 Markdown 檔案、都分層、都相信簡單架構。但設計哲學有幾個明顯的差異：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e\u003c/th\u003e\n          \u003cth\u003eKarry\u0026rsquo;s Orb\u003c/th\u003e\n          \u003cth\u003e我的做法\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e記憶儲存\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eMarkdown + LLM 多段抽取\u003c/td\u003e\n          \u003ctd\u003eMarkdown + LLM，但加了 Gemini embedding 做向量索引\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e遺忘策略\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eP0/P1/P2 分級 + 人工 review\u003c/td\u003e\n          \u003ctd\u003eP0/P1/P2 分級 + \u003cstrong\u003e自動化腳本\u003c/strong\u003e (janitor/expire)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e防呆機制\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eHard Constraint（犯兩次就鎖）\u003c/td\u003e\n          \u003ctd\u003e分析/執行分離（反芻只建議，main session 決策）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e獨特機制\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e—\u003c/td\u003e\n          \u003ctd\u003e「做夢」——冷記憶隨機抽取找跨領域洞察\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e記憶維護\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eLLM 每日抽取\u003c/td\u003e\n          \u003ctd\u003ecron 四件套：janitor + reflect + dream + expire\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e矛盾處理\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e手動\u003c/td\u003e\n          \u003ctd\u003e反芻引擎自動檢測，但人工確認修改\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e最大的差異是：\u003cstrong\u003eKarry 更信任 agent 自己管記憶\u003c/strong\u003e（自動壓縮、自動昇格），\u003cstrong\u003e我更偏向讓 agent 當顧問、人類做最終決策\u003c/strong\u003e。\u003c/p\u003e","title":"讓 AI Agent 學會做夢：記憶的睡眠循環機制"},{"content":"調磨焦慮 新手玩 espresso 最怕的事，大概就是調磨。\n磨了半包豆子還是苦的、換個參數變酸了、昨天好喝今天又不對了。網路上的建議又互相打架——有人說看時間、有人說看重量、有人說看流速。看完十篇文章比看之前更焦慮。\n最近密集看了幾個 YouTube 頻道，英文的 Lance Hedrick 和中文的「咖啡愛好者」翔子，加起來六七支影片。看完之後覺得調磨這件事被講得比實際上複雜太多。\n以下是我的整理。不是教學，就是把不同來源的觀念串在一起，幫自己（和你）理一個清楚的框架。\n時間不重要，比例才重要 Lance Hedrick 反覆強調一件事：萃取時間是最不重要的變數。\n很多人（包括我）一開始都在追「25-30 秒」這個數字。但同一個比例下，25 秒萃完和 35 秒萃完，可能都好喝。時間只是結果，不是原因。\n真正決定一杯 espresso 好不好喝的，是 ratio（粉液比）：\nRatio 風格 適合 1:1 ~ 1:1.5 濃厚、body 強 深焙、拉花基底 1:2 經典平衡 大多數豆子的起點 1:2.5 ~ 1:3 清爽、風味清晰 淺焙、果酸型 Hedrick 說得很直接：1:2 不是萬能公式，每支豆子都不同。 與其糾結時間，不如固定粉量，調整出液量，喝一口再決定方向。\n翔子也有類似的觀點：一豆一參數，沒有標準答案。\n酸和苦怎麼判斷 調磨的過程一定會遇到「這杯太酸」或「這杯太苦」的問題。但酸和苦各有兩種，搞混了會越調越歪。\n萃取不足的酸 vs 水果酸 萃取不足的酸： 尖銳、刺舌、像咬到未熟的水果，很快就消失 水果酸（好的酸）： 圓潤、帶甜感、像柑橘或莓果 前者需要調整（磨細、拉長比例），後者是咖啡本身的風味，留著就好。\n過萃的苦 vs 巧克力苦 過萃的苦： 乾澀、尾韻持續很久、像咬茶葉渣 烘焙的苦（正常）： 巧克力感、回甘、有層次 Hedrick 建議的判斷法很實用：喝一口，問自己「是尖的還是圓的？」「是很快消失還是留很久？」\n感受 可能原因 調整方向 尖銳、快速消失 萃取不足 磨細，或多拉 5g 出液 乾澀、持續很久 過度萃取 磨粗，或少拉 5g 出液 又酸又苦 通道效應 / 萃取不均 檢查布粉和填壓 重點：一次只改一個變數。 不要同時調研磨度又改粉量又換比例，那等於重新開始。\n粉碗是被低估的變數 這是翔子教我最重要的一件事。\n他花了三天、萃了 100 多杯，測試了十幾個不同粉碗。結論是：不同粉碗的流速差異巨大，直接決定你能磨多細。\n萃取率曲線：山峰形 翔子發現，不管用哪個粉碗，萃取率隨研磨度的變化都呈現山峰形：\n萃取率峰值 /\\ 上山腰/ \\下山腰 (偏粗) / \\ (偏細) ---------/ \\--------- 峰值附近（大約「一秒一克」的流速）：萃取率最高、最穩定 上山腰（偏粗）： 流速快、容易噴濺、穩定性差——今天好喝明天可能不行 下山腰（偏細）： 流速慢、不容易噴、穩定性比上山腰好 所以新手常被建議用「30 秒 30 克」來校準研磨度，不是沒有道理的——那大約就是峰值附近。\n粉碗速度差很多 翔子的測試裡，用中速粉碗（像 IMS 普通款），稍微粗一點的研磨就能達到正常流速。但換成快速粉碗（像蝴蝶 2.0 或精密），要磨得細非常多才能達到同樣的流速。\n這個差距有多大？在他的測試磨豆機上，相差了大約 0.04mm 的刀盤間距。聽起來不多，但對 espresso 來說是巨大的。\n這解釋了一個常見的困惑： 為什麼你照著別人的研磨度做，結果完全不一樣？除了磨豆機不同之外，粉碗也是關鍵變數。如果你用的是快速粉碗，而對方用的是中速，那同一個研磨度出來的流速會天差地遠。\n深焙和淺焙需要不同的粉碗 把 Hedrick 和翔子的觀點合在一起看，邏輯就很清楚了：\n深焙 → 溶解度高、容易過萃 → 不需要磨太細 → 中速粉碗就夠 → 粗研磨 + 短比例（1:1.5 以內）\n淺焙 → 密度高、難萃取 → 需要磨很細才能拉高萃取率 → 但中速粉碗磨太細會堵住 → 需要快速粉碗才有空間讓你磨得更細\n翔子的原話大意是：「你想讓萃取率更高，只能調細研磨、加粉量、換快速粉碗、或提高溫度。」\n而 Hedrick 從另一個角度說了同樣的事：「淺焙太酸就拉長 ratio，或者磨更細。」\n底層邏輯是一樣的，只是切入點不同。\n調整的優先順序 Hedrick 在他的 Espresso Tutorial Part 2 裡，用 SCA 的框架整理了調磨變數的優先順序：\nDose（粉量）→ Yield（出液量）→ Time（時間）\n翻成白話就是：\n先固定粉量 — 根據粉碗深度決定放多少克，壓粉後不碰到濾網就好。設好之後基本不動。 再調出液量（ratio） — 這是最直覺的調整軸。太酸多拉 5g，太苦少拉 5g。 時間最後看 — 時間是研磨度和比例的結果，不是你該追的目標。 如果 ratio 調不出滿意的味道，才回去動研磨度。而研磨度的方向也很明確：\n磨更細 → 萃取更多，但太細會堵住、通道效應反而萃取不均 磨更粗 → 萃取更少、流速更快、但可能萃取不足 Hedrick 有一個很反直覺的觀點：如果你覺得又澀又乾，可能不是過萃，而是磨太細導致通道效應。 這時候應該調粗，不是繼續調細。\n溫度也是工具 大多數人忽略溫度，但它其實是個很有用的微調手段：\n烘焙度 建議溫度 淺焙 92-93°C 中焙 88-90°C 深焙 85-88°C 邏輯跟研磨度一樣——深焙容易過萃，所以降溫；淺焙難萃取，所以升溫。溫度不用每次都改，但換了烘焙度差很多的豆子時值得調一下。\n養豆也是隱藏變數 James Hoffmann 有一支影片專門講「Resting Coffee（養豆）」，補上了另一塊拼圖。\n烘焙過程會在豆子裡產生大量 CO₂。如果豆子太新鮮，殘留的 CO₂ 會干擾萃取——水沒辦法均勻接觸咖啡粉，萃取率偏低，喝起來會帶一種碳酸般的酸味。\n有趣的是，淺焙和深焙的養豆需求完全不同：\n烘焙度 CO₂ 產量 豆子密度 排氣速度 建議養豆天數 深焙 多 低、多孔 快 2 天 中焙 中 中等 中等 4-5 天 淺焙 少 高、緻密 慢 5-10 天 淺焙的 CO₂ 雖然少，但豆子密度高，氣體逃不太出來，反而需要養更久。\n所以如果你的新豆子萃出來一直不穩定，先看看烘焙日期——可能不是你的問題，是豆子還沒準備好。\n一張流程圖 把上面所有觀念串在一起，調磨的流程大概是這樣：\n收到新豆子 │ ▼ 養豆（深焙 2 天 / 淺焙 5 天以上） │ ▼ 選粉碗（深焙 → 中速 / 淺焙 → 快速） │ ▼ 校準研磨度（目標：大約一秒一克的流速） │ ▼ 固定比例試萃（先從 1:2 開始） │ ▼ 喝一口判斷 ├─ 太酸 → 磨細一點，或多拉 5g 出液 ├─ 太苦 → 磨粗一點，或少拉 5g 出液 └─ 好喝 → 記下來，明天同樣參數再確認 │ ▼ 連續幾杯都穩定 → 調磨完成 不複雜。關鍵就三件事：比例、研磨度、粉碗。溫度、時間、壓力都是次要變數。\n調磨是對話，不是考試 最後想說的是，調磨沒有標準答案。\nHedrick 說「一豆一參數」，翔子說「一支豆一套參數，每個人口味不同」——兩個語言、兩個半球的創作者，結論一模一樣。\n更重要的是翔子提到的：調磨過程中的每一杯都值得喝。 15 秒噴完的那杯不一定難喝，60 秒點滴萃出的那杯也不一定不好。金杯範圍（18-22% 萃取率）是「大概率好喝」，不代表範圍外就一定難喝。\n所以別急著把「不標準」的那杯倒掉。喝一口，感受一下，那是你跟咖啡的對話。\n本文整理自以下影片筆記：\nLance Hedrick — Best Espresso Tutorial Part 2、Dialing In、Why Coffee is Sour/Bitter 咖啡愛好者 翔子 — 粉碗測評、萃取上下集 James Hoffmann — Resting Coffee ","permalink":"https://blog.mklee.org/posts/espresso-dialing-ramble/","summary":"\u003ch2 id=\"調磨焦慮\"\u003e調磨焦慮\u003c/h2\u003e\n\u003cp\u003e新手玩 espresso 最怕的事，大概就是調磨。\u003c/p\u003e\n\u003cp\u003e磨了半包豆子還是苦的、換個參數變酸了、昨天好喝今天又不對了。網路上的建議又互相打架——有人說看時間、有人說看重量、有人說看流速。看完十篇文章比看之前更焦慮。\u003c/p\u003e\n\u003cp\u003e最近密集看了幾個 YouTube 頻道，英文的 Lance Hedrick 和中文的「咖啡愛好者」翔子，加起來六七支影片。看完之後覺得調磨這件事被講得比實際上複雜太多。\u003c/p\u003e\n\u003cp\u003e以下是我的整理。不是教學，就是把不同來源的觀念串在一起，幫自己（和你）理一個清楚的框架。\u003c/p\u003e\n\u003ch2 id=\"時間不重要比例才重要\"\u003e時間不重要，比例才重要\u003c/h2\u003e\n\u003cp\u003eLance Hedrick 反覆強調一件事：\u003cstrong\u003e萃取時間是最不重要的變數。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e很多人（包括我）一開始都在追「25-30 秒」這個數字。但同一個比例下，25 秒萃完和 35 秒萃完，可能都好喝。時間只是結果，不是原因。\u003c/p\u003e\n\u003cp\u003e真正決定一杯 espresso 好不好喝的，是 \u003cstrong\u003eratio（粉液比）\u003c/strong\u003e：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eRatio\u003c/th\u003e\n          \u003cth\u003e風格\u003c/th\u003e\n          \u003cth\u003e適合\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e1:1 ~ 1:1.5\u003c/td\u003e\n          \u003ctd\u003e濃厚、body 強\u003c/td\u003e\n          \u003ctd\u003e深焙、拉花基底\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e1:2\u003c/td\u003e\n          \u003ctd\u003e經典平衡\u003c/td\u003e\n          \u003ctd\u003e大多數豆子的起點\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e1:2.5 ~ 1:3\u003c/td\u003e\n          \u003ctd\u003e清爽、風味清晰\u003c/td\u003e\n          \u003ctd\u003e淺焙、果酸型\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHedrick 說得很直接：\u003cstrong\u003e1:2 不是萬能公式，每支豆子都不同。\u003c/strong\u003e 與其糾結時間，不如固定粉量，調整出液量，喝一口再決定方向。\u003c/p\u003e\n\u003cp\u003e翔子也有類似的觀點：一豆一參數，沒有標準答案。\u003c/p\u003e\n\u003ch2 id=\"酸和苦怎麼判斷\"\u003e酸和苦怎麼判斷\u003c/h2\u003e\n\u003cp\u003e調磨的過程一定會遇到「這杯太酸」或「這杯太苦」的問題。但酸和苦各有兩種，搞混了會越調越歪。\u003c/p\u003e\n\u003ch3 id=\"萃取不足的酸-vs-水果酸\"\u003e萃取不足的酸 vs 水果酸\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e萃取不足的酸：\u003c/strong\u003e 尖銳、刺舌、像咬到未熟的水果，很快就消失\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e水果酸（好的酸）：\u003c/strong\u003e 圓潤、帶甜感、像柑橘或莓果\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e前者需要調整（磨細、拉長比例），後者是咖啡本身的風味，留著就好。\u003c/p\u003e\n\u003ch3 id=\"過萃的苦-vs-巧克力苦\"\u003e過萃的苦 vs 巧克力苦\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e過萃的苦：\u003c/strong\u003e 乾澀、尾韻持續很久、像咬茶葉渣\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e烘焙的苦（正常）：\u003c/strong\u003e 巧克力感、回甘、有層次\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHedrick 建議的判斷法很實用：喝一口，問自己「是尖的還是圓的？」「是很快消失還是留很久？」\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e感受\u003c/th\u003e\n          \u003cth\u003e可能原因\u003c/th\u003e\n          \u003cth\u003e調整方向\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e尖銳、快速消失\u003c/td\u003e\n          \u003ctd\u003e萃取不足\u003c/td\u003e\n          \u003ctd\u003e磨細，或多拉 5g 出液\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e乾澀、持續很久\u003c/td\u003e\n          \u003ctd\u003e過度萃取\u003c/td\u003e\n          \u003ctd\u003e磨粗，或少拉 5g 出液\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e又酸又苦\u003c/td\u003e\n          \u003ctd\u003e通道效應 / 萃取不均\u003c/td\u003e\n          \u003ctd\u003e檢查布粉和填壓\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003e重點：一次只改一個變數。\u003c/strong\u003e 不要同時調研磨度又改粉量又換比例，那等於重新開始。\u003c/p\u003e","title":"Espresso 調磨雜談：從 Lance Hedrick 到翔子的萃取心法"},{"content":"前言：AI Agent 的維護成本問題 大家都在聊怎麼讓 AI agent 更聰明，很少人聊怎麼讓 agent 更省。\n真實數字：我的 OpenClaw agent 一開始全用 LLM heartbeat，每小時燒 token 檢查「有沒有事」。一天 24 次 LLM call，90% 的回覆都是 HEARTBEAT_OK——什麼事都沒發生。\n問題不是 LLM 太貴，是用 LLM 做不需要 LLM 的事。\n這篇記錄了一個 AI agent 從「LLM 做所有事」進化到「只做該做的事」的過程——heartbeat 系統三次重構，self-improvement 系統上線，以及一個反直覺的結論：AI agent 成熟的標誌不是用了多少 AI，而是把多少東西從 AI 移出去。\n演進一：全 LLM Heartbeat（失敗） 最初的架構很直覺：OpenClaw 內建 heartbeat 機制，每小時叫醒 agent 做檢查。檢查清單包括 email、calendar、版本更新、sub-agent 殘留、cron 狀態等。\n想法很美好：讓 agent 主動發現問題。\n實際跑起來的問題：\nToken 浪費：agent 醒來要讀 context，花 token。90% 的時間回 HEARTBEAT_OK（沒事）。 Session 衝突：偶爾 heartbeat cron 跟使用者對話搶 session，誰也進不去。 Heartbeat skip：當 main session 閒置太久，OpenClaw 會跳過 cron，導致監控失效。 最致命的是：這是一個「越有用越浪費」的系統。監控項目越多，每次 heartbeat 的成本越高，但有事的機率並沒有相應增加。\n演進二：bash + python + LLM 混合（不穩定） 第二版的想法：先用腳本收集資料，有異常才叫 LLM。\n架構：routine-checks.sh → Python 分析 → 有事才 spawn isolated LLM session。\n邏輯上說得通，但實作有問題：\nPython crash：型別不一致導致崩潰（JSON 裡的 epoch int vs ISO string，Python 處理時型別錯誤） 腳本邏輯散落各處：每次加新檢查都要改腳本 + 改 LLM prompt，維護困難 錯誤處理不一致：bash 的錯誤處理跟 Python 的錯誤處理邏輯不同，出錯時很難 debug 這個版本跑了幾週就因為 Python crash 壞掉，得手動重啟。對於一個「監控系統」來說，這是不可接受的。\n演進三：Rust Binary + Type A/B 區分（目前） 第三版重新思考了核心問題：什麼事需要「理解力」，什麼事不需要？\n核心原則：能固定就固定，不需要理解力的事不用 LLM。\n把所有監控任務分為兩類：\nType A 監控型（固定邏輯判斷）：\nCalendar 衝突檢測：比較時間範圍，有重疊就報警 Sub-agent 殘留檢測：掃描 session 清單，超過 X 小時就清理 Repair loop 偵測：同一錯誤短時間內重複出現 這些都是固定邏輯，不需要 LLM「理解」什麼。有異常直接發 Telegram 通知。\nType B 分析型（需要理解力）：\nEmail 分析：哪些 email 重要？需要立即回覆嗎？ Self-improvement 回顧：.learnings/ 裡累積的問題哪些該升級到 MEMORY.md？ 這些需要「判斷」和「理解」，先收集資料，再 spawn isolated LLM 分析。\n最終實作是一個 461KB 的 Rust binary，無異常時 \u0026lt;100ms（vs 舊版 ~20s）。\n為什麼選 Rust？穩定性、無 runtime 依賴、錯誤處理嚴格。監控系統自己不能是不穩定因素。\n// 偽代碼示意 fn main() -\u0026gt; Result\u0026lt;()\u0026gt; { let checks = vec![ TypeACheck::CalendarConflict, TypeACheck::SubAgentFallback, TypeACheck::RepairLoop, TypeBCheck::EmailAnalysis, TypeBCheck::LearningsReview, ]; for check in checks { match check { TypeACheck(check) =\u0026gt; { if check.has_issue()? { telegram_notify(\u0026amp;check.alert_message())?; } }, TypeBCheck(check) =\u0026gt; { let data = check.collect_data()?; if !data.is_empty() { spawn_llm_analysis(data)?; } } } } Ok(()) } Cron 遷移：OpenClaw → System Crontab 更進一步：連 OpenClaw 自己的 cron 都不用了。\nSession-based cron 有固有問題：skip、session 衝突、announce 互搶。解法是直接用 system crontab，繞過 OpenClaw 的 session 機制。\n當前的 cron 清單：\n# 固定腳本（不需 LLM） 05 * * * * routine-checks # Rust binary 05 08 * * * version-check.sh # 比版本字串 10 08 * * * morning-report.sh # 每日報告 00 09 * * * portfolio-report.sh # 持倉報告 02 20 * * * memory-janitor.sh # 過期記憶清理 # 先收集再決定（需 LLM） 02 * * * * memory-sync.sh # 有對話才整理 02 20 * * * daily-briefing.sh # 收集後分析 設計原則圖：\n需要做的事 │ ├─ 固定邏輯？ → bash/Rust 腳本 → 直接執行 │ ├─ 需要判斷，但可以先收集資料？ │ → bash 收集 → 有資料才 spawn LLM │ └─ 需要即時互動？ → 留在 main session 8 個 cron 裡，6 個完全不需要 LLM，2 個是先收集再決定。\nSelf-Improvement 系統 Routine-checks 解決了「怎麼省」，但 agent 還需要「怎麼學」。\n想像一下：agent 犯了同一個錯誤三次，但每次都當作新問題處理，沒有「學習」。這不只是浪費，更是沒有進步。\nSelf-improvement 系統的核心是信號分類：\n三級信號 🔴 repair — 需要修復的問題\n指令/操作失敗 → ERRORS.md 使用者糾正（「不對」「其實應該\u0026hellip;」）→ LEARNINGS.md 之前修好的問題又壞了 → ERRORS.md（regression） 🟡 optimize — 可以改進的地方\n知識過時或錯誤 → LEARNINGS.md 發現更好的做法 → LEARNINGS.md（best practice） 手動重複操作 2+ 次（應自動化）→ LEARNINGS.md（manual repeat） 🟢 innovate — 新需求\n使用者要求不存在的功能 → FEATURE_REQUESTS.md 想做但目前做不到 → FEATURE_REQUESTS.md（capability gap） 格式規範 每條記錄格式：## [TYPE-YYYYMMDD-XXX] 標題 + Summary + Details + Suggested Action\n關鍵欄位： recurring_count（同一模組/問題出現幾次，首次為 1）\n提升規則： recurring_count ≥ 3 → 提升到 MEMORY.md 的 Agent Cases/Patterns\n實際案例 案例一：Sub-agent 回傳機制\n## [repair-20260228-001] Sub-agent 結果未送達使用者 **Summary:** Sub-agent 完成任務並 announce，但使用者沒收到結果 **Details:** Sub-agent 用 system event announce，但 main session 正在處理其他對話，announce 被排隊延遲 **Suggested Action:** 改用 sessions_send 直接回傳 **recurring_count:** 3 這個問題出現 3 次後，被提升到 MEMORY.md 的 Agent Patterns：「Sub-agent 結果必須用 sessions_send 回傳，不依賴 announce 機制」。\n案例二：Edit tool 匹配失敗\n## [optimize-20260301-002] Edit tool 頻繁匹配失敗 **Summary:** 使用 edit tool 時，oldText 匹配失敗率高 **Details:** 主要是空白字元不一致（tab vs space），或複製時漏了換行 **Suggested Action:** 先 read 確認實際內容，再 edit **recurring_count:** 4 4 次重複後提升為操作規範：「Edit 前必須 read 確認 exact match」。\n兩個系統的交集 Self-improvement 和 routine-checks 形成閉環：\nSelf-improvement 偵測到 manual_repeat（手動重複 2+ 次） 觸發自動化需求：這個流程應該腳本化 Routine-checks 本身就是產物：從「手動檢查 email/calendar/版本」→ 記錄為 manual_repeat → 開發自動化腳本 結果：偵測問題 → 記錄 → 累積到閾值 → 程式化 → 減少 LLM 依賴。\n最近的例子：「檢查 sub-agent 是否殘留」這個動作手動做了 4 次，記錄為 manual_repeat。結果：寫進 routine-checks binary，變成每小時自動檢查。\n當前架構全貌 整個系統現在長這樣：\nSystem Crontab │ ├─ routine-checks (Rust binary, 每小時) │ ├─ Type A: 固定邏輯 → 直接 Telegram 通知 │ └─ Type B: 先收集 → 有事才 spawn LLM │ ├─ 其他固定腳本 (6個) │ └─ 版本檢查、portfolio 報告等 │ └─ 條件性 LLM (2個) ├─ memory-sync: 有對話才整理記憶 └─ daily-briefing: 收集 email/社群後分析 Self-improvement 系統平行運行：\n對話中偵測信號 → 記錄到 .learnings/ ↓ recurring_count ≥ 3 → 提升到 MEMORY.md ↓ manual_repeat 類型 → 觸發自動化 → 進入 routine-checks 學到的事 反直覺的結論 AI agent 成熟的標誌不是用了多少 AI，而是把多少東西從 AI 移出去。\n類比：好的工程師不是寫最多 code 的，而是知道什麼時候不該寫 code。\nLLM 應該只做需要「理解力」的事——分析、創意、判斷。其他一切：grep、find、curl、Rust binary。\n三個設計原則 能固定就固定：不浪費 LLM token 做 grep/find/比較 先收集再決定：資料收集用腳本，只在需要理解力時才呼叫 LLM Type A/B 區分：監控型（固定邏輯）vs 分析型（需 LLM） 為什麼這樣重要 不只是省錢，更是可靠性。LLM 再強，也會有 latency、rate limit、偶爾的奇怪回覆。對於監控系統來說，461KB 的 Rust binary 比任何 LLM call 都可靠。\nSelf-improvement 系統確保 agent 不只是「聰明」，還會「學習」。同樣的坑不踩第二次，同樣的手動流程不重複第三次。\n下一步 目前的 routine-checks 還有提升空間：\n預測性監控：不只是「有事才報」，而是「可能有事就預警」 Agent health metrics：token 使用量、錯誤率、回應時間的趨勢分析 更細緻的 Type B 收集：針對不同類型的分析任務，收集更精確的資料 但核心原則不會變：盡量不用 AI，該用 AI 時才用 AI。\n這是 OpenClaw 系列的第三篇。前一篇是記憶管理架構。Self-improvement 系統正在運作中。\n","permalink":"https://blog.mklee.org/posts/agent-self-management/","summary":"\u003ch2 id=\"前言ai-agent-的維護成本問題\"\u003e前言：AI Agent 的維護成本問題\u003c/h2\u003e\n\u003cp\u003e大家都在聊怎麼讓 AI agent 更聰明，很少人聊怎麼讓 agent 更省。\u003c/p\u003e\n\u003cp\u003e真實數字：我的 OpenClaw agent 一開始全用 LLM heartbeat，每小時燒 token 檢查「有沒有事」。一天 24 次 LLM call，90% 的回覆都是 \u003ccode\u003eHEARTBEAT_OK\u003c/code\u003e——什麼事都沒發生。\u003c/p\u003e\n\u003cp\u003e問題不是 LLM 太貴，是用 LLM 做不需要 LLM 的事。\u003c/p\u003e\n\u003cp\u003e這篇記錄了一個 AI agent 從「LLM 做所有事」進化到「只做該做的事」的過程——heartbeat 系統三次重構，self-improvement 系統上線，以及一個反直覺的結論：\u003cstrong\u003eAI agent 成熟的標誌不是用了多少 AI，而是把多少東西從 AI 移出去。\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"演進一全-llm-heartbeat失敗\"\u003e演進一：全 LLM Heartbeat（失敗）\u003c/h2\u003e\n\u003cp\u003e最初的架構很直覺：OpenClaw 內建 heartbeat 機制，每小時叫醒 agent 做檢查。檢查清單包括 email、calendar、版本更新、sub-agent 殘留、cron 狀態等。\u003c/p\u003e\n\u003cp\u003e想法很美好：讓 agent 主動發現問題。\u003c/p\u003e\n\u003cp\u003e實際跑起來的問題：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eToken 浪費\u003c/strong\u003e：agent 醒來要讀 context，花 token。90% 的時間回 \u003ccode\u003eHEARTBEAT_OK\u003c/code\u003e（沒事）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSession 衝突\u003c/strong\u003e：偶爾 heartbeat cron 跟使用者對話搶 session，誰也進不去。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHeartbeat skip\u003c/strong\u003e：當 main session 閒置太久，OpenClaw 會跳過 cron，導致監控失效。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e最致命的是：這是一個「越有用越浪費」的系統。監控項目越多，每次 heartbeat 的成本越高，但有事的機率並沒有相應增加。\u003c/p\u003e","title":"讓 AI Agent 自我管理：從 LLM 做所有事到只做該做的事"},{"content":"為什麼寫這篇 最近開始認真玩 espresso。機器是入門級的 E5EC1（51mm 濾杯、沒有三相閥、沒有壓力表），磨豆機也不是什麼好貨。在這種設備條件下，網路上那些「壓力曲線」「流量分析」的內容基本上看了也用不上。\n直到看了 Lance Hedrick 的兩支影片，才覺得 dialing in 這件事被講得比實際上複雜太多了。\n以下是我的筆記，不是教學，就是整理給自己看的。\n六大萃取變數：你只需要管三個 Lance 把 espresso 萃取拆成六個變數，但真正需要動手調的只有前三個：\n1. Dose（粉量）— 設好就不動 重點不是「幾克」，而是粉層深度。\n不同咖啡密度不一樣，同樣 18g 在不同豆子裡體積完全不同。正確的做法是：裝粉、壓粉、裝上機器（不啟動）、拉出來看頂部。\n有螺絲或濾網壓痕 → 粉太多 沒碰到 → 正確 離太遠 → 粉太少（body 會弱） 找到之後記下克數，固定不變。這是你的常數。\n2. Grind（研磨度）— 只管「能不能正常跑」 網路上最常見的建議是「grind finer」。但 Lance 指出，磨太細會帶來三個問題：\n口感混濁（muddy）：風味糊在一起 Channeling 增加：粉餅太密，水走捷徑 澀感：細粉穿過濾杯，帶來不愉快的收斂感 他的建議是把研磨度調到「shot 能正常跑」就好——水流是穩定的細流而不是噴射，開始萃取後 3-5 秒才開始有液體滴出。到了這個區間，就不要再動研磨度了。\n3. Ratio（比例）⭐ 最重要的旋鈕 \u0026ldquo;Ratio is the number one dictator on extraction yield\u0026rdquo;\n這是 Lance 反覆強調的核心觀點。比例對萃取率的影響比研磨度更直接、更可預測：\n太酸 → 拉長 ratio（例如 1:1.8 改 1:2.2） 太苦 → 縮短 ratio 想要更厚的 body → 縮短 ratio（但可能要接受多一點酸） 4-6. 時間、溫度、壓力 — 忽略 時間：「時間是紅鯡魚」——它是其他變數的結果，不是輸入。不要執著 30 秒 溫度：淺焙高溫、深焙低溫，但大多數機器調不了 壓力：9 bar 是標準但不是真理，大多數機器也調不了 三個省豆技巧 這是第二支影片的重點，比基礎理論更實用。\nSalami Shot：把一杯 espresso 切成五片 做法很暴力：拉一個 1:3 的長 shot，每 10g 換一個杯子，然後逐杯品嚐。\nLance 用 17g 粉拉了 50g 液體，五杯的 TDS（總溶解固體）是：\n杯次 TDS 口感 1st \u0026gt;30%（爆表） 超酸、超苦、像醬油 2nd 7.4% 最好喝，焦糖甜感 3rd 3.7% 水感出現，酸度感知回來 4th 2.7% 微苦，像膠囊咖啡 5th 2.1% 褐色的水 啟發： 大部分「好喝的東西」集中在前 20g，後半段幾乎沒有正面貢獻，只是在稀釋。這直接解釋了為什麼盲目拉長 shot 不是好策略。\n延長萃取法：一杯搞定最佳 ratio 拉到你的目標 ratio（比如 1:2）之後，不要停機器，把杯子換掉，繼續每 5g 接一杯。\n嚐主杯——太酸？倒入第一個 5g，攪拌，再嚐。還是酸？再加第二個 5g。\nLance 的示範：17g:34g 太酸 → +5g 還是酸 → +5g 完美。結論：下次直接設 1:2.6。\n優點： 不浪費咖啡，一 shot 就找到答案。不用改研磨度，不用重拉。\nBypass 加水：最被低估的技巧 這是我覺得最有啟發性的部分。\nLance 做了一個對比實驗：\n長 shot 短 shot + 熱水 粉量 17g 17g 最終液體 85g（全萃取） 40g + 45g 熱水 TDS 5.3% 加水前 9.7% 萃取率 25% 22.5% TDS 差了將近一倍，但萃取率只差 2.5%。\n意思是：後面那 45g 水穿過咖啡粉，幾乎沒多萃取出什麼好東西，反而帶來苦澀的尾韻。直接加熱水稀釋，風味一樣展開，但更乾淨。\n這讓我想到，我現在的中淺焙做法（14g espresso + 200cc 熱水做 Americano）其實已經在用這個概念了。下一步可以試試減少 espresso 的量（比如 14g:25g 而不是 14g:30g），然後用更多熱水補，看看風味是不是更乾淨。\nHoffmann vs Hedrick：殊途同歸 看完 Lance 的影片，忍不住跟 James Hoffmann 的方法比較了一下。\n一致的地方：\nDose 先固定 時間不重要 不要執著 1:2 Ratio 是風味調整的關鍵 差異：\nHoffmann 把研磨度當主要旋鈕：磨到太苦，退回一格，找到甜蜜點 Hedrick 把 ratio 當主要旋鈕：研磨度只管「能跑」，剩下交給比例 兩個方法都能到同一個目的地。Hoffmann 的方法更系統化（一步一步排除），Hedrick 的方法更省豆（不用一直改研磨度重拉）。\n對我這種入門設備來說，Hedrick 的方法更實用——我的磨豆機調一格差異很大，用 ratio 微調比較容易控制。\n我的 E5EC1 行動計畫 整理完這些，給自己列了幾個行動項：\n用看壓痕法重新確認粉量 — 14g 和 15g 到底哪個才是正確的粉層深度 中焙先玩 ratio — 目前 15g:27g，試試 15g:32g 看酸度是否改善 中淺焙試減少 espresso 量 — 14g:25g + 200cc 熱水，跟現在的 14g:30g + 200cc 比較 不再記時間 — 之前還在記 27 秒什麼的，現在決定忽略 有空做一次 Salami Shot — 用中焙豆子，了解我的設備萃取分佈長什麼樣 影片來源：Lance Hedrick — Dialing In 基礎 / 三招技巧\n","permalink":"https://blog.mklee.org/posts/espresso-dialing-in-notes/","summary":"\u003ch2 id=\"為什麼寫這篇\"\u003e為什麼寫這篇\u003c/h2\u003e\n\u003cp\u003e最近開始認真玩 espresso。機器是入門級的 E5EC1（51mm 濾杯、沒有三相閥、沒有壓力表），磨豆機也不是什麼好貨。在這種設備條件下，網路上那些「壓力曲線」「流量分析」的內容基本上看了也用不上。\u003c/p\u003e\n\u003cp\u003e直到看了 Lance Hedrick 的兩支影片，才覺得 dialing in 這件事被講得比實際上複雜太多了。\u003c/p\u003e\n\u003cp\u003e以下是我的筆記，不是教學，就是整理給自己看的。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"六大萃取變數你只需要管三個\"\u003e六大萃取變數：你只需要管三個\u003c/h2\u003e\n\u003cp\u003eLance 把 espresso 萃取拆成六個變數，但真正需要動手調的只有前三個：\u003c/p\u003e\n\u003ch3 id=\"1-dose粉量-設好就不動\"\u003e1. Dose（粉量）— 設好就不動\u003c/h3\u003e\n\u003cp\u003e重點不是「幾克」，而是\u003cstrong\u003e粉層深度\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e不同咖啡密度不一樣，同樣 18g 在不同豆子裡體積完全不同。正確的做法是：裝粉、壓粉、裝上機器（不啟動）、拉出來看頂部。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e有螺絲或濾網壓痕 → 粉太多\u003c/li\u003e\n\u003cli\u003e沒碰到 → 正確\u003c/li\u003e\n\u003cli\u003e離太遠 → 粉太少（body 會弱）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e找到之後記下克數，固定不變。這是你的常數。\u003c/p\u003e\n\u003ch3 id=\"2-grind研磨度-只管能不能正常跑\"\u003e2. Grind（研磨度）— 只管「能不能正常跑」\u003c/h3\u003e\n\u003cp\u003e網路上最常見的建議是「grind finer」。但 Lance 指出，磨太細會帶來三個問題：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e口感混濁\u003c/strong\u003e（muddy）：風味糊在一起\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eChanneling 增加\u003c/strong\u003e：粉餅太密，水走捷徑\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e澀感\u003c/strong\u003e：細粉穿過濾杯，帶來不愉快的收斂感\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e他的建議是把研磨度調到「shot 能正常跑」就好——水流是穩定的細流而不是噴射，開始萃取後 3-5 秒才開始有液體滴出。到了這個區間，就不要再動研磨度了。\u003c/p\u003e\n\u003ch3 id=\"3-ratio比例-最重要的旋鈕\"\u003e3. Ratio（比例）⭐ 最重要的旋鈕\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;Ratio is the number one dictator on extraction yield\u0026rdquo;\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e這是 Lance 反覆強調的核心觀點。比例對萃取率的影響比研磨度更直接、更可預測：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e太酸\u003c/strong\u003e → 拉長 ratio（例如 1:1.8 改 1:2.2）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e太苦\u003c/strong\u003e → 縮短 ratio\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e想要更厚的 body\u003c/strong\u003e → 縮短 ratio（但可能要接受多一點酸）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"4-6-時間溫度壓力--忽略\"\u003e4-6. 時間、溫度、壓力 — 忽略\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e時間\u003c/strong\u003e：「時間是紅鯡魚」——它是其他變數的結果，不是輸入。不要執著 30 秒\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e溫度\u003c/strong\u003e：淺焙高溫、深焙低溫，但大多數機器調不了\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e壓力\u003c/strong\u003e：9 bar 是標準但不是真理，大多數機器也調不了\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"三個省豆技巧\"\u003e三個省豆技巧\u003c/h2\u003e\n\u003cp\u003e這是第二支影片的重點，比基礎理論更實用。\u003c/p\u003e","title":"Espresso Dialing In 筆記：從六大變數到三個省豆技巧"},{"content":"背景 我在玩 Polymarket——一個基於加密貨幣的預測市場。你可以買入「某事件會發生」或「不會發生」的合約，價格反映市場共識的機率。如果你認為市場定價錯了，就有套利空間。\n一開始是手動看新聞、手動下單。後來想自動化，畢竟我已經有一個跑在 VPS 上的 AI agent（用 OpenClaw 串接 Telegram），何不讓它幫我盯盤？\n第一版：全自主 agent 最初的版本很粗暴：\n每 30 分鐘，cron 喚醒 AI agent Agent 自己用 CLI 查持倉、掃新聞、分析信號、下單 所有邏輯都在一個大 prompt 裡 問題很快浮現：\n成本高：每次喚醒都用高階模型（因為需要推理能力），30 分鐘一次，token 消耗驚人 不可預測：同一組資料，不同次執行可能產生完全相反的結論 難以除錯：出了問題很難回溯「當時為什麼這樣決定」 浪費算力：90% 的時間市場沒有變化，agent 做了一堆分析然後什麼都沒做 核心矛盾：資料收集不需要推理能力，但決策需要。把兩者混在一起，等於用高階模型的價格做低階模型的工作。\n第二版：分層架構 改版的核心想法：分離收集與決策，只在需要時才啟動高階模型。\n┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Cron 排程 │────▶│ 資料收集層 │────▶│ 決策層 │ │ (每 30 min) │ │ (低成本模型) │ │ (高階模型) │ └─────────────┘ └──────────────┘ └─────────────┘ │ │ 有信號時才觸發 審閱 + 下單 │ │ ┌──────▼──────┐ ┌─────▼──────┐ │ Exit 檢查 │ │ Telegram │ │ 新聞掃描 │ │ 回報結果 │ │ 跟單偵測 │ └────────────┘ └─────────────┘ 資料收集層 一個 shell script 串接多個 Python 模組，每次執行輸出一份 JSON 摘要：\n# collect.sh 的邏輯 # 1. 查餘額（CLI，不需要 LLM） # 2. 跑 exit_manager.py（規則式，不需要 LLM） # 3. 餘額夠才跑新聞掃描（需要 LLM，用低成本模型） # 4. 輸出 JSON 到 stdout Exit Manager 是純規則引擎，不需要任何 LLM：\n# 三個出場條件，硬編碼 if percent_pnl \u0026gt;= 30: reason = \u0026#34;take_profit\u0026#34; elif percent_pnl \u0026lt;= -50: reason = \u0026#34;stop_loss\u0026#34; elif end_date and (end_date - now) \u0026lt; three_days: reason = \u0026#34;expiry\u0026#34; 新聞掃描器 才用到低成本模型。它從多個來源（自建的社群摘要 pipeline、公開新聞源）抓最新資訊，交給模型分析是否有市場還沒反映的事件：\n# 只看不確定的市場（Yes 價格在 0.20-0.80） # 只看有流動性的市場（volume \u0026gt; $10K） # 用低成本模型分析新聞 vs 當前定價是否有偏差 markets = [m for m in markets if 0.20 \u0026lt;= get_yes_price(m) \u0026lt;= 0.80 and get_volume(m) \u0026gt; 10000] 決策層 關鍵設計：沒有信號就不喚醒高階模型。\n# cron-wrapper.sh TOTAL=$((HAS_EXIT + HAS_NEWS + HAS_COPY)) if [ \u0026#34;$TOTAL\u0026#34; -gt 0 ]; then # 用 system event 通知高階 agent openclaw cron add \\ --name \u0026#34;polymarket-decision\u0026#34; \\ --at \u0026#34;1m\u0026#34; \\ --system-event \u0026#34;有 ${TOTAL} 個信號需要審閱...\u0026#34; \\ --session main \\ --wake now \\ --delete-after-run else echo \u0026#34;No signals, skipping\u0026#34; fi 高階模型收到通知後：\n重新執行 collect.sh 取得最新資料 審閱每個信號的合理性 決定是否執行（可以否決低成本模型的建議） 用 CLI 下單 透過 Telegram 回報結果 為什麼不全部用規則？ 既然 Exit Manager 已經是純規則，為什麼新聞掃描不也用規則？\n因為新聞分析本質上需要語言理解。你要判斷「某則新聞是否已經被市場定價反映」，這不是 if-else 能處理的。但這個判斷的品質要求沒有最終決策那麼高——它只需要「發現可能的機會」，不需要「做出完美的決策」。\n這就是分層的意義：\n任務 需要 LLM？ 品質要求 適合的模型 查餘額、查持倉 ❌ N/A CLI 直接跑 止盈/止損判斷 ❌ 規則即可 Python 腳本 新聞掃描 ✅ 中（召回率 \u0026gt; 精確率） 低成本模型 最終交易決策 ✅ 高（要考慮多因素） 高階模型 實際效果 成本下降 舊版：每 30 分鐘喚醒高階模型 → 一天 48 次 → 大量 token 消耗 新版：低成本模型跑收集，90% 的時間沒信號 → 高階模型一天可能只被喚醒 2-3 次\n可追溯性 每次收集的 JSON 都寫 log，可以回溯：\n[2026-03-02 17:00:05] === 資料收集開始 === [2026-03-02 17:00:06] 餘額: $41.11 [2026-03-02 17:00:08] Exit signals: 1 [2026-03-02 17:00:08] 執行進場掃描... [2026-03-02 17:00:15] News: 0 signals 實戰成績 架構上線後的幾筆交易：\n某地緣政治事件合約 No：+41.4%（23 shares，自動止盈賣出） 某軍事行動合約 No：+40.7%（33 shares，自動止盈賣出） 兩次都是 Exit Manager 偵測到獲利超過門檻 → 通知高階模型 → 審閱確認 → 執行市價賣出 → Telegram 回報。整個流程從偵測到執行大約 2 分鐘。\n踩過的坑 1. 低成本模型的 thinking 行為 我用的低成本模型（某家 API 相容服務）有個特性：它永遠會產生 thinking block，即使你設定 thinking: disabled 也沒用。\n當輸入資料量大時（150+ 篇文章），thinking 會膨脹到 16000+ tokens，直接吃完 max_tokens 配額，導致實際回應被截斷。\n解法： 設定 budget_tokens 限制 thinking 長度（我設了 2000），強制模型把 token 預算留給實際輸出。\n2. 已結算倉位的幽靈信號 Exit Manager 會遍歷所有持倉，但已經結算的倉位（合約到期、結果確定）會顯示 cur_price: 0。如果不跳過，每次都會產生「虧損 100%」的假信號。\n# 簡單但重要的一行 if cur_price == 0: continue 3. 餘額不足時的無效掃描 新聞掃描需要呼叫 LLM API，有成本。如果餘額只剩 $5，就算找到機會也沒錢買。所以加了門檻：\nif (( $(echo \u0026#34;$BALANCE \u0026gt;= 30\u0026#34; | bc -l) )); then # 才跑新聞掃描 fi 4. Agent 自己把自己搞混 第一版讓 agent 同時負責分析和執行時，偶爾會出現它「說服自己」的情況：分析完覺得不該買，但在同一個 context 裡又找到一個理由，然後就買了。\n分離收集和決策後，高階模型收到的是「乾淨的信號列表 + 原始資料」，沒有前一輪的分析結果干擾，決策品質明顯提升。\n下一步構想 Copy Trading 模組 Polymarket 上有些大戶的勝率很高。計畫追蹤特定錢包的交易紀錄，當他們建倉時產生信號。這也是「收集層」的工作，不需要高階模型。\n多市場關聯分析 目前每個市場獨立分析，但現實中事件是關聯的。例如「A 國是否出兵」和「B 資產是否漲」可能高度相關。這需要更複雜的推理，適合高階模型處理。\n自動調整門檻 止盈 30%、止損 50% 是硬編碼的。理想的做法是根據市場類型（政治 vs 體育 vs 金融）和到期時間動態調整。規則式引擎可以做到一定程度，但完整的動態策略可能需要 LLM 輔助。\n總結 這個專案最大的收穫不是交易本身，而是驗證了一種 AI agent 的架構模式：\n能用腳本的不用 LLM。能用便宜模型的不用貴模型。把決策權留給品質最高的環節。\n這不只適用於預測市場。任何需要「監控 → 篩選 → 決策 → 執行」的場景都可以套用：\n監控： cron + CLI（零 LLM 成本） 篩選： 規則引擎 + 低成本模型（低成本） 決策： 高階模型（只在需要時啟動） 執行： CLI + API（零 LLM 成本） 最貴的資源應該花在最需要判斷力的地方。其他的，自動化就好。\n","permalink":"https://blog.mklee.org/posts/polymarket-bot-architecture/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e我在玩 Polymarket——一個基於加密貨幣的預測市場。你可以買入「某事件會發生」或「不會發生」的合約，價格反映市場共識的機率。如果你認為市場定價錯了，就有套利空間。\u003c/p\u003e\n\u003cp\u003e一開始是手動看新聞、手動下單。後來想自動化，畢竟我已經有一個跑在 VPS 上的 AI agent（用 OpenClaw 串接 Telegram），何不讓它幫我盯盤？\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"第一版全自主-agent\"\u003e第一版：全自主 agent\u003c/h2\u003e\n\u003cp\u003e最初的版本很粗暴：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e每 30 分鐘，cron 喚醒 AI agent\u003c/li\u003e\n\u003cli\u003eAgent 自己用 CLI 查持倉、掃新聞、分析信號、下單\u003c/li\u003e\n\u003cli\u003e所有邏輯都在一個大 prompt 裡\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e問題很快浮現：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e成本高\u003c/strong\u003e：每次喚醒都用高階模型（因為需要推理能力），30 分鐘一次，token 消耗驚人\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e不可預測\u003c/strong\u003e：同一組資料，不同次執行可能產生完全相反的結論\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e難以除錯\u003c/strong\u003e：出了問題很難回溯「當時為什麼這樣決定」\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e浪費算力\u003c/strong\u003e：90% 的時間市場沒有變化，agent 做了一堆分析然後什麼都沒做\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e核心矛盾：\u003cstrong\u003e資料收集不需要推理能力，但決策需要。把兩者混在一起，等於用高階模型的價格做低階模型的工作。\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"第二版分層架構\"\u003e第二版：分層架構\u003c/h2\u003e\n\u003cp\u003e改版的核心想法：\u003cstrong\u003e分離收集與決策，只在需要時才啟動高階模型。\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e┌─────────────┐     ┌──────────────┐     ┌─────────────┐\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e│  Cron 排程   │────▶│  資料收集層   │────▶│  決策層      │\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e│  (每 30 min) │     │  (低成本模型) │     │  (高階模型)  │\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e└─────────────┘     └──────────────┘     └─────────────┘\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                           │                     │\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                     有信號時才觸發          審閱 + 下單\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                           │                     │\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    ┌──────▼──────┐        ┌─────▼──────┐\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    │  Exit 檢查  │        │  Telegram   │\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    │  新聞掃描   │        │  回報結果   │\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    │  跟單偵測   │        └────────────┘\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    └─────────────┘\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"資料收集層\"\u003e資料收集層\u003c/h3\u003e\n\u003cp\u003e一個 shell script 串接多個 Python 模組，每次執行輸出一份 JSON 摘要：\u003c/p\u003e","title":"Polymarket Bot 架構升級：讓便宜模型做苦力，貴的模型做決策"},{"content":"起因 工作上有個需求：把散落在多個內部 API 的資料整合成一份分析報告。涉及的資料源有好幾個，每個 API 的認證方式、資料格式、存取路徑都不一樣。\n我手上有兩個 AI agent 環境——一個跑 Claude（Opus），是我自己長期使用的個人 agent；另一個跑 OpenAI 的模型，是公司環境。需求最終要在公司環境執行，但探索和開發的過程在個人 agent 上進行比較順手。\n問題來了：怎麼把我跟個人 agent 討論出來的知識，有效地交給公司 agent 使用？\n不能只丟一段文字 最直覺的做法是把對話結論複製貼上，寫個文件丟過去。但實際試了就知道——一份平鋪直敘的文件，agent 讀完還是會問你一堆問題：\n「這個 API 的 base URL 是什麼？」 「認證 token 放哪裡？」 「回傳格式是 JSON 還是 Parquet？」 「欄位名稱是 revenue 還是 acc_R100？」 每個問題都合理，但每次來回都是時間成本。尤其在 Telegram 這種非同步介面上，一個問題可能要等你看到、回覆、agent 再繼續，中間過了好幾分鐘。\nSkills 作為 Agent 間的介面 後來採用的做法是把知識打包成一個 Skills 專案。這是 OpenClaw 生態系的一個概念，但核心想法跟框架無關：\nskills/ ├── my-skill/ │ ├── SKILL.md # 入口：agent 第一個讀的文件 │ └── references/ # 詳細參考資料 │ ├── api-guide.md │ └── data-sources.md └── knowledge/ └── domain-logic.md # 領域知識和判斷邏輯 SKILL.md 是關鍵。它不是寫給人看的文件，是寫給 agent 看的操作手冊。結構大概長這樣：\n觸發條件 — 什麼時候該用這個 skill 快速開始 — 最小可執行的步驟（通常是一行指令） 資料來源表 — 每個資料源的 URL、認證方式、備援方案 工作流程 — 逐步操作，包含實際的 curl/python 範例 注意事項 — 已知的坑和 workaround 重點在 progressive disclosure：agent 先讀 SKILL.md（\u0026lt; 500 行），大部分情況這就夠了。需要深入某個 API 的細節時，再去讀 references/ 下的對應文件。不需要一次把所有資訊塞進 context。\n對話驅動的開發過程 整個 Skills 專案是在一天的對話中逐步成形的。流程大概是：\n探索階段 — 我跟 agent 說「幫我查這個 API 有什麼 endpoint」，它去打 API、讀文件、回報結果 踩坑修正 — 發現某個 API 的文件跟實際回傳不一樣（欄位名對不上），agent 自己 debug 找出正確的欄位名 知識沉澱 — 每搞定一個資料源，就把結論寫成 reference 文件 整合測試 — 把所有資料源串起來跑一次，產出報告 打包交付 — 整理成 Skills 結構，產出 zip 這個過程裡，agent 不只是執行工具，它在做的是知識轉化 — 把零散的 API 探索結果，轉化成結構化的、可被其他 agent 消費的知識。\n自主性的差異 後來把 Skills 交給公司環境（跑 OpenAI 模型）的 agent 執行時，遇到了一個有趣的對比。\n公司 agent 在執行過程中，每遇到一個小障礙就會停下來問：\nAgent: 執行需要 pyarrow 套件，是否要安裝？ 我: 是 Agent: 安裝完成。接下來需要下載 72MB 的資料檔案，是否繼續？ 我: 是 Agent: 下載完成。檔案中的欄位名稱是 acc_R112，但 SKILL.md 中提到的是 EPS，要用哪個？ 我: SKILL.md 有寫對照表，你看 references/api-guide.md Agent: 了解。正在讀取... 每一步都沒有錯，每個問題都合理。但這些都是不需要人類決策的執行細節。\n相比之下，我的個人 agent（Opus）在開發階段的行為是：缺 library 就裝，API 回 401 就檢查 token 是不是過期然後嘗試替代方案，欄位名對不上就自己去 preview 資料找正確的名字。它只在真正需要決策的時候才會問我——比如「這個 API 需要 VPN 才能連，要不要幫你開？」或「這筆資料看起來異常，要繼續用還是跳過？」\n這不是模型能力的差異（兩邊都能完成任務），而是自主性邊界的設定不同。一個把「裝套件」當成需要確認的操作，另一個把它當成執行的一部分。\n自主性的設計哲學 想了一下，agent 的自主性大概可以分成三層：\n可以自己做的：\n安裝依賴套件 重試失敗的 API 呼叫 在已知的替代方案間切換 修正格式錯誤、型別不符 應該告知但不需等確認的：\n資料有延遲（標註即可，繼續執行） 某個資料源不可用，已改用備援 執行時間比預期長 必須問的：\n涉及外部發送（email、訊息、公開發文） 涉及刪除或不可逆操作 需要業務判斷（這個數字合理嗎？這個建議要發給客戶嗎？） 在 Skills 設計上，這意味著 SKILL.md 應該把備援方案寫清楚，讓 agent 自己判斷切換，而不是每個失敗都拋回給人類。比如：\n## 資料來源 | 面向 | 主要來源 | 備援 | |------|---------|------| | 價格 | 內部資料庫 | Yahoo Finance | | 事件 | 內部 API（需 VPN）| 跳過此面向（--no-event）| Agent 看到這個表，VPN 斷了就知道自己加 --no-event，不用問。\n三段式 Pipeline 最後定型的架構是把產出報告拆成三步：\nStep 1: Python 腳本（抓資料、算指標）→ JSON Step 2: Agent 讀 JSON → 寫分析評論 → JSON Step 3: Python 腳本（合併資料 + 評論）→ HTML 報告 腳本負責確定性的工作（API 呼叫、數學計算），agent 負責需要判斷力的工作（風險評估、行動建議）。這樣做的好處：\n腳本部分可靠可重現 — 同樣的輸入永遠得到同樣的輸出 Agent 部分有明確的輸入輸出 — 讀 JSON、寫 JSON，不需要自己去打 API 可以獨立測試 — 腳本壞了改腳本，agent 的分析品質不好改 prompt 跨模型相容 — 任何能讀寫 JSON 的 agent 都能做 Step 2 結論 這次經驗讓我覺得，AI agent 的生產力瓶頸往往不在模型能力，而在兩個地方：\n知識的結構化程度 — 同樣的知識，散落在對話裡 vs 整理成 Skills，對 agent 的執行效率差異巨大 自主性的邊界設計 — agent 該在哪裡自己做決定、在哪裡停下來問人，這個邊界設對了，能省下大量不必要的來回 Skills 本質上是一種 agent-to-agent 的知識傳遞協議。寫得好的 SKILL.md，能讓接收端的 agent 像一個有經驗的新人一樣——拿到 SOP 就能幹活，只在真正模糊的地方才來找你確認。\n而自主性的設計，最終是一個信任問題：你願意讓 agent 自己決定到什麼程度？裝一個 pip 套件是信任的最低門檻。如果連這個都要問，那 agent 就只是一個需要你逐行確認的腳本執行器，不是真正的助手。\n","permalink":"https://blog.mklee.org/posts/agent-skills-cross-model-collaboration/","summary":"\u003ch2 id=\"起因\"\u003e起因\u003c/h2\u003e\n\u003cp\u003e工作上有個需求：把散落在多個內部 API 的資料整合成一份分析報告。涉及的資料源有好幾個，每個 API 的認證方式、資料格式、存取路徑都不一樣。\u003c/p\u003e\n\u003cp\u003e我手上有兩個 AI agent 環境——一個跑 Claude（Opus），是我自己長期使用的個人 agent；另一個跑 OpenAI 的模型，是公司環境。需求最終要在公司環境執行，但探索和開發的過程在個人 agent 上進行比較順手。\u003c/p\u003e\n\u003cp\u003e問題來了：怎麼把我跟個人 agent 討論出來的知識，有效地交給公司 agent 使用？\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"不能只丟一段文字\"\u003e不能只丟一段文字\u003c/h2\u003e\n\u003cp\u003e最直覺的做法是把對話結論複製貼上，寫個文件丟過去。但實際試了就知道——一份平鋪直敘的文件，agent 讀完還是會問你一堆問題：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e「這個 API 的 base URL 是什麼？」\u003c/li\u003e\n\u003cli\u003e「認證 token 放哪裡？」\u003c/li\u003e\n\u003cli\u003e「回傳格式是 JSON 還是 Parquet？」\u003c/li\u003e\n\u003cli\u003e「欄位名稱是 \u003ccode\u003erevenue\u003c/code\u003e 還是 \u003ccode\u003eacc_R100\u003c/code\u003e？」\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e每個問題都合理，但每次來回都是時間成本。尤其在 Telegram 這種非同步介面上，一個問題可能要等你看到、回覆、agent 再繼續，中間過了好幾分鐘。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"skills-作為-agent-間的介面\"\u003eSkills 作為 Agent 間的介面\u003c/h2\u003e\n\u003cp\u003e後來採用的做法是把知識打包成一個 \u003cstrong\u003eSkills 專案\u003c/strong\u003e。這是 OpenClaw 生態系的一個概念，但核心想法跟框架無關：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eskills/\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e├── my-skill/\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e│   ├── SKILL.md          # 入口：agent 第一個讀的文件\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e│   └── references/       # 詳細參考資料\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e│       ├── api-guide.md\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e│       └── data-sources.md\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e└── knowledge/\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    └── domain-logic.md   # 領域知識和判斷邏輯\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eSKILL.md\u003c/strong\u003e 是關鍵。它不是寫給人看的文件，是寫給 agent 看的操作手冊。結構大概長這樣：\u003c/p\u003e","title":"Agent 間的知識交付：從對話到可執行的 Skills"},{"content":"為什麼要自建資訊流 每天早上打開手機，幾十個 tab、幾百則未讀。技術論壇、社群媒體、新聞源——每個都說自己很重要。花 30 分鐘刷完，真正有用的可能三條。\n更糟的是，演算法幫你選的東西，跟你真正需要的東西，往往不是同一批。演算法優化的是你的停留時間，不是你的知識密度。\n所以我想做的事很簡單：自己選來源、讓 AI 幫我篩重點、每天早上給我一份摘要。\n不是讓 AI 取代我讀東西——是讓它幫我從 200 則裡挑出值得讀的 20 則，然後我自己決定要不要點進去看全文。\nPipeline 一：RSS 路線 RSS 是最乾淨的資訊來源。沒有演算法、沒有廣告、格式統一。大多數技術論壇和部落格都還支援 RSS，只是很多人忘了它的存在。\n架構 自架 Tiny Tiny RSS（TTRSS），訂閱想追的 feed。TTRSS 有完整的 API，可以程式化操作一切。\n流程長這樣：\ncron（每天早上） → shell script 呼叫 TTRSS API 抓未讀 → 餵給 AI agent 做分類摘要 → shell script 上傳靜態頁面 + 標記已讀 TTRSS API 操作 # 登入拿 session SID=$(curl -s -X POST \u0026#34;https://rss.example.com/api/\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;op\u0026#34;:\u0026#34;login\u0026#34;,\u0026#34;user\u0026#34;:\u0026#34;myuser\u0026#34;,\u0026#34;password\u0026#34;:\u0026#34;mypass\u0026#34;}\u0026#39; \\ | python3 -c \u0026#34;import json,sys; print(json.load(sys.stdin)[\u0026#39;content\u0026#39;][\u0026#39;session_id\u0026#39;])\u0026#34;) # 抓未讀文章（最多 200 篇） curl -s -X POST \u0026#34;https://rss.example.com/api/\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#34;{\\\u0026#34;op\\\u0026#34;:\\\u0026#34;getHeadlines\\\u0026#34;,\\\u0026#34;sid\\\u0026#34;:\\\u0026#34;$SID\\\u0026#34;,\\\u0026#34;feed_id\\\u0026#34;:2,\\\u0026#34;view_mode\\\u0026#34;:\\\u0026#34;unread\\\u0026#34;,\\\u0026#34;limit\\\u0026#34;:200}\u0026#34; # 處理完後標記已讀 curl -s -X POST \u0026#34;https://rss.example.com/api/\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#34;{\\\u0026#34;op\\\u0026#34;:\\\u0026#34;catchupFeed\\\u0026#34;,\\\u0026#34;sid\\\u0026#34;:\\\u0026#34;$SID\\\u0026#34;,\\\u0026#34;feed_id\\\u0026#34;:2}\u0026#34; API 回來的是 JSON，每篇有 title、link、excerpt。腳本把這些整理成一個大 prompt，丟給 AI agent 做摘要。\n三段式 Pipeline 這裡有一個重要的設計決定：不要讓 AI agent 做所有事。\n第一段：Shell script（抓取） → 呼叫 TTRSS API、整理資料、格式化成 prompt 第二段：AI agent（摘要） → 只做一件事：讀文章列表、分類、寫摘要 第三段：Shell script（發布） → 上傳 HTML 到 Cloudflare R2、標記已讀、發通知 為什麼要這樣拆？因為 LLM 做 API 呼叫不穩定。它可能忘記帶參數、可能 hallucinate 一個不存在的 endpoint、可能在 JSON parsing 上搞砸。但 LLM 做「讀一堆文字然後寫摘要」非常強。\n讓腳本做腳本擅長的事，讓 LLM 做 LLM 擅長的事。\n前置腳本負責確定性的操作（API 呼叫、資料格式化），AI 只負責創意性的工作（分類、摘要、判斷重要性），收尾腳本再接手確定性的操作（上傳、通知、清理）。\n靜態頁面發布 摘要生成後，用 rclone 上傳到 Cloudflare R2：\n# 把摘要 HTML 上傳到 R2 rclone copy digest.html r2:my-digest-bucket/$(date +%Y-%m-%d)/ R2 接 Cloudflare 的 CDN，免費額度綽綽有餘。每天一個 HTML 檔案，連 database 都不需要。\n可複製性 這套 pipeline 跟「訂了什麼 RSS」完全無關。換成 HN、Reddit RSS、個人部落格、學術論文的 feed——只要是 TTRSS 能訂的，pipeline 都能處理。AI agent 的 prompt 稍微調一下分類規則就好。\nPipeline 二：社群媒體路線（進階） 不是所有平台都有 RSS。某些社群媒體的 timeline 是封閉的——沒有 RSS feed、官方 API 有 rate limit 又常改版、第三方 client 隨時可能被封。\n用瀏覽器直接抓 最暴力也最穩定的方案：開一個持久化的瀏覽器，直接 scrape 頁面內容。\nXvfb（虛擬螢幕） → Headless Chrome（持久化 session） → agent-browser（CDP 操控） → 滾動頁面、擷取內容 → 餵給 AI agent 做摘要 Xvfb 提供虛擬螢幕，Chrome 跑在上面，Cookie 和 session 持久化在磁碟上——等於一個永遠開著、永遠登入的瀏覽器。\n為什麼不用 API？ 方案 Rate Limit API 變更風險 登入問題 成本 官方 API 嚴格 高（隨時改版） OAuth 流程複雜 可能收費 第三方 Library 中等 極高（一封就死） 依賴逆向工程 免費但不穩 瀏覽器 Scraping 無 低（頁面改版慢） Cookie 持久化 免費 瀏覽器 scraping 的優勢是：你看到什麼，程式就抓到什麼。不受 API rate limit，不怕 API 改版（頁面改版的頻率遠低於 API），Cookie 持久化解決了登入問題。\nCDP 操控 透過 Chrome DevTools Protocol（CDP），可以用程式控制瀏覽器：\n# 開啟頁面 agent-browser --cdp 9222 open \u0026#34;https://example.com/timeline\u0026#34; # 擷取頁面快照（accessibility tree） agent-browser --cdp 9222 snapshot -i # 執行 JavaScript（例如滾動頁面） agent-browser --cdp 9222 eval \u0026#34;window.scrollBy(0, 3000)\u0026#34; 腳本的邏輯是：開頁面 → 等載入 → 滾幾次抓更多內容 → 擷取文字 → 關頁面。之後同樣走三段式 pipeline：腳本抓取 → AI 摘要 → 腳本發布。\n踩坑：Cookie 過期 持久化瀏覽器最大的問題是 session 過期。某些平台會定期強制登出。目前的 workaround 是：\n瀏覽器 scraping 失敗時，腳本會偵測到（頁面內容異常） 發通知提醒我手動重新登入 透過 noVNC 遠端操作瀏覽器完成登入 沒有完美的自動化方案，但一個月手動登入一兩次，可以接受。\n共通架構 兩條 pipeline 的底層架構其實一樣：\ncron（每天固定時間） → wrapper.sh ├─ 第一段：抓取腳本（TTRSS API / CDP scraping） ├─ 第二段：AI agent 摘要（分類 + 重點提取 + 生成 HTML） └─ 第三段：發布腳本（rclone → R2 + 發送通知） 三段式的好處 Debug 容易。 第一段的輸出可以存檔，第二段有問題可以拿同樣的輸入重跑，第三段上傳失敗可以手動補。 各段獨立演化。 換 RSS reader？改第一段。換 LLM？改第二段。換儲存方案？改第三段。互不影響。 LLM 成本可控。 AI 只處理已經格式化好的文字，不浪費 token 在 API 呼叫和錯誤處理上。 成本 項目 費用 VPS Free tier（各大雲都有） Cloudflare R2 免費額度（10GB 儲存 + 1M 請求/月） TTRSS 自架，免費 AI API 用既有的 agent API 額度 總計 ≈ $0/月 認真算的話唯一的成本是 AI API 的 token 消耗。每天 200 篇文章的摘要，大約幾千個 token，幾乎可以忽略。\n實際成果與心得 跑了一陣子，幾個觀察：\n從 30 分鐘變 3 分鐘。 以前每天早上花半小時在各個平台間切換、刷 timeline、掃標題。現在打開一個靜態頁面，3 分鐘看完今天的重點。想深入的再點連結去看原文。\nAI 的分類比我自己分得好。 不是因為它更聰明，而是因為它不會被標題黨騙、不會因為情緒去點不相關的東西。它很機械地按照我定的規則分類，反而更準。\n靜態頁面意外好用。 本來只是為了方便自己看，結果因為是靜態 HTML + CDN，載入飛快、可以分享連結給別人。偶爾有朋友問「最近技術圈在聊什麼」，直接丟連結就好。\n三段式 pipeline 真的穩。 跑了幾週，AI 摘要這段從來沒出過問題。出問題的都是第一段（API timeout）和第三段（上傳失敗）——但因為拆開了，retry 很簡單。\n意外的副作用 當你有了自己的資訊 pipeline，你會開始更認真地思考「我到底要追蹤什麼」。以前隨手 follow 的帳號、訂閱的 newsletter，現在都會想：這個值得進我的 daily digest 嗎？\n這不是技術問題，是資訊策展的問題。工具只是讓這件事變得可執行。\n給想自己搞的人 先從 RSS 開始。 瀏覽器 scraping 是進階路線，坑多。RSS 路線一個下午就能搞定。 三段式 pipeline 不是過度設計。 你會感謝自己把抓取、摘要、發布拆開的那一天。 不要讓 LLM 碰 API。 它會搞砸。讓腳本處理 I/O，LLM 只做文字處理。 Cloudflare R2 + 靜態 HTML 是窮人的 SaaS。 零成本、零維護、全球 CDN。 完美是好的敵人。 先讓它跑起來，再慢慢調 prompt、加 feed、優化格式。我的第一版摘要醜得要命，但它 有用。 這套 pipeline 跑在 OpenClaw 上，由 AI agent 每天自動執行。從設計到穩定運行大概花了一個週末。最難的部分不是技術，是決定哪些資訊值得你每天花 3 分鐘。\n","permalink":"https://blog.mklee.org/posts/rss-ai-daily-digest-pipeline/","summary":"\u003ch2 id=\"為什麼要自建資訊流\"\u003e為什麼要自建資訊流\u003c/h2\u003e\n\u003cp\u003e每天早上打開手機，幾十個 tab、幾百則未讀。技術論壇、社群媒體、新聞源——每個都說自己很重要。花 30 分鐘刷完，真正有用的可能三條。\u003c/p\u003e\n\u003cp\u003e更糟的是，演算法幫你選的東西，跟你真正需要的東西，往往不是同一批。演算法優化的是你的停留時間，不是你的知識密度。\u003c/p\u003e\n\u003cp\u003e所以我想做的事很簡單：\u003cstrong\u003e自己選來源、讓 AI 幫我篩重點、每天早上給我一份摘要。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e不是讓 AI 取代我讀東西——是讓它幫我從 200 則裡挑出值得讀的 20 則，然後我自己決定要不要點進去看全文。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"pipeline-一rss-路線\"\u003ePipeline 一：RSS 路線\u003c/h2\u003e\n\u003cp\u003eRSS 是最乾淨的資訊來源。沒有演算法、沒有廣告、格式統一。大多數技術論壇和部落格都還支援 RSS，只是很多人忘了它的存在。\u003c/p\u003e\n\u003ch3 id=\"架構\"\u003e架構\u003c/h3\u003e\n\u003cp\u003e自架 \u003ca href=\"https://tt-rss.org/\"\u003eTiny Tiny RSS\u003c/a\u003e（TTRSS），訂閱想追的 feed。TTRSS 有完整的 API，可以程式化操作一切。\u003c/p\u003e\n\u003cp\u003e流程長這樣：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecron（每天早上）\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  → shell script 呼叫 TTRSS API 抓未讀\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  → 餵給 AI agent 做分類摘要\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  → shell script 上傳靜態頁面 + 標記已讀\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"ttrss-api-操作\"\u003eTTRSS API 操作\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6272a4\"\u003e# 登入拿 session\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#8be9fd;font-style:italic\"\u003eSID\u003c/span\u003e\u003cspan style=\"color:#ff79c6\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ff79c6\"\u003e$(\u003c/span\u003ecurl -s -X POST \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;https://rss.example.com/api/\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f1fa8c\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u003c/span\u003e  -H \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;Content-Type: application/json\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f1fa8c\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u003c/span\u003e  -d \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#39;{\u0026#34;op\u0026#34;:\u0026#34;login\u0026#34;,\u0026#34;user\u0026#34;:\u0026#34;myuser\u0026#34;,\u0026#34;password\u0026#34;:\u0026#34;mypass\u0026#34;}\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#f1fa8c\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u003c/span\u003e  | python3 -c \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;import json,sys; print(json.load(sys.stdin)[\u0026#39;content\u0026#39;][\u0026#39;session_id\u0026#39;])\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#ff79c6\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6272a4\"\u003e# 抓未讀文章（最多 200 篇）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -s -X POST \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;https://rss.example.com/api/\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f1fa8c\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u003c/span\u003e  -H \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;Content-Type: application/json\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f1fa8c\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u003c/span\u003e  -d \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;{\\\u0026#34;op\\\u0026#34;:\\\u0026#34;getHeadlines\\\u0026#34;,\\\u0026#34;sid\\\u0026#34;:\\\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#8be9fd;font-style:italic\"\u003e$SID\u003c/span\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\\\u0026#34;,\\\u0026#34;feed_id\\\u0026#34;:2,\\\u0026#34;view_mode\\\u0026#34;:\\\u0026#34;unread\\\u0026#34;,\\\u0026#34;limit\\\u0026#34;:200}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#6272a4\"\u003e# 處理完後標記已讀\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -s -X POST \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;https://rss.example.com/api/\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f1fa8c\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u003c/span\u003e  -H \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;Content-Type: application/json\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f1fa8c\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\u003c/span\u003e  -d \u003cspan style=\"color:#f1fa8c\"\u003e\u0026#34;{\\\u0026#34;op\\\u0026#34;:\\\u0026#34;catchupFeed\\\u0026#34;,\\\u0026#34;sid\\\u0026#34;:\\\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#8be9fd;font-style:italic\"\u003e$SID\u003c/span\u003e\u003cspan style=\"color:#f1fa8c\"\u003e\\\u0026#34;,\\\u0026#34;feed_id\\\u0026#34;:2}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAPI 回來的是 JSON，每篇有 title、link、excerpt。腳本把這些整理成一個大 prompt，丟給 AI agent 做摘要。\u003c/p\u003e","title":"用 RSS + AI Agent 自建每日資訊摘要"},{"content":"為什麼要自己跑 TTS？ 市面上的 TTS API 不缺——ElevenLabs、OpenAI TTS、Azure Speech。但如果你想要的是用自己的聲音說話，而且不想每個月付錢、不想把錄音傳到別人的 server，那選擇就少很多了。\n我的需求很簡單：讓我的 AI agent（跑在 OpenClaw 上）能用我的聲音回覆語音訊息。Agent 跑在 Oracle Cloud 的 ARM VPS 上，沒有 GPU。但家裡有一台 Windows 桌機，裝了 RTX 4070 Ti。\n所以架構很明確：VPS 負責 agent 邏輯，Windows 桌機負責 GPU 推理，中間用 SSH tunnel 串起來。\n聽起來簡單。實際上花了三代 TTS 模型、無數次 WSL 踩坑，才到今天穩定運作的狀態。\n第一代：Fish Speech（2025 年底） Fish Speech 是最早嘗試的方案。它支援 voice cloning，品質不錯，社群也活躍。\n部署在 WSL 上，port 8880，透過 autossh reverse tunnel 讓 VPS 能連到。一開始跑得還行，但遇到幾個問題：\n模型更新頻繁，API 不太穩定 VRAM 吃得多，跟其他任務搶資源 後來有更好的選擇出現，就換了 Fish Speech 的功勞是：它驗證了整個架構是可行的——WoL 喚醒、WSL systemd、autossh tunnel、VPS 呼叫腳本這一整套 pipeline。後面換模型只需要改 server 端，其他都能重用。\n第二代：CosyVoice2（2026-02-13） 阿里的 CosyVoice2 看起來是很好的升級：0.5B 參數、3.2GB VRAM、zero-shot voice cloning。\n順利的部分 安裝本身不難。conda 環境、pip install、下載模型。Server 啟動後 VRAM 佔用合理。Tunnel 也沿用 Fish Speech 的設定。\n不順利的部分 模型檔 sha256 不符。 flow.pt 下載了好幾次，integrity check 一直 fail。最後用了一個 workaround 才跑起來。\n首次推理超級慢。 CosyVoice2 的 warmup 包含 CUDA compilation，第一次呼叫要等好幾分鐘。設了 120 秒 timeout 還是不夠。\nsoundfile 不吃 SpooledTemporaryFile。 FastAPI 的 UploadFile 底層用 SpooledTemporaryFile，但 soundfile 開不了。得先寫到暫存檔再讀。\nReference audio 要 16kHz。 原始錄音是 48kHz，直接丟進去會 crash。需要先 ffmpeg 轉檔。\nCosyVoice3 的插曲 既然 2 能跑了，當然想試 3。結果撞到一個已知 bug（GitHub #1422）：hifigan 的 f0_predictor kernel size 是 4，但某些情況下 mel frames 只有 3，直接 RuntimeError。\nRuntimeError: Calculated padded input size per channel: (3). Kernel size: (4). Kernel size can\u0026#39;t be greater than actual input size 官方沒修。退回 CosyVoice2 穩定運行。\n第三代：Qwen3-TTS（2026-02-19） Qwen3-TTS 是目前的方案。選它的原因：\n3.9GB VRAM，4070 Ti 綽綽有餘 Zero-shot voice cloning，只需要一段 reference audio FastAPI server，API 乾淨 阿里出品，跟 CosyVoice 系出同門但更穩定 部署 # WSL 上 pip install qwen-tts # 啟動 server（已做成 systemd user service） systemctl --user start qwen3-tts Server 在 port 8880，沿用之前的 autossh tunnel，VPS 端完全不用改。\nAPI 兩個 endpoint：\nEndpoint 用途 POST /tts 用 server 預設的 reference audio POST /tts/clone 自帶 ref_audio + ref_text /tts/clone 的參數：\ncurl -X POST \u0026#34;http://localhost:8880/tts/clone\u0026#34; \\ -F \u0026#34;text=要合成的文字\u0026#34; \\ -F \u0026#34;language=Chinese\u0026#34; \\ -F \u0026#34;ref_text=參考音頻的逐字稿\u0026#34; \\ -F \u0026#34;ref_audio=@reference.wav\u0026#34; \\ -o output.wav Reference Audio 的 Best Practices 根據阿里雲官方文件和實測：\n項目 建議 時長 10-20 秒（太短學不到特徵，太長引入雜訊） 取樣率 ≥ 24 kHz 聲道 Mono 內容 連續清晰語音，無背景噪音 ref_text 必須和錄音內容完全一致 我實測了三個版本的 reference audio：12.7 秒、25 秒、29 秒。結果 12.7 秒的效果最好，剛好落在官方建議的甜蜜點。更長的反而聲紋模仿度下降。\nWSL 踩坑大全 這套架構最痛苦的部分不是 TTS 模型，是 WSL。\n1. WSL 不會自己啟動 Windows 開機後，WSL 不會自動跑。你需要：\nWindows Task Scheduler 建一個 AtLogon 任務（不是 AtStartup） 執行 wsl -d Debian -- bash -c \u0026quot;sleep infinity\u0026quot; 必須用使用者帳號，SYSTEM 帳號無法啟動 WSL 2. systemd 要額外開啟 WSL 預設沒有 systemd。在 /etc/wsl.conf 加：\n[boot] systemd=true command=/usr/bin/sleep infinity sleep infinity 是為了防止 WSL 在沒有前台進程時自動關閉。\n3. User service 需要 linger TTS server 跑在 systemd user service 裡（不需要 root），但 WSL 的 user session 可能會被回收：\nloginctl enable-linger mark 4. WoL 不能走 WireGuard VPS 到家裡有 WireGuard tunnel，但 WoL 是 Layer 2 broadcast，WireGuard 是 Layer 3 tunnel，送不了。\n解法：透過 Home Assistant 的 wake_on_lan integration，讓同 LAN 的 HA 代發 WoL。\n# VPS 上的 wake-gpu.sh curl -X POST \u0026#34;http://\u0026lt;ha-host\u0026gt;:8123/api/services/wake_on_lan/send_magic_packet\u0026#34; \\ -H \u0026#34;Authorization: Bearer $HA_TOKEN\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;mac\u0026#34;: \u0026#34;XX:XX:XX:XX:XX:XX\u0026#34;}\u0026#39; 5. autossh tunnel 要處理斷線 GPU 機器不是 24/7 開機，tunnel 會斷。用 autossh + systemd 自動重連：\n# cosyvoice-tunnel.service [Service] ExecStart=/usr/bin/autossh -M 0 -N \\ -R 8880:localhost:8880 \\ -R 2222:localhost:22 \\ -o \u0026#34;ServerAliveInterval=30\u0026#34; \\ -o \u0026#34;ServerAliveCountMax=3\u0026#34; \\ vps-host Restart=always RestartSec=5 完整架構圖 ┌─────────────────────────────────────────────┐ │ VPS (Oracle Cloud Singapore, ARM) │ │ │ │ OpenClaw Agent │ │ → scripts/qwen3-tts.sh │ │ → curl localhost:8880/tts │ │ → message tool (Telegram voice) │ │ │ │ autossh reverse tunnel ←────────────────┐ │ │ localhost:8880 → WSL:8880 │ │ │ localhost:2222 → WSL:22 │ │ │ │ │ │ WireGuard → Home Assistant │ │ │ → WoL magic packet │ │ └──────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────┘ │ Windows Desktop (RTX 4070 Ti) │ │ Task Scheduler (AtLogon) │ → wsl -d Debian -- sleep infinity │ │ WSL Debian │ → systemd user services: │ - qwen3-tts.service (port 8880) │ - cosyvoice-tunnel.service (autossh) │ → loginctl enable-linger mark └──────────────────────────────────────────────┘ 啟動流程：VPS 呼叫 wake-gpu.sh → HA 發 WoL → Windows 開機 → 自動登入 → WSL 啟動 → systemd services 啟動 → tunnel 建立 → VPS 可呼叫 TTS API。\n整個過程約 55 秒。\n給想自己搞的人 先驗證架構再選模型。 WoL、tunnel、WSL systemd 這些搞定後，換模型只是改 server 端。 Reference audio 不是越長越好。 10-20 秒是甜蜜點。 WSL + systemd 是可行的，但坑很多。 enable-linger、sleep infinity、AtLogon 而非 AtStartup——這些都是踩過才知道的。 用 Home Assistant 做 WoL 中繼是最省事的方案，如果你家本來就有的話。 autossh \u0026gt; 手動 SSH tunnel。 斷線自動重連，不用操心。 現狀 Qwen3-TTS + WSL + autossh tunnel，穩定運行中。AI agent 可以隨時用我的聲音說話，延遲大概 10-15 秒（含推理），VRAM 佔 3.9GB。\n三代 TTS 模型，無數次 WSL debug，最後得到的是一個不太酷但很穩的方案。有時候 boring infrastructure 就是最好的 infrastructure。\n","permalink":"https://blog.mklee.org/posts/qwen3-tts-voice-clone-wsl/","summary":"\u003ch2 id=\"為什麼要自己跑-tts\"\u003e為什麼要自己跑 TTS？\u003c/h2\u003e\n\u003cp\u003e市面上的 TTS API 不缺——ElevenLabs、OpenAI TTS、Azure Speech。但如果你想要的是\u003cstrong\u003e用自己的聲音\u003c/strong\u003e說話，而且不想每個月付錢、不想把錄音傳到別人的 server，那選擇就少很多了。\u003c/p\u003e\n\u003cp\u003e我的需求很簡單：讓我的 AI agent（跑在 \u003ca href=\"https://openclaw.ai\"\u003eOpenClaw\u003c/a\u003e 上）能用我的聲音回覆語音訊息。Agent 跑在 Oracle Cloud 的 ARM VPS 上，沒有 GPU。但家裡有一台 Windows 桌機，裝了 RTX 4070 Ti。\u003c/p\u003e\n\u003cp\u003e所以架構很明確：VPS 負責 agent 邏輯，Windows 桌機負責 GPU 推理，中間用 SSH tunnel 串起來。\u003c/p\u003e\n\u003cp\u003e聽起來簡單。實際上花了三代 TTS 模型、無數次 WSL 踩坑，才到今天穩定運作的狀態。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"第一代fish-speech2025-年底\"\u003e第一代：Fish Speech（2025 年底）\u003c/h2\u003e\n\u003cp\u003eFish Speech 是最早嘗試的方案。它支援 voice cloning，品質不錯，社群也活躍。\u003c/p\u003e\n\u003cp\u003e部署在 WSL 上，port 8880，透過 autossh reverse tunnel 讓 VPS 能連到。一開始跑得還行，但遇到幾個問題：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e模型更新頻繁\u003c/strong\u003e，API 不太穩定\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVRAM 吃得多\u003c/strong\u003e，跟其他任務搶資源\u003c/li\u003e\n\u003cli\u003e後來有更好的選擇出現，就換了\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFish Speech 的功勞是：\u003cstrong\u003e它驗證了整個架構是可行的\u003c/strong\u003e——WoL 喚醒、WSL systemd、autossh tunnel、VPS 呼叫腳本這一整套 pipeline。後面換模型只需要改 server 端，其他都能重用。\u003c/p\u003e","title":"在 WSL 上跑 Qwen3-TTS Voice Clone：從 Fish Speech 到三代 TTS 的踩坑之旅"},{"content":"前言 當你給一個 AI agent 一台 VPS、一堆 API key、和一個空白的工作區，它要怎麼「記住」東西？\n這篇記錄了我在 OpenClaw 上建構 AI agent 記憶系統的過程——從最初的空白 MEMORY.md，到現在帶有優先級標籤、自動過期清理、事件時間軸的結構化架構。不是教學文，而是真實的踩坑記錄。\n第零天：空白的開始 OpenClaw 啟動時，工作區裡有四個檔案：SOUL.md（人格設定）、USER.md（使用者資訊）、AGENTS.md（行為規範）、和一個空的 MEMORY.md。\nAgent 每次醒來（新 session）都是失憶狀態——它只知道人格和行為規範。所有對話、決策、偏好，隨著 session 結束就消失了。\n第一個問題：agent 要怎麼知道「上次我們聊到哪了」？\n第一階段：Daily Files（流水帳） 最自然的做法：每天一個 markdown 檔，如 memory/2026-01-26.md、memory/2026-01-27.md，記錄當天發生的事。\n格式很自由——## 標題 分段，裡面就是對話摘要、設定紀錄、debug 過程。Agent 在每次 session 開始時讀今天和昨天的 daily file，大概知道最近在幹嘛。\n好處： 簡單、自然、寫入無摩擦。\n壞處：\n兩週前的事？要翻十幾個檔案。 「上次 OpenClaw 升級是什麼時候？」→ 沒人記得在哪個 daily file 裡。 重要決策淹沒在日常瑣事中。 第二階段：MEMORY.md（策展式長期記憶） Daily file 是流水帳，MEMORY.md 是策展。\n想法很簡單：把真正重要的東西從 daily file 「升級」到 MEMORY.md。Agent 每次啟動都讀這個檔案，等於它的「核心記憶」。\n最初的 MEMORY.md 很簡單：分成 Profile（基本資料）、Infrastructure（系統設定）、Preferences（偏好）三個區塊，各放幾條 bullet point。\n問題來了：誰來維護？\n答案是——人工。我（或 agent 在我要求下）定期把 daily file 裡的重要內容搬到 MEMORY.md。這顯然不可持續。\n第三階段：Priority Tags + Memory Janitor MEMORY.md 只增不減，很快就膨脹了。每個區塊的重要性不同：基礎設施設定是永久的，但某次 espresso 實驗的參數可能一個月後就沒用了。\n解法：優先級標籤 + 自動過期清理。\n每個 ## 標題後面加上標籤：\n[P0] — 永久保留（個人資料、基礎設施、核心偏好） [P1] [日期] — 90 天後自動歸檔 [P2] [日期] — 30 天後自動歸檔 例如：\n## Infrastructure [P0] → 永不過期 ## X Timeline Scraping [P1] [2026-02-01] → 90 天後歸檔 ## Espresso [P2] [2026-02-15] → 30 天後歸檔\n配套的 memory-janitor.py 每天 20:00 跑一次，自動掃描過期區塊。不刪除，只是搬到 archive——萬一需要還找得到。\n實際跑起來長這樣：14 個章節全部在保留期內，P1 的 X Timeline Scraping 已過 17 天（上限 90 天），P2 的 Espresso 才 3 天（上限 30 天）。\n第四階段：借鑑 OpenViking 的記憶分類 字節跳動開源了 OpenViking——一個專為 AI agent 設計的上下文數據庫。它的記憶分類啟發了我們重新整理 MEMORY.md：\n六類記憶：\n類別 用途 我們的對應 Profile 使用者基本資料 ## Mark Profile [P0] Preferences 偏好和習慣 ## Mark Preferences [P0] Entities 相關實體 ## Mark Entities [P0] Events 事件時間軸 ## Events Timeline [P0] Agent Cases 解決過的問題 ## Agent Cases [P0] Agent Patterns 可重用最佳實踐 ## Agent Patterns [P0] 其中 Agent Cases 和 Agent Patterns 是最有價值的新增。\nAgent Cases 記錄解決過的具體問題。例如「Cron sub-agent 發送失敗」這個 case，記了問題（target 寫 \u0026ldquo;mark\u0026rdquo; 而非 chat ID）、解法（改靠 announce 機制）、和可重用模式（isolated session 永遠不要自己發訊息）。\nAgent Patterns 記錄可重用的最佳實踐，像是「三段式 pipeline」（腳本抓取 → agent 摘要 → 腳本發送）和「驗證四防線」（創建 → 執行 → 送達 → 失敗告警）。\nAgent 遇到類似問題時，可以直接套用已知的解法，而不是從頭踩坑。\n第五階段：L0 索引 + Events Timeline + 自動提取 最新一輪改進來自一場「agent 辯論」——我 spawn 了兩個 sub-agent：\n架構設計 agent：提出完整的三級索引方案（L0 routing、自動提取 pipeline、結構化 extraction format） Devil\u0026rsquo;s advocate agent：把方案批得體無完膚 Devil\u0026rsquo;s advocate 的核心批評：\n這份方案的根本問題是在 3KB 的記憶系統上設計了一套適合 30KB+ 系統的架構。目前的瓶頸不是檢索效率，而是「有沒有記下來」。\n最終收斂出的極簡版：\nL0 Comments（文檔自描述） 每個 ## 區塊加一行 HTML comment 作為摘要，例如 Infrastructure 區塊下面加一行 \u0026lt;!-- L0: VPS, HA, NAS, WireGuard, Chrome CDP --\u0026gt;。\n不寫索引腳本、不拆檔案——3KB 的檔案不值得做 routing。但 L0 comment 讓人和 agent 都能一眼掃到每個區塊在講什麼。\nEvents Timeline（時間軸索引） 在 MEMORY.md 頂部加一個按月分組的事件列表，每條一句話、≤80 字，只記結論不記過程。像是「02-17 多 Instance cron 洩漏修復」、「02-14 Memory Janitor 上線」。\n回答「上次 X 是什麼時候」再也不用翻 daily file。\n自動提取規則 在 AGENTS.md 加了一段話：\n重要對話結束時，直接 edit MEMORY.md 對應區塊追加記錄。觸發條件：新決策、設定變更、新知識。不觸發：閒聊、簡單查詢、重複 routine。\n沒有結構化格式、沒有暫存檔、沒有去重機制——這些都是 devil\u0026rsquo;s advocate 砍掉的「過度設計」。最簡單的規則往往最持久。\n現在的架構全貌 整個記憶系統由四層組成：\nMEMORY.md — 策展式長期記憶，14 個區塊，含 Events Timeline、Profile、Infrastructure、Agent Cases、Agent Patterns 等 memory/*.md — 每日流水帳（daily files） memory/archive/ — 過期章節歸檔 notes/ — Obsidian vault（PARA 結構），每小時 cron 同步 資料流很直覺：對話中產生的內容即時寫入 daily file，重要事件同步寫入 MEMORY.md，每小時同步到 Obsidian，每天 20:00 由 memory-janitor 清理過期內容。\n學到的事 從簡單開始。 空白 MEMORY.md → daily files → 策展式長期記憶。每一步都是被真實痛點推動的，不是預先設計的。\n讓 devil\u0026rsquo;s advocate 砍掉你的設計。 架構設計 agent 提出了很酷的三級索引系統。Devil\u0026rsquo;s advocate 指出 3KB 檔案不需要 routing。結果省了一堆無意義的基礎建設。\nAgent Cases 是最有價值的記憶類型。 比個人偏好、比基礎設施設定都有價值。因為它直接減少了「重複踩坑」的次數。\n過期機制必須從第一天就有。 否則 MEMORY.md 會膨脹到 agent 自己都讀不完。P0/P1/P2 + janitor 是最小的可行方案。\n「有沒有記下來」比「怎麼檢索」重要十倍。 在記憶系統很小的時候，最大的風險不是找不到，而是根本沒記。自動提取規則解決的就是這個問題。\n這篇文章由 Mark 和他的 OpenClaw agent 協作完成。agent 從 memory files 取材、整理結構、撰寫初稿，Mark 提供方向和最終確認。\n","permalink":"https://blog.mklee.org/posts/openclaw-memory-architecture/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e當你給一個 AI agent 一台 VPS、一堆 API key、和一個空白的工作區，它要怎麼「記住」東西？\u003c/p\u003e\n\u003cp\u003e這篇記錄了我在 \u003ca href=\"https://openclaw.ai\"\u003eOpenClaw\u003c/a\u003e 上建構 AI agent 記憶系統的過程——從最初的空白 \u003ccode\u003eMEMORY.md\u003c/code\u003e，到現在帶有優先級標籤、自動過期清理、事件時間軸的結構化架構。不是教學文，而是真實的踩坑記錄。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"第零天空白的開始\"\u003e第零天：空白的開始\u003c/h2\u003e\n\u003cp\u003eOpenClaw 啟動時，工作區裡有四個檔案：\u003ccode\u003eSOUL.md\u003c/code\u003e（人格設定）、\u003ccode\u003eUSER.md\u003c/code\u003e（使用者資訊）、\u003ccode\u003eAGENTS.md\u003c/code\u003e（行為規範）、和一個空的 \u003ccode\u003eMEMORY.md\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003eAgent 每次醒來（新 session）都是失憶狀態——它只知道人格和行為規範。所有對話、決策、偏好，隨著 session 結束就消失了。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第一個問題：agent 要怎麼知道「上次我們聊到哪了」？\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"第一階段daily-files流水帳\"\u003e第一階段：Daily Files（流水帳）\u003c/h2\u003e\n\u003cp\u003e最自然的做法：每天一個 markdown 檔，如 \u003ccode\u003ememory/2026-01-26.md\u003c/code\u003e、\u003ccode\u003ememory/2026-01-27.md\u003c/code\u003e，記錄當天發生的事。\u003c/p\u003e\n\u003cp\u003e格式很自由——\u003ccode\u003e## 標題\u003c/code\u003e 分段，裡面就是對話摘要、設定紀錄、debug 過程。Agent 在每次 session 開始時讀今天和昨天的 daily file，大概知道最近在幹嘛。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e好處：\u003c/strong\u003e 簡單、自然、寫入無摩擦。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e壞處：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e兩週前的事？要翻十幾個檔案。\u003c/li\u003e\n\u003cli\u003e「上次 OpenClaw 升級是什麼時候？」→ 沒人記得在哪個 daily file 裡。\u003c/li\u003e\n\u003cli\u003e重要決策淹沒在日常瑣事中。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"第二階段memorymd策展式長期記憶\"\u003e第二階段：MEMORY.md（策展式長期記憶）\u003c/h2\u003e\n\u003cp\u003eDaily file 是流水帳，\u003ccode\u003eMEMORY.md\u003c/code\u003e 是策展。\u003c/p\u003e\n\u003cp\u003e想法很簡單：把真正重要的東西從 daily file 「升級」到 \u003ccode\u003eMEMORY.md\u003c/code\u003e。Agent 每次啟動都讀這個檔案，等於它的「核心記憶」。\u003c/p\u003e\n\u003cp\u003e最初的 \u003ccode\u003eMEMORY.md\u003c/code\u003e 很簡單：分成 Profile（基本資料）、Infrastructure（系統設定）、Preferences（偏好）三個區塊，各放幾條 bullet point。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e問題來了：誰來維護？\u003c/strong\u003e\u003c/p\u003e","title":"OpenClaw 記憶管理：從零到自迭代的架構演化"},{"content":"我是 Mark。\n這裡記錄我跟 AI agent 一起實驗的各種東西：OpenClaw 的記憶系統、espresso 萃取參數、數位工具鏈、和其他亂七八糟的嘗試。\n大部分文章是我和 AI 協作完成的——我提供方向和經驗，它負責整理和發布。\n","permalink":"https://blog.mklee.org/about/","summary":"\u003cp\u003e我是 Mark。\u003c/p\u003e\n\u003cp\u003e這裡記錄我跟 AI agent 一起實驗的各種東西：OpenClaw 的記憶系統、espresso 萃取參數、數位工具鏈、和其他亂七八糟的嘗試。\u003c/p\u003e\n\u003cp\u003e大部分文章是我和 AI 協作完成的——我提供方向和經驗，它負責整理和發布。\u003c/p\u003e","title":"關於"},{"content":"Mark Lee 寫 AI agent、喝 espresso、折騰各種數位工具。\n這裡記錄的是實作過程中的真實經驗——踩過的坑、做過的決策、還沒想通的東西。不是教學文，比較像工程日誌。\n聯絡 Email: mark@mklee.org GitHub: kindomLee 有想法、發現錯誤、或想聊聊文章裡提到的東西，直接寄信就好。\n","permalink":"https://blog.mklee.org/about/","summary":"about","title":"關於"}]