はじめに
コードレビューはバグを発見し、知識を共有するための素晴らしいメカニズムですが、エンジニアリングチームのボトルネックになる最も確実な方法の1つでもあります。マージリクエストがキューに溜まり、レビュアーが最終的にコンテキストスイッチしてdiffを読み、変数名についていくつかの細かい指摘をして、著者が応答し、サイクルが繰り返されます。社内プロジェクト全体で、最初のレビューまでの中央値の待機時間は、しばしば数時間で測定されていました。
AIコードレビューの初期段階
AIコードレビューの実験を始めたとき、ほとんどの人が取るであろう道を進みました。いくつかの異なるAIコードレビューツールを試し、多くのツールが非常にうまく機能し、多くのツールが優れたカスタマイズと設定可能性を提供していることに気付きました。しかし、繰り返し出てくるテーマは、Cloudflareのような規模の組織に対して十分な柔軟性とカスタマイズを提供していなかったということです。
そこで、次に明らかな道に進みました。それは、git diffを取得し、半分焼けたプロンプトに突っ込み、大規模言語モデルにバグを見つけるよう求めることでした。結果は、曖昧な提案の洪水、幻覚した構文エラー、既に実装されている関数に「エラーハンドリングの追加を検討してください」というアドバイスなど、予想通りノイズが多くありました。
素朴な要約アプローチでは、特に複雑なコードベースで望む結果が得られないことに気付きました。
新しいアプローチ:オーケストレーションシステム
モノリシックなコードレビューエージェントをゼロから構築する代わりに、オープンソースのコーディングエージェントであるOpenCodeの周りにCI-ネイティブなオーケストレーションシステムを構築することにしました。
今日、Cloudflareのエンジニアがマージリクエストを開くと、調整されたAIエージェントの多様な組み合わせから初期パスを受け取ります。1つの大規模な汎用プロンプトを持つ1つのモデルに依存するのではなく、セキュリティ、パフォーマンス、コード品質、ドキュメント、リリース管理、および社内エンジニアリングコデックスへの準拠をカバーする最大7つの専門レビュアーを起動します。
これらの専門家は、コーディネーターエージェントによって管理されます。このエージェントは、彼らの発見を重複排除し、問題の実際の重大度を判断し、単一の構造化されたレビューコメントを投稿します。
実績
数万のマージリクエスト全体でこのシステムを社内で実行しています。クリーンなコードを承認し、実際のバグを印象的な精度でフラグし、本物の深刻な問題またはセキュリティ脆弱性を見つけたときにマージを積極的にブロックします。
これは、Code Orange: Fail Smallの一部として、エンジニアリングの回復力を向上させるための多くの方法の1つです。
アーキテクチャ:プラグインで月へ
数千のリポジトリ全体で実行する必要がある内部ツールを構築する場合、バージョン管理システムやAIプロバイダーをハードコーディングすることは、6か月以内にすべてを書き直すことを保証する素晴らしい方法です。
GitLabを今日サポートし、明日は何をサポートするかわかりませんが、異なるAIプロバイダーと異なる内部標準要件をサポートする必要があり、どのコンポーネントも他のコンポーネントについて知る必要がありません。
エントリーポイントがすべての設定をプラグインに委譲する構成可能なプラグインアーキテクチャでシステムを構築しました。プラグインは一緒に構成され、レビューの実行方法を定義します。
実行フロー
マージリクエストがレビューをトリガーするときの実行フローは次のようになります:
各プラグインは、3つのライフサイクルフェーズを持つReviewPluginインターフェースを実装します。
- Bootstrap hooks:並行して実行され、致命的ではありません。テンプレートフェッチが失敗した場合、レビューはそれなしで続行されます。
- Configure hooks:順序立てて実行され、致命的です。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 | ファイア・アンド・フォーゲットレビュー追跡 |
OpenCodeの内部での使用方法
いくつかの理由でOpenCodeをコーディングエージェントの選択肢として選びました:
- 社内で広範に使用しており、既にその動作方法に非常に精通していました
- オープンソースなので、上流に機能とバグ修正を貢献でき、問題を見つけたときに簡単に調査できます(執筆時点で、Cloudflareエンジニアは上流に45以上のプルリクエストをランディングしています!)
- 優れたオープンソースSDKを持っており、シームレスに機能するプラグインを簡単に構築できます
- 最も重要なことに、サーバーファーストとして構造化されており、テキストベースのユーザーインターフェースとデスクトップアプリがその上のクライアントとして機能します。これは、プログラムでセッションを作成し、SDKを介してプロンプトを送信し、CLIインターフェースをハックすることなく複数の同時セッションから結果を収集する必要があったため、ハード要件でした。
オーケストレーション層
オーケストレーションは2つの異なるレイヤーで機能します:
コーディネータープロセス
Bun.spawnを使用してOpenCodeを子プロセスとして生成します。コマンドライン引数ではなくstdinを介してコーディネータープロンプトを渡します。これは、ログでいっぱいの大規模なマージリクエスト説明をコマンドライン引数として渡そうとしたことがある場合、LinuxカーネルのARG_MAX制限に遭遇した可能性があるためです。
非常に大きなマージリクエストに対してCI ジョブの小さな割合でE2BIGエラーが表示され始めたときに、これを非常に迅速に学びました。
プロセスは--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",
},
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とは何か、そしてそれを何に使用するのか
このようなシステムで直面する大きな課題の1つは、構造化ログの必要性です。JSONは素晴らしい構造化形式ですが、有効なJSONブロブであるために「閉じられた」状態にあるすべてが必要です。これは、アプリケーションが有効なJSONブロブをディスクに書き込む機会を得る前に早期に終了する場合に特に問題があります。これはしばしば、デバッグログが最も必要なときです。
これが、JSONL(JSON Lines)を使用する理由です。これは、缶に書かれているとおりのことを行います。すべての行が有効で自己完結したJSONオブジェクトであるテキスト形式です。標準的なJSON配列とは異なり、ドキュメント全体を解析して最初のエントリを読む必要はありません。行を読み、解析して、先に進みます。
これは、メモリに大規模なペイロードをバッファリングしたり、子プロセスがメモリ不足になったために到着しない可能性のある閉じる]を望む必要がないことを意味します。
実際には、次のようになります:
Stripped: authorization, cf-access-token, host
Added: cf-aig-authorization: Bearer <API_KEY>
cf-aig-metadata: {"userId": "<anonymous-uuid>"}
長時間実行されるプロセスから構造化出力を解析する必要があるすべてのCIシステムは、最終的にJSONLのようなものに着地します。しかし、私たちは車輪を再発明したくありませんでした。(そしてOpenCodeはすでにそれをサポートしています!)
ストリーミングパイプライン
コーディネーターの出力をリアルタイムで処理しますが、100行(または50ms)ごとにバッファリングしてフラッシュして、ディスクを遅いが苦痛なappendFileSyncの死から救います。
ストリームが流れるときに特定のトリガーを監視し、step_finishイベントからトークン使用量を引き出して、コストを追跡し、エラーイベントを使用して再試行ロジックを開始します。
また、出力の切り詰めに注意を払うようにしてください。step_finishがreason: "length"で到着した場合、モデルがmax_tokens制限に達し、途中で切り取られたことがわかるため、自動的に再試行する必要があります。
予測しなかった運用上の頭痛
Claude Opus 4.7やGPT-5.4のような大規模で高度なモデルは、問題を考え抜くのに非常に長い時間を費やすことができ、ユーザーにとってこれはハングしたジョブとまったく同じに見えることができます。
ユーザーはジョブをキャンセルし、レビュアーが意図したとおりに機能していないと不平を言うことが頻繁にあることがわかりました。実際には、バックグラウンドで機能していました。
これに対抗するために、30秒ごとに「Model is thinking... (Ns since last output)」を出力する非常にシンプルなハートビートログを追加しました。これはほぼ完全に問題を排除しました。
1つの大きなプロンプトの代わりに専門化されたエージェント
1つのモデルにすべてをレビューするよう求める代わりに、レビューをドメイン固有のエージェントに分割します。各エージェントには、何を探すべきか、そしてより重要なことに、何を無視するべきかを正確に伝える厳密にスコープされたプロンプトがあります。
たとえば、セキュリティレビュアーには、「悪用可能または具体的に危険な」問題のみをフラグするという明示的な指示があります:
フラグを立てるもの
- インジェクション脆弱性(SQL、XSS、コマンド、パストラバーサル)
- 変更されたコードの認証/認可バイパス
- ハードコードされたシークレット、認証情報、またはAPIキー
- 安全でない暗号化の使用
- 信頼境界での信頼できないデータの入力検証の欠落
フラグを立てないもの
- 起こりそうにない前提条件を必要とする理論的なリスク
- 主要な防御が適切な場合の多層防御の提案
- このMRが影響しない変更されていないコード内の問題
- 「ライブラリXの使用を検討してください」スタイルの提案
LLMに何をしないかを伝えることが、実際のプロンプトエンジニアリング値が存在する場所であることがわかります。これらの境界がなければ、開発者が即座に無視する投機的な理論的警告の消防ホースが得られます。