Ollama の構造化出力は、format に JSON Schema を渡すとモデルの出力をその形に制約します。ところがサーバの版によっては、think=false を一緒に送った瞬間にこの制約が外れます。モデルはプロンプトどおりの自然文を返し、受け取った側の json.loads() が落ちる、という壊れ方です。
同じスクリプトが、Ollama サーバの版を上げるだけで通ります。直すのはクライアントではなくサーバです。
出るエラー
Python から呼んでいるなら、多くはパース側の例外として現れます。
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
例外を捕まえて表示している場合、見えるのはメッセージ部分だけです。
Expecting value: line 1 column 1 (char 0)
例外にならず、「JSON のはずが英語の文章で返ってくる」という形で気づくこともあります。実際に返ってきたのは次の文字列でした。
The weather is okay today, with a total of seven sunny hours expected!
結論
Ollama サーバを 0.31.2 以上に上げてください。 クライアントのコードは変更不要です。
サーバをコンテナで動かしているなら、イメージのタグを上げて作り直す手順です。11434 番は既存のサーバが使っているので、先に止めないとポート衝突で失敗します。
docker rm -f ollama # 稼働中の旧サーバを止める(名前は docker ps で確認)
docker run -d --name ollama \
-v ollama:/root/.ollama \
-p 127.0.0.1:11434:11434 \
ollama/ollama:0.31.2
-p にホスト側 IP を書かないと Docker は全インターフェース(0.0.0.0)へ公開します。ローカルで使うだけなら 127.0.0.1: を付けて loopback に閉じてください。モデルは名前付き volume ollama に残るので、再ダウンロードは要りません。
Docker Compose なら image: のタグを ollama/ollama:0.31.2 に書き換えて docker compose up -d で作り直します。上がったら curl http://127.0.0.1:11434/api/version で版を確かめてください。
再現するコード
think=false と format を同時に送るだけで再現します。ここでは schema に反する出力をモデルに促すため、プロンプトで「JSON を使うな」と指示しました。format が守られていれば schema が勝ち、無視されればプロンプトが勝つ、という切り分けです。
import json
from ollama import Client
SCHEMA = {
"type": "object",
"properties": {
"answer": {"type": "string", "enum": ["ok"]},
"count": {"type": "integer", "enum": [7]},
},
"required": ["answer", "count"],
"additionalProperties": False,
}
PROMPT = (
"Do not use JSON. Answer as one friendly English sentence. "
"The answer value should be ok and the count should be 7."
)
client = Client(host="http://127.0.0.1:11434")
resp = client.chat(
model="qwen3.5:9b",
messages=[{"role": "user", "content": PROMPT}],
format=SCHEMA,
think=False,
options={"temperature": 0, "num_predict": 64, "num_ctx": 2048},
)
content = resp.message.content
print("raw content:", repr(content))
data = json.loads(content)
assert data == {"answer": "ok", "count": 7}, data
print("FORMAT_OK", data)
ollama/ollama:0.31.1 のサーバに対して実行すると、raw content に自然文が入り、次の行の json.loads() が JSONDecodeError を送出して終了コード 1 になります。ollama/ollama:0.31.2 に向ければ、同じスクリプトが {"answer": "ok", "count": 7} を出して終了コード 0 で終わります。
プロンプトの敵対性は再現の条件ではありません。「JSON を使うな」を外した中立な指示(The answer value should be ok and the count should be 7.)でも、0.31.1 は schema を無視して文章を返しました(num_predict: 64 で打ち切られたため末尾は途中で切れています)。
Understood! I'll ensure:
- The **answer value** is reasonable/acceptable ("ok").
- The **count** equals exactly **7**.
版の境界
errfix の検証ハーネスで実測した2点です。モデルもスクリプトも固定し、入れ替えたのはサーバの版だけです。
| Ollama サーバ | think=false + format | 返ってくる message.content |
|---|---|---|
ollama/ollama:0.31.1 | schema が無視される | 自然文 |
ollama/ollama:0.31.2 | schema どおり | { "answer": "ok", "count": 7 } |
上流でも境界がそのまま追えます。qwen3.5 系での報告が issue #14645(報告版は 0.17.6)、修正が PR #15901、そのコミット 892e7f6 は v0.31.1…v0.31.2 の差分に含まれます。v0.31.2 のリリースノートの1行が指しているのも、この修正です。
Fixed structured output for thinking models when thinking is disabled
つまり不具合は 0.31.1 で急に生えたものではなく、少なくとも 0.17.6 の報告まで遡ります。古いサーバを使い続けている環境ほど踏みます。
この不具合は、手元では出ないのに古い推論サーバに向けたときだけ出ます。クライアントライブラリ(ollama パッケージ)の版を上げても直りませんし、pip freeze にも痕跡が残りません。修正が入ったのはサーバの server/routes.go だからです。Docker Compose でタグを固定したまま放置している環境や、チームで共有している推論サーバだけが落ちる、という形になりやすいところです。
temperature: 0 で測っており、同じ環境の3回はすべて同じ出力でした。あらゆる入力・あらゆる環境で同じ文字列が返るという意味ではありません。判定に使うのは文面の一致ではなく、JSON としてパースでき schema に合うかどうかです。
なぜ起きるか
format の制約は、thinking 対応のモデルではすぐには適用されません。サーバは thinking の出力から通常の出力へ切り替わる瞬間を合図にして、そこから制約をかけ始めます。think=false を送るとモデルは thinking をまったく出さないので、その合図が一度も来ません。結果として format は最後まで適用されないままになります。
0.31.1 の server/routes.go には、この即時適用が gemma4 だけの暫定対応として書かれていました。
// TODO(parthsareen): temporary fix for https://github.com/ollama/ollama/issues/15260.
forceImmediate := m.Config.Parser == "gemma4" && req.Think != nil && !req.Think.Bool()
PR #15901 はこれを、thinking に対応したパーサ全般へ広げます。
forceImmediate := builtinParser != nil && builtinParser.HasThinkingSupport() && req.Think != nil && !req.Think.Bool()
qwen3.5 はこの一般化を待っていた側でした。チャットテンプレートを書き換えても直りません。 判定しているのはサーバであって、モデル側のテンプレートではないからです。
どのモデルで出るか
thinking に対応したパーサを持つモデルだけが、この経路に入ります。ハーネスで再検証したのは qwen3.5:9b の1点で、残りは GPU ホストでの参考実測です。
| モデル | 0.31.1 での think=false + format | 理由 |
|---|---|---|
qwen3.5:9b | schema を無視して自然文(ハーネスで再検証) | 一般化を待っていた側 |
qwen3.6:35b-a3b | schema を無視して自然文(参考実測) | 同上 |
qwen3:4b / 8b / 14b | 正しい JSON(参考実測) | thinking の経路に入らない |
gemma4:26b | 正しい JSON(参考実測) | 上の暫定対応で先に直っていた |
同じ Qwen でも qwen3 系では出ず、qwen3.5 と qwen3.6 で出ます。gemma4 が無事なのはモデルが優れているからではなく、gemma4 だけを名指しする暫定対応が先に入っていたからです。0.31.2 の一般化で、この差はなくなりました。
0.31.1 に留まる場合
think パラメータを送らないという回避策があります。false を明示しなければ format は適用されます。上流の issue #15260 にも同じことが書かれており、errfix でも qwen3.5:9b で1回だけ確認しました(num_predict の上限を外した状態で {"answer": "ok", "count": 7} が返り、done_reason は stop)。
引き換えは thinking のコストです。同じ観測で thinking は 31,234 文字まで伸び、CPU 推論では応答まで約20分かかりました。この長さはモデルとプロンプトに強く依存するので、相場ではなく最も膨らんだ一例として読んでください。think=false はまさにこのコストを避けるための指定なので、回避策として万能ではありません。
そして生成枠を絞ったままでは成立しません。num_predict を 64 や 1024 にすると thinking がその枠を使い切り、content は空文字のまま done_reason: length で返ります。回避策を試したつもりが、別の壊れ方を見ているだけです。かといって num_predict: -1(無制限)を共有サーバで常用すると、1リクエストが長時間 CPU や GPU を占有します。採るなら、クライアント側のタイムアウトと同時実行数を決めたうえで、有限の上限をモデルごとに測って選んでください。本番や共有環境では、回避策よりサーバの版上げを優先するほうが安全です。
なお chat_template_kwargs({"think": false, "chat_template_kwargs": {"enable_thinking": false}})は回避策になりません。これは llama.cpp などのサーバ側オプションで、Ollama の /api/chat にはこのパラメータ自体が存在しません(0.31.1 の api/types.go にあるのは format と think です)。送っても黙って無視されるので、think=false を単独で送ったときと結果は変わりません。
切り分け
content が空文字で返る。 thinking が生成枠を使い切っている可能性があります。num_predict を絞ったまま thinking を有効にすると、content に一文字も到達しないまま done_reason: length で返ります。これは 0.31.1 でも 0.31.2 でも同じように起きるので、この症状だけを見て版境界の不具合と判断しないでください。レスポンスの thinking の長さと done_reason を見れば切り分けられます。0.31.2 に上げても空文字が続くなら、見るべきは版ではなく生成枠です。
format を渡していないのに JSON が返ってこない。 それは構造化出力ではなくプロンプトの問題で、この記事の対象外です。
/v1/ の OpenAI 互換エンドポイントを使っている。 errfix はこちらを実測していません。ただし 0.31.1 の openai/openai.go を読むと、response_format の JSON Schema は内部の Format へ、reasoning_effort は内部の Think へ変換され、/api/chat と同じリクエストに合流します。同じ症状が出る可能性があります。その場合も直すのはサーバの版です。
クライアント側の版を上げた。 修正はサーバの server/routes.go に入りました(PR #15901)。クライアントを更新しても症状は変わりません。確認すべきなのはサーバの /api/version が返す値です。
検証環境
ollama/ollama:0.31.1(再現)/ollama/ollama:0.31.2(消滅)- モデル
qwen3.5:9b、GPU 無し(CPU 推論) - Python クライアント
ollama==0.6.2 - リクエスト
POST /api/chat、stream: false、options: {"temperature": 0, "num_predict": 64, "num_ctx": 2048}
再現から修正までは errfix の検証ハーネスが機械的に確認しています。再現時にシグネチャが出ること、修正後に消えること、終了コードが 1 から 0 に変わることの3点です。上流の版・PR・機構の記述は harness の保証外で、GitHub の issue・PR・タグ差分・ソースで裏を取っています。