先日の更新で、Android のコマンドラインで動くエージェントが、標準的なツールに比べてタスク完了まで約3倍速く、トークン使用量を約70%削減した、という数字が出ていました。速くなったこと自体は素直に嬉しいのですが、その数字を眺めているうちに、自分の手元のエージェント運用が「いま実際どれだけのコンテキストを送り、どれだけ待っているのか」をまったく把握できていないことに気づきました。削減の話は、現状を測れて初めて意味を持ちます。
私は個人開発で複数のアプリと複数のブログを並行運用していて、Antigravity CLI に細かな修正タスクを投げる場面が日に何度もあります。けれども「このタスクは妙に遅い」「今日はクォータの減りが早い」という感覚はあっても、それを数字で言える状態ではありませんでした。そこで、まず測るための仕組みを薄く一枚かぶせて、そこから削る、という順番で取り組んだ記録を残しておきます。
何を測れば「無駄」が見えてくるのか
トークンの正確な消費量は、CLI 側のクォータ画面や利用状況のレポートを見るのが確実です。ただ、画面の数字はタスク単位ではなくセッション全体の合算になりがちで、「どのタスクが重いのか」までは分解できません。
そこで私は、自分が制御できる代理指標を二つ決めました。
待ち時間(wall-clock 秒)
これは体感に直結します。エージェントが返ってくるまでの実時間を秒で記録するだけでも、タスクごとの重さの差がはっきり見えてきます。
明示的に渡したコンテキストのバイト数
意味検索が裏で何を拾うかは完全には制御できませんが、「自分が手で渡したファイルや範囲」のバイト数なら正確に数えられます。トークンそのものではないものの、無駄に大きな入力を送っている兆候はこれで十分つかめます。
この二つを、タスクに付けたタグと一緒に1行ずつ追記していく。たったそれだけの台帳(ledger)を起点にしました。
1タスクを記録する薄いラッパー
実際の計測は、CLI を直接叩く代わりに、前後で時刻とコンテキスト量を書き出すラッパー経由で呼ぶようにしました。コマンドや引数の形は使っている版に合わせて読み替えてください。ここで大事なのは「呼び出しのたびに、聞かずに自動で1行残る」ことです。
#!/usr/bin/env bash
# agy-run.sh — Antigravity CLI を呼ぶ前後で待ち時間とコンテキスト量を記録する薄いラッパー
set -euo pipefail
LEDGER = "${ HOME }/.agy/ledger.csv"
mkdir -p "$( dirname " $LEDGER ")"
[ -f " $LEDGER " ] || echo "ts,tag,context_bytes,context_files,seconds,status" > " $LEDGER "
TAG = " ${1 :? usage : agy-run < tag > < prompt-file > [context files or ranges...] } "
PROMPT_FILE = " ${2 :? prompt file required } "
shift 2
CTX = ( " $@ " )
# 明示的に渡したコンテキストの合計バイト数(自分が制御できる代理指標)
# "path:120-164" のような範囲指定はファイル名部分だけを見て概算する
ctx_bytes = 0
for item in "${ CTX [ @ ]}" ; do
f = "${ item %%:* }"
[ -f " $f " ] && ctx_bytes = $(( ctx_bytes + $( wc -c < " $f " ) ))
done
start = $( date +%s )
# 実際の呼び出し。--context で渡したものだけをグラウンディング対象にする
agy run --prompt " $PROMPT_FILE " ${CTX[ @ ] : +--context "${ CTX [ @ ]}" }
status = $?
end = $( date +%s )
printf '%s,%s,%d,%d,%d,%d\n' \
"$( date -u +%FT%TZ)" " $TAG " " $ctx_bytes " "${ # CTX [ @ ]}" "$(( end - start ))" " $status " \
>> " $LEDGER "
exit " $status "
ポイントは三つあります。第一に、set -euo pipefail で途中失敗を握りつぶさないこと。第二に、終了コードも一緒に記録して、あとで「速かったけれど実は失敗していた」タスクを弾けるようにしたこと。第三に、範囲指定(path:120-164)を渡したときはファイル名部分だけを見てバイト数を概算している点です。範囲だけ正確に数えたい場合は sed -n で抜き出して wc -c に流せますが、まずは粗くても続けられる形を優先しました。
記録した ledger を週次で棚卸しする
1行ずつ溜まった CSV は、タグ別に集計して初めて意味を持ちます。私は週に一度、次のスクリプトでタスクの重さ順に並べて眺めています。
#!/usr/bin/env python3
"""ledger_report.py — agy-run.sh が書き出した ledger.csv をタグ別に集計する。"""
import csv
import statistics
import sys
from collections import defaultdict
from pathlib import Path
path = Path(sys.argv[ 1 ]) if len (sys.argv) > 1 else Path.home() / ".agy/ledger.csv"
rows = list (csv.DictReader(path.open( encoding = "utf-8" )))
by_tag = defaultdict( lambda : { "sec" : [], "kb" : [], "fail" : 0 })
for r in rows:
by_tag[r[ "tag" ]][ "sec" ].append( int (r[ "seconds" ]))
by_tag[r[ "tag" ]][ "kb" ].append( int (r[ "context_bytes" ]) / 1024 )
if r.get( "status" , "0" ) != "0" :
by_tag[r[ "tag" ]][ "fail" ] += 1
print ( f " { 'tag' :30 }{ 'runs' :>5 }{ 'median_s' :>10 }{ 'ctxKB' :>9 }{ 'fail' :>6 } " )
for tag, d in sorted (by_tag.items(), key =lambda x: - statistics.median(x[ 1 ][ "sec" ])):
print ( f " { tag :30 }{ len (d[ 'sec' ]) :>5 } "
f " { statistics.median(d[ 'sec' ]) :>10.0f } "
f " { statistics.median(d[ 'kb' ]) :>9.1f } "
f " { d[ 'fail' ] :>6 } " )
並べ替えの基準を「中央値の待ち時間」にしているのは、たまの外れ値より「いつも重いタスク」を先に直したいからです。出力の上の方に、コンテキスト KB が突出して大きいタグが並んでいれば、そこが削りどころになります。私の場合、上位を占めていたのはどれも「ファイルを丸ごと渡していた」タスクでした。
タグの付け方も、振り返って効いてきました。最初は思いつきで名前を付けていたのですが、それでは集計しても粒度がばらついて比べられません。私は「対象の領域+動詞」(たとえば fix-paywall や gen-article-ja)という緩い規則に寄せ、同じ種類の作業が同じタグに集まるようにしました。こうしておくと、レポートの一行が「いつもの作業の平均的な重さ」を表すようになり、先月との比較が素直にできます。
ファイル全文ではなく範囲で渡す
重いタスクの中身を見ると、原因は単純でした。修正したいのは一箇所なのに、ファイル全体を毎回コンテキストに積んでいたのです。1,200 行あるページコンポーネントのうち、触りたいのは数十行だけ、という状況です。
# ❌ ファイル全文を渡す(無関係な 1,200 行も一緒に送られる)
agy-run fix-paywall prompt.md src/app/article/page.tsx
# ✅ 該当範囲だけ渡し、残りは意味検索に任せる
agy-run fix-paywall prompt.md "src/app/article/page.tsx:120-164"
私の手元では、ページコンポーネントを全文(約 38KB・1,200 行ほど)渡していたタスクで、待ち時間が 70 秒前後でした。渡す範囲を 120〜164 行(約 1.5KB)に絞ったところ、25 秒前後まで縮みました。体感で 2〜3倍速くなった印象と、台帳の数字がきれいに一致したときは、測ってよかったと素直に思いました。
範囲指定にすると応答が雑になるのでは、と最初は心配しましたが、実際には逆でした。無関係なコードが混ざらないぶん、エージェントが「どこを直すべきか」で迷わなくなり、的外れな差分が減ったのです。意味検索は、範囲の外で必要になった定義をきちんと拾ってきてくれます。
バイト数はトークンではない、という落とし穴
ここで注意点を一つ。ラッパーが数えているのは「自分が明示的に渡したバイト数」であって、トークンそのものでも、意味検索が裏で読み込んだ総量でもありません。だから、この代理指標を「絶対のトークン量」として扱うと判断を誤ります。
私が実際にハマったのは、コンテキストを範囲に絞ったのに待ち時間がほとんど変わらないタスクがあったことです。調べると、意味検索が大きな生成ファイル(ビルド成果物)を関連扱いで拾い込んでいました。入力を小さくしても、検索側が太っていれば効果は出ません。対処として、検索の対象から外したいディレクトリを除外設定に加え、生成物をグラウンディングの範囲から締め出しました。代理指標と、クォータ画面の実際の数字を月に一度だけ突き合わせる習慣を持っておくと、こうしたズレに早く気づけます。
つまり、この台帳は「真実」ではなく「あたりをつけるための地図」です。地図として割り切って使うぶんには、十分すぎるほど役に立ちます。
もう少し正確に範囲のバイト数を数えたくなったら、ラッパーの集計部分を sed -n '120,164p' file | wc -c のように差し替えれば、渡した行だけを正確に測れます。ただ私は、正確さを上げるほど運用が面倒になって続かなくなる、という失敗を何度もしてきました。粗いまま毎回残ることのほうが、精密だけれど途切れる記録より、最終的に役に立ちます。
放置運用にこそ効かせたい
私がこの仕組みを一番ありがたく感じているのは、AdMob 連携アプリの売上集計や、ブログの定期的な記事整合性チェックのように、人が見ていないところで自動で回しているタスクです。こうした放置運用は、一回あたりは小さくても回数が多く、無駄なコンテキストがそのまま積み上がってクォータを静かに削っていきます。
自動処理のスクリプトからも agy-run.sh 経由で呼ぶようにしておけば、台帳には人手のタスクと同じ粒度で記録が残ります。週次のレポートで「夜間に回しているタスクのコンテキストが先月の倍になっている」と気づけたときは、原因が参照ファイルの肥大化だと特定できて、本番運用を止める前に手を打てました。App Store や Google Play への配信前チェックのように、失敗が後工程に響くタスクほど、終了コードまで残しておく価値があります。
判断の指針として、私は「同じタグのタスクが中央値で 30 秒を超え、かつコンテキストが 10KB を超えていたら、範囲指定への置き換えを検討する」という緩い線引きを置いています。数字そのものより、線引きを一つ決めて測り続けることをお勧めします。基準は手元の運用に合わせて動かしてかまいません。
もう一つ、放置運用で地味に効いたのは、失敗の偏りが見えるようになったことです。終了コードを残しておくと、レポートの fail 列に「特定のタグだけ夜間に時々こける」といった癖が浮かび上がります。私の場合、ネットワークが不安定な時間帯に走らせていた集計タスクが、月に数回だけ静かに失敗していました。待ち時間やコンテキスト量だけを見ていたら気づけなかったはずです。速さの計測のついでに、信頼性の穴まで一緒に見えてくるのは、台帳を続けるうえでの思わぬ収穫でした。
次の一歩
まずは agy-run.sh を1ファイル置いて、いつものタスクを一日だけこのラッパー経由で呼んでみてください。一日分の ledger が溜まれば、自分のどのタスクが重いのかが、感覚ではなく数字で見えてきます。削るのは、その地図ができてからで十分です。
測ることと削ることを分けて考えられるようになってから、私自身、エージェントへの指示の出し方が少し落ち着いた気がしています。同じように複数のプロジェクトを回している方の参考になれば嬉しいです。