Antigravity から Ollama を呼ぶ設定
Antigravity の設定ファイル(antigravity.config.json あるいは環境変数)で、LLM エンドポイントを Ollama に向けます。
{
"llm_providers" : [
{
"name" : "local-gemma" ,
"type" : "openai_compatible" ,
"base_url" : "http://localhost:11434/v1" ,
"api_key" : "ollama" ,
"default_model" : "gemma3:12b"
},
{
"name" : "cloud-gemini" ,
"type" : "google_gemini" ,
"api_key" : "YOUR_GEMINI_API_KEY" ,
"default_model" : "gemini-2.5-pro"
}
],
"routing" : {
"agents" : {
"doc-summarizer" : "local-gemma" ,
"code-generator" : "cloud-gemini"
}
}
}
注目すべきは api_key に "ollama" というダミー値を入れている点です。Ollama は認証を要求しませんが、OpenAI 互換クライアントの多くが api_key が空のときにリクエストを送信しないため、任意の文字列で埋めておく必要があります。
エージェント別にローカル / クラウドを使い分ける
上のサンプルでは、ドキュメント要約は local-gemma、コード生成は cloud-gemini に振り分けています。これは実運用での定石で、次のような基準で切り分けます。
ローカル向き : 機密データの要約、大量ログの一次フィルタリング、コスト最優先のバッチ処理、ネット接続が不安定な環境での開発作業
クラウド向き : 高精度が必要なコード生成、多言語対応が問われる翻訳、最新のリファレンス情報を参照する必要がある回答
「とりあえず全部ローカル」「全部クラウド」ではなく、タスクの性質で分ける運用が、コストと品質の両立に効きます。
127.0.0.1:11434 に届かない日のこと
手元のターミナルでは curl http://localhost:11434/api/tags がきちんと JSON を返すのに、コンテナの中で動かしたエージェントからは接続を拒否される——この食い違いに出くわした夜、私は半日ほど Antigravity 側の設定ファイルを疑い続けました。原因は待ち受けているアドレスの方でした。
Ollama は既定で 127.0.0.1 の 11434 番だけを開きます(Ollama FAQ )。ループバックに閉じているので、同じマシンの中からしか届きません。Docker コンテナ、WSL2 のゲスト、LAN 上の別マシン——このどれかから叩く構成にした瞬間に、接続は拒否されます。設定ファイルの base_url に localhost と書いてあるかどうかは、この段階では関係がありません。
開き先を決めるのは OLLAMA_HOST です。ここに、ひと言では済まない落とし穴があります。ターミナルで OLLAMA_HOST=0.0.0.0 ollama serve と打っても、macOS のアプリ版や Linux の systemd サービスとして既に常駐している Ollama には、その環境変数が届きません。常駐しているプロセスに渡すには、プラットフォームごとの作法が要ります。
反映できたかどうかは、呼びたい側のマシンから確かめます。手元で通ることは確認になりません。
# 呼び出す側(コンテナ内・別マシン)から実行する
curl -s --max-time 3 http://192.0.2.10:11434/api/tags | head -c 200
そのうえで、線引きを一つ置いています。開けるのは経路であって、権限ではありません。 Ollama 自体には認証がありません。設定 JSON の api_key にダミーの文字列を入れて済ませられたのは、その裏返しです。0.0.0.0 に開くというのは、同じネットワークにいる誰でもモデルを呼べる状態を作ることでもあります。私は、同一マシンで完結する構成なら既定のまま何も触らず、どうしても外から使う必要があるときだけ Nginx か Cloudflare Tunnel を前に置き、認証はその層に持たせるようにしております。
そのプロキシを立てるときに、もう一つ知っておくと手が止まりません。FAQ のリバースプロキシ例で proxy_set_header Host localhost:11434 が指定されているのには理由があって、Ollama は届いた Host ヘッダを見ています。素通しすると弾かれます。ngrok の --host-header="localhost:11434"、cloudflared の --http-host-header="localhost:11434" も同じ事情です。
なお、ブラウザ拡張や Web UI から直接叩く場合は、経路とは別に OLLAMA_ORIGINS が要ります。既定で許可されているクロスオリジンは 127.0.0.1 と 0.0.0.0 だけなので、chrome-extension://* のようなオリジンは明示的に足してください。ここだけは、経路を開いても解決しません。
モデルサイズ選定表 — 手元のメモリから逆算する
実用上、どのサイズを選ぶかで推論速度と出力品質のバランスが大きく変わります。私が日常的に触っている感覚を表にまとめます。
タグ名について一点補足しておきます。以降の節では gemma4:9b のような表記も出てきますが、Ollama のレジストリ側のタグは世代ごとに変わりますので、実際に手元で使う前に ollama list と ollama show <tag> でタグ名とパラメータ数を確認してください。私は設定ファイルにタグを直書きせず、ANTIGRAVITY_LOCAL_MODEL のような環境変数で外に出しておく運用にしています。世代が変わったときに触る場所が 1 箇所で済みます。
Apple Silicon の M2 Max 64GB では gemma3:27b が 15〜20 tokens/sec 程度、gemma3:12b なら 40 tokens/sec 程度が目安です。RTX 4090 搭載の Linux 機なら 12b モデルで 60〜80 tokens/sec が出ます。
量子化レベル(q4_0・q5_K_M・q8_0 など)も選べます。q4_0 がデフォルトで、q5_K_M にすると品質が明確に上がりますが、メモリ使用量も増えます。私は「12b の q5_K_M」を基準に、ハードが許すなら上へ、足りないなら 4b に落とす運用にしています。
フォールバック戦略 — 落ちるより、遅く落ちる方が厄介でした
ローカル LLM を併用する最大の理由は、クラウド側が使えなくなったときの保険です。ただ、個人開発の環境で数か月回してみて実際に困ったのは、想定していた「ネットが切れる」ではありませんでした。困ったのは逆向きのケースです。ローカルの Ollama が返事をしなくなり、それでもそちらへ投げ続けてしまう構成の方でした。
モデルのロード中、メモリが足りずスワップに入ったとき、OLLAMA_MAX_LOADED_MODELS の上限に触れて別モデルの退避が走っているとき。Ollama は接続そのものは受け付けたまま、応答だけを返しません。エラーが返るなら分岐できます。「返ってこない」は分岐できないのです。
Antigravity 側では、次のような形でフォールバック先を宣言できます。
{
"routing" : {
"default" : "cloud-gemini" ,
"fallback" : "local-gemma" ,
"fallback_triggers" : [ "network_error" , "rate_limit" , "api_error" ]
}
}
宣言としてはこれで足ります。ただ、この 1 枚が実際にどう振る舞うかは、書いた側からは見えません。
そこで、素朴なフォールバックと、失敗を記憶するフォールバックを同じ条件で測りました。測定対象は Ollama 本体ではなく、OpenAI 互換の応答を返すだけのローカルスタブです。ここで見たいのは推論の速さではなく、ルータが「壊れている側」へ何回・何ミリ秒を払い続けるかだからです。ローカル側は「503 を即返す」「接続を受けたまま黙る」の 2 条件、クラウド側は 40ms で正常応答する 1 条件に固定しました。
まず、素朴な実装
import json, time, urllib.request
def call (url, timeout):
req = urllib.request.Request(
url, data = json.dumps({ "messages" : []}).encode(),
headers = { "content-type" : "application/json" })
with urllib.request.urlopen(req, timeout = timeout) as r:
return json.load(r)
class Naive :
"""毎回ローカルを試し、失敗したらクラウドへ落とす。"""
def __init__ (self, local, cloud, timeout):
self .l, self .c, self .t = local, cloud, timeout
def send (self):
try :
return call( self .l, self .t), "local"
except Exception :
return call( self .c, self .t), "cloud"
上の JSON 設定が内部でやっていることは、おおむねこれです。動作としては正しく、ローカルが死んでいてもリクエストは返ります。
失敗を記憶する実装
class Breaker :
"""連続 fail_max 回の失敗で開放し、cooldown 秒は試行そのものを省く。"""
def __init__ (self, local, cloud, timeout, fail_max = 2 , cooldown = 2.0 ):
self .l, self .c, self .t = local, cloud, timeout
self .fail_max, self .cooldown = fail_max, cooldown
self .fails, self .open_until = 0 , 0.0
self .skipped = 0
def send (self):
if time.monotonic() < self .open_until:
self .skipped += 1 # ローカルを試さずクラウドへ
return call( self .c, self .t), "cloud"
try :
r = call( self .l, self .t)
self .fails = 0 # 1 回でも通れば記憶を捨てる
return r, "local"
except Exception :
self .fails += 1
if self .fails >= self .fail_max:
self .open_until = time.monotonic() + self .cooldown
return call( self .c, self .t), "cloud"
差分は「直近の失敗を覚えておき、しばらくローカルを試さない」という一点だけです。
速い失敗と遅い失敗で、結果が割れました
各条件 12 リクエスト、ローカル側のタイムアウトは 0.5 秒で揃えています。
エラーが即座に返る条件では、両者に差はありませんでした。0.6ms です。この条件だけを見て「うちは大丈夫」と判断していたのが、以前の私自身でした。
差が出たのは、応答が返らない条件です。素朴な実装はリクエスト 1 本ごとにタイムアウト分の 0.5 秒を払い続け、合計で 4.3 倍かかりました。しかも払っているのは待ち時間だけで、得ているものは何もありません。ローカルが壊れているという同じ事実を、12 回買い直している状態です。
エージェントが 1 ターンで 5 回 LLM を呼ぶなら、この 0.5 秒は 1 ターンあたり 2.5 秒になります。体感として「Antigravity が重い」と感じていた時間の一部は、ここでした。
cooldown を短くするほど、壊れている間の持ち出しが増えます
では cooldown を何秒にするか。8 秒の窓でリクエストを送り続け、処理できた件数を数えました。ローカルは黙ったままです。
cooldown 0.5 秒は 5.0 秒に比べて処理件数が 41% 少なくなりました。復帰を早く拾おうとして頻繁に探りを入れると、その探りが毎回タイムアウト待ちになるからです。
代わりに得られるのが復帰の速さです。cooldown 2.0 秒で、ローカルが実際に回復してから最初にローカルへ戻るまでの時間を 4 回測ったところ、0.32 / 0.95 / 1.63 / 2.26 秒(中央値 1.29 秒)でした。探りのタイミングと復帰のタイミングが独立なので、0 から cooldown までの間に素直に散ります。cooldown を n 秒にすると、復帰の検知は平均で n/2 秒、最悪で n 秒遅れます。それだけの話です。
私はこの数字を見て、cooldown を 2 秒に置きました。ローカルモデルのロードは速くても数秒かかりますので、0.5 秒で探りに行っても間に合わないことの方が多いのです。一方 5 秒まで延ばすと、ollama serve を再起動した直後に手が止まる時間が体感で気になりました。2 秒は、そのあいだで折り合いのついた値です。
そしてもう一点。ローカル側のタイムアウトは、クラウドより明確に短く設定しています。ローカルは「速いから使う」のであって、クラウドより待つなら使う理由がありません。手元では 0.5 秒で見切り、クラウドには通常どおりの秒数を与えています。
先ほどの JSON 設定に戻ると、fallback_triggers に並んでいるのは network_error・rate_limit・api_error の 3 つでした。いずれも「相手が何かを返してくれる」故障です。返ってこない故障は、この一覧のどこにも入っていません。設定ファイルで書ける範囲と、実際に起きる範囲は違うのだと、測って初めて腑に落ちました。
運用で詰まるポイント
セットアップ自体はスムーズに進むことが多いのですが、1 週間ほど使い込むと、次のような問題にぶつかることが多いです。
コンテキスト長の制限 : Ollama の既定のコンテキスト長は 4096 トークンで(Ollama FAQ )、Antigravity のエージェントが長い会話履歴を送ると途中で切られます。OLLAMA_CONTEXT_LENGTH 環境変数、または Modelfile で num_ctx 16384 のように明示することで拡張できます。ただしメモリ使用量がコンテキスト長に比例して増えますので、ハードの余裕を見ながら設定してください。
モデルのロード待ち : Ollama は初回リクエスト時にモデルをメモリにロードします。最初の応答が 10〜30 秒遅れることがあります。OLLAMA_KEEP_ALIVE=-1 を設定するとモデルを常駐させられ、体感速度が大きく改善します。
複数モデルの同時使用 : Antigravity が複数のエージェントを並列実行すると、Ollama 側で複数モデルを同時にロードしようとしてメモリが溢れることがあります。OLLAMA_MAX_LOADED_MODELS でロード同時数を制限しつつ、重要なエージェント間で同じモデルを共有する設計にするのが安全です。
モデルを 1 つに揃えても溢れるとき — num_ctx は並列数のぶんだけ掛け算されます
上の対処に従ってエージェント全員へ同じモデルを共有させたのに、3 本目を立ち上げた途端に落ちてしまいました。ここで私は一度つまずいています。ロードされているモデルは 1 つなのだから、メモリはそれ以上増えないはずだ、と思い込んでいたのです。
増えます。Ollama は 1 つのモデルへの並列リクエストを OLLAMA_NUM_PARALLEL で受け付けますが、必要なメモリは OLLAMA_NUM_PARALLEL × OLLAMA_CONTEXT_LENGTH に比例して伸びます 。ドキュメントに載っている例で言えば、2K のコンテキストで 4 並列を受けると、8K のコンテキストを 1 本抱えているのと同じ重さになります(Ollama FAQ )。
この記事の後半で「実際の履歴長の 1.5 倍を num_ctx に置く」と書いています。あの見積もりは、並列数が 1 のときの話です。並列数を上げるなら、同じ掛け算を通します。
順番としては、並列数を上げる前に num_ctx を下げます。私は、エージェントを増やしたい日はまず num_ctx を半分に落とし、それで会話が途中で切られないかを先に確かめるようにしております。切られなければ、そのまま並列数を足せます。
手当てがもう一つあります。OLLAMA_KV_CACHE_TYPE=q8_0 です。KV キャッシュを 8bit に量子化すると、既定の f16 に対しておよそ半分のメモリで済み、精度の低下はほとんど気づかない程度とされています。Flash Attention が有効な構成が前提で、設定は特定のモデルではなく全体に効きます。
重みの量子化はモデルの賢さに、KV キャッシュの量子化は履歴の長さに効きます。 この二つを同じ引き出しに入れていたころ、私はコンテキストを伸ばしたくなるたびに一段小さいモデルへ逃げていました。分けて考えられるようになってから、12B のまま履歴だけを伸ばす、という選択が取れるようになりました。
いま何が起きているかは ollama ps が早く教えてくれます。PROCESSOR 列が 100% GPU から 48%/52% CPU/GPU のような表示に変わっていれば、モデルが GPU に収まりきらず、システムメモリへこぼれた合図です。tok/s が落ちるのはたいていこの境目で、設定を疑う前にまずこの 1 行を見ると回り道が減ります。
Gemma 4 自体を理解する — 前世代からの改善点
統合の前提として、Gemma 4 そのものの特性を簡単に押さえておきます。Antigravity でローカル LLM として使う上で、コンテキスト長や得意・不得意を知っておくと、モデルサイズの選択やタスク振り分けが楽になります。
Gemma 4 は Gemma 3 から、次の方向で変わっています。
モデル構成 : 同じ用途をより小さなパラメータ数でこなせる構成が増え、手元のメモリから選べる幅が広がりました
マルチモーダル : テキストに加えて画像の入力を受け取れます
コンテキスト長 : 前世代より伸び、長い会話履歴をそのまま渡せる場面が増えました
日本語 : 学習データの拡充で、前世代よりは扱いやすくなっています
ここに「何 % 向上」という数字を並べないのは、公称値と手元の体感が噛み合わずに選定をやり直した経験が、私の側に何度かあるからです。公称のベンチマークは候補を絞るために、手元の実測は決めるために。 この記事の後半に、TTFT と tok/s を自分の機械で測るための短いスクリプトを置いています。サイズを決める段になったら、そちらの数字をご覧ください。
AGENTS.md でローカル/クラウドのモデル切り替えを制御する
Antigravity のエージェント設定(AGENTS.md)では、エージェントごとに使用モデルを切り替えるルールを書けます。機密データ処理は強制的にローカル、ネットワーク接続が確認できないときはオフラインモードに切り替えるなど、運用要件に合わせた制御が可能です。
Antigravity のワークスペースに AGENTS.md ファイルを置くことで、どのタスクでローカルモデルを使うかを明示的に制御できます。
# Agent Configuration
### Default Model
Use `gemini-3-pro` for planning tasks and complex reasoning.
### Privacy-Sensitive Tasks
Use `gemma4-local` for the following:
- Reading or processing files under `private/`
- Any task involving customer data
- Initial draft generation (review with cloud model before finalizing)
### Offline Mode
When network is unavailable, fall back to `gemma4-local` for all tasks.
このファイルがワークスペースルートにあると、Antigravity のエージェントがタスクごとに適切なモデルを自動選択してくれます。特に「プライバシーセンシティブなタスクだけローカル、高度な推論はクラウド」というハイブリッド構成が、実運用で最もバランスが良いと感じています。
推論パフォーマンス最適化
ローカル推論は CPU/GPU のリソース管理次第で速度が大きく変わります。実用的なレスポンスタイムを出すための調整ポイントを整理します。
初期設定のままだと、Gemma 4 9B は十分な速度が出ないことがあります。3つの設定変更で大きく改善できます。
量子化レベルを調整します 。Ollama はデフォルトで Q4_K_M(4bit 量子化)を使いますが、Apple Silicon の M3 以降なら Q5_K_M や Q6_K を試す価値があります。品質が向上しつつ、メモリに余裕があれば速度もほぼ変わりません。
ollama pull gemma4:9b-q5_K_M
コンテキスト長を実用範囲に絞ります 。モデル側の上限と、Ollama が実際に確保する num_ctx は別物です。後者の既定は 4096 で、ここをモデルの上限まで引き上げると KV キャッシュがそのぶん増え、メモリと推論時間を直接押し上げます。手元では、扱うタスクの実際の履歴長を測ってから、その 1.5 倍程度を num_ctx に置くようにしております。
keep_alive の設定 。デフォルトではモデルが5分間使われないとアンロードされます。頻繁に呼び出すなら OLLAMA_KEEP_ALIVE=30m のように長めに設定すると、都度のロード時間(数秒)を省けます。
実際の開発ワークフロー — Antigravity × ローカル Gemma 4 の使いどころ
ローカル LLM を組み込んだ Antigravity 開発の典型的なワークフローと、それぞれの場面で Gemma 4 がどう機能するかを具体例で見ていきます。
パターン1: デイリー開発はローカル、複雑な設計はクラウド
日常的なコーディングの大半(補完・修正・テスト作成)はローカルの Gemma 4 で処理し、複雑なアーキテクチャ議論や長い文脈が必要な場合だけクラウドに切り替えます。
# Antigravity + Gemma 4 でよく使うパターン
# 1. 関数の補完(ローカルで十分)
def parse_config (file_path: str ) -> dict :
"""設定ファイルを読み込んで辞書を返す"""
# ← ここでAntigravity(Gemma4)が自動補完
# 2. コメント生成(ローカルで十分)
# ← 関数にカーソルを当てて「このコードを説明するdocstringを生成」
# 3. バグ修正(ローカルで十分)
# ← エラーをAntigravityに貼り付けて修正を依頼
# クラウド(Claude/Gemini)に切り替える判断基準
- プロジェクト全体を俯瞰した設計議論
- 10ファイル以上に跨るリファクタリング計画
- セキュリティ脅威モデルの分析
- API 設計レビュー(外部公開前)
パターン2: 機密プロジェクト専用ワークフロー
金融機関・医療関連・社内ツール開発など、コードを社外に出せないプロジェクトでは、Gemma 4 専用モードで運用します。
# .env または Antigravity 設定で強制ローカルモード
ANTIGRAVITY_MODEL = local
ANTIGRAVITY_ALLOW_CLOUD = false
ANTIGRAVITY_LOCAL_ENDPOINT = http://localhost:11434/v1
ANTIGRAVITY_LOCAL_MODEL = gemma4:26b
# 環境変数でクラウドへの接続を完全にブロック
# → どんな操作をしてもコードはローカルから出ない
パターン3: オフライン開発(出張・機内など)
あらかじめモデルをダウンロードしておき、オフラインでも開発を継続できます。
# オフライン前の準備
ollama pull gemma4:e2b # モデルをキャッシュ
ollama pull gemma4:e4b # 余裕があれば大きいモデルも
# オフライン中は自動的にローカルモデルが使われる
# (Antigravity がオフライン検出してフォールバック)
Gemma 4 のコーディング性能ベンチマーク
ローカル LLM を選ぶときに最も気になるのがコード生成の品質です。GSM8K、HumanEval、MBPP といった標準ベンチマークでの Gemma 4 の数値を、実用感とすり合わせながら整理します。
Gemma 4 E4B を Antigravity と組み合わせた際の実測値(M3 Max MacBook Pro、64GB RAM):
Python コード補完 : 平均 2.1 秒(Claude 3.5 Sonnet 比: 品質 85%、速度 1.3倍速)
TypeScript 関数実装 : 平均 4.3 秒(Claude 3.5 Sonnet 比: 品質 80%、速度 0.9倍)
バグ修正・エラー解析 : 平均 3.8 秒(Claude 3.5 Sonnet 比: 品質 75%、速度 1.1倍)
ドキュメント生成 : 平均 5.2 秒(Claude 3.5 Sonnet 比: 品質 88%、速度 1.2倍)
日常的なコーディング支援では Gemma 4 E4B で十分な品質が出ており、API コストがゼロになる点を考えると、積極的に使う価値があります。
Gemma 4 の得意・不得意 — タスク振り分けの判断基準
ローカル / クラウドのハイブリッド運用では「どのタスクをローカルに任せるか」の判断が要です。Gemma 4 が安定して強い領域と、クラウド側に投げた方が結果が良い領域を整理します。
Gemma 4 はオープンウェイトモデルとして非常に優秀ですが、用途によって向き不向きがあります。
得意なこと
140言語のコードに対応しており、Python・TypeScript・Go・Rust などの主要言語で高品質なコード生成ができます。また、比較的短いコードの理解・修正・補完は非常に正確です。日本語の技術文書読解も実用レベルです。
不得意なこと
非常に長いコンテキスト(1万行以上のコードベース全体)の把握は苦手で、最新のフレームワーク(2025年以降に登場したもの)の知識はトレーニングデータに依存します。また、微妙な設計上のトレードオフの判断は、より大型のクラウドモデルの方が優れています。
このため、「ローカル Gemma 4 × クラウドモデル」のハイブリッド が現実的な最適解になります。
実運用で分かった落とし穴
机上の設計通りに動かないケースが、ローカル LLM 統合では特に多く発生します。実運用でぶつかった代表的なトラブルと回避策を共有します。
ここまでの手順で動きますが、実際に運用してみて初めて気づく落とし穴が3つあります。
ひとつ目:日本語の応答品質はやや劣ります 。Gemma 4 9B は英語中心の学習データで作られており、日本語での応答は Gemini 3 Pro と比べると明らかに劣ります。特に敬語の一貫性や専門用語の訳出で差が出ます。日本語での長文生成が主用途なら、Gemma 4 27B か、クラウドモデルに留めた方が無難です。
ふたつ目:ツールコールの精度が低めです 。ローカル LLM 全般の課題ですが、Gemma 4 9B はツール呼び出しの JSON 生成が不安定です。複雑なエージェントで多くのツールを定義すると、間違った引数を渡すことがあります。ローカルモデルには「単純なツールを1〜2個だけ使うタスク」を任せるのが無難です。
みっつ目:発熱とバッテリー消費 。ノート PC で 9B モデルを連続実行すると、筐体が熱くなり、バッテリーは2〜3時間で切れます。長時間使うなら電源接続は必須で、デスクトップ PC か外部マシンに Ollama を置くのが長期運用では楽です。
表の数字を信じないための 30 行 — 自分の環境で TTFT と tok/s を測る
ここまで挙げた速度の目安は、あくまで私の手元の機材での話です。私自身、同じ設定を別の Mac に移した途端に体感が変わって面食らったことがあります。同じ M3 でもメモリ帯域とサーマル状況で数字は動きますし、num_ctx を伸ばせば当然遅くなります。モデルサイズを決めるときに他人のベンチを眺めても、結局は自分の機械で測るのが一番早い、というのが何度かやり直した末の結論でした。
計測で見るべき値は 2 つです。TTFT(最初のトークンが返るまでの時間) と tok/s(生成スループット) 。エディタ上の補完では体感を支配するのはほぼ TTFT で、tok/s は長文生成のときにだけ効いてきます。ここを分けずに「速い / 遅い」と語ると、モデル選定を毎回間違えます。
Ollama の OpenAI 互換エンドポイントはストリーミングに対応していますので、標準ライブラリだけで両方測れます。
# bench_ollama.py — TTFT と tok/s をモデル別に測る
import json
import time
import urllib.request
ENDPOINT = "http://localhost:11434/v1/chat/completions"
PROMPT = "次の関数にドキュメンテーション文字列を付けてください: \n def parse_config(path): \n ..."
def measure (model: str , num_predict: int = 256 ) -> dict :
payload = {
"model" : model,
"messages" : [{ "role" : "user" , "content" : PROMPT }],
"stream" : True ,
"max_tokens" : num_predict,
}
req = urllib.request.Request(
ENDPOINT ,
data = json.dumps(payload).encode( "utf-8" ),
headers = { "Content-Type" : "application/json" , "Authorization" : "Bearer ollama" },
)
started = time.perf_counter()
first_token_at = None
tokens = 0
with urllib.request.urlopen(req) as res:
for raw in res:
line = raw.decode( "utf-8" ).strip()
if not line.startswith( "data: " ):
continue
body = line[ len ( "data: " ):]
if body == "[DONE]" :
break
delta = json.loads(body)[ "choices" ][ 0 ][ "delta" ]
if not delta.get( "content" ):
continue
if first_token_at is None :
first_token_at = time.perf_counter()
tokens += 1
finished = time.perf_counter()
ttft = (first_token_at or finished) - started
gen_seconds = finished - (first_token_at or started)
return {
"model" : model,
"ttft_sec" : round (ttft, 2 ),
"tokens" : tokens,
"tok_per_sec" : round (tokens / gen_seconds, 1 ) if gen_seconds > 0 else 0.0 ,
}
if __name__ == "__main__" :
for tag in ( "gemma3:4b" , "gemma3:12b" , "gemma3:27b" ):
# 1 回目はモデルのロード時間を含むため捨てて、2 回目以降を採用します
measure(tag, num_predict = 32 )
print (measure(tag))
意図的に 2 回呼んでいるところが要点です。1 回目にはモデルをメモリに載せる時間が丸ごと乗りますので、そのまま記録すると TTFT が数十秒という無意味な数字になります。前述の OLLAMA_KEEP_ALIVE を効かせた状態の値こそが、実際に毎日体感する数字です。
トークン数はストリームのチャンク数で近似しています。厳密なトークン数ではありませんが、モデル間の相対比較には十分実用に足ります。
測った数字をどう判断に落とすか
数字が出たあとの判断基準を、私は次のように置いています。
この表を作ってから、モデル選定で迷う時間がほぼ消えました。特に効いたのは、tok/s が高いという理由だけで大きいモデルを選ばなくなったことです。私の M3 Max では、12B と 27B の tok/s は 2 倍ほどの差でしたが、TTFT の差は 3 倍以上ありました。補完用途では 12B の方が明確に「速い」と感じます。数字を分けて測らなければ、この逆転には気づけませんでした。
もう一点。計測は必ず電源に繋いだ状態と、バッテリー駆動の状態の両方で回してください。ノート PC では省電力設定によって tok/s が半分近くまで落ちることがあり、机の上での検証が外出先で再現しません。
次に検討すること
ローカル LLM 統合が安定したら、次のステップとして「ローカル専用のエージェント」を設計することをおすすめします。たとえば機密度の高いコードレビュー、社内ドキュメントの要約、個人メモの構造化など、クラウドに一度も情報を送らないことを保証できるエージェントです。
Antigravity のマルチエージェント構成については Antigravity マルチエージェント・オーケストレーション入門 で詳しく扱っています。ローカル LLM と組み合わせる設計例も近々アップデート予定ですので、定期的に覗いてみてください。