3行で
- ログ 389 ファイルから委譲 138 件を取り出した。サブエージェントは 1 回の委譲あたり中央値 20 回ツールを呼び、合計 3,093 件のツール結果を読んでいた。
- 親に返るのは 中央値 1,151 字の文章 1 つだけ。読んだツール結果は 0 件が親に届く。
- サブエージェントが書いた本文 530,652 字のうち、親に届いたのは 55.0%。残りは親が見ることのないまま消える。
委譲は「文脈を汚さない」ための仕組みだ
サブエージェントに仕事を渡す理由ははっきりしている。大量のツール出力を親の文脈に入れないためだ。100 ファイルを探させて、結論だけ受け取る。ファイルの中身は親のウィンドウに入らない。
これは設計として正しい。問題は、その「入らない」がどのくらいなのかを、私が数字で把握していなかったことだ。測った。
サブエージェント側でやったこと(1委譲あたり)
ツール呼び出し 中央値 20 / 平均 22 / 最大 70 / 合計 3,093
0 回以下 8 = 5.8%
5 回以下 22 = 15.9%
20 回以下 71 = 51.4%
50 回以下 125 = 90.6%
output_tokens 中央値 12,298 / 最大 49,545 / 合計 2,021,336
**中央値 20 回。**半分の委譲が 20 回以上ツールを呼んでいる。最大は 70 回。1 委譲あたり中央値 12,298 トークンを出力していて、合計は 202 万トークンになる。
一方、親が受け取るものはこれだ。
親に返ってきたもの(Agent の結果 66 件)
指示 中央値 816 字 / 最大 4,201 字
戻り 中央値 1151 字 / 最大 15,404 字
サブが書いた本文 530,652 字 → 親に届いた 292,082 字 = 55.0%
サブが読んだツール結果 3,093 件 → 親に届いた 0 件
**816 字の指示を出して、1,151 字が返る。**その裏で 20 回のツール呼び出しが起きている。
0 件、というのが要点
数字のうち一番大事なのは 3,093 → 0 だ。
サブエージェントが読んだ grep の結果、ファイルの中身、コマンドの出力。**そのどれ 1 つも親には届かない。**届くのは、サブエージェントがそれらを読んだうえで書いた文章だけだ。
つまり親から見ると、サブエージェントの報告は検証不能な二次情報になる。
サブ: 20回ツールを呼ぶ → 3,093件の一次データを読む
↓
1,151字の文章にまとめる
↓
親: 文章だけを受け取る(一次データは無い)
親は「そのファイルに本当にそう書いてあったか」を確かめられない。確かめるには、親が自分でもう一度読むしかない。それは委譲した意味を打ち消す行為だ。
この非対称は避けられない。委譲の利益(文脈が汚れない)と、委譲のコスト(検証できない)は、同じ性質の裏表だからだ。文脈に入らないから安いし、文脈に入らないから確かめられない。
55% は消える
もう 1 つ気になったのがこれだ。
サブが書いた本文 530,652 字 → 親に届いた 292,082 字 = 55.0%
サブエージェントが書いた文章のうち、親に届くのは 55.0%。残りの 45% は、サブエージェント自身の中間思考や経過報告で、最終的な戻り値には入らない。
これも設計通りではある。だが「サブエージェントが 20 回ツールを呼んで、その過程で 45% の文章を書いて捨てている」というのは、どこで判断が起きたかが親から見えないということでもある。前に書いた通り、思考の本文はログにも残らない。過程は二重に見えない。
それで、報告をどう扱うか
1. 報告を根拠として使うなら、一次データで裏を取る。特に「ログを見たらこうなっていた」「調べたらこの件数だった」という数字の報告は、サブエージェント側で検証されていない。私は自分の運用規約にこう書いている。
- **サブエージェントの報告は実物で裏取りする**: 委譲成果の監査では自己申告を鵜呑みに
せず、各主張を diff・テスト結果・ログの実データに突き合わせて確認する。
これは精神論ではなく、構造上そうなっているからそう書いてある。3,093 件が 0 件になる経路に、検証は含まれていない。
2. 裏取りが不可能な仕事を委譲しない。「探して結論を出す」は委譲に向く。結論が間違っていても、親が自分で 1 回検索すれば分かるからだ。「大量のファイルを読んで要約する」は向かない。裏取りに、委譲したのと同じ量の作業が要る。
3. 一次データそのものを返させる。「ファイルパスと行番号を返せ」「コマンドと出力を返せ」と指示すれば、戻り値に一次データが入る。1,151 字という中央値には、まだ余裕がある(最大は 15,404 字だった)。要約ではなく証拠を返させるほうが、後で使える。
3 番目が一番効いた。要約を求めると要約が返ってくるが、それは検証できない。「どこを見てそう言ったか」を返させると、親が数行の確認で済む。
測るスクリプト
#!/usr/bin/env python3
"""委譲したサブエージェントが「見たもの」と「返したもの」の差を測る。
python3 delegation_gap.py <ログのルート>
サブエージェントの作業は isSidechain の行に、agentId ごとに残る。
親に返るのは Agent ツールの結果1つだけ。その落差を数える。
"""
import sys, os, json, glob, collections, statistics as st
def result_text(b):
ct = b.get("content")
if isinstance(ct, list):
return "".join(x.get("text", "") for x in ct if isinstance(x, dict))
return ct if isinstance(ct, str) else ""
def main(root):
files = glob.glob(os.path.join(root, "**", "*.jsonl"), recursive=True)
# agentId -> サブエージェント側の作業量
agents = collections.defaultdict(
lambda: {"tool": 0, "result": 0, "text": 0, "out": 0})
# 親に返ってきたもの
returned = []
subtypes = collections.Counter()
for f in files:
names, inputs = {}, {}
for ln in open(f, errors="replace"):
try:
d = json.loads(ln)
except json.JSONDecodeError:
continue
msg = d.get("message") or {}
content = msg.get("content")
side = bool(d.get("isSidechain"))
if side:
a = agents[d.get("agentId") or "?"]
a["out"] += (msg.get("usage") or {}).get("output_tokens", 0)
if not isinstance(content, list):
continue
for b in content:
if not isinstance(b, dict):
continue
if side:
a = agents[d.get("agentId") or "?"]
if b.get("type") == "text":
a["text"] += len(b.get("text", ""))
elif b.get("type") == "tool_use":
a["tool"] += 1
elif b.get("type") == "tool_result":
a["result"] += 1
continue
# 親側: Agent 呼び出しと、その戻り
if b.get("type") == "tool_use":
names[b.get("id")] = b.get("name")
inputs[b.get("id")] = b.get("input") or {}
elif (b.get("type") == "tool_result"
and names.get(b.get("tool_use_id")) == "Agent"):
i = inputs.get(b.get("tool_use_id"), {})
returned.append((len(i.get("prompt", "") or ""),
len(result_text(b))))
subtypes[i.get("subagent_type") or "(既定)"] += 1
if not agents:
print("サブエージェントの記録が無い")
return 1
tools = sorted(a["tool"] for a in agents.values())
outs = sorted(a["out"] for a in agents.values())
print(f"ログ {len(files)} ファイル / 委譲(agentId){len(agents)} 件")
print()
print("サブエージェント側でやったこと(1委譲あたり)")
print(f" ツール呼び出し 中央値 {st.median(tools):.0f} / "
f"平均 {st.mean(tools):.0f} / 最大 {max(tools)} / 合計 {sum(tools):,}")
for k in (0, 5, 20, 50):
c = sum(1 for x in tools if x <= k)
print(f" {k:3d} 回以下 {c:4d} = {100 * c / len(tools):5.1f}%")
print(f" output_tokens 中央値 {st.median(outs):,.0f} / "
f"最大 {max(outs):,} / 合計 {sum(outs):,}")
print()
if returned:
p = [x for x, _ in returned]
r = [y for _, y in returned]
text_total = sum(a["text"] for a in agents.values())
print(f"親に返ってきたもの(Agent の結果 {len(returned)} 件)")
print(f" 指示 中央値 {st.median(p):.0f} 字 / 最大 {max(p):,} 字")
print(f" 戻り 中央値 {st.median(r):.0f} 字 / 最大 {max(r):,} 字")
print(f" サブが書いた本文 {text_total:,} 字 → 親に届いた {sum(r):,} 字 "
f"= {100 * sum(r) / text_total:.1f}%")
print(f" サブが読んだツール結果 {sum(a['result'] for a in agents.values()):,} 件 "
f"→ 親に届いた 0 件")
print()
for k, v in subtypes.most_common():
print(f" {v:4d} {k}")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else
os.path.expanduser("~/.claude/projects")))
python3 delegation_gap.py ~/.claude/projects
この記事の限界
- 1人分のログである。389 ファイル・委譲 138 件。
- 委譲 138 件に対し、親側で結果が取れたのは 66 件。差の 72 件は、ワークフロー経由で起動したものや、結果が別ファイルに記録されたものを含む。**「1委譲あたり」の作業量(138 件で計算)と「戻りの長さ」(66 件で計算)は、母集団が違う。**同じ委譲について両方を突き合わせてはいない。
- 「親に届いた 0 件」は、ツール結果ブロックが 0 という意味である。サブエージェントが本文の中に出力を引用していれば、内容としては届いている。引用の有無は数えていない。
- **
Exploreが 49 件と大半を占める。**探索用のサブエージェントは元々「結論だけ返す」設計なので、この偏りが 55.0% という数字を押し下げている可能性がある。 - **委譲が悪いという話ではない。**202 万トークンぶんの作業を親の文脈に入れずに済んでいる。測ったのは利益ではなくコストのほうだけだ。
まとめ
- 委譲 1 回あたり、サブエージェントは中央値 20 回ツールを呼ぶ。合計 3,093 件の一次データを読む。
- 親に届くのは中央値 1,151 字の文章 1 つ。一次データは 0 件。
- サブエージェントが書いた文章の 45% は親に届かない。
- だから報告は構造上、検証不能な二次情報になる。裏取りするか、証拠ごと返させるかしかない。
委譲が安いのは、親が見ないからだ。同じ理由で、親は確かめられない。
関連
一次データが0件しか返らない相手に、規約が届いたかどうかも同じく返ってきません。委譲先に何をどう渡すかは、報告の信頼性そのものを決めます。
委譲を含む構成パターン別に、どの層に何を置くかをまとめた本があります。
→ AIコンテキスト設計ガイド ── 規約を配ったのに守られない理由(2,000円)
第3章「規約が効かなくなる4つのパターン」まで無料で読めます。
/compact の後もルートの CLAUDE.md は再注入されるが、サブディレクトリと paths: 付きは再注入されない ── 長いセッションで静かに落ちるのはどちらか、という章です。