AIエージェントの構成比較で評価環境に騙されない

AI エージェントの指示やハーネスを変えてスコアを比べても、評価環境に穴があると結論を誤る。ISUCON2026 を無人で解いた準備記録から、比べる前に確かめる 6 点と手順を解説する。

AIエージェントの構成比較で評価環境に騙されない

本記事はプロモーションを含みます。

はじめに: スコアが上がっても、構成が良くなったとは限らない

構成を比べる前に、起動条件・ばらつき・環境の上限・追試・ watchdog・素の構成との差の 6 点を確かめる

AI エージェントに渡す指示や skill、hook を変えて、同じ課題を解かせてスコアを比べる。よくなったら採用し、悪くなったら戻す。この比べ方は、評価する環境が正しいことを前提にしている。

前提が崩れていると、エージェントは筋の通った行動をしているのに、比べた結論が誤る。結論を先に書くと、比べる前に次の 6 点を確かめる。

確かめること怠ると起きること
実行ごとにまっさらな状態で起動しているか手元の設定が混ざり、構成どうしを比べられない
同じ構成を測り直したときの幅偶然の上振れを改善と数える
評価環境の上限と、本番との違い環境の不備をアプリの限界と読み、逆向きに直す
追試が、初期化の前のデータを見ているか再起動でデータが消える構成を最高点として残す
watchdog が「止まった」と「入力待ち」を分けているか止まった run を見逃す、空のターンに費用を払う
作り込んだ構成を、素の構成と比べたか足した手順が足を引っ張っていても気付かない

何の話か: ISUCON2026 を無人で解いたエージェントの 3 か月

1 時間時点で、素に近い bare 構成は作り込んだ full 構成の最高点を超えていた

ISUCON2026 で優勝しました(準備編)

Zenn

  • @kawasy さんは、人が介入しない Claude Code のセッションで ISUCON2026 に出て優勝した。この記事はその 3 か月の準備の記録
  • 過去問の環境を AWS に立て、エージェントに 8 時間解かせて記録を回収する「評価ランナー」を作り、評価 run を 159 回回した。issue は評価の仕組みに関するものが 409 件で、エージェント本体に関するものより多い
  • 作り込んだ構成 (full) は、素の Claude Code に近い構成 (bare) に負けた。最後は bare に、評価で失敗した場面の手当てを 1 つずつ足した構成で本番に出た
  • 振り返りで、評価環境の不備で判断を誤った例を 4 件挙げている

この記事では、元記事で起きた事故を、自分のエージェントや prompt を比べるときの点検項目に置き換える。ISUCON に限らず、エージェントに課題を繰り返し解かせて構成を選ぶ場面なら同じことが起きる。

1. 実行ごとにまっさらな状態で起動する

普段の user skill・MCP・~/.claude/CLAUDE.md・親ディレクトリの CLAUDE.md が混ざると、run ごとに条件が変わる

普段使っている Claude Code には、user skill、plugin、MCP サーバー、~/.claude/CLAUDE.md、auto memory が入っている。これが評価の run に混ざると、run ごとに条件が変わり、構成どうしを比べられない。元記事の筆者は、切り離す手段を 1 つずつ試して確かめている。

混ざるもの元記事で採った手段確かめた結果
user skill・plugin・auto memoryCLAUDE_CONFIG_DIR を評価専用のディレクトリに向ける消えた。そのディレクトリで事前に 1 回ログインしないと起動できない
MCP のツール--strict-mcp-config消えた
~/.claude/CLAUDE.md--setting-sources project,local残った。settings.json にしか効かず、ファイル自体を空にした
親ディレクトリの CLAUDE.md作業ディレクトリを開発用の repo の外に、run ごとに作って捨てる読まれなくなる

最後の行は、Claude Code が作業ディレクトリと、その上にあるすべてのディレクトリの CLAUDE.md を読むためだ (公式ドキュメント)。評価用のディレクトリを開発用の repo の中に作ると、開発用の指示が評価の run にも載る。

元記事の手段を起動の形にすると、こうなる。権限は --dangerously-skip-permissions を使わず、作業ディレクトリに置く .claude/settings.json に allow と deny を書く。

mkdir -p "$HOME/.agent-eval"
run_dir=$(mktemp -d "$HOME/.agent-eval/run.XXXXXX")
cp -r harness/. "$run_dir/"
cd "$run_dir"
CLAUDE_CONFIG_DIR="$HOME/.agent-eval/config" \
  claude -p "$(cat prompt.md)" --strict-mcp-config --setting-sources project,local

この形は元記事の記述と claude --help から組んだもので、筆者の手元では動かしていない。元記事の筆者は、run の前に同じ起動方法で読み込まれたファイルの一覧を出させ、想定どおりかを確かめる検査も入れている。手段を足した後は、効いたかを毎回確かめる。

