コードグラフでエージェントの読み込みトークンは98%減った(ただしgrepが勝つ問いもある) — AI利用コスト実測レポート #02
この記事について
シリーズ第1回は「定額プランで月いくら得しているか」という総量の話だった。今回は逆方向、投げるトークンそのものを減らす側を測る(第1回で予告したモデル別内訳は次回に回す)。
素材は Graphify(tree-sitter でソースコードを AST 解析し、関数・クラス・ファイルの関係を知識グラフにする OSS。Apache-2.0、PyPI パッケージ名は graphifyy)を実プロジェクトに導入した記録。測ったのは推論そのものではなく、エージェントが1つの問いに答えるまでにコンテキストへ流し込まれるツール出力の量である。
| 項目 | 内容 |
|---|---|
| 測定日 | 2026-07-30 |
| 対象 | 稼働中の制作リポジトリ1本(記事・スクリプト・定期ジョブ設定が同居) |
| トークナイザ | tiktoken o200k_base(Claude のトークナイザではないため絶対値は近似) |
| 比較対象 | grep 経由の調査 vs graphify の affected / explain / query |
| グラフ構築コスト | API 0 トークン・約7秒(tree-sitter の AST 解析のみ。LLM を一切呼ばない) |
結論を先に書くと、5問の合計で 195,988 → 3,193 トークン(98%減)。ただし grep が勝つ問いが混ざっている。そこが今回の本題でもある。
測り方
比較を成立させるために、baseline(grep 側)を不利にしない条件を先に決めた。
| 列 | 中身 |
|---|---|
grepのみ | grep の出力だけ。ヒット行を見た時点で答えが確定する場合の下限 |
grep+全文 | grep 出力 + ヒットしたファイルを実際に開いた全文(最大12本)。呼び出し関係まで確認するなら実際にはこうなる |
graphify | affected / explain / query の出力 |
- グラフの生成物ディレクトリを grep 対象から除外した。自分の生成物を grep させて水増しするのは循環になる
.git/node_modulesも除外し、拡張子は必ず絞った。無指定 grep の爆発を baseline にすると比較にならない- grep が勝つ問いを意図的に混ぜた。勝ち筋だけ並べた比較は測定ではなく宣伝になる
結果
| 問い | grepのみ | grep+全文 | graphify | 削減 |
|---|---|---|---|---|
| 投稿系の共通モジュールを変更すると何が壊れるか | 1,989 | 81,860 | 497 | 99% |
| 記事投稿スクリプトを起動しているものすべて(シェル・CI・定期ジョブ込み) | 795 | 49,392 | 451 | 99% |
| 計測監視スクリプトは何に依存し誰に呼ばれるか | 534 | 12,619 | 382 | 97% |
| ある常駐ジョブ(plist)は何を起動するか | 550 | 550 | 98 | 82% |
| 計測まわりに触るスクリプトの全体像 | 245 | 51,567 | 1,765 | 97%(※) |
| 合計 | 4,113 | 195,988 | 3,193 | 98% |
※ 最後の1問だけは grep -rln(ファイル名だけ)が 245 トークンで、graphify の 1,765 より安い。「どのファイルか」だけ欲しいなら grep が勝つ。
再現用のベンチスクリプトはリポジトリに置いてあり、同じ5問を同じ条件で回し直せる。
grep が勝つ線はどこか
負けた1問を消さずに載せているのは、この線が導入判断そのものだからだ。
| 問いの型 | 安い手段 | 理由 |
|---|---|---|
| 「その文字列はどこにあるか」 | grep -rln | ファイル名のリストは最初から短い。グラフは近傍ノードを付けて返すぶん重い |
| 「そのファイルは何をしているか」 | ファイルを1本読む | 1本なら全文の方が確実。グラフは関係しか持たない |
| 「変更したら何が壊れるか」 | graphify | 呼び出し関係を辿るには結局ファイルを開く必要があり、そこで桁が変わる |
| 「誰がこれを起動しているか」 | graphify | 呼ぶ側は文字列一致では見つかるが、経路の全体は grep では組み立てられない |
grep は「文字列の所在」を返す道具で、グラフは「関係」を返す道具である。トークンが桁で変わるのは、問いが関係を要求したときだけ。 所在だけ聞いているうちは grep で十分だし、その方が安い。
削減率より重要なこと
数字より効いたのは、grep では出ない答えが出ることだった。
$ graphify affected "<投稿共通モジュール>" --depth 2
- <媒体A投稿スクリプト> [imports_from] scripts/....py:L32
- <媒体B投稿スクリプト> [imports_from] scripts/....py:L40
- <ダッシュボードサーバ> [imports] scripts/....py:L1521
- <記事組み立て処理> [imports] scripts/....py:L1323
(媒体名を含むファイル名は伏せている。行番号と関係ラベルは実出力のまま)
文字列検索は「その語がある場所」を返す。上は「呼び出し深さ2で影響が及ぶ範囲」を行番号つきで返している。媒体をまたいだ結合が1コマンドで出るのは、grep の出力を人間が読んで組み立てていた作業がまるごと消えるということだ。
素で入れただけでは答えられなかったこと
導入直後のグラフを調べたところ、このリポジトリの自動化を実際に繋いでいる経路が1本も辺になっていなかった。
| 経路 | 辺の数 |
|---|---|
.sh → .py(シェルから Python を起動) | 0 |
.md → コード(ドキュメントからの言及) | 0 |
.plist → 何か | 対応拡張子ですらない |
.yml(CI ワークフロー)→ スクリプト | ノードとして存在しない |
AST 解析は言語の内側で閉じるので、これは当然の結果ではある。ただ結果として、「どの定期ジョブが最終的に本番へ書くか」という、このリポジトリで実際に事故ってきた問いには答えられない。トークンが減っても、答えが出ない問いが残るなら導入としては半分である。
320本の辺を自前で注入した
そこで接着用のスクリプトを書き、plist と CI ワークフローをノード化して、起動関係を 320本の辺として graph.json に注入した。冪等・注入印つき・AST 由来のノードには触らない設計にしてある。補完後はこう答える。
$ graphify explain "<常駐ジョブ>.plist"
Node: <常駐ジョブ>
Connections (1):
--> <常駐デーモン>.py [calls] [EXTRACTED] scripts/....plist:L1
「plist → 常駐スクリプト」の1本が通ったことで、定期ジョブから実装までの経路が辿れるようになった。
詰まった点(同じ実装をする人向け)
同種の接着を書く人が確実に踏むポイントを3つ残しておく。
# 1. plist が XML として不正なことがある
# コメント内の "--dry-run" の "--" は XML コメントに書けず plistlib が落ちる
# → パース前にコメントを除去する
# 2. ファイルノードの見分け方が言語で違う
# bash: metadata.kind == "file" / Python: None
# → 共通して成り立つのは「ラベルがファイル名で L1 にある」
# 3. 注入ノードに id を入れ忘れると networkx が連番 int を振る
# graphify の _find_node が nid.lower() で AttributeError を出し
# path / explain が全滅する。norm_label も必須
もう1点、graphify update は graph.json を丸ごと書き直すため、注入は更新のたびにやり直しになる。update を直接叩かせず、更新用のラッパースクリプトに入口を寄せた。
入れなかったもの
導入時に見送った3つも、理由とセットで残しておく。
| 見送ったもの | 理由 |
|---|---|
| インストーラが登録する PreToolUse フック | 絶対パス直書きで、home が違う別マシンには存在しない。無人で回っている定期ジョブが毎ツール呼び出しでフック失敗する。加えて全 Bash 呼び出しに「まずグラフを検索せよ」が注入されるため、読むのが本体の執筆セッションでは純粋な妨害になる |
| 記事コンテンツのグラフ化 | .md はモデル API を通るため、597本を食わせるとトークンを溶かす。得られる「記事同士のリンク」は公開登録簿と検証スクリプトが既に正本として持っている |
| 生成物ディレクトリのコミット | 5MB超。7秒・費用ゼロで再生成できるので各マシンで作り直す |
グラフが持つのは「今の構造」だけで、「なぜそうしたか」は別の道具(セッション履歴の検索)の担当である。日本語の自然文クエリも弱く、見出しに fuzzy match してノイズを返すため、query より affected / explain / path を使う。
次回予告
次はモデル別(Opus/Sonnet/Fable/Haiku)の使用比率とコスト構成比を見る。用途に応じた使い分けが実際にコストを下げているかを確認する回にする。