🐕

AIコーディングエージェント、どれを選ぶ?公平に比較するベンチマークを作ってみた【企画編】

に公開

はじめに

選択肢が増えた今、私たちが直面している課題

「このタスク、Codexに任せる? それともClaude Code? あれ、OpenCodeもあったな...」

AIコーディングエージェントを使っている方なら、こんな場面に出くわしたことがあるのではないでしょうか。私もその一人です。毎日の開発で複数のエージェントを使い分けていると、ふと疑問が湧いてきました。

「結局、どのエージェントが何に向いているんだろう?」

モデルは頻繁に更新されるし、公式のベンチマークは存在するものの、自分たちの実際のコードベースでの挙動とは違う気がする。かといって、毎回手動で試して比較するのは時間がかかりすぎる...。

そこで考えました。**「自分たちで、実務に即したベンチマークを作ってしまおう」と。

この記事は、その企画段階の思考プロセスをまとめたものです。実装編は次回お届けします。


なぜ独自ベンチマークが必要なのか

既存のベンチマークでは足りない3つの理由

1. 実際のコードベースと乖離している

公開されているベンチマークは素晴らしいですが、多くは学術的な課題や一般的なコーディング問題が中心です。でも、私たちが日々向き合うのは:

  • レガシーコードのバグ修正
  • 既存のAPIエンドポイント追加
  • 複数ファイルにまたがる複雑な修正

こういった「リアルな現場の課題」を評価したいんです。

2. 比較条件が統一されていない

「あれ、このエージェントは外部ライブラリを追加してるけど、こっちは標準ライブラリだけで解いてる」なんてこと、ありませんか? 公平に比較するには、同じリポジトリ、同じ制約条件で評価する必要があります。

3. 継続的な評価ができない

モデルは日々進化します。今日のベストチョイスが来月も同じとは限りません。定期的に、簡単に再評価できる仕組みが欲しい。

私たちが目指すベンチマークの姿

これらの課題を解決するため、以下の特徴を持つベンチマークを設計することにしました:

実務に近い課題設定

  • 関数のバグ修正(L1)
  • RESTエンドポイント追加(L2)
  • 複数ファイルにまたがる複合修正(L3)

公平な比較環境

  • 同じリポジトリ、同じプロンプト
  • 新規依存追加禁止などの統一ルール
  • 外部ネットワークアクセスの制御

再現可能な評価

  • すべての実行ログを保存
  • タイムスタンプ付きで結果を記録
  • いつでも過去の実行を検証可能

自動化された継続評価

  • ワンコマンドでベンチマーク実行
  • CSV/Markdown/HTMLで結果を出力
  • モデル更新時も簡単に再評価

どうやって「公平」を担保するか

課題:エージェントごとの癖と特性

Codex、Claude Code、OpenCodeはそれぞれ異なるCLIインターフェース、出力形式、実行モデルを持っています。まるで異なる言語を話す3人の専門家に同じ仕事を頼むようなもの。

ここで重要なのは、「違いを無くす」のではなく「条件を揃える」こと。

解決策:アダプタパターンと統一プロンプト

レストランで例えてみましょう。3人のシェフ(エージェント)に同じ料理を作ってもらいたい時、どうしますか?

  1. 同じレシピ(プロンプト)を渡す
  2. 同じ食材と調理器具(リポジトリと制約)を使ってもらう
  3. 制限時間(タイムバジェット)を設定する
  4. 完成品を同じ基準(テスト)で評価する

これをコードで実現するのが私たちの設計です。

// 統一されたプロンプト生成
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 + リセットスクリプト

図書館で本を借りて、返却時に元の場所に戻すイメージです。

  1. .bench-fixtures/に「壊れた状態のスナップショット」を保存
  2. 必要な時にそれを復元する
  3. テストで「ちゃんと壊れているか」を検証
# 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分)

実行フローの全体像

全体の流れを図解してみましょう。

ポイント:

  1. シャッフルで公平性を担保

    • 実行順による有利不利を排除
    • キャッシュやシステム状態の影響を最小化
  2. 複数フォーマットで出力

    • CSV → データ分析・BIツール連携
    • Markdown → GitHub/Notionで共有
    • HTML → ブラウザで見やすく表示
    • JSON → プログラムでの再処理
  3. 完全なログ保存

    • 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
]

この先の展望

次回(実装編)で詳しく解説すること

  1. アダプタ実装の詳細

    • エラーハンドリングのベストプラクティス
    • タイムアウト処理の工夫
    • ログ設計の実践
  2. プロンプトテンプレート管理

    • 可変要素の扱い方
    • エージェント固有の最適化
    • バージョン管理
  3. 継続的評価の自動化

    • 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

次回予告:【実装編】アダプタパターンとログ設計の実践

実際のコードを見ながら:

  • エラーハンドリングの実装
  • テスト結果パースの詳細
  • 拡張ポイントの具体例

をお届けします。

Discussion