Фундамент, без которого не построить ни одно LLM-решение


Когда я впервые столкнулся с задачей развернуть LLM для обработки внутренней документации, казалось — ну что там сложного. Есть API, есть документация, берём и делаем.

На третий день проект встал.

Модель не влезала в память. Длинные документы обрезались непонятно где. Latency плавало от 200ms до 8 секунд. Логи я не мог нормально интерпретировать, потому что не понимал, как эта штука работает внутри.

Пришлось остановиться и разобраться с фундаментом. С тем, как устроен Transformer, что такое attention, почему токенизация определяет половину успеха проекта.

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


Часть 1: Attention

Что было до:

Чтобы понять ценность attention, нужно понять боль, которую он решил.

До 2017 года тексты обрабатывали рекуррентными сетями. Принцип простой — читаем слово за словом, каждое следующее обрабатываем с учётом предыдущего состояния.

Предложение: "Клиент подал заявку на ипотеку в январе"

Обработка RNN:
[Клиент] → состояние_1
[подал] + состояние_1 → состояние_2  
[заявку] + состояние_2 → состояние_3
[на] + состояние_3 → состояние_4
[ипотеку] + состояние_4 → состояние_5
[в] + состояние_5 → состояние_6
[январе] + состояние_6 → финальное_состояние

Три проблемы, которые убили RNN в продакшене:

Проблема 1: Последовательная обработка

Нельзя распараллелить. Каждый шаг ждёт предыдущий. GPU простаивает на 80%, потому что его сила — параллельные вычисления, а мы заставляем его работать последовательно.

Практический эффект: обработка 10 000 документов занимает часы вместо минут.

Проблема 2: Затухающий градиент

Информация с начала текста «размывается» к концу. Сеть буквально забывает, что было в начале документа.

Документ на 3 страницы:

Страница 1: "Исполнитель: ООО Ромашка, ИНН 7712345678..."
Страница 2: [описание услуг]
Страница 3: "...Исполнитель обязуется выполнить работы в срок"

Вопрос к модели: "Какой ИНН у исполнителя?"
Ответ RNN: не знаю / галлюцинация

К третьей странице модель уже «забыла» ИНН с первой.

Проблема 3: Бутылочное горлышко

Весь смысл документа сжимается в один вектор фиксированного размера. Представьте — договор на 50 страниц нужно запихнуть в 512 чисел. Потери неизбежны.


Attention: каждое слово видит каждое

В 2017 году вышла статья «Attention Is All You Need». Идея радикально простая:

Зачем читать последовательно, если можно сразу посмотреть на всё?

Механизм attention позволяет каждому слову «посмотреть» на все остальные слова и решить, какие из них важны для понимания текущего.

Предложение: "Заявка была отклонена, потому что клиент не предоставил справку"

Когда модель обрабатывает слово "отклонена", attention показывает:
- "справку" → высокий вес (причина отклонения)
- "клиент" → средний вес (кто не предоставил)
- "Заявка" → средний вес (что отклонено)
- "была" → низкий вес (вспомогательное)

Модель сама учится определять, на что обращать внимание в зависимости от задачи.


Как работает Self-Attention: разбираем по шагам

Здесь начинается математика, но я объясню через аналогию, которая помогла мне самому.

Три роли каждого слова: Query, Key, Value

Представьте библиотеку:

  • Query (запрос) — вопрос, который вы задаёте: «Мне нужна информация о причине отклонения»
  • Key (ключ) — метки на книгах: «Эта книга про отклонения», «Эта про клиентов»
  • Value (значение) — содержимое книги, которое вы получите, если выберете её

Каждое слово в предложении одновременно выступает во всех трёх ролях.

Пошаговый расчёт

Возьмём упрощённый пример. Предложение из трёх слов: «Заявка отклонена банком»

Шаг 1: Получаем эмбеддинги слов

Каждое слово превращается в вектор чисел (как именно — разберём в части про токенизацию).

