ИИ-агенты и prompt engineering Как устроена LLM с точки зрения инженера: токены, контекст, температура, стоимость
0%

Как устроена LLM с точки зрения инженера: токены, контекст, температура, стоимость

Как устроена LLM с точки зрения инженера: токены, контекст, температура, стоимость

Есть два способа понимать большие языковые модели. Первый — «как исследователь»: трансформер, механизм внимания, градиентный спуск, лосс. Второй — «как инженер»: чёрный ящик с известным контрактом, у которого есть единица измерения (токен), ограничение (контекстное окно), рычаги управления (параметры сэмплирования и усилия), цена (доллары за миллион токенов) и профиль латентности (TTFT + TPOT).

Эта статья — про второй способ. Но не в стиле «вам не нужно знать, как оно работает». Наоборот: почти каждый практический баг в LLM-приложении — от «модель не может сложить два числа» до «мой кэш никогда не срабатывает» — объясняется одной-двумя деталями внутреннего устройства. Разберём ровно те детали, которые окупаются.

Формально о самой архитектуре: Attention Is All You Need (Vaswani et al., 2017). Мы будем ссылаться на неё, но пересказывать не будем — нас интересуют следствия.


1. Токен — единица всего

Модель не видит символы и не видит слова. Она видит последовательность целых чисел — идентификаторов токенов из фиксированного словаря (обычно 50–200 тысяч записей). Токенизатор — детерминированная программа, которая переводит байты в эти числа и обратно.

Доминирующий алгоритм — BPE (byte-pair encoding), перенесённый в NLP из сжатия данных в Neural Machine Translation of Rare Words with Subword Units (Sennrich et al., 2015). Идея простая до неприличия:

1. Начать со словаря из всех отдельных байтов (256 записей).
2. Пройти по обучающему корпусу, найти самую частую пару соседних токенов.
3. Слить эту пару в один новый токен, добавить в словарь.
4. Повторять, пока словарь не достигнет заданного размера.

Сложность обучения токенизатора — O(N · V) наивно, O(N log N) с приоритетной очередью, где N — размер корпуса, V — целевой размер словаря. Кодирование строки — O(L log L) или O(L) в зависимости от реализации. Для нас важно другое: границы токенов определяются статистикой обучающего корпуса, а не смыслом.

Токенизация: один смысл, разное число токенов

Три следствия, которые бьют по проду

Следствие 1: неанглийский текст дороже. Английский на типичных токенизаторах даёт ~4 символа на токен. Русский — 2–3.5. Китайский — 1–1.5. Один и тот же документ в русском переводе стоит в полтора-два раза дороже. Это не наценка за язык, это прямое следствие того, что в обучающем корпусе английского было больше и BPE выучил для него более длинные слияния.

Практический эффект: если у вас RAG-система на русских документах, ваш реальный бюджет контекста в символах — примерно вдвое меньше, чем вы посчитали по английским бенчмаркам.

Следствие 2: модель плохо считает. Число 1234567 может разбиться на 123|456|7. Модель не «видит» разряды — она видит три произвольных куска. Отсюда классические ошибки в умножении многозначных чисел и в сравнении версий (3.9 против 3.11). Лечение — не промптинг, а инструменты: вынесите арифметику в code execution или в function calling. Об этом подробно в статье про структурированный вывод.

Следствие 3: подсчёт токенов «на глаз» всегда неверен. Правило «4 символа = 1 токен» промахивается на 15–30% на коде, на 50%+ на русском, и катастрофически на JSON с длинными ключами. У каждого провайдера свой токенизатор, и он меняется между поколениями моделей.

Как считать токены правильно

Единственный корректный способ — спросить у провайдера.

# pip install anthropic
from anthropic import Anthropic

client = Anthropic()

def count(text: str, model: str = "claude-opus-4-8") -> int:
    """Точный подсчёт токенов для конкретной модели.
    Эндпоинт /v1/messages/count_tokens бесплатен и не тратит квоту вывода."""
    resp = client.messages.count_tokens(
        model=model,
        messages=[{"role": "user", "content": text}],
    )
    return resp.input_tokens

en = "Unbelievable performance improvements across the board."
ru = "Невероятные улучшения производительности по всем направлениям."

print(f"EN: {len(en):3d} симв. → {count(en):3d} ток.  ({len(en)/count(en):.1f} симв/ток)")
print(f"RU: {len(ru):3d} симв. → {count(ru):3d} ток.  ({len(ru)/count(ru):.1f} симв/ток)")