--bare も同じ目的に使える。CLI リファレンスでは、hook、skill、plugin、MCP サーバー、auto memory、CLAUDE.md の自動での読み込みを飛ばすと書かれている (CLI reference)。ただし claude --help によると、認証は ANTHROPIC_API_KEY か --settings で渡す apiKeyHelper だけになる。Stop hook のように hook で動きを足すハーネスを比べるなら、何が読まれ何が読まれないかを先に確かめてから選ぶ。

2. 同じ構成を何度か測り、ばらつきの幅を先に知る

同じ commit を 58 回測るとスコアに 21% の幅があり、同じ構成の 3 回で 2.4 億点と 1,940 万点に分かれた問題もあった

元記事では、同じ commit で 58 回回したベンチマークのスコアに 21% の幅があった。実装を変えなくても最高点が更新されるので、終盤の最高点の更新だけでは改善を確かめられない。

エージェント自体の選ぶ手もばらつく。同じ bare の構成で同じ問題を 3 回解かせた結果が、212,472,000 / 240,412,000 / 19,409,000 点だった。低かった回では、エージェントが自分で入れた手を、規則に反するかもしれないという懸念から 1 回も測らずに外していた。

構成を比べる前に、変えていない構成を何回か測って幅を出しておく。その幅に収まる差は、改善とも悪化とも数えない。

#!/usr/bin/env bash
set -euo pipefail
# 使い方: ./measure.sh <回数> <スコアを 1 行で出すコマンド...>
n=$1
shift
for _ in $(seq "$n"); do
  "$@"
done | sort -n | awk '
  { s[NR] = $1 }
  END {
    min = s[1]; max = s[NR]; med = s[int((NR + 1) / 2)]
    printf "n=%d min=%d median=%d max=%d range=%.1f%%\n", NR, min, med, max, (max - min) / med * 100
  }'

乱数でスコアを返すコマンドで動作を確かめた結果がこれだ。

$ ./measure.sh 10 sh -c 'echo $((1000 + RANDOM % 200))'
n=10 min=1088 median=1123 max=1198 range=9.8%

元記事の筆者も、1 回の結果で結論を広げすぎないようにしたと書いている。effort を xhigh と high で 1 本ずつ比べた run でも、どちらも 1 回なので effort の影響かは分からないとしている。

3. 評価環境の上限と、本番との違いを先に確かめる

ベンチマーカーの fd の上限が 1024 のままだと、エージェントはそれをアプリの限界と読み、アプリの処理を絞った

元記事で、評価環境の不備がエージェントの判断を誤らせた例が 2 つある。

  • 評価環境の systemd の unit に LimitNOFILE が無く、ベンチマーカーの fd の上限が既定の 1024 のままだった。負荷走行の一部が失格になり、エージェントはそれをアプリの性能の上限と解釈して、アプリの処理を絞った
  • 評価環境のポータルが、ベンチマーカーの出力を全部返していた。各年の本番のポータルは末尾の一部しか返していなかったので、本番では見えない行を読む練習をしていた

どちらも、エージェントは与えられた環境で筋の通った行動をしている。直すのは環境の側だ。

fd の上限の既定は、systemd の DefaultLimitNOFILE= が 1024:524288 (soft が 1024) と定めている (systemd-system.conf)。筆者の手元 (Manjaro) でもこの値だった。

$ systemctl show -p DefaultLimitNOFILE,DefaultLimitNOFILESoft
DefaultLimitNOFILE=524288
DefaultLimitNOFILESoft=1024

評価に使うサービスごとに、効いている値を見る。<unit> はベンチマーカーやアプリの unit 名に置き換える。

systemctl show -p LimitNOFILE,LimitNOFILESoft <unit>
grep 'open files' /proc/"$(systemctl show -p MainPID --value <unit>)"/limits

足りなければ systemctl edit <unit> で drop-in に書き、サービスを再起動する (systemd.exec)。

[Service]
LimitNOFILE=65536

上限のほかにも、元記事では評価環境の側で次のものが結果を動かしていた。

  • ベンチ機で OS の保守ジョブ (unattended-upgrades) が負荷走行と重なって動いていた。イメージを作り直して止めた
  • lite と bare の 17% の差は、配置によるベンチ機からの往復時間 (0.18ms と 0.38ms) で説明できた。同じバイナリを 2 通りの配置で 36 回測って確かめている

確かめる順は、エージェントの構成をいじる前だ。元記事の筆者も、評価環境の正しさと本番との違いは、もっと序盤に確認しておくべきだったと振り返っている。

4. 再起動の確認は、初期化の前のデータを見る

初期化を呼んでから確かめると、再起動でデータが消える構成でも通る。再起動 => 初期化の前に読む、の順にする

ISUCON には、競技の後にサーバーを再起動し、アプリが動き続けるかを確かめる追試がある。元記事の評価環境もこれを再現していたが、負荷走行の最初に初期化を呼ぶ作りで、再起動前のデータを 1 つも見ていなかった。

その結果、データをメモリだけに持ち、再起動で消える構成が 199,818,000 点の記録として残った。記録を洗い直すと、本来なら失格になる run がほかにも見つかっている。

