Temperature controls randomness in LLM output by scaling the logits before the softmax step. A temperature of 1.0 leaves the distribution unchanged. Values below 1.0 sharpen the distribution, making high-probability tokens more likely. Values above 1.0 flatten it, increasing the chance of lower-probability tokens. This guide walks through the mechanics, practical selection, and the mistakes engineers make when tuning this parameter in production.
The math behind temperature
The model outputs raw logits — unnormalized scores for each token in the vocabulary. The standard softmax converts these to probabilities:
def softmax(logits):
exp_logits = np.exp(logits - np.max(logits)) # numerical stability
return exp_logits / exp_logits.sum()
Temperature divides the logits before softmax:
def softmax_with_temperature(logits, temperature):
scaled = logits / temperature
exp_logits = np.exp(scaled - np.max(scaled))
return exp_logits / exp_logits.sum()
At temperature = 0.5, the model becomes more confident — the gap between the top token and the rest widens. At temperature = 2.0, the distribution flattens, and the model “hallucinates” more diverse (often nonsensical) continuations. At temperature = 0, the operation is undefined (division by zero), so implementations treat it as greedy decoding: argmax(logits).
Choosing temperature for your use case
There is no universal setting. The right value depends on what you’re building.
Code generation and structured output: 0.0 – 0.3
You want deterministic, syntactically valid output. A single wrong bracket breaks the parse. Use greedy (0.0) or near-greedy (0.1–0.2) for:
- SQL generation
- JSON/API payload construction
- Function calling argument extraction
- Code completion in IDEs
# OpenAI-compatible call
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
max_tokens=500,
)
Creative writing and brainstorming: 0.7 – 1.2
You want variety. The model should explore less obvious paths. This range works for:
- Marketing copy variants
- Story continuation
- Idea generation
- Synthetic data augmentation
General chat and reasoning: 0.3 – 0.7
Most conversational assistants live here. Low enough to stay coherent, high enough to avoid robotic repetition. Start at 0.5 and adjust based on evals.
Classification and extraction: 0.0
When the task has a single correct answer — sentiment, entity extraction, yes/no — any randomness is noise. Use temperature=0 (or the provider’s greedy equivalent) and pair it with top_p=1.0 to disable nucleus sampling.
Temperature interacts with top-p and top-k
Temperature is not the only sampling knob. Providers typically apply them in this order:
- Top-k: Keep only the
khighest-probability tokens. - Top-p (nucleus): Keep the smallest set of tokens whose cumulative probability exceeds
p. - Temperature: Scale the remaining logits, then sample.
If you set temperature=0.7 and top_p=0.1, the temperature operates on a heavily truncated distribution. The effective randomness is lower than temperature alone suggests. Conversely, top_p=1.0 (the default on most APIs) means temperature has full effect.
Practical rule: Pick one primary knob. For most teams, temperature is the intuitive lever. Fix top_p=1.0 and top_k=0 (disabled) unless you have a specific reason to constrain the candidate set.
# Explicitly disable top-p and top-k to isolate temperature
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.5,
top_p=1.0,
# top_k not in OpenAI API; some providers expose it
)
Common pitfalls
Treating temperature as a “creativity slider”
Temperature controls distribution sharpness, not semantic creativity. A high temperature on a factual QA task produces confident hallucinations, not creative insights. A low temperature on a poetry task produces repetitive loops, not “focused” creativity. Match the parameter to the task’s entropy requirements, not a vague creativity notion.
Assuming temperature=0 is fully deterministic
It is not. Sources of non-determinism remain:
- Floating-point non-determinism across GPU architectures or batch sizes
- Model updates (providers swap model weights without version bumps)
- Race conditions in distributed inference (token stream order)
- Logit processor side effects (repetition penalties, banned tokens)
If you need bitwise reproducibility, pin the model version, fix the seed (if the API exposes it), and run single-threaded. Even then, provider infrastructure changes can break it.
Using the same temperature across a pipeline
A RAG pipeline has stages with different entropy needs:
| Stage | Recommended temperature | Reason |
|---|---|---|
| Query rewriting | 0.0 | Deterministic expansion |
| Retrieval (if LLM-based) | 0.1 | Consistent ranking |
| Answer synthesis | 0.3–0.5 | Balanced coherence |
| Follow-up suggestion | 0.7 | Variety for user choice |
Hardcoding one value across all stages degrades at least one stage.
Ignoring token-level variance
Temperature affects per-token entropy. A 1000-token completion at temperature=0.7 accumulates massive sequence-level variance. Two runs diverge completely after 20–30 tokens. If you need consistent structure (e.g., “always output JSON with these keys”), low temperature alone is insufficient — use constrained decoding, grammar-based sampling, or post-generation validation.
Evaluating temperature systematically
Don’t guess. Run a small eval grid.
import json
from openai import OpenAI
client = OpenAI()
def evaluate_temperature(task_prompt, expected_pattern, temperatures=[0.0, 0.2, 0.5, 0.7, 1.0], n=5):
results = {}
for temp in temperatures:
passes = 0
for _ in range(n):
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": task_prompt}],
temperature=temp,
max_tokens=200,
)
output = resp.choices[0].message.content
# Custom validation per task
if validate(output, expected_pattern):
passes += 1
results[temp] = passes / n
return results
def validate(output, pattern):
# Example: check valid JSON with required keys
try:
data = json.loads(output)
return all(k in data for k in pattern)
except json.JSONDecodeError:
return False
# Run
task = "Extract name, email, company as JSON from: 'John Doe, john@acme.com, Acme Corp'"
print(evaluate_temperature(task, ["name", "email", "company"]))
Typical output pattern:
{0.0: 1.0, 0.2: 1.0, 0.5: 0.8, 0.7: 0.4, 1.0: 0.1}
The cliff varies by task. Find it empirically.
Temperature in streaming and caching contexts
When streaming, each token is sampled independently with the same temperature. The first token’s randomness cascades. If you cache the first N tokens (e.g., for speculative decoding or prefix caching), you lock in that randomness. Subsequent continuations from the cached prefix will be identical — which may be desirable (consistency) or undesirable (stuck in a bad branch).
Some gateways and providers expose a seed parameter. Combined with temperature=0, this gives reproducible streams. With temperature>0, the seed fixes the RNG state, but the distribution shape still depends on the model’s logits, which can vary by hardware batching.
# Reproducible sampling (provider support varies)
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.7,
seed=42, # OpenAI, Anthropic, and others support this
)
When to use temperature vs. other strategies
| Goal | Better than temperature alone |
|---|---|
| Enforce JSON schema | Constrained decoding / grammar-based sampling (e.g., llama.cpp GBNF, outlines, guidance) |
| Reduce repetition | Repetition penalty, presence_penalty, frequency_penalty |
| Factual accuracy | RAG, retrieval, verification loops — not lower temperature |
| Diverse few-shot examples | Explicit few-shot variation in prompt, not high temperature |
| Safety filtering | Post-generation classifiers, not temperature tuning |
Temperature is a blunt instrument. It shifts the entire distribution. Modern constrained decoding libraries let you keep temperature for fluency while guaranteeing structural validity. Use them.
Practical checklist for production
- Default to 0.3–0.5 for general chat. Log the temperature used with every request.
- Pin temperature per task type in your prompt registry or routing logic, not in the model call site.
- Run evals at three temperatures (low, medium, high) before shipping a new prompt.
- Expose temperature to power users via API, but hide it from general users — most cannot reason about it.
- Monitor entropy metrics: average token probability, unique n-gram ratio, repetition rate. Alert on drift.
- Document the model version alongside temperature in your eval artifacts. A model update shifts the effective temperature curve.
One concrete example: routing by task
If you run a gateway that handles multiple task types, route temperature server-side:
TASK_TEMPERATURES = {
"code_generation": 0.1,
"sql": 0.0,
"classification": 0.0,
"summarization": 0.3,
"chat": 0.5,
"creative_writing": 0.8,
"brainstorming": 1.0,
}
def route_request(task_type: str, user_temp: float | None) -> float:
# User override only for creative tasks
if task_type in ("creative_writing", "brainstorming") and user_temp is not None:
return max(0.0, min(2.0, user_temp))
return TASK_TEMPERATURES.get(task_type, 0.5)
This prevents a user from setting temperature=1.2 on SQL generation and getting invalid syntax, while still letting them explore variants for marketing copy.
Temperature controls randomness by reshaping the probability distribution the model emits. It is a necessary knob, not a sufficient one. Treat it like a hyperparameter: validate per task, log per request, and combine it with constrained decoding for structure. The engineers who ship reliable LLM features don’t tune temperature by feel — they measure it.