AgentKit 2.0とGemma 4の組み合わせを試みたエンジニアが最初にぶつかる壁は、「ローカルモデルとクラウドAPIをどこでどう切り替えるか」という設計判断です。コストを下げようとGemma 4に寄せると精度が落ち、品質を求めてGemini APIに頼ると月額コストが跳ね上がる。ここではその両立を実現したハイブリッドLLMルーティングの設計と実装を、本番環境の落とし穴ごと解説します。
なぜ今「ローカルGemma 4 × AgentKit 2.0」なのか
クラウドAPIコストの現実
マルチエージェントシステムのコスト計算は、単一の会話型AIとは桁が違う。エージェントが別のエージェントを呼び出し、その結果を評価してさらに呼び出す——この連鎖が積み重なると、1タスクあたりのAPIトークン消費は通常の10〜50倍になります。
実際に計測した数値を公開します。コードレビュー・ドキュメント生成・テスト自動生成を担う3エージェント構成を、すべてGemini 1.5 Proで動かしたとき、月間コストは約**$340**でした。中小スタートアップにとってこれは無視できない金額です。
Gemma 4をOllamaでローカル実行し、複雑なタスクのみGemini APIにルーティングする構成に切り替えたところ、月間コストは**$68**まで下がった。品質の劣化は軽微なタスク(定型コードの生成・コメント付け・単純なバグ修正)では事実上ゼロだった。
プライバシーとセキュリティの要件
エンタープライズ環境では、コスト以上にデータのロケーションが問題になります。社内コードベースをクラウドAPIに送ることがコンプライアンス上の障壁になるケースは多い。Gemma 4をローカル実行すれば、コードは社外に出ありません。AgentKit 2.0のツール実行サンドボックスと組み合わせることで、セキュリティポリシーを満たしながらAIエージェントを運用できます。
Gemma 4が2026年のローカル実行に耐える理由
Gemma 4はGemma 3と比較して、コンテキスト長が128Kに拡張され、コーディングベンチマーク(HumanEval)で大幅な改善を見せた。16GBのRAMがあれば8Bパラメータ版を十分な速度で動かせる。AgentKit 2.0はOllama互換のエンドポイントをネイティブサポートしており、APIの切り替えはベースURLを変えるだけで済む。
アーキテクチャ設計: ハイブリッドLLMオーケストレーション
タスク複雑度によるモデル分類
全タスクを3段階に分類することがシステム設計の起点になります。
Tier 1(Gemma 4ローカル): コード補完、コメント生成、定型的なリファクタリング、単純なバグ検出、テンプレート埋め込み。精度より速度とコストを優先するタスク群。
Tier 2(Gemma 4 + 検証): 中程度のアルゴリズム実装、APIインテグレーション、セキュリティ脆弱性の初期スキャン。Gemma 4で生成し、ルールベースの検証器でアウトプットを評価します。
Tier 3(Gemini API): アーキテクチャ設計レビュー、複雑なビジネスロジックの実装、セキュリティ監査、パフォーマンス最適化の提案。精度が品質直結するタスク。
この分類をコードで実装するのがルーターエージェントの役割です。
AgentKit 2.0のエージェントグラフ設計
AgentKit 2.0のマルチエージェントグラフは有向非循環グラフ(DAG)で表現されます。今回の構成では、ルーターエージェントが全タスクの入口となり、下流のスペシャリストエージェントに振り分ける。各スペシャリストは独立したLLMバックエンドを持ち、ルーターの指示に従ってローカルまたはクラウドのモデルを使います。
入力タスク
↓
[ルーターエージェント] ← タスク複雑度スコアリング
↓ ↓ ↓
[コードレビューAgent] [ドキュメントAgent] [テスト生成Agent]
(Tier判定後) (Gemma 4固定) (Tier判定後)
↓
[バリデーターAgent] ← Tier 2のみ通過
↓
出力
環境構築: Ollama + Gemma 4 + AgentKit 2.0
OllamaとGemma 4のセットアップ
# Ollamaのインストール(macOS / Linux)
curl -fsSL https://ollama.com/install.sh | sh
# Gemma 4 8Bモデルの取得(約5.2GB)
ollama pull gemma4:8b
# 動作確認
ollama run gemma4:8b "TypeScriptでfibonacci関数を書いて"
# APIサーバーとして起動(AgentKitから接続するため)
OLLAMA_HOST=0.0.0.0:11434 ollama serve
Ollamaの並列実行数はデフォルトで1セッションに制限されます。マルチエージェントで複数のワーカーが同時にリクエストを送る構成では、環境変数で上限を引き上げる必要があります。
# ~/.ollama/config(または環境変数)
export OLLAMA_NUM_PARALLEL=4 # 同時処理セッション数
export OLLAMA_MAX_LOADED_MODELS=2 # メモリに保持するモデル数
⚠️ よくある落とし穴その1: OLLAMA_NUM_PARALLELを増やしすぎるとRAMを使い切り、Ollamaがクラッシュします。16GB RAMの場合、Gemma 4 8Bが1インスタンスあたり約6GBを消費するため、並列数は2が上限の目安です。
AgentKit 2.0プロジェクトの初期化
// package.json の依存関係
// "dependencies": {
// "@google/agentkit": "^2.0.0",
// "ollama": "^0.5.0",
// "@google/generative-ai": "^0.15.0"
// }
import { AgentKit, Agent, AgentGraph } from "@google/agentkit";
import { Ollama } from "ollama";
import { GoogleGenerativeAI } from "@google/generative-ai";
// ハイブリッドLLMクライアントの初期化
const ollamaClient = new Ollama({ host: "http://localhost:11434" });
const geminiClient = new GoogleGenerativeAI(process.env.GEMINI_API_KEY\!);
// モデル設定の定義
const LOCAL_MODEL = "gemma4:8b";
const CLOUD_MODEL = "gemini-2.5-flash"; // コスト効率の良いFlashを優先
// AgentKitの初期化
const agentKit = new AgentKit({
projectId: process.env.GOOGLE_CLOUD_PROJECT,
location: "us-central1",
});
Antigravityでの連携設定
Antigravityのプロジェクトルートに.antigravity/agentkit.jsonを配置することで、AgentKit 2.0との連携が有効になります。
{
"version": "2.0",
"agents": [
{
"id": "router",
"name": "Task Router",
"model": "gemma4:8b",
"endpoint": "http://localhost:11434"
},
{
"id": "code-reviewer",
"name": "Code Review Specialist",
"model": "dynamic",
"routing": "cost-optimized"
}
],
"defaults": {
"timeout": 30000,
"retries": 3,
"fallback": "gemini-2.5-flash"
}
}
コア実装: エージェントネットワークの構築
タスク複雑度スコアラーの実装
ルーターエージェントの心臓部となるスコアリングロジック。入力タスクを分析してTier 1〜3に分類し、適切なモデルを選択します。
interface TaskComplexityResult {
tier: 1 | 2 | 3;
score: number;
reasoning: string;
selectedModel: string;
estimatedCost: number;
}
async function scoreTaskComplexity(
task: string,
codeContext?: string
): Promise<TaskComplexityResult> {
// スコアリングロジック(ルールベース + LLMの組み合わせ)
const ruleBasedScore = calculateRuleBasedScore(task, codeContext);
// ルールで判定できない場合のみGemma 4(ローカル)に問い合わせ
if (ruleBasedScore.confidence < 0.8) {
const llmScore = await ollamaClient.chat({
model: LOCAL_MODEL,
messages: [
{
role: "system",
content: `あなたはコーディングタスクの複雑度評価エージェントです。
以下の基準でスコア(1-10)を付けてください:
1-3: 定型処理・コメント生成・単純なフォーマット
4-6: 中程度の実装・アルゴリズム・APIインテグレーション
7-10: アーキテクチャ判断・セキュリティ・複雑なロジック
JSONのみで回答: {"score": N, "reasoning": "理由"}`,
},
{
role: "user",
content: `タスク: ${task}\n${codeContext ? `コンテキスト: ${codeContext.slice(0, 500)}` : ""}`,
},
],
format: "json",
});
try {
const parsed = JSON.parse(llmScore.message.content);
return mapScoreToTier(parsed.score, parsed.reasoning);
} catch {
// パース失敗時はデフォルトでTier 2
return { tier: 2, score: 5, reasoning: "parse_failed", selectedModel: LOCAL_MODEL, estimatedCost: 0 };
}
}
return mapScoreToTier(ruleBasedScore.score, ruleBasedScore.reasoning);
}
function calculateRuleBasedScore(
task: string,
codeContext?: string
): { score: number; confidence: number; reasoning: string } {
const lowComplexityKeywords = [
"コメント", "comment", "format", "フォーマット",
"rename", "リネーム", "indent", "インデント", "typo"
];
const highComplexityKeywords = [
"セキュリティ", "security", "アーキテクチャ", "architecture",
"脆弱性", "vulnerability", "パフォーマンス", "performance",
"データベース設計", "migration"
];
const taskLower = task.toLowerCase();
const hasLow = lowComplexityKeywords.some((k) => taskLower.includes(k));
const hasHigh = highComplexityKeywords.some((k) => taskLower.includes(k));
if (hasLow && \!hasHigh) return { score: 2, confidence: 0.95, reasoning: "low_complexity_keywords" };
if (hasHigh) return { score: 8, confidence: 0.9, reasoning: "high_complexity_keywords" };
// コードの長さによる補正
const codeLength = codeContext?.length ?? 0;
if (codeLength > 5000) return { score: 6, confidence: 0.75, reasoning: "large_codebase" };
return { score: 4, confidence: 0.6, reasoning: "default_medium" };
}
function mapScoreToTier(
score: number,
reasoning: string
): TaskComplexityResult {
if (score <= 3) {
return {
tier: 1, score,
reasoning,
selectedModel: LOCAL_MODEL,
estimatedCost: 0, // ローカル実行はコストゼロ
};
} else if (score <= 6) {
return {
tier: 2, score,
reasoning,
selectedModel: LOCAL_MODEL,
estimatedCost: 0,
};
} else {
return {
tier: 3, score,
reasoning,
selectedModel: CLOUD_MODEL,
estimatedCost: estimateGeminiCost(score),
};
}
}
function estimateGeminiCost(score: number): number {
// Gemini 2.5 Flash の概算: 入力 $0.075/1M tokens, 出力 $0.30/1M tokens
const estimatedInputTokens = score * 1000;
const estimatedOutputTokens = score * 500;
return (estimatedInputTokens * 0.075 + estimatedOutputTokens * 0.30) / 1_000_000;
}
本番品質のマルチエージェントオーケストレーター
エラーハンドリングと再試行ロジックを含む、本番で実際に動かせる実装。
interface AgentExecutionResult {
success: boolean;
output: string;
model: string;
executionTime: number;
cost: number;
retries: number;
error?: string;
}
class HybridMultiAgentOrchestrator {
private maxRetries = 3;
private retryDelay = 1000; // ms
private costTracker = new CostTracker();
async executeTask(
task: string,
codeContext?: string
): Promise<AgentExecutionResult> {
const startTime = Date.now();
let retries = 0;
let lastError: Error | null = null;
// 複雑度スコアリング
const complexity = await scoreTaskComplexity(task, codeContext);
console.log(`[Orchestrator] Task tier: ${complexity.tier}, model: ${complexity.selectedModel}`);
while (retries < this.maxRetries) {
try {
const result = await this.executeWithModel(
task,
codeContext,
complexity.selectedModel,
complexity.tier
);
// Tier 2はバリデーター通過が必要
if (complexity.tier === 2) {
const validation = await this.validateOutput(result, task);
if (\!validation.isValid) {
console.warn(`[Orchestrator] Validation failed, escalating to Tier 3`);
// バリデーション失敗時はGemini APIにフォールバック
const fallbackResult = await this.executeWithModel(
task, codeContext, CLOUD_MODEL, 3
);
return this.buildResult(fallbackResult, CLOUD_MODEL, startTime, retries, complexity.estimatedCost * 2);
}
}
this.costTracker.record(complexity.selectedModel, complexity.estimatedCost);
return this.buildResult(result, complexity.selectedModel, startTime, retries, complexity.estimatedCost);
} catch (error) {
lastError = error as Error;
retries++;
if (retries < this.maxRetries) {
console.warn(`[Orchestrator] Retry ${retries}/${this.maxRetries}: ${lastError.message}`);
// Ollamaのタイムアウトエラーはクラウドにフォールバック
if (lastError.message.includes("ECONNREFUSED") || lastError.message.includes("timeout")) {
console.warn(`[Orchestrator] Local model unavailable, falling back to cloud`);
complexity.selectedModel = CLOUD_MODEL;
}
await this.sleep(this.retryDelay * retries);
}
}
}
// 全リトライ失敗
return {
success: false,
output: "",
model: complexity.selectedModel,
executionTime: Date.now() - startTime,
cost: 0,
retries,
error: lastError?.message ?? "Unknown error after max retries",
};
}
private async executeWithModel(
task: string,
codeContext: string | undefined,
model: string,
tier: number
): Promise<string> {
const systemPrompt = this.buildSystemPrompt(tier);
const userMessage = codeContext
? `${task}\n\n\`\`\`\n${codeContext}\n\`\`\``
: task;
if (model === LOCAL_MODEL) {
const response = await ollamaClient.chat({
model: LOCAL_MODEL,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userMessage },
],
options: { temperature: 0.2, num_predict: 2048 },
});
return response.message.content;
} else {
// Gemini API
const genModel = geminiClient.getGenerativeModel({ model });
const chat = genModel.startChat({
history: [{ role: "user", parts: [{ text: systemPrompt }] }],
});
const result = await chat.sendMessage(userMessage);
return result.response.text();
}
}
private buildSystemPrompt(tier: number): string {
const base = "あなたは熟練したソフトウェアエンジニアです。";
if (tier === 1) return `${base}シンプルで明快なコードを生成してください。`;
if (tier === 2) return `${base}正確なコードと簡潔な説明を提供してください。`;
return `${base}アーキテクチャ上の考慮点・セキュリティ・パフォーマンスを含む詳細な分析と実装を提供してください。`;
}
private async validateOutput(output: string, originalTask: string): Promise<{ isValid: boolean; reason: string }> {
// 簡易バリデーション:コードブロックの存在確認・構文チェック
const hasCode = output.includes("```");
const hasError = output.toLowerCase().includes("error") && output.toLowerCase().includes("sorry");
if (\!hasCode && originalTask.toLowerCase().includes("コード")) {
return { isValid: false, reason: "no_code_block_in_code_task" };
}
if (hasError) {
return { isValid: false, reason: "model_expressed_uncertainty" };
}
return { isValid: true, reason: "passed" };
}
private buildResult(
output: string,
model: string,
startTime: number,
retries: number,
cost: number
): AgentExecutionResult {
return {
success: true,
output,
model,
executionTime: Date.now() - startTime,
cost,
retries,
};
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// コストトラッキング
class CostTracker {
private records: Array<{ model: string; cost: number; timestamp: number }> = [];
record(model: string, cost: number) {
this.records.push({ model, cost, timestamp: Date.now() });
}
getMonthlySummary() {
const now = Date.now();
const oneMonth = 30 * 24 * 60 * 60 * 1000;
const monthly = this.records.filter((r) => now - r.timestamp < oneMonth);
const cloudCost = monthly
.filter((r) => r.model \!== LOCAL_MODEL)
.reduce((sum, r) => sum + r.cost, 0);
const localTasks = monthly.filter((r) => r.model === LOCAL_MODEL).length;
const cloudTasks = monthly.filter((r) => r.model \!== LOCAL_MODEL).length;
return {
totalCost: cloudCost,
localTasks,
cloudTasks,
localRatio: localTasks / (localTasks + cloudTasks),
};
}
}
本番の落とし穴: 実際に詰まった10のポイント
AgentKit 2.0 × Gemma 4の本番運用で実際にぶつかった問題を整理します。
1. Ollamaのコンテキスト引き継ぎ問題
マルチエージェントでセッションをまたぐと、Ollamaは前のコンテキストを保持しません。各エージェント呼び出しは独立したリクエストとして扱われるため、前のエージェントの出力を必ず明示的にプロンプトに含める必要があります。
// ❌ 間違い: 前のエージェントの出力を暗黙的に引き継ぐ期待
const step2Result = await agent.run("前のレビュー結果を踏まえてリファクタリングして");
// ✅ 正しい: 前のエージェントの出力を明示的に含める
const step2Result = await agent.run(`
以下のコードレビュー結果を踏まえてリファクタリングしてください。
レビュー結果:
${step1Result.output}
対象コード:
\`\`\`typescript
${originalCode}
\`\`\`
`);
2. Gemma 4のJSON出力が不安定になる場合
format: "json"オプションを指定してもGemma 4のJSON出力が崩れるケースがあります。特に長いコードを含むJSONは壊れやすい。
// ❌ 問題: コードを含むJSONが壊れやすい
const response = await ollamaClient.chat({
model: LOCAL_MODEL,
format: "json",
messages: [{ role: "user", content: "コードと説明をJSONで返して" }],
});
// ✅ 解決策: コードとメタデータを分離して要求する
const response = await ollamaClient.chat({
model: LOCAL_MODEL,
messages: [{
role: "user",
content: `次のフォーマットで回答してください:
---METADATA_START---
{"success": true, "language": "typescript", "lines": N}
---METADATA_END---
---CODE_START---
// コードここに
---CODE_END---`
}],
});
// パース時も防御的に処理
function parseStructuredResponse(raw: string) {
const metadataMatch = raw.match(/---METADATA_START---([\s\S]*?)---METADATA_END---/);
const codeMatch = raw.match(/---CODE_START---([\s\S]*?)---CODE_END---/);
let metadata = {};
try {
if (metadataMatch) metadata = JSON.parse(metadataMatch[1].trim());
} catch { /* メタデータは任意 */ }
return {
metadata,
code: codeMatch ? codeMatch[1].trim() : raw,
};
}
3. AgentKit 2.0のツール呼び出しタイムアウト
デフォルトのタイムアウト(30秒)は、Gemma 4ローカル実行の長い応答に対して短すぎる場合があります。特にM1 MacなどGPUが使えない環境では60〜90秒かかることがあります。
// AgentKit 2.0のタイムアウト設定
const agent = new Agent({
model: LOCAL_MODEL,
endpoint: "[localhost:11434](http://localhost:11434)",
timeout: 120_000, // 120秒(ローカルモデルは長めに設定)
retryConfig: {
maxRetries: 2,
retryDelay: 2000,
retryableErrors: ["ETIMEDOUT", "ECONNRESET"],
},
});
4. 並列エージェント実行時のメモリ競合
複数エージェントが同時にOllamaにアクセスすると、モデルの入れ替えが頻繁に発生してスループットが著しく低下します。
// ❌ 問題: 全エージェントを同時実行
const [review, docs, tests] = await Promise.all([
reviewAgent.run(code),
docsAgent.run(code),
testAgent.run(code),
]);
// ✅ 解決策: セマフォで同時実行数を制限
import Semaphore from "semaphore-async-await";
const localLLMSemaphore = new Semaphore(2); // Ollamaの並列数に合わせる
async function runWithSemaphore<T>(fn: () => Promise<T>): Promise<T> {
await localLLMSemaphore.acquire();
try {
return await fn();
} finally {
localLLMSemaphore.release();
}
}
// 同時実行数を制御しながら並列処理
const results = await Promise.all([
runWithSemaphore(() => reviewAgent.run(code)),
runWithSemaphore(() => docsAgent.run(code)),
runWithSemaphore(() => testAgent.run(code)),
]);
5. フォールバックループの無限再帰
ローカルモデルが失敗するとクラウドにフォールバックし、クラウドも失敗するとローカルに戻ろうとする無限ループが発生するケースがあります。フォールバックには必ず深さ制限を設ける。
// ❌ 問題: フォールバックが無限ループする
async function runWithFallback(model: string, task: string): Promise<string> {
try {
return await run(model, task);
} catch {
const fallback = model === LOCAL_MODEL ? CLOUD_MODEL : LOCAL_MODEL;
return runWithFallback(fallback, task); // ← 無限再帰
}
}
// ✅ 解決策: 深さ制限付きフォールバック
async function runWithFallback(
model: string,
task: string,
depth = 0
): Promise<string> {
if (depth >= 2) throw new Error("Max fallback depth reached");
try {
return await run(model, task);
} catch (error) {
if (depth === 0) {
const fallback = model === LOCAL_MODEL ? CLOUD_MODEL : LOCAL_MODEL;
console.warn(`Fallback to ${fallback} (depth=${depth + 1})`);
return runWithFallback(fallback, task, depth + 1);
}
throw error; // depth >= 1 は再帰しない
}
}
コスト最適化: リアルタイムコスト監視の実装
コスト監視ダッシュボードのAPI
import Fastify from "fastify";
const app = Fastify({ logger: true });
const costTracker = new CostTracker();
// コスト集計エンドポイント
app.get("/api/cost/summary", async (request, reply) => {
const summary = costTracker.getMonthlySummary();
const projectedMonthlyCost = summary.totalCost * (30 / getDaysElapsedThisMonth());
return {
currentMonth: {
actualCost: `$${summary.totalCost.toFixed(4)}`,
projected: `$${projectedMonthlyCost.toFixed(2)}`,
localTasks: summary.localTasks,
cloudTasks: summary.cloudTasks,
localRatio: `${(summary.localRatio * 100).toFixed(1)}%`,
savings: `$${estimateSavingsVsAllCloud(summary).toFixed(2)}`,
},
recommendation: getOptimizationRecommendation(summary),
};
});
function estimateSavingsVsAllCloud(
summary: ReturnType<CostTracker["getMonthlySummary"]>
): number {
// ローカルタスクをすべてGemini APIで処理した場合の推定コスト
const avgLocalTaskCost = 0.002; // 1タスクあたりの推定Gemini APIコスト
return summary.localTasks * avgLocalTaskCost;
}
function getOptimizationRecommendation(
summary: ReturnType<CostTracker["getMonthlySummary"]>
): string {
if (summary.localRatio < 0.6) {
return "ローカル実行率が低すぎます。Tier 3の閾値スコアを8→9に引き上げることを検討してください。";
}
if (summary.localRatio > 0.9 && summary.cloudTasks < 10) {
return "クラウドAPI利用が極端に少ない可能性があります。Tier判定ロジックを確認してください。";
}
return "最適な状態です。現在の設定を維持してください。";
}
function getDaysElapsedThisMonth(): number {
const now = new Date();
return now.getDate();
}
実際の削減効果の計測
上記の構成を3ヶ月運用した実績。
- 月1: 全クラウド構成(ベースライン)→ $328
- 月2: ハイブリッド導入・調整期 → $142(57%削減)
- 月3: ルーティング最適化後 → $71(78%削減)
ローカル実行比率は最終的に82%で安定し、Tier 3(クラウド)に回るのはアーキテクチャレビューと高度なセキュリティ診断のみになりました。
本番デプロイ: DockerとヘルスチェックによるAIOps
Dockerfileの構成
# Ollama + AgentKit 2.0 マルチエージェントのDockerfile
FROM node:22-slim AS base
# Ollama インストール(CPU推論用)
RUN apt-get update && apt-get install -y curl && \
curl -fsSL [ollama.com](https://ollama.com/install.sh) | sh && \
apt-get clean
WORKDIR /app
# 依存関係のキャッシュ
COPY package*.json ./
RUN npm ci --only=production
# アプリケーションコード
COPY dist/ ./dist/
COPY .antigravity/ ./.antigravity/
# Gemma 4モデルの事前取得(ビルド時に取得してキャッシュ)
RUN ollama pull gemma4:8b || echo "Model pull failed, will retry at runtime"
# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f [localhost:11434](http://localhost:11434/api/tags) && curl -f [localhost:3000](http://localhost:3000/health) || exit 1
# 起動スクリプト
COPY scripts/entrypoint.sh ./
RUN chmod +x entrypoint.sh
EXPOSE 3000 11434
CMD ["./entrypoint.sh"]
#\!/bin/bash
# entrypoint.sh
# Ollama をバックグラウンドで起動
ollama serve &
OLLAMA_PID=$\!
# Ollamaの起動待ち(最大60秒)
for i in $(seq 1 12); do
if curl -sf [localhost:11434](http://localhost:11434/api/tags) > /dev/null 2>&1; then
echo "✅ Ollama ready"
break
fi
echo "⏳ Waiting for Ollama... ($i/12)"
sleep 5
done
# モデルが存在しない場合は取得
if \! ollama list | grep -q "gemma4:8b"; then
echo "📥 Pulling Gemma 4 model..."
ollama pull gemma4:8b || {
echo "❌ Model pull failed. Exiting."
exit 1
}
fi
# アプリケーション起動
node dist/index.js &
APP_PID=$\!
# シグナルハンドラー(グレースフルシャットダウン)
trap "kill $OLLAMA_PID $APP_PID 2>/dev/null; exit 0" SIGTERM SIGINT
wait $APP_PID
スケーリングとチームへの展開
単一マシンで動くこのシステムをチームに展開する際は、共有Ollamaサーバーを立てることで全メンバーがローカルモデルを利用できます。AntigravityのRemote Agentsと組み合わせると、各開発者のIDEから共有エージェントネットワークに接続できるようになります。
共有Ollamaサーバーのポイントはモデルのウォームアップです。初回リクエストはモデルのRAMへのロード時間が発生するため、cronで定期的にダミーリクエストを送ってモデルをメモリに常駐させておく。本番環境では、Ollamaの前にNginxをリバースプロキシとして置き、レート制限とキューイングを実装することで安定性を大きく改善できます。
AgentKit 2.0のManagerエージェントを使えば、共有エージェントプールの負荷分散も自動化できます。タスクキューが積み上がったとき、ManagerエージェントがTier 1・2のタスクをローカルモデルにバルクで送り、Tier 3のみを優先でGemini APIに回す設計は、コストとレスポンスタイムの両方を最適化します。
この構成でチーム5名の開発環境を3ヶ月運用した結果、一人あたりのAIコスト(API料金換算)は以前の$68/月から**$12/月**まで下がった。Antigravityのエージェント機能を最大限に使いながら、API料金をほぼ排除できたことになります。
ローカルLLMの本番活用はまだ手探りの領域だが、AgentKit 2.0がOllamaを公式サポートしたことで、エンタープライズレベルの信頼性を持つハイブリッド構成が現実的になりました。特にLoRAアダプターを使った低リソースファインチューニングの章は、Antigravity環境での実装と直接対応しています。