Важно: не используйте tiktoken для не-OpenAI моделей. Это токенизатор OpenAI; на текстах для Claude он систематически занижает счёт на 15–20%, а на коде и не-английском — сильно больше. Ошибка в оценке бюджета контекста означает 400-е ошибки в проде.

Токенизаторы, которые полезно посмотреть вживую:

  • openai/tiktoken — быстрая реализация BPE на Rust с биндингами в Python.
  • huggingface/tokenizers — универсальная библиотека, поддерживает BPE, WordPiece, Unigram.
  • Tiktokenizer — интерактивная визуализация разбиения.

2. Что происходит между запросом и ответом

Инженерно генерация делится на две принципиально разные фазы. Их путают чаще всего, а от их различия зависит и латентность, и стоимость, и вся стратегия оптимизации.

Prefill и decode: две фазы генерации

Prefill (предзаполнение). Модель обрабатывает весь промпт за один проход. Все n токенов идут через сеть параллельно — это большие матричные умножения, GPU загружен под завязку. Фаза compute-bound. Стоимость внимания здесь квадратична по длине: O(n²·d) по времени, где d — размерность модели. Именно поэтому промпт в 100k токенов обрабатывается заметно дольше, чем в 10 раз дольше промпта в 10k.

Decode (декодирование). Модель генерирует по одному токену за проход. На каждом шаге прогоняется ровно один новый токен, но приходится читать из памяти все веса модели. Фаза memory-bandwidth-bound: GPU в основном ждёт память. Здесь вычислений мало, но шагов много, и они строго последовательны — распараллелить генерацию собственного ответа нельзя.

Между шагами decode переиспользуется KV-кэш: ключи и значения внимания для уже обработанных токенов не пересчитываются. Без него каждый шаг стоил бы O(n²), с ним — O(n). Цена — память: KV-кэш растёт линейно с длиной последовательности и в длинном контексте занимает десятки гигабайт. Управление этой памятью — предмет отдельной инженерии; см. PagedAttention / vLLM (Kwon et al., 2023).

Формулы латентности, которыми можно пользоваться

TTFT   ≈ сеть + очередь + (n_input − n_cached) / скорость_prefill
TPOT   ≈ 1 / скорость_decode                    (токенов в секунду)
Общее  ≈ TTFT + n_output · TPOT

Из этого следует главное правило оптимизации латентности:

Что сокращаем На что влияет Насколько эффективно
Длину промпта TTFT Средне — квадратичная зависимость помогает, но убрать много обычно нельзя
Кэшируемый префикс (prompt caching) TTFT Очень сильно — кэш-хит убирает prefill почти целиком
Длину ответа Общую латентность Очень сильно — линейно, и это обычно единственный настоящий рычаг
Выбор модели TPOT и TTFT Сильно — младшие модели декодируют кратно быстрее
Стриминг Воспринимаемую латентность Сильно — пользователь видит первый токен через TTFT, а не через полное время

Ответ на 2000 токенов при TPOT = 20 мс — это 40 секунд, что бы вы ни делали с промптом. Если вам нужен быстрый отклик, единственный работающий приём — заставить модель отвечать короче.


3. Контекстное окно: что это и почему «влезает» ≠ «работает»

Контекстное окно — максимальное число токенов, которые модель может держать «в поле зрения» за один запрос: система, определения инструментов, вся история диалога, документы и генерируемый ответ. У современных моделей Anthropic это 1M токенов (у Haiku 4.5 — 200K), с потолком вывода 128K (64K у Haiku).

Три ловушки.

Ловушка 1: окно общее для входа и выхода. max_tokens — это лимит на вывод, и он вычитается из общего бюджета. Если промпт занял почти всё окно, места на ответ не осталось. При этом max_tokens — не подсказка модели, а жёсткий обрыв: превышение даёт stop_reason: "max_tokens" и обрезанный посередине текст. Проверять stop_reason нужно всегда.

Ловушка 2: заявленное окно ≠ эффективное. Модель, формально принимающая 1M токенов, не обязательно одинаково хорошо использует всю длину. Классический результат — Lost in the Middle: How Language Models Use Long Contexts (Liu et al., 2023): точность извлечения факта заметно проседает, когда факт лежит в середине длинного контекста, и максимальна на краях. Кривая U-образная.

Практика: важное — в начало и в конец. Инструкции сверху, ключевой вопрос снизу, «наполнитель» в середине. Для RAG это означает, что порядок чанков после реранжирования — не косметика; см. RAG.

Ловушка 3: длинный контекст дорог и медленен. Внимание квадратично. Даже с FlashAttention (Dao et al., 2022), который убирает материализацию матрицы внимания в HBM и даёт линейную по памяти реализацию, вычислительная сложность остаётся O(n²). Забить окно «на всякий случай» — самый дорогой способ ничего не улучшить.

Код, который обрабатывает response.content[0].text без проверки stop_reason, — это код с латентным багом. При refusal массив content может быть пустым, при max_tokens — обрезанным, при tool_use — вообще не содержать текста.


4. Температура и сэмплирование: чем на самом деле управляют

На каждом шаге decode модель выдаёт вектор логитов размером со словарь — по числу на каждый возможный следующий токен. Дальше нужно выбрать один. Способ выбора и есть сэмплирование.

Softmax с температурой превращает логиты в вероятности: логиты делятся на T, затем нормализуются. Эффект:

  • T → 0: распределение схлопывается в дельта-функцию, всегда выбирается самый вероятный токен (жадное декодирование).
  • T = 1: распределение ровно такое, каким его выдала модель.
  • T > 1: распределение сглаживается, хвост становится вероятнее, текст — «креативнее» и бессвязнее.

Top-k оставляет только k самых вероятных кандидатов и ренормализует. Грубо, потому что k фиксировано: в уверенном контексте отсекает слишком мало, в неуверенном — слишком много.

Top-p (nucleus sampling) оставляет минимальное множество токенов, суммарная вероятность которых ≥ p. Адаптивно: там, где модель уверена, ядро из 2–3 токенов; там, где нет, — из сотен. Введено в The Curious Case of Neural Text Degeneration (Holtzman et al., 2019) — там же показано, почему чисто жадное декодирование даёт вырожденный, зацикливающийся текст.

import numpy as np

def sample(logits: np.ndarray, temperature: float = 1.0, top_p: float = 1.0) -> int:
    """Референсная реализация temperature + nucleus sampling.
    Сложность: O(V log V) из-за сортировки, где V — размер словаря."""
    if temperature <= 0:                      # жадное декодирование
        return int(np.argmax(logits))

    scaled = logits / temperature
    scaled = scaled - scaled.max()            # стабилизация: избегаем переполнения exp
    probs = np.exp(scaled)
    probs /= probs.sum()

    if top_p < 1.0:
        order = np.argsort(-probs)            # по убыванию вероятности
        cum = np.cumsum(probs[order])
        # оставляем минимальное ядро, покрывающее top_p вероятности
        keep = order[: int(np.searchsorted(cum, top_p) + 1)]
        mask = np.zeros_like(probs, dtype=bool)
        mask[keep] = True
        probs = np.where(mask, probs, 0.0)
        probs /= probs.sum()

    return int(np.random.choice(len(probs), p=probs))

Важно: temperature=0 никогда не давала детерминизма

Это самое живучее заблуждение в отрасли. Даже при жадном декодировании результат не воспроизводится побитово, потому что:

  • сложение чисел с плавающей точкой на GPU не ассоциативно, а порядок редукции зависит от того, как ядро разбило работу;
  • батчинг на сервере меняется от запроса к запросу, а вместе с ним и порядок операций;
  • MoE-модели маршрутизируют токены по экспертам в зависимости от состава батча;
  • при равных логитах у двух токенов argmax выбирает произвольный.

Если вам нужна воспроизводимость — её обеспечивает не температура, а валидация схемой и кэширование ответов, а не надежда на детерминизм модели.

Отдельно: в современных API температуры может просто не быть

Это важное изменение контракта, о котором стоит знать заранее. На Claude Opus 4.7/4.8, Sonnet 5 и Fable 5 параметры temperature, top_p и top_k удалены — запрос с ними возвращает 400. Модели этого поколения управляются иначе:

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=16000,
    thinking={"type": "adaptive"},        # модель сама решает, сколько «думать»
    output_config={"effort": "high"},     # low | medium | high | xhigh | max
    messages=[{"role": "user", "content": "..."}],
)
# temperature / top_p / top_k здесь дали бы 400 invalid_request_error

Логика смены рычага: temperature — это управление на уровне отдельного токена, вслепую. effort — управление на уровне задачи: сколько токенов рассуждения и сколько шагов инструментов модель имеет право потратить. Для инженера это более осмысленная ручка, потому что она напрямую отображается в стоимость и латентность.