# Упрощённо, размерность 4 вместо реальных 768-4096
заявка = [0.2, 0.5, 0.1, 0.8]
отклонена = [0.9, 0.1, 0.7, 0.3]
банком = [0.4, 0.8, 0.2, 0.6]

Шаг 2: Создаём Q, K, V через умножение на матрицы весов

# Три обучаемые матрицы весов
W_q = [...] # матрица для Query
W_k = [...] # матрица для Key  
W_v = [...] # матрица для Value

# Для каждого слова получаем три вектора
Q_заявка = заявка × W_q
K_заявка = заявка × W_k
V_заявка = заявка × W_v

# И так для каждого слова

Шаг 3: Считаем веса внимания

Для каждого слова считаем, насколько оно «релевантно» каждому другому:

# Скалярное произведение Query одного слова на Key всех слов
score_заявка_к_заявка = Q_заявка · K_заявка
score_заявка_к_отклонена = Q_заявка · K_отклонена
score_заявка_к_банком = Q_заявка · K_банком

# Нормализуем через softmax, чтобы получить веса от 0 до 1
weights = softmax([score_1, score_2, score_3])
# Например: [0.2, 0.7, 0.1] — слово "заявка" больше всего внимания уделяет "отклонена"

Шаг 4: Взвешенная сумма Value

# Итоговое представление слова "заявка" с учётом контекста
output_заявка = 0.2 × V_заявка + 0.7 × V_отклонена + 0.1 × V_банком

Теперь вектор слова «заявка» содержит информацию из всего предложения, взвешенную по важности.

Формула, которую стоит запомнить

Attention(Q, K, V) = softmax(QK^T / √d_k) × V

Где √d_k — это нормализация, чтобы значения не становились слишком большими при большой размерности.


Multi-Head Attention: зачем нужно несколько «голов»

Одна голова attention — это один «взгляд» на текст. Но для понимания нужны разные типы связей:

  • Синтаксические: подлежащее-сказуемое
  • Семантические: причина-следствие
  • Референтные: на кого ссылается «он»

Multi-Head Attention — это несколько параллельных attention, каждый со своими весами W_q, W_k, W_v.

# В GPT-3: 96 голов
# В LLaMA-7B: 32 головы
# В типичной модели: 8-64 головы

head_1 = Attention(Q×W_q1, K×W_k1, V×W_v1)  # учится ловить синтаксис
head_2 = Attention(Q×W_q2, K×W_k2, V×W_v2)  # учится ловить семантику
...
head_n = Attention(Q×W_qn, K×W_kn, V×W_vn)

# Результаты конкатенируются и проецируются
output = Concat(head_1, ..., head_n) × W_o

Практический смысл для архитектора:

Количество голов влияет на:

  • Память: больше голов = больше параметров
  • Качество: до определённого предела больше голов = лучше понимание
  • Скорость: головы считаются параллельно, но результат нужно собирать

Почему это важно для архитектурных решений

Расчёт памяти для attention

Главная боль: attention требует O(n²) памяти, где n — длина последовательности.

Контекст 2048 токенов: 2048 × 2048 = 4M элементов матрицы
Контекст 8192 токена: 8192 × 8192 = 67M элементов
Контекст 32K токенов: 32768 × 32768 = 1B элементов

При float16: 1B элементов = 2GB только на матрицу attention
И это на ОДИН слой. В модели их 32-96.

Практический вывод: Когда бизнес говорит «хотим загружать документы по 100 страниц», вы должны сразу понимать — это требует специальных техник (sparse attention, sliding window) или очень дорогого железа.

Latency: откуда берётся задержка

Время inference = 
    время токенизации +
    время прохода через все слои × количество генерируемых токенов

Attention — самая тяжёлая часть каждого слоя. При генерации каждый новый токен требует пересчёта attention ко всем предыдущим.

Практический вывод: Если SLA требует ответ за 500ms, а модель генерирует 100 токенов по 50ms каждый — математика не сходится. Нужно либо менять модель, либо менять SLA, либо использовать speculative decoding.


Часть 2: Архитектура Transformer

Общая структура: encoder, decoder и их комбинации

Оригинальный Transformer из статьи 2017 года имел две части:

┌─────────────────────────────────────────────────────────┐
│                    TRANSFORMER                          │
│                                                         │
│   ┌─────────────┐              ┌─────────────┐          │
│   │   ENCODER   │              │   DECODER   │          │
│   │             │              │             │          │
│   │ Понимает    │──контекст──▶│ Генерирует  │          │
│   │ входной     │              │ выходной    │          │
│   │ текст       │              │ текст       │          │
│   └─────────────┘              └─────────────┘          │
│                                                         │
└─────────────────────────────────────────────────────────┘

Но современные модели используют разные комбинации:

Encoder-only: BERT и его потомки

Вход: [CLS] Клиент жалуется на качество [SEP]
              ↓
         ┌─────────┐
         │ ENCODER │ (12-24 слоя)
         └─────────┘
              ↓
Выход: векторное представление всего текста
       или каждого токена

Где используется в enterprise:

  • Классификация обращений (Тинькофф, Сбер)
  • Поиск по базе знаний (semantic search)
  • NER: извлечение сущностей из документов
  • Определение тональности отзывов

Почему encoder: Видит весь текст сразу (bidirectional attention). Для классификации не нужно генерировать текст — нужно понять входной.

Decoder-only: GPT, LLaMA, GigaChat

Вход: Напиши ответ клиенту на жалобу:
              ↓
         ┌─────────┐
         │ DECODER │ (32-96 слоёв)
         └─────────┘
              ↓
Выход: Уважаемый клиент, благодарим за обращение...
       (генерируется токен за токеном)

Где используется:

  • Генерация ответов в чат-ботах
  • Суммаризация документов
  • Генерация кода
  • Любые задачи, где нужен текстовый output

Ключевое отличие: Causal (причинный) attention — каждый токен видит только предыдущие, не последующие. Это позволяет генерировать текст последовательно.

Маска causal attention:

            Клиент  подал  заявку  на   кредит
Клиент      [1      0      0       0    0    ]
подал       [1      1      0       0    0    ]
заявку      [1      1      1       0    0    ]
на          [1      1      1       1    0    ]
кредит      [1      1      1       1    1    ]

1 = может видеть, 0 = замаскировано

Encoder-Decoder: T5, BART

Вход (encoder): Переведи на английский: Заявка одобрена
                        ↓
                   ┌─────────┐
                   │ ENCODER │
                   └─────────┘
                        ↓ контекст
                   ┌─────────┐
                   │ DECODER │
                   └─────────┘
                        ↓
Выход: Application approved

Где используется:

  • Машинный перевод
  • Суммаризация с сильным сжатием
  • Задачи seq2seq с сильно отличающимся выходом

Детальная анатомия одного слоя Transformer

Каждый слой (а их в модели 12-96) состоит из:

┌────────────────────────────────────────────────────────────┐
│                    TRANSFORMER LAYER                       │
│                                                            │
│   Вход (эмбеддинги или выход предыдущего слоя)             │
│                          ↓                                 │
│   ┌──────────────────────────────────────────┐             │
│   │         Multi-Head Attention             │             │
│   └──────────────────────────────────────────┘             │
│                          ↓                                 │
│                   Add & LayerNorm ←── residual connection  │
│                          ↓                                 │
│   ┌──────────────────────────────────────────┐             │
│   │         Feed-Forward Network             │             │
│   │        (два линейных слоя + активация)   │             │
│   └──────────────────────────────────────────┘             │
│                          ↓                                 │
│                   Add & LayerNorm ←── residual connection  │
│                          ↓                                 │
│   Выход (на следующий слой или финальный)                  │
│                                                            │
└────────────────────────────────────────────────────────────┘

Feed-Forward Network: где живут «знания»

После attention идёт FFN — два линейных слоя:

FFN(x) = Linear2(GELU(Linear1(x)))

