この記事は Zenn に投稿したもの の再掲です。内容は同じで、更新も両方に反映します。
3行で
- ログ 389 ファイルから
userロールのメッセージ 588 件を取り出した。総量 5,928,433 字。 - 人間が実際に打ったのは 282 件(48.0%)。だが文字量では 37,583 字、全体の 0.63% しかない。
- 人間が打つ入力の中央値は 29 字。同じ枠から入ってくる自動投入は 2,153 字、セッション引き継ぎは 12,710 字。件数はほぼ半々なのに、量は 150 倍以上違う。
「ユーザー入力」を数えようとして詰まった
エージェントに人間が何を言っているかを数えようとした。type: "user" のメッセージを集めて、長さを見る。単純な話のはずだった。
発話 587 件 / 総量 5,928,408 字
中央値 242 / 平均 10100 = 平均は中央値の 42 倍
平均が中央値の 42 倍。分布がおかしい。上位を見るともっとおかしかった。
上位 1件 = 総量の 27.3%
上位 3件 = 総量の 55.1%
下位半分(293件) = 総量の 0.26%
**1件で全体の 27.3%。**最長のものは 1,620,019 字あった。162 万字を人間が打つことはない。
1万字を超えるメッセージ 154 件の先頭を見たら、正体が分かった。
-
87 件が
This session is being continued from a previous conversation— 会話が長くなったときの引き継ぎ文書 - 51 件が自動パイプラインからの定型投入
- 9 件がコマンド展開
人間の入力を数えているつもりで、機械の投入を数えていた。
promptSource で分かれる
ログには promptSource というフィールドがある。これで綺麗に割れた。
出どころ 件数 件数比 文字数 文字比 中央値
typed 274 46.6% 37,339 0.6% 29
sdk 187 31.8% 4,459,378 75.2% 2153
(なし) 119 20.2% 1,431,472 24.1% 12710
queued 6 1.0% 214 0.0% 25
suggestion_accepted 2 0.3% 30 0.0% 15
人間が打った 282 件 = 48.0% / 37,583 字 = 0.63%
それ以外 306 件 = 52.0% / 5,890,850 字 = 99.37%
-
typed— キーボードから打ったもの。274 件、中央値 29 字。 -
queued/suggestion_accepted— 実行中に打ち込んだもの、補完を受け入れたもの。これも人間。合わせて 8 件。 -
sdk— SDK 経由の自動投入。187 件で 75.2% の文字量。中央値 2,153 字。 -
(なし)— 引き継ぎ文書、中断通知、他セッションからの連絡。119 件で 24.1%。中央値 12,710 字。
件数は 48.0% 対 52.0% でほぼ半々だ。だが文字量は 0.63% 対 99.37%。
人間は 29 字しか打たない
一番はっきりしたのはこれだった。typed の中央値 29 字。
私の 274 件を見ても、短いものは「動作確認して。」「終わった?」「引き続き進めて。」といった一行に集中していた。長いものもあるが、それは資料の貼り付けや要件の列挙で、頻度は低い。
**指示は短く、文脈は機械が運ぶ。**これがエージェント運用の実態だった。
人間が 29 字を打つ。そのあと、引き継ぎ文書が 12,710 字を運ぶ。自動投入が 2,153 字を運ぶ。エージェントが読む「ユーザーからの入力」の 99% 以上は、人間がその場で書いたものではない。
何が変わるか
**1. プロンプトの推敲より、注入される文脈のほうが効いている。**29 字を 40 字にしても、その隣に 12,710 字が置かれる。改善の余地があるのは短いほうではなく長いほうだ。
**2. 引き継ぎ文書の品質が、そのままセッションの品質になる。**中央値 12,710 字が「前のセッションで何をしたか」の要約だ。ここに誤りが入ると、そのセッションは誤った前提の上で全部を進める。人間が打つ 29 字にはそれを訂正する余地がほとんど無い。
3. 「ユーザーが言った」を根拠にするなら、出どころを見る必要がある。type: "user" の枠には、人間・自動化・引き継ぎ・他セッションからの連絡が全部同居している。同じ枠に入っているからといって、同じ重みではない。承認を確認するなら promptSource が typed か queued かを見るしかない。
3 番目は自分でも意外だった。ログ上、機械が入れた 12,710 字と人間が打った 29 字は、同じ user ロールの、同じ形をしたメッセージとして並んでいる。区別する情報は promptSource にしか無い。
測るスクリプト
#!/usr/bin/env python3
"""エージェントに届く「ユーザー入力」の出どころを分ける。
python3 who_talks.py <ログのルート>
user ロールのメッセージには promptSource が付く。人間が打ったもの(typed)と、
自動化やセッション引き継ぎで注入されたものを、件数と文字量の両方で数える。
"""
import sys, os, json, glob, collections, statistics as st
# 人間が入力したもの
HUMAN = {"typed", "queued", "suggestion_accepted"}
def user_text(d):
"""user メッセージの本文。ツール結果やシステム注入は除く。"""
c = (d.get("message") or {}).get("content")
if isinstance(c, str):
t = c
elif isinstance(c, list):
t = "".join(x.get("text", "") for x in c
if isinstance(x, dict) and x.get("type") == "text")
else:
return None
t = t.strip()
# ツール結果ブロックと、ハーネスが差し込む注意書きは対象外
if not t or t.startswith("<") or "system-reminder" in t:
return None
return t
def main(root):
files = glob.glob(os.path.join(root, "**", "*.jsonl"), recursive=True)
count = collections.Counter()
chars = collections.Counter()
lens = collections.defaultdict(list)
entry = collections.Counter()
for f in files:
for ln in open(f, errors="replace"):
try:
d = json.loads(ln)
except json.JSONDecodeError:
continue
# サブエージェント側の投入は親の「ユーザー入力」ではない
if d.get("type") != "user" or d.get("isMeta") or d.get("isSidechain"):
continue
t = user_text(d)
if t is None:
continue
src = d.get("promptSource") or "(なし)"
count[src] += 1
chars[src] += len(t)
lens[src].append(len(t))
entry[d.get("entrypoint") or "?"] += 1
n = sum(count.values())
total = sum(chars.values())
if not n:
print("user メッセージが見つからない")
return 1
print(f"ログ {len(files)} ファイル / user メッセージ {n} 件 / {total:,} 字")
print()
print(f"{'出どころ':22s} {'件数':>6s} {'件数比':>7s} {'文字数':>12s} {'文字比':>7s} {'中央値':>7s}")
for src, c in count.most_common():
print(f"{src:22s} {c:6d} {100 * c / n:6.1f}% "
f"{chars[src]:12,} {100 * chars[src] / total:6.1f}% "
f"{st.median(lens[src]):7.0f}")
hc = sum(c for s, c in count.items() if s in HUMAN)
ht = sum(v for s, v in chars.items() if s in HUMAN)
print()
print(f"人間が打った {hc:5d} 件 = {100 * hc / n:5.1f}% / "
f"{ht:,} 字 = {100 * ht / total:.2f}%")
print(f"それ以外 {n - hc:5d} 件 = {100 * (n - hc) / n:5.1f}% / "
f"{total - ht:,} 字 = {100 * (total - ht) / total:.2f}%")
print()
print("entrypoint:", dict(entry))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else
os.path.expanduser("~/.claude/projects")))
python3 who_talks.py ~/.claude/projects
この記事の限界
-
私の使い方が偏っている。
entrypointはcli384 に対しsdk-cli173 /sdk-py31。3 分の 1 が自動実行だ。**対話でしか使わない人ならtypedはほぼ 100% になる。**この 0.63% という数字は一般的な値ではなく、自動化を併用している環境の値である。 -
(なし)を「人間ではない」に入れているが、微妙である。引き継ぎ文書は、前のセッションの人間の発言とエージェントの作業から生成されている。その場で打たれたものではない、という意味で分けた。由来を辿れば人間に行き着くものも含む。 - **文字数はトークン数ではない。**日本語と英語では字数あたりのトークン数が違うので、この比率がそのまま課金比率になるわけではない。
- 1人分のログである。389 ファイル・588 メッセージ。
-
promptSourceが無いメッセージ(119 件)の内訳は先頭文字列で目視分類しただけで、機械的に判定していない。
まとめ
-
userメッセージ 588 件のうち、人間が打ったのは 282 件(48.0%)。 - だが文字量では 0.63%。残り 99.37% は自動投入とセッション引き継ぎ。
- 人間が打つ入力の中央値は 29 字。引き継ぎ文書は 12,710 字。
- ログ上、両者は同じ形のメッセージとして並ぶ。区別できるのは
promptSourceだけ。
エージェントに何を伝えたかを測るなら、自分が打った文字ではなく、**自分が打っていない文字のほうを読んだほうがいい。**そちらが 99% を占めている。
関連
エージェントに届く文字の99%を人間が打っていないなら、規約や指示ファイルが「ちゃんと届いているか」も、書いた側の感覚では判断できません。配ったのに守られない、の多くはここで起きています。
層をどう切るか、効かなくなる4つのパターンの見分け方、測定結果をどう読むかをまとめた本があります。
→ AIコンテキスト設計ガイド ── 規約を配ったのに守られない理由(2,000円)
第3章「規約が効かなくなる4つのパターン」まで無料で読めます。
/compact の後もルートの CLAUDE.md は再注入されるが、サブディレクトリと paths: 付きは再注入されない ── 長いセッションで静かに落ちるのはどちらか、という章です。