Если раньше вы использовали temperature=0 для «предсказуемости» — замена не в другой температуре, а в effort: "low" плюс более жёсткий промпт и схема вывода. Если использовали temperature=1.2 для разнообразия — замена в явной инструкции («предложи четыре разных варианта, затем выбери») либо в нескольких независимых вызовах.

Сравнительная сводка по рычагам:

Рычаг Что делает Когда применять Побочный эффект
temperature (наследие) Сглаживает распределение токенов Устаревшие модели, творческая генерация Ниже связность на высоких значениях
top_p (наследие) Отсекает хвост распределения Вместе с temperature, не вместо На малых p — шаблонность
effort Глубина рассуждения и число шагов Современные модели, все задачи Прямо влияет на цену и латентность
thinking: adaptive Модель сама решает глубину По умолчанию для нетривиальных задач Расход токенов рассуждения
max_tokens Жёсткий потолок вывода Всегда Обрыв посередине, если мало
stop_sequences Досрочная остановка Парсеры с явным разделителем Пропущенный разделитель = полный вывод

5. Экономика: как считать деньги

Тарификация посимвольно-токенная и асимметричная: выходные токены стоят в 4–5 раз дороже входных. Причина видна из раздела 2 — decode последовательный и не батчится так же хорошо, как prefill.

Актуальные ставки Anthropic (за миллион токенов):

Модель Контекст Вход Выход Когда брать
claude-fable-5 1M 10.00 $ 50.00 $ Самые сложные длинногоризонтные задачи
claude-opus-4-8 1M 5.00 $ 25.00 $ Дефолт для сложных агентных и кодовых задач
claude-sonnet-5 1M 3.00 $ 15.00 $ Высоконагруженный прод, качество близко к Opus
claude-haiku-4-5 200K 1.00 $ 5.00 $ Классификация, роутинг, простые извлечения

Соотношение вход/выход в реальных нагрузках сильно разное, и от него зависит, какой рычаг оптимизации работает:

Тип нагрузки Типичный вход Типичный выход Где деньги
Классификация тикетов 500 5 Целиком во входе → кэшируйте system
RAG-ответ 8 000 400 Во входе → кэшируйте документы, сжимайте чанки
Генерация кода 3 000 2 500 Пополам → важны обе стороны
Длинный отчёт 2 000 8 000 В выводе → единственный рычаг — короче писать
Агентный цикл (20 шагов) 20 × растущий 20 × 300 История растёт квадратично → кэш обязателен

Кэширование промптов — главный рычаг

Кэш работает по совпадению префикса. Порядок рендеринга запроса: toolssystemmessages. Любое изменение байта в префиксе инвалидирует всё после него.

Экономика на примере Opus 4.8 (5 $ за 1M входных):

Запись в кэш (TTL 5 мин):  1.25× базовой цены  →  $6.25 / 1M
Запись в кэш (TTL 1 час):  2.00× базовой цены  →  $10.00 / 1M
Чтение из кэша:            0.10× базовой цены  →  $0.50 / 1M
Обычный вход:              1.00×               →  $5.00 / 1M

Точка безубыточности для 5-минутного TTL — два запроса: 1.25 + 0.1 = 1.35 против 2.0 без кэша. Для часового TTL нужно минимум три.

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=4096,
    system=[{
        "type": "text",
        "text": BIG_STABLE_SYSTEM_PROMPT,   # неизменный между запросами
        "cache_control": {"type": "ephemeral"},
    }],
    messages=[{"role": "user", "content": user_question}],  # меняется каждый раз
)

# Проверка, что кэш реально работает:
u = response.usage
print(f"записано в кэш: {u.cache_creation_input_tokens}")
print(f"прочитано из кэша: {u.cache_read_input_tokens}")
print(f"обработано без кэша: {u.input_tokens}")

Если cache_read_input_tokens стабильно ноль при одинаковых промптах — где-то тихий инвалидатор. Типичные виновники:

Паттерн Почему ломает
datetime.now() в system-промпте Префикс уникален на каждый запрос
uuid4() / request_id в начале контента То же самое
json.dumps(d) без sort_keys=True Недетерминированная сериализация
Итерация по set при сборке промпта Порядок не гарантирован
Список инструментов, зависящий от пользователя tools рендерятся первыми — ломается всё
Смена модели посреди диалога Кэш привязан к модели
Условные блоки if flag: system += ... Каждая комбинация флагов — свой префикс

