大規模なAIコードレビューのオーケストレーション
2026-04-20 Ryan Skidmore — 読了 19分
コードレビューはバグを見つけ知識を共有する素晴らしい仕組みですが、同時にエンジニアリングチームをボトルネックにする最も確実な方法の一つでもあります。マージリクエストがキューに入り、レビュアーが差分を読むためにコンテキストスイッチし、変数名に関する細かい指摘をいくつか残し、著者が応答し、そのサイクルが繰り返されます。社内プロジェクトでは、初回レビューまでの中央値が数時間で測定されることがよくありました。
AIコードレビューを実験し始めたとき、私たちが最初に取った道はおそらく多くの人が取る道と同じでした:いくつかのAIコードレビューツールを試してみると、多くはかなりうまく動き、カスタマイズや設定が豊富に用意されているものもありました!しかし残念ながら、繰り返し出てきた共通のテーマは、Cloudflare の規模の組織には柔軟性やカスタマイズ性が十分ではない、ということでした。
そこで次に取ったもっとも明白な道は、git diff を取得して、半端なプロンプトに押し込み、大規模言語モデルにバグを探させる、というものでした。結果は予想通り雑音が多く、漠然とした提案の洪水、幻覚による文法エラー、すでに存在する関数に対して「エラーハンドリングを追加することを検討してください」というような助言が返ってくる、といったものでした。
私たちはすぐに、単純な要約アプローチでは複雑なコードベースに対して望む結果は得られないことを理解しました。そこで、単一のモノリシックなコードレビューエージェントを一から作るのではなく、OpenCode(オープンソースのコーディングエージェント)を中心に据えたCIネイティブのオーケストレーションシステムを構築することにしました。
今日では、Cloudflare のエンジニアがマージリクエストを開くと、コーディネートされた複数のAIエージェントから初期レビューが行われます。巨大で汎用的なプロンプトを持つ単一モデルに依存するのではなく、セキュリティ、パフォーマンス、コード品質、ドキュメント、リリース管理、内部のEngineering Codex準拠など、最大7つの専門的なレビュアーを起動します。これらの専門家はコーディネーターエージェントによって管理され、重複する指摘を除去し、実際の問題の深刻度を判断し、単一の構造化されたレビューコメントを投稿します。
このシステムは社内で数万件のマージリクエストに対して運用されています。クリーンなコードを承認し、実際のバグを高い精度でフラグし、本当に重大な問題やセキュリティ脆弱性を検出した場合はマージを積極的にブロックします。これは、Code Orange: Fail Small の一環としてエンジニアリングのレジリエンシーを高めている多くの取り組みの一つです。
以下では、我々がどのように構築したか、採用したアーキテクチャ、およびCI/CDパイプラインの重要経路、さらにエンジニアのコーディング作業の進行を妨げないようLLMを組み込む際に直面する具体的な技術的課題について詳しく掘り下げます。
アーキテクチャ:プラグイン至上主義
数千のリポジトリで動かす内部ツールを作るとき、バージョン管理システムやAIプロバイダをハードコードするのは、六ヶ月後に全て書き直すことを保証する良い方法です。私たちは今日はGitLabをサポートする必要があり、明日は何が来るか分からない、異なるAIプロバイダや内部基準要件にも対応しつつ、各コンポーネントが他を知る必要がないようにする必要がありました。
そのため、エントリポイントがすべての設定をプラグインに委譲し、プラグイン同士が合成されてレビューの実行方法を定義する、コンポーザブルなプラグインアーキテクチャを構築しました。マージリクエストがレビューをトリガーしたときの実行フローは次のようになります:
- 各プラグインは ReviewPlugin インターフェースを実装し、3つのライフサイクルフェーズを持ちます。
- Bootstrap フックは並列に実行され、非致命的です。テンプレートの取得に失敗しても、レビューはそれなしで続行されます。
- Configure フックは逐次実行され、致命的です。たとえば VCS プロバイダが GitLab に接続できない場合、ジョブを継続する意味はありません。
- postConfigure は構成が組み立てられた後に実行され、リモートのモデルオーバーライド取得などの非同期作業を処理します。
ConfigureContext により、プラグインはレビューに影響を与えるための制御された表面を得ます。エージェントの登録、AIプロバイダの追加、環境変数の設定、プロンプトセクションの注入、エージェントの細かな権限の変更などが可能です。どのプラグインも最終的な構成オブジェクトに直接アクセスできません。コンテキスト API を通じて寄与し、コアのアセンブラがそれらをマージして OpenCode が消費する opencode.json を生成します。
この分離により、GitLab プラグインは Cloudflare AI Gateway の設定を読み取らず、Cloudflare プラグインは GitLab API トークンについて何も知りません。VCS 固有の結合は単一の ci-config.ts ファイルに隔離されています。
典型的な内部レビューのプラグイン陣容
- @opencode-reviewer/gitlab — GitLab VCS プロバイダ、MR データ、MCP コメントサーバ
- @opencode-reviewer/cloudflare — AI Gateway 設定、モデル階層、フェイルバックチェーン
- @opencode-reviewer/codex — エンジニアリング RFC に対する内部コンプライアンスチェック
- @opencode-reviewer/braintrust — 分散トレーシングと可観測性
- @opencode-reviewer/agents-md — リポの AGENTS.md が最新か検証
- @opencode-reviewer/reviewer-config — Cloudflare Worker からのレビュワー別リモートモデルオーバーライド
- @opencode-reviewer/telemetry — フォローせずにレビュー追跡(fire-and-forget)
OpenCode を内部でどう使っているか
私たちが OpenCode をコーディングエージェントに選んだ理由はいくつかあります:
- 社内で広く使っており、動作に非常に詳しかったこと
- オープンソースなので、上流に機能やバグ修正をコントリビューションでき、問題を発見したときに簡単に調査できること(執筆時点で Cloudflare のエンジニアは上流に45件以上のプルリクエストをマージしています)
- 優れたオープンソースSDKがあり、プラグインを容易に構築できること
しかし最も重要なのは、OpenCode がサーバ優先の構造であり、そのテキストベースUIやデスクトップアプリがクライアントとして動作する点でした。これは私たちにとって必須条件でした。なぜなら、セッションをプログラム的に作成し、SDK 経由でプロンプトを送り、複数の並行セッションから結果を収集する必要があり、CLI をハックして回避したくなかったからです。
オーケストレーションは2つの明確なレイヤーで動作します:
コーディネータープロセス
私たちは OpenCode を Bun.spawn を使って子プロセスとして生成します。コーディネータープロンプトはコマンドライン引数としてではなく stdin 経由で渡します。大きなマージリクエスト説明やログ満載の文字列をコマンドライン引数で渡そうとすると、Linux カーネルの ARG_MAX 制限に当たることがあるためです。E2BIG エラーが一部のCIジョブで発生するのを見て、これを学びました。
プロセスは --format json で動作するため、すべての出力は stdout に JSONL イベントとして届きます:
const proc = Bun.spawn( ["bun", opencodeScript, "--print-logs", "--log-level", logLevel, "--format", "json", "--agent", "review_coordinator", "run"], { stdin: Buffer.from(prompt), env: { ...sanitizeEnvForChildProcess(process.env), OPENCODE_CONFIG: process.env.OPENCODE_CONFIG_PATH ?? "", BUN_JSC_gcMaxHeapSize: "2684354560", // 2.5 GB heap cap }, stdout: "pipe", stderr: "pipe", }, );
レビュープラグイン
OpenCode プロセス内では、ランタイムプラグインが spawn_reviewers ツールを提供します。コーディネーターLLM がコードレビューを開始するタイミングと判断すると、このツールを呼び出してサブレビュアーセッションを OpenCode の SDK クライアントを通じて起動します:
const createResult = await this.client.session.create({ body: { parentID: input.parentSessionID }, query: { directory: dir }, });
this.client.session.promptAsync({ path: { id: task.sessionID }, body: { parts: [{ type: "text", text: promptText }], agent: input.agent, model: { providerID, modelID }, }, });
各サブレビュアーは独自の OpenCode セッションで独自のエージェントプロンプトを持って実行されます。コーディネーターはサブレビュアーがどのツールを使うかを見たり制御したりしません。彼らは必要に応じてソースファイルを読み、grep を実行し、コードベースを検索する自由があり、完了すると構造化された XML として所見を返します。
JSONL とは何で、何に使うのか?
このようなシステムで一般的に直面する大きな課題の一つは構造化ロギングの必要性です。JSON は優れた構造化フォーマットですが、有効な JSON ブロブにするにはすべてを “閉じる” 必要があります。アプリケーションが早期に終了して有効な JSON を書き出す前に落ちると、デバッグログが読めなくなります。これが JSONL(JSON Lines)を使う理由です。JSONL はその名の通り、各行が有効で自己完結した JSON オブジェクトであるテキストフォーマットです。標準的な JSON 配列とは違い、最初のエントリを読むためにドキュメント全体を解析する必要はありません。1行読み、解析し、先に進みます。
これにより巨大なペイロードをメモリにバッファする必要や、子プロセスがメモリ不足で終了して閉じカッコが来ないケースを気にする必要がなくなります。実際には次のように見えます:
Stripped: authorization, cf-access-token, host
Added: cf-aig-authorization: Bearer <API_KEY>
cf-aig-metadata: {"userId": "<anonymous-uuid>"}
長時間実行されるプロセスから構造化出力を解析する必要があるすべてのCIシステムは最終的に JSONL のようなものへ行き着きます(そして OpenCode はすでにそれをサポートしています)。
ストリーミングパイプライン
我々はコーディネーターの出力をリアルタイムで処理しますが、ディスクへの書き込み負担を減らすために100行(または50ms)ごとにまとめてフラッシュします。ストリームが流れてくる中で特定のトリガーを監視し、step_finish イベントからトークン使用量を抜き出してコストを追跡したり、error イベントでリトライロジックを起動したりします。また出力の切断にも注意を払っています。もし step_finish が reason: "length" で到着したら、モデルが max_tokens 制限に当たって途中で切られたことを意味し、自動的にリトライするべきだとわかります。
予想していなかった運用上の頭痛の種の一つは、Claude Opus 4.7 や GPT-5.4 のような大きく高度なモデルが問題を考え込むのにかなり時間を費やすことがあり、ユーザーから見るとジョブがハングしているように見えてしまう点でした。ユーザーは頻繁にジョブをキャンセルしてレビュワーが機能していないと苦情を言いましたが、実際にはバックグラウンドで動作していました。これに対抗するため、30秒ごとに "Model is thinking... (Ns since last output)" を出力する非常に単純なハートビートログを追加したところ、この問題はほぼ完全に解消されました。
一つの大きなプロンプトではなく専門化されたエージェント群
一つのモデルに全てをレビューさせる代わりに、レビューをドメイン別エージェントに分割しました。各エージェントは何を探すか、そしてより重要なことに何を無視するかを厳密に指定した狭いスコープのプロンプトを持ちます。
例えばセキュリティレビューアは、明示的に「悪用可能または具体的に危険な」問題だけをフラグするよう指示されています:
フラグすべきもの
- 注入脆弱性(SQL、XSS、コマンド、パストラバーサル)
- 変更されたコードにおける認証/認可のバイパス
- ハードコードされたシークレット、資格情報、APIキー
- 不適切な暗号利用
- 信頼境界上の信頼できないデータに対する入力検証の欠如
フラグすべきでないもの
- 起こりにくい前提条件を必要とする理論的リスク
- 主要な防御が適切にある場合の多重防御(defense-in-depth)提案
- このMRに影響を与えない、変更されていないコードの問題
- "Consider using library X" スタイルの提案
実は、LLM に「何をしないか」を伝えることこそが真のプロンプトエンジニアリングの価値の所在でした。これらの境界がなければ、開発者はすぐに...