はじめに
選択肢が増えた今、私たちが直面している課題
「このタスク、Codexに任せる? それともClaude Code? あれ、OpenCodeもあったな...」
AIコーディングエージェントを使っている方なら、こんな場面に出くわしたことがあるのではないでしょうか。私もその一人です。毎日の開発で複数のエージェントを使い分けていると、ふと疑問が湧いてきました。
「結局、どのエージェントが何に向いているんだろう?」
モデルは頻繁に更新されるし、公式のベンチマークは存在するものの、自分たちの実際のコードベースでの挙動とは違う気がする。かといって、毎回手動で試して比較するのは時間がかかりすぎる...。
そこで考えました。**「自分たちで、実務に即したベンチマークを作ってしまおう」と。
この記事は、その企画段階の思考プロセスをまとめたものです。実装編は次回お届けします。
なぜ独自ベンチマークが必要なのか
既存のベンチマークでは足りない3つの理由
1. 実際のコードベースと乖離している
公開されているベンチマークは素晴らしいですが、多くは学術的な課題や一般的なコーディング問題が中心です。でも、私たちが日々向き合うのは:
- レガシーコードのバグ修正
- 既存のAPIエンドポイント追加
- 複数ファイルにまたがる複雑な修正
こういった「リアルな現場の課題」を評価したいんです。
2. 比較条件が統一されていない
「あれ、このエージェントは外部ライブラリを追加してるけど、こっちは標準ライブラリだけで解いてる」なんてこと、ありませんか? 公平に比較するには、同じリポジトリ、同じ制約条件で評価する必要があります。
3. 継続的な評価ができない
モデルは日々進化します。今日のベストチョイスが来月も同じとは限りません。定期的に、簡単に再評価できる仕組みが欲しい。
私たちが目指すベンチマークの姿
これらの課題を解決するため、以下の特徴を持つベンチマークを設計することにしました:
✅ 実務に近い課題設定
- 関数のバグ修正(L1)
- RESTエンドポイント追加(L2)
- 複数ファイルにまたがる複合修正(L3)
✅ 公平な比較環境
- 同じリポジトリ、同じプロンプト
- 新規依存追加禁止などの統一ルール
- 外部ネットワークアクセスの制御
✅ 再現可能な評価
- すべての実行ログを保存
- タイムスタンプ付きで結果を記録
- いつでも過去の実行を検証可能
✅ 自動化された継続評価
- ワンコマンドでベンチマーク実行
- CSV/Markdown/HTMLで結果を出力
- モデル更新時も簡単に再評価
どうやって「公平」を担保するか
課題:エージェントごとの癖と特性
Codex、Claude Code、OpenCodeはそれぞれ異なるCLIインターフェース、出力形式、実行モデルを持っています。まるで異なる言語を話す3人の専門家に同じ仕事を頼むようなもの。
ここで重要なのは、「違いを無くす」のではなく「条件を揃える」こと。
解決策:アダプタパターンと統一プロンプト
レストランで例えてみましょう。3人のシェフ(エージェント)に同じ料理を作ってもらいたい時、どうしますか?
- 同じレシピ(プロンプト)を渡す
- 同じ食材と調理器具(リポジトリと制約)を使ってもらう
- 制限時間(タイムバジェット)を設定する
- 完成品を同じ基準(テスト)で評価する
これをコードで実現するのが私たちの設計です。
// 統一されたプロンプト生成
const message = [
`# Task: ${task.task_id}`,
`\n## Requirements`,
...task.requirements.map((r) => `- ${r}`),
`\n## Constraints`,
...task.constraints.map((c) => `- ${c}`),
`\n## Additional Rules`,
`- New dependencies: ${allowNewDeps ? "Allowed" : "Forbidden"}`,
`- Network access: ${allowNetwork ? "Allowed" : "Forbidden"}`,
`- Time budget: ${timeBudget} minutes`,
].join("\n")
**各エージェントは同じプロンプトを受け取りますが、それぞれの強みを活かして解決できます。**これが真の公平性です。
難易度設計:L1からL3まで
実務の複雑さを段階的に再現するため、3つの難易度レベルを設定しました。
🟢 L1:関数のバグ修正(基礎編)
課題例:parseUser
関数がnullで落ちる
task_id: fix-null-check
difficulty: L1
requirements:
- "undefined/nullのuserでもparseUserが落ちないこと"
- "ケース: null, {}, {name:''} を通過"
constraints:
- "新規依存追加は禁止"
- "関数シグネチャは変更しない"
time_budget_min: 15
なぜこの課題?
- 実務でよくある「予期しないnull」への対応
- 単一ファイル完結で、基礎的なコード理解力を測定
- テストが明確で、成否判定がシンプル
🟡 L2:RESTエンドポイント追加(応用編)
課題例:ヘルスチェックエンドポイントの実装
task_id: add-health-endpoint
difficulty: L2
requirements:
- "GET /health が 200 を返す"
- "JSON { status: 'ok' } を返す"
constraints:
- "新規依存追加は禁止"
- "既存のリンタ/テストを緑維持"
time_budget_min: 20
なぜこの課題?
- 複数ファイルの編集が必要(ルーティング、ハンドラ、テスト)
- 既存コードの構造理解が求められる
- スタイルガイド遵守など、文脈に合わせた実装力を測定
🔴 L3:複合バグの復元(実戦編)
課題例:壊れた機能を完全に復旧
task_id: restore-health-and-user
difficulty: L3
context_paths:
- src/utils/parseUser.ts
- src/server/index.ts
- src/server/routes/health.ts
- tests/parseUser.test.ts
- tests/health.test.ts
requirements:
- "parseUser(null)が'Anonymous'を返す"
- "GET /healthが正常動作"
- "すべてのテストが通過"
constraints:
- "新規依存追加は禁止"
- "既存テストとスタイルを維持"
time_budget_min: 30
なぜこの課題?
- 実務で最も多い「複数の問題が絡み合った状況」
- 優先順位付けと全体設計の理解が必要
- デバッグ能力と問題解決プロセスを総合評価
「壊れた状態」をどう再現するか
課題:毎回同じバグを仕込む難しさ
ベンチマークで重要なのは「再現性」です。でも、手動でファイルを編集してバグを仕込んでいたら:
- 人為的ミスが入る
- 時間がかかる
- 同じバグ状態を保証できない
解決策:Fixtures + リセットスクリプト
図書館で本を借りて、返却時に元の場所に戻すイメージです。
.bench-fixtures/
に「壊れた状態のスナップショット」を保存- 必要な時にそれを復元する
- テストで「ちゃんと壊れているか」を検証
# L1レベルのバグを適用
pnpm prepare:l1
# ちゃんと壊れているか確認したい時
pnpm prepare:l1 -- --verify
# 壊した状態でベンチマークまで一気に実行
pnpm bench:l1
仕組みの詳細:
# 1. メインブランチに戻る(本を元の場所に戻す)
git checkout main && git reset --hard
# 2. 壊れた状態を適用(借りたい本を持ってくる)
rsync -av .bench-fixtures/L1/ src/
# 3. 検証モードなら、テストが失敗することを確認
if [ "$VERIFY" = "true" ]; then
pnpm test -- --testPathPattern parseUser
# 期待:テスト失敗 → バグが正しく適用されている証拠
fi
これにより:
- ワンコマンドで任意の難易度に切り替え可能
- 100%同じバグ状態を保証
- 「壊れている」ことの検証も自動化
評価指標:何を測るか、どう測るか
単なる「成功/失敗」を超えて
ただ「動いた」「動かなかった」だけでは不十分です。実務では:
- どのくらい速く解決できたか
- どのくらい正確に解決できたか
- どのくらい安定して解決できるか
これらすべてが重要です。
5つの評価軸
1. ステータス(Status):成功の度合い
success → 完璧! (終了コード0 & 全テスト通過)
partial → 一部成功(終了コードOK or テスト部分通過)
failed → 失敗(両方ダメ)
2. スコア(Score):数値化
const score =
status === "success" ? 1.0 :
status === "partial" ? 0.5 :
0
シンプルですが、集計や比較がしやすい。
3. テスト通過率(Success Rate)
passed / total テスト数 = 成功率
「8割できてる」みたいな部分的成功も可視化。
4. 時間効率(Time Efficiency)
実測時間 / 予算時間 = 効率
- 0.5 → 予算の半分で完了(効率的!)
- 2.0 → 予算の2倍かかった(要改善)
5. 安定性(複数回実行での分散)
同じタスクを3回実行して、すべて同じ結果が出るか? これが実務での信頼性につながります。
集計レベル
エージェント別サマリ:
Codex:
実行数: 15回
成功: 12回 (80%)
平均スコア: 0.87
平均成功時間: 8.3分
時間効率: 0.65 (予算比65%)
タスク別概要:
fix-null-check (L1):
成功: Codex, Claude Code, OpenCode
部分成功: なし
失敗: なし
最速: OpenCode (6.2分)
実行フローの全体像
全体の流れを図解してみましょう。
ポイント:
-
シャッフルで公平性を担保
- 実行順による有利不利を排除
- キャッシュやシステム状態の影響を最小化
-
複数フォーマットで出力
- CSV → データ分析・BIツール連携
- Markdown → GitHub/Notionで共有
- HTML → ブラウザで見やすく表示
- JSON → プログラムでの再処理
-
完全なログ保存
- CLIの標準出力・エラー出力
- Git操作のログ
- いつでも「なぜこの結果になったか」を検証可能
プロンプト設計の哲学
曖昧さとの戦い
「ちゃんと動くようにして」
「きれいに書いて」
「バグを直して」
これらは人間には伝わりますが、AIには曖昧すぎます。
具体性の3原則
1. 要件(Requirements)は入出力例で示す
❌ 悪い例:
- エラーハンドリングをちゃんとする
✅ 良い例:
- parseUser(null) → "Anonymous" を返す
- parseUser(undefined) → "Anonymous" を返す
- parseUser({name: ''}) → "Anonymous" を返す
- parseUser({name: 'Alice'}) → "Alice" を返す
2. 制約(Constraints)は禁止事項を明記
- 新規依存追加は禁止(package.json変更不可)
- 関数シグネチャは変更しない
- 既存のエラー処理仕様を変更しない
なぜ禁止事項?
制約がないと、エージェントによって解決方法がバラバラになります:
- あるエージェントは外部ライブラリで解決
- 別のエージェントは標準ライブラリのみ
→ 公平な比較ができない
3. コンテキスト(Context Paths)で範囲を限定
context_paths:
- src/utils/parseUser.ts
- tests/parseUser.test.ts
「この2ファイルだけ見てください」と明示することで:
- 無関係なファイルへの迷走を防ぐ
- 実行時間を短縮
- 予期しない副作用を防止
アダプタ設計:異なるCLIを統一的に扱う
課題:3つのまったく異なるインターフェース
# Codex
codex exec --experimental-json --skip-git-repo-check "Fix the bug"
# Claude Code
claude --print --output-format json "Fix the bug"
# OpenCode
opencode run --format json "Fix the bug"
コマンドも、オプションも、出力形式も違う...
解決策:共通インターフェース + アダプタ
まず、理想的な共通インターフェースを定義:
interface RunInput {
task: Task // タスク定義
message: string // 統一プロンプト
repoPath: string // リポジトリパス
timeBudgetMin: number // 時間予算
sessionId: string // セッションID
resultsDir: string // 結果保存先
}
interface RunOutput {
agent: string
task_id: string
startedAt: string
endedAt: string
wallTimeSec: number
exitCode: number | null
passedTests: number | null
totalTests: number | null
failedTests: number | null
notes?: string
}
各エージェント用のアダプタは、この共通I/Fを実装するだけ:
// src/adapters/claudecode.ts
export async function runClaudeCode(
input: RunInput
): Promise<RunOutput> {
const startedAt = new Date()
// 1. コマンド実行
const result = await execWithTimeout(
"claude",
["--print", "--output-format", "json", input.message],
{ timeoutMs: input.timeBudgetMin * 60 * 1000 }
)
// 2. テスト実行・評価
const tests = await runTests(input.repoPath)
// 3. 統一フォーマットで返す
return {
agent: "claudecode",
task_id: input.task.task_id,
startedAt: startedAt.toISOString(),
endedAt: new Date().toISOString(),
wallTimeSec: calculateDuration(startedAt),
exitCode: result.exitCode,
passedTests: tests.passed,
totalTests: tests.total,
failedTests: tests.failed,
}
}
利点:
- 新しいエージェント追加が簡単
- ベンチマーク本体は変更不要
- テストも統一的に書ける
テスト評価の工夫
課題:Jestの出力をどう解析するか
テスト結果を正確に取得するのは意外と難しい:
- 標準出力とエラー出力が混在
- 色付きコードが含まれる
- フォーマットがバージョンで変わる可能性
2段階フォールバック戦略
第1段階:公式のJSON出力
jest --json --outputFile=results.json
構造化されたデータで確実:
{
"numTotalTests": 10,
"numPassedTests": 8,
"numFailedTests": 2
}
第2段階:正規表現でパース
JSON出力が失敗した場合、標準出力から抽出:
const TESTS_LINE_RE = /Tests:\s+(?:(\d+)\s+passed,\s+)?(?:(\d+)\s+failed,\s+)?(\d+)\s+total/i
// "Tests: 8 passed, 2 failed, 10 total" → 抽出
const match = output.match(TESTS_LINE_RE)
結果:
- 理想的なケースは確実に
- 問題が起きても最低限の情報は取得
- どちらの方法を使ったかもログに記録
運用の実際:こう使っています
日常的な使い方
週次評価(モデル更新の追跡)
# 毎週月曜日に実行
pnpm bench:l1 && pnpm bench:l2 && pnpm bench:l3
# トレンドを確認
ls -lt results/
# 2025-10-02T09:00:00Z/
# 2025-09-25T09:00:00Z/
# 2025-09-18T09:00:00Z/
新エージェント評価
# 新しいエージェントを追加したら
pnpm bench # 全タスク・全エージェントで評価
open results/latest/summary.html # 結果をブラウザで確認
特定タスクの詳細分析
# L3が苦手なようだ...詳しく見てみよう
pnpm prepare:l3
pnpm bench
# ログを直接確認
cat results/latest/*.claudecode.log
cat results/latest/*.codex.log
チーム内共有
1. HTMLレポートをSlackに投稿
pnpm bench:l1
cp results/latest/summary.html /path/to/shared/
# → Slackにリンク投稿
2. CSVをGoogleスプレッドシートにインポート
# 定期実行で自動アップロード
cron: 0 9 * * 1 # 毎週月曜9時
pnpm bench:all
upload results/latest/summary.csv to GoogleDrive
3. トレンドグラフ作成
-- BigQueryなどで時系列分析
SELECT
DATE(timestamp) as date,
agent,
AVG(score) as avg_score,
AVG(wallTimeSec) as avg_time
FROM benchmark_results
GROUP BY date, agent
ORDER BY date DESC
拡張性:こんな使い方もできます
エージェント追加は2ステップ
1. config.tsに定義追加
export default {
agents: {
// 既存
codex: { cmd: "codex", args: [...] },
claudecode: { cmd: "claude", args: [...] },
opencode: { cmd: "opencode", args: [...] },
// 新規追加
newagent: {
cmd: "newagent",
args: (sessionId: string, message: string) => [
"run",
"--json",
message
]
}
}
}
2. アダプタ実装
// src/adapters/newagent.ts
export async function runNewAgent(
input: RunInput
): Promise<RunOutput> {
// 共通I/Fに準拠するだけ
// 実装の自由度は高い
}
タスク追加も簡単
# tasks/my-custom-task.yaml
task_id: my-custom-task
difficulty: L2
requirements:
- "..."
constraints:
- "..."
success_criteria:
test_cmd: "pnpm test"
must_pass: 100
time_budget_min: 20
スコアリングのカスタマイズ
// src/score.ts
// デフォルト
const score = status === "success" ? 1.0 : 0.5 : 0
// カスタム例:テスト通過率を反映
const score = status === "success" ? 1.0 :
testSuccessRate > 0.8 ? 0.7 :
testSuccessRate > 0.5 ? 0.4 : 0
よくあるつまずきポイントと解決策
1. 「タスクを実行しても何も変わらない」
原因:
バグ適用を忘れている
解決:
# ベンチ実行前に必ずリセット
pnpm prepare:l1 # または l2, l3
# 確実な方法:ワンコマンド実行
pnpm bench:l1 # 内部でprepare→build→benchを実行
2. 「テストが遅すぎる」
原因:
全テストを実行している
解決:
# タスク定義で対象を絞る
success_criteria:
test_cmd: "pnpm test -- --testPathPattern parseUser"
# 特定ファイルのみ実行
3. 「結果が不安定(実行ごとに変わる)」
原因:
- ネットワーク状態の違い
- システムリソースの変動
- 非決定的な処理
解決:
# 複数回実行して統計を取る
for i in {1..5}; do
pnpm bench:l1
done
# summary.csvを集計して平均・分散を確認
4. 「ログファイルが大きすぎる」
原因:
詳細なデバッグ出力
解決:
// アダプタでログレベルを調整
const args = [
"run",
"--log-level", "error", // warnやerrorのみ
message
]
この先の展望
次回(実装編)で詳しく解説すること
-
アダプタ実装の詳細
- エラーハンドリングのベストプラクティス
- タイムアウト処理の工夫
- ログ設計の実践
-
プロンプトテンプレート管理
- 可変要素の扱い方
- エージェント固有の最適化
- バージョン管理
-
継続的評価の自動化
- GitHub Actionsとの連携
- 結果の可視化ダッシュボード
- アラート設定
将来的な拡張の方向性
品質指標の追加
- コードの可読性スコア(複雑度分析)
- コミットメッセージの質
- セキュリティチェック結果
テストランナーの多様化
- Vitest対応
- Playwright対応
- カスタム評価スクリプト
実行環境の拡張
- Dockerコンテナ内での実行
- クラウド環境での大規模評価
- マルチプラットフォーム対応
まとめ:なぜこのベンチマークを作る価値があるのか
この記事で設計したベンチマークは、以下を実現します:
✅ データドリブンな意思決定
「なんとなく良さそう」ではなく、定量データに基づいてエージェントを選択できます。
✅ 継続的な最適化
モデルが更新されるたびに、自動で評価を更新。常に最新の情報でチーム運用を最適化。
✅ 知識の蓄積
すべての実行結果がログとして残るため、過去の失敗から学び、プロンプトや評価基準を改善し続けられます。
✅ 実務への即応性
一般的なベンチマークではなく、自分たちのコードベース・要件に最適化された評価が可能。
最後に
AIコーディングエージェントは、もはや実験的なツールではありません。日々の開発に不可欠な存在になりつつあります。
だからこそ、「どのエージェントを、どの場面で使うか」を科学的に判断できる仕組みが必要です。
このベンチマークは、その第一歩です。
次回の実装編では、ここで設計した仕組みを実際にコードに落とし込み、運用可能な形にしていきます。お楽しみに!
クイックスタート再掲
最後に、実際に試してみたい方のために、もう一度手順をまとめます:
# 1. セットアップ
./setup.sh
# 2. L1レベルを試す
pnpm bench:l1
# 3. 結果を確認
open results/latest/summary.html
# 4. 他のレベルも試す
pnpm bench:l2
pnpm bench:l3
# 5. 壊れ具合を検証したい時
pnpm prepare:l1 -- --verify
次回予告:【実装編】アダプタパターンとログ設計の実践
実際のコードを見ながら:
- エラーハンドリングの実装
- テスト結果パースの詳細
- 拡張ポイントの具体例
をお届けします。