Ещё одна тонкость: минимальная кэшируемая длина префикса зависит от модели. Для Opus 4.8 — 4096 токенов, для Sonnet 4.6 и Fable 5 — 2048, для Sonnet 4.5 — 1024. Промпт короче порога не закэшируется молча, без ошибки: просто cache_creation_input_tokens: 0. Промпт на 3000 токенов кэшируется на Sonnet и не кэшируется на Opus.

Максимум — 4 точки разрыва (cache_control) на запрос.

Batch API

Если задача не интерактивная — пакетная обработка даёт 50% скидки на все токены. Ограничения: до 100 000 запросов или 256 МБ на пакет, большинство завершается в течение часа, максимум — 24 часа. Батч отлично комбинируется с кэшированием: общий системный промпт в тысячах запросов кэшируется один раз.

Калькулятор стоимости

from dataclasses import dataclass

PRICING = {                       # ($ за 1M вход, $ за 1M выход)
    "claude-fable-5":   (10.0, 50.0),
    "claude-opus-4-8":  ( 5.0, 25.0),
    "claude-sonnet-5":  ( 3.0, 15.0),
    "claude-haiku-4-5": ( 1.0,  5.0),
}

@dataclass
class Workload:
    model: str
    requests_per_day: int
    input_tokens: int
    output_tokens: int
    cached_prefix: int = 0        # сколько входных токенов кэшируются
    cache_hit_rate: float = 0.0   # доля запросов с попаданием в кэш
    batch: bool = False

    def daily_cost(self) -> float:
        p_in, p_out = PRICING[self.model]
        fresh = self.input_tokens - self.cached_prefix

        # Попадание: свежие токены по полной + кэш по 0.1x
        hit  = fresh * p_in + self.cached_prefix * p_in * 0.10
        # Промах: свежие по полной + запись кэша по 1.25x
        miss = fresh * p_in + self.cached_prefix * p_in * 1.25

        cost_in  = self.cache_hit_rate * hit + (1 - self.cache_hit_rate) * miss
        cost_out = self.output_tokens * p_out
        per_req  = (cost_in + cost_out) / 1_000_000

        if self.batch:
            per_req *= 0.5
        return per_req * self.requests_per_day


rag = Workload("claude-sonnet-5", 50_000, 8_000, 400,
               cached_prefix=6_500, cache_hit_rate=0.9)
print(f"RAG в день: ${rag.daily_cost():,.2f}  → в месяц ${rag.daily_cost()*30:,.2f}")

naive = Workload("claude-sonnet-5", 50_000, 8_000, 400)
print(f"Без кэша:   ${naive.daily_cost():,.2f}  → в месяц ${naive.daily_cost()*30:,.2f}")

При 90% попаданий кэш срезает входную часть счёта примерно в четыре раза. На нагрузке с преобладанием входа это разница между «дорого» и «нормально».


6. Дерево решений: какую модель брать

Ключевое здесь — узел «на своём eval-наборе». Выбор модели по чужим бенчмаркам — это гадание; про построение собственных наборов и метрик см. оценку и бенчмарки.

Важное правило миграции: не понижайте модель ради экономии по умолчанию. Сначала измерьте, где реально уходят деньги. В 8 случаях из 10 это не тир модели, а невключённый кэш и раздутый вывод.


7. Типичные ошибки

Оценка токенов по длине строки. «4 символа = 1 токен» ломается на русском, коде и JSON. Считайте через count_tokens. Стоимость вызова — ноль.

tiktoken для не-OpenAI моделей. Систематическое занижение. Планировали 180k токенов, получили 400 «context length exceeded».

Игнорирование stop_reason. Обрезанный по max_tokens JSON парсится с исключением где-то ниже по стеку, и вы будете отлаживать парсер вместо того, чтобы поднять лимит.

max_tokens «с запасом» в 200. Модель начинает писать нормальный ответ и обрывается. Для нестримовых запросов разумный дефолт — около 16 000; для стримовых — до 64 000. Занижать стоит только по конкретной причине (классификация — 256, жёсткий кост-кап).

Ожидание детерминизма от temperature=0. Не бывает. Стройте систему на валидации, а не на воспроизводимости.

Динамический system-промпт. Дата, имя пользователя, режим — всё это в system-промпте убивает кэш для всех. Выносите изменяемое в конец messages.

Недетерминированная сериализация инструментов. tools рендерятся первыми. Список, собранный итерацией по словарю без сортировки, ломает кэш целиком.

Забивание контекста «на всякий случай». Больше контекста ≠ лучше ответ. Работает lost-in-the-middle, платите вы квадратично, а качество падает.

Смена набора инструментов посреди диалога. Добавили один инструмент на пятом ходу — инвалидировали весь кэш разговора. Если нужен динамический набор — используйте tool search, который дописывает схемы, а не подменяет их.

Оптимизация не того конца. Если 80% счёта — выходные токены, сжатие промпта на 20% не даст ничего. Сначала посмотрите на разбивку usage, потом оптимизируйте.


8. Практика продакшена

Считайте usage на каждом запросе и складывайте в метрики. Минимальный набор: input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, stop_reason, латентность, модель. Без этого разговор об оптимизации беспредметен.

import time, logging

def instrumented_call(client, **kwargs):
    t0 = time.monotonic()
    resp = client.messages.create(**kwargs)
    elapsed = time.monotonic() - t0

    u = resp.usage
    total_in = u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
    hit_rate = u.cache_read_input_tokens / total_in if total_in else 0.0

    logging.info(
        "llm_call model=%s in=%d cached=%d out=%d hit_rate=%.2f "
        "stop=%s latency_ms=%d req_id=%s",
        resp.model, u.input_tokens, u.cache_read_input_tokens,
        u.output_tokens, hit_rate, resp.stop_reason,
        int(elapsed * 1000), resp._request_id,
    )
    return resp

Обратите внимание: input_tokens — это только незакэшированный остаток. Полный размер промпта — сумма трёх полей. Агент, который проработал час и показывает input_tokens: 4000, не «маленький» — остальное пришло из кэша.

Всегда обрабатывайте stop_reason явно. Отдельная ветка на max_tokens (повторить с большим лимитом или перейти на стриминг), на refusal (не ретраить тот же промпт), на tool_use (выполнить инструмент), на pause_turn (переотправить историю).

Стримьте всё, что длиннее ~16k max_tokens. SDK отказывается делать нестримовые запросы, которые он оценивает дольше десяти минут, — простой обрыв соединения по таймауту вы получите и без этой защиты.

Проектируйте промпт под кэш с первого дня. Стабильное — вперёд (замороженный system, детерминированно отсортированные инструменты), изменчивое — назад, после последней точки разрыва. Переделывать это потом дороже, чем сделать сразу.

Ставьте потолки. Пер-запросный лимит max_tokens, дневной бюджет на пользователя, алерт на аномальный рост output_tokens. Агентный цикл без потолка — это открытый счёт.

Прогревайте кэш, если первая задержка видна пользователю. Запрос с max_tokens: 0 прогоняет prefill и записывает кэш, возвращаясь мгновенно с пустым content. Имеет смысл при старте воркера или после деплоя — но не когда трафик и так плотнее, чем TTL кэша.

Подробнее про роутинг моделей, наблюдаемость и бюджеты — в статье про ИИ в продакшене.


Мини-итог

  • Токен — единица счёта, стоимости и лимитов. Границы токенов задаёт статистика корпуса, а не смысл, — отсюда дорогой русский текст и сломанная арифметика. Считайте токены API-методом, а не эвристикой.
  • Генерация делится на prefill и decode. Prefill параллелен, quadratic по длине и кэшируется. Decode последователен, линеен по числу выходных токенов и не кэшируется никак. Отсюда: длинный промпт лечится кэшем, длинный ответ — только сокращением ответа.
  • Контекстное окно — общий бюджет входа и выхода. Заявленный объём не равен эффективному: работает lost-in-the-middle, важное держите на краях.
  • Температура управляет распределением на уровне токена и никогда не давала детерминизма. В современных API Anthropic её нет вовсе — рычаг сместился на effort и adaptive thinking, что для инженера удобнее: эта ручка прямо отображается в цену и латентность.
  • Экономика асимметрична: выход дороже входа в 4–5 раз. Главные рычаги в порядке эффективности — кэширование префикса, сокращение вывода, Batch API, выбор модели.
  • Наблюдаемость usage — предусловие любой оптимизации. Оптимизировать нужно тот конец, где реально уходят деньги, а не тот, который проще сжать.

Дальше начинается собственно работа с моделью: как формулировать задачу так, чтобы модель её понимала, и как получать ответ в нужной форме.


Источники


Что дальше

Основы промптинга: zero-shot, few-shot, роли, структура и формат ответа — как из понимания токенов и контекста вырастает практика формулирования задач: почему few-shot работает, куда класть инструкции, как задавать формат ответа и какие структуры промпта устойчиво выигрывают на замерах.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов