背景と前提
Antigravity IDE は Gemini をネイティブに内蔵した強力な AI 開発環境ですが、標準のチャット機能やコード補完だけでは対応しきれない、チーム固有のワークフローやプロジェクト特有のニーズがあります。
ここで扱うのはGemini API を直接呼び出して カスタム AI 開発ツール を構築する方法を、設計パターンから実装コード、プロダクション運用まで体系的に解説します。コードレビューの自動化、API ドキュメントの自動生成、テストケースの自動作成という3つの実践的なツールを通じて、AI 駆動の開発パイプラインを自ら設計・構築できるスキルを身につけましょう。
対象読者 : Antigravity を日常的に使用しており、TypeScript / Node.js の基礎知識がある開発者
前提環境 :
Antigravity IDE(最新版)
Node.js 20 以上
Google AI Studio のアカウントと API キー
Gemini API の基本セットアップ
API キーの取得と環境設定
まず、Google AI Studio から API キーを取得し、プロジェクトに安全に組み込みます。
// .env.local(gitignore に必ず含めること)
GEMINI_API_KEY = YOUR_GEMINI_API_KEY
// src/lib/gemini-client.ts
import { GoogleGenerativeAI } from "@google/generative-ai" ;
const genAI = new GoogleGenerativeAI (process.env. GEMINI_API_KEY ! );
// Gemini 2.5 Pro を使用(コード理解に最適)
export const codeModel = genAI. getGenerativeModel ({
model: "gemini-2.5-pro" ,
generationConfig: {
temperature: 0.2 , // コード関連は低温度で正確性重視
maxOutputTokens: 8192 ,
},
});
// Gemini 2.5 Flash を使用(高速処理向け)
export const flashModel = genAI. getGenerativeModel ({
model: "gemini-2.5-flash" ,
generationConfig: {
temperature: 0.3 ,
maxOutputTokens: 4096 ,
},
});
レート制限とリトライの実装
本番運用では、API のレート制限を適切に処理する仕組みが不可欠です。
// src/lib/rate-limiter.ts
interface RateLimiterConfig {
maxRequestsPerMinute : number ;
retryAttempts : number ;
baseDelay : number ; // ミリ秒
}
class GeminiRateLimiter {
private requestTimestamps : number [] = [];
private config : RateLimiterConfig ;
constructor ( config : RateLimiterConfig ) {
this .config = config;
}
async execute < T >( fn : () => Promise < T >) : Promise < T > {
await this . waitForSlot ();
for ( let attempt = 0 ; attempt < this .config.retryAttempts; attempt ++ ) {
try {
this .requestTimestamps. push (Date. now ());
return await fn ();
} catch ( error : any ) {
if (error?.status === 429 && attempt < this .config.retryAttempts - 1 ) {
// 指数バックオフでリトライ
const delay = this .config.baseDelay * Math. pow ( 2 , attempt);
console. warn ( `Rate limited. Retrying in ${ delay }ms...` );
await this . sleep (delay);
} else {
throw error;
}
}
}
throw new Error ( "Max retry attempts exceeded" );
}
private async waitForSlot () : Promise < void > {
const now = Date. now ();
const oneMinuteAgo = now - 60_000 ;
// 1分以内のリクエストのみカウント
this .requestTimestamps = this .requestTimestamps. filter (
( t ) => t > oneMinuteAgo
);
if ( this .requestTimestamps. length >= this .config.maxRequestsPerMinute) {
const oldestInWindow = this .requestTimestamps[ 0 ];
const waitTime = oldestInWindow + 60_000 - now;
await this . sleep (waitTime);
}
}
private sleep ( ms : number ) : Promise < void > {
return new Promise (( resolve ) => setTimeout (resolve, ms));
}
}
// 使用例: 無料枠は15RPM、有料枠は1000RPM
export const rateLimiter = new GeminiRateLimiter ({
maxRequestsPerMinute: 14 , // 余裕を持たせて14に設定
retryAttempts: 3 ,
baseDelay: 2000 ,
});
ツール1: AI コードレビュー自動化
設計方針
手動のコードレビューを補完する AI レビューツールを構築します。Git の差分(diff)を解析し、セキュリティリスク、パフォーマンス問題、設計上の懸念点を自動検出します。
// src/tools/code-reviewer.ts
import { codeModel } from "../lib/gemini-client" ;
import { rateLimiter } from "../lib/rate-limiter" ;
import { execSync } from "child_process" ;
interface ReviewResult {
severity : "critical" | "warning" | "info" ;
file : string ;
line ?: number ;
message : string ;
suggestion ?: string ;
}
interface ReviewReport {
summary : string ;
issues : ReviewResult [];
score : number ; // 0-100
}
const REVIEW_PROMPT = `あなたはシニアソフトウェアエンジニアです。以下のコード差分をレビューしてください。
## レビュー観点
1. **セキュリティ**: インジェクション、認証漏れ、機密情報の露出
2. **パフォーマンス**: N+1クエリ、不要な再レンダリング、メモリリーク
3. **設計**: SOLID原則違反、責務の混在、テスタビリティ
4. **エラーハンドリング**: 未処理の例外、不適切なエラーメッセージ
## 出力形式
JSON配列で返してください。各要素は以下の形式:
{
"severity": "critical" | "warning" | "info",
"file": "ファイルパス",
"line": 行番号(特定できる場合),
"message": "問題の説明",
"suggestion": "改善案のコード例(該当する場合)"
}
最後に全体スコア(0-100)と要約も含めてください。
## コード差分
` ;
export async function reviewGitDiff (
baseBranch : string = "main"
) : Promise < ReviewReport > {
// Git差分を取得
const diff = execSync ( `git diff ${ baseBranch }...HEAD` , {
encoding: "utf-8" ,
maxBuffer: 1024 * 1024 , // 1MB
});
if ( ! diff. trim ()) {
return {
summary: "差分がありません。" ,
issues: [],
score: 100 ,
};
}
// 大きな差分はチャンクに分割して処理
const chunks = splitDiffByFile (diff);
const allIssues : ReviewResult [] = [];
for ( const chunk of chunks) {
const result = await rateLimiter. execute ( async () => {
const response = await codeModel. generateContent (
REVIEW_PROMPT + chunk
);
return response.response. text ();
});
const parsed = parseReviewResponse (result);
allIssues. push ( ... parsed);
}
// 重複を除去してスコアを算出
const uniqueIssues = deduplicateIssues (allIssues);
const score = calculateScore (uniqueIssues);
return {
summary: generateSummary (uniqueIssues),
issues: uniqueIssues,
score,
};
}
function splitDiffByFile ( diff : string ) : string [] {
// diff --git で分割し、1チャンクあたり約4000トークンに収める
const files = diff. split ( / ^ diff --git / m ). filter (Boolean);
const chunks : string [] = [];
let currentChunk = "" ;
for ( const file of files) {
if ((currentChunk + file). length > 12000 ) {
if (currentChunk) chunks. push (currentChunk);
currentChunk = file;
} else {
currentChunk += `diff --git ${ file }` ;
}
}
if (currentChunk) chunks. push (currentChunk);
return chunks;
}
function calculateScore ( issues : ReviewResult []) : number {
let score = 100 ;
for ( const issue of issues) {
if (issue.severity === "critical" ) score -= 20 ;
else if (issue.severity === "warning" ) score -= 5 ;
else score -= 1 ;
}
return Math. max ( 0 , score);
}
CLI からの実行
// scripts/review.ts
import { reviewGitDiff } from "../src/tools/code-reviewer" ;
async function main () {
console. log ( "🔍 AI コードレビューを開始... \n " );
const report = await reviewGitDiff ( "main" );
console. log ( `📊 スコア: ${ report . score }/100` );
console. log ( `📝 サマリー: ${ report . summary } \n ` );
for ( const issue of report.issues) {
const icon =
issue.severity === "critical" ? "🔴" :
issue.severity === "warning" ? "🟡" : "🔵" ;
console. log ( `${ icon } [${ issue . severity }] ${ issue . file }:${ issue . line || "?"}` );
console. log ( ` ${ issue . message }` );
if (issue.suggestion) {
console. log ( ` 💡 ${ issue . suggestion }` );
}
console. log ();
}
}
main (). catch (console.error);
// 実行: npx tsx scripts/review.ts
// 期待出力:
// 🔍 AI コードレビューを開始...
// 📊 スコア: 75/100
// 📝 サマリー: 2件のセキュリティ警告と3件のパフォーマンス改善提案
// 🔴 [critical] src/api/auth.ts:42
// ユーザー入力がサニタイズされていません
// 💡 zod スキーマでバリデーションを追加してください
ツール2: API ドキュメント自動生成
TypeScript 型定義からドキュメントを生成
既存のコードベースから API ドキュメントを自動生成するツールです。TypeScript の型情報とコメントを解析し、開発者向けドキュメントを出力します。
// src/tools/doc-generator.ts
import { codeModel } from "../lib/gemini-client" ;
import { rateLimiter } from "../lib/rate-limiter" ;
import * as fs from "fs" ;
import * as path from "path" ;
import { glob } from "glob" ;
interface DocSection {
title : string ;
description : string ;
parameters ?: Array <{
name : string ;
type : string ;
required : boolean ;
description : string ;
}>;
returnType ?: string ;
examples : string [];
}
const DOC_PROMPT = `あなたはテクニカルライターです。以下のTypeScriptコードから、
開発者向けのAPIドキュメントをMarkdown形式で生成してください。
## 要件
1. 各関数・クラスの目的を簡潔に説明
2. パラメータの型と説明を表形式で記載
3. 戻り値の型と説明
4. 実際に動作するコード例を最低1つ
5. エラーケースと対処法
6. 関連する関数・クラスへのリンク
## 対象コード
` ;
export async function generateApiDocs (
sourceDir : string ,
outputPath : string
) : Promise < void > {
// API ルートファイルを検出
const apiFiles = await glob ( `${ sourceDir }/**/route.ts` , {
ignore: [ "**/node_modules/**" ],
});
const sections : string [] = [];
sections. push ( "# API リファレンス \n " );
sections. push ( `> 自動生成日時: ${ new Date (). toISOString () } \n ` );
for ( const file of apiFiles) {
const content = fs. readFileSync (file, "utf-8" );
const relativePath = path. relative (sourceDir, file);
// API エンドポイントのパスを推定
const endpoint = relativePath
. replace ( / \/ route \. ts $ / , "" )
. replace ( / \\ / g , "/" );
console. log ( `📄 ドキュメント生成中: /${ endpoint }` );
const doc = await rateLimiter. execute ( async () => {
const response = await codeModel. generateContent (
DOC_PROMPT +
` \n ファイル: ${ relativePath } \n エンドポイント: /${ endpoint } \n\n ${ content }`
);
return response.response. text ();
});
sections. push ( `## \` /${ endpoint } \`\n ` );
sections. push (doc);
sections. push ( " \n --- \n " );
}
// 型定義ファイルも処理
const typeFiles = await glob ( `${ sourceDir }/**/types.ts` , {
ignore: [ "**/node_modules/**" ],
});
if (typeFiles. length > 0 ) {
sections. push ( "# 型定義 \n " );
for ( const file of typeFiles) {
const content = fs. readFileSync (file, "utf-8" );
const relativePath = path. relative (sourceDir, file);
const doc = await rateLimiter. execute ( async () => {
const response = await codeModel. generateContent (
`以下のTypeScript型定義をドキュメント化してください。 \n\n ${ content }`
);
return response.response. text ();
});
sections. push ( `## ${ relativePath } \n ` );
sections. push (doc);
}
}
fs. writeFileSync (outputPath, sections. join ( " \n " ), "utf-8" );
console. log ( `✅ ドキュメント生成完了: ${ outputPath }` );
}
差分ドキュメント更新
フルスキャンは重い処理なので、変更されたファイルのみドキュメントを更新する差分モードも実装します。
// src/tools/doc-updater.ts
import { execSync } from "child_process" ;
import { generateApiDocs } from "./doc-generator" ;
export async function updateChangedDocs (
sourceDir : string ,
docPath : string
) : Promise < string []> {
// 直近のコミットで変更されたファイルを取得
const changedFiles = execSync ( "git diff --name-only HEAD~1 HEAD" , {
encoding: "utf-8" ,
})
. trim ()
. split ( " \n " )
. filter (( f ) => f. endsWith ( ".ts" ) && f. includes ( "api" ));
if (changedFiles. length === 0 ) {
console. log ( "📝 API 変更なし — ドキュメント更新をスキップ" );
return [];
}
console. log (
`📝 ${ changedFiles . length } ファイルのドキュメントを更新...`
);
// 該当セクションのみ更新(既存ドキュメントとマージ)
await generateApiDocs (sourceDir, docPath);
return changedFiles;
}
// 実行: npx tsx scripts/update-docs.ts
// 期待出力:
// 📝 3 ファイルのドキュメントを更新...
// 📄 ドキュメント生成中: /api/checkout
// 📄 ドキュメント生成中: /api/verify-session
// 📄 ドキュメント生成中: /api/webhook
// ✅ ドキュメント生成完了: docs/api-reference.md
ツール3: テストケース自動生成
ソースコードからテストを生成
関数の実装を解析し、境界値テスト、正常系/異常系テスト、エッジケースを自動生成します。
// src/tools/test-generator.ts
import { codeModel } from "../lib/gemini-client" ;
import { rateLimiter } from "../lib/rate-limiter" ;
import * as fs from "fs" ;
const TEST_PROMPT = `あなたはテストエンジニアです。以下のTypeScript関数に対するテストコードを
Vitest形式で生成してください。
## 要件
1. 正常系テスト(基本的な入出力の検証)
2. 境界値テスト(空文字列、0、null、undefined、最大値)
3. 異常系テスト(不正な入力、例外の検証)
4. エッジケーステスト(並行実行、タイムアウト、大量データ)
5. テスト名は日本語で書くこと(describe/itのラベル)
6. モックが必要な外部依存は vi.mock() で処理
## 出力
テストコードのみ出力(説明は不要)
## 対象コード
` ;
interface TestGenerationResult {
sourceFile : string ;
testFile : string ;
testCount : number ;
content : string ;
}
export async function generateTests (
sourceFile : string
) : Promise < TestGenerationResult > {
const source = fs. readFileSync (sourceFile, "utf-8" );
const testFile = sourceFile
. replace ( / \. ts $ / , ".test.ts" )
. replace ( / \/ src \/ / , "/tests/" );
const testCode = await rateLimiter. execute ( async () => {
const response = await codeModel. generateContent (
TEST_PROMPT + ` \n ファイル: ${ sourceFile } \n\n ${ source }`
);
return response.response. text ();
});
// マークダウンのコードブロックを除去
const cleanCode = testCode
. replace ( /```typescript \n ? / g , "" )
. replace ( /``` \n ? / g , "" )
. trim ();
// テストケース数をカウント
const testCount = (cleanCode. match ( / \b (it | test) \s * \( / g ) || []). length ;
// ディレクトリ作成
const dir = testFile. substring ( 0 , testFile. lastIndexOf ( "/" ));
fs. mkdirSync (dir, { recursive: true });
// ファイル書き出し
fs. writeFileSync (testFile, cleanCode, "utf-8" );
return {
sourceFile,
testFile,
testCount,
content: cleanCode,
};
}
// バッチ生成: ディレクトリ配下の全ファイルに対してテストを生成
export async function generateTestsForDirectory (
dir : string
) : Promise < TestGenerationResult []> {
const { glob } = await import ( "glob" );
const files = await glob ( `${ dir }/**/*.ts` , {
ignore: [
"**/*.test.ts" ,
"**/*.spec.ts" ,
"**/node_modules/**" ,
"**/*.d.ts" ,
],
});
const results : TestGenerationResult [] = [];
for ( const file of files) {
console. log ( `🧪 テスト生成中: ${ file }` );
const result = await generateTests (file);
console. log ( ` ✅ ${ result . testCount } テストケース → ${ result . testFile }` );
results. push (result);
}
return results;
}
// 実行: npx tsx scripts/generate-tests.ts src/lib/
// 期待出力:
// 🧪 テスト生成中: src/lib/rate-limiter.ts
// ✅ 8 テストケース → tests/lib/rate-limiter.test.ts
// 🧪 テスト生成中: src/lib/gemini-client.ts
// ✅ 5 テストケース → tests/lib/gemini-client.test.ts
プロダクション統合: CI/CD パイプライン
GitHub Actions との連携
これら3つのツールを CI/CD パイプラインに統合し、PR ごとに自動実行する設定例です。
# .github/workflows/ai-review.yml
name : AI Code Review & Docs
on :
pull_request :
branches : [ main ]
jobs :
ai-review :
runs-on : ubuntu-latest
permissions :
pull-requests : write
steps :
- uses : actions/checkout@v4
with :
fetch-depth : 0
- uses : actions/setup-node@v4
with :
node-version : 20
- run : npm ci
- name : AI コードレビュー
env :
GEMINI_API_KEY : ${{ secrets.GEMINI_API_KEY }}
run : npx tsx scripts/review.ts > review-report.json
- name : ドキュメント更新チェック
env :
GEMINI_API_KEY : ${{ secrets.GEMINI_API_KEY }}
run : npx tsx scripts/update-docs.ts
- name : レビュー結果をPRにコメント
uses : actions/github-script@v7
with :
script : |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('review-report.json', 'utf8'));
const body = `## 🤖 AI Code Review\n\nスコア: **${report.score}/100**\n\n${report.summary}`;
github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body
});
コスト最適化のプラクティス
Gemini API の利用コストを抑えながら品質を維持するための戦略を紹介します。
モデルの使い分け : 軽量な処理(lint チェック、フォーマット確認)には gemini-2.5-flash を、深い解析(セキュリティレビュー、アーキテクチャ評価)には gemini-2.5-pro を使用
キャッシュの活用 : 同一ファイルの解析結果をローカルにキャッシュし、変更がない限り再利用
// src/lib/response-cache.ts
import * as fs from "fs" ;
import * as crypto from "crypto" ;
const CACHE_DIR = ".ai-cache" ;
export function getCachedResponse ( input : string ) : string | null {
const hash = crypto. createHash ( "sha256" ). update (input). digest ( "hex" );
const cachePath = `${ CACHE_DIR }/${ hash }.json` ;
if (fs. existsSync (cachePath)) {
const cached = JSON . parse (fs. readFileSync (cachePath, "utf-8" ));
// 24時間以内のキャッシュのみ有効
if (Date. now () - cached.timestamp < 86_400_000 ) {
return cached.response;
}
}
return null ;
}
export function cacheResponse ( input : string , response : string ) : void {
const hash = crypto. createHash ( "sha256" ). update (input). digest ( "hex" );
fs. mkdirSync ( CACHE_DIR , { recursive: true });
fs. writeFileSync (
`${ CACHE_DIR }/${ hash }.json` ,
JSON . stringify ({ timestamp: Date. now (), response }),
"utf-8"
);
}
バッチ処理 : 複数のファイルをまとめて1回の API 呼び出しで処理(トークン制限内であれば)
差分ベースの処理 : フルスキャンを避け、変更されたファイルのみを解析対象にする
エラーハンドリングとフォールバック戦略
本番環境では API 障害を想定した堅牢なエラーハンドリングが重要です。
// src/lib/error-handler.ts
type FallbackStrategy = "skip" | "cache" | "simplified" ;
interface ToolConfig {
name : string ;
fallback : FallbackStrategy ;
timeout : number ;
}
export async function executeWithFallback < T >(
config : ToolConfig ,
primaryFn : () => Promise < T >,
fallbackFn ?: () => Promise < T >
) : Promise < T | null > {
try {
// タイムアウト付きで実行
const result = await Promise . race ([
primaryFn (),
new Promise < never >(( _ , reject ) =>
setTimeout (
() => reject ( new Error ( `Timeout: ${ config . name }` )),
config.timeout
)
),
]);
return result;
} catch ( error : any ) {
console. error ( `❌ ${ config . name } failed: ${ error . message }` );
switch (config.fallback) {
case "cache" :
// キャッシュから前回の結果を返す
console. log ( "📦 キャッシュから前回の結果を使用..." );
return fallbackFn ? fallbackFn () : null ;
case "simplified" :
// 軽量モデルに切り替えて再試行
console. log ( "⚡ Flash モデルにフォールバック..." );
return fallbackFn ? fallbackFn () : null ;
case "skip" :
default :
console. log ( "⏭️ スキップして次の処理へ..." );
return null ;
}
}
}
// 使用例
// const report = await executeWithFallback(
// { name: "code-review", fallback: "simplified", timeout: 30000 },
// () => reviewWithPro(diff),
// () => reviewWithFlash(diff)
// );
セキュリティに関する注意事項
Gemini API を使ったツールを運用する際のセキュリティ上の注意点をまとめます。
API キーの管理 : 環境変数で管理し、コードに直接記述しません。CI/CD では GitHub Secrets や Cloudflare Worker Secrets を使用
入力データのサニタイズ : コードの差分や型定義を送信する際、機密情報(.env の値、API キー、個人情報)が含まれていないか事前にフィルタリングする
出力の検証 : AI が生成したコード(テストやドキュメント)は、実行前に必ずレビューします。特にテストコード内の外部 API 呼び出しやファイルシステム操作に注意
// src/lib/sanitizer.ts
const SENSITIVE_PATTERNS = [
/(?:api [_-] ? key | secret | password | token) \s * [:=]\s * ['"][ ^ '"] + ['"] / gi ,
/-----BEGIN (?:RSA | EC ) ? PRIVATE KEY-----/ g ,
/(?:AKIA | ABIA | ACCA | ASIA) [0-9A-Z] {16} / g , // AWS Access Key
];
export function sanitizeForApi ( content : string ) : string {
let sanitized = content;
for ( const pattern of SENSITIVE_PATTERNS ) {
sanitized = sanitized. replace (pattern, "[REDACTED]" );
}
return sanitized;
}
個人開発者の視点から(実体験メモ)
ここまでの要点
重要なのは、これらのツールは人間の判断を置き換えるものではなく、補完するものだという点です。AI が検出した問題点をチームで議論し、AI が生成したテストを手動で検証するプロセスを組み合わせることで、初めて品質の高い開発パイプラインが完成します。
Antigravity MCP サーバー構築ガイドや Vercel AI SDK 連携ガイド とあわせて読むことで、AI 駆動の開発環境をさらに拡張できるでしょう。