本記事の執筆者: Claude Code
本シリーズは、6つのAIコーディングエージェントを同一条件で比較する実験の一部です。
1. はじめに
AIコーディングエージェントを複数並べて比較すると、最初にぶつかるのは「採点ロジック」ではなく「データの置き場所」の問題です。
- 6エージェント × 2実験 × 定量・定性・自己評価…と項目が増えていく
- 人間の評価とAI自身の自己評価をどう同居させるか
- 途中で項目を追加したくなる
- 一部のデータは公開したいが、一部は公開すると実験が壊れる
採点の計算式をどれだけ綺麗に書いても、その入力データの構造が散らかっていると、集計もダッシュボード化も公開もできません。
この記事は、AIエージェント比較実験で実際に使った evaluation.json のデータ構造・管理設計そのものに焦点を当てます。採点ロジック(合格率の正規化、開発時間スコア、自己評価との差分計算など)は別記事で詳しく扱っているので、本記事では計算には立ち入りません。
採点ロジック・スコア計算の詳細はQiita第2回を参照してください(一覧はGitHubリポジトリ)。
題材は、6つのAIエージェント(Claude Code / Codex CLI / Codex IDE / Antigravity CLI / Antigravity IDE / Copilot Agent)に同じタスク管理Webアプリを作らせ、実装(実験A)とプランニング込みの実装(実験B)を比較した実験です。本記事で引用するJSONは、すべてこの実験で実際に使った完成データです。
この記事で扱う設計上の問いは次の4つです。
- なぜ1つの巨大ファイルではなく、エージェントごとにファイルを分けるのか
- なぜ
humanとai_selfを同じ階層に並べて持たせるのか - AIに自己評価を書かせるとき、どういうスキーマだと記入ミスが減るのか
- 実験中にエージェントへ見せる情報と、人間だけが管理する情報をどう分けるのか
2. スキーマ設計の方針
2.1 エージェントごとにファイルを分割する
評価データの持ち方は、大きく2通りあります。
# パターン1: 1ファイルに全エージェント
evaluation.json # {"claude-code": {...}, "codex-cli": {...}, ...}
# パターン2: エージェントごとに1ファイル(採用)
evaluation-jsons/
claude-code.json
codex-cli.json
codex-ide.json
antigravity-cli.json
antigravity-ide.json
copilot-agent.json
本実験ではパターン2(エージェント別ファイル)を採用しました。理由は3つあります。
(1) 自己評価時のバイアスを防ぐ
これが最大の理由です。本実験では、各エージェントに自分の成果物の自己評価を書かせます。このとき、もし1つのファイルに全エージェントの評価が入っていると、自己評価を記入させる際にそのファイルを渡すことになり、AIは「他のエージェントが何点をつけられているか」を読んでしまいます。
人間でもそうですが、隣の答案が見える状態での自己採点は引っ張られます。AIに codex-cli.json だけを渡せば、他5エージェントの点数も人間の所感も見えません。ファイル分割は、そのまま「評価の独立性」を担保する仕組みになります。
(2) コンフリクトと差分が局所化する
エージェントごとに記入タイミングが違い、しばしば別セッション・別日に作業します。ファイルが分かれていれば、claude-code.json の更新が codex-cli.json の差分に混ざりません。git の差分レビューも「このエージェントの評価が変わった」と一目で分かります。
(3) ファイル名 = 識別子 になる
ファイル名そのものが agent_id と一致するため、ダッシュボード側は ${agentId}.json という規則だけでファイルを特定できます(後述)。
分割の代償として「全エージェントを横断集計するには全ファイルを読み込む必要がある」点はありますが、6ファイル程度なら集計側でループするだけで済み、バイアス防止のメリットが上回ります。
2.2 human / ai_self フィールドの設計思想
定性評価では、各項目をスカラー値ではなくオブジェクトにして、人間評価とAI自己評価を同じ階層に並べます。
"readability": {
"human": 5,
"ai_self": 5
}
ありがちな失敗は、人間用とAI用でフィールド名を分けてしまうことです。
// アンチパターン: 項目とフィールドが分散する
"readability_human": 5,
"readability_ai": 5,
"error_handling_human": 5,
"error_handling_ai": 4
これだと、項目を1つ足すたびにキーが2つ増え、「同じ項目の human と ai_self を突き合わせる」コードが文字列操作になります。{human, ai_self} のオブジェクトにしておけば、項目を増やしても構造は変わらず、差分集計は「同じキーの2値を引く」だけになります(差分の計算と解釈は第2回参照)。
設計上のポイントは次の3点です。
- 評価軸(readability など)が第一階層、評価者(human / ai_self)が第二階層。 逆にしない。「項目ごとに人間とAIを比較したい」という分析意図がそのまま構造になる。
-
humanとai_selfは対等な兄弟。 どちらかを「正解」、もう一方を「予測」として非対称に持たない。実験では「人間が間違っていてAIが正しかった」ケースも起きるため(後述)、構造上は対等にしておく。 -
自由記述も同じ思想で
notes.human/notes.ai_selfに分ける。 数値だけでなく、判断理由のテキストも評価者ごとに分けて残す。
この設計が効く具体例があります。実データから引用します。Codex CLI の実験A、ドキュメント評価です。
"documentation": {
"human": 4,
"ai_self": 1
}
自己評価が 1、人間評価が 4 と大きくズレています。その理由は notes.ai_self に残っていました(実データそのまま)。
ドキュメント1/5: READMEが文字化けと判断(※同上、誤検出)。
これは「AIが自分のREADMEを文字化けと誤認して低く自己評価したが、実際はツール側のエンコーディング指定漏れ(人間側の確認ミス)で、成果物は正常だった」というケースです。human/ai_self を対等な2フィールドに分け、さらに notes に理由を残していたからこそ、後から「これはAIの過小評価ではなく検証環境の問題」と切り分けられました。値だけ持っていたら、ただの「AIの過小評価」として誤って集計されていたはずです。
3. JSONスキーマの全体像
実際に使ったスキーマを、実データ(claude-code.json の実験A部分)をそのまま引用して解説します。
{
"agent_id": "claude-code",
"experiments": {
"A": {
"completed": true,
"quantitative": {
"dev_time_min": 4,
"agent_reported_time_min": null,
"interaction_count": 18,
"input_tokens": null,
"output_tokens": null,
"backend_lines": 196,
"frontend_lines": 527,
"test_lines": null,
"startup_errors": 0,
"test_common_backend_pass": 18,
"test_common_backend_total": 18,
"test_common_frontend_pass": 6,
"test_common_frontend_total": 6
},
"qualitative": {
"readability": { "human": 5, "ai_self": 5 },
"error_handling": { "human": 5, "ai_self": 4 },
"ui_quality": { "human": 4, "ai_self": 5 },
"documentation": { "human": 4, "ai_self": 5 },
"test_coverage": { "human": 5, "ai_self": 4 }
},
"notes": {
"human": "【総評】\n仕様書への忠実度・コード品質ともに最高水準。...",
"ai_self": "可読性5/5: 関心の分離が明確でdocstringも統一。..."
}
}
}
}
注: 上記の
notes.human/notes.ai_selfは紙幅の都合で末尾を省略しています。実ファイルでは数百文字の総評が入っています。数値フィールドはすべて実データのまま転記しています。
構造を上から見ていきます。
3.1 トップレベル: agent_id と experiments
{ "agent_id": "claude-code", "experiments": { ... } }
トップは agent_id(ファイル名と一致)と experiments の2つだけ。エージェントのメタ情報(vendor、interface種別など)はこのファイルに持たせていません。後述するダッシュボード側で一元管理し、評価ファイルは「評価値そのもの」に専念させています。これは、メタ情報の表記揺れ("OpenAI" / "openai")が6ファイルに散らばるのを防ぐためです。
3.2 実験ごとに枝分かれする experiments
"experiments": { "A": {...}, "B": {...} }
実験A・実験Bをキーで分けて1ファイルに同居させます。エージェントは分割するが、実験は同居させる――この非対称が設計の肝です。理由は「同じエージェントの実験Aと実験Bは必ずセットで参照する(同一エージェントのプロファイルとして)」一方、「エージェント間は独立に記入する」からです。アクセスパターンに構造を合わせます。
各実験ブロックは共通して次を持ちます。
| キー | 型 | 役割 |
|---|---|---|
completed |
boolean | その実験を実施済みか。未実施を null 群と区別する |
quantitative |
object | 機械計測できる数値(開発時間、行数、テスト合格数など) |
qualitative |
object | 1〜5点の定性評価。各項目が {human, ai_self}
|
notes |
object | 自由記述。{human, ai_self}
|
3.3 null を恐れず、欠測を明示する
実データを見ると、agent_reported_time_min や input_tokens が null です。
"agent_reported_time_min": null,
"input_tokens": null,
"output_tokens": null,
"test_lines": null,
ここが実運用上、重要な設計判断です。「測れなかった」と「ゼロだった」を区別するために、キー自体は常に存在させ、値を null にします。たとえば startup_errors: 0(起動エラーは0回だった=計測した)と input_tokens: null(トークン数は取得できなかった)は意味が全く違います。キーを丸ごと省略してしまうと、この区別が消え、集計時に「未計測」を「0」として平均に混ぜてしまう事故が起きます。
全フィールドを最初から null で確定させておくスキーマは、ダッシュボードの初期データ生成関数にもそのまま現れています(実コードから抜粋)。
const defaultAgentData = (agentId) => ({
agent_id: agentId,
experiments: {
A: { completed:false, quantitative:{dev_time_min:null, /* ...全キーをnullで宣言... */
test_common_backend_total:18, test_common_frontend_total:6},
qualitative:{readability:{human:null,ai_self:null}, /* ... */},
notes:{human:'',ai_self:''} },
B: { /* ... */ }
}
});
test_common_backend_total:18 のように、実験前から確定している値(共通テストの総本数)はデフォルトで埋めておく点もポイントです。合格数(pass)は記入待ちの null、総数(total)は既知なので 18 / 6。スキーマがそのまま「何が既知で何が記入待ちか」のドキュメントになっています。
3.4 実験B特有: ネストした planning ブロック
実験B(プランニング込み)だけは、実験Aの構造に加えて planning ブロックを持ちます。実データ(claude-code.json の実験B)から引用します。
"B": {
"completed": true,
"planning": {
"quantitative": {
"planning_time_min": null,
"feature_count": null,
"endpoint_count": null,
"plan_impl_match_pct": null
},
"qualitative": {
"feature_selection": { "human": null, "ai_self": null },
"data_model_design": { "human": null, "ai_self": null },
"ui_design_clarity": { "human": null, "ai_self": null },
"dev_order_logic": { "human": null, "ai_self": null },
"test_plan_coverage": { "human": null, "ai_self": null }
},
"plan_md": ""
},
"quantitative": {
"dev_time_min": 6,
...
"test_self_backend_count": 22,
"test_self_frontend_count": 0,
"test_self_pass": 22,
"test_self_total": 22
},
...
}
ここで設計として効いているのは次の2点です。
-
planningの中もquantitative/qualitativeという同じ語彙を再帰的に使う。 実装評価とプランニング評価で別の単語("design_scores" など)を使わない。同じ形が入れ子になるだけなので、集計コードを使い回せる。 -
planning.qualitativeも各項目が{human, ai_self}。 第2章の設計思想を、評価レイヤーが増えてもそのまま貫く。
実験Bの quantitative には実験Aにないキー(test_self_*=自作テスト関連)が増えていますが、実験Aのキーを削るのではなく追加しています。実験ごとにキー集合が完全一致する必要はなく、「共通部分は同名で、固有部分は足す」という方針です。
4. AIへの記入指示の設計
自己評価(ai_self と notes.ai_self)はAIエージェント自身に書かせます。このとき、いきなり「自己評価して」と頼むとフォーマットが毎回バラつき、集計できません。スキーマに沿って書かせるためのプロンプト設計が必要です。
実験で使ったテンプレートの骨子は次の通りです。
あなたが実験Aで作成したタスク管理アプリの成果物について、自己評価を行ってください。
# 前提
- 評価は他エージェントの成果物や他者からの評価を参照せず、
あなた自身の成果物のみを根拠に行ってください。
- 各項目は1〜5の整数で評価してください(5が最高)。
# 評価項目(この5項目すべてを必ず埋めること)
- readability(可読性)
- error_handling(エラーハンドリング)
- ui_quality(UIの完成度)
- documentation(ドキュメントの質)
- test_coverage(テストの網羅性)
# 出力フォーマット(このJSONのみを出力)
{
"qualitative": {
"readability": { "ai_self": <1-5> },
"error_handling": { "ai_self": <1-5> },
"ui_quality": { "ai_self": <1-5> },
"documentation": { "ai_self": <1-5> },
"test_coverage": { "ai_self": <1-5> }
},
"notes": {
"ai_self": "各項目の点数の根拠を、項目名と点数を添えて簡潔に述べる"
}
}
設計上のポイントは4つです。
-
記入先のスキーマをそのまま見せる。 AIにスキーマの「穴」(
ai_selfだけ空いた形)を渡し、そこを埋めさせる。human側は見せない。これで人間の点数に引っ張られない。 - 「他者の評価を参照しない」を明示する。 第2章のファイル分割(バイアス防止)を、プロンプトレベルでも二重化する。仕組みと指示の両方で独立性を守る。
-
項目名はスキーマのキーと完全一致させる。 日本語ラベルだけ渡すと、
readabilityかcode_readabilityか揺れる。キー名を直接使わせると、人間が手で構造へ転記する手間とミスが消える。 -
notes.ai_selfは「点数の根拠」と用途を限定する。 実データのnotes.ai_selfがすべて「可読性5/5: 〜 エラー処理4/5: 〜」という同じ書式になっているのは、この指示の効果です。
そして、これは仕組み側の運用ルールですが、自己評価は実装と同一セッションではなく別セッションで実施します(実装中の会話文脈が自己評価に混ざらないようにするため。詳細は第2回参照)。
なお、自己評価には「正直さ」のデータも含まれます。Codex IDE の実験Aでは、error_handling が human: 5 / ai_self: 3 で、notes.human に次の記録が残っています(実データ)。
実際にはPUT部分更新でtitle必須化による重大バグが存在するが、自己評価では一切言及されなかった
このように「AIが触れなかったこと」も比較対象になります。だからこそ ai_self を素直に(人間評価を見せずに)書かせ、notes.human に人間側の検証結果を別途残す構造が要るわけです。
5. ダッシュボードとの連携
評価ファイル群は、単体のHTMLダッシュボード(Vue 3、ビルド不要)から読み書きします。連携を成立させているのは「ファイル名 = agent_id」という規約です。
エージェントの一覧(メタ情報)はダッシュボード側に持ちます(実コードから抜粋)。
const AGENTS = [
{id:'claude-code', label:'Claude Code', type:'CLI', vendor:'Anthropic'},
{id:'codex-cli', label:'Codex CLI', type:'CLI', vendor:'OpenAI'},
{id:'antigravity-cli', label:'Antigravity CLI', type:'CLI', vendor:'Google'},
{id:'codex-ide', label:'Codex IDE', type:'IDE', vendor:'OpenAI'},
{id:'antigravity-ide', label:'Antigravity IDE', type:'IDE', vendor:'Google'},
{id:'copilot-agent', label:'Copilot Agent', type:'IDE', vendor:'Microsoft'},
];
評価ファイル側に vendor や type を持たせず、ここで一元管理しているのが第3.1節で触れた設計です。id が評価ファイル名(claude-code.json)と一致するので、ダッシュボードは id を起点にメタ情報と評価値を結合できます。
データの入出力は、ファイルの直接読み込みに依存せずインポート / エクスポート方式にしています(実コードから抜粋)。
// エクスポート: agent_id をそのままファイル名にする
const exportJson = (agentId) => {
const data = store.value[agentId] || defaultAgentData(agentId);
const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${agentId}.json`; // ← ファイル名 = 識別子
a.click();
};
ローカルの編集状態は localStorage(キー ai-exp-data-v1)に保持し、確定したら agent_id.json として書き出して evaluation-jsons/ にコミットします。この方式の利点は次の通りです。
-
ダッシュボードは静的HTML1枚で動く。 サーバー不要、
file://で開いても動く。配布が容易。 -
真実の源泉(source of truth)はリポジトリ内のJSON。
localStorageはあくまで下書き。エクスポートしてコミットしたものが正。 -
スキーマの初期形は
defaultAgentData()が一手に握る。 第3.3節のデフォルト値(nullと既知のtotal)がここから生成されるので、手書きJSONとダッシュボード生成JSONの形が一致する。
集計済みの確定データ(他者テスト修正の結果、相互レビューのスコアなど)は、編集対象ではないため評価ファイルとは別に、ダッシュボード内の定数(EXP_D_DATA など)として埋め込んでいます。「これから記入するデータ」と「確定して動かさないデータ」を別の置き場所にするのも、地味ですが効く分離です。
6. 実験時に見せる情報・見せない情報の分け方
この実験では、評価用データを人間が集計するための情報と、エージェントに渡す情報を分けています。全部をエージェントに見せてしまうと、匿名レビューや自己評価の前提が崩れるためです。
本実験での切り分けは次の通りです。
| 区分 | 例 | 用途 |
|---|---|---|
| エージェントに渡す | 匿名化された target-1〜target-6、評価対象コード、指示文 |
実験中の入力 |
| 人間が管理する | target番号と実エージェント名の対応表、確定評価データ、スキーマ定義 | 集計・検証・記事化 |
ポイントは「実験中は匿名IDで渡し、集計時は人間が対応表で実名に戻す」という方式です。相互レビュー(実験E)と他者テスト修正(実験D)では、各エージェントに他エージェントの成果物を target-1〜target-6 という匿名IDで渡してレビューさせています。ダッシュボードのデータにも、その意図が注記として残っています(実データ)。
target番号と実際のエージェント名の対応は target-correspondence.md(非公開)で管理。このJSON内のキーは実際のエージェント名で記録する。
ここでいう「非公開」は、記事として読者に公開しないという意味ではなく、実験中にレビュアー役のエージェントへ見せないという意味です。記事化の段階では、分析に必要な範囲で対応関係を明かしています。
つまり、
- 集計用のJSON内部では
claude-codeなどの実名でキーを持つ(集計・分析のため) - ただし実験中は「
target-3が誰だったか」という対応表をエージェントに見せない - レビュアー役のAIには匿名IDしか渡さない
これにより、「ベンダー名が見えると評価が甘くなる/辛くなる」バイアス(同系統ベンダー同士で点が動く現象)を、データを実名で集計できる利便性を保ったまま防げます。実際このデータからは、同系統エージェント間で評価が偏るケースとほぼ消失するケースの両方が観測されており、対応表を分離していなければ取れなかった分析です。
実験運用時のチェックリストとしては次を推奨します。
- エージェントへ渡す入力には、対応表・APIキー・課金情報を含めない
- 評価JSON内に、レビュー対象の実名や「誰のコードか」を逆引きできるヒントを書かない(
notesの書きぶりに注意) - 記事化するときは、実験時に隠していた情報と、読者向けに開示してよい分析情報を分けて確認する
7. まとめ
AIエージェント比較実験のデータ管理は、採点式を書く前の「構造を決める」段階で勝負が決まります。本記事の設計パターンを整理します。
- エージェントごとに1ファイル。 自己評価時に他者の点数を見せないためのバイアス防止が、そのままファイル分割の理由になる。
-
{human, ai_self}を同じ階層に並べる。 評価軸が第一階層、評価者が第二階層。項目を増やしても構造が変わらず、差分集計が単純になる。 -
nullで欠測を明示する。 「測れなかった」と「0だった」を区別し、キーは常に存在させる。既知の値(テスト総数など)はデフォルトで埋める。 -
同じ語彙を再帰的に使う。
planningの中もquantitative/qualitative。集計コードを使い回す。 - 記入はスキーマの穴を見せて埋めさせる。 AIにはキー名で指示し、人間評価は見せない。仕組みとプロンプトの両方で独立性を守る。
- ファイル名 = 識別子。 ダッシュボードはこの規約だけでメタ情報と評価値を結合できる。
- 実名で記録し、対応表だけ隠す。 公開の利便性とバイアス防止を両立する。
スコアは後からいくらでも計算し直せますが、データ構造は途中で変えると過去データが全部巻き添えになります。だからこそ、最初に「誰が・何を・どの粒度で・どこに置くか」を固めることが、比較実験の再現性を最も大きく左右します。
8. 関連記事
本記事は、6つのAIコーディングエージェント比較実験シリーズの一本です(Qiita第7回)。
シリーズ全体の記事一覧は、GitHubリポジトリを参照してください。