1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-10-02

はじめに

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

「このタスク、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

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

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

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

をお届けします。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?