第一篇第二篇我們把一個 agent 的骨架搭起來了:核心迴圈會呼叫模型、執行工具、把結果塞回對話,然後再問一次模型。這套流程處理「幫我讀這個檔、改那一行」綽綽有餘。但只要任務一變大,你自己在 Claude Code 裡大概都體驗過那個場景:它跑到一半開始鬼打牆,忘了三步前答應要做的事,或是對話長到某個點突然變慢、變貴,最後乾脆漏掉一半需求。

模型沒變笨,它只是扛不住了。大任務會從兩個方向壓垮一個 agent:一是沒有一份看得見的計畫,走著走著就迷路;二是 context window 被塞爆,該記住的東西被無關的雜訊擠出去。這一篇要講的 Layer 2,就是專門解決「扛不住」的四招:planning & todos、subagents、skills、context management。它們的共同目標只有一句話:讓 agent 在面對真正複雜的工作時,還能保持頭腦清醒。

第一招:planning & todos,先把大任務拆成 ticket

你接到一個橫跨十幾個檔案的重構,第一件事會做什麼?不會是打開第一個檔案就開始改,而是先在 Jira 上開一串 ticket,把大目標拆成一條條可勾選的待辦,然後一張一張推進。planning 這一招做的就是這件事,只是把白板換成了 agent 的工作記憶。

它其實同時解掉兩個獨立的問題。第一個是執行時需要一份當前的檢查清單。如果計畫只留在某一輪的 prompt 裡,經過十幾個工具結果之後,那段文字早就被推到對話深處,模型很容易失去頭緒。第二個是理解任務之前不該動手,你不會希望它還沒搞懂需求就開始亂改檔案。

Claude Code 用兩個工具對應這兩件事。第一個是 TodoWrite:模型每次都覆寫一整份結構化的待辦清單,每個項目帶著 contentstatus(pending / in_progress / completed)跟 activeForm。這個工具刻意設計成沒有任何副作用:它不碰檔案、不跑 shell,只更新 session 裡的清單狀態,所以從權限的角度看它是唯讀的,永遠直接放行。你在 Claude Code 畫面上看到那個會即時打勾的待辦列表,背後就是它。

第二個是 plan mode。進入之後整個 session 變成唯讀:模型可以到處讀檔、探索、寫出一份計畫,但不准編輯任何東西,直到它呼叫 ExitPlanMode 把計畫交出來、你按下核准。這個核准動作是關鍵的一次握手。權限層在你點頭之前,會把所有寫入操作一律擋掉。等於強迫 agent 先想清楚再動手。

踩雷提醒: 別對小任務過度規劃。幫一個單步驟的工作列出五項待辦,只會增加雜訊、拖慢節奏。瑣碎的事直接做。另一個常見毛病是「清單過時」:模型做完事卻忘了把項目關掉,你會看到一堆卡在 in_progress 的假進度,這時要提醒它保持清單同步。還有,這份記憶體裡的待辦清單只活在當前 session,一旦工作要跨越好幾個回合甚至重開才能完成,它就會隨 context 消失,那種情況得換成有磁碟後盾的持久化任務系統(後面的篇章會談)。

第二招:subagents,把子任務外包,只收結論不收過程

假設你在查一個 bug,需要先搞清楚某個第三方套件的行為。你會怎麼做?大概是找個熟這塊的同事:「幫我確認這個 API 在併發下會不會出事,查完給我結論就好。」你要的是那句結論,不是他翻過的二十頁文件。subagent 就是這套外包模式。

一個 subagent 本質上就是「在一次 tool call 裡跑起來的另一個 agent 迴圈」。parent 給它一段 prompt,它拿到全新的 messages[],從零開始一路跑到完成,然後只把最終那段文字回傳給 parent。它中間讀過的檔案、跑過的指令、走過的死路,全部隨著它的 transcript 一起丟掉。骨架長這樣:

1
2
3
4
5
6
7
8
def agent_tool(model, child_registry, parent_session):
def spawn(a):
# child 從全新的 Session 起跑,繼承 parent 的權限但不繼承 parent 的對話歷史
child = Session(mode=parent_session.mode, # 繼承權限模式
allow_rules=set(parent_session.allow_rules)) # 繼承 allow 規則
messages = [{"role": "user", "content": a["description"]}] # child 的對話從這段委派任務開始
return run_turn(messages, model, child_registry, child) # 跑同一套迴圈,最後只回傳文字
return Tool("Agent", spawn, is_read_only=True)

