AI向けにコマンド出力を最適化するCLIプロキシ 前編 rtkの仕組みとトークン削減の実測
コマンドの出力をAI向けに圧縮するCLIプロキシrtkを取り上げます。フックで書き換えを自動化する仕組みを説明したうえで、公式が掲げる60〜90%の削減が手元で再現するかを、1か月分の記録から測りました。
はじめに
AIコーディングエージェントは、作業のあいだに何度もコマンドを実行します。その出力はすべてコンテキストに取り込まれ、トークンを消費します。テストを走らせて数百行の成功ログが流れても、AIの判断に必要なのは失敗した数件だけ、ということは珍しくありません。だとすれば、使われない出力は最初から渡さずに済ませたいところです。
今回解説するrtk(Rust Token Killer)は、この無駄に着目したツールです。コマンドの実行はそのままに、出力をAI向けに圧縮するCLIプロキシとして働きます。公式のREADMEは、LLMのトークン消費を60〜90%削減すると掲げています。
トークンを節約する目的は、利用量を抑えることだけではありません。コンテキストウインドウは有限で、埋まるほどAIの性能も落ちていきます。この制約については、「Claude Codeの原則 | 第1回 コンテキストウインドウという制約」で詳しく解説しています。
筆者はClaude Codeと組み合わせて、rtkをしばらく使い、利用リミットに到達するまでの時間が伸びたように感じています。とはいえ、1か月以上たった今、公式が掲げる60〜90%という削減が手元で起きているのかは確かめていません。そこで、削減量を測り直しました。
この記事ではrtkの仕組みと、実測の結果を紹介します。先に結果をお伝えすると、筆者の使い方では、コマンド出力の削減率は50.2%で、公式の掲げる数値には届きませんでした。後に詳しく検証内容をお伝えしますが、モデルへの入力全体で見た削減は20%ほどにとどまります。
なお、検証に使ったのはrtk 0.43.0とClaude Codeの組み合わせです。
人間向けのCLI出力はAIには長すぎる
CLIの出力は、人間が端末で読むことを前提に設計されています。テストランナーなら、進捗の表示や成功したテストの一覧、実行時間まで並べます。人間はスクロールして必要な部分だけを拾えますが、AIエージェントは出力の全体をコンテキストとして受け取ります。判断に使うのが「何が失敗したか」「次に何をすべきか」だけだとしてもです。
rtkはこの冗長な出力を削減します。rtk vitestのように既存のコマンドをラップして実行し、人間向けの装飾や繰り返しを取り除いた結果だけを返します。対応コマンドは100を超え、git・npm・vitest・tsc・docker・grepなど、開発で日常的に使うものをカバーしています。
小さな例として、ls -laの出力を生の場合とrtk経由の場合で比べてみます。
生の出力とrtk経由の出力
$ ls -la articles/
total 0
drwxr-xr-x@ 6 kyosuke staff 192 Jun 15 12:18 .
drwxr-xr-x@ 19 kyosuke staff 608 Jul 19 22:54 ..
drwxr-xr-x@ 4 kyosuke staff 128 Jul 6 19:48 announce
drwxr-xr-x@ 10 kyosuke staff 320 Jul 19 17:46 codegrid
drwxr-xr-x@ 3 kyosuke staff 96 Jan 26 11:57 corporate-site
drwxr-xr-x@ 10 kyosuke staff 320 Jul 6 16:44 essay
$ rtk ls -la articles/
755 announce/
755 codegrid/
755 corporate-site/
755 essay/
所有者や日時など、AIの判断にまず使われない列が削除され、パーミッションと名前だけが残りました。1回の差は小さくても、エージェントは同種のコマンドをセッションのあいだ繰り返すので積み上がっていきます。
フックがコマンドを自動で書き換える
rtkのインストールには、スクリプト、Cargo、ビルド済みバイナリなど複数の手段が用意されています。手順は公式のINSTALL.mdにまとまっています。
Claude Codeと組み合わせる場合は、フックで自動化します。rtk init -gを実行し、表示に従ってClaude Codeの設定にフックを登録します。反映のため再起動すると、エージェントがBashツールに渡したコマンドは、実行の直前にrtk同等のコマンドへ透過的に書き換えられます。git statusを実行したつもりでも、実際に走るのはrtk git statusです。プロンプトや運用を変える必要はありません。
書き換わるのはBashツールのコマンドだけです。Claude Code組み込みのReadツールやGrepツールはrtkを経由しません。
どのコマンドがどう書き換わるかは、rtk hook checkで確認できます。
rtk hook checkによる書き換えの確認
$ rtk hook check "git status"
rtk git status
$ rtk hook check "vitest run"
rtk vitest
削減の半分は1コマンドから
さて、冒頭で「筆者の使い方では、コマンド出力の削減率は50.2%」とお伝えしましたが、この削減率は本当にモデルに届いているのでしょうか。何が削減されているのかまで知っておかないと、どれだけトークン節約に寄与しているかがわからないかもしれません。そこで、削減効果の内訳を調べてみました。
削減効果の集計にはrtk gainを使います。rtkが処理したコマンドについて、生の出力と圧縮後の出力の差を記録し続ける機能です。筆者の統計は次のとおりでした。
rtk gainの出力
Total commands: 4522
Input tokens: 19.1M
Output tokens: 9.5M
Tokens saved: 9.6M (50.2%)
tokens(トークン)と表示されますが、これはモデルが数えたトークンではなく、rtkが出力のバイト数を4で割った値です。273,678バイトのJSONは68,420トークンとして記録されていました。実際のトークン数は内容によって変わるので、以下ではrtkの数字をバイト数に戻して扱います。
4,522回のコマンドを処理して、生出力なら約76MBあったものが38MBに減った、という集計です。削減率は50.2%。公式の60〜90%には届きませんが、体感の裏づけとしては十分な数字に見えました。
ところが、内訳を見ると様子が変わります。rtkは1コマンドごとの記録をSQLiteに残していて、設定ファイルと同じディレクトリのhistory.dbに推定値と実行したコマンドが入っています。削減量の大きい順に並べると、上位はこうなりました。
削減量の上位5件
$ sqlite3 history.db "select saved_tokens*4, substr(original_cmd,1,44) \
from commands order by saved_tokens desc limit 5;"
19540184|grep -n <entry>\|読書\|花火 /Users/kyosuke/GitHu
5121052|grep -n ke_pri\|re_pri JMdict_e.xml
1171844|cat /Users/kyosuke/.claude/projects/-Users-k
843828|git diff pipeline/data/generated/distractors
718868|grep -n re_restr /Users/kyosuke/GitHub/kyosu
左の列が削減バイト数です。最大の1件が19.5MBで、記録された削減38MBの51%を占めていました。中身は、辞書データのJSONやXMLに対するgrepです。最大の1件を含めた上位10件の合計が77%、さらに上位50件では94%に達します。残る4,472件は件数で見ると全体の98.9%ですが、削減量への寄与は6%にとどまりました。
1コマンドあたりの中央値は、生の出力が160バイト、圧縮後が84バイトでした。削減の中央値は0バイトで、半分以上のコマンドは1バイトも減っていません。つまり、削減の恩恵は一部の大きなコマンドに偏っているようです。
モデルに渡らない出力との比較
この最大の1件となる19.5MBは、rtkがなければ本当に消費されていたのでしょうか。
Claude CodeはBashツールの出力をそのまま渡すわけではありません。長すぎる出力は中央を省略して切り詰めます。筆者のセッション記録から同じ期間のBash結果4,029件を集計すると、中央値は262字、90パーセンタイルで1,879字、最大が28,630字でした。3万字を超えるものは1件もありません。
巨大データへのgrepが100万文字を出力したとしても、モデルが受け取るのは切り詰められた3万字弱です。「19.5MBの削減」は、そもそも起こらなかった消費との比較になります。
そこで、実際にモデルに届く量で計算し直してみました。切り詰めの単位は文字数ですが、rtkの記録はバイト数です。コマンドの出力はASCII中心なので、3万字を3万バイトとみなして削減量を引き直しました。生の出力と圧縮後の出力の両方に同じ上限をかけ、その差を合計します。圧縮した出力も、長ければ同じように切り詰められるからです。
こうして得た削減は3.4MB、rtkが表示する38MBの9%でした。生の出力が3万バイトを超えたコマンドは72件、全体の1.6%しかありませんでした。そのうち8件は、圧縮したあともなお3万バイトを超えています。rtkを通しても通さなくても切り詰められるので、この8件の削減は0と数えました。上限をどこに置くかで数字は動きますが、桁は変わりません。
| 見方 | 削減量 |
|---|---|
rtk gainの表示 |
38MB |
| 上限を10万字と置く | 6.7MB |
| 上限を3万字と置く | 3.4MB |
| 上限を1万字と置く | 1.8MB |
rtk gainが示した削減量は38MBでしたが、筆者の記録での最大に合わせて上限を3万字と置くと、削減は3.4MBにとどまります。
入力全体で見た削減幅
この3.4MBの削減は、セッションのどれくらいを占めるのでしょうか。記録から、モデルへの入力となったものを文字数で数えました。対象はモデルが受け取る側で、rtkが読み込む側とは向きが逆です。残っていたのは約1か月分なので、以下はその範囲で揃えた数字です。
合計は1,281万字でした。ツールの結果が832万字(65.0%)、人間の指示が350万字(27.3%)、AIの応答が99万字(7.7%)です。ツールの結果のうちBashは316万字、つまり入力全体の24.7%です。もっとも多いのはClaude Code組み込みのReadの419万字(32.7%)で、こちらはrtkを通りません。人間の指示として数えた350万字には、CLAUDE.mdやリマインダーの注入も含まれます。
rtkが触れるのはBashの24.7%だけです。残りの4分の3は、どれだけ圧縮しても減りません。
同じ範囲のrtkの削減は3.3MB、1バイトを1字とみなせば約330万字です。圧縮がなければBashの結果は646万字に膨らみ、入力の合計も1,611万字になります。届いていたはずのBash出力の半分は消えている計算で、入力全体で見た削減幅は20%ほどでした。この計算はシステムプロンプトやツール定義を分母に入れていないので、実際の比率はもう少し小さくなります。
入力が20%減れば、同じ作業を1.2倍ほど長く続けられます。セッションが伸びたと感じていたのは、この余裕と、仕切り直す回数が減ったことの重なりでしょう。公式の60〜90%はコマンド出力の削減率なので、この20%とは分母が違います。並べて比べる数字ではありませんが、コンテキストウインドウの残りとしては小さくない差です。
コマンドごとの削減率
20%という平均は、どんな作業でも一様に得られるわけではありません。同じ上限をかけてコマンドの種類ごとに集計すると、削減率は大きく散らばりました。
| コマンド | 回数 | 削減率 | 削減量 |
|---|---|---|---|
vitest |
57 | 98.7% | 1.0MB |
ps |
18 | 88.8% | 396KB |
find |
62 | 85.4% | 29KB |
ls |
522 | 60.9% | 341KB |
cat |
478 | 56.9% | 967KB |
grep |
738 | 44.1% | 431KB |
git |
1,605 | 23.9% | 223KB |
| npmスクリプト | 449 | 2.4% | 6KB |
curl |
40 | 0.0% | 0 |
高いのは、定型の行を大量に吐くコマンドです。Vitestの成功ログ、ps auxのプロセス一覧、ls -laのメタデータ列は、AIが読む必要のある情報だけを残せば数分の1になります。中位はファイルの読み出しと検索で、cat相当が56.9%、grepが44.1%でした。
低いほうを見ると、gitは23.9%です。もっとも多く実行しているのに、削減は小さいままです。git statusやgit diffの出力はもともと簡潔で、削り代がありません。npmスクリプトの2.4%とcurlの0.0%は、出力の形が決まっていないため、rtkが手を出せない領域です。
削減が0以下だったものの内訳は、サイズが変わらなかった2,110件と、出力の増えた355件です。増加のほうは中央値32バイト、最大でも124バイトで、80%はgit addやgit pushのように何も表示しないコマンドです。出力のないところにrtkの1行が足された結果なので、実害はありません。
ここから、自分の作業でどれだけ削減されるかはある程度見積もれます。テストを何度も回すタスク、環境や動いているプロセスを調べるタスクでは削減が大きくなります。Git操作中心のレビューやリファクタリングは、実行回数が多くても削減が限られます。npmスクリプトやAPIの応答を読む場面では、ほとんど変わりません。コードを読み進めるときの主役は組み込みのReadなので、そもそもrtkの対象外です。
20%は手数が増えない前提
ここまでの数字には、確かめていない前提が1つあります。圧縮してもAIの手数は変わらない、という点です。情報が落ちて追加のコマンドが走れば、その分だけ削減は目減りします。
手元のセッション記録でその痕跡を探すと、圧縮の通知を見たAIが生の出力を取り直した例は1件、生実行の明示的な指定は79件(Bashの実行全体の2%ほど)でした。大量に発生している様子はありません。ただしrtk導入前の記録が残っていないので、導入によって手数がどれだけ増えたのかは測れていません。20%は、ここから差し引かれる可能性を含んだ数字です。
手数まで含めて測った外部の検証
JetBrainsのDenis Shiryaevが、rtkの有無をそろえた86タスク・425回の試行でA/B比較しています。結果はDoes "rtk" skill really cut agent tokens by 60-90%? We tested itにまとまっています。
タスクを終えるまでの新規入力トークンは+3.2%(p=0.23)でした。トークン消費に連動する費用でも、推論の努力度が低い設定では中央値が7.6%増加し(p=0.004)、高い設定では±0%でした。
どちらの指標でも、削減は見えていません。
記事はrtk gainの数字の性質をこう表現しています。
a tool's self-reported savings are a claim about its counterfactual, not about your bill.
ツールが自己申告する削減は、請求額についての主張ではなく、起こらなかった場合についての主張だ。
対象のタスクや環境は筆者のものと違うので、この結果がそのまま手元に当てはまるわけではありません。ただ、筆者が測れなかった手数の増分まで含めるとどうなるかを知る手がかりにはなります。
まとめ
rtk gainが表示した削減率は50.2%でした。ところが内訳を見ると、削減量の51%は1コマンドに由来し、上位50件で94%に達します。残る98.9%のコマンドが担うのは6%でした。しかもこうした巨大な出力は、rtkがなくてもClaude Code側で切り詰められていたはずです。起こらなかった消費を、削減に含めていたわけです。
切り詰めたあとの量で計算すると、削減は3.4MB、rtk gainが表示する38MBの9%です。モデルへの入力全体に対しては20%ほどでした。公式の60〜90%と同じ土俵にあるのは最初の50.2%までで、それも届いていません。それでも20%は、コンテキストウインドウの残りとして小さくない差です。ただし平均であって、Vitestの98.7%からcurlの0%まで、コマンドによって大きく散らばります。
この20%も、圧縮で手数が増えなければ、という条件つきです。増えた分は手元の記録では測れていません。
自分の環境でどれだけ減ったかは、導入後にAIへ調査を依頼するとよいでしょう。rtk gainの統計だけでなく、history.dbの履歴まで含めて詳しく出してくれます。調べるのにトークンを費やすのは本末転倒に見えますが、rtkを有効に使うには必要なコストです。
次回は、その20%で手数を増やさないための設定を扱います。rtkの圧縮はコマンドごとの解釈に頼っているため、実態とずれると問題が起きます。実際に起きた事故と、そこから決めた「どこまで圧縮させるか」の判断を紹介します。