ローカルで動かしたエージェントが、朝いちばんのリクエストだけ 30 秒近く沈黙する。ログにはエラーもタイムアウトも出ていない。原因は Ollama が既定でアイドル時にモデルをメモリから降ろしていたことでした。推論そのものではなく、その周りの運用が最初に躓く場所だったわけです。
Gemma 4 は、オープンソースの大規模言語モデルとして手元のマシンで扱える現実的な選択肢になりました。コーディングと論理的推論の性能が上がり、9B クラスでも実務に耐える応答が返ってきます。Antigravity 側から見ると、これは「API キーの残高を気にせず何度でも叩ける推論エンジン」が手元に増えたことを意味します。
ここではモデルの選び方から、Ollama でのセットアップ、AgentKit 2.0 を使ったツール呼び出しとメモリ、マルチエージェント化、そして常駐させてから壊れる箇所の手当てまでを順に扱います。単発で動かすところまでは公式ドキュメントでも辿り着けますので、本稿は「動いたあと、動かし続ける」側に紙幅を割いています。
Gemma 4 の全体像:バリアントとAntigravityとの相性
Gemma 4 は、Google DeepMindが公開したオープンソースの大規模言語モデルです。既存のLLMと大きく異なる点は、バリアント戦略 です。
Gemma 4 には3つのメインバリアントが存在します。
Gemma 4 2B
パラメータ数:20億
推奨メモリ:4GB(量子化時)〜 6GB(フルプレシジョン)
推論速度:極めて高速(トークン/秒が最大)
用途:軽量なエッジデバイス、リアルタイム応答が必須の用途、プロトタイピング
Gemma 4 9B
パラメータ数:90億
推奨メモリ:8GB(量子化時)〜 18GB(フルプレシジョン)
推論速度:高速かつバランスの取れた性能
用途:汎用エージェント、ほとんどのプロダクション環境の標準
Gemma 4 27B
パラメータ数:270億
推奨メモリ:16GB(量子化時)〜 54GB(フルプレシジョン)
推論速度:低速だが高精度
用途:複雑な推論が必須の専門分野エージェント、高精度が課金対象の場合
Antigravityとの相性
AntigravityはローカルLLM向けに最適化されており、特にこれら複数バリアントの管理を効率的に行えます。
ローカル実行 :APIキー不要で、クラウド費用ゼロ
推論カスタマイズ :温度・トークン長・ビーム探索パラメータを細粒度で制御可能
エージェントフレームワーク :AgentKit 2.0 により、Tool Use(外部ツール呼び出し)とMemory(会話メモリ)が統合
マルチモデル対応 :Ollama経由で、複数のGemma 4バリアントを同時実行可能
Antigravityは Ollama を通じて Gemma 4 と通信するため、バージョン管理やモデル切り替えが非常に柔軟です。
Ollama で Gemma 4 をセットアップする
実装に入る前に、ローカル環境にGemma 4をインストールします。
Ollama のインストール確認
まず、Ollama がインストール済みで、起動できるか確認します。
ollama --version
インストール済みなら、バージョン情報が表示されます。未インストールの場合は Ollama公式サイト からダウンロード・インストールしてください。
Gemma 4 モデルのプル
Gemmaシリーズは Ollama のレジストリに登録されており、以下の ollama pull コマンドでダウンロード可能です。
# Gemma 4 9B のプル(推奨:バランスの取れたサイズ)
ollama pull gemma4:9b
# または、より軽量な 2B
ollama pull gemma4:2b
# または、より高精度な 27B
ollama pull gemma4:27b
初回ダウンロード時は、モデルサイズに応じて数分〜数十分かかります。例えば、Gemma 4 9B は約 9GB のデータをダウンロードします。
量子化バリアントの選択
デフォルトでプルされるのは量子化版(通常 q4_0 または q5_K_M)です。メモリに余裕があれば、より高精度な量子化レベルを明示的に指定できます。
# より高精度な量子化版(約 18GB、8GB RAM推奨)
ollama pull gemma4:9b-q5_K_M
# フルプレシジョン版(約 34GB、24GB RAM推奨 — ほぼ推奨しない)
ollama pull gemma4:9b-fp16
量子化レベルと精度・メモリのトレードオフは以下の通りです。
量子化タイプ ファイルサイズ メモリ 精度 推奨用途
q2_K ~5GB 6GB 低 超軽量デバイス
q3_K ~6GB 8GB 低〜中 エッジAI
q4_0 ~9GB 12GB 中 標準プロダクション
q5_K_M ~11GB 14GB 中〜高 高精度エージェント
fp16 ~18GB 28GB 最高 リサーチ・開発用
Ollama サーバーの起動と確認
モデルをプルしたら、Ollama サーバーを起動します。
ollama serve
別のターミナルで以下を実行し、モデルが認識されているか確認:
ollama list
出力例:
NAME ID SIZE MODIFIED
gemma4:9b a1b2c3d4e5f6 9.5 GB 2 minutes ago
Ollama API の動作確認
curl を使ってOllamaのAPI が応答しているか確認:
curl -X POST http://localhost:11434/api/generate \
-H "Content-Type: application/json" \
-d '{
"model": "gemma4:9b",
"prompt": "What is the capital of Japan?",
"stream": false
}'
レスポンス例:
{
"model" : "gemma4:9b" ,
"created_at" : "2026-04-09T11:30:00Z" ,
"response" : "The capital of Japan is Tokyo." ,
"done" : true
}
ここまでで、Ollama 経由でGemma 4 を呼び出す基盤ができました。
Antigravity から Gemma 4 に接続する
次に、Antigravity プロジェクト内でGemma 4 への接続を設定します。
環境変数の設定
プロジェクトルートの .env.local ファイル(未作成なら新規作成)に以下を記述:
# LLM プロバイダー設定
NEXT_PUBLIC_LLM_PROVIDER = ollama
NEXT_PUBLIC_OLLAMA_BASE_URL = http://localhost:11434
NEXT_PUBLIC_OLLAMA_MODEL = gemma4:9b
# オプション:推論パラメータ
NEXT_PUBLIC_OLLAMA_TEMPERATURE = 0.7
NEXT_PUBLIC_OLLAMA_TOP_P = 0.9
NEXT_PUBLIC_OLLAMA_TOP_K = 40
NEXT_PUBLIC_OLLAMA_NUM_PREDICT = 2048
各パラメータの説明:
TEMPERATURE:出力の多様性(0.0 = 確定的、1.0 = 創造的)
TOP_P:上位確率の累積値でサンプリング(0.9 = 上位90%の確率質量から選択)
TOP_K:上位K個のトークンからのみサンプリング
NUM_PREDICT:最大生成トークン数
Antigravity 設定ファイルの作成
より詳細な設定が必要な場合、プロジェクト内に antigravity.config.ts を作成:
// antigravity.config.ts
export const llmConfig = {
provider: "ollama" ,
baseUrl: process.env. NEXT_PUBLIC_OLLAMA_BASE_URL || "http://localhost:11434" ,
model: process.env. NEXT_PUBLIC_OLLAMA_MODEL || "gemma4:9b" ,
temperature: parseFloat (process.env. NEXT_PUBLIC_OLLAMA_TEMPERATURE || "0.7" ),
topP: parseFloat (process.env. NEXT_PUBLIC_OLLAMA_TOP_P || "0.9" ),
topK: parseInt (process.env. NEXT_PUBLIC_OLLAMA_TOP_K || "40" , 10 ),
numPredict: parseInt (process.env. NEXT_PUBLIC_OLLAMA_NUM_PREDICT || "2048" , 10 ),
};
export const agentConfig = {
maxIterations: 10 ,
verbose: true ,
timeout: 30000 , // ミリ秒
};
開発サーバーの再起動
設定ファイルを編集したら、開発サーバーを再起動します。
# 既存サーバーを停止(Ctrl+C)
# 新規起動
npm run dev
接続テスト
ブラウザコンソール(F12)またはサーバーログで、Ollama への接続が成功しているか確認します。
// コンソールに表示されるログ例
[Antigravity] Connected to Ollama at http : //localhost:11434
[Antigravity] Model loaded : gemma4 :9b
基本エージェント:Antigravity + Gemma 4 の実装
ここからが実装です。最初は、シンプルな問い合わせエージェント から始めましょう。
エージェントの骨組み
// src/agents/BasicGemma4Agent.ts
import { OllamaClient } from "@ollama/sdk" ;
const client = new OllamaClient ({
baseUrl: "http://localhost:11434" ,
});
interface AgentState {
input : string ;
history : Array <{ role : string ; content : string }>;
}
export async function basicAgent ( input : string ) : Promise < string > {
const state : AgentState = {
input,
history: [],
};
// 会話メモリに入力を追加
state.history. push ({
role: "user" ,
content: input,
});
// Gemma 4 に送信
const response = await client. generate ({
model: "gemma4:9b" ,
prompt: buildPrompt (state.history),
temperature: 0.7 ,
top_p: 0.9 ,
top_k: 40 ,
num_predict: 2048 ,
});
// レスポンスをメモリに記録
state.history. push ({
role: "assistant" ,
content: response.response,
});
return response.response;
}
function buildPrompt ( history : Array <{ role : string ; content : string }>) : string {
let prompt = "You are a helpful AI assistant. \n\n " ;
for ( const msg of history) {
if (msg.role === "user" ) {
prompt += `User: ${ msg . content } \n ` ;
} else {
prompt += `Assistant: ${ msg . content } \n ` ;
}
}
prompt += "Assistant:" ;
return prompt;
}
エージェントの呼び出し
// 使用例
const result = await basicAgent ( "What are the main benefits of local LLMs?" );
console. log (result);
出力例:
Local LLMs offer several key benefits: (1) Privacy - your data never leaves your machine, (2) Cost efficiency - no per-token billing, (3) Latency - no network roundtrips, and (4) Customization - fine-tune models for your specific use case.
AgentKit 2.0 との統合:Tool Use と Memory
実際のプロダクション環境では、LLM だけでなく外部ツール (web search、データベース照会、ファイル操作)や持続的なメモリ が不可欠です。ここで AgentKit 2.0 の出番です。
AgentKit 2.0 とは
AgentKit 2.0 は、Antigravity の付属エージェントフレームワークで、以下の機能を統合します:
Tool Registry :利用可能なツール(関数)を宣言
Tool Use Loop :LLMが「このツールを使おう」と判断した際の自動実行
Memory Management :会話履歴・状態・スクラッチパッドの管理
Error Handling :ツール実行エラー時の自動リトライ
ツールの定義
まず、エージェントが使用可能な外部ツールを定義します。
// src/agents/tools.ts
import { Tool } from "@antigravity/agentkit" ;
export const searchTool : Tool = {
name: "web_search" ,
description: "Search the web for current information" ,
inputSchema: {
type: "object" ,
properties: {
query: {
type: "string" ,
description: "Search query" ,
},
},
required: [ "query" ],
},
execute : async ( params : { query : string }) => {
// 実装:外部API(Serper、SerpAPI など)呼び出し
const response = await fetch ( `https://api.example.com/search` , {
method: "POST" ,
headers: { "Authorization" : `Bearer ${ process . env . SEARCH_API_KEY }` },
body: JSON . stringify ({ q: params.query }),
});
return await response. json ();
},
};
export const databaseQueryTool : Tool = {
name: "query_database" ,
description: "Query the application database" ,
inputSchema: {
type: "object" ,
properties: {
sql: {
type: "string" ,
description: "SQL query (SELECT only)" ,
},
},
required: [ "sql" ],
},
execute : async ( params : { sql : string }) => {
// 実装:DB接続、クエリ実行
const db = getDBConnection ();
const result = await db. query (params.sql);
return result;
},
};
export const calculateTool : Tool = {
name: "calculate" ,
description: "Perform mathematical calculations" ,
inputSchema: {
type: "object" ,
properties: {
expression: {
type: "string" ,
description: "Math expression (e.g., '2 + 2 * 3')" ,
},
},
required: [ "expression" ],
},
execute : async ( params : { expression : string }) => {
// 実装:計算エンジン(evalは安全性考慮が必須)
const result = safeEval (params.expression);
return { result };
},
};
AgentKit ルーパーの実装
// src/agents/GemmaAgentWithTools.ts
import { Agent, AgentState, Tool } from "@antigravity/agentkit" ;
import { OllamaClient } from "@ollama/sdk" ;
import { searchTool, databaseQueryTool, calculateTool } from "./tools" ;
export class GemmaProductionAgent {
private agent : Agent ;
private ollama : OllamaClient ;
constructor () {
this .ollama = new OllamaClient ({
baseUrl: "http://localhost:11434" ,
});
this .agent = new Agent ({
model: "gemma4:9b" ,
llmClient: this .ollama,
tools: [searchTool, databaseQueryTool, calculateTool],
maxIterations: 10 ,
verbose: true ,
});
}
async run ( input : string ) : Promise < string > {
const state : AgentState = {
input,
messages: [],
toolCalls: [],
scratchpad: "" ,
};
// ループ:LLMが「終了」と言うまで繰り返す
for ( let i = 0 ; i < this .agent.maxIterations; i ++ ) {
// LLMに「何をする?」と聞く
const response = await this .agent. step (state);
if (response.action === "finish" ) {
return response.thought;
}
if (response.action === "tool" ) {
// ツール実行
const toolName = response.toolName;
const toolInput = response.toolInput;
const tool = this .agent.tools. find (( t ) => t.name === toolName);
if ( ! tool) {
state.scratchpad += `Tool ${ toolName } not found. \n ` ;
continue ;
}
const toolResult = await tool. execute (toolInput);
state.messages. push ({
role: "tool" ,
toolName,
result: toolResult,
});
state.scratchpad += `Tool: ${ toolName } \n Input: ${ JSON . stringify ( toolInput ) } \n Result: ${ JSON . stringify ( toolResult ) } \n ` ;
}
}
throw new Error ( "Max iterations reached" );
}
}
使用例
const agent = new GemmaProductionAgent ();
const result = await agent. run (
"Search for the latest Gemma 4 benchmarks and tell me how it compares to Llama 3."
);
console. log (result);
エージェントが自動的に以下の流れを実行します:
ユーザーの質問を受け取る
「web_search ツールを使おう」と判断
検索クエリを実行
結果を整理して、最終回答を生成
マルチエージェント化:並列実行とスケーリング
1つのエージェント(単一の Gemma 4 モデル)では、複数の並列タスクは実行できません。ここでマルチエージェント アーキテクチャ が登場します。
Manager-Subordinate パターン
// src/agents/MultiAgentOrchestrator.ts
import { GemmaProductionAgent } from "./GemmaProductionAgent" ;
interface Task {
id : string ;
description : string ;
priority : "high" | "medium" | "low" ;
}
export class MultiAgentOrchestrator {
private agents : Map < string , GemmaProductionAgent > = new Map ();
private taskQueue : Task [] = [];
constructor ( numAgents : number = 3 ) {
// 複数のエージェントインスタンスを生成
for ( let i = 0 ; i < numAgents; i ++ ) {
this .agents. set ( `agent-${ i }` , new GemmaProductionAgent ());
}
}
async delegateTask ( task : Task ) : Promise < string > {
// タスクを優先度別にキューイング
this .taskQueue. push (task);
this .taskQueue. sort (
( a , b ) =>
({ high: 0 , medium: 1 , low: 2 }[b.priority] -
{ high: 0 , medium: 1 , low: 2 }[a.priority])
);
// 利用可能なエージェントを割り当て
const availableAgent = Array. from ( this .agents. values ())[ 0 ];
return await availableAgent. run (task.description);
}
async processBatch ( tasks : Task []) : Promise < Map < string , string >> {
const results = new Map < string , string >();
// 並列実行:すべてのタスクを同時に処理
const promises = tasks. map ( async ( task ) => {
const agentArray = Array. from ( this .agents. values ());
const agent = agentArray[Math. floor (Math. random () * agentArray. length )];
const result = await agent. run (task.description);
results. set (task.id, result);
});
await Promise . all (promises);
return results;
}
}
使用例
const orchestrator = new MultiAgentOrchestrator ( 3 ); // 3つのエージェント
const tasks : Task [] = [
{
id: "task-1" ,
description: "Analyze customer feedback from Q1 2026" ,
priority: "high" ,
},
{
id: "task-2" ,
description: "Generate code documentation for the API module" ,
priority: "medium" ,
},
{
id: "task-3" ,
description: "Summarize latest AI research papers" ,
priority: "low" ,
},
];
const results = await orchestrator. processBatch (tasks);
for ( const [ taskId , result ] of results) {
console. log ( `${ taskId }: ${ result }` );
}
パフォーマンス考慮
マルチエージェント実行時の注意点:
メモリ管理 :各エージェント instance が独立した Gemma 4 モデルをメモリにロード。メモリ容量に応じてエージェント数を決定(Gemma 4 9B × 3 = 約36GB)
CPU スレッド :複数エージェントの並列推論は CPU を集中的に使用。num_workers パラメータで CPU アフィニティ設定可能
レイテンシ :全タスク完了時間は、最も遅いタスクで決定される(Amdahl法則)
プロダクション最適化:量子化選択とパフォーマンスチューニング
本番環境でのエージェント稼働を想定した、実践的な最適化方法を紹介します。
量子化レベルの選択基準
プロダクション環境では、精度・速度・メモリのバランスが重要です。推奨設定:
レイテンシ重視 (リアルタイムチャット、推奨応答):gemma4:9b-q4_0 + temperature=0.5
精度重視 (複雑なタスク、分析):gemma4:9b-q5_K_M + temperature=0.7
汎用バランス型 (標準):gemma4:9b-q4_K_M + temperature=0.7
速度の数値は環境差が非常に大きいため、絶対値ではなく桁感として扱ってください。Apple Silicon の上位機で 9B の量子化版を回すと毎秒数十トークン、27B では十数トークンという水準に落ち着く、というのがおおよその目安です。以下は用途を判断するための相対比較として読んでいただければと思います。
モデル 速度 精度 メモリ 推奨用途
gemma4:2b-q4_0 9B の 3〜4倍 低 4GB エッジ・分類
gemma4:9b-q4_0 基準(1.0) 中 12GB 標準
gemma4:9b-q5_K_M 9B q4_0 の 0.7 倍前後 中〜高 16GB 高精度
gemma4:27b-q4_0 9B q4_0 の 0.3 倍前後 高 24GB 専門
自分の環境の実数は、次のコマンドで一度だけ測っておけば十分です。eval rate が生成速度、prompt eval rate が入力の読み込み速度にあたります。
ollama run gemma4:9b --verbose "Summarize the following in three bullet points: ..."
エージェントの設計判断に効くのは eval rate の絶対値よりも、「9B から 27B に上げたときに何倍遅くなるか」という比率のほうです。比率が分かれば、どのタスクを大きいモデルに回せるかを机上で決められます。
コンテキスト長の調整
Gemma 4 のコンテキスト長(入力可能な最大トークン数)を調整することで、メモリ使用量を削減できます。
// antigravity.config.ts
export const llmConfig = {
model: "gemma4:9b" ,
// コンテキスト長を 4096 に制限(デフォルト 8192)
contextLength: 4096 ,
// スライディングウィンドウ:古い会話を自動削除
slidingWindow: true ,
windowSize: 2048 , // 最新 2048 トークンのみ保持
};
ビーム探索パラメータ
推論品質を微調整するパラメータ:
const generateParams = {
temperature: 0.7 , // 多様性
topP: 0.95 , // nucleus sampling
topK: 50 , // 上位 K トークン
repetitionPenalty: 1.1 , // 同じトークンの繰り返し抑制
frequencyPenalty: 0.0 ,
presencePenalty: 0.0 ,
};
キャッシング戦略
頻繁に使われるプロンプトやレスポンスをキャッシュすることで、不要な推論を削減:
import NodeCache from "node-cache" ;
const cache = new NodeCache ({ stdTTL: 3600 }); // 1時間
async function cachedGenerate ( prompt : string ) : Promise < string > {
const cacheKey = createHash ( "sha256" ). update (prompt). digest ( "hex" );
// キャッシュ確認
const cached = cache. get (cacheKey);
if (cached) return cached;
// キャッシュなければ生成
const response = await ollama. generate ({
model: "gemma4:9b" ,
prompt,
});
// キャッシュに保存
cache. set (cacheKey, response.response);
return response.response;
}
常駐させてから壊れる場所:コールドスタート・並列度・中断
ここまでの構成は、手元で一度動かす分には問題なく回ります。壊れ始めるのは、エージェントを常駐させて人間が不定期に叩くようになってからです。私がローカルLLMを実務に載せるときに毎回踏む三つの箇所を、順に手当てしていきます。
その1:アイドル後の最初の一発だけ極端に遅い
Ollama はリクエストが途切れると、しばらくしてモデルをメモリから降ろします。この状態で次のリクエストが来ると、応答時間にモデルのロード時間がまるごと乗ります。9B クラスでも数秒から数十秒、27B なら体感でははっきり「固まった」と見える長さです。
厄介なのは、これがエラーとして記録されないことです。監視には「たまに遅い」としか出てきません。
対処は keep_alive の明示です。リクエストごとに指定できます。
await fetch ( `${ OLLAMA_BASE_URL }/api/generate` , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
model: "gemma4:9b" ,
prompt,
stream: false ,
// 30分間はメモリに載せたままにする(-1 で無期限、0 で即アンロード)
keep_alive: "30m" ,
}),
});
ただし常駐はメモリを占有します。作業機と共用しているなら無期限は避け、稼働時間帯に合わせた有限値にしておくほうが安全です。加えて、サービス起動時に短いプロンプトを一度だけ投げてウォームアップしておくと、最初の利用者が待たされる問題が消えます。
export async function warmUpModel ( model : string ) : Promise < void > {
try {
await fetch ( `${ OLLAMA_BASE_URL }/api/generate` , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({ model, prompt: "ok" , stream: false , keep_alive: "30m" }),
signal: AbortSignal. timeout ( 120_000 ), // 初回ロードは長い。専用に長めの猶予を取る
});
} catch {
// ウォームアップの失敗でプロセスを落とさない。次の実リクエストでロードされる
}
}
その2:並列に投げても並列に処理されていない
マルチエージェント構成で Promise.all を並べると、コード上は同時実行に見えます。しかし実際に同時処理できる本数は Ollama 側のスロット数で決まり、そこを超えた分は内部で待たされます。待たされている間にクライアント側のタイムアウトが先に来て、「原因不明の失敗が並列度に比例して増える」という現象になります。
サーバー側の上限は環境変数で決めます。
# 同時に処理するリクエスト数
OLLAMA_NUM_PARALLEL = 4
# 同時にメモリへ載せるモデル数(複数モデルを使い分ける場合のみ増やす)
OLLAMA_MAX_LOADED_MODELS = 1
クライアント側は、この本数を超えて投げないように絞ります。素朴なセマフォで十分です。
class Semaphore {
private queue : Array <() => void > = [];
private active = 0 ;
constructor ( private readonly limit : number ) {}
async run < T >( task : () => Promise < T >) : Promise < T > {
if ( this .active >= this .limit) {
await new Promise < void >(( resolve ) => this .queue. push (resolve));
}
this .active ++ ;
try {
return await task ();
} finally {
this .active -- ;
this .queue. shift ()?.();
}
}
}
// OLLAMA_NUM_PARALLEL と同じ値にそろえる
const llmGate = new Semaphore ( 4 );
export function generate ( prompt : string ) : Promise < string > {
return llmGate. run (() => callOllama (prompt));
}
数を絞ると総処理時間が伸びそうに思えますが、実際には逆になることが多いです。捌けない分をキューに積むだけなら遅くなるのは末尾のタスクだけですが、タイムアウトで落ちるとリトライで同じ推論をもう一度走らせることになるためです。
その3:ユーザーが画面を閉じても推論が回り続ける
ストリーミングで応答を返している最中にタブが閉じられても、Ollama 側の生成は止まりません。GPU は最後まで回り、そのぶん後続のリクエストが待たされます。長いプロンプトほど損失が大きくなります。
AbortController をリクエストと接続の両方に結びつけておきます。
export async function streamGenerate (
prompt : string ,
onToken : ( token : string ) => void ,
externalSignal : AbortSignal , // リクエストの切断シグナルを渡す
) : Promise < void > {
const controller = new AbortController ();
const abort = () => controller. abort ();
externalSignal. addEventListener ( "abort" , abort, { once: true });
try {
const res = await fetch ( `${ OLLAMA_BASE_URL }/api/generate` , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({ model: "gemma4:9b" , prompt, stream: true , keep_alive: "30m" }),
signal: controller.signal,
});
if ( ! res.body) throw new Error ( "no response body" );
const reader = res.body. getReader ();
const decoder = new TextDecoder ();
let buffer = "" ;
while ( true ) {
const { done , value } = await reader. read ();
if (done) break ;
buffer += decoder. decode (value, { stream: true });
// Ollama は NDJSON を返す。行が完結した分だけ処理する
const lines = buffer. split ( " \n " );
buffer = lines. pop () ?? "" ;
for ( const line of lines) {
if ( ! line. trim ()) continue ;
const chunk = JSON . parse (line) as { response ?: string ; done ?: boolean };
if (chunk.response) onToken (chunk.response);
}
}
} finally {
externalSignal. removeEventListener ( "abort" , abort);
controller. abort (); // 正常終了時もソケットを確実に閉じる
}
}
finally で必ず abort() を呼んでいるのは、例外経路だけでなく正常終了時にも接続を残さないためです。ここを省くと、長時間稼働で少しずつ滞留していきます。
リトライを足す前に確認すること
失敗が増えたときに反射的にリトライを足したくなりますが、ローカルLLMでは順番が逆です。多くの場合、原因は並列度の出しすぎか、コンテキスト長がメモリに対して大きすぎることのどちらかです。リトライは同じ負荷をもう一度かける操作なので、原因が負荷側にあると事態を悪化させます。
先にセマフォの上限を下げ、それでも落ちるならコンテキスト長を削る。リトライを入れるのはその後で、しかも副作用のない読み取り系の呼び出しに限る、という順序をおすすめします。
ユースケース別活用例
実際のプロダクション環境での Gemma 4 エージェント活用例を紹介します。
使用例1:コードレビュー自動化
const codeReviewAgent = await orchestrator. delegateTask ({
id: "code-review-1" ,
description: `
Review the following pull request code for:
1. Security vulnerabilities
2. Performance issues
3. Code style adherence
4. Test coverage
Code:
${ pullRequestCode }
` ,
priority: "high" ,
});
Gemma 4 は、セキュリティベストプラクティスとパフォーマンス最適化を同時に分析し、構造化されたレビューコメントを生成します。
使用例2:ドキュメント生成
const docGenerationAgent = await orchestrator. delegateTask ({
id: "doc-gen-1" ,
description: `
Generate comprehensive API documentation for:
- Endpoint: POST /api/agents
- Method: createAgent(config: AgentConfig)
- Include: description, parameters, return type, examples, error cases
Implementation code:
${ implementationCode }
` ,
priority: "medium" ,
});
使用例3:テスト自動生成
const testGenerationAgent = await orchestrator. delegateTask ({
id: "test-gen-1" ,
description: `
Generate unit tests for the following function:
${ functionCode }
Requirements:
- Use Jest framework
- Include happy path and edge cases
- Minimum 80% code coverage
- Add mocks for external dependencies
` ,
priority: "medium" ,
});
個人開発者の視点から(実体験メモ)
ローカルLLMを個人開発に入れるかどうかで、私が最初に見るのは性能ではなく回数です。一日に何百回も走る処理で、しかも一度失敗しても取り返しがつくもの。ここから移すと効果がはっきり出ます。逆に、月に数回しか走らない重い処理をローカルに寄せても、モデルの管理コストのほうが上回りました。
もうひとつ効いたのは、クラウドAPIを全部置き換えようとしないことでした。分類・要約・タグの下ごしらえといった前段はローカルの 9B で回し、ユーザーの目に触れる最終的な文章生成はクラウドに任せる。この分担にしてから、品質を落とさずに呼び出し回数を大きく減らせています。
見落としがちなのが、そのマシンが自分の作業機でもあるという点です。私自身、ここで一度失敗しています。ビルドを走らせながら推論を回すと、両方が遅くなります。私はバッチ的な推論を夜間のキューにまとめ、日中は単発の対話だけを許可する運用に落ち着きました。処理時間そのものより、作業が中断されないことのほうが体感の改善は大きかったです。
モデルサイズの選択では、27B に手を伸ばしたくなる場面が何度もありました。ただ、9B で通る設計に作り直したほうが結果的に速く仕上がっています。大きいモデルで押し切ろうとすると、プロンプトの曖昧さがそのまま残るためです。出力が安定しないときは、量子化レベルを上げる前にプロンプトを短く具体的に書き直すほうが、たいてい先に効きます。
もちろん、これは個人開発という規模での判断です。私自身の環境では同時利用者が実質ひとりなので、待ち行列の設計をほとんど省略できています。同時利用者が増えれば結論は変わりますし、その場合は素直にサーバー側のGPUを検討したほうが早いと考えています。
次の一歩:まず一つだけ移す
ここまでの内容を一度に全部入れる必要はありません。最初にやることは一つで十分です。
いま自分が一日でいちばん多く投げているプロンプトを一つだけ選び、それを Gemma 4 の 9B に振り替えて一週間だけ動かしてみてください。記録するのは二つ、--verbose で出る eval rate と、期待した形式で返ってこなかった割合です。この二つが分かれば、次にどのタスクを移せるか、どこで大きいモデルが要るのかが自分の環境の数字で判断できるようになります。
そのうえで、常駐させる段になったら keep_alive とセマフォの二つだけは先に入れておくことをおすすめします。この二つを後回しにすると、原因の分かりにくい遅さとして跳ね返ってきます。
ローカルで動かす価値は、速さよりも「何度でも試せること」にあると私は感じています。失敗のコストが下がると、試す回数が増えます。試す回数が増えると、プロンプトもエージェントの設計も自然と良くなっていきます。その循環に入るまでが少しだけ手間で、入ってしまえば戻れなくなる、という類のものです。
長い記事にお付き合いいただき、ありがとうございました。手元の環境で最初の eval rate が出たら、それがあなたの設計の基準値になります。