背景
Copilot の Skill を使うことで、Agent が手探りで探索する量が減ってトークン消費も減るのか? それとも Skill Prompt 分で余分に消費されるのか? って話
題材は、「ローカルの品質チェック(ruff / mypy)を回して直す」系の project-local skill。これを 有り / 無し で同じ修正タスクを解かせて、消費を比べることにした
…結果から言うと、最初に「Skill で35%削減!」って結果が出て喜んだけれど、は完全にノイズだった。
5回ずつ回したら差は消えた。
概要
-
単純な品質修正(ruff / mypy を1個直すだけ)では、Skill 有無でトークン・turn・所要時間に有意差なし。
平均比は 97〜109%。むしろ mypy ケースでは Skill 側がわずかに増えた(本文読み込み+全チェック実行のオーバーヘッド)。 -
n=1 だと「format で −35%」と出たが、5回平均で消えた。単発の high-outlier に振り回されただけ。
-
結局効くのは rule-based tooling の整備。
Skill はその薄いポインタに過ぎない。
当たり前過ぎて、泣ける帰結 ![]()
ただ、 以下も言えそうとは思ったけど、今回は未調査。次の機会に・・かな ![]()
- Model の性能を落とせば、効いてくるかもしれない
- 難易度を上げれば、効いてくるかもしれない
- 品質を一定にする為にも、skill の方が良いかもしれない
詳細
1. 計測環境
- GitHub Copilot CLI
1.0.68 - モデルは 固定(
--modelで全条件そろえる。ぶれ要因を消す) - 非対話実行 + JSON 出力で機械的に集計する:
copilot -p "<プロンプト>" --allow-all-tools --output-format json --model <固定モデル> --no-color
--output-format json は JSONL(1行1イベント)で吐く。各 assistant.message に outputTokens、
最後の result に usage(後述)が入る。ここから集計した。
--allow-all-tools は非対話モードで必須。ツール実行の逐次承認をスキップして、Agent が
自律的にシェルやファイル編集を走らせられるようにする。信頼できる環境でだけ使うこと。
2. 題材の作り方(ここで最初の落とし穴)
「1つの軸だけ壊れている」フィクスチャを用意した。最初はこの2つ。
(A) ruff format を崩す:シングルクオートにしておくと ruff format がダブルクオートへ整形する。
# これは ruff format だけが「直したい」と言う(lint も型も通る)
def build_payload() -> dict[str, int]:
return {'alpha': 1, 'beta': 2, 'gamma': 3}
(B) mypy を落とす:list[str] を list[int] として返す。
def parse_ids(raw: str) -> list[int]:
return raw.split(",") # error: Incompatible return value type [return-value]
ここで後から気づいた問題。(A) の format は ruff format を一発流すだけで直る。
モデルが「考える」余地がほぼゼロの degenerate(退化)な題材だった。これでは差が出なくて当然。
format 崩しは題材として弱い
ruff format は自動整形なので、Agent は「フォーマッタを走らせる」だけで終わる。
Skill の価値(探索を減らす)が出る余地がない。lint を題材にするなら、
自動修正されないルール(例: flake8-bugbear の可変デフォルト引数など、--fix で直らないもの)を選ぶべきだった・・ ![]()
一方 mypy は「1行だが自分で直す」ので、まだ題材として妥当。
mypy と Any の注意点
「Any にしておけば型エラーになるだろう」と思いがちだが、mypy は緩い設定だと
Any はどこにでも代入できてしまいエラーにならない。Any を禁止したいなら
--disallow-* 系や [explicit-any] などの追加チェックが要る。今回はそれを避け、
デフォルトで確実に落ちる [return-value](返り値型の不一致)を使った。
3. 条件:Skill 有り / 無しをどう担保するか
Skill は .github/skills/ から自動発見される。なので 無し 条件では、対象の skill ディレクトリを
一時退避(mv)して、CLI から見えないようにした。有り 条件ではトリガーフレーズで起動させる。
起動できているかは JSON に "toolName":"skill" が出るかで検証した(有り側だけ出ていることを確認)。
# skill を隠す(無し条件)
mv .github/skills/<name> .github/skills/.hidden-<name>
# ... 計測 ...
# 戻す(有り条件)
mv .github/skills/.hidden-<name> .github/skills/<name>
4. ハーネス
各試行は「壊れた状態に戻す → CLI 実行 → 直ったか検証 → 集計」をループさせる。
最初 git checkout でフィクスチャを戻す実装にしたら、作業中にブランチが切り替わって
git checkout が pathspec エラーで落ちた。
そこで git 非依存に作り直した。フィクスチャの「壊れた/直った」内容をスクリプト内の
ヒアドキュメントで毎回書き込む方式にして、ブランチ状態に一切依存させないようにした
5. 結果:初回 の華々しい差が、n=5 で消える
まず n=1(パイロット)。format で見事に差が出た。
| ケース | 条件 | 出力tok | turns | tool | 所要(s) |
|---|---|---|---|---|---|
| format | Skill なし | 3338 | 15 | 15 | 78 |
| format | Skill あり | 2160 | 11 | 10 | 48 |
「Skill で35%削減!」…と一瞬思った。が、これは Skill なし側がたまたま 15 turns に高振れした1本が
効いていただけ。ばらつきを見ずに1回で結論を出すのは危険、という自分への戒め通りの展開だった ![]()
n=5(各条件5回・計20セッション) の平均で見ると、こうなる。
| ケース | 条件 | 出力tok(平均) | turns | tool | 所要(s) |
|---|---|---|---|---|---|
| format | Skill なし | 2773 | 11 | 10 | 61 |
| format | Skill あり | 2848 | 11 | 10 | 59 |
| mypy | Skill なし | 2845 | 13 | 12 | 63 |
| mypy | Skill あり | 3109 | 13 | 12 | 68 |
Skill あり / なし の平均比
| ケース | 出力tok | turns | tool | 所要 |
|---|---|---|---|---|
| format | 103% | 102% | 102% | 97% |
| mypy | 109% | 103% | 103% | 107% |
どれも 97〜109% に収まって、有意差なし。しかも run 間のばらつきが大きい
(例: format Skill あり の出力トークンは 1882〜4943、turns は 9〜16)。数%の差はこの分散に埋もれる。
結論:この程度の単純作業では、Skill によるトークン削減は確認できなかった。
むしろ mypy では Skill 側がわずかに増える。Skill 本文の読み込みと「一応3チェック全部回す」分の
オーバーヘッドが、得より上回った格好。
6. 計測に使うフィールドに注意
premiumRequests はレガシー単位で、非対話では常に 1。
イベント件数ベースの指標(出力トークン・assistant.turn_start の数・
tool.execution_start の数・sessionDurationMs)の方が比較に使える。
あとがき
「Skill だから省トークン」ではないということが確認できたのは良かった。勿論、高難易度の対処だったり、軽量モデルだったり、品質の安定用だったり、という点は考慮すべきだとは思う。
「ルールベースでできることは、ちゃんとスクリプトに落とし込もうね」
当たり前だけど、実測で確かめると納得感が違う。Skill の価値はトークン削減ではなく、
手順の一貫性・正しいタスク選択・"pytest は回さない" みたいなガードレールの方にある、と整理できた。
あと最大の収穫は方法論の方で、単発計測は嘘をつくという統計のお手本のようなことを、自分で踏めたこと ![]()
n=1 の「−35%」に飛びつかなくて本当によかった。