同じ修正を Antigravity のエージェントに3回投げて、3回とも違うコードが返ってきたことがあります。どれも一見もっともらしく、しかし1つはテストを壊し、1つは型が通らず、残る1つだけが実際に動きました。問題は「どれが動くか」を人間が毎回読んで見抜かなければならなかったことです。レビューに15分かかり、自動化したはずの作業が結局手作業に戻っていました。個人開発で複数のサイトを一人で回していると、この「人間が最後に毎回見抜く」工程こそが自動化の最大のボトルネックになります。私自身、ここを設計し直すまで、夜間バッチの成果を翌朝ずっと検品していました。
6/18 に Gemini CLI が Antigravity CLI へ統合され、動力が Gemini 3.5 Flash になります。Flash は同等タスクで上位モデルの数倍速とされ、1つの問題に対して候補を3つ、5つと並行生成しても費用と時間が現実的な範囲に収まります。つまり「1案を信じる」前提から「複数案から選ぶ」前提へ、設計の重心が移ります。
ここで主役になるのは生成側ではなく、**裁定側(アービター)**です。複数の候補をどう比べ、何を根拠に1つへ絞るのか。本稿はその設計に絞って書きます。
多数決と自己申告が当てにならない理由
最初に考えるのは「同じ答えが多い案を採る」多数決です。けれど LLM の誤りは独立ではありません。同じプロンプトと同じモデルから生成すれば、間違いも揃って出ます。3案のうち2案が同じバグを共有していれば、多数決はそのバグを「正解」として選びます。
次に思いつくのが、モデル自身に自信度を答えさせる方法です。これも危ういものです。自己申告の確信度は、実際の正しさとほとんど相関しません。流暢で断定的な誤答ほど高い自信を返すことすらあります。
採用基準は、生成モデルから独立した、検証可能な信号でなければなりません。コードであれば、型が通るか、テストが緑か、実際に起動するか。これらはモデルの気分に左右されない客観的な事実です。アービターの仕事は、候補をこれらの事実に通し、生き残ったものを選ぶことに尽きます。
設計の全体像
パイプラインは3段に分けて考えると整理できます。
| 段 | 責務 | 独立性 |
| 生成(Generator) | 同一仕様から N 個の候補を並行生成する | モデル依存 |
| 検証(Verifier) | 各候補を客観ゲートに通しスコア化する | モデルから独立 |
| 裁定(Arbiter) | スコアと予算から採用案を1つ決める | 規則ベース |
肝心なのは、検証と裁定を生成から完全に切り離すことです。生成側がどのモデルでも、何案でも、検証と裁定のコードは変わりません。この境界を守ると、モデルが入れ替わっても評価基準は安定します。
検証ゲートを段階に並べる
全候補に全ゲートを通すのは無駄が多いです。型検査で落ちる候補にテストを走らせる意味はありません。安いゲートから順に並べ、落ちた候補はそこで脱落させます。
| 順 | ゲート | 目安コスト | 判定 |
| 1 | 静的検査(lint / 型) | 数百ms | 失敗で即脱落 |
| 2 | 関連テスト(影響範囲のみ) | 数秒 | 失敗数でスコア減点 |
| 3 | スモーク実行(起動・主要経路) | 十数秒 | 例外でスコア減点 |
この順序にすると、明らかに駄目な候補はテストやスモークに到達する前に消えます。Flash で5案つくっても、実際にスモークまで届くのは1〜2案ということが多く、検証の総コストは候補数に比例しません。
アービターの実装
候補をゲートに通し、スコア化し、採用案を返す中核です。TypeScript で示します。検証関数は呼び出し側の環境(型検査コマンドやテストランナー)に合わせて差し替えてください。
type Candidate = {
id: string;
apply: () => Promise<void>; // 候補をワークツリーに反映
revert: () => Promise<void>; // 反映を巻き戻す
};
type GateResult = { passed: boolean; penalty: number; note: string };
type Gate = {
name: string;
run: () => Promise<GateResult>;
fatal: boolean; // true: 失敗で即脱落 / false: 減点のみ
};
type Verdict =
| { kind: "adopt"; id: string; score: number; trail: string[] }
| { kind: "all_failed"; trail: string[] }
| { kind: "budget_exhausted"; trail: string[] };
async function arbitrate(
candidates: Candidate[],
gates: Gate[],
opts: { maxVerifyMs: number },
): Promise<Verdict> {
const trail: string[] = [];
const start = Date.now();
let best: { id: string; score: number } | null = null;
for (const c of candidates) {
if (Date.now() - start > opts.maxVerifyMs) {
trail.push(`budget: ${c.id} 未検証で打ち切り`);
return best
? { kind: "adopt", id: best.id, score: best.score, trail }
: { kind: "budget_exhausted", trail };
}
await c.apply();
let score = 100;
let eliminated = false;
for (const g of gates) {
const r = await g.run();
trail.push(`${c.id}/${g.name}: ${r.passed ? "ok" : "ng"} ${r.note}`);
if (!r.passed && g.fatal) { eliminated = true; break; }
if (!r.passed) score -= r.penalty;
}
await c.revert();
if (eliminated) continue;
// 同点は先着優先(決定性を保つため候補は安定順で渡す)
if (!best || score > best.score) best = { id: c.id, score };
}
if (!best) return { kind: "all_failed", trail };
return { kind: "adopt", id: best.id, score: best.score, trail };
}
設計上のポイントが3つあります。第一に、候補ごとに apply → 検証 → revert を必ず往復させ、候補同士が状態を汚染しないようにします。第二に、fatal なゲートで早期脱落させ、無駄な検証を省きます。第三に、すべての判定を trail に残します。なぜその案が選ばれた(落ちた)かを後から再現できることが、自律運用では命綱になります。
同点・全滅・予算切れをどう畳むか
幸せな経路よりも、うまくいかない経路の設計のほうが効きます。
同点のときは多数決へ戻したくなりますが、ここでは安定順の先着を採ります。乱数で選ぶと、同じ入力で実行のたびに結果が変わり、再現性が壊れます。候補は常に同じ順で渡し、裁定を決定的に保ちます。
全候補が落ちたとき(all_failed)は、自動採用してはいけません。最もペナルティの小さい候補を「人間レビュー待ち」のキューへ積み、本流には入れません。ここで無理に1つ通すと、壊れた変更が自動でマージされる最悪の事故になります。
予算切れ(budget_exhausted)は、検証に時間がかかりすぎたときの保険です。maxVerifyMs を超えたら、その時点での暫定ベストを採るか、ベストがなければ中断します。並行エージェントを夜間に回すなら、1タスクの検証に上限を設けないと、1本の遅延が全体を止めます。私が実際に踏んだ落とし穴は、maxVerifyMs を候補数で割らずに固定していたことでした。候補を増やした夜に検証が間に合わず、暫定ベストばかりが採られていたのです。候補数に応じて上限を伸縮させることで回避できます。
どれだけ案を出すべきか
N は大きいほど良いわけではありません。検証コストと採用品質は逓減します。手元の定型修正タスクで観察した範囲では、おおむね次の傾向でした。
| 候補数 N | 1案以上が全ゲート通過する割合 | 1タスクあたり検証時間 |
| 1 | 約60% | 約12秒 |
| 3 | 約88% | 約20秒 |
| 5 | 約93% | 約27秒 |
1案から3案への伸びは大きく、3案から5案は小さい、という形です。個人的には、定型タスクでは N=3 を推奨します。過去に失敗が多かった種類のタスクだけ N=5 へ上げる運用にしています。Flash の速さで生成側はほぼ一定なので、効いてくるのは段階ゲートで検証側を太らせないことのほうです。
運用に乗せる前の3つの確認
実際に自律パイプラインへ組み込む前に、私は次の3点を必ず確かめています。
- 検証ゲートが生成と本当に独立しているか。型検査やテスト自体を同じエージェントに書かせていると、独立性が崩れます。ゲートは人間が固定し、生成だけをエージェントに任せます。
revert が確実に巻き戻すか。ワークツリーが汚れたまま次の候補を apply すると、スコアが前の候補に引きずられます。git worktree で候補ごとに隔離するのが堅実です。
- 全滅時に本流へ入れない経路になっているか。ここだけは自動化を諦め、人間に渡す設計を崩さないことが、長く運用するうえでの安全弁になります。
候補を増やすより、検証を独立かつ段階的に組むこと。生成が速く安くなった時代に効くのは、選ぶ側の設計だと考えています。同じ悩みを抱える方の手がかりになれば幸いです。