本週主題:把「大量搜尋、讀檔」的工作交給一個唯讀的 subagent,主對話只收到摘要,既保護 context,也用工具白名單擋住誤改檔案。
1. 問題
你請 AI 編程助理回答「session 到期是在哪裡判斷的」。助理會先 grep、再開十幾個檔案、再讀測試,最後才回答。這些搜尋過程的輸出全部留在同一個對話裡。context window(模型一次能看到的文字上限)被這些中間結果占滿之後,真正重要的需求與決策反而被擠到後面,模型更容易忘記前面的約定,也更貴、更慢。
第二個問題是安全。你只想「查」,但同一個助理手上有寫檔、跑 shell 的權限。一句含糊的指示,就可能讓它順手「修好」某個檔案。
兩個問題的共同原因是:探索(exploration)與實作(implementation)用了同一個 context、同一組權限。
2. 原理
這個方法叫做 context 隔離的委派(delegation),不綁定任何工具。核心只有三件事:
- 獨立的 context window。subagent(子代理,由主代理啟動的另一個模型對話)有自己的對話歷史。它搜尋、讀檔產生的大量中間輸出,只存在它自己的 context 裡。
- 只回傳摘要。主代理收到的是一段結論,例如「檔案路徑加行號加一句說明」。主對話因此維持乾淨,後續推理不會被雜訊干擾。
- 權限收斂到任務所需。探索型的子代理只需要「讀」。把它能用的工具限制在讀取類,它就算被指示去改檔案,也沒有手可以改。
要讓這三件事成立,有兩個設計重點。第一,輸出格式要在子代理的 system prompt 裡寫死(例如「最多 5 行,附路徑與行號,不貼程式碼」),否則子代理可能回傳一大段原始碼,隔離就白做了。第二,description(說明欄)要寫清楚「什麼時候該委派給它」,因為主代理就是靠這段文字決定要不要委派。
成本面也要先講清楚:每次委派都是另一個模型對話,要多花 token 與時間。所以這個方法適合「搜尋範圍大、結果可以壓縮成幾行」的工作;如果只是讀一個你已經知道路徑的檔案,直接讀比較省。
3. 在 Claude Code 實作
Claude Code 把子代理定義成一個 markdown 檔,放在專案的 .claude/agents/,或個人的 ~/.claude/agents/。官方文件指出:只有 name 與 description 是必填;欄位名稱是 camelCase,必須完全吻合,因為「Claude Code ignores unrecognized fields without reporting an error」,打錯欄位不會有任何錯誤訊息。
- 建立
.claude/agents/code-explorer.md:用tools只給 Read、Glob、Grep;model用 haiku 讓搜尋更便宜;description 寫「use proactively」鼓勵主代理主動委派。 - 準備三個小檔案當測試用的程式碼庫(
src/auth.py、src/billing.py、src/api.py)。 - 測試 A:請主代理使用 code-explorer 查「session 到期在哪裡判斷」。
- 測試 B:請主代理指示 code-explorer 去修改
SESSION_TTL,驗證唯讀限制真的生效。 - 測試 C:用
--output-format stream-json確認主代理確實呼叫了 Agent 工具並指定這個子代理。
完整的子代理檔案如下(.claude/agents/code-explorer.md):
---
name: code-explorer
description: Read-only code explorer. Use proactively to find where something is implemented and report file paths with line numbers. Never modifies files.
tools: Read, Glob, Grep
model: haiku
---
You are a read-only code explorer. Search the repository, then reply with at most 5 lines:
the relevant file paths with line numbers and one sentence on what each does.
Do not paste code. Do not attempt to modify any file.
三個測試用檔案如下。src/auth.py:
import time
SESSION_TTL = 3600
def create_session(user_id):
return {"user": user_id, "expires": time.time() + SESSION_TTL}
def is_expired(session):
return session["expires"] < time.time()
src/billing.py:
def charge(user_id, cents):
if cents <= 0:
raise ValueError("amount must be positive")
return {"user": user_id, "charged": cents}
src/api.py:
from auth import create_session, is_expired
from billing import charge
def login(user_id):
return create_session(user_id)
def pay(session, cents):
if is_expired(session):
raise PermissionError("session expired")
return charge(session["user"], cents)
測試 A:
$ cd ~/demo && claude -p "Use the code-explorer subagent to find where session expiry is decided and which function calls it. Then tell me, in one sentence, which subagent you used and what it returned." --model sonnet --setting-sources project --strict-mcp-config --disable-slash-commands --allowedTools "Read,Write,Edit"
I used the `code-explorer` subagent, and it found that `is_expired(session)` (`src/auth.py:10-11`) decides session expiry by checking `session["expires"] < time.time()`, and that its only caller is `pay(session, cents)` (`src/api.py:9-12`), which raises `PermissionError("session expired")` when it returns true.
主代理的回答只有路徑、行號與一句說明,搜尋過程沒有進入主對話。這正是第 2 章說的「只回傳摘要」。
測試 B(唯讀限制):主代理的提示是「Use the code-explorer subagent. Instruct it to change SESSION_TTL in src/auth.py from 3600 to 7200 and report back what happened. Do not edit any file yourself. Then state whether src/auth.py was modified (check with Read).」輸出與事後檢查:
The code-explorer subagent did not make the change, and `src/auth.py` was not modified. A Read of the file shows `SESSION_TTL = 3600` on line 3.
The subagent only has read-only tools (Read, Glob, Grep). It ran one Grep, found the line, and reported that it could not edit. …
$ grep -n SESSION_TTL src/auth.py
3:SESSION_TTL = 3600
7: return {"user": user_id, "expires": time.time() + SESSION_TTL}
檔案沒被改動。這個限制來自 tools 白名單,不是靠 prompt 裡的「請不要修改」。要注意:這個測試的主代理自己也被禁止編輯,所以它證明的是「子代理沒有寫入工具」這件事,不是主代理的行為。
測試 C(確認委派真的發生):用 stream-json 輸出,只取工具呼叫的名稱與前段參數:
Agent {"description":"Find charge amount validation","subagent_type":"code-explorer","prompt":"Find where charge amounts are validated in the codebase at ~/demo …
Grep {"pattern":"(?i)(charge|payment|amount).*(valid|positive|min|max|…
Grep {"pattern":"(?i)amount","path":"~/demo","output_mode":"files_with_matches"}
Read {"file_path":"~/demo/src/billing.py"}
…
第一行是主代理呼叫 Agent 工具、指定 subagent_type 為 code-explorer;後面出現的都是 Grep 與 Read,沒有任何 Write 或 Edit。
測試環境:Claude Code 2.1.302,預設設定(不含個人 plugins、hooks、CLAUDE.md),測試日期 2026-10-19。每個測試各跑一次,結果可能因模型輸出而略有不同。
⚠️ 未實測:isolation: worktree、background: true、memory、子代理巢狀深度(CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH)。以上只引用官方文件,沒有在這次測試中執行。
另外,文件說明 Claude Code 內建了 Explore 子代理:「A fast, read-only agent optimized for searching and analyzing codebases」,Write 與 Edit 都被拒絕。如果你只需要通用的唯讀搜尋,不必自己寫,內建的就夠用;自訂的好處是你能固定輸出格式、模型與工具範圍。
4. 搬到其他工具
這個方法不依賴 Claude Code。我查了另外三個工具的官方文件,各自對應的做法如下。
- Cursor:子代理放在
.cursor/agents/(文件也列出.claude/agents/與.codex/agents/)。frontmatter 有readonly欄位:「Iftrue, the subagent runs with restricted write permissions (no file edits, no state-changing shell commands)」。文件說「Each subagent has its own context window」,且「The parent only sees the final summary」。我在文件裡沒有看到可以逐一指定工具的白名單欄位,只有readonly這個開關。(官方文件) - OpenAI Codex:自訂代理是放在
.codex/agents/的 TOML 檔,必填name、description、developer_instructions。重點差異是:子代理繼承父代理的 sandbox,「A custom agent file can't replace those settings」,sandbox_mode等欄位放在代理檔裡不會覆蓋父設定;文件並說自訂指令「don't create a separate read-only sandbox」。也就是說,Codex 的子代理檔可以隔離 context,但不能用它來收窄權限,要收窄權限得改父層的 sandbox。(官方文件) - Gemini CLI:子代理是
.gemini/agents/*.md,frontmatter 的tools是可選清單,「If omitted, it inherits all tools from the parent session」,並支援*、mcp_*、mcp_server_*萬用字元。文件寫明「subagents cannot call other subagents」,也就是不能巢狀。該頁面同時註明 Gemini CLI 已在 2026 年 6 月 18 日被 Antigravity CLI 取代,所以細節請以你實際使用的版本為準。(官方文件)
三個工具的共同點是:都有「獨立 context、回傳摘要」。差別在「怎麼限制權限」:Claude Code 與 Gemini CLI 用工具白名單,Cursor 用 readonly 開關,Codex 的代理檔則不能改權限。選工具或寫團隊規範時,這個差別比語法差異更重要。
5. 限制與代價
- 多花 token。Codex 文件明說「subagent workflows consume more tokens than comparable single-agent runs」。短任務不要委派。
- 子代理看不到你的對話。Claude Code 文件說非 fork 的子代理「doesn't see your conversation history」,Cursor 文件也說「Subagents start with a clean context」。主代理必須把需要的資訊寫進委派的 prompt,漏寫就會查錯方向。
- 摘要會損失細節。子代理可能漏看檔案,也可能被環境雜訊干擾。測試 C 的最終回答就提到,另一處含有 "amount" 的是同一個目錄裡的輸出紀錄檔,因為輸出檔也被搜尋到了。重要結論請回頭開原始檔確認。
- 打錯欄位不會報錯。見第 3 章:欄位名稱必須完全吻合,否則會被靜默忽略。如果你寫成錯誤的欄位名,白名單可能根本沒生效(子代理在省略
tools時會繼承所有可用工具)。請用像測試 B 那樣的「嘗試越權」來驗證。 - 工具白名單不等於完整防護。它擋的是「有沒有這個工具」。若你給了 Bash,子代理就可能用 shell 寫檔。探索型代理請只給讀取類工具。
- 委派不保證發生。主代理是看 description 決定的;想要穩定觸發,就在提示裡直接點名子代理,或在 description 加入「use proactively」。
- 本文的測試規模很小。三個檔案的程式碼庫、每個測試只跑一次,只能證明機制,不能代表大型專案的成效與 token 節省量。
6. 延伸閱讀
- Create custom subagents(Anthropic,Claude Code 官方文件)
- Claude Code power user tips(Anthropic,頁面標示「Updated this week」,無明確日期)
- Subagents(Cursor,官方文件)
- Subagents(OpenAI,官方文件)
- Subagents(Gemini CLI,官方文件)
📌 本週短訊
- How the Claude Code team does code review(Anthropic Claude Code 團隊,2026-10-15)
做法是分層:先在開 PR 前由 Claude 跑測試、build、linter 並貼出輸出,再由 subagent 在新的 context 審 diff;PR 上再由多個 agent 並行找 bug,另一組投票,只有多數同意的發現才會留言;人工審核的人數依變更風險分級(低 0 人、中 1 人、高 2 人)。
值得看:把「有沒有人讀過 code」換成「這次變更可能出什麼錯,該由哪一種檢查來擋」,可以直接拿去設計自己的審核流程。
- Claude dynamic workflows(flows)(Latent Space,2026-10-12)
主代理先規劃,再寫程式去啟動多個 agent、收集結果並轉送資訊,不必事先設定固定的子代理名單;工作被切成「discrete, verifiable units」。文中建議設定 session 預算、保持預設 agent 數量低,並在 Claude Console 的圖形檢視中檢查主代理交給每個子代理的內容。
值得看:這是本週主題「委派」的下一步,但成本較高,文中也建議只用在大型任務。
Claude Code 更新
| 版本 | 變更 | 對讀者的影響 |
|---|---|---|
| 2.1.302 | fork subagents(subagent_type: "fork")在 claude -p、Agent SDK、desktop app 與 VS Code 預設開啟,終端機原本就已開啟;CLAUDE_CODE_FORK_SUBAGENT=0 可關閉。桌面 app 與 VS Code 的主對話子代理一律在背景執行 | 用 claude -p 或 SDK 跑自動化的人,行為會變:fork 子代理現在可能被用到;不想要請設環境變數 |
| 2.1.302 | prompt 與 agent hooks 同一事件最多同時執行 12 個,其餘排隊 | 掛很多 hooks 的人,執行順序與延遲可能改變 |
| 2.1.296 | 子代理 frontmatter 與 --agents 定義新增 autoCompactWindow,子代理可以比主對話更早自動壓縮 | 長時間執行的子代理可以用它控制 context 大小 |
| 2.1.295 | 子代理最多預載 32 個 skills 欄位中的 skill,每個只載入一次;有 Skill 工具的子代理仍可呼叫其餘的 | 預載的 skill 清單過長時,超過 32 個的不會預載 |
| 2.1.292 | Agent 工具新增 effort 參數,Claude 可依你的要求用指定的 effort 等級執行子代理 | 可以要求探索用低 effort、困難的分析用高 effort |