# Размерности:
# Linear1: d_model → d_ff (обычно d_ff = 4 × d_model)
# Linear2: d_ff → d_model

# Пример для LLaMA-7B:
# d_model = 4096
# d_ff = 11008
# Параметров в одном FFN: 4096×11008 + 11008×4096 ≈ 90M

Важно для архитектора: FFN занимает около 2/3 параметров модели. Именно здесь, по текущим исследованиям, хранятся «факты» — знания модели о мире.

Residual Connections: почему глубокие сети вообще работают

output = LayerNorm(x + Attention(x))
output = LayerNorm(output + FFN(output))

Без residual connections градиенты затухают, и сеть глубже 5-6 слоёв не обучается. С ними — можно строить сети в 100+ слоёв.

Layer Normalization: стабилизация обучения

# Нормализуем каждый вектор к среднему 0 и std 1
LayerNorm(x) = γ × (x - mean(x)) / std(x) + β

# γ и β — обучаемые параметры

Практический нюанс: Есть Pre-LN (норма до attention) и Post-LN (после). Современные модели чаще используют Pre-LN — стабильнее обучается.


Positional Encoding: как модель понимает порядок слов

Attention сам по себе не знает порядка. Для него «кот съел мышь» и «мышь съела кота» — одинаковы (bag of words).

Решение: добавляем к эмбеддингам информацию о позиции.

Синусоидальное кодирование (оригинальный Transformer)

PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

# pos — позиция токена (0, 1, 2, ...)
# i — индекс в векторе эмбеддинга

Почему синусы: позволяет модели легко вычислять относительные позиции (sin(a+b) раскладывается через sin(a), cos(a), sin(b), cos(b)).

RoPE: Rotary Position Embedding (LLaMA, современные модели)

# Вместо сложения — поворот вектора в комплексной плоскости
# Позволяет лучше обобщаться на длины больше, чем в обучении

Практический смысл: Модель с RoPE, обученная на контексте 4K, может работать (с некоторой деградацией) на 8K-16K. С синусоидальным — нет.

ALiBi: Attention with Linear Biases

# Штраф за расстояние между токенами
attention_score -= m × |позиция_query - позиция_key|

Практический смысл: Ещё лучше экстраполяция на длинные контексты.


Часть 3: Токенизация — фундамент

Токенизация определяет успех проекта

Реальная история: команда внедряла LLM для обработки технической документации. Модель отлично работала на английском, но на русском — каша. Причина нашлась в токенизации.

Английский: "application" = 1 токен
Русский: "приложение" = 4 токена ["при", "лож", "ени", "е"]

Результат:
- Контекст заканчивается быстрее (в 2-3 раза меньше текста влезает)
- Модель хуже понимает морфологию
- Inference медленнее (больше токенов генерировать)
- Стоимость API выше (платим за токены)

Как работает токенизация: от символов к токенам

Уровень 1: Посимвольная токенизация

"Заявка" → ["З", "а", "я", "в", "к", "а"]

Проблемы:

  • Очень длинные последовательности
  • Модель должна сама учить, что «З»+»а»+»я»+»в»+»к»+»а» = заявка

Уровень 2: Пословная токенизация

"Заявка одобрена" → ["Заявка", "одобрена"]

Проблемы:

  • Словарь огромный (сотни тысяч слов)
  • Что делать с опечатками? «Заяввка» → [UNK]
  • Что делать с новыми словами? «криптокошелёк» → [UNK]

Уровень 3: Subword токенизация (BPE, WordPiece, SentencePiece)

Золотая середина: разбиваем на частотные подслова.

"Заявка" → ["За", "явка"] или ["Заяв", "ка"]
"криптокошелёк" → ["крипто", "кошел", "ёк"]
"Заяввка" (опечатка) → ["За", "яв", "в", "ка"]

BPE: Byte Pair Encoding — алгоритм по шагам

BPE — самый распространённый алгоритм. Используется в GPT, LLaMA.

Обучение токенизатора

Шаг 0: Начинаем с посимвольного словаря
Словарь: {а, б, в, ..., я, A, B, ..., пробел, ...}

Корпус: "заявка заявка заявка одобрена"

Шаг 1: Считаем частоты пар символов
"за" — 3 раза
"ая" — 3 раза  
"яв" — 3 раза
"вк" — 3 раза
"ка" — 3 раза
"од" — 1 раз
...

Шаг 2: Самая частая пара → новый токен
Добавляем "за" в словарь
Корпус: "за_явка за_явка за_явка одобрена"

Шаг 3: Повторяем
"яв" — 3 раза → добавляем
Корпус: "за_яв_ка за_яв_ка за_яв_ка одобрена"

...продолжаем до нужного размера словаря (32K-100K токенов)

Применение токенизатора

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b")

text = "Клиент подал заявку на ипотечный кредит"
tokens = tokenizer.tokenize(text)
# ['▁Клиент', '▁подал', '▁за', 'явку', '▁на', '▁ип', 'оте', 'чный', '▁кредит']

token_ids = tokenizer.encode(text)
# [1, 14205, 43591, 1107, 29599, 603, 5. 1772, 11658, 5765, 2]
# 1 = <s> (начало), 2 = </s> (конец)

Практические проблемы токенизации в enterprise

Проблема 1: Русский язык токенизируется неэффективно

# Сравнение эффективности токенизации

english_text = "The client submitted a mortgage application"
russian_text = "Клиент подал заявку на ипотеку"

# GPT-4 tokenizer:
len(tokenizer.encode(english_text))  # ~7 токенов
len(tokenizer.encode(russian_text))  # ~15 токенов

# В 2 раза больше токенов = в 2 раза дороже API
# = в 2 раза меньше контекста
# = в 2 раза медленнее генерация

Решение: Использовать модели с токенизатором, обученным на русском корпусе (GigaChat, YandexGPT, ruGPT).

Проблема 2: Специальная терминология

# Медицинская документация
text = "Диагноз: гастроэзофагеальная рефлюксная болезнь (ГЭРБ)"

# Стандартный токенизатор:
# ["гастро", "эзо", "фаге", "альная", "реф", "люкс", "ная", ...]
# 15+ токенов на один термин

# Модель может не понимать, что это единый концепт

Решение:

  • Дообучение токенизатора на доменном корпусе
  • Добавление специальных токенов для терминов
  • Использование моделей, обученных на специализированных данных

Проблема 3: Числа и даты

text = "Сумма кредита: 15789432 рублей, дата: 23.11.2024"

# Токенизация чисел:
# "15789432" → ["157", "89", "432"] или ["1", "578", "9432"]
# Модель плохо понимает числовые отношения

# "23.11.2024" → ["23", ".", "11", ".", "2024"] или ["23", ".", "11", ".", "20", "24"]
# Теряется понимание формата даты

Решение:

  • Нормализация чисел перед подачей в модель
  • Использование специальных форматов: «дата: двадцать третье ноября две тысячи двадцать четвёртого года»
  • Вынос числовых вычислений во внешние инструменты (калькулятор)

Проблема 4: Специальные символы и форматирование

# JSON в тексте
text = '{"client_id": 123, "status": "approved"}'

# Каждая скобка, кавычка, двоеточие — отдельный токен
# Съедает контекст, модель может "сломать" JSON при генерации

Решение:

  • Структурированный output через function calling
  • JSON mode в современных API
  • Валидация и исправление выходного JSON

Специальные токены: служебные сигналы для модели

# Типичные специальные токены:

<s>      # начало последовательности (BOS - Beginning Of Sequence)
</s>     # конец последовательности (EOS - End Of Sequence)  
<pad>    # заполнитель для выравнивания батчей
<unk>    # неизвестный токен
<mask>   # маска для MLM (BERT)

# Chat-специфичные (LLaMA-2, ChatGPT):
[INST]   # начало инструкции пользователя
[/INST]  # конец инструкции
<<SYS>>  # системный промпт
<</SYS>> # конец системного промпта

Практический пример форматирования для LLaMA-2:

prompt = """<s>[INST] <<SYS>>
Ты — помощник для обработки заявок. Отвечай кратко и по делу.
<</SYS>>

Классифицируй обращение клиента: "Не могу войти в личный кабинет, пишет неверный пароль"
[/INST]"""

Неправильное форматирование — частая причина плохого качества ответов.


Часть 4: Собираем всё вместе — полный путь текста через Transformer

От текста до ответа: пошаговый разбор

Вход: "Одобрить заявку на кредит?"

Шаг 1: ТОКЕНИЗАЦИЯ
["▁Од", "обрить", "▁заявку", "▁на", "▁кредит", "?"]
→ [15836, 22064, 43920, 603, 5765, 29973]

Шаг 2: EMBEDDING
Каждый token_id → вектор размерности d_model (4096 для LLaMA-7B)
Матрица эмбеддингов: vocab_size × d_model = 32000 × 4096

Шаг 3: POSITIONAL ENCODING
embedding[i] += positional_encoding[i]
Теперь модель знает порядок токенов

Шаг 4: ПРОХОД ЧЕРЕЗ СЛОИ (×32 для LLaMA-7B)
Для каждого слоя:
    x = LayerNorm(x)
    x = x + MultiHeadAttention(x)
    x = LayerNorm(x)
    x = x + FeedForward(x)

Шаг 5: ФИНАЛЬНАЯ ПРОЕКЦИЯ
Выход последнего слоя → Linear(d_model, vocab_size)
Получаем логиты для каждого токена словаря

Шаг 6: СЭМПЛИРОВАНИЕ
logits → softmax → вероятности
Выбираем следующий токен (greedy / top-k / top-p / temperature)

Шаг 7: ДЕКОДИРОВАНИЕ
Выбранный token_id → текст
Добавляем к контексту, повторяем с шага 4

Выход: "Да" или "Нет" или развёрнутый ответ

Визуализация размерностей для LLaMA-7B

Параметры модели:
- vocab_size: 32,000
- d_model (hidden_size): 4,096
- n_layers: 32
- n_heads: 32
- d_ff (intermediate_size): 11,008
- context_length: 4,096

Память на параметры:
- Embedding: 32,000 × 4,096 × 2 bytes = 250 MB
- Каждый слой attention: ~67 MB
- Каждый слой FFN: ~180 MB
- Всего: ~13 GB в float16

Память на inference (batch_size=1, seq_len=2048):
- KV-cache: 32 слоя × 2 (K,V) × 2048 × 4096 × 2 bytes = 1 GB
- Активации: ~2-4 GB
- Итого: 16-20 GB GPU RAM

Часть 5: Практические задачи

Задача 1: Расчёт памяти для inference

Условие:
Вам нужно развернуть LLaMA-2-13B для внутреннего чат-бота. Доступны серверы с NVIDIA A100 40GB.

Параметры модели:

  • 13B параметров
  • hidden_size: 5120
  • n_layers: 40
  • n_heads: 40
  • context_length: 4096

Вопросы:

  1. Поместится ли модель в один GPU?
  2. Какой максимальный batch_size возможен?
  3. Что изменится при квантизации до int8?

Решение:

# 1. Память на параметры
params = 13e9
bytes_per_param_fp16 = 2
model_memory = params * bytes_per_param_fp16 / 1e9  # 26 GB

# 2. KV-cache на один запрос (seq_len=4096)
kv_cache_per_request = (
    40 *        # n_layers
    2 *         # K и V
    4096 *      # seq_len
    5120 *      # hidden_size
    2           # bytes (fp16)
) / 1e9  # = 3.35 GB

# 3. Доступная память
available = 40 - 26  # = 14 GB

# 4. Максимальный batch_size
max_batch = 14 / 3.35  # ≈ 4 запроса одновременно

# 5. При int8 квантизации
model_memory_int8 = 13  # GB
available_int8 = 40 - 13  # = 27 GB
max_batch_int8 = 27 / 3.35  # ≈ 8 запросов

Ответ:

  1. Да, в fp16 модель занимает 26GB, влезает в 40GB
  2. Максимум 4 параллельных запроса с контекстом 4096
  3. При int8: модель 13GB, до 8 параллельных запросов

Задача 2: Анализ токенизации для оценки стоимости

Условие:
Бизнес хочет обрабатывать 10,000 клиентских обращений в день через GPT-4 API. Средняя длина обращения: 500 символов на русском. Нужен ответ ~200 символов.

Стоимость GPT-4: $0.03/1K input tokens, $0.06/1K output tokens.

Вопросы:

  1. Оцените дневную стоимость
  2. Как снизить расходы?

Решение:

# Эмпирическое соотношение для русского текста в GPT-4:
# ~1 токен на 2-3 символа (хуже, чем английский ~1:4)

chars_per_request = 500
tokens_per_request_input = chars_per_request / 2.5  # ≈ 200 токенов

chars_per_response = 200
tokens_per_response = chars_per_response / 2.5  # ≈ 80 токенов

# Добавляем системный промпт (~100 токенов)
total_input_tokens = (200 + 100) * 10000  # = 3M токенов
total_output_tokens = 80 * 10000  # = 800K токенов

# Стоимость
input_cost = 3000 * 0.03  # = $90
output_cost = 800 * 0.06  # = $48
daily_cost = 90 + 48  # = $138/день
monthly_cost = 138 * 30  # = $4,140/месяц

Способы оптимизации:

  1. GPT-3.5-turbo вместо GPT-4: в 10-20 раз дешевле
  2. Батчинг похожих запросов
  3. Кэширование ответов на частые вопросы
  4. Собственная модель (ruGPT, Saiga) — фиксированная стоимость инфраструктуры

Задача 3: Выбор архитектуры модели

Условие:
Три задачи для enterprise-системы:

  1. Классификация обращений по 50 категориям
  2. Генерация ответов на типовые вопросы
  3. Извлечение сущностей (ФИО, даты, суммы) из договоров

Вопрос: Какую архитектуру (encoder/decoder/encoder-decoder) выбрать для каждой задачи и почему?

Решение:

Задача 1: Классификация → ENCODER (BERT, RuBERT)
Причины:
- Не нужна генерация, только понимание
- Bidirectional attention — видит весь контекст
- Быстрый inference (один проход)
- Маленькая модель (110M-340M параметров)

Задача 2: Генерация ответов → DECODER (GPT, LLaMA, GigaChat)
Причины:
- Нужна генерация текста
- Causal attention для авторегрессии
- Можно использовать few-shot примеры

Задача 3: Извлечение сущностей → ENCODER (BERT + NER head)
Причины:
- Задача sequence labeling, не генерация
- Нужно видеть контекст с обеих сторон
- Стандартная задача для BERT-подобных моделей

Альтернатива для задачи 3: 
- Decoder с structured output (JSON mode)
- Плюс: гибче, можно извлекать сложные связи
- Минус: дороже, медленнее, менее надёжно

Задача 4: Диагностика проблемы с контекстом

Условие:
Чат-бот для внутренней документации. Пользователи жалуются: «Бот забывает, о чём мы говорили 5 сообщений назад».

Используется LLaMA-2-7B с context_length=4096.

Вопросы:

  1. Как диагностировать проблему?
  2. Какие решения предложить?

Решение:

# Диагностика: проверяем, сколько токенов занимает диалог

def diagnose_context(conversation_history, tokenizer, max_context=4096):
    full_text = "n".join(conversation_history)
    tokens = tokenizer.encode(full_text)

    print(f"Сообщений в истории: {len(conversation_history)}")
    print(f"Токенов в контексте: {len(tokens)}")
    print(f"Лимит: {max_context}")
    print(f"Заполненность: {len(tokens)/max_context*100:.1f}%")

    if len(tokens) > max_context:
        print("ПРОБЛЕМА: контекст обрезается!")
        # Показываем, что именно обрезается
        truncated = tokenizer.decode(tokens[:max_context])
        lost = tokenizer.decode(tokens[max_context:])
        print(f"Потеряно символов: {len(lost)}")

