2026.09.08
トークンマネジメント技術の実測検証
こんにちは。次世代システム研究室のT.M です。
はじめに
トークン管理の記事は「読む量を減らす」「安いモデルに変える」という話に偏りがちです。それ自体は重要ですが、正直なところ自明な話でもあります。
今回は、AIエージェント特有の非自明なメカニズムに基づく技術を3つ選び、実際に自分のClaude Code運用環境で動かして検証しました。結果は「効くはずのものが、実は効いていなかった」ケースと、「効いたが、想定と違う理由で効いていた」ケースの両方に分かれました。
ケース1: キャッシュプリウォーム
説明
私は日々の定常作業(勤怠チェック・日報登録・進捗レポートなど16個のタスク)を、Claude Codeのバックグラウンドサブエージェント(`Agent`ツール)を並列起動して処理しています。
このオーケストレーション処理には、以前「キャッシュプリウォーム」という2段階の起動方式を組み込んでいました。
1. 起動する全Agentの中から、モデル系統ごとに代表1体を先行起動します
2. 5秒待ちます
3. 残り全員を1メッセージで一斉起動します
理屈
Anthropicのプロンプトキャッシュは、同一モデル・同一プレフィックス(システムプロンプトやツール定義など)に対して、初回は「キャッシュ書き込み」(割高)、それ以降1時間以内の再利用は「キャッシュ読み込み」(大幅に安い)という課金構造になっています。
16個のサブエージェントは、共通のCLAUDE.md・ツール一覧・Skill候補リストという同一のプレフィックスを持ちます(モデル単位でキャッシュは分かれます)。これらを完全に同時起動すると、キャッシュへの書き込みが確定する前に他のリクエストも飛んでしまい、全Agentが「初回」と判定されて全員分の書き込みコストを払ってしまう可能性があります。1体だけ先に起動して数秒待てば、その1体の書き込みが確定したあとに残りが飛ぶため、残り全員がキャッシュ読み込みで済む、という理屈です。
出典
これはAnthropic公式ドキュメントに明記されている挙動です。Prompt caching – Claude Platform Docs の「Pre-warming the cache」という専用セクションに、まさに今回の検証内容そのものが書かれています。
> For concurrent requests, note that a cache entry only becomes available after the first response begins. If you need cache hits for parallel requests, wait for the first response before sending subsequent requests.
(並列リクエストの場合、キャッシュエントリは最初のレスポンスが開始された後にしか使えるようになりません。並列リクエストでキャッシュヒットが必要なら、最初のレスポンスを待ってから後続を送るべき、という趣旨です)
結果
本番のスキルファイルには一切変更を加えず、起動方式だけを「一斉起動」に戻した非公開の参照ファイルを別途作成し、本番と同一のスキル群で比較しました。
同日に2回比較する場合、プロンプトキャッシュのTTL(1時間、混雑時5分)をまたぐ間隔を空けないと、後発の実行が先発のキャッシュにただ乗りして結果が偏ります。そのため90分空けて実行しました。
| 実行方式 | 合計トークン(16 Agent) |
|---|---|
| 一斉起動(施策前の状態) | 957,265 |
| 2段起動(現行の状態) | 972,724(+1.6%) |
2段起動の方がわずかに多いという結果になりました。
さらに、各Agentの最初のAPI呼び出しの`usage.cache_creation_input_tokens`/`cache_read_input_tokens`を実際のログ(JSONLトランスクリプト)から抽出したところ、興味深いことが分かりました。
# 一斉起動時の実測(抜粋、tokens) haiku代表: cache_creation=35122 cache_read=0 ← 書き込み 他haiku群: cache_creation=16427〜16591 cache_read=18590 ← 読み込み sonnet代表: cache_creation=52022 cache_read=0 ← 書き込み 他sonnet群: cache_creation=27603〜27732 cache_read=24433 ← 読み込み
一斉起動でも、2段起動でも、まったく同じパターン(モデルごとに1体だけが書き込みを払い、残り全員が読み込みで済む)になっていました。各Agentの最初のAPI呼び出しのタイムスタンプを確認すると、1メッセージで16個のAgentツール呼び出しを同時に発行しても、実際の最初のAPI呼び出しは数秒〜1分以上ずれて発火していました。
考察
「全サブエージェントを完全同時起動すると、全サブエージェントがキャッシュ書き込みを重複して払う」という前提そのものが誤りでした。もう一段掘り下げて、「起動を要求した時刻→そのサブエージェントが実際に起動した時刻(起動ラグ)」と「起動した時刻→最初の応答が返るまで(応答生成時間)」を分けて実測したところ、次のことが分かりました。
– 応答生成時間は、どのサブエージェントでも一貫して約2.7〜3.6秒と速く安定していました。時間差の主因ではありません
– 一方、1つのメッセージで複数のサブエージェント起動を一斉に要求しても、実際の起動タイミングは呼び出しを書いた順番通りに段階的にずれていました(数秒→十数秒→2分半、というように)
これは、Claude Code自身が1ターン内の複数のサブエージェント起動リクエストを、実質的に順番に処理していることを示しています。さらに、サブエージェントを並列起動する方法自体、Claude Codeが提供する「1メッセージで複数のAgentツール呼び出しを並べる」というやり方が唯一の手段であり、これより並列度を上げる方法は用意されていません。つまりこの段階的な起動は、使い方の工夫では避けられません。
このプリウォーム技術は、そもそも存在しない問題を解決しようとしていた、というだけでなく、その対策を打つ手段自体が(Claude Codeの範囲内では)存在せず、かつ対策なしでも自然に解決していた、という二重の意味で不要だったことになります。
備考
今回実測した具体的な秒数(起動ラグが数秒〜2分半、応答生成が約3秒)は、あくまで私の環境(マシン・ネットワーク・実行しているタスクの構成)での値であり、他の環境でも同じ秒数になるとは限りません。
さらに掘り下げると、2番目に起動したサブエージェントは、1番目のサブエージェントの応答が「完了」する0.4秒前に起動していました。それでもキャッシュはヒットしていたので、キャッシュが実際に使えるようになるタイミングは、応答が完了した時点ではなく、もっと早い段階(生成を開始した直後あたり)にあると考えられます。逆に言えば、今回はたまたま十分な余裕があっただけで、常に安全という保証ではありません。最初のタスクの応答が極端に速く終わる(思考トークンがほぼ無く、出力も一瞬で終わる)ような場合は、キャッシュ書き込みが確定する前に次のリクエストが届いてしまう可能性が理論上残ります。つまり、タスクの内容によっては、プリウォームが依然として必要になるケースも排除できません。
この検証結果は「Claude Codeならプリウォームは常に不要」という一般論ではなく、「今回検証した16タスクの組み合わせでは、実測の結果、効果が見られなかった」という限定的な結論として読んでいただきたいです。同じ検証を行う場合は、今回のように実際のタイムスタンプとcache_creation/cache_read_input_tokensを確認してから判断することを勧めます。
ケース2: codexの出力ノイズ除去
説明
Claude Codeから`codex exec`(OpenAI Codex CLI)をサブプロセスとして呼び出す運用をしています。当初は`codex exec`の生stdout(Bashツールの結果)を直接読んで報告に使っていましたが、現在は`–output-last-message`オプションで最終回答だけを別ファイルに書き出し、そのファイルをReadツールで読む方式に変更しています。
# 変更前 codex exec --sandbox read-only -C <dir> "<prompt>" < /dev/null # 変更後 codex exec --sandbox read-only -C <dir> --output-last-message /tmp/result.txt "<prompt>" < /dev/null # → 生stdoutは直接読まず、/tmp/result.txt をReadツールで読む
理屈
codexは単純な1コマンドではなく、内部で何十ステップも「思考→ツール呼び出し→観察」を繰り返すエージェントです。その生stdoutには、最終的な結論だけでなく、途中の思考過程・ツール呼び出しログ・中間結果がすべて含まれます。`–output-last-message`は、codex自身に「最終回答だけ」を抽出させ、途中のノイズを含まない蒸留済みの結果を得られます。
結果
実装タスクではなくレビュータスク(`–sandbox read-only`、ファイル変更なし)として、実在するPRを対象に検証しました。
PR1(13ファイル・1327行の変更)
| 生stdout直読み | --output-last-message+Read |
|
|---|---|---|
| トークン | 74,202 | 60,263(-19%) |
| tool呼び出し数 | 5 | 2 |
PR2(4ファイル・293行の変更)
| 生stdout直読み | --output-last-message+Read |
|
|---|---|---|
| トークン | 110,046 | 61,847(-44%) |
| tool呼び出し数 | 7 | 3 |
興味深いのは、「生stdout直読み」側も実際には生ログがそのまま会話に流れ込んでいたわけではなかったことです。Claude Code自体に、Bashコマンドの出力が約30,000文字を超えると自動的に別ファイルへ保存する仕組みがあります(`BASH_MAX_OUTPUT_LENGTH`環境変数で変更可能)。今回の2つのPRはいずれも生stdoutが200KB台〜700KB台あり、この閾値を大きく超えていました。つまり両方とも最終的にはファイル経由でしたが、「生stdout直読み」側はその自動保存先を自力で発見して読む分の追加ツール呼び出しが発生していました(5〜7回 vs 2〜3回)。
もう一つ興味深いのは、PR2(小さい方)の「生stdout直読み」がPR1(大きい方)より多くのトークンを消費していたことです。codexの生ログの量は、diffの行数そのものより、codexが探索的に読む関連ファイルの量に強く依存するため、diffが小さくてもログが大きくなることがあります。一方、`–output-last-message`側のトークン数はタスク規模に関わらずほぼ一定でした(60,263→61,847)。
考察
この技術の本質的な効果は、「平均的にトークンを減らす」ことより、「タスクの規模に関わらず、常に一定の低いオーバーヘッドで済むようにする(挙動を予測可能にする)」ことにあります。生ログ直読み方式は、タスクによって「発見の手間」が安定せず、時にはむしろ悪化します。
備考
この技術が効くのは、「呼び出す対象が内部で複数ステップの思考・ツール呼び出しを行う、もう一つのAIエージェントである場合」に限られます。単純な1回限りのCLIコマンド(`gh`や`git`など)は元々こうした思考トレースを持たないため対象になりません。現時点で自分の環境でこのパターンに当てはまる実例はcodex以外にないため、他のAIエージェントCLIへの一般化はここでは行いません。
ケース3: VTT構造ノイズ除去
説明
Zoomの会議録画から自動生成されるVTT字幕ファイルを要約する処理で、以前はファイルをそのまま読み込んでいましたが、現在はタイムスタンプ・インデックス番号などの構造的な行を除去してから読み込む方式に変更しています。
# VTTのノイズ除去処理(抜粋)
TIMESTAMP_RE = re.compile(r"-->")
INDEX_RE = re.compile(r"^\d+$")
def strip_vtt(lines):
out = []
for line in lines:
stripped = line.rstrip("\n")
if stripped == "WEBVTT" or INDEX_RE.match(stripped) or TIMESTAMP_RE.search(stripped):
continue
if not stripped.strip():
continue
out.append(stripped)
return out
理屈
VTTファイルには、発言内容そのものに加えて、タイムスタンプ(`00:01:23.456 –> 00:01:25.789`)やキャプション番号といった、LLMにとって意味を持たない構造的なマークアップが大量に含まれます。ファイルを頭から切り詰める(offset/limit)のではなく、発言内容はすべて保持したまま、構造的に意味を持たない行だけを除去することで、情報を損なわずに入力量を減らせます。
結果
実在する2件の会議VTTファイルに対して、Strip前後の行数・文字数を計測しました。
| ファイル | 行数(前→後) | 文字数(前→後、≒トークン数の目安) |
|---|---|---|
| 会議A | 1,526 → 381(-75%) | 50,225 → 35,464(-29%) |
| 会議B | 1,106 → 276(-75%) | 40,666 → 30,000(-26%) |
(厳密なトークン数の計測にはAnthropicのcount_tokens APIが必要ですが、今回の検証環境ではAPI認証が用意できず、文字数ベースの近似値としています)
考察
行数は75%も減るのに、文字数(≒コスト)の削減は26〜29%程度にとどまります。除去しているタイムスタンプ・インデックス行は「行数」としては多いですが、1行あたりの文字数が短いためです。
スクリプトが出力する`lines: N -> M`のようなログだけを見ると、削減効果を過大評価してしまいます。効果を語るときは、行数ではなく文字数(あるいは実際のトークン数)で見る必要がある、という地味だが実務的な注意点です。
まとめ
3つのケースを並べると、共通の軸が見えてきます。「量を減らす」のではなく、同じ量の仕事でも、いつ実行するか(ケース1)、何を結果として受け取るか(ケース2)、何が意味のある内容で何がノイズかを見分けるか(ケース3)によってコストが変わる、という話です。
もう一つの教訓は、「有名な対策」がそのまま自分の環境で効くとは限らないということです。ケース1のように、対策自体は正しい理屈でも、実行環境(今回はClaude Codeのサブエージェント起動処理)の実装詳細によって前提が崩れていることがあります。実際に動かして、できればメカニズムのレベルまで掘り下げて確認しないと、「効いた」と思い込んだまま何もしていない施策を続けてしまうことになります。
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD


