Фундамент, без которого не построить ни одно 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
Вопросы:
- Поместится ли модель в один GPU?
- Какой максимальный batch_size возможен?
- Что изменится при квантизации до 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 запросов
Ответ:
- Да, в fp16 модель занимает 26GB, влезает в 40GB
- Максимум 4 параллельных запроса с контекстом 4096
- При int8: модель 13GB, до 8 параллельных запросов
Задача 2: Анализ токенизации для оценки стоимости
Условие:
Бизнес хочет обрабатывать 10,000 клиентских обращений в день через GPT-4 API. Средняя длина обращения: 500 символов на русском. Нужен ответ ~200 символов.
Стоимость GPT-4: $0.03/1K input tokens, $0.06/1K output tokens.
Вопросы:
- Оцените дневную стоимость
- Как снизить расходы?
Решение:
# Эмпирическое соотношение для русского текста в 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/месяц
Способы оптимизации:
- GPT-3.5-turbo вместо GPT-4: в 10-20 раз дешевле
- Батчинг похожих запросов
- Кэширование ответов на частые вопросы
- Собственная модель (ruGPT, Saiga) — фиксированная стоимость инфраструктуры
Задача 3: Выбор архитектуры модели
Условие:
Три задачи для enterprise-системы:
- Классификация обращений по 50 категориям
- Генерация ответов на типовые вопросы
- Извлечение сущностей (ФИО, даты, суммы) из договоров
Вопрос: Какую архитектуру (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.
Вопросы:
- Как диагностировать проблему?
- Какие решения предложить?
Решение:
# Диагностика: проверяем, сколько токенов занимает диалог
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: Теория (обязательно)
- Объясните своими словами, почему attention имеет сложность O(n²) по памяти. Как это влияет на максимальную длину документа?
- Сравните encoder-only и decoder-only архитектуры. Заполните таблицу:
| Критерий | Encoder (BERT) | Decoder (GPT) |
|---|---|---|
| Тип attention | ? | ? |
| Типовые задачи | ? | ? |
| Скорость inference | ? | ? |
| Размер моделей | ? | ? |
- Рассчитайте, сколько токенов займёт этот текст в GPT-4 и в ruGPT-3:
"Уважаемый клиент! Ваша заявка №12345 на потребительский кредит
в размере 500 000 рублей одобрена. Срок кредита: 36 месяцев.
Процентная ставка: 12.9% годовых."
Уровень 2: Практика (рекомендуется)
- Напишите код для анализа токенизации:
# Установите: pip install transformers tiktoken
# Сравните токенизацию одного текста разными токенизаторами:
# - tiktoken (GPT-4)
# - LLaMA tokenizer
# - ruGPT tokenizer (sberbank-ai/rugpt3large_based_on_gpt2)
# Для каждого выведите:
# - Количество токенов
# - Список токенов
# - Среднее количество символов на токен
- Проведите эксперимент с attention visualization:
# Используйте bertviz или ecco для визуализации attention
# Возьмите предложение: "Банк одобрил заявку, потому что клиент предоставил все документы"
# Проанализируйте:
# - На что обращает внимание слово "одобрил"?
# - Как связаны "заявку" и "документы"?
Уровень 3: Архитектурный (для продвинутых)
- Спроектируйте решение:
Компания хочет внедрить AI-помощника для юридического отдела. Требования:
- Анализ договоров до 100 страниц
- Ответы на вопросы по содержанию
- Выделение ключевых условий и рисков
- Работа с конфиденциальными данными (on-premise)
Подготовьте:
- Выбор модели с обоснованием
- Расчёт требований к инфраструктуре
- Архитектуру решения (как обрабатывать длинные документы?)
- Оценку стоимости владения (TCO) на год
Чек-лист самопроверки
После прочтения вы должны уметь:
- [ ] Объяснить, как работает self-attention на пальцах
- [ ] Нарисовать архитектуру Transformer-слоя
- [ ] Рассчитать память для модели и KV-cache
- [ ] Выбрать encoder/decoder для конкретной задачи
- [ ] Оценить эффективность токенизации для русского текста
- [ ] Диагностировать проблемы с контекстом
- [ ] Объяснить бизнесу, почему длинные документы стоят дороже








