Когда решил разобраться, как работают трансформеры в деталях, столкнулся с тем, что большинство туториалов и реализаций — на Python. Это классика, но для меня Python — это чаще black box: я запускаю код, поправляю параметры, но не всегда понимаю, что именно происходит “под капотом”. Чтобы снять этот вопрос, решил построить LLM (large language model) с нуля — но не на Python, а на TypeScript.
Почему TypeScript? В первую очередь, чтобы выжать максимум понимания. Переписывая идеи трансформеров в другом языке, приходится буквально заново осваивать нюансы реализации, а не просто копировать чужие паттерны. Кроме того, TypeScript — это не только фронтенд-инструмент, а удобный язык с типизацией, который помогает ловить ошибки на этапе компиляции. В итоге получилась поэтапная реализация, где разобраны основные части: даталоадеры, эмбеддинги, attention.
В этой статье хочу поделиться тем, что удалось сделать, где были сложности и что из этого вышло — без розовых очков и с практическим взглядом.
Зачем строить LLM на TypeScript?
Если вы когда-то пытались копаться в исходниках Hugging Face или OpenAI, то знаете: часто там десятки тысяч строк, куча оптимизаций, GPU-ускорение и сложные зависимости. С одной стороны — удобно, с другой — сложно разобраться, как именно работает attention или position encoding.
TypeScript подходит, чтобы:
- Писать код максимально близко к исходной математике (без магии, «черных ящиков»).
- Использовать строгую типизацию, чтобы избежать бессмысленных багов.
- Запускать и отлаживать модель в привычной среде без сложных ML-фреймворков.
- Получать быстрый фидбек и менять архитектуру по ходу эксперимента.
В итоге это отличный способ понять, почему именно так устроен трансформер, как он обучается и почему attention — ключ к работе модели.
Основные этапы и что сделано
Даталоадеры и подготовка данных
Первый блок — загрузка и подготовка текста. На практике я делал минимальный токенайзер, который превращает текст в числа (индексы слов или subword). В TypeScript пришлось аккуратно обрабатывать строки и создать эффективный batch loader, который формирует пакеты данных для обучения.
Это оказалось не тривиально, потому что нужно было учесть:
- Разную длину предложений.
- Паддинг и маскирование (чтобы модель не смотрела на паддинги).
- Случайное перемешивание данных для обучения.
Сделал класс DataLoader, который умеет выдавать mini-batches с нужным форматированием.
Эмбеддинги
Дальше — эмбеддинги. Это преобразование токенов в векторы фиксированной размерности. Здесь всё стандартно: создал слой эмбеддинга как матрицу весов, которая обучается вместе с моделью. В TypeScript это просто двумерный массив с градиентами (градиенты считаю вручную, без автодифа).
Практическая боль — отсутствие готовых библиотек для работы с матрицами и градиентами. В итоге написал простейшие операции с векторами и матрицами (умножение, сложение, транспозиция).
Внимание (Attention)
Самая интересная часть — механизм attention. Он позволяет модели фокусироваться на разных частях входа.
Реализовал scaled dot-product attention:
function scaledDotProductAttention(
q: number[][],
k: number[][],
v: number[][]
): number[][] {
const dk = k[0].length;
const scores = matMul(q, transpose(k)).map(row =>
row.map(score => score / Math.sqrt(dk))
);
const weights = softmax(scores);
return matMul(weights, v);
}
Здесь пришлось самостоятельно писать матричные операции и softmax, следить, чтобы не было переполнений и сделать маскирование для будущих токенов при генерации.
Обратное распространение (backpropagation)
Пожалуй, самый сложный этап. В TypeScript нет автодифа, поэтому все градиенты считаю вручную. Это сильно тормозит, но зато точно понимаешь, как считаются производные и как градиенты идут по слоям.
Процесс напоминает написание своего маленького фреймворка для обучения нейросетей.
Где TypeScript помогает, а где мешает
Плюсы
- Строгая типизация помогает не напортачить с форматом данных.
- Возможность быстро дебажить код, используя привычные инструменты фронтенд-разработчика.
- Не нужно ставить и настраивать Python-окружение.
- Можно интегрировать с другими веб-инструментами и визуализировать результаты.
Минусы
- Отсутствие GPU и высокопроизводительных библиотек — обучение идет медленно.
- Нет автодифференцирования, приходится тратить время на ручные градиенты.
- Много низкоуровневой математики, которую в Python закрывают фреймворки.
- Реализация ограничена по масштабируемости — это скорее учебный проект.
Что дальше и кому это полезно
Если вы senior dev и хотите понять, как устроена архитектура LLM, такая реализация — отличный способ снять “магические” слои. TypeScript здесь — инструмент для глубокого погружения, а не для продакшена.
Для тех, кто хочет добавить vibe coding в AI-разработку, это упражнение помогает прочувствовать, что происходит “под капотом”. Можно экспериментировать с разными вариантами attention, менять размер эмбеддинга, исследовать влияние position encoding.
Однако если цель — быстро сделать что-то, что работает и масштабируется — лучше использовать проверенные Python-библиотеки и GPU. Но если хочется прокачать понимание и получить hands-on experience с минимальными зависимостями — эта дорога стоит того.
Итог
Переписывать LLM на TypeScript — это не про скорость и удобство, а про понимание. Это заставляет разбираться в деталях, ловить нюансы и не упускать из виду, как устроена модель внутри.
Если хотите попробовать что-то подобное — начните с простого трансформера, реализуйте даталоадер и эмбеддинги, затем attention, и постепенно переходите к обучению. Со временем можно добавить agentic workflows или встроить модель в веб-интерфейс для общения.
В моем опыте это не просто упражнение, а способ выйти за рамки шаблонных туториалов и почувствовать модель, а не просто использовать её как черный ящик. Рекомендую всем, кто хочет не просто юзать AI, а понимать его на уровне кода.
Попробуй сам: Cursor — AI-редактор для разработчиков.
Источник: https://book.humanexperiences.io/