直し方は順序だ。

  1. データを書き込む
  2. 全台を再起動する。順序は毎回変える
  3. 初期化を呼ぶ前に、書き込んだデータが読めるかを確かめる
  4. そのあとで初期化と負荷走行に進む

元記事では、再起動の順序を毎回ランダムにした理由を、規則の「主催者がサーバーを再起動する順序は保証しない」という文面に合わせたためとしている。ISUCON 以外でも、「確かめる手順の前に、確かめたい状態を消す処理が走っていないか」は同じ形で起きる。テストが通るのに壊れている例は「テストもCIも緑なのに壊れた実装を通す穴」にまとめた。

5. watchdog は「止まった」と「入力待ち」を分ける

書き込みが続くと無音の検知が効かず 2 時間止まり、入力待ちを停止と誤認すると continue を約 370 回送った

無人で長く走らせるなら、止まったら止める watchdog が要る。元記事では、この watchdog が両方向に誤った。

  • 止まっているのに気付かない: エージェントが許可されていないツールを呼び、許可のダイアログを出したまま 2 時間 6 分止まった。20 分の無音で止める仕組みは、残っていたベンチマークのループが 72 秒ごとにファイルを書き続けていたため作動しなかった
  • 待っているのに止まったと見なす: API エラーでターンが終わって入力待ちに戻ると、watchdog が作業の終わりと判断して /exit を送り、run が 1 時間 9 分で終わった。別の構成では、入力待ちを停止と誤認した watchdog が約 3 時間で continue を約 370 回送り、トークンを API の単価で換算した額の 66% がこの空のターンの分だった

watchdog が見るのは、ファイルの更新のような間接の信号でなく、エージェントのターンの状態にする。元記事の本番の構成では、watchdog は API エラーで終わったターンにだけ continue を送り、競技終了の 10 分前に /exit を送る形になっている。

評価ランナーを動かす手元の Mac でも、元記事では run が止まっている。バッテリーで動いていた Mac がスリープして約 4 時間、まとめて止まった。Claude のデスクトップアプリを終了したら、スリープを止めていたのがそのアプリだったと分かった。以後は評価ランナーが caffeinate でスリープを止めている。

無人で回すスクリプトの落とし穴は「自動実行のbashが突然落ちる・失敗を見逃す4つの罠」と「AIエージェントが叩くとシェルが止まる・壊れる4つの原因」にも書いた。

6. 作り込む前に、素の構成と比べる

full は出力トークンの 51% を探索記録に使い、書いたコードは bare の半分以下だった

元記事の full は、作業を 4 つのフェーズに分け、仮説と検証を記録し、1 手ごとに commit して測るよう指示していた。同じ ISUCON13 で比べると、最高点はこう並んだ。

最高スコア1h4h8h
bare697,671867,611873,282
full (並列)3,718453,380688,906

差の理由として、元記事は 3 つを挙げている。

  • full はマニュアルを読んで手順どおりに準備するだけで、最初の 50-70 分スコアが初期値のままだった
  • full は探索中の出力トークンの 51% を探索記録に使っていた。書いた量は記録 229k・コード 106k で、bare は記録 7k・コード 224k
  • 「1 つずつ小さく」という指示のため、bare が開始 12 分で選んだ、アプリを丸ごと書き直す大きな手が選ばれにくかった

元記事の筆者は full の開発を止め、bare を土台に、評価で失敗した場面ごとに手当てを 1 つずつ足した。API エラー 1 回で run が終わる場面には continue を送る watchdog、持ち時間を残して手を止める場面には続けさせる Stop hook、という形だ。

普段の開発で同じ結果になるかは分からない、と元記事の筆者は書いている。それでも、手順や道具を足す前に、足さない構成と同じ環境で比べる価値はある。比べなければ、足したものが効いているのか足を引っ張っているのかが分からない。

ISUCON の課題そのもの、つまり計測して律速を見つけて直す流れを人の手で学ぶなら、この本が定番だ。エージェントが出した手を読んで良し悪しを判断するにも、この知識が要る。

改訂新版 達人が教えるWebパフォーマンスチューニング ISUCONから学ぶ高速化の実践 (藤原俊一郎 他著、技術評論社) 画像: 楽天市場

改訂新版 達人が教えるWebパフォーマンスチューニング ISUCONから学ぶ高速化の実践 (藤原俊一郎 他著、技術評論社)

まとめ

  • 評価環境に穴があると、エージェントが筋の通った行動をしても、構成の比較の結論が誤る
  • 評価の run は、普段の設定と親ディレクトリの CLAUDE.md から切り離して起動し、切り離せたかを毎回確かめる
  • 変えていない構成を何回か測り、その幅に収まる差を改善と数えない
  • fd の上限 (systemd の既定は soft 1024) や、本番より多い情報など、環境の側の差を先に潰す
  • 再起動の確認は、初期化を呼ぶ前にデータを読む
  • watchdog はファイルの更新でなく、ターンの状態で止まったかを判断する
  • 作り込む前に、素の構成と同じ環境で比べる

参考