效果就是 context 隔離:那些旁支調查的雜訊,完全不會污染 parent 的主對話。parent 保持聚焦在大局,只吸收乾淨的結論。這正是你在 Claude Code 裡派 Explore agent 去掃 repo、或派 general-purpose agent 去查一個獨立問題時發生的事,你的主對話不會被塞進它讀過的三十個檔案,只會收到一份摘要。Claude Code 內建了好幾種 persona(general-purpose、explore、plan、verification 等),透過 subagent_type 挑選;背景執行的 subagent 甚至可以之後用 SendMessage 續跑。

這裡有兩個容易誤會的點要講清楚。第一,context 隔離不等於權限隔離。child 會複製 parent 的權限模式跟 allow 規則,該過的權限閘門一個都不能少,別因為它是獨立 context 就以為它比較安全。第二,多數 demo 會刻意不把 Agent 工具給 child,避免 child 又生 child,無上限遞迴下去。

踩雷提醒: subagent 不是免費的。每派一個出去就是一次完整的迴圈,濫用只會讓整體變慢又變貴。單純讀一兩個已知路徑的檔案,主對話自己做就好,犯不著外包。另一個風險是摘要壓縮過頭:child 可能把關鍵細節也一起砍掉,parent 拿到太單薄的結論,只好再問一次或回頭去讀 child 寫的檔案,反而更慢。解法是在派工時就要求它把重要發現寫到磁碟上,而不是全靠回傳的那段文字。

第三招:skills,需要時才 import 的能力函式庫

你寫程式不會把所有用得到的函式庫全 import 進每一個檔案的開頭,那樣編譯又慢、命名空間又髒。你是需要哪個才 import 哪個。skill 對 agent 來說就是這個道理:它是一包自成一體的專長(一套指令,外加會用到的 script 和參考檔案),平常安安靜靜躺著,只在某個任務真的需要時才載入。

問題的核心在於 context 成本。如果把每一個 skill 的完整內容都塞進 system prompt,光開場就先燒掉大把 token,而且一堆觸發詞會互相干擾,模型反而選不出該用哪個。skill 系統用 progressive disclosure(漸進揭露)三層來解這件事:

  • L1 目錄(catalog):只放每個 skill 的 namedescription 跟路徑,成本極低,每一輪都待在 system prompt 裡讓模型知道「有哪些能力可用」。
  • L2 指令本文SKILL.md 的正文。只有當模型判斷這個任務需要它,才會用一般的 Read 工具去讀那個檔,內容這時才進入對話。
  • L3 資源檔:skill 資料夾裡的額外 script 或參考資料,只有指令指到它時才會被讀進來。

漂亮的地方在於,它根本不需要什麼專門的機制:目錄列出路徑,載入 skill 就只是「讀一個檔案」而已。一個 SKILL.md 的結構大概長這樣:

1
2
3
4
5
6
7
8
9
10
---
name: pdf-table-extract
description: 從 PDF 抽取表格並轉成 CSV。當使用者提到「解析 PDF」「抽表格」時載入。
allowed-tools: Read, Bash
---

# 抽取步驟
1. 用 scripts/extract.py 把指定頁面轉成純文字
2. 表格區塊交給 scripts/to_csv.py,輸出到 out/ 目錄
3. 回報產出的 CSV 路徑與列數

那段 frontmatter 就是進到 L1 目錄的內容,底下的正文則是 L2、被指到的 scripts/ 是 L3。這跟你在 Claude Code 裡看到的 Skill 一模一樣:你打 /skill名 或它自己判斷該用時,才會把本文載入,平常只有那行描述佔著位子。Claude Code 的 frontmatter 還支援 when_to_usecontext: 'fork'(在 forked subagent 裡跑這個 skill)、paths(碰到符合的檔案時被動啟用)等欄位,能力更完整。更進階的系統(README 裡拿 Hermes 當例子)甚至會讓 skill store 自己演化:把做過的工作流程沉澱成新 skill、用 .usage.json 記錄使用頻率、由一個背景 curator agent 定期封存沒人用的舊 skill。

