AIが書いたコードがPRに溢れるようになってから、私のレビュー時間は逆に増えました。生成されたコードは一見きれいに見えるのですが、セキュリティの考慮が抜けていたり、テストが薄かったり、微妙な設計上の問題を含んでいたりします。人間の目だけで追うには限界があります。
そこで試したのが、AIエージェントにレビューをさせる逆転の発想です。AIが書いたコードをAIがチェックします。聞こえはシンプルですが、単一のLLMに「このコードをレビューして」と投げるだけでは不十分でした。コードレビュー、テスト確認、セキュリティスキャンはそれぞれ専門性が異なり、一つのエージェントに全部やらせると品質が落ちます。
AgentKit 2.0とGitHub Actionsを使って、3つの専門エージェントが並列に動くCI/CD品質ゲートを構築する方法を順番にご紹介します。実際に本番で動かして学んだ設計上の判断や落とし穴も含めて、できる限り具体的に書きます。
品質ゲートの全体アーキテクチャ
マルチエージェント品質ゲートは、PRが作成・更新されるたびに自動実行されます。全体の流れはシンプルです。
GitHub ActionsがPRイベントをトリガーし、Antigravity上で動くAgentKit 2.0オーケストレーターが起動します。オーケストレーターは3つのサブエージェントを並列に起動します。
- コードレビューエージェント: diffを読み、設計上の問題・可読性・ベストプラクティス逸脱を指摘します
- テスト生成エージェント: 変更されたコードに対応するテストケースが不足していれば、自動で提案します
- セキュリティスキャンエージェント: SQLインジェクション・XSS・秘密情報のハードコードなどを検出します
3つのエージェントが完了したら、オーケストレーターが結果を統合してGitHub PRにコメントとして投稿します。問題が重大であればPRをブロックします。
この設計の核心は「エージェントの専門化」です。コードレビューと脆弱性検出では必要なコンテキストもプロンプトの最適化方向も異なります。一つのエージェントに全部やらせると、どれも中途半端になります。私が最初に試した単一エージェント方式では、セキュリティの指摘が薄くてSQLインジェクションを見逃したことがありました。賢いモデルを一つ置けば済む——そう期待していたこと自体が甘かったのだと気づいたのは、見逃しに気づいたあとでした。
AgentKit 2.0 オーケストレーターの実装
オーケストレーターは、3つのエージェントを並列に走らせ、結果を一つにまとめる薄い関数です。この記事のコードは並列実行と失敗の切り分けだけに絞り、Promise.allSettledで書いています。AgentKit 2.0のオーケストレーターAPIへ載せ替える場合も、各エージェントを「PRContextを受け取りAgentResultを返す関数」にしておけば、差し替えは呼び出し側の数行で済みます。
// scripts/quality-gate/orchestrator.ts
import { Octokit } from "@octokit/rest";
import { runCodeReviewAgent } from "./agents/code-review-agent";
import { runTestGenerationAgent } from "./agents/test-generation-agent";
import { runSecurityScanAgent } from "./agents/security-scan-agent";
import { formatComment } from "./format-comment";
import { upsertComment } from "./upsert-comment";
interface PRContext {
owner: string;
repo: string;
prNumber: number;
diff: string;
changedFiles: string[];
headSha: string;
}
interface AgentResult {
agentName: string;
findings: Finding[];
severity: "critical" | "warning" | "info" | "pass";
tokensUsed: number;
}
interface Finding {
file: string;
line?: number;
message: string;
severity: "critical" | "warning" | "info";
suggestion?: string;
}
export async function runQualityGate(ctx: PRContext): Promise<void> {
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
// 3エージェントを並列実行(名前を持たせて、失敗時にどれが落ちたか分かるようにする)
const agents = [
{ name: "コードレビュー", run: runCodeReviewAgent },
{ name: "テスト生成", run: runTestGenerationAgent },
{ name: "セキュリティスキャン", run: runSecurityScanAgent },
] as const;
const settled = await Promise.allSettled(agents.map(a => a.run(ctx)));
// 結果の集約
const results: AgentResult[] = settled.map((result, i) => {
if (result.status === "fulfilled") return result.value;
// エージェント失敗時は警告として記録(全体を止めない)
console.error(`Agent failed: ${agents[i].name}`, result.reason);
return {
agentName: agents[i].name,
findings: [{
file: "N/A",
message: `エージェント実行エラー: ${result.reason?.message ?? "unknown"}`,
severity: "warning" as const,
}],
severity: "warning" as const,
tokensUsed: 0,
};
});
// PRへのコメント投稿(既存コメントがあれば更新。実装は「よくある落とし穴」の4)
await upsertComment(octokit, ctx, formatComment(results));
// クリティカルな問題があればPRチェックを失敗させる
const hasCritical = results.some(r =>
r.findings.some(f => f.severity === "critical")
);
if (hasCritical) {
await octokit.repos.createCommitStatus({
owner: ctx.owner,
repo: ctx.repo,
sha: ctx.headSha,
state: "failure",
description: "Critical issues found by AI quality gate",
context: "ai-quality-gate",
});
process.exit(1);
}
await octokit.repos.createCommitStatus({
owner: ctx.owner,
repo: ctx.repo,
sha: ctx.headSha,
state: "success",
description: "AI quality gate passed",
context: "ai-quality-gate",
});
}ここでPromise.allではなくPromise.allSettledを選んでいます。Promise.allはどれか一つが失敗した時点で全体が失敗しますが、allSettledなら落ちたエージェントだけを警告に置き換えて、残り二つの結果を使えます。エージェントは外部APIに頼る部分が大きく、一時的なエラーは日常的に起きます。本番ではallSettledのほうが安全です。
エージェント名を配列で持たせているのも、同じ理由からです。失敗したときにコメントへ「unknown」と出ても、どこを見ればよいのか分かりません。「テスト生成」が落ちたと分かれば、読む側は原因を探す場所を絞れます。
コードレビューエージェントの実装
コードレビューエージェントは、Gemini 2.5 Proを使います。コードの意味理解が深く、設計上の問題を捉えるのに向いています。Flashより高価ですが、このエージェントだけはProを使う価値があると判断しています。
// scripts/quality-gate/agents/code-review-agent.ts
import { GoogleGenAI } from "@google/genai";
const client = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
export async function runCodeReviewAgent(ctx: PRContext): Promise<AgentResult> {
const systemPrompt = `あなたはシニアエンジニアとしてPRコードレビューを行います。
以下の観点でdiffを分析し、JSON形式で結果を返してください。
観点:
- コードの可読性と保守性
- DRY原則・単一責任原則の逸脱
- エラーハンドリングの漏れ
- パフォーマンス上の問題(N+1クエリ、不要なループ等)
- 型安全性の問題(TypeScriptの場合)
出力形式:
{
"findings": [
{
"file": "ファイルパス",
"line": 行番号,
"message": "問題の説明",
"severity": "critical|warning|info",
"suggestion": "改善案"
}
]
}
severity基準:
- critical: バグ・データ損失リスク・重大なパフォーマンス問題
- warning: ベストプラクティス逸脱・可読性の大きな問題
- info: スタイル・軽微な改善提案`;
const userPrompt = `以下のdiffをレビューしてください。
変更ファイル: ${ctx.changedFiles.join(", ")}
\`\`\`diff
${ctx.diff.slice(0, 50000)}
\`\`\``;
// diffが長すぎる場合は先頭50,000文字に切り詰める
const response = await client.models.generateContent({
model: "gemini-2.5-pro",
contents: [{ role: "user", parts: [{ text: userPrompt }] }],
config: {
systemInstruction: systemPrompt,
responseMimeType: "application/json",
temperature: 0.1, // 低温でより一貫した判断を促す
maxOutputTokens: 4096,
},
});
const raw = response.text ?? "{}";
let parsed: { findings: Finding[] };
try {
parsed = JSON.parse(raw);
} catch {
// JSONパース失敗時はフォールバック
console.error("Code review agent: JSON parse failed", raw.slice(0, 200));
parsed = { findings: [] };
}
const severity = parsed.findings.some(f => f.severity === "critical")
? "critical"
: parsed.findings.some(f => f.severity === "warning")
? "warning"
: "pass";
return {
agentName: "コードレビュー",
findings: parsed.findings,
severity,
tokensUsed: response.usageMetadata?.totalTokenCount ?? 0,
};
}responseMimeType: "application/json" を指定することで、Geminiが確実にJSON形式で返します。これを指定しないとマークダウンのコードブロックで囲まれた文字列が返ってきて、パースが複雑になります。最初にこれを知らなくて、正規表現でJSONを抽出するコードを書いていました。
テスト自動生成エージェントの実装
テストエージェントはFlashを使います。テスト生成はPro相当の深い思考より、変更コードに対応したテンプレートを高速に生成する方が重要だからです。
// scripts/quality-gate/agents/test-generation-agent.ts
export async function runTestGenerationAgent(ctx: PRContext): Promise<AgentResult> {
// テスト対象ファイルのみ抽出(テストファイル自体は除外)
const sourceFiles = ctx.changedFiles.filter(f =>
!f.includes(".test.") &&
!f.includes(".spec.") &&
!f.includes("__tests__") &&
(f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js"))
);
if (sourceFiles.length === 0) {
return {
agentName: "テスト生成",
findings: [{ file: "N/A", message: "テスト対象のソースファイルなし", severity: "info" }],
severity: "pass",
tokensUsed: 0,
};
}
const systemPrompt = `あなたはテストエンジニアです。
変更されたコードに対してVitest/Jestでのテストケースが充足しているか確認し、
不足しているテストがあれば具体的なテストコードを提案してください。
出力形式:
{
"findings": [
{
"file": "変更されたファイル",
"message": "テスト状況の説明",
"severity": "warning|info|pass",
"suggestion": "追加すべきテストコードの例"
}
]
}`;
const userPrompt = `以下の変更を確認し、テストカバレッジを評価してください。
変更ソースファイル: ${sourceFiles.join(", ")}
\`\`\`diff
${ctx.diff.slice(0, 40000)}
\`\`\`
特に以下を確認してください:
1. 追加された関数・メソッドへのユニットテスト
2. エラーケース・境界値のテスト
3. 非同期処理のテスト`;
const response = await client.models.generateContent({
model: "gemini-2.5-flash",
contents: [{ role: "user", parts: [{ text: userPrompt }] }],
config: {
systemInstruction: systemPrompt,
responseMimeType: "application/json",
temperature: 0.2,
maxOutputTokens: 8192,
},
});
const raw = response.text ?? "{}";
let parsed: { findings: Finding[] };
try {
parsed = JSON.parse(raw);
} catch {
parsed = { findings: [{ file: "N/A", message: "テスト評価の解析に失敗しました", severity: "info" }] };
}
const severity = parsed.findings.some(f => f.severity === "warning") ? "warning" : "pass";
return {
agentName: "テスト生成",
findings: parsed.findings,
severity,
tokensUsed: response.usageMetadata?.totalTokenCount ?? 0,
};
}テスト対象ファイルの抽出フィルターは、地味ですが効きます。テストファイル自体が変更されているときに「それをさらにテストしてください」と返すのは、ノイズでしかありません。また、.ts・.tsx・.jsに限定しておけば、マークダウンやYAMLのような非コードファイルへのテスト提案も防げます。
セキュリティスキャンエージェントの実装
セキュリティエージェントはFalse Positiveとの戦いです。過検知が多いとエンジニアが警告を無視するようになります。私の運用では、Criticalを「即修正必須」、Warningを「マージ前に確認推奨」として、infoは基本的にノイズとして扱っています。
// scripts/quality-gate/agents/security-scan-agent.ts
export async function runSecurityScanAgent(ctx: PRContext): Promise<AgentResult> {
const systemPrompt = `あなたはセキュリティエンジニアです。
コードの変更を以下の脆弱性パターンで分析してください。
チェック項目:
- SQLインジェクション(動的クエリ構築)
- XSS(未サニタイズのユーザー入力)
- 秘密情報のハードコード(APIキー・パスワード・トークン)
- 安全でない乱数生成
- 認証・認可の迂回
- オープンリダイレクト
- 依存パッケージの直接インストール指示(requirements.txtなしのpip install等)
重要: 確実に脆弱性と判断できるものだけをcritical/warningにしてください。
推測や可能性レベルのものはinfoにとどめてください。
出力形式:
{
"findings": [
{
"file": "ファイルパス",
"line": 行番号,
"message": "脆弱性の説明(具体的に)",
"severity": "critical|warning|info",
"suggestion": "修正方法"
}
]
}`;
const userPrompt = `以下のdiffをセキュリティ観点でスキャンしてください。
\`\`\`diff
${ctx.diff.slice(0, 45000)}
\`\`\``;
const maxRetries = 3;
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await client.models.generateContent({
model: "gemini-2.5-flash",
contents: [{ role: "user", parts: [{ text: userPrompt }] }],
config: {
systemInstruction: systemPrompt,
responseMimeType: "application/json",
temperature: 0.1,
maxOutputTokens: 4096,
},
});
const raw = response.text ?? "{}";
let parsed: { findings: Finding[] };
try {
parsed = JSON.parse(raw);
} catch {
parsed = { findings: [] };
}
const severity = parsed.findings.some(f => f.severity === "critical")
? "critical"
: parsed.findings.some(f => f.severity === "warning")
? "warning"
: "pass";
return {
agentName: "セキュリティスキャン",
findings: parsed.findings,
severity,
tokensUsed: response.usageMetadata?.totalTokenCount ?? 0,
};
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err));
if (attempt < maxRetries) {
// 指数バックオフ: 1回目の失敗後に2秒、2回目の失敗後に4秒(3回目は待たずに打ち切る)
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
}
}
}
// 3回リトライしても失敗した場合はwarningとして記録してスキップ
return {
agentName: "セキュリティスキャン",
findings: [{
file: "N/A",
message: `スキャンを完了できませんでした: ${lastError?.message ?? "unknown"}`,
severity: "warning",
}],
severity: "warning",
tokensUsed: 0,
};
}セキュリティエージェントだけ指数バックオフのリトライを実装しています。セキュリティスキャンの漏れは致命的なので、エラー時にもなるべく実行しきるためです。一方、3回試みて失敗した場合はcriticalではなくwarningとして記録しています。スキャン自体が失敗したことで次のデプロイを完全にブロックするより、「確認できなかった」という警告を残してエンジニアに判断を委ねる設計です。
GitHub Actions ワークフローへの統合
GitHub Actionsのワークフロー設定です。このYAMLをリポジトリの .github/workflows/ai-quality-gate.yml に配置します。
# .github/workflows/ai-quality-gate.yml
name: AI Quality Gate
on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main, develop]
jobs:
quality-gate:
name: AI Code Review & Security Scan
runs-on: ubuntu-latest
timeout-minutes: 10 # エージェントが無限ループしないよう上限設定
permissions:
contents: read
pull-requests: write
statuses: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 変更ファイルの周辺コードを読ませる拡張に備えて全履歴を取得
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci --workspace=scripts/quality-gate
- name: Get PR diff
id: diff
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr diff ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} > /tmp/pr.diff
echo "diff_size=$(wc -c < /tmp/pr.diff)" >> $GITHUB_OUTPUT
- name: Run AI Quality Gate
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
npx tsx scripts/quality-gate/index.ts \
--diff /tmp/pr.diff \
--pr $PR_NUMBER \
--owner $REPO_OWNER \
--repo $REPO_NAME \
--sha $HEAD_SHA
- name: Upload agent logs
if: always()
uses: actions/upload-artifact@v4
with:
name: quality-gate-logs
path: /tmp/quality-gate-*.json
retention-days: 7timeout-minutes: 10 は必ず設定してください。私が最初に運用したとき、Geminiのレート制限に引っかかったエージェントがリトライループで30分以上走り続けたことがありました。タイムアウトなしのアクションはGitHub Actionsの課金を予想外に増やします。
fetch-depth: 0 については補足があります。この構成ではdiffをgh pr diff(GitHub API経由)で取得するため、チェックアウトが浅いままでも動きます。全履歴を取っているのは、変更ファイルの周辺コードをエージェントに読ませたくなったときのための備えです。不要なら外して、チェックアウトの時間を削ってかまいません。
オーケストレーターの結果統合とコメント整形
エージェントの結果を読みやすいGitHubコメントに変換する部分です。
// scripts/quality-gate/format-comment.ts
import { MARKER } from "./upsert-comment";
export function formatComment(results: AgentResult[]): string {
const totalFindings = results.flatMap(r => r.findings);
const criticals = totalFindings.filter(f => f.severity === "critical");
const warnings = totalFindings.filter(f => f.severity === "warning");
const totalTokens = results.reduce((sum, r) => sum + r.tokensUsed, 0);
const statusEmoji = criticals.length > 0 ? "🔴" : warnings.length > 0 ? "🟡" : "✅";
const statusText = criticals.length > 0
? `${criticals.length}件のCritical問題が見つかりました。マージ前に修正が必要です。`
: warnings.length > 0
? `${warnings.length}件の警告があります。確認を推奨します。`
: "問題は検出されませんでした。";
let comment = `${MARKER}\n## ${statusEmoji} AI品質ゲート結果\n\n${statusText}\n\n`;
for (const result of results) {
if (result.findings.length === 0) continue;
comment += `### ${result.agentName}\n\n`;
const agentCriticals = result.findings.filter(f => f.severity === "critical");
const agentWarnings = result.findings.filter(f => f.severity === "warning");
const agentInfos = result.findings.filter(f => f.severity === "info");
for (const finding of [...agentCriticals, ...agentWarnings, ...agentInfos]) {
const icon = finding.severity === "critical" ? "🔴" : finding.severity === "warning" ? "🟡" : "ℹ️";
const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
comment += `${icon} **${location}**\n`;
comment += `${finding.message}\n`;
if (finding.suggestion) {
comment += `> 💡 ${finding.suggestion}\n`;
}
comment += "\n";
}
}
comment += `---\n`;
comment += `*消費トークン合計: ${totalTokens.toLocaleString()} tokens*\n`;
comment += `*実行エージェント: ${results.map(r => r.agentName).join(", ")}*\n`;
return comment;
}コメントの末尾にトークン消費量を表示しています。これはコスト把握に役立ちます。1PR あたり何トークン使っているかを可視化することで、「大きなPRは分割してほしい」という文化的なインセンティブにもなります。
よくある落とし穴と対処法
実際に運用してつまずいたポイントを正直に書きます。
1. 巨大なdiffでコンテキストウィンドウをオーバーフローする
1000行を超えるdiffを投げると、Geminiがdiffの末尾を無視することがあります。私の対策は、diffを50,000文字で切り詰めることと、変更ファイルが20件を超えるPRはエージェントをスキップしてコメントで「PRを分割してください」と通知することです。大きすぎるPRは人間のレビューも難しいので、エージェントではなくPRサイズ自体を問題として扱う設計にしました。
2. レート制限エラーが頻発する
3エージェントを並列実行するとGemini APIのRPM(リクエスト/分)制限に当たりやすくなります。対策は2つあります。まず、Flash系モデルとPro系モデルを混在させてクォータを分散させることです。次に、GitHub Actionsのconcurrency設定を使って同一ブランチのワークフローが同時に複数走らないようにすることです。
concurrency:
group: quality-gate-${{ github.head_ref }}
cancel-in-progress: true3. セキュリティエージェントがプレースホルダーを秘密情報と誤検知する
.env.example に含まれる YOUR_API_KEY_HERE のようなプレースホルダーを「APIキーのハードコード」と判断することがあります。システムプロンプトに「.env.exampleと.env.sampleは除外してください」と明示すると、この種の誤検知は減ったと感じています。ただし件数までは数えていないので、効果の大きさは皆さまの環境でお確かめください。
4. pushのたびにPRのコメントが増えていく
synchronizeイベント(commitのpush)ごとにコメントが追記されて、PRが長大なコメントで埋まります。対策は、直前のAI品質ゲートコメントを探して更新(edit)し、1PRにつき1コメントだけ残すことです。探す目印には、見出しの文言ではなくHTMLコメントのマーカーを使います。見出しは文言を直した途端に一致しなくなりますが、マーカーなら表示に影響せず、変わりません。もう一点、listCommentsは既定で30件しか返しません。会話の長いPRでは自分のコメントが2ページ目以降に押し出され、見つからないまま新規投稿が続いてしまうため、paginateで全件を辿ります。
// scripts/quality-gate/upsert-comment.ts
import type { Octokit } from "@octokit/rest";
export const MARKER = "<!-- ai-quality-gate -->";
export async function upsertComment(
octokit: Octokit,
ctx: { owner: string; repo: string; prNumber: number },
body: string,
): Promise<void> {
// listCommentsは1ページ30件が既定。paginateで全ページを辿る
const comments = await octokit.paginate(octokit.issues.listComments, {
owner: ctx.owner, repo: ctx.repo, issue_number: ctx.prNumber, per_page: 100,
});
const existing = comments.find(c => c.body?.includes(MARKER));
if (existing) {
await octokit.issues.updateComment({
owner: ctx.owner, repo: ctx.repo, comment_id: existing.id, body,
});
} else {
await octokit.issues.createComment({
owner: ctx.owner, repo: ctx.repo, issue_number: ctx.prNumber, body,
});
}
}formatCommentの先頭にMARKERを埋め込むのを忘れないでください。マーカーのないコメントは「自分のコメント」として認識されません。
5. エージェントが「問題なし」を返してくれない
プロンプトを工夫しないと、エージェントが必ず何かを指摘しようとします。「何も問題がなければ空の配列を返してよい」と明示的に伝えることが大切です。そうしないと、些細なコーディングスタイルを全てwarningとして上げてくるノイジーなエージェントができあがります。
コスト管理と本番モニタリング
運用コストの試算と、LangFuseを使った可視化について紹介します。
1PRあたりの典型的なトークン消費量は、私の場合以下の通りです。
- コードレビューエージェント(Pro): 約8,000〜25,000 tokens
- テスト生成エージェント(Flash): 約5,000〜15,000 tokens
- セキュリティスキャンエージェント(Flash): 約4,000〜12,000 tokens
中規模のPRで合計2万トークン前後、大きなPRで5万トークン程度です。Gemini 2.5 Proの価格(2026年5月時点)で私が試算した範囲では、1回の実行あたり$0.05〜$0.15が目安でした。
ここに、見落としやすい点が一つあります。このワークフローはsynchronizeでも走るので、課金の単位はPRではなく「pushの回数」です。1日10本のPR、営業日を22日として220PR。実行が1PRにつき1回なら月$11〜$33、平均3回pushされるなら月$33〜$99と、幅が3倍に開きます。最初に月額を見積もったとき、私はPRの本数だけを掛けて、pushの回数を数え忘れていました。最初の1か月はLangFuseで実行回数を数えてから、予算を決めていただければと思います。
LangFuseとの連携はAntigravity × LangFuse エージェント可観測性ガイドで詳しく解説していますが、品質ゲートのコンテキストでは以下の4つのメトリクスをトレースすることをおすすめします。
- 各エージェントのlatency(何秒かかったか)
- トークン消費量(モデルごと)
- Finding件数と severity 分布
- False Positive率(エンジニアが「この指摘は不要」とコメントした割合)
False Positive率は手動追跡が必要ですが、月次でPRコメントをサンプリングして計算することで、プロンプトの改善サイクルを回せます。この数字は、母数が小さいうちは大きく揺れます。それでも、プロンプトを直す前と後で「直した/却下した」を同じやり方で数えておけば、改善が効いたのかどうかは見えてきます。数字そのものより、同じ測り方を続けることのほうが大切だと感じています。
AgentKitの実装詳細はAgentKit 2.0 実践ガイドを、マルチエージェントの並列設計パターンは上級マルチエージェントオーケストレーションも参考にしてください。
この記事のコードを実走して確かめたこと
記事に載せたコードのうち、Gemini APIやGitHub APIを呼ばずに確かめられる部分は、Node 22(22.23.2)で実際に動かしました。エージェントとOctokitはモックに置き換えているため、確認できたのは「分岐とアルゴリズムの挙動」までです。実APIでの応答は、皆さまの環境でお確かめください。
| 確認した箇所 | 結果 |
|---|---|
| 3エージェントのうち1つが429で失敗 | 他の2つは結果が残り、失敗分は「テスト生成」の名前付きでwarningになりました |
| 35件のコメントがあるPR(自分のコメントは33番目) | 1ページ目だけ(30件)を見る実装では見つからず、paginateでは33番目を更新できました |
| リトライ3回のバックオフ | 待機は2,000msと4,000msの2回だけでした。以前コメントに書いていた「8秒」は、一度も発生しない値でした |
最後の行は、自分の書いたコメントに自分で驚いた部分です。リトライの上限を3回にしていながら、3回目の失敗後には待つ理由がありません。コメントと実装が食い違っていても、動くコードは何も言ってくれません。**止まるときは、どこで止まったかを言えるようにしておきます。**そのためにも、コードの横に書いた数字は、一度は実際に走らせて確かめるようにしています。
今日から始める一歩
まずはセキュリティスキャンエージェントだけをGitHub Actionsに組み込んでみてください。コードレビューやテスト生成より実装が単純で、効果がわかりやすいからです。
.github/workflows/ai-security-scan.yml として単一エージェントのワークフローを作り、1週間動かしてみてください。どんな指摘が来るか、どれが本物の問題でどれがノイズかを把握してから、3エージェント構成に拡張するのが、スムーズに本番導入できる順序だと思います。
いま思えば、最初に単一エージェントで済ませようとしたのは、レビューを早く終わらせたい気持ちが先に立っていたからかもしれません。
AIが書いたコードをAIに見てもらう構図は、いまも少し奇妙に感じています。それでも、指摘の一つ一つが「見るべき場所の叩き台」になってくれたおかげで、私の目は設計の判断に使えるようになりました。見る場所の絞り込みはエージェントが、マージの判断は私が。 この線引きだけは、PRが立て込む週でも崩さないようにしています。