同時に走らせるエージェントの数を 2 から 4 に増やした日、全体の仕上がりがむしろ遅くなりました。個人開発で Dolice Labs の複数サイトを並行運用している私自身、各サイトの記事生成を別々のエージェントに割り当てれば単純に倍速になるはずだ、と踏んでいたのです。実際には、生成そのものは速くなったのに、最後の検証と push がひとつの順番待ちの列に詰まり、待ち時間が増えていました。
Antigravity 2.0 が「複数エージェントの真の並列実行」を前面に出したことで、あるエージェントがコンポーネントを書き、別のエージェントが API ルートを組み、さらに別のエージェントが視覚回帰テストを回す、という構図が現実になりました。ただ、並列の口が広がったぶん「とりあえず全部並べれば速い」という錯覚も起こりやすくなっています。本題は、どの仕事を並列にし、どこを直列のまま残すかを、感覚ではなく構造から決めることです。
並列化が「タダ」だと錯覚する瞬間
並列実行が無条件に得だと感じるのは、各エージェントの仕事を独立した箱として眺めているときです。けれど実運用では、箱と箱は見えない線でつながっています。同じ git の作業ツリー、同じディスク、同じモデル API のレート枠、そして「日本語版と英語版の記事数を一致させる」といった全体にまたがる不変条件。これらはどれも、並列に踏み込んだ瞬間に競合の火種になります。
並列化で得られるのは「独立に進められる部分」の短縮だけです。共有資源に触れる部分や、順番が意味を持つ部分は、エージェントを何台積んでも縮みません。むしろ、調整のためのロックや再試行が増えて遅くなることすらあります。最初にやるべきは台数を増やすことではなく、自分の仕事のどこが独立していて、どこが共有なのかを言葉にすることです。
並列にしてよい仕事を見分ける3つの問い
ある作業を並列の列に入れてよいかは、次の3つの問いで判定できます。ひとつでも「いいえ」が混ざる場合は、そのまま並べると壊れる可能性が高い処理です。
問い はい(並列向き) いいえ(直列に寄せる)
成果物は他と独立しているか サイトAの下書きとサイトBの下書きは互いを参照しない 後段が前段の出力をそのまま入力にする
共有する可変状態に書き込むか 各自が別ファイル・別ブランチにだけ書く 同じインデックス・同じ集計ファイルを更新する
外部のレート制限を共有するか 呼び出し先が分かれている、または上限に十分余裕がある 同一モデルの同一クォータを全員で食い合う
この3問のうち、見落とされやすいのが3つ目です。エージェントごとの処理は独立していても、全員が同じ Gemini のクォータを叩いていれば、並列数を上げた瞬間に 429 エラーが増え、再試行で実時間が膨らみます。本番運用では、ここを避けるために同時呼び出し数に上限を設けるのが安全です。注意点として、並列度の上限は「手元の台数」ではなく「共有資源のうち最も細い管」で決まると考えてください。
直列のまま残すべき処理を不変条件から逆算する
何を直列に残すかは、好みではなく「壊してはいけない約束(不変条件)」から逆算します。私のパイプラインでいちばん硬い約束は、日本語記事数と英語記事数が常に一致していることです。言語切替時の 404 を避けるための前提で、ここが崩れるとサイト全体の信頼が傷みます。
この不変条件を守るには、「日英の対を1単位として書き込む」操作を分割してはいけません。2つのエージェントが同じカテゴリに同時に push を仕掛けると、リベースの最中に片方の英語版だけが先に入る、といった中途半端な状態が生まれます。これは実際に私が踏んだ落とし穴で、回避するには結合を直列に閉じるしかありませんでした。だから生成(下書き作り)は並列でよくても、検証 → 件数確認 → push という結合処理は、リポジトリ単位で必ず直列の一本道にします。
[並列OK] サイトごとの下書き生成(独立した成果物)
├─ agent-A: claudelab の ja/en 下書き
├─ agent-B: gemilab の ja/en 下書き
└─ agent-C: antigravitylab の ja/en 下書き
│
[直列必須] リポジトリ単位の結合(不変条件を守る一本道)
for repo in repos:
gate(repo) -> assert ja_count == en_count -> push(repo)
ポイントは、直列にする範囲をできるだけ小さく絞ることです。「全部直列」でも安全ですが遅い。守りたい不変条件に触れる最小の区間だけを直列にし、それ以外は並列に開放する。この境界線の引き方が、そのまま全体の速さになります。
依存グラフから同時実行可能な集合を求める
線引きを頭の中だけで管理すると、タスクが増えたときに必ず破綻します。タスク間の依存を明示的なグラフにしておけば、「いま同時に走らせてよい集合」を機械的に求められます。次は、依存マップからクリティカルパスと各段の並列可能集合を計算する最小実装です。Antigravity の SDK でオーケストレーションを書くときも、この前処理を挟むと「並べてよい口」が自動で見えてきます。
// tasks: 各タスクの依存(前提となるタスク名の配列)を宣言する
const tasks = {
draftClaude: { deps: [], cost: 40 }, // 生成は独立
draftGemini: { deps: [], cost: 40 },
draftAnti: { deps: [], cost: 40 },
gateClaude: { deps: [ "draftClaude" ], cost: 8 }, // 検証は対応する下書きに依存
gateGemini: { deps: [ "draftGemini" ], cost: 8 },
gateAnti: { deps: [ "draftAnti" ], cost: 8 },
pushAll: { deps: [ "gateClaude" , "gateGemini" , "gateAnti" ], cost: 6 }, // 結合点
};
// 依存が満たされたタスクを「波」ごとにまとめる(同じ波=同時実行可能集合)
function scheduleWaves ( tasks ) {
const done = new Set ();
const waves = [];
const all = Object. keys (tasks);
while (done.size < all. length ) {
const ready = all. filter (
( t ) => ! done. has (t) && tasks[t].deps. every (( d ) => done. has (d))
);
if (ready. length === 0 ) throw new Error ( "循環依存があります" );
waves. push (ready);
ready. forEach (( t ) => done. add (t));
}
return waves;
}
// クリティカルパス長(理論上の最短実時間)を求める
function criticalPath ( tasks ) {
const memo = {};
const longest = ( t ) => {
if (memo[t] != null ) return memo[t];
const base = tasks[t].deps. reduce (( m , d ) => Math. max (m, longest (d)), 0 );
return (memo[t] = base + tasks[t].cost);
};
return Math. max ( ... Object. keys (tasks). map (longest));
}
console. log ( scheduleWaves (tasks));
// => [['draftClaude','draftGemini','draftAnti'], ['gateClaude','gateGemini','gateAnti'], ['pushAll']]
console. log ( "critical path:" , criticalPath (tasks), "→ 40 + 8 + 6 = 54" );
ここで criticalPath が返す 54 という値が大事です。これは「無限に台数があっても、これより速くはならない」理論限界を表します。3つの下書きを並列にしても、結合点の pushAll がある以上、全体は下書き1本ぶん+検証+結合の長さに張り付きます。台数を増やす前に、この限界値を知っておくと判断を誤りません。
並列数を上げても線形に速くならないことを実測する
理屈で限界が見えても、実際の伸びは計測しないと分かりません。並列度を変えながら実時間を測る、素朴なハーネスを置いておくと、自分のパイプラインの「効く台数」が掴めます。
#!/usr/bin/env bash
# 並列度を 1,2,4 と変えて、同じジョブ群の総実時間を測る
set -euo pipefail
jobs = ( draftClaude draftGemini draftAnti ) # 独立に走らせられるジョブ
run_one () { sleep "$(( RANDOM % 3 + 2 ))" ; } # ここを実際の生成呼び出しに置き換える
for p in 1 2 4 ; do
start = $( date +%s )
printf '%s\n' "${ jobs [ @ ]}" | xargs -P " $p " -I {} bash -c 'run_one "{}"' _ 2>/dev/null || \
printf '%s\n' "${ jobs [ @ ]}" | xargs -P " $p " -I {} sleep 2
end = $( date +%s )
echo "並列度 ${ p }: $((end - start)) 秒"
done
私の環境で記事生成パイプラインを測ったとき、単純計算なら 4 倍速のはずが、並列度 4 でも実時間は 1.8 倍ほどにしか縮みませんでした。理由は明快で、生成は並列に開けても、検証と push の直列区間とモデル API の共有クォータが残るからです。この「1.8 倍」という数字こそが、台数をこれ以上増やしても割に合わない、という撤退ラインになります。速度を上げたいなら、台数ではなく直列区間そのものを短くする(検証を軽くする、結合の粒度を変える)ほうが効きます。
私のパイプラインでの線引き
最終的に落ち着いた境界は、とてもシンプルな3点です。
下書きの生成はサイト単位で並列に開放する(独立した成果物なので並べてよい)。
検証・件数確認・push はリポジトリ単位で直列に閉じる(不変条件を守る一本道)。
モデル API の同時呼び出しには上限を設け、全エージェントで共有クォータを食い潰さないようにする。
私はこの順番を推奨します。守りたい約束を先に言葉にして、それに触れる最小区間だけを直列に残し、残りを並べる。この3点だけで、台数を増やしたときに起きていた順番待ちの詰まりはほぼ消えました。
並列化は、独立した部分を見つけて開放する作業であって、すべてを同時に動かす競争ではありません。次の一歩として、いま自分が並列で回している処理のうち「同じ可変状態に書き込んでいる箇所」を1つだけ書き出してみてください。そこが、台数を増やしても速くならない本当のボトルネックです。