Lily

Lily

發佈於 7月 23日 • 最初發表於 dev.to

快速問答:您在 ~/.claude/agents/ 中定義的子代理程式,實際上有多少個在本月被呼叫過?我之前也無法回答這個問題——直到我開始測量,而今天早上的統計顯示有 44 個代理程式在 30 天內甚至一次都沒有被呼叫。這篇文章是我「Claude Code 環境」系列的一部分(接續 自動終端視窗排列),延續保持設定不臃腫的主題。我將以真實程式碼逐步說明如何透過 Stop hook 將子代理程式呼叫記錄到 JSONL,並使用每日報告來清除這些殭屍代理程式。

問題:定義與使用的落差

在 Claude Code 中新增代理程式只要在 .claude/agents/*.md 中撰寫 frontmatter 即可。因為太過簡單,「先定義看看」的代理程式就會不斷堆積。問題是沒有辦法檢查代理程式是否真的被使用。

  • 沒有跨工作階段累計呼叫次數的機制
  • 手寫的 INDEX.md 會立刻過時
  • 沒有依據來判斷「能不能刪除」

使用次數為零的代理程式仍會佔用啟動時注入的上下文。置之不理就會變成殭屍:已定義但未使用 → 不清楚是否有效 → 又無法刪除。

整體流程

這個系統由三個元件組成。

工作階段結束
  └─ Stop hook (stop_agent_tracker.sh)
       └─ 解析 transcript.jsonl → 追記到 agent-invocations.jsonl
                                              │
                     每天 10:15 launchd ──────┘
                       └─ agent-usage-summary.sh 7d 30d
                            └─ agent-usage-latest.md(Top10 + 0次列表)

  手動或 cron
   └─ agents-index.sh → INDEX.md 自動重新產生

進入全螢幕模式 離開全螢幕模式

步驟 1:使用 Stop hook 記錄到 JSONL

~/.claude/hooks/stop_agent_tracker.sh 會在工作階段結束時執行。Stop hook 會從 stdin 接收工作階段資訊和 transcript_path,因此腳本會解析該 transcript 並挑出 Agent 工具的呼叫。

# stop_agent_tracker.sh(摘錄)
# stdin: {"session_id":"...","transcript_path":"...","hook_event_name":"Stop",...}
# 輸出:  ~/.claude/logs/agent-invocations.jsonl

OUT_LOG="$LOG_DIR/agent-invocations.jsonl"

進入全螢幕模式 離開全螢幕模式

Python 部分的精髓在於對 transcript 的兩階段解析。

# 第一階段: 將 tool_use (name="Agent") 與 tool_result 建立索引
uses = {}    # id -> (ts, name, input, caller)
results = {} # tool_use_id -> (ts, is_error)

for b in content:
    if btype == "tool_use" and b.get("name") == "Agent":
        inp = b.get("input") or {}
        if "subagent_type" not in inp:
            continue
        uses[uid] = (ts, b.get("name"), inp, b.get("caller"))
    elif btype == "tool_result":
        results[rid] = (ts, bool(b.get("is_error")))

進入全螢幕模式 離開全螢幕模式

注意:在 Claude Code 的 transcript 中,「Task」工具會被記錄為 name="Agent"subagent_type 存在於 input.subagent_type。此說明也出現在程式碼本身中。

為避免重複,相同的 session_id + tool_use_id 組合會被跳過(因此即使 Stop hook 在同一個工作階段多次觸發,也不會寫入重複的記錄)。

單一 JSONL 記錄看起來像這樣。

{"ts": "2026-05-28T16:27:41.766Z", "session_id": "TEST-AGENT-TRACKER-001",
 "cwd": "~", "tool_use_id": "toolu_014MM...", "subagent_type": "general-purpose",
 "description": "launchd + cron 總稽核", "duration_ms": 177, "status": "ok",
 "caller": {"type": "direct"}}

進入全螢幕模式 離開全螢幕模式

目前已累積 546 筆記錄(約 176 KB)。

步驟 2:產生 7 天/30 天視窗的 Top 10 與零呼叫清單

~/.claude/scripts/agent-usage-summary.sh 負責聚合處理。它是內嵌的 python3,不需額外函式庫。

# 用法
agent-usage-summary.sh           # 預設 7d
agent-usage-summary.sh 30d       # 30 天
agent-usage-summary.sh 7d 30d    # 兩者(launchd 使用此設定)

進入全螢幕模式 離開全螢幕模式

內部處理分為三個步驟。

# ① 載入 JSONL 並依視窗篩選
cutoff = now - td
recent = [r for r in records if r["_dt"] >= cutoff]

# ② 依 subagent_type 計數 + 錯誤數
counts = Counter(r.get("subagent_type", "") for r in recent if r.get("subagent_type"))
errors = Counter(r.get("subagent_type", "") for r in recent if r.get("status") == "error")

# ③ 與 ~/.claude/agents/*.md 的檔名清單比對,找出未使用的代理程式
known_agents = set()
for fp in glob.glob(os.path.join(agents_dir, "*.md")):
    known_agents.add(os.path.splitext(os.path.basename(fp))[0])

unused = sorted(known_agents - set(counts.keys()))

進入全螢幕模式 離開全螢幕模式

以下是今天早上的實際輸出(~/.claude/logs/agent-usage-latest.md)。

=== Agent usage (last 7d) ===
total invocations: 127  unique types: 5

Top 10:
  agent                                     calls  errors
  general-purpose                              76       0
  Explore                                      45       0
  Content Creator                               2       0
  fork                                          2       0
  reviewer                                      2       0

0-call agents (defined locally but not used in 7d): 47
  - a11y-architect
  - architect
  - build-error-resolver
  - code-architect
  - code-explorer
  - code-reviewer
  ...

進入全螢幕模式 離開全螢幕模式

在 7 天內,只有 5 種代理程式類型被呼叫,47 種完全沒有被呼叫。即使在 30 天視窗中,仍有 44 種維持零呼叫。general-purposeExplore 占所有呼叫的 95%。

步驟 3:使用 agents-index.sh 自動重新產生 INDEX.md

要決定是否可以刪除某個代理程式,您需要跨領域檢視每個代理程式是為什麼而撰寫。agents-index.sh 會讀取 ~/.claude/agents/*.md 的 frontmatter 並建立 INDEX.md

# 簡易 frontmatter 解析器(不依賴 PyYAML)
m = re.match(r"^---\n(.*?)\n---\n", text, flags=re.DOTALL)
body = m.group(1)
for line in body.split("\n"):
    k, _, v = line.partition(":")
    out[k.strip()] = v.strip()

進入全螢幕模式 離開全螢幕模式

產生的 INDEX.md 看起來像這樣。

<!-- AUTO-GENERATED by ~/.claude/scripts/agents-index.sh — DO NOT EDIT MANUALLY -->
# Agents Index (51 agents · 2026-07-14 10:15)

| Name | Model | Description | Tools |
|------|-------|-------------|-------|
| `general-purpose` (general-purpose.md) | - | General-purpose agent for... | * |
...

進入全螢幕模式 離開全螢幕模式

使用 --json 旗標也會寫出 .index.json,這可以被像是成本追蹤器之類的程式重複使用。

缺少 frontmatter 中 namedescription 的檔案會被列在 ⚠️ Validation warnings 區段。

步驟 4:使用 launchd 產生每日報告

~/Library/LaunchAgents/com.shun.agent-usage-daily.plist 每天 10:15 執行。

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>10<integer>
    <key>Minute</key>
    <integer>15<integer>
<dict>

進入全螢幕模式 離開全螢幕模式

執行的指令很簡單。

<string>/bin/zsh -c
  ~/.claude/scripts/agent-usage-summary.sh 7d 30d
  > ~/.claude/logs/agent-usage-latest.md 2>&1<string>

進入全螢幕模式 離開全螢幕模式

PATH 會明確包含 nvm 和 Homebrew,因為 launchd 的預設 PATH 找不到 python3

有了這個設定,agent-usage-latest.md 每天早上 10:15 都會重新整理,因此我總是知道今天有多少代理程式已經 30 天沒有被呼叫。

操作例行程序:清除殭屍代理程式

每週一次,我會執行這些指令。

# 重新產生 INDEX(frontmatter 變更或新增後務必執行)
~/.claude/scripts/agents-index.sh

# 確認 0 次列表
cat ~/.claude/logs/agent-usage-latest.md | grep -A 9999 "0-call agents"

進入全螢幕模式 離開全螢幕模式

我的決策標準如下。

狀態 動作
30 天內 0 次呼叫,且閱讀描述後發現沒有實際使用案例 刪除
30 天內 0 次呼叫,但有未來的可能使用案例 保留,並在描述中明確說明使用條件
7 天內 0 次呼叫,30 天內有少數呼叫 可能是季節性工作 — 暫緩處理
Top 5 經常使用 重新檢視其權限和工具定義以進一步優化

在這次的清理中,我刪除了 14 個確認 30 天內零呼叫的代理程式,包括 a11y-architectdjango-reviewergo-build-resolver。它們會從 INDEX.md 中消失——其他都不會改變。

我遇到的陷阱

  • 篩選 name="Task" 沒有匹配到任何東西 → 在 Claude Code 的 transcript 中,Task 呼叫會被記錄為 name="Agent"。一開始因為這個原因,每筆記錄都回傳空值
  • Stop hook 在同一個工作階段多次觸發,造成重複寫入 → 透過在 (session_id, tool_use_id) 上加入 seen_ids 重複檢查來修正
  • 出現 subagent_type 為空字串的記錄 → 使用 if r.get("subagent_type") 進行篩選(空字串的 falsy 特性就足夠了)
  • agents-index.sh 解析了 INDEX.md 本身,造成迴圈 → 使用 if p.name == "INDEX.md": continue 排除
  • launchd 的最小 PATH 找不到 python3 → 在 plist 的 EnvironmentVariables 中明確設定 nvm 和 Homebrew 路徑
  • 30 天視窗中零呼叫的代理程式在 7 天視窗中看起來「不存在」known_agents 是從 agents_dir 的檔案列表建立,因此不依賴視窗。這是正確的行為

總結

  • Stop hook 會以 session_id + tool_use_id 為鍵追記到 JSONL → 呼叫歷史會跨工作階段累積
  • agent-usage-summary.sh 會輸出 7 天/30 天視窗的 Top 10 和零呼叫代理程式清單。由於它會交叉參照 ~/.claude/agents/*.md 中的檔名,任何新加入的代理程式都會自動被追蹤
  • agents-index.sh 會從 frontmatter 自動重新產生 INDEX.md。手寫的索引會立刻過時,因此這個機制取代了手寫索引
  • launchd 每天 10:15 執行聚合並寫入 agent-usage-latest.md,因此「今天有多少殭屍代理程式?」這個問題總是能得到答案
  • 確認 30 天內零呼叫的代理程式會被刪除。當刪除的依據來自數字時,就不需要再猶豫

下次,我將使用這些日誌資料來視覺化哪些代理程式會被組合用於哪種類型的工作。


作者 **Lily* — 我開發 iOS 應用程式,並使用 Claude Code 自動化我的內容工作流程。
追蹤我: 作品集 · X · GitHub*