CATHODE COASTaccess to tools
9 min read

curlが空を返す — 思考モデルは完成品を持ったまま力尽きていた

Ollamaで27Bモデルを叩いたら、エラーも出ないのに応答が空。原因は思考モデルのトークン切れでした。しかも思考の中には完成した本文が入っていた話。

shareB!

はじめに

EC2のGPUインスタンスに27BクラスのLLMを載せ、速度の測定まで終わったところでした。あとは実際に生成された文章を見るだけ、という段階です。

curl -s http://localhost:11434/api/chat -d '{
  "model": "qwen3.6:27b",
  "messages": [{"role":"user","content":"桜の季節を舞台にした恋愛小説の冒頭を200文字程度で書いてください。"}],
  "stream": false,
  "options": {"num_predict": 300, "num_ctx": 8192}
}' | python3 -c "import json,sys; print(json.load(sys.stdin)['message']['content'])"

実行すると、何も出力されずにプロンプトが返ってきました。

エラーメッセージはありません。Pythonの例外も出ません。ただ空行が1つ出て終わりです。

最初はパイプかJSONパースを疑いました。しかし原因はまったく別のところにあり、しかも探していた文章は最初から存在していました

執筆時点(2026年7月)、Ollama v0.32.5 / qwen3.6:27b での記録です。


先にまとめ

  1. 思考モデルの応答は contentthinking に分かれる。 content だけを見ると「空」に見えることがあります。
  2. 思考は英語で行われていた。 日本語のプロンプトに日本語で答えるモデルが、思考パートだけ英語で構造化された分析をしていました。
  3. 思考の中には完成した本文があった。 モデルは200文字の小説を書き上げ、文字数を検算し、「出力の準備ができた」と宣言した直後にトークン上限に達していました。

対処

{
  "stream": false,
  "think": false,           トップレベルに置く(options の中ではない)
  "options": {"num_predict": 500}
}

これで113トークン、約8秒で完成した本文が返るようになりました。思考ありでは3,000トークン・約3分25秒を費やして、成果物ゼロでした。


実際にやったこと

症状: エラーなし、出力なし

まず切り分けのため、レスポンス全体を見ることにしました。content だけでなく、周辺のメタデータも含めて表示します。

curl -s http://localhost:11434/api/chat -d '{
  "model": "qwen3.6:27b",
  "messages": [{"role":"user","content":"桜の季節を舞台にした恋愛小説の冒頭を200文字程度で書いてください。"}],
  "stream": false,
  "options": {"num_predict": 300, "num_ctx": 8192}
}' | python3 -c "
import json,sys
d=json.load(sys.stdin); m=d.get('message',{})
print('done_reason:', d.get('done_reason'))
print('eval_count :', d.get('eval_count'))
print('--- thinking ---'); print((m.get('thinking') or '(なし)')[:400])
print('--- content  ---'); print(repr(m.get('content')))
"

結果です。

done_reason: length
eval_count : 300
--- thinking ---
Here's a thinking process:
1.  **Analyze User Input:**
   - **Topic:** Romance novel opening scene (恋愛小説の冒頭)
   - **Setting:** Cherry blossom season (桜の季節)
   - **Length:** Approximately 200 Japanese characters (200文字程度)
2.  **Key Elements to Include:**
   - Cherry blossoms/spring atmosphere
   - Romantic tone/mood
   - Opening hook/scene setup
   - Character introduction or interaction (subtle)
--- content  ---
''

content は空文字列。しかし thinking には内容が詰まっています。

qwen3.6は思考モデルで、応答を推論パートと本文パートに分けて返します。Ollamaはこれを別々のフィールドに入れるため、content だけを取りに行くと何も無いように見えていました。

done_reason: lengtheval_count: 300 から、状況ははっきりします。指定した上限300トークンをすべて思考に使い切り、本文を書き始める前に打ち切られていたわけです。

伏線は測定データにあった

思えば、速度測定の結果に兆候が出ていました。

num_ctx出力tok
4096300
8192300
16384300
32768300
65536300

全行がちょうど300。 一度も自然終了していません。プロンプトも条件も同じなら生成量は多少ばらつくはずで、全部が上限に張り付いているのは異常でした。

このとき気づいていれば、もっと早く原因に辿り着けたはずです。done_reason を表示に含めていなかったのが反省点でした。

思考が英語で行われている

もう一つ、思考の中身自体が興味深いものでした。

Here's a thinking process:
1.  **Analyze User Input:**
   - **Topic:** Romance novel opening scene (恋愛小説の冒頭)

プロンプトは日本語、期待する出力も日本語。しかし思考は完全に英語です。 しかも箇条書きで構造化されており、要件定義書のような体裁になっています。

日本語話者としては少し不思議な感覚ですが、トークン効率の観点では合理的かもしれません。英語は1トークンあたりの情報量が多く、日本語は1文字がほぼ1トークンになります。思考を英語で行えば、同じ内容をより少ないトークンで処理できます。


思考の中に、完成品があった

上限を3,000トークンまで引き上げて再実行しました。

curl -s http://localhost:11434/api/chat -d '{
  "model": "qwen3.6:27b",
  "messages": [{"role":"user","content":"桜の季節を舞台にした恋愛小説の冒頭を200文字程度で書いてください。"}],
  "stream": false,
  "options": {"num_predict": 3000, "num_ctx": 8192}
}' | python3 -c "
import json,sys
d=json.load(sys.stdin); m=d.get('message',{})
th=m.get('thinking') or ''
print('reason:', d.get('done_reason'), '/ total tokens:', d.get('eval_count'))
print('thinking length:', len(th), 'chars')
print('---')
print(m.get('content'))
"
reason: length / total tokens: 3000
thinking length: 5325 chars
---

3,000トークン使って、やはり本文は空。 所要時間は約3分25秒でした。

10倍に増やしても届かない。何をそんなに考えているのか、思考の末尾を確認しました。

... | python3 -c "
import json,sys
th=json.load(sys.stdin)['message'].get('thinking') or ''
print('=== 末尾800字 ==='); print(th[-800:])
"

出てきたものがこれです。

上げると、隣の席に知らない青年がいた。彼の横顔に桜の影が落ちている。
目が合った瞬間、息が止まったような静けさが訪れた。「今年も桜だね」と心の中で呟く。
その言葉は届かないまま、胸の奥でゆっくりと溶けていった。春の光が二人を包み込むように、物語は始まる。
 
   Added: 春の光が二人を包み込むように、物語は始まる。(21 chars + punctuation = ~23)
   Total now ~188. Close enough to 200. Let's count precisely one more time or adjust to hit exactly ~200.
   Actually, Japanese character count usually includes punctuation. I'll aim for 195-205.
   Let's tweak:
   桜吹雪が舞い散る四月の朝、駅前のベンチに独りで座っていた。柔らかな風が枝を揺らし、
   淡い花びらが肩越しに降ってくる。ふと視線を上げると、隣の席に知らない青年がいた。
   (中略)
   Count: ~198 including punctuation. Perfect.
   Check tone: Romantic, sakura season setting, novel opening feel, hints at meeting/connection.
   All constraints met. Ready to output.✅

All constraints met. Ready to output.✅

モデルは小説を書き上げ、3回書き直し、文字数を数え、「句読点を含めるべきか」まで検討したうえで、「出力の準備ができた」と宣言していました。

その直後に、3,000トークンの上限に到達しています。

つまり何が起きていたか

実態
思考パート完成した本文を含んでいた(3回の推敲つき)
content
原因「出力する」の一歩手前でトークン切れ

成果物は存在していたのに、受け取れなかった。 ゴールテープの直前で燃料切れを起こしたような状態です。

「暴走した」でも「失敗した」でもありません。モデルは仕事を終えていたのに、それを渡す余力が残っていなかったというのが正確な描写です。

日本語の文字数を英語で数え直しているのも印象的でした。「200文字程度」という指示に律儀に応えようとした結果、検算にトークンを費やして力尽きています。思考が5,325文字という異常な長さになったのは、英語の分析フレームの中に日本語の本文候補が繰り返し埋め込まれていたためでした。


思考をオフにする

Ollamaでは think パラメータで制御できます。options の中ではなくトップレベルに置く点に注意が要ります。

curl -s http://localhost:11434/api/chat -d '{
  "model": "qwen3.6:27b",
  "messages": [{"role":"user","content":"桜の季節を舞台にした恋愛小説の冒頭を200文字程度で書いてください。"}],
  "stream": false,
  "think": false,
  "options": {"num_predict": 500, "num_ctx": 8192}
}' | python3 -c "
import json,sys
d=json.load(sys.stdin); m=d.get('message',{})
print('reason:', d.get('done_reason'), '/ tokens:', d.get('eval_count'))
print('---')
print(m.get('content'))
"
reason: stop / tokens: 113
---
桜吹雪が舞う公園ベンチ。彼が差し出した温かい紅茶カップから、白煙が静かに立ち昇る。
数年ぶりの再会だった。彼は一言、「変わりないね」と呟き、目元を柔らかにした。
私は答えに窮し、ただ掌を温めるようにカップを握りしめる。季節は巡って花は散るけれど、
この瞬間だけ時は止まっているかのよう。微かに震える指先が、今、新しい物語の始まりを告げていた。

113トークン、done_reason: stop(自然終了)、約8秒。

約170文字で、「200文字程度」という指示もおおむね守られています。情景から入り、再会を示し、心情で締める構成になっていて、文章としても破綻がありません。

コストの比較

思考オフ思考オン
出力トークン1133,000(上限で打ち切り)
終了理由stoplength
本文✅ 完成❌ 空
所要時間約8秒約205秒
概算コスト約0.4円約10.3円

26倍のトークンと3分25秒を費やして、成果物はゼロ。

(コストはg6.xlargeの東京オンデマンド $1.1672/時から算出。1ドル155円換算)


前回のモデルとの比較

前回、メモリ1GBのVPSで qwen2.5:0.5b に同じ趣旨のプロンプトを投げています。並べると差が明確です。

qwen2.5:0.5b (CPU)qwen3.6:27b (GPU)
「◯◯文字程度で」652トークン書き続けた113トークンで自然終了
日本語「その美しい景色ととに」「過ごすことがでました」破綻なし
構成同じ表現の反復情景→再会→心情
速度13.9 tok/秒14.7 tok/秒

速度はほぼ同じでも、1トークンあたりの価値がまるで違います。

0.5Bは指示を無視して652トークン出力し、内容も反復が目立ちました。27Bは113トークンで的確に終わっています。必要なトークン数が6分の1なら、実質6倍速いと言えます。

「トークン/秒」だけを比較指標にしていると、この差は見えません。


ハマりどころまとめ

症状原因対処
応答が空、エラーも無い思考モデルで content が空thinking フィールドを確認する
出力トークンが常に上限ちょうど思考でトークンを使い切っているdone_reason を必ず表示する
上限を上げても本文が出ない思考が長大(今回は5,325文字)think: false にする
think を指定しても効かないoptions の中に書いているトップレベルに置く
生成が異様に遅い思考に時間を使っている思考オフで26倍の差

やってみての所感

「答えはそこにあった」

今回いちばん印象に残ったのは、探していたものが最初から目の前にあったことでした。

content が空だったので「生成に失敗した」と解釈しましたが、実際には完成した文章が thinking の中にありました。しかもモデル自身が「準備ができた」と宣言していた。症状の解釈を間違えると、正解の隣を素通りしてしまいます。

前回の記事でも、生成に61秒かかったのを「遅い」と解釈しかけて、実際は「書きすぎていた」だけでした。症状と原因を短絡させないというのは、何度失敗しても身につかない類の教訓なのかもしれません。

思考モデルで応答が空のときは、thinking の末尾を必ず見る。 これは今後も使える指針になりそうです。

推論モデルは常に賢いわけではない

「推論モデルのほうが高品質」という理解でいましたが、今回はそう単純ではありませんでした。

品質そのものは思考ありのほうが上だったはずです。3回推敲し、文字数を検算し、トーンまで確認していました。問題はそれに26倍のコストを払う価値があるかという点です。

思考モデルは検証可能な答えを持つ問題に向けて調整されている印象があります。コーディングや数学のように「正しいかどうか」が判定できるタスクでは、推敲するほど答えに近づきます。一方で創作には正解がなく、「もっと良い表現があるのでは」という推敲が止まる契機を持ちません。

思考の冒頭が英語の構造化されたチェックリストだったのも、そうした最適化の名残に見えました。

実運用での判断

日常的に使うチャットとしては、思考オフを既定にするという結論になりました。3分待って何も出てこないのは、実用上の致命傷です。

ただし思考を捨てるのではなく、必要なときだけ有効にする設計にしておくのが妥当でしょう。そのうえで、思考ありの場合は上限を桁違いに確保する必要があります。今回の例では3,000でも足りていません。

もう一つ気づいたのは、思考トークンも課金対象だということです。自前ホストなら時間課金ですが、APIを使うなら思考分がそのまま請求されます。本文200トークンのために思考3,000トークンを払う構造は、使用量を可視化しておかないと気づけません。


まとめ

curl が空を返す。エラーは無い。原因は思考モデルのトークン切れで、探していた文章は thinking の中に完成した状態で入っていました。

対処は think: false を渡すだけです。113トークン、8秒で的確な文章が返るようになりました。思考ありでは3,000トークン・3分25秒をかけて成果物ゼロだったので、差は26倍以上です。

思考モデルを扱うときは、レスポンス全体を見る癖をつけたほうがよさそうです。content だけを取りに行くコードは、モデルが思考モデルに切り替わった瞬間に静かに壊れます。エラーも出さずに空を返すのが、いちばん厄介な壊れ方でした。


参考リンク