# Типичный результат:
# Сообщений в истории: 10
# Токенов в контексте: 5200
# Лимит: 4096
# Заполненность: 127%
# ПРОБЛЕМА: контекст обрезается!

Решения:

1. Sliding window с суммаризацией:
   - Храним последние N сообщений полностью
   - Старые сообщения суммаризируем

2. RAG (Retrieval-Augmented Generation):
   - Храним всю историю в векторной БД
   - Достаём релевантные части по запросу

3. Модель с большим контекстом:
   - LLaMA-2 с расширенным контекстом (rope scaling)
   - Claude (100K+ контекст)
   - GPT-4-turbo (128K контекст)

4. Иерархическая память:
   - Краткосрочная: последние 3-5 сообщений
   - Среднесрочная: суммари текущей сессии
   - Долгосрочная: профиль пользователя, предпочтения

Домашнее задание

Уровень 1: Теория (обязательно)

  1. Объясните своими словами, почему attention имеет сложность O(n²) по памяти. Как это влияет на максимальную длину документа?
  2. Сравните encoder-only и decoder-only архитектуры. Заполните таблицу:
Критерий Encoder (BERT) Decoder (GPT)
Тип attention ? ?
Типовые задачи ? ?
Скорость inference ? ?
Размер моделей ? ?
  1. Рассчитайте, сколько токенов займёт этот текст в GPT-4 и в ruGPT-3:
"Уважаемый клиент! Ваша заявка №12345 на потребительский кредит 
в размере 500 000 рублей одобрена. Срок кредита: 36 месяцев. 
Процентная ставка: 12.9% годовых."

Уровень 2: Практика (рекомендуется)

  1. Напишите код для анализа токенизации:
# Установите: pip install transformers tiktoken

# Сравните токенизацию одного текста разными токенизаторами:
# - tiktoken (GPT-4)
# - LLaMA tokenizer
# - ruGPT tokenizer (sberbank-ai/rugpt3large_based_on_gpt2)

# Для каждого выведите:
# - Количество токенов
# - Список токенов
# - Среднее количество символов на токен
  1. Проведите эксперимент с attention visualization:
# Используйте bertviz или ecco для визуализации attention
# Возьмите предложение: "Банк одобрил заявку, потому что клиент предоставил все документы"
# Проанализируйте:
# - На что обращает внимание слово "одобрил"?
# - Как связаны "заявку" и "документы"?

Уровень 3: Архитектурный (для продвинутых)

  1. Спроектируйте решение:

Компания хочет внедрить AI-помощника для юридического отдела. Требования:

  • Анализ договоров до 100 страниц
  • Ответы на вопросы по содержанию
  • Выделение ключевых условий и рисков
  • Работа с конфиденциальными данными (on-premise)

Подготовьте:

  • Выбор модели с обоснованием
  • Расчёт требований к инфраструктуре
  • Архитектуру решения (как обрабатывать длинные документы?)
  • Оценку стоимости владения (TCO) на год

Чек-лист самопроверки

После прочтения вы должны уметь:

  • [ ] Объяснить, как работает self-attention на пальцах
  • [ ] Нарисовать архитектуру Transformer-слоя
  • [ ] Рассчитать память для модели и KV-cache
  • [ ] Выбрать encoder/decoder для конкретной задачи
  • [ ] Оценить эффективность токенизации для русского текста
  • [ ] Диагностировать проблемы с контекстом
  • [ ] Объяснить бизнесу, почему длинные документы стоят дороже
Prompt Engineering: как заставить LLM делать то, что нужно

Prompt Engineering: как заставить LLM делать то, что нужно

12.01.2026 Read More
Ограничения LLM: галлюцинации, context window, knowledge cutoff

Ограничения LLM: галлюцинации, context window, knowledge cutoff

11.01.2026 Read More

Leave a Reply