踩雷提醒: skill 最大的兩個坑都出在 description。寫太含糊,模型永遠不會載入它——描述要寫成「帶觸發條件的形狀」,明確講出「什麼情況該用我」。寫太廣,則會到處誤觸發,該用 A 的時候硬是載入了 B。第二個坑是目錄膨脹:skill 一多就把 prompt 擠爆,反而害了原本想省的 context,所以每個 skill 都要保持聚焦,並讓 loader 做裁剪。

第四招:context management,記憶體不夠時的分頁與快取

前面三招都在「產生」內容(計畫、子任務結論、載入的 skill 本文),它們全都會累積到 messages[] 裡。每一個工具結果、每一次模型回覆、每一輪使用者輸入,對話都在長大。長到某個點就會撞上模型的 context limit,然後三件事會同時發生:API 可能直接拒絕請求、每次呼叫變得更慢更貴、舊的沒用內容開始跟當前任務的關鍵資訊搶空間。

這一招就是作業系統在記憶體不夠時做的事:分頁、換出、快取。差別在於它有一套分層順序,從最不痛的做起,最傷的留到最後:

  • budget:把超大的工具結果先寫到磁碟,對話裡只留一段 preview 加一個像路徑的標記。這步幾乎不損失資訊。
  • snip / micro:丟掉中間那些過時的回合、只留開頭跟最近的尾端;把舊的工具結果本體換成一個 stub 佔位。成本低、大致無損。
  • auto:真的還是太長,才呼叫一次模型,把整段歷史摘要成一則訊息。這步要花一次 model call,而且可能遺失細節,是最後手段。
  • reactive:萬一上面全做完還是收到 prompt_too_long,才做一次有界限的裁掉開頭再重試,並設一個重試上限避免無限迴圈。

順序為什麼重要?舉個例子:大型工具結果一定要先持久化到磁碟,之後的 pass 才能安心把它的本體換成 stub,順序反了就會真的丟資料。這也是為什麼 context management 是唯一一招動到核心迴圈本身:它必須在每次 model call 之前跑,把對話壓進 window 才送出去。你在 Claude Code 裡看到的 auto-compact、或是那個「壓縮對話」的動作,就是這整套 reducer 在運作;被搬到磁碟的檔案內容,之後需要時還能在一定的 token 預算內被讀回來。

踩雷提醒: 壓縮是會丟資訊的,尤其是走到 auto 摘要那一步。模型之後可能會用到的細節,很可能在摘要時被省略掉,所以策略上要盡量靠前面幾層低成本、無損的 reducer 撐住,真正完整的輸出則持久化到檔案、需要時重讀,而不是全押在摘要上。另一個技術性的坑是:壓縮時千萬不能把一個 tool_use 跟它配對的 tool_result 拆開,配對斷了 API 會直接報錯。

四招怎麼搭在一起

這四招不是各玩各的,它們在一次大任務裡是接力的:planning 把任務拆成清單、定義出邊界;遇到獨立的子問題就丟給 subagent 處理,把雜訊擋在主對話之外;需要特定專長時才載入對應的 skill,而不是把全部能力扛在身上;而 context management 在整段過程中默默回收空間,讓對話撐得夠久、跑得完。它們共同的設計哲學,就是這個系列一開始講的那句話:模型負責推理與決策,harness 負責提供工具、狀態與限制。

招式 解決什麼 Claude Code 對應
planning & todos 大任務迷路、太早動手 TodoWrite 待辦清單、plan mode / ExitPlanMode 核准握手
subagents 旁支調查污染主對話 Agent 工具、explore / general-purpose 等 persona
skills 能力全塞會擠爆 prompt SKILL.md 加 progressive disclosure、/skill 載入
context management 對話長到撞 context limit auto-compact、budget / micro / reactive reducer

有了這四招,你的 agent 已經能扛住相當大的任務而不會中途崩掉。但它還有一個明顯的短板:這一切都活在單一 session 裡。session 一結束,計畫、學到的東西、做到一半的進度,全部歸零;跑到一半如果 API 斷線或工具出錯,它也不知道怎麼優雅地爬起來。下一篇(第 4 篇)就要補上這塊,也就是記憶與韌性:怎麼讓 agent 把該記住的東西寫進跨 session 的長期記憶,又怎麼在錯誤發生時穩住、重試、接著跑,而不是整局重來。我們下篇見。