Что такое токен

Токен — это не слово и не символ. Это фрагмент текста, на который языковая модель разбивает входные данные перед обработкой. Токен может быть целым словом, частью слова, отдельным символом или даже пробелом.

Посмотри, как GPT разбивает обычные фразы на токены:

Пример токенизации — русский текст
ИИ-агент получает запрос от пользователя
10 токенов · 10 слов → ~1 токен/слово (но в русском обычно больше)
Пример токенизации — английский текст (эффективнее)
The AI agent receives a request from the user
9 токенов · 9 слов → ~1 токен/слово
Код токенизируется иначе — каждый символ важен
async def fetch(url: str) -> str:
10 токенов
ℹ️ Практическое правило

Для оценки без подсчёта: ~1 токен = 4 символа в английском, ~2–4 токена = 1 слово в русском. Русский и другие не-английские языки кодируются менее эффективно, потому что токенизаторы обучались преимущественно на английском тексте.

Как работает токенизатор: Byte Pair Encoding

Большинство современных LLM используют алгоритм BPE (Byte Pair Encoding). Суть простая: из огромного корпуса текстов алгоритм находит самые частые пары символов и объединяет их в один токен. Затем снова ищет частые пары — и так тысячи раз.

Как строится словарь BPE (упрощённо):

Шаг 0: Начинаем с отдельных символов
  [ h, e, l, l, o, w, o, r, l, d ]

Шаг 1: "ll" встречается часто → объединяем
  [ h, e, ll, o, w, o, r, l, d ]

Шаг 2: "wo" встречается часто → объединяем
  [ h, e, ll, o, wo, r, l, d ]

Шаг N: После тысяч итераций получаем словарь ~50,000 токенов
  "hello" → один токен
  "world" → один токен
  "непопулярноеслово" → 5-7 токенов (разбивается на части)

Итоговый словарь GPT-4 содержит ~100 256 токенов (cl100k_base). Claude использует собственный токенизатор схожего размера.

Почему это важно для промптов

Частые слова из обучающего корпуса — один токен. Редкие слова, имена, технические термины — несколько токенов. Числа часто разбиваются: 12345 может быть 2–3 токенами. Это влияет на то, как модель «видит» и обрабатывает данные.

tiktoken: считаем токены в коде

tiktoken — официальная библиотека OpenAI для подсчёта токенов. Используй её перед отправкой запроса, чтобы проверить объём и не превысить лимиты:

Установка и базовый подсчёт
bash
pip install tiktoken
Подсчёт токенов в тексте
python
import tiktoken

# Получаем энкодер для конкретной модели
enc = tiktoken.encoding_for_model("gpt-4o")
# Или по имени кодировки:
# enc = tiktoken.get_encoding("cl100k_base")  # GPT-4, GPT-3.5
# enc = tiktoken.get_encoding("o200k_base")   # GPT-4o, GPT-4o-mini

text = "AI-агент получает запрос и выполняет задачу"

# Кодируем текст → список токенов (чисел)
tokens = enc.encode(text)
print(f"Токены: {tokens}")
# [15836, 44, 18822, 61066, 25, 2099, 23485, 11, 7, 19897, 9847, 12, 2234, 44]

print(f"Количество токенов: {len(tokens)}")
# Количество токенов: 14

# Декодируем обратно (для отладки)
for token_id in tokens[:5]:
    fragment = enc.decode([token_id])
    print(f"  {token_id:6d} → {repr(fragment)}")
Подсчёт токенов в сообщениях чата — для агентов
python
import tiktoken

def count_message_tokens(
    messages: list[dict],
    model: str = "gpt-4o",
) -> int:
    """
    Точный подсчёт токенов в списке сообщений чата.
    Учитывает overhead на разметку ролей (~4 токена на сообщение).
    Формула из официальной документации OpenAI.
    """
    enc = tiktoken.encoding_for_model(model)

    # Каждое сообщение: 3 токена overhead (role + content separators)
    tokens_per_message = 3
    # Каждый ответ начинается с: <|start|>assistant<|message|>
    tokens_per_reply_prefix = 3

    total = tokens_per_reply_prefix
    for message in messages:
        total += tokens_per_message
        for key, value in message.items():
            total += len(enc.encode(str(value)))

    return total

# Пример: история диалога агента
messages = [
    {"role": "system", "content": "Ты — полезный AI-агент. Отвечай кратко и по делу."},
    {"role": "user", "content": "Найди информацию о библиотеке LangGraph"},
    {"role": "assistant", "content": "LangGraph — библиотека для создания агентов на основе графов..."},
    {"role": "user", "content": "Как установить LangGraph?"},
]

total = count_message_tokens(messages)
print(f"Токенов в истории: {total}")
# Токенов в истории: ~85

# Проверка перед отправкой запроса
MAX_CONTEXT = 128_000  # gpt-4o
MAX_RESPONSE = 4_096

if total + MAX_RESPONSE > MAX_CONTEXT:
    print("⚠️ Контекст переполнен! Нужно укоротить историю")
else:
    available = MAX_CONTEXT - total - MAX_RESPONSE
    print(f"✅ Свободно для документов/инструментов: {available:,} токенов")
Визуализация токенов — полезно для отладки промптов
python
import tiktoken

def show_tokens(text: str, model: str = "gpt-4o") -> None:
    """Распечатывает текст с разбивкой на токены."""
    enc = tiktoken.encoding_for_model(model)
    token_ids = enc.encode(text)

    print(f"Текст ({len(text)} символов) → {len(token_ids)} токенов\n")
    for i, tid in enumerate(token_ids):
        fragment = enc.decode([tid])
        print(f"  [{i:3d}] id={tid:6d}  {repr(fragment)}")

show_tokens("Hello, world!")
# Текст (13 символов) → 4 токена
#   [  0] id= 9906  'Hello'
#   [  1] id=   11  ','
#   [  2] id=  995  ' world'
#   [  3] id=   0  '!'

show_tokens("Привет, мир!")
# Текст (12 символов) → 6 токенов
# (русский кодируется менее эффективно)

Контекстное окно: память модели

Контекстное окно — это максимальное количество токенов, которые модель может обработать за один вызов: и входящие данные (prompt), и генерируемый ответ вместе.

Модель «видит» только то, что находится в окне. Всё, что вышло за пределы — модель не помнит. Это не баг, это архитектурное ограничение трансформера.

GPT-4o mini
OpenAI
128K
токенов · ~96k слов
GPT-4o
OpenAI
128K
токенов · ~300 страниц
Claude Sonnet 4
Anthropic
200K
токенов · ~500 страниц
Claude Haiku 4
Anthropic
200K
токенов · быстро и дёшево
Gemini 2.0 Flash
Google
1M
токенов · целые кодовые базы
Llama 3.3 70B
Meta / Local
128K
токенов · self-hosted
⚠️ Большой контекст ≠ хорошая работа на краях

Модели хуже «помнят» информацию из середины длинного контекста — эффект известен как lost in the middle. Важные данные лучше помещать в начало (системный промпт) или в конец (последнее сообщение пользователя).

Что занимает контекст агента

Контекст агента — не просто разговор. Он состоит из нескольких частей, каждая из которых «стоит» токены:

Типичное распределение токенов в запросе агента (128K окно)
System
Tools
История
RAG-контекст
Свободно
System prompt ~8K
Описания инструментов ~15K
История диалога ~25K
RAG-документы ~32K
Свободно для ответа ~48K
Оценка бюджета токенов агента
python
import tiktoken

def estimate_agent_context(
    system_prompt: str,
    tools: list[dict],
    history: list[dict],
    rag_docs: list[str],
    model: str = "gpt-4o",
    model_max_tokens: int = 128_000,
) -> dict:
    """Показывает, сколько токенов занимает каждая часть контекста агента."""
    import json
    enc = tiktoken.encoding_for_model(model)

    def count(text: str) -> int:
        return len(enc.encode(text))

    system_tokens  = count(system_prompt)
    tools_tokens   = count(json.dumps(tools))
    history_tokens = sum(count(m["content"]) + 4 for m in history)
    docs_tokens    = sum(count(doc) for doc in rag_docs)
    used           = system_tokens + tools_tokens + history_tokens + docs_tokens

    print(f"{'Компонент':<25} {'Токены':>8}  {'%':>5}")
    print("─" * 42)
    print(f"{'System prompt':<25} {system_tokens:>8,}  {system_tokens/model_max_tokens*100:>4.1f}%")
    print(f"{'Описания инструментов':<25} {tools_tokens:>8,}  {tools_tokens/model_max_tokens*100:>4.1f}%")
    print(f"{'История диалога':<25} {history_tokens:>8,}  {history_tokens/model_max_tokens*100:>4.1f}%")
    print(f"{'RAG-документы':<25} {docs_tokens:>8,}  {docs_tokens/model_max_tokens*100:>4.1f}%")
    print("─" * 42)
    print(f"{'ИТОГО занято':<25} {used:>8,}  {used/model_max_tokens*100:>4.1f}%")
    print(f"{'Свободно для ответа':<25} {model_max_tokens - used:>8,}  {(model_max_tokens-used)/model_max_tokens*100:>4.1f}%")

    return {
        "used": used, "free": model_max_tokens - used,
        "overflow": used > model_max_tokens,
    }

# Пример
result = estimate_agent_context(
    system_prompt="Ты — AI-агент для анализа документов...",
    tools=[{"name": "search", "description": "..."}],
    history=[
        {"role": "user", "content": "Проанализируй этот отчёт"},
        {"role": "assistant", "content": "Хорошо, начинаю анализ..."},
    ],
    rag_docs=["Текст документа 1...", "Текст документа 2..."],
)

Стратегии управления контекстом агента

Когда история диалога растёт — контекст заполняется. Вот основные стратегии борьбы с этим:

Скользящее окно

Sliding window — отбрасываем старые сообщения
python
import tiktoken

def trim_history(
    messages: list[dict],
    max_tokens: int = 8_000,
    model: str = "gpt-4o",
    keep_system: bool = True,
) -> list[dict]:
    """
    Обрезает историю сообщений до max_tokens.
    Всегда сохраняет системный промпт и последнее сообщение пользователя.
    """
    enc = tiktoken.encoding_for_model(model)

    def count(msgs: list[dict]) -> int:
        return sum(len(enc.encode(m["content"])) + 4 for m in msgs)

    # Отделяем system prompt
    system = [m for m in messages if m["role"] == "system"]
    dialog = [m for m in messages if m["role"] != "system"]

    # Всегда сохраняем последнее сообщение пользователя
    last = dialog[-1:] if dialog else []
    middle = dialog[:-1]

    # Убираем старые сообщения с начала истории пока не влезем
    while middle and count(system + middle + last) > max_tokens:
        middle.pop(0)  # Удаляем самое старое

    trimmed = system + middle + last
    original_count = count(messages)
    new_count = count(trimmed)

    if new_count < original_count:
        print(f"История обрезана: {original_count} → {new_count} токенов")

    return trimmed

Суммаризация истории

Суммаризация — сжимаем старую историю
python
from openai import AsyncOpenAI
import tiktoken

client = AsyncOpenAI()

async def compress_history(
    messages: list[dict],
    token_threshold: int = 6_000,
    model: str = "gpt-4o-mini",
) -> list[dict]:
    """
    Если история превышает порог — суммаризируем старые сообщения
    и заменяем их одним компактным сообщением-резюме.
    """
    enc = tiktoken.encoding_for_model(model)
    total = sum(len(enc.encode(m["content"])) + 4 for m in messages)

    if total <= token_threshold:
        return messages  # Ещё влезает — ничего не делаем

    # Берём первые 70% сообщений для суммаризации
    system_msgs = [m for m in messages if m["role"] == "system"]
    dialog      = [m for m in messages if m["role"] != "system"]
    split       = max(2, int(len(dialog) * 0.7))
    to_compress = dialog[:split]
    to_keep     = dialog[split:]

    # Суммаризируем через LLM
    history_text = "\n".join(
        f"{m['role'].upper()}: {m['content']}" for m in to_compress
    )
    summary_resp = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": (
                "Сделай краткое резюме следующего диалога (2-4 предложения). "
                "Сохрани ключевые факты, решения и контекст:\n\n" + history_text
            ),
        }],
        max_tokens=300,
    )
    summary = summary_resp.choices[0].message.content

    # Заменяем старую историю резюме
    summary_msg = {
        "role": "assistant",
        "content": f"[Резюме предыдущего разговора]: {summary}",
    }

    compressed = system_msgs + [summary_msg] + to_keep
    new_total = sum(len(enc.encode(m["content"])) + 4 for m in compressed)
    print(f"История сжата: {total} → {new_total} токенов")
    return compressed

Сравнение стратегий

Стратегия Как работает Плюсы Минусы Когда применять
Sliding window Отбрасываем старые сообщения Просто, бесплатно Теряем контекст Короткие сессии
Суммаризация LLM сжимает старую историю Сохраняем суть Стоит токены, потеря деталей Длинные диалоги
External memory Факты → в векторную БД, в контекст — только релевантное Неограниченная история Сложная инфраструктура Production-агенты, RAG
Prompt caching Статичные части промпта кэшируются провайдером -90% стоимость кэшированных токенов Только у Anthropic/OpenAI Большой system prompt или RAG

Стоимость токенов: считаем деньги

Каждый вызов LLM API тарифицируется по токенам отдельно для входных (input) и выходных (output). Выходные токены обычно дороже — их генерировать сложнее.

Модель Input (за 1M) Output (за 1M) Cached input
GPT-4o $2.50 $10.00 $1.25
GPT-4o mini $0.15 $0.60 $0.075
Claude Sonnet 4 $3.00 $15.00 $0.30
Claude Haiku 4 $0.80 $4.00 $0.08
Gemini 2.0 Flash $0.10 $0.40
Калькулятор стоимости запроса
python
from dataclasses import dataclass

@dataclass
class ModelPricing:
    input_per_1m: float   # $ за 1M input токенов
    output_per_1m: float  # $ за 1M output токенов
    cached_per_1m: float = 0.0  # $ за 1M кэшированных input

PRICING = {
    "gpt-4o":           ModelPricing(2.50,  10.00, 1.25),
    "gpt-4o-mini":      ModelPricing(0.15,   0.60, 0.075),
    "claude-sonnet-4":  ModelPricing(3.00,  15.00, 0.30),
    "claude-haiku-4":   ModelPricing(0.80,   4.00, 0.08),
    "gemini-2.0-flash": ModelPricing(0.10,   0.40),
}

def calc_cost(
    model: str,
    input_tokens: int,
    output_tokens: int,
    cached_tokens: int = 0,
) -> float:
    p = PRICING[model]
    cost = (
        (input_tokens - cached_tokens) / 1_000_000 * p.input_per_1m
        + cached_tokens               / 1_000_000 * p.cached_per_1m
        + output_tokens               / 1_000_000 * p.output_per_1m
    )
    return cost

# Пример: 1000 запросов агента в день
input_tok  = 2_000   # токенов на запрос (история + RAG + system)
output_tok = 500     # токенов на ответ

for model in PRICING:
    daily = calc_cost(model, input_tok, output_tok) * 1000
    print(f"{model:<22} {daily:>8.3f}$/день  {daily*30:>8.1f}$/мес")

Prompt Caching: экономим на повторных вызовах

Если системный промпт или большой RAG-контекст одинаковы в каждом запросе — провайдер может их кэшировать. Повторные вызовы обходятся в 5–10x дешевле:

Anthropic: cache_control для системного промпта
python
import anthropic

client = anthropic.Anthropic()

# Большой статичный системный промпт — идеальный кандидат для кэширования
SYSTEM_PROMPT = """Ты — опытный AI-агент для анализа финансовых документов.
... (длинный промпт на несколько тысяч токенов) ...
"""

# Добавляем cache_control: {"type": "ephemeral"} к статичным блокам
message = client.messages.create(
    model="claude-haiku-4-5-20251001",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": SYSTEM_PROMPT,
            "cache_control": {"type": "ephemeral"},  # ← кэшируем!
        }
    ],
    messages=[{"role": "user", "content": "Проанализируй этот отчёт..."}],
)

# Смотрим использование кэша в ответе
usage = message.usage
print(f"Cache hit:   {usage.cache_read_input_tokens:,} токенов  (дёшево!)")
print(f"Cache write: {usage.cache_creation_input_tokens:,} токенов  (первый раз дороже)")
print(f"Обычные:     {usage.input_tokens:,} токенов")
Когда выгоден prompt caching

Anthropic кэширует блоки от 1024 токенов и выше. Кэш живёт 5 минут (ephemeral). Это выгодно когда: большой system prompt (инструкции агента), одни и те же RAG-документы в каждом запросе, описания инструментов, корпоративные регламенты.

Шпаргалка

Токены и контекст — cheatsheet
python
import tiktoken

# ── Подсчёт токенов ───────────────────────────────────
enc = tiktoken.encoding_for_model("gpt-4o")   # o200k_base
# enc = tiktoken.encoding_for_model("gpt-3.5-turbo")  # cl100k_base

tokens = enc.encode("Текст для подсчёта")
count  = len(tokens)                  # Количество токенов
text   = enc.decode(tokens)           # Обратно в текст

# ── Оценка без библиотеки (грубо) ─────────────────────
# ~4 символа = 1 токен (английский)
# ~2 символа = 1 токен (русский)
# ~3/4 слова = 1 токен (английский)
# ~1/2 слова = 1 токен (русский)
# 1 страница A4 (~500 слов) ≈ 600–800 токенов

# ── Контекстные окна (2025) ───────────────────────────
# gpt-4o:                 128K input / 16K max output
# gpt-4o-mini:            128K input / 16K max output
# claude-sonnet-4:        200K input / 64K max output
# claude-haiku-4:         200K input / 16K max output
# gemini-2.0-flash:         1M input / 8K max output

# ── Правило «потеря в середине» ───────────────────────
# Важное → в начало (system prompt) или конец (последнее сообщение)
# Середина длинного контекста → модель хуже «помнит»

# ── Стоимость (примерно, $/1M токенов, 2025) ─────────
# gpt-4o:        $2.50 in / $10 out
# gpt-4o-mini:   $0.15 in / $0.60 out
# claude-sonnet: $3.00 in / $15 out / $0.30 cached
# claude-haiku:  $0.80 in / $4.00 out / $0.08 cached

# ── Overhead сообщений чата ───────────────────────────
# ~4 токена на каждое сообщение (разметка роли)
# ~3 токена на начало ответа ассистента

Практическое задание

Задание: токенный аудит агента

  1. Установи tiktoken, напиши функцию count_tokens(text: str, model: str) -> int
  2. Возьми любой системный промпт для агента (минимум 200 слов). Подсчитай его стоимость для 1000 запросов в день у каждой модели из таблицы
  3. Напиши функцию trim_to_budget(messages, max_tokens) — обрезает историю, сохраняя system prompt и последнее сообщение
  4. Реализуй estimate_cost(input_tokens, output_tokens, model) -> float и добавь её в реальный вызов LLM: выводи стоимость каждого запроса в лог
  5. Сравни: сколько токенов занимает одно и то же предложение на русском и английском?

Что дальше