コーディングエージェントは「デモでは動くが、実際のコードベースでは使い物にならない」という評価を受けることが多いです。その理由は、多くの実装が単一のモデルに全てを任せる設計だからです。コードレビュー、テスト生成、リファクタリング提案は、それぞれ求められるコンテキスト戦略も、失敗した時の回復方法も、根本的に異なります。
Gemma 4はローカル実行と低コストAPI呼び出しの両方で使えるモデルとして、個人開発者にとって現実的な選択肢になりました。API料金を気にせずに実験し、プロダクションではクラウドAPIに切り替える運用が可能です。ここではAntigravityのAgentKit 2.0を使い、この3つの役割を別々のエージェントに割り当てて協調させるシステムを実際に構築します。
コーディングエージェントが「使えるツール」になる条件
Gemma 4が登場する前、個人がコーディングエージェントを本番運用しようとすると、コスト問題が壁になっていました。GPT-4やGemini 2.5 Proのような高性能モデルはコードの品質理解に優れていますが、大規模なコードベースを継続的に処理するとAPIコストが膨らみます。
私の手元では、500行前後の差分レビュー1回あたりのコストが、同じ仕事をGemini 2.5 Proに任せた場合のおよそ20分の1に収まりました。金額そのものより、桁がひとつ変わることのほうが効きます。1日50件のPRを回しても月額が数ドルの範囲に留まるなら、「レビューを回すかどうか」を毎回コストで悩まなくて済みます。判断の回数が減ることが、個人開発では一番の効き目でした。
ただしコストだけで選ぶと失敗します。実際に作ってみて見えた得意と不得意を、そのまま書いておきます。
Gemma 4が得意なこと:コードスタイルとベストプラクティスのレビュー、既存テストのパターンを参考にしたテストコード生成、特定スコープのリファクタリング提案(関数1つ、クラス1つ単位)。
Gemma 4が苦手なこと:複数ファイルにまたがる設計上の問題の把握、初見のコードベースから「何をテストすべきか」を自律的に判断すること、抽象度の高いアーキテクチャレビュー。
ここは誤解されやすいので、はっきり書いておきます。苦手な理由は「コンテキストウィンドウが小さいから」ではありません。Gemma 4 モデルカードでは 12B / 26B A4B / 31B が 256K トークン、E2B / E4B でも 128K のコンテキストを持つと明記されています。リポジトリ数十ファイル分なら、物理的には入ります。
問題は入るかどうかではなく、入れた後に思い出せるかどうかです。同じモデルカードの長文脈ベンチマーク(MRCR v2・128K の中に8つの手がかりを埋めた設定)は、31B が 66.4%、26B A4B が 44.1%。半分近く取りこぼす前提で組むほうが安全です。「全部渡して賢く判断してもらう」ではなく「渡す前に人間側で絞る」という設計になるのは、そのためです。
この特性を踏まえると、「Gemma 4に全てを任せる1つのエージェント」ではなく、「各エージェントがGemma 4の得意領域だけを担当する3エージェント構成」が現実的な設計になります。
システム全体設計 — 3エージェント協調アーキテクチャ
本記事で構築するシステムは以下の構成を取ります。
Manager Agent (Antigravity AgentKit 2.0)
├── Code Review Agent ← Gemma 4 (diff解析 + スタイルチェック)
├── Test Generation Agent ← Gemma 4 (既存テストパターン参照 + 新テスト生成)
└── Refactoring Agent ← Gemma 4 (関数/クラス単位のリファクタリング提案)
なぜManager Agentパターンを使うか
AgentKit 2.0のManager Agentは、タスクを受け取り適切なサブエージェントに委譲する調整役を担います。このパターンの利点は、各サブエージェントが独立して失敗・リトライできることです。Code Review Agentが失敗しても、Test Generation Agentの実行は影響を受けません。
単一エージェントで全タスクを処理する場合、一か所でエラーが発生すると全体が停止します。Manager Agentパターンでは、Manager自体は「どのエージェントに何を任せるか」の判断だけを行い、実際の処理は各サブエージェントに分離されます。
コンテキスト戦略の設計
3つのエージェントそれぞれで、Gemma 4に渡すコンテキストを意図的に絞ります。
- Code Review Agent: gitのdiffのみ(コミット前後の差分)
- Test Generation Agent: 対象ファイル + 既存テストファイル + テストフレームワーク設定
- Refactoring Agent: 対象関数/クラスのみ(ファイル全体ではない)
この設計は先ほどの取りこぼしを避けるためのもので、「必要な情報だけを正確に渡す」という原則に従っています。
環境構築と初期セットアップ
まずAntigravity上でAgentKit 2.0のプロジェクトを初期化します。
# Antigravity ターミナルで実行
npm create agentkit@latest my-coding-agent
cd my-coding-agent
npm install
# Gemma 4 API(Google AI Studio)の設定
# .env.local に以下を追加
# GEMMA_API_KEY=YOUR_GEMMA_API_KEY
# GEMMA_MODEL=gemma-4-26b-a4b-itプロジェクト構造は以下のようにします。
my-coding-agent/
├── src/
│ ├── agents/
│ │ ├── manager.ts ← Manager Agent
│ │ ├── code-review.ts ← コードレビューエージェント
│ │ ├── test-gen.ts ← テスト生成エージェント
│ │ └── refactor.ts ← リファクタリングエージェント
│ ├── tools/
│ │ ├── git-tools.ts ← git diff取得ツール
│ │ ├── file-tools.ts ← ファイル読み書きツール
│ │ └── github-tools.ts ← GitHub PR操作ツール
│ └── index.ts
├── .env.local
└── package.json
次にGemma 4クライアントの初期化コードを書きます。エラーハンドリングを最初から組み込んでおく点が肝心です。後から追加しようとすると、エージェント間の状態管理が複雑になります。
その前に、ここで一番つまずきやすい箇所を先に潰しておきます。Gemini API 経由で呼べる Gemma 4 のモデルIDは gemma-4-31b-it と gemma-4-26b-a4b-it の2つです(Run Gemma with the Gemini API)。Gemma 3 の感覚で 27B 相当のIDを書くと、そのサイズが Gemma 4 には存在しないため呼び出しが通りません。エラーはモデル名を返すだけなので、エージェントを3本書き上げてから気づくと切り分けに時間を取られます。最初の1回で疎通を確認してから先へ進んでください。
既定値には gemma-4-26b-a4b-it を置いています。総パラメータ 25.2B に対して推論時に動くのは 3.8B という MoE 構成で、31B Dense より応答が速く、それでいて LiveCodeBench v6 は 77.1%(31B は 80.0%)。日常のレビューでこの差は体感しにくい一方、待ち時間の差ははっきり出ます。設計レビューのように精度を優先したいときだけ 31B に切り替える、という使い分けにしています。
SDK にも一点。JavaScript の @google/generative-ai は 2025年8月31日でサポートが終了し、現行は @google/genai に統合されました。以下は新しい SDK 前提のコードです。
// src/lib/gemma-client.ts
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMMA_API_KEY });
export interface GemmaCallOptions {
model?: string;
maxOutputTokens?: number;
temperature?: number;
retries?: number;
}
export async function callGemma(
systemPrompt: string,
userMessage: string,
options: GemmaCallOptions = {}
): Promise<string> {
const {
model = "gemma-4-26b-a4b-it",
maxOutputTokens = 2048,
temperature = 0.2, // コーディングタスクは低めが安定する
retries = 3,
} = options;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await ai.models.generateContent({
model,
contents: userMessage,
config: {
systemInstruction: systemPrompt,
maxOutputTokens,
temperature,
},
});
const text = response.text;
if (!text || text.trim().length === 0) {
throw new Error("Empty response from Gemma 4");
}
return text;
} catch (error: unknown) {
const isLastAttempt = attempt === retries;
const errorMessage = error instanceof Error ? error.message : String(error);
if (isLastAttempt) {
throw new Error(
`Gemma 4 call failed after ${retries} attempts: ${errorMessage}`
);
}
// 429 以外でも必ず待つ。即座に再送すると同じ失敗を3回繰り返して終わる
const isRateLimited =
errorMessage.includes("429") || errorMessage.includes("rate limit");
const waitMs = isRateLimited ? 2 ** attempt * 1000 : 500;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
throw new Error("Unreachable");
}temperature: 0.2 にしている理由は、コーディングタスクでは再現性が重要だからです。同じ差分を2回レビューして全く違う指摘が返ってくると、開発者はレビュー結果を信用しなくなります。0.2 は品質を保ちながら文面が硬直しすぎない値として、運用の中で落ち着いた数字です。
ただしこれは公式の推奨から意図的に外した設定です。モデルカードのベストプラクティスは全ユースケース共通で temperature=1.0 / top_p=0.95 / top_k=64 を挙げています。ベンチマークのスコアはこの条件で出ているので、精度を再現したいなら推奨値に戻すべきです。私は「同じ入力に同じ指摘が返る」ことを精度より優先しました。どちらが正しいかではなく、何を優先するかで決まる箇所です。私自身は、レビュー結果が揺れることのほうが運用上の痛手が大きいと感じたので再現性を取りました。自分のエージェントでどちらを取るのかは、先に決めておいたほうが後の議論が短くなります。
もう一つ、Gemma 4 から思考モードが入りました。Gemini API では thinkingConfig.thinkingLevel に "high" か "minimal" を渡して切り替えます。スタイル違反の指摘のような定型作業で "high" のままにすると、返ってくる指摘は大きく変わらないのに応答が遅くなります。逆にリファクタリング提案では効きます。3つのエージェントで一律にせず、役割ごとに変えるのが現実的です。
コードレビューエージェントの実装
コードレビューエージェントの核心は「diffを受け取り、問題点をJSON形式で返す」設計にあります。テキスト形式だと後処理が難しいため、構造化された出力を最初から設計します。
// src/agents/code-review.ts
import { callGemma } from "../lib/gemma-client";
export interface ReviewComment {
file: string;
line: number;
severity: "error" | "warning" | "suggestion";
category: "security" | "performance" | "style" | "logic" | "test-coverage";
message: string;
suggestion?: string; // 修正案(省略可能)
}
export interface ReviewResult {
comments: ReviewComment[];
summary: string;
approvalStatus: "approved" | "changes-requested" | "comment";
}
const CODE_REVIEW_SYSTEM_PROMPT = `You are an expert code reviewer. Analyze the provided git diff and return feedback as valid JSON only.
Rules:
- Focus only on the changed lines in the diff
- Do NOT comment on unchanged code
- Return JSON matching the ReviewResult type exactly
- If no issues found, return empty comments array with "approved" status
- severity "error" = blocks merge, "warning" = should fix, "suggestion" = optional improvement
- Be specific: include the actual problematic code snippet in the message
- Return ONLY valid JSON. Do NOT wrap in markdown code fences.`;
export async function runCodeReviewAgent(
diff: string,
prTitle: string,
prDescription: string
): Promise<ReviewResult> {
const MAX_DIFF_CHARS = 8000;
let processedDiff = diff;
if (diff.length > MAX_DIFF_CHARS) {
// 変更行のみに絞り込む(+/-行以外は省略してコンテキスト節約)
processedDiff = diff
.split("\n")
.filter(
(line) =>
line.startsWith("+") ||
line.startsWith("-") ||
line.startsWith("@@") ||
line.startsWith("diff")
)
.join("\n")
.slice(0, MAX_DIFF_CHARS);
processedDiff += "\n[... diff truncated for context limit ...]";
}
const userMessage = `PR Title: ${prTitle}
PR Description: ${prDescription}
Git Diff:
\`\`\`diff
${processedDiff}
\`\`\`
Review this code change and return JSON only.`;
const rawResponse = await callGemma(CODE_REVIEW_SYSTEM_PROMPT, userMessage, {
temperature: 0.1,
maxOutputTokens: 1024,
});
try {
// Gemma 4がコードブロックで包む場合があるので除去
const cleaned = rawResponse
.replace(/^```json\n?/, "")
.replace(/\n?```$/, "")
.trim();
return JSON.parse(cleaned) as ReviewResult;
} catch {
// パース失敗時はフォールバック応答を返す(全体を止めない)
console.error(
"Failed to parse code review response:",
rawResponse.slice(0, 200)
);
return {
comments: [],
summary:
"Review completed but response parsing failed. Manual review recommended.",
approvalStatus: "comment",
};
}
}このコードで注目すべき点が2つあります。1つ目は、diffが長い場合に変更行のみに絞り込む前処理です。Gemma 4の実測では、変更されていないコンテキスト行は精度改善にほとんど寄与しないため、削除しても問題ありません。むしろ削除することで、Gemma 4が重要な変更箇所に集中できます。
2つ目は、JSONパース失敗時のフォールバック処理です。エラーで全体が止まるより、「レビューできなかった」という情報を返す方が運用上の価値が高いです。
テスト自動生成エージェントの実装
テスト生成エージェントは、コードレビューエージェントとは異なるコンテキスト戦略を取ります。「既存のテストを参照させる」ことが最も重要です。
多くのチームにはテストの書き方に暗黙のルールがあります。describeとitのネスト構造、モックの書き方、アサーションの粒度など。Gemma 4にこのルールを推論させる最も効果的な方法は、既存テストをSystem Promptではなくユーザーメッセージとして渡すことです。
// src/agents/test-gen.ts
import { callGemma } from "../lib/gemma-client";
export interface TestGenerationResult {
testCode: string;
testFilePath: string;
testFramework: "vitest" | "jest" | "unknown";
coveredScenarios: string[];
missingScenarios: string[]; // Gemma 4が「本来テストすべきだが今回省いた」と判断したもの
}
const TEST_GEN_SYSTEM_PROMPT = `You are an expert test engineer specializing in TypeScript/JavaScript testing.
Your task: Generate test code for the provided source file, following the EXACT style of the existing tests.
Rules:
- Match the existing test structure precisely (same describe/it patterns, same assertion style)
- Use the same mocking approach as existing tests
- Generate tests for: happy path, edge cases, error cases
- Return ONLY valid JSON. Do NOT wrap in markdown code fences.
- The test code must be ready to run without modification`;
export async function runTestGenerationAgent(params: {
sourceFile: string;
sourcePath: string;
existingTestFile?: string;
existingTestPath?: string;
testFramework: string;
}): Promise<TestGenerationResult> {
const { sourceFile, sourcePath, existingTestFile, existingTestPath, testFramework } = params;
const existingTestSection = existingTestFile
? `## Existing Test Example (FOLLOW THIS STYLE EXACTLY)
File: ${existingTestPath}
\`\`\`typescript
${existingTestFile.slice(0, 3000)}${existingTestFile.length > 3000 ? "\n// ... (truncated)" : ""}
\`\`\``
: `## No existing tests found. Use ${testFramework} best practices.`;
const userMessage = `${existingTestSection}
## Source File to Test
File: ${sourcePath}
\`\`\`typescript
${sourceFile}
\`\`\`
Generate comprehensive tests. Return JSON only:
{"testCode":"...","testFilePath":"...","testFramework":"vitest|jest|unknown","coveredScenarios":["..."],"missingScenarios":["..."]}`;
const rawResponse = await callGemma(TEST_GEN_SYSTEM_PROMPT, userMessage, {
maxOutputTokens: 2048,
temperature: 0.1,
});
try {
const cleaned = rawResponse
.replace(/^```json\n?/, "")
.replace(/\n?```$/, "")
.trim();
const result = JSON.parse(cleaned) as TestGenerationResult;
if (!result.testCode || result.testCode.trim().length < 50) {
throw new Error("Generated test code is too short or empty");
}
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Test generation failed: ${errorMessage}`);
}
}実装で意図的な判断をした箇所が「missingScenariosフィールド」です。Gemma 4は時に「テストできるが今回の生成には含めなかったシナリオ」を判断できます。これを明示的にJSONフィールドとして要求することで、「このテストで何がカバーされていないか」をエンジニアが把握できます。実際の運用では、このフィールドに「非同期エラーシナリオ」「null入力の境界値ケース」が記録されることが多かった。
リファクタリング提案エージェントの実装
リファクタリングエージェントの設計は、他の2つと異なるアプローチを取ります。「提案を返す」だけでなく「なぜそのリファクタリングが必要か」の理由も同時に返すことで、エンジニアが自分で判断できるようにします。
// src/agents/refactor.ts
import { callGemma } from "../lib/gemma-client";
export interface RefactoringProposal {
original: string;
refactored: string;
rationale: string;
impactLevel: "low" | "medium" | "high";
breakingChange: boolean;
estimatedEffort: "minutes" | "hours" | "days";
relatedPatterns: string[];
}
const REFACTOR_SYSTEM_PROMPT = `You are a senior software architect specializing in code refactoring.
Analyze the provided code and return a single, highest-priority refactoring proposal as JSON.
Focus on one of these, in priority order:
1. Security vulnerabilities
2. Performance bottlenecks (N+1, unnecessary loops, memory leaks)
3. Readability and maintainability
4. Design pattern improvements
Return ONLY valid JSON. Do NOT include markdown code fences in the JSON string values.`;
export async function runRefactoringAgent(params: {
targetCode: string;
language: string;
context?: string;
}): Promise<RefactoringProposal | null> {
const { targetCode, language, context } = params;
// コードが短すぎる場合はスキップ
if (targetCode.trim().split("\n").length < 5) {
return null;
}
const userMessage = `Language: ${language}
${context ? `Context: ${context}\n` : ""}
Code to refactor:
\`\`\`${language}
${targetCode}
\`\`\`
Return the highest-priority refactoring as JSON:
{"original":"...","refactored":"...","rationale":"...","impactLevel":"low|medium|high","breakingChange":false,"estimatedEffort":"minutes|hours|days","relatedPatterns":["..."]}`;
const rawResponse = await callGemma(REFACTOR_SYSTEM_PROMPT, userMessage, {
temperature: 0.15,
maxOutputTokens: 1500,
});
try {
const cleaned = rawResponse
.replace(/^```json\n?/, "")
.replace(/\n?```$/, "")
.trim();
return JSON.parse(cleaned) as RefactoringProposal;
} catch {
// リファクタリング提案の失敗は非致命的 — nullを返してManager Agentに判断させる
return null;
}
}「1つの最優先リファクタリングのみを返す」設計にしている理由は、複数の提案を一度に返すと「どれから手をつけるか」の判断コストが発生するからです。優先度の高い1つに絞ることで、明確なアクションを提供できます。
Manager Agentによる統合オーケストレーション
3つのエージェントを統合するManager Agentを実装します。このコンポーネントが本システムの肝です。
// src/agents/manager.ts
import { runCodeReviewAgent, ReviewResult } from "./code-review";
import { runTestGenerationAgent, TestGenerationResult } from "./test-gen";
import { runRefactoringAgent, RefactoringProposal } from "./refactor";
export interface CodingAgentInput {
diff: string;
prTitle: string;
prDescription: string;
changedFiles: Array<{
path: string;
content: string;
testFilePath?: string;
testFileContent?: string;
language: string;
}>;
}
export interface CodingAgentOutput {
review: ReviewResult | null;
tests: TestGenerationResult[];
refactoring: RefactoringProposal[];
processingTime: number;
errors: string[];
}
export async function runCodingAgentSystem(
input: CodingAgentInput
): Promise<CodingAgentOutput> {
const startTime = Date.now();
const errors: string[] = [];
const tests: TestGenerationResult[] = [];
const refactoring: RefactoringProposal[] = [];
// コードレビューを並列で開始(diffはファイルに依存しないため先行実行可能)
const reviewPromise = runCodeReviewAgent(
input.diff,
input.prTitle,
input.prDescription
).catch((err: Error) => {
errors.push(`Code review failed: ${err.message}`);
return null;
});
// テスト生成とリファクタリングを並列で実行
const fileProcessingPromises = input.changedFiles.map(async (file) => {
// TypeScript/JavaScriptファイルのみテスト生成対象
if (["ts", "tsx", "js", "jsx"].some((ext) => file.path.endsWith(`.${ext}`))) {
try {
const testResult = await runTestGenerationAgent({
sourceFile: file.content,
sourcePath: file.path,
existingTestFile: file.testFileContent,
existingTestPath: file.testFilePath,
testFramework: detectTestFramework(file.testFileContent),
});
tests.push(testResult);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
errors.push(`Test generation failed for ${file.path}: ${errorMessage}`);
}
}
// 全ファイルでリファクタリング提案を取得
try {
const refactorResult = await runRefactoringAgent({
targetCode: file.content,
language: file.language,
});
if (refactorResult) {
refactoring.push(refactorResult);
}
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
errors.push(
`Refactoring analysis failed for ${file.path}: ${errorMessage}`
);
}
});
const [review] = await Promise.all([
reviewPromise,
Promise.all(fileProcessingPromises),
]);
return {
review,
tests,
refactoring,
processingTime: Date.now() - startTime,
errors,
};
}
function detectTestFramework(testFileContent?: string): string {
if (!testFileContent) return "vitest";
if (
testFileContent.includes("from 'vitest'") ||
testFileContent.includes('from "vitest"')
) {
return "vitest";
}
if (
testFileContent.includes("from 'jest'") ||
testFileContent.includes("@jest/globals")
) {
return "jest";
}
return "vitest";
}Promise.allでコードレビューとファイル処理を並列実行している点に注目してほしい。直列実行と比較して処理時間が約60%短縮されます。コードレビューはdiffのみを必要とし、ファイル処理はdiffに依存しないため、完全に並列化できます。
Cloudflare Workersへのデプロイと運用最適化
AntigravityはCloudflare Workersへのデプロイをサポートしています。コーディングエージェントをWebhook APIとして公開することで、GitHub ActionsやCI/CDから呼び出せるようになります。
// src/index.ts (Cloudflare Workers エントリポイント)
import { runCodingAgentSystem, CodingAgentInput } from "./agents/manager";
interface Env {
WEBHOOK_TOKEN: string;
GEMMA_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const authHeader = request.headers.get("Authorization");
if (authHeader !== `Bearer ${env.WEBHOOK_TOKEN}`) {
return new Response("Unauthorized", { status: 401 });
}
let input: CodingAgentInput;
try {
input = (await request.json()) as CodingAgentInput;
} catch {
return new Response("Invalid JSON body", { status: 400 });
}
// Cloudflare Workers の30秒タイムアウト対策
const timeoutMs = 25000;
const agentPromise = runCodingAgentSystem(input);
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Processing timeout")), timeoutMs)
);
try {
const result = await Promise.race([agentPromise, timeoutPromise]);
return new Response(JSON.stringify(result), {
headers: { "Content-Type": "application/json" },
});
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
return new Response(JSON.stringify({ error: errorMessage }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
},
};コスト削減のための事前フィルタリング
本番運用で重要なのが、無駄なAPIコールを省く最適化です。
// 不要なAPI呼び出しを事前にスキップ
function shouldSkipReview(diff: string): boolean {
const nonCodeExtensions = [".md", ".txt", ".json", ".yml", ".yaml", ".toml"];
const changedFiles = diff
.split("\n")
.filter((line) => line.startsWith("diff --git"))
.map((line) => line.split(" b/")[1]);
return changedFiles.every((file) =>
nonCodeExtensions.some((ext) => file?.endsWith(ext))
);
}
function shouldSkipTestGeneration(filePath: string): boolean {
return (
filePath.includes(".test.") ||
filePath.includes(".spec.") ||
filePath.endsWith(".d.ts") ||
filePath.includes("config.") ||
filePath.includes("/constants/")
);
}このフィルタリングにより、ドキュメントのみの変更やテストファイルへのコミット時に無駄なAPI呼び出しを回避できます。実際の運用では、全コミットの約30%がこのフィルタリングで除外された。
よくある落とし穴トップ5と解決策
Gemma 4とAgentKit 2.0でコーディングエージェントを本番運用した経験から、特に注意すべき落とし穴をまとめます。
落とし穴1: JSONレスポンスにコードブロックが混入する
Gemma 4は指示に反して、JSONをマークダウンのコードブロックで包むことがあります。対策は、replaceでコードブロックのマーカーを除去してからパースすること。System Promptに「Return ONLY valid JSON. Do NOT wrap in code fences.」と明示することで発生頻度を下げられます。
落とし穴2: コンテキストが長くなると後半の指示を無視する
Gemma 4に限らず、多くのLLMはコンテキストが長くなると後半の指示に対する注意が低下します。コーディングエージェントでは、ファイルの後半に書かれた重要な関数がレビューされないという形で現れます。冒頭で触れた長文脈ベンチマークの数字は、この現象を測定可能にしたものです。
対策は、System Promptを短く保ち(200トークン以下)、重要な指示はユーザーメッセージの冒頭に配置することです。
そのうえで、自分の環境で実際に取りこぼしが起きているかを測っておくと安心できます。私は検証用の差分を1つ用意し、末尾に明らかなバグ(未使用変数の混入や比較演算子の反転)を1箇所だけ仕込んで、それが指摘されるかを見ています。差分の行数を段階的に増やしていくと、自分の構成で「末尾が読まれなくなる長さ」がどのあたりかが分かります。私の構成では差分が 800 行を超えたあたりから見逃しが出始めたため、その手前でファイル単位に分割する処理を入れました。
落とし穴3: 並列API呼び出しがRate Limitに引っかかる
Promise.allで複数ファイルを並列処理すると、Rate Limitエラーが頻発します。対策は、p-limitライブラリで同時実行数を制限することです。
import pLimit from "p-limit";
const limit = pLimit(3); // 同時3リクエストまで
const fileProcessingPromises = input.changedFiles.map((file) =>
limit(async () => {
// ファイル処理のコード
})
);落とし穴4: Cloudflare Workersの30秒タイムアウトとの戦い
大きなPR(変更ファイルが10個以上)を処理しようとすると、Cloudflare Workersの30秒タイムアウトに引っかかります。短期的な対策はタイムアウト前に部分結果を返すことです。根本的な解決策としてはDurable Objectsを使った非同期処理への移行があります。本記事のコードでは25秒でタイムアウトし、それまでに処理できた結果を返す設計にしています。
落とし穴5: エラーログが不十分で運用中の問題を検知できない
本番で最初の週は「なぜかレビューが返ってこない」という問題が散発しました。原因はGemma 4のAPI呼び出し失敗を全てサイレントに無視していたからです。errors配列に全エラーを記録し、レスポンスに含める設計にすることで、問題の原因を素早く特定できるようになりました。
本番運用1ヶ月後の実測値
このシステムを個人プロジェクトで1ヶ月間運用した結果、API利用料は月間約4ドルに収まりました。GeminiやGPT-4を使った場合の試算(約80ドル)と比べると、コスト効率は十分です。
精度については、Gemini 2.5 Proと比べて「見逃し」が多いです。特にビジネスロジックの深い部分やアーキテクチャ上の問題は、Gemma 4では検出できないことが多くありました。一方で、スタイル違反・明らかなバグ・未使用変数・テストカバレッジのギャップといった定型的な問題の検出率は、Gemini 2.5 Proとほぼ同等でした。
個人開発者やスモールチームがCI/CDに組み込む用途としては、Gemma 4ベースのコーディングエージェントは十分に実用的です。全てを任せる相手としてではなく、定型的な指摘だけを引き受けてもらう相手として置くと、噛み合います。
次の一歩として、まずは gemma-4-26b-a4b-it で Code Review Agent の1本だけを動かし、自分のリポジトリの差分を10件ほど流してみてください。見逃しの傾向がつかめてから、Test Generation Agent と Refactoring Agent を足すほうが手戻りが少なくて済みます。
AgentKit 2.0 のオーケストレーション設計はAntigravity AgentKit 2.0 — 本番マルチエージェントオーケストレーション、エージェントの暴走を止めるガードレールの設計はAntigravity エージェント安全設計 — 暴走防止とガードレール実装でそれぞれ扱っています。
モデルIDひとつで半日を溶かした側の記録として、この記事が誰かの半日を守れたら嬉しく思います。