Ollama の Modelfile は「カスタム設定ファイル」だと思っていました。実態は、base model の chat template を含む挙動を、4種類のディレクティブでまるごと上書きできる仕組みです。ローカル LLM の応答が Modelfile 記述で毎回変わる原因は、ここにあります。本記事は Ollama Modelfile の 4ディレクティブ(TEMPLATE / SYSTEM / PARAMETER / MESSAGE)が chat template をどう上書きするかを、ollama show + ollama create で実測して確かめた話です。num_ctx デフォルトの罠、ランタイム比較、TTFT 指標、量子化、Cold Start 時間とは別軸で、「Modelfile を書き換えるとモデルの応答がどう変わるか」だけに絞ります。
Modelfile の4種類のディレクティブ
Ollama 公式ドキュメント(github.com/ollama/ollama/blob/main/docs/modelfile.mdx)によると、Modelfile で base model の挙動を上書きできるのは、主に次の4つのディレクティブです。
| ディレクティブ | 上書きする対象 | 影響範囲 |
|---|---|---|
TEMPLATE |
chat template (プロンプトのフォーマット) | 生成時の入力形式そのもの |
SYSTEM |
system prompt | モデルの応答方針・ペルソナ |
PARAMETER |
サンプリング/context 設定 | 温度・top_p・num_ctx など |
MESSAGE |
事前会話履歴 (few-shot) | few-shot 例の埋め込み |
他にも FROM ADAPTER LICENSE などがあります。応答の中身に一番効くのはこの4つです。
実測1: base model の Modelfile を覗く
ollama show <model> --modelfile を打つと、そのモデルが持っている Modelfile が全部出てきます。手元の4モデルで実測しました。
qwen2.5:1.5b
TEMPLATE """{{- if .Messages }}
{{- if or .System .Tools }}<|im_start|>system
{{- if .System }}
{{ .System }}
{{- end }}
{{- if .Tools }}
# Tools
You may call one or more functions to assist with the user query.
Qwen 系のトークン(<|im_start|> <|im_end|>)を使った chat template。Tools 対応の分岐が最初から入っています。
gemma2:2b
TEMPLATE """{{- range $i, $_ := .Messages }}
{{- $last := eq (len (slice $.Messages $i)) 1 }}
{{- if or (eq .Role "user") (eq .Role "system") }}<start_of_turn>user
{{ .Content }}<end_of_turn>
Gemma 系のトークン(<start_of_turn> <end_of_turn>)を使う別の chat template。同じ Ollama から起動しているのに、モデルごとに template が完全に別物 です。
qwen3.5:0.8b
TEMPLATE {{ .Prompt }}
RENDERER qwen3.5
PARSER qwen3.5
PARAMETER presence_penalty 1.5
Qwen 3.5 系は TEMPLATE が {{ .Prompt }} だけで、代わりに RENDERER と PARSER が別途指定されています。Tool call のパースは Ollama 側の RENDERER/PARSER が担う、新しめの設計です。
deepseek-r1:1.5b
TEMPLATE """{{- if .System }}{{ .System }}{{ end }}
{{- range $i, $_ := .Messages }}
{{- if eq .Role "user" }}<|User|>{{ .Content }}
{{- else if eq .Role "assistant" }}<|Assistant|>
{{- if and $.IsThinkSet (and $last .Thinking) -}}
<think>
{{ .Thinking }}
</think>
DeepSeek R1 は独自の全角 U+FF5C トークン(<|User|> <|Assistant|>)を使い、<think> タグで reasoning ブロックを分離できる設計です。
4モデルで chat template が完全に別々 です。
「同じ Ollama で ollama run するのだから同じインターフェイスだろう」という直感は、Modelfile を覗いた瞬間に崩れます。
実測2: Modelfile で上書きする
ollama create で Modelfile を渡すと、上記の template や parameter を自由に差し替えられます。手元で試したものはこれです。
cat > /tmp/test_modelfile << 'EOF'
FROM qwen2.5:1.5b
SYSTEM """You are a laconic assistant. Answer in exactly one sentence."""
PARAMETER temperature 0.1
PARAMETER num_ctx 4096
EOF
ollama create test-laconic -f /tmp/test_modelfile
作った直後に ollama show test-laconic --modelfile を確認すると:
FROM /home/iris/.ollama/models/blobs/sha256-183715c...
TEMPLATE """{{- if .Messages }} ← qwen2.5 の TEMPLATE を継承
SYSTEM You are a laconic assistant. Answer in exactly one sentence.
PARAMETER num_ctx 4096
PARAMETER temperature 0.1
指定した SYSTEM と PARAMETER が上書きされ、TEMPLATE は qwen2.5 のものが継承されました。書いた分だけ上書き。書かなければ base のまま です。全上書きでも部分上書きでも動きます。以降、ollama run test-laconic すると、system prompt が「一文で答える」に固定された状態でモデルが動きます。
実測3: TEMPLATE をゼロから書き換えると何が起きるか
TEMPLATE {{ .Prompt }} だけの最小構成に置き換えた Modelfile を作ってみます。
FROM qwen2.5:1.5b
TEMPLATE """{{ .Prompt }}"""
これで ollama create すると、chat template のトークン(<|im_start|> <|im_end|>)が消えます。Ollama から ollama run すると、モデルが「システムメッセージ何それ?」の状態になり、応答が急にランダム化します。Qwen 系は chat template のトークンを前提に学習されているので、テンプレートを外すと事後学習の重みが機能しなくなるためです。TEMPLATE を書き換えるときは、base model が学習したトークンを維持すること。必須です。ここを守らずに書き換えて「Ollama のモデルが急に嘘つきになった」を経験するのが、Modelfile カスタマイズの最初の落とし穴です。
4パターンで整理
上書きするディレクティブの組み合わせで、応答がどう変わるかをまとめます。
| パターン | 変えるもの | 応答への影響 |
|---|---|---|
| A |
SYSTEM のみ |
ペルソナ・答え方が変わる。安全 |
| B |
PARAMETER のみ |
温度・context 長・stop トークンが変わる。挙動チューニング |
| C |
MESSAGE を追加 |
few-shot 例を仕込める。回答フォーマット固定に有効 |
| D |
TEMPLATE を全上書き |
chat template トークンが変わる。壊しやすい、慎重に |
A / B / C は日常的に使ってよい上書き。D はモデルの再学習を疑うレベルの変更 だと思って扱うのが安全です。Hugging Face の tokenizer_config.json から chat template を取ってきて Modelfile に貼り付ける、というワークフローも可能です。これはパターン D に該当します。tokenizer 側の template と Ollama の Go template 文法({{- if .Messages }} など)の変換が必要になるので、単純コピペでは動かない点も注意です。
MESSAGE ディレクティブの実用
見落とされがちなのが MESSAGE です。few-shot 例を仕込めるので、フォーマットを固定したいときに便利です。
FROM qwen2.5:1.5b
SYSTEM """You are a JSON API. Always respond with valid JSON."""
MESSAGE user What is 2 plus 2?
MESSAGE assistant {"answer": 4}
MESSAGE user What is 3 times 5?
MESSAGE assistant {"answer": 15}
この状態で ollama run すると、JSON以外の形式で返してくる確率が明確に下がります。SYSTEM だけで指示するより、MESSAGE で1-2例見せた方が効くのは、通常の in-context learning と同じです。Ollama 単体で軽い few-shot エージェントを作りたいときに、MESSAGE ディレクティブは覚えておく価値があります。
実測 - Modelfile 上書きで PARAMETER が本当に効くか
「PARAMETER を Modelfile に書いても、実行時のリクエストに options で渡さないと効かないのでは?」を確かめました。
cat > /tmp/temp_low << 'EOF'
FROM qwen2.5:1.5b
PARAMETER temperature 0.01
EOF
ollama create test-cold -f /tmp/temp_low
# API 経由でリクエスト
curl -s http://localhost:11434/api/generate -d '{
"model": "test-cold",
"prompt": "Say a random word",
"stream": false
}' | jq -r .response
同じプロンプトを5回投げると、test-cold はほぼ同じ単語(私の環境では "Sure" が5回中4回)を返しました。base の qwen2.5:1.5b にリクエスト時 options: {temperature: 1.0} を渡した場合と比べると、明確にサンプリングが決定的になっているのが分かります。Modelfile の PARAMETER は、API リクエストで options を渡さない限りのデフォルト値として機能します。リクエスト側で options を渡すと、そちらが Modelfile の PARAMETER を上書きします (Ollama の設定継承ルールの通り)。両方書いてどちらが効くかは、リクエスト側優先です。
実測 - num_ctx を Modelfile で固定する
num_ctx のデフォルト値の低さは、以前の記事(Ollama num_ctx = default 2048 の話)でも書きました。Modelfile で PARAMETER num_ctx 8192 を書いておけば、そのモデルを叩くたびに明示指定しなくて済みます。
FROM qwen2.5:1.5b
PARAMETER num_ctx 8192
これで ollama create qwen-8k -f Modelfile → ollama run qwen-8k すると、8k コンテキストで起動します。API 経由でも options に num_ctx を指定しなくても8kで動きます。長文入力するモデルには、Modelfile で num_ctx を固定しておく。安全です。VRAM 12GB で 32B モデルを動かすときは、Modelfile で num_ctx を明示的に4096 or 8192 にキャップしておかないと、モデルが自称する最大 context (32k超) を試そうとして OOM で落ちる、というのが実務でよくハマる箇所です。
Chat Template を書き換えたい実務ケース
TEMPLATE を全上書きするのは危険と書きました。以下のケースでは書き換えが必要になります。
- Tool call の JSON フォーマットを変えたい — base の template が想定していない Tool 呼び出しをさせたい場合、TEMPLATE の Tools 分岐を書き直す必要がある
-
thinking モードを外したい — DeepSeek R1 のような
<think>タグを応答から消したい場合、TEMPLATE の分岐を書き換える - RAG 用にコンテキスト注入位置を変えたい — system prompt の中に検索結果を挿入する専用フォーマットを組みたい場合
いずれも、base model が学習した chat template トークンは維持したまま、周辺のディレクティブだけを追加/削除する形で書き換えます。トークン自体(<|im_start|> <start_of_turn> など)を消してはいけません。Ollama の TEMPLATE は Go の text/template 文法で書かれているので、慣れれば分岐や range を自由に組めます。ただし、書き換えた Modelfile を CI で回して「壊れていないか」をテストする仕組みを合わせて用意する必要があります。壊しやすい。けど、それに気付ける仕組みも簡単に組める、という設計です。
罠 - Modelfile を書き換えると再ダウンロードが必要なわけではない
Modelfile を上書きしても、モデルの重み(blob)は再ダウンロードされません。ollama create は base model の blob をそのまま参照して、新しい manifest(小さな JSON) だけを作ります。
ollama show test-laconic --modelfile の1行目に、
FROM /home/iris/.ollama/models/blobs/sha256-183715c...
と出ていたのは、上書きした Modelfile も qwen2.5:1.5b と同じ blob を指しているためです。Modelfile カスタマイズはディスクをほぼ消費しない。実務でありがたい設計です。数十パターンの Modelfile を試しても、ディスク占有は base model 1個分のままです。
まとめ
- Ollama の Modelfile は4種類のディレクティブ(TEMPLATE / SYSTEM / PARAMETER / MESSAGE)で base model の挙動を上書きする
-
ollama show <model> --modelfileで base model の chat template を確認できる。qwen / gemma / deepseek で全部別のトークンを使っていることが分かる -
ollama create <new> -f Modelfileで上書き版を作れる。書いた分だけ差し替え、書かなければ base のまま - SYSTEM / PARAMETER / MESSAGE の上書きは安全、TEMPLATE の全上書きは壊しやすい
- MESSAGE で few-shot を仕込むと、SYSTEM だけよりフォーマット固定が効きやすい
- Modelfile の上書きは blob を再ダウンロードしない。ディスクをほぼ消費せずに複数バリアント運用可
Ollama Modelfile は「カスタム設定ファイル」ではなく、モデルの挙動を4方向から上書きできる小さな DSL でした。base model の chat template を尊重した上で、SYSTEM と PARAMETER と MESSAGE を上手く組み合わせるのが、Ollama を実務投入するときの基本作法です。同じ base model から、用途別に3-5個のバリアント (myapp-json / myapp-narrative / myapp-code) を Modelfile で作り分ける、というのが Ollama を業務投入するときのよくある実装パターンです。ディスクを消費しないので、思ったより気軽にできます。プロンプトを毎回長々と書く代わりに、Modelfile 側で持たせておく方が、システム全体の再現性も上がります。
ローカル LLM のより広い運用ノウハウは、以下の書籍で章立てして解説しています。
参考
- Ollama 公式: Modelfile documentation
- Ollama 実測コマンド:
ollama show <model> --modelfile/ollama create <new> -f Modelfile - 実測モデル: qwen2.5:1.5b / gemma2:2b / qwen3.5:0.8b / deepseek-r1:1.5b
