Temperature и sampling:
управление генерацией LLM
Почему одинаковый промпт дает разные ответы каждый раз? Как сделать LLM предсказуемым для агентов и живым для чата? Разбираем temperature, top-p, top-k и другие параметры на уровне логитов.
Как LLM предсказывает следующий токен
LLM — это по сути очень сложная функция f(контекст) → вероятности_всех_токенов.
Для каждого следующего токена модель выдаёт логит (raw score) для каждого слова в словаре (50 000+ токенов).
Затем softmax превращает логиты в вероятности.
Это вся цепочка. Temperature и другие параметры вмешиваются до sampling — они изменяют форму распределения вероятностей.
Представим, что модель генерирует продолжение фразы «Функция возвращает» и получила такое распределение:
NoneTrueFalseсписокстроку
При temperature=0 мы всегда выберем None (argmax).
При temperature=1 выбираем случайно по этому распределению.
При temperature=2 распределение «размажется» — шанс редких токенов вырастет.
Temperature: от детерминизма до хаоса
Temperature — главный параметр управления «случайностью». Формула проста:
import numpy as np
def apply_temperature(logits: list[float], temperature: float) -> list[float]:
"""Применяем temperature к логитам перед softmax."""
if temperature == 0:
# Детерминированный режим: argmax
max_idx = np.argmax(logits)
result = [-1e9] * len(logits)
result[max_idx] = 0.0
return result
return [l / temperature for l in logits]
def softmax(logits: list[float]) -> list[float]:
e = np.exp(np.array(logits) - np.max(logits)) # стабильный softmax
return (e / e.sum()).tolist()
# Пример: одни логиты, разные temperature
logits = [4.2, 2.1, 1.8, 0.9, 0.3]
for t in [0.0, 0.3, 0.7, 1.0, 1.5, 2.0]:
probs = softmax(apply_temperature(logits, t))
print(f"T={t:.1f}: {[f'{p:.3f}' for p in probs]}")
# T=0.0: ['1.000', '0.000', '0.000', '0.000', '0.000'] ← всегда первый
# T=0.3: ['0.988', '0.009', '0.003', '0.000', '0.000'] ← почти детерминировано
# T=0.7: ['0.854', '0.084', '0.047', '0.012', '0.004'] ← умеренное разнообразие
# T=1.0: ['0.655', '0.142', '0.106', '0.060', '0.037'] ← распределение из модели
# T=1.5: ['0.474', '0.191', '0.163', '0.107', '0.065'] ← заметная случайность
# T=2.0: ['0.374', '0.209', '0.189', '0.143', '0.086'] ← почти равномерно
API-параметр
temperature=0 обычно реализован как argmax (greedy decoding) и действительно детерминирован.
temperature=0.0001 всё ещё делает sampling, просто с очень острым распределением.
Для воспроизводимости используйте именно 0.
Что происходит при разных значениях
| Temperature | Поведение | Когда использовать |
|---|---|---|
0 |
Детерминированный, всегда argmax | Тесты, отладка, fact extraction |
0.1–0.3 |
Почти детерминированный, минимальная вариативность | Генерация кода, SQL, JSON |
0.5–0.7 |
Умеренная случайность | Агенты с tool use, анализ данных |
0.8–1.0 |
Стандартное поведение | Чат, объяснения, саммари |
1.2–1.5 |
Заметная случайность | Brainstorming, вариации идей |
> 1.5 |
Хаотичный, часто несвязный текст | Почти никогда (разве что эксперименты) |
Top-p (nucleus sampling)
Top-p — более умный способ ограничить пространство токенов. Вместо «взять top-k токенов» берём минимальное подмножество токенов, чья суммарная вероятность ≥ p.
def top_p_filter(probs: list[float], p: float) -> list[float]:
"""Оставляем только токены, суммарная вероятность которых <= p."""
# Сортируем по убыванию вероятности
indexed = sorted(enumerate(probs), key=lambda x: x[1], reverse=True)
cumulative = 0.0
keep_indices = set()
for idx, prob in indexed:
cumulative += prob
keep_indices.add(idx)
if cumulative >= p:
break # достаточно токенов набрано
# Обнуляем невыбранные токены
filtered = [prob if i in keep_indices else 0.0
for i, prob in enumerate(probs)]
# Ренормализуем
total = sum(filtered)
return [p / total for p in filtered]
# Пример
probs = [0.62, 0.18, 0.09, 0.05, 0.03, 0.02, 0.01]
tokens = ["None", "True", "False", "список", "строку", "dict", "int"]
for p_val in [0.7, 0.9, 0.95, 1.0]:
filtered = top_p_filter(probs, p_val)
kept = [t for t, fp in zip(tokens, filtered) if fp > 0]
print(f"top_p={p_val}: keeping {kept}")
# top_p=0.70: keeping ['None', 'True']
# top_p=0.90: keeping ['None', 'True', 'False', 'список']
# top_p=0.95: keeping ['None', 'True', 'False', 'список', 'строку']
# top_p=1.00: keeping ['None', 'True', 'False', 'список', 'строку', 'dict', 'int']
При чёткой уверенности модели (
None=62%) top-p=0.9 возьмёт 4 токена.
При неуверенности (10 токенов по ~10%) top-p=0.9 возьмёт все 10.
Top-k всегда берёт фиксированное число и не адаптируется к ситуации.
Рекомендации по top-p
| top_p | Эффект | Применение |
|---|---|---|
0.7 | Очень ограниченное nucleus | Строгий код, JSON |
0.9 | Стандартный nucleus | Агенты, чат |
0.95 | Широкий nucleus | Creative writing |
1.0 | Нет фильтрации (только temperature) | По умолчанию в большинстве API |
Top-k sampling
Top-k оставляет только k наиболее вероятных токенов, остальные обнуляет. Менее гибкий, чем top-p, но всё ещё используется (особенно в Google/Gemini API).
def top_k_filter(probs: list[float], k: int) -> list[float]:
"""Оставляем только top-k токенов."""
if k >= len(probs):
return probs
# Находим k-й по величине порог
threshold = sorted(probs, reverse=True)[k - 1]
filtered = [p if p >= threshold else 0.0 for p in probs]
# Ренормализуем
total = sum(filtered)
return [p / total for p in filtered]
top_k как параметр. Для OpenAI/Anthropic используйте top_p.
Repetition penalty и frequency penalty
Без штрафов модель может зациклиться: «Привет, привет, привет, привет…». Два основных параметра борются с этим:
# OpenAI параметры:
# frequency_penalty: [-2.0, 2.0] — штраф пропорционален частоте токена в тексте
# presence_penalty: [-2.0, 2.0] — штраф за сам факт появления токена (бинарный)
# При генерации следующего токена:
# adjusted_logit[token] = logit[token]
# - frequency_penalty * count(token_in_generated_text)
# - presence_penalty * (1 if token_appeared else 0)
# Пример — если "функция" встретилась 3 раза:
# frequency_penalty=0.5: logit -= 0.5 * 3 = -1.5
# presence_penalty=0.5: logit -= 0.5 * 1 = -0.5
# Anthropic НЕ имеет этих параметров — борьба с повторами встроена в обучение
| Параметр | Диапазон | Эффект > 0 | Эффект < 0 |
|---|---|---|---|
frequency_penalty |
-2 … 2 | Меньше повторов слов | Больше повторов (редко нужно) |
presence_penalty |
-2 … 2 | Больше новых тем | Фокус на уже упомянутом |
Если установить
frequency_penalty=1.5, модель будет избегать повтора переменных,
ключевых слов, скобок — и начнёт генерировать синтаксически некорректный код.
Для кода держите frequency_penalty <= 0.3.
Seed: воспроизводимые результаты
Параметр seed позволяет получать одинаковые ответы при одинаковом промпте.
Полезно для тестирования и отладки.
import openai
client = openai.AsyncOpenAI()
async def ask_with_seed(question: str, seed: int = 42) -> str:
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": question}],
temperature=1.0, # даже при temperature=1 seed даёт стабильность
seed=seed,
)
# Проверяем, был ли ответ действительно детерминированным
# (API гарантирует это только при system_fingerprint совпадении)
print(f"System fingerprint: {response.system_fingerprint}")
return response.choices[0].message.content
# Несколько запусков с одинаковым seed должны давать одинаковый ответ
# (при одинаковом system_fingerprint — версии модели на сервере)
Однако полная воспроизводимость не гарантируется из-за флоатинговой арифметики на GPU. На практике при seed + temperature=0 почти всегда получаете одинаковый ответ.
Параметры в OpenAI и Anthropic API
import openai
client = openai.AsyncOpenAI()
response = await client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Напиши функцию сортировки на Python"},
],
# ── Основные sampling параметры ──
temperature=0.2, # 0-2, по умолчанию 1. Низкое для кода
top_p=0.95, # 0-1, по умолчанию 1. Nucleus sampling
# ── Штрафы ──
frequency_penalty=0.0, # -2 до 2, по умолчанию 0
presence_penalty=0.0, # -2 до 2, по умолчанию 0
# ── Управление длиной ──
max_tokens=1024, # максимум новых токенов
# ── Воспроизводимость ──
seed=42,
# ── Стоп-последовательности ──
stop=["```", "END"], # остановиться на этих строках
# ── Количество вариантов ──
n=1, # сколько вариантов генерировать
)
text = response.choices[0].message.content
usage = response.usage # prompt_tokens, completion_tokens, total_tokens
import anthropic
client = anthropic.AsyncAnthropic()
response = await client.messages.create(
model="claude-opus-4-6",
# ── Основные sampling параметры ──
temperature=0.2, # 0-1 у Anthropic (не 0-2!), по умолчанию 1
top_p=0.95, # nucleus sampling
top_k=50, # Anthropic поддерживает top_k
# ── Управление длиной ──
max_tokens=1024, # ОБЯЗАТЕЛЬНЫЙ параметр у Anthropic
# ── Стоп-последовательности ──
stop_sequences=["```", "END"],
messages=[
{"role": "user", "content": "Напиши функцию сортировки на Python"}
],
system="You are a helpful coding assistant.",
)
text = response.content[0].text
usage = response.usage # input_tokens, output_tokens
OpenAI:
temperature от 0 до 2.Anthropic:
temperature от 0 до 1.При портировании кода между API не забывайте это нормировать.
Сравнительная таблица параметров
| Параметр | OpenAI | Anthropic | Gemini |
|---|---|---|---|
temperature |
0–2, default 1 | 0–1, default 1 | 0–2, default 1 |
top_p |
✅ 0–1 | ✅ 0–1 | ✅ 0–1 |
top_k |
❌ | ✅ | ✅ |
frequency_penalty |
✅ -2…2 | ❌ | ❌ |
presence_penalty |
✅ -2…2 | ❌ | ❌ |
seed |
✅ | ✅ (beta) | ❌ |
max_tokens |
optional | required | optional |
stop |
✅ до 4 строк | ✅ stop_sequences | ✅ stop_sequences |
Готовые пресеты для разных задач
В production обычно не подбирают параметры вручную — используют проверенные пресеты:
top_p=1.0
seed=42
top_p=0.9
top_p=0.9
top_p=0.95
presence_penalty=0.5
from dataclasses import dataclass
from enum import Enum
from typing import Optional
@dataclass
class SamplingConfig:
temperature: float = 1.0
top_p: float = 1.0
top_k: Optional[int] = None
frequency_penalty: float = 0.0
presence_penalty: float = 0.0
seed: Optional[int] = None
max_tokens: int = 1024
class SamplingPreset(Enum):
CODE = SamplingConfig(temperature=0.0, seed=42)
AGENT = SamplingConfig(temperature=0.3, top_p=0.9)
CHAT = SamplingConfig(temperature=0.7, top_p=0.9)
ANALYSIS = SamplingConfig(temperature=0.5, top_p=0.9)
CREATIVE = SamplingConfig(temperature=1.0, top_p=0.95, presence_penalty=0.5)
BRAINSTORM = SamplingConfig(temperature=1.3, top_p=0.95)
def config_to_openai(cfg: SamplingConfig) -> dict:
"""Конвертируем конфиг в kwargs для OpenAI API."""
params = {
"temperature": cfg.temperature,
"top_p": cfg.top_p,
"max_tokens": cfg.max_tokens,
}
if cfg.frequency_penalty != 0:
params["frequency_penalty"] = cfg.frequency_penalty
if cfg.presence_penalty != 0:
params["presence_penalty"] = cfg.presence_penalty
if cfg.seed is not None:
params["seed"] = cfg.seed
return params
def config_to_anthropic(cfg: SamplingConfig) -> dict:
"""Конвертируем конфиг в kwargs для Anthropic API.
Важно: temperature нормируем в диапазон 0-1.
"""
params = {
"temperature": min(cfg.temperature, 1.0), # Anthropic max = 1.0
"max_tokens": cfg.max_tokens,
}
if cfg.top_p < 1.0:
params["top_p"] = cfg.top_p
if cfg.top_k is not None:
params["top_k"] = cfg.top_k
return params
# Использование
import openai
import anthropic
async def generate_code(prompt: str) -> str:
client = openai.AsyncOpenAI()
preset = SamplingPreset.CODE.value
response = await client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
**config_to_openai(preset),
)
return response.choices[0].message.content
async def analyze_document(text: str) -> str:
client = anthropic.AsyncAnthropic()
preset = SamplingPreset.ANALYSIS.value
response = await client.messages.create(
model="claude-opus-4-6",
messages=[{"role": "user", "content": text}],
**config_to_anthropic(preset),
)
return response.content[0].text
Sampling в агентах: особые случаи
Проблема: агент циклится
При низком temperature агент может зациклиться — постоянно выбирать один и тот же инструмент или генерировать одинаковые мысли. Решение: динамически повышать temperature при обнаружении петли.
import hashlib
from collections import deque
class AdaptiveAgent:
"""Агент с автоматической адаптацией sampling параметров."""
def __init__(self, base_temperature: float = 0.3):
self.base_temperature = base_temperature
self.recent_responses: deque[str] = deque(maxlen=5)
self.consecutive_repeats: int = 0
def _response_hash(self, text: str) -> str:
"""Хэш первых 100 символов для обнаружения повторов."""
return hashlib.md5(text[:100].encode()).hexdigest()[:8]
def _detect_loop(self, response: str) -> bool:
"""Обнаруживаем, если агент генерирует похожие ответы."""
h = self._response_hash(response)
if h in [self._response_hash(r) for r in self.recent_responses]:
self.consecutive_repeats += 1
return self.consecutive_repeats >= 2
self.consecutive_repeats = 0
return False
def _current_temperature(self) -> float:
"""Динамически повышаем temperature при обнаружении цикла."""
bump = min(self.consecutive_repeats * 0.2, 0.8)
return min(self.base_temperature + bump, 1.0)
async def step(self, messages: list[dict], client) -> str:
temp = self._current_temperature()
if temp > self.base_temperature:
print(f"⚠️ Обнаружен цикл, повышаем temperature: {temp:.1f}")
response = await client.messages.create(
model="claude-opus-4-6",
messages=messages,
max_tokens=512,
temperature=temp,
)
text = response.content[0].text
self.recent_responses.append(text)
if self._detect_loop(text):
print("🔄 Агент застрял, сбрасываем контекст...")
# В реальном агенте здесь можно сжать историю или добавить
# системный промпт "Try a different approach"
return text
Проблема: недетерминированные тесты агента
import os
from contextlib import contextmanager
@contextmanager
def deterministic_mode():
"""Контекст-менеджер для детерминированного поведения агента."""
original = os.environ.get("AGENT_TEMPERATURE")
os.environ["AGENT_TEMPERATURE"] = "0"
os.environ["AGENT_SEED"] = "42"
try:
yield
finally:
if original is None:
del os.environ["AGENT_TEMPERATURE"]
else:
os.environ["AGENT_TEMPERATURE"] = original
os.environ.pop("AGENT_SEED", None)
# В тестах:
async def test_agent_tool_selection():
with deterministic_mode():
result = await my_agent.run("Какая погода в Москве?")
assert "weather_tool" in result.tool_calls
Проблема: sampling для structured output
from pydantic import BaseModel
import openai
client = openai.AsyncOpenAI()
class TaskAnalysis(BaseModel):
priority: int # 1-5
category: str
estimated_hours: float
requires_human: bool
async def analyze_task(description: str) -> TaskAnalysis:
# Для structured output рекомендуется temperature=0 или очень низкая
# Модель и так ограничена схемой, случайность не нужна
response = await client.beta.chat.completions.parse(
model="gpt-4o",
messages=[
{"role": "system", "content": "Analyze the task and return structured data."},
{"role": "user", "content": description},
],
response_format=TaskAnalysis,
temperature=0, # детерминированно для парсинга
)
return response.choices[0].message.parsed
Лучшие практики
- Код, JSON, SQL →
temperature=0,seed=42 - Tool use в агентах →
temperature=0.1–0.3 - Не используйте
frequency_penalty > 0.3для структурированных ответов - В тестах всегда фиксируйте seed и используйте
temperature=0 - При A/B тестировании промптов — зафиксируйте seed для честного сравнения
- Anthropic temperature нормируйте в 0–1 при переносе из OpenAI
Как подбирать параметры методично
import asyncio
import statistics
from itertools import product
async def evaluate_sampling(
prompt: str,
expected_keywords: list[str],
temperatures: list[float],
top_ps: list[float],
runs_per_config: int = 5,
) -> list[dict]:
"""Подбираем лучшие sampling параметры для задачи."""
client = openai.AsyncOpenAI()
results = []
for temperature, top_p in product(temperatures, top_ps):
scores = []
for _ in range(runs_per_config):
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
top_p=top_p,
max_tokens=200,
)
text = response.choices[0].message.content.lower()
# Считаем, сколько ключевых слов нашли
score = sum(1 for kw in expected_keywords if kw.lower() in text)
scores.append(score / len(expected_keywords))
results.append({
"temperature": temperature,
"top_p": top_p,
"avg_score": statistics.mean(scores),
"std_score": statistics.stdev(scores) if len(scores) > 1 else 0,
})
return sorted(results, key=lambda x: x["avg_score"], reverse=True)
# Запуск:
# results = await evaluate_sampling(
# prompt="Объясни что такое gradient descent",
# expected_keywords=["градиент", "оптимизация", "функция потерь"],
# temperatures=[0.0, 0.3, 0.7, 1.0],
# top_ps=[0.9, 0.95, 1.0],
# )
# print(results[:3]) # топ-3 конфигурации
Проверь себя
Вопросы для самопроверки
- Что происходит с распределением вероятностей при
temperature=0.1vstemperature=2.0? - Почему top-p считается лучше top-k для большинства задач?
- Какие параметры sampling НЕ поддерживает Anthropic API?
- При каком значении temperature рекомендуется генерировать JSON для агента?
- Чем отличается
frequency_penaltyотpresence_penalty?
Показать ответы
- При 0.1 логиты делятся на 0.1 (×10) — распределение становится очень острым, топ-токен получает почти 100%. При 2.0 логиты делятся на 2 — распределение сглаживается, менее вероятные токены получают больший шанс.
- Top-k всегда берёт фиксированное число токенов. Top-p адаптируется: при уверенной модели берёт мало токенов, при неуверенной — больше. Это предотвращает выбор случайных редких токенов в ситуациях высокой уверенности.
- Anthropic не поддерживает
frequency_penaltyиpresence_penalty. - Рекомендуется
temperature=0(deterministic) для JSON — нужна максимальная предсказуемость структуры. - frequency_penalty штрафует пропорционально числу повторений токена (накапливается). presence_penalty штрафует бинарно — за сам факт появления токена хотя бы раз.
Итог урока
- Temperature делит логиты: <1 → острее, >1 → равномернее. 0 = greedy (argmax)
- Top-p (nucleus) оставляет минимальный набор токенов с суммой вероятности ≥ p — адаптивный подход
- Top-k — фиксированный набор; Anthropic его поддерживает, OpenAI — нет
- Frequency/presence penalty борются с повторами; осторожно с кодом — лучше ≤ 0.3
- OpenAI temperature 0–2, Anthropic temperature 0–1 — разные диапазоны!
- Агент + tool use →
temperature=0.1–0.3, код/JSON →temperature=0 - Используйте
seedдля воспроизводимых тестов