氷河期世代のクラウドエンジニア、terralien です。AIが書いたコードをレビューしていると、temperature=0.7 という行に一番よく出くわします。なぜ 0.7 なのかを聞いても、たぶん誰も答えられません。
そして厄介なことに、出力が間違っていたときの最初の一手も、だいたい「じゃあ temperature を下げよう」なんですよね。この記事は、それをやると何が起きるかを20回ずつ数えた記録です。
数字はすべて手元の実測です。MacBook Pro(Apple M5 Pro / 64GB / macOS 26.6.1)+ Ollama 0.32.14 + gemma4:latest(8.0B・Q4_K_M)、クライアントは Python 3.11.2。2026-08-29 に計測しました。推測値は書いていません。
用途 ── つまみが決めるのは「品質」ではなく「ばらつき」
先に結論を出します。まったく同じ問いを、温度だけ変えて20回ずつ投げた結果です。問いは「税込合計はいくらか」で、正解は 14,168 円(1,280×7 + 980×4 = 12,880、×1.1)です。
| 設定 | 正答(20回中) | 出てきた答えの種類 | いちばん多かった答え |
|---|---|---|---|
temperature=0 | 0 | 1 | 11888(20回とも) |
temperature=0.7 | 0 | 16 | 11704(3回) |
temperature=1.5 | 0 | 20 | ばらばら(全部違う) |
温度をどう動かしても、正答は 0 のままです。動いたのは間違い方の散らばりだけでした。
ダーツの手ブレの話に似ています。温度は「腕のブレの大きさ」を決めるつまみで、的の位置は決めません。狙いがそもそも隣の的を向いているなら、腕を固めれば固めるほど、同じ外れ方が正確に再現されるだけです。temperature=0 で20回とも 11888 が出たのは、まさにこれです。
temperature は正解率のつまみではありません。ここを取り違えたまま「0 にしたので大丈夫です」と書かれたコードは、監査を通してはいけません。
代表要素 ── 3つのつまみ
| 引数 | 何をするか | 既定値の罠 |
|---|---|---|
temperature(0〜2 程度) | 次の1トークンを選ぶときの確率分布を、平らにするか尖らせるか。0 は「いちばん確率の高いものを必ず選ぶ」 | 0 は「正しい」ではなく「毎回同じ」。多くの SDK の既定は 1.0 で、AIが書くコードは 0.7 を置きがち |
top_p(0〜1) | 確率の高い順に足していって、合計が top_p を超えたところで候補を打ち切る | temperature と効き目が重なる。両方いじると片方が打ち消される(後述) |
max_tokens / num_predict | 出力トークンの上限 | 超えても例外は出ない。finish_reason="length" が付いた、切れた文字列が正常応答として返る |
AIが書く例 ── 実際に書かせたもの
手元の Qwen3.8:27b に「議事録を要約する関数を書いてください」と頼んだら、これが返ってきました(一字も直していません)。
from openai import OpenAI
def summarize_minutes(text: str, model: str = "gpt-4o") -> str:
client = OpenAI()
response = client.chat.completions.create(
model=model,
messages=[
{
"role": "system",
"content": "あなたは議事録の要約専門家です。提供された議事録テキストを簡潔かつ正確に要約してください。"
},
{
"role": "user",
"content": f"以下の議事録を要約してください:\n\n{text}"
}
],
temperature=0.3
)
return response.choices[0].message.content
つまみは temperature=0.3 の1つだけ。max_tokens は書かれていません。そして finish_reason を誰も見ていません。
max_tokens を書かないのは、一見すると「切り詰めないので安全」に見えます。実際は逆で、モデル側の既定上限に当たったときに、同じように黙って切れます。しかも上限が何トークンかはモデルを変えるたびに変わるので、切れる長さがコードのどこにも書いていない状態になります。長い議事録が来たら、この関数は途中で終わった要約を、成功として返します。
監査① ── temperature=0 は「正しい」ではなく「毎回同じ」
さっきの表の続きです。temperature=0 は再現性のためのつまみであって、正しさのつまみではありません。
では何が効いたのか。同じ問いに、思考(reasoning)を有効にして temperature=0 のまま投げ直しました。
| 設定 | 正答 |
|---|---|
temperature=0・思考なし | 0 / 20 |
temperature=0.7・思考なし | 0 / 20 |
temperature=1.5・思考なし | 0 / 20 |
temperature=0・思考あり | 10 / 10(全部 14168) |
温度は1ミリも動かしていません。動かしたのは途中計算をさせるかどうかでした。
つまり読むべき順序はこうです。出力が間違っているとき、まず疑うのは温度ではなく、タスクがそのモデルの1発では解けない形になっていないか。ここを飛ばして温度を下げると、間違いの再現性だけが上がります。
温度が効く場面もちゃんとあります。「ブログのキャッチコピーを1つ」を10回頼んだところ、temperature=0 では10回とも同じ1文、temperature=1.0 では10回とも別の文でした。候補が欲しい用途では、温度0は道具として壊れています。 抽出・分類・要約は下げる、案出しは上げる。分けるのはタスクであって、好みではありません。
監査② ── top_p を一緒にいじると、temperature が消えます
これが2つめの罠です。AIが書いたコードには、両方が並んで置かれていることがよくあります。
| 設定 | 出てきた答えの種類(20回中) | いちばん多かった答え |
|---|---|---|
temperature=1.5 / top_p=1.0 | 20(全部違う) | ― |
temperature=1.5 / top_p=0.1 | 1 | 11888(20回とも) |
temperature=0 / top_p=1.0 | 1 | 11888(20回とも) |
top_p=0.1 を足した瞬間、temperature=1.5 の出力が temperature=0 とまったく同じものに戻りました。候補を上位1割に絞ってしまうと、そこにはもう最有力候補しか残っていないので、温度で分布を平らにしても選ばれるものが変わらないんです。
読むときのルールは単純です。
| コードの見た目 | 判定 |
|---|---|
temperature だけ指定 | ○ 意図が読める |
top_p だけ指定 | ○ 意図が読める |
| 両方に既定値でない値が入っている | ❌ どちらが効いているか誰にも分からない。片方を消す |
| 「両方下げたので確実です」というコメント付き | ❌ 二重に絞っているだけ。一方は無意味 |
監査③ ── max_tokens は静かに切ります
3つめは、例外が飛ばないので気づけないやつです。同じ抽出タスクを JSON で返させ、max_tokens だけを変えて10回ずつ通しました。
max_tokens | JSON としてパースできた | finish_reason="length" | 切れた末尾 |
|---|---|---|---|
| 16 | 0 / 10 | 10 / 10 | { "date": "2026年8 |
| 32 | 0 / 10 | 10 / 10 | { "date": "...", "amount": "1,284 |
| 64 | 10 / 10 | 0 / 10 | ― |
| 256 | 10 / 10 | 0 / 10 | ― |
max_tokens=16 のとき、API は HTTP 200 を返しています。例外は飛びません。response.choices[0].message.content にはちゃんと文字列が入っていて、それが { "date": "2026年8 なだけです。この文字列を json.loads() に渡した側が、遠く離れた場所で JSONDecodeError を出します。
だから読むところは1つです。
resp = client.chat.completions.create(..., max_tokens=1000)
# ★ここが無いコードは、切れた出力を正常系として下流に流す
if resp.choices[0].finish_reason == "length":
raise ValueError("出力が max_tokens で打ち切られました")
return resp.choices[0].message.content
監査④ ── 思考する設定では、max_tokens は本文の前に消えます
最近のモデル特有の罠です。思考(reasoning)が有効なとき、max_tokens は思考ぶんも含めた上限として効きます。
max_tokens | 消費トークン | finish_reason | 思考の長さ | 本文 |
|---|---|---|---|---|
| 200 | 200 | length | 262文字 | 空文字 |
| 600 | 258 | stop | 332文字 | 14168 |
| 1500 | 258 | stop | 332文字 | 14168 |
max_tokens=200 のとき、200トークンぶんきっちり課金されて、返ってきた本文は空文字です。エラーではありません。「成功したリクエスト」として集計されます。
思考ありのモデルに切り替えたとたん出力が空になる、という報告が来たら、まずここを見ます。上限を上げれば直りますが、上げすぎても意味はありません。600 と 1500 で結果が同じなのは、必要なだけ使って止まっているからです。上限は蛇口の太さであって、水の量ではありません。
隣で読むもの
| 記事 | ここと繋がるところ |
|---|---|
| JSON で返ってきたから正しい、ではありません | 切れた JSON をどこで止めるか。finish_reason の次に見る層の話です |
| ストリーミング応答を読む | ストリーミングでは finish_reason も usage も、意識しないと手に入りません |
チートシート
| 見たもの | 疑うこと | 直し方 |
|---|---|---|
temperature=0.7(根拠なし) | タスクに合っているか | 抽出・分類は 0 付近/案出しは 1.0 付近 |
| 「間違うので温度を下げた」 | 正解率は上がらない(実測 0/20 のまま) | 手順を分ける・思考を有効にする・入力を整える |
temperature と top_p の両方 | 片方が打ち消している | どちらか一方だけ残す |
top_p=0.1 | 実質 temperature=0 と同じ出力 | 意図がそれなら温度側で書く |
max_tokens があるのに finish_reason を見ていない | 切れた出力が正常系で流れる | == "length" で弾く |
思考ありなのに max_tokens が小さい | 本文が空で成功する | 思考ぶんを上乗せする |
案出しなのに temperature=0 | 10回頼んで1種類しか出ない | 上げる |
読めるようになったか、ひとつだけ確認
Q. 抽出処理の答えが間違っています。temperature を 0.7 から 0 に下げるべきですか?
正解率は変わりません。実測では 0.7 でも 0 でも正答 0/20 で、下げたことで同じ誤答が20回そろって出るようになっただけでした。下げる価値があるのは「毎回結果が違って検証できない」という別の困りごとのほうです。答えを合わせにいくなら、つまみではなく手順を疑います。
……と書いておいてなんですが、こちらも去年まで、出力が安定しないと聞けば反射で温度を下げていました。「安定した」と報告して、実は同じ間違いが安定していただけ、という可能性を今になって思い出しています。当時の関係者の方、すみませんでした。