0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

エージェントに届く「ユーザー入力」の 99.4% は、人間が打った文字ではなかった

0
Posted at

この記事は 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" の枠には、人間・自動化・引き継ぎ・他セッションからの連絡が全部同居している。同じ枠に入っているからといって、同じ重みではない。承認を確認するなら promptSourcetypedqueued かを見るしかない。

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

この記事の限界

  • 私の使い方が偏っている。entrypointcli 384 に対し sdk-cli 173 / sdk-py 31。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: 付きは再注入されない ── 長いセッションで静かに落ちるのはどちらか、という章です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?