Lily

Lily

发布于 7 月 23 日 • 最初发布于 dev.to

快速提问:在 ~/.claude/agents/ 中定义的子代理,这个月实际被调用过的到底有多少?我无法告诉你——直到我开始统计,而今天早上的统计结果显示有 44 个代理在 30 天内一次都没有被调用过。这篇文章是我「Claude Code 环境」系列的一部分(接续 自动终端窗口平铺),延续了防止配置膨胀的主题。我将用真实代码,逐步介绍一个通过 Stop 钩子将子代理调用记录到 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回リスト)

  手動 or cron
   └─ agents-index.sh → INDEX.md 自動再生成

Enter fullscreen mode Exit fullscreen mode

步骤 1:使用 Stop 钩子记录到 JSONL

~/.claude/hooks/stop_agent_tracker.sh 在会话结束时运行。Stop 钩子通过 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"

Enter fullscreen mode Exit fullscreen mode

Python 部分的核心是对 transcript 进行两遍解析。

# 第1パス: 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")))

Enter fullscreen mode Exit fullscreen mode

注意:在 Claude Code 的 transcript 中,「Task」工具被记录为 name="Agent"subagent_type 存在于 input.subagent_type 中。代码本身也有相同的注释。

为防止重复写入,会跳过相同的 session_id + tool_use_id 组合(因此即使 Stop 钩子在一次会话中多次触发,也不会写入重复记录)。

单条 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"}}

Enter fullscreen mode Exit fullscreen mode

截至目前,已累计 546 条记录(约 176 KB)。

步骤 2:在 7d/30d 窗口内生成 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はこれ)

Enter fullscreen mode Exit fullscreen mode

内部处理分为三步。

# ① JOSNLをロードしてウィンドウでフィルタ
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()))

Enter fullscreen mode Exit fullscreen mode

这是今天早上的实际输出(~/.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
  ...

Enter fullscreen mode Exit fullscreen mode

在 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()

Enter fullscreen mode Exit fullscreen mode

生成的 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... | * |
...

Enter fullscreen mode Exit fullscreen mode

使用 --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>

Enter fullscreen mode Exit fullscreen mode

它运行的命令很简单。

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

Enter fullscreen mode Exit fullscreen mode

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"

Enter fullscreen mode Exit fullscreen mode

我的决策标准如下。

状态 操作
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 钩子在一次会话中多次触发,导致重复写入 → 通过在 (session_id, tool_use_id) 上添加 seen_ids 重复检查来修复
  • 出现了 subagent_type 为空字符串的记录 → 使用 if r.get("subagent_type") 进行过滤(空字符串为假值就足够了)
  • 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 钩子按 session_id + tool_use_id 追加到 JSONL → 调用历史跨会话累积
  • agent-usage-summary.sh 在 7d/30d 窗口内输出 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*