M00. Старт: окружение, модели и инструменты
Теория модуля
Зачем этот модуль
За ближайшие месяцы вы разберёте устройство больших языковых моделей, научитесь строить RAG-системы и мультиагентные приложения на LangChain и LangGraph, писать MCP-серверы и оценивать качество LLM-систем. Всё это требует рабочего места, на котором:
- любой урок запускается одной командой и через полгода запустится так же;
- модели доступны из кода по единому интерфейсу — неважно, крутятся они у вас на ноутбуке или на сервере;
- медленная генерация не превращает каждый перезапуск ноутбука в ожидание.
Этот модуль короткий, но он закладывает понятия, которые пройдут через весь курс: OpenAI-совместимый API, токены и контекстное окно, скорость генерации, память, которую занимает модель, и кассеты — записанные ответы моделей.
Уроки модуля: 01-environment (окружение), 02-docker (контейнеры и сервисы), 03-models (первый разговор с моделью), 04-opencode (AI-агент как напарник). Упражнения — в exercises/.
Кто такой AI-инженер
Ещё недавно, чтобы применить машинное обучение, нужно было собрать данные, обучить модель и вывести её в продакшн. С появлением больших языковых моделей (LLM) центр тяжести сместился: самые способные модели уже обучены, и основная работа — построить вокруг модели надёжную систему.
| Роль | Главный вопрос | Типичные задачи |
|---|---|---|
| Data scientist | Что говорят данные? | анализ, эксперименты, статистика |
| ML-инженер | Как обучить и выкатить модель? | признаки, обучение, сервинг, мониторинг |
| AI-инженер | Как построить продукт на готовых моделях? | промпты, RAG, агенты, инструменты, оценка качества, стоимость |
AI-инженер не обязан обучать модели с нуля, но обязан понимать, как они устроены: почему модель «забывает» середину длинного контекста, почему генерация медленнее чтения промпта, откуда берутся галлюцинации и почему агент зацикливается. Поэтому курс начинается с устройства LLM и только потом переходит к приложениям.
Карта курса
- Фундамент-экспресс (M01–M05) — математика, классическое ML, PyTorch, NLP и обзор остальных областей. Сжато: только то, что понадобится дальше.
- Устройство LLM (M06–M08) — трансформер своими руками, как обучают LLM, как устроен инференс.
- Работа с LLM (M09–M13) — промпты и контекст, структурированный вывод и вызов инструментов, эмбеддинги, RAG.
- Агенты (M14–M21) — агенты с нуля, LangChain, LangGraph, MCP, экосистема фреймворков, coding-агенты.
- Надёжность (M22–M23) — оценка качества, observability, безопасность.
- Продакшн и смежные темы (M24–M27) и капстоун-проекты.
Как учиться по курсу
Каждая тема проходит спираль интуиция → формализм → код с нуля → фреймворк → проект. Код с нуля — не упражнение ради упражнения: только написав ReAct-агента на чистом Python, понимаешь, что именно делает за тебя LangGraph и где он может ошибиться.
Второй принцип — всё измеряется. Улучшение промпта или RAG-пайплайна считается улучшением, только если оно видно на тестовом наборе. Привычка измерять — главное, что отличает инженера от человека, который «потыкал в чат».
Окружение: воспроизводимость прежде всего
Проблема «у меня работает»
Экосистема Python для ML обновляется очень быстро. Урок, написанный под langchain одной версии, через полгода может не запуститься на другой. Решение — зафиксировать всё:
- версию интерпретатора (в курсе — Python 3.12);
- точные версии всех пакетов, включая транзитивные зависимости, — в lock-файле
uv.lock; - системное окружение — в Docker-образе.
uv
uv — менеджер пакетов и окружений для Python. Он заменяет pip, venv, pip-tools, pyenv и pipx, а работает в десятки раз быстрее за счёт реализации на Rust и глобального кэша пакетов.
| Команда | Что делает |
|---|---|
uv sync |
создаёт .venv и ставит ровно то, что записано в uv.lock |
uv add httpx |
добавляет зависимость в pyproject.toml и обновляет uv.lock |
uv run python script.py |
запускает команду внутри окружения проекта |
uv python install 3.12 |
скачивает нужную версию Python |
pyproject.toml — это намерение («мне нужен httpx версии не ниже 0.28»), а uv.lock — факт («стоит httpx 0.28.1 с такими-то хэшами»). В git коммитятся оба файла.
У каждого модуля курса своё окружение (modules/Mxx/pyproject.toml). Так зависимости модуля про LangGraph не конфликтуют с зависимостями модуля про CrewAI, а общая библиотека курса mlcourse подключается к каждому модулю как локальный пакет.
Контейнеры и Apple Silicon
Образ, контейнер, volume
Контейнер — изолированный процесс со своей файловой системой, сетью и набором программ. В отличие от виртуальной машины, контейнер использует ядро хостовой ОС, поэтому стартует за секунды.
- Образ (image) — неизменяемый шаблон: ОС, Python, uv, системные библиотеки. Описывается
Dockerfile. - Контейнер — запущенный экземпляр образа.
- Volume — папка, которая живёт дольше контейнера. В курсе через volume в контейнер монтируется весь репозиторий: правите файлы в редакторе на Mac — контейнер сразу видит изменения.
- Docker Compose — описание нескольких связанных контейнеров в одном YAML-файле: JupyterLab, векторная база Qdrant, Postgres и т. д.
Почему в Docker на Mac нет GPU
Docker Desktop на macOS запускает контейнеры внутри лёгкой Linux-виртуалки: ядро Linux нужно контейнерам, а у macOS своё ядро. Графический ускоритель Apple (Metal) внутрь этой виртуалки не пробрасывается. Отсюда правило курса:
- в Docker — всё, что не требует GPU: LLM-приложения, RAG, агенты, базы данных, сервисы;
- нативно на Mac — обучение нейросетей (PyTorch с бэкендом MPS, MLX) и сама Ollama, которая использует Metal для ускорения.
Контейнер обращается к Ollama на хосте по специальному имени host.docker.internal. Библиотека курса делает это автоматически: если код запущен в контейнере, localhost в настройках заменяется на host.docker.internal.
Unified memory: почему Mac вообще тянет LLM
В обычном ПК у видеокарты своя память (VRAM), и модель должна в неё поместиться. У Apple Silicon общая (unified) память: CPU и GPU работают с одним и тем же пулом RAM. Поэтому Mac с 16 ГБ может запустить модель, которой нужно ~6–8 ГБ, — большинству видеокарт с 8 ГБ VRAM это не под силу.
Обратная сторона — пропускная способность памяти. У M1 она около 68 ГБ/с, у серверных GPU — 2–3 ТБ/с. Ниже мы увидим, что именно она ограничивает скорость генерации.
Модели: где живут и как с ними говорить
Три способа получить модель
| Способ | Примеры | Плюсы | Минусы |
|---|---|---|---|
| Локально | Ollama, llama.cpp, MLX, LM Studio | бесплатно, приватно, офлайн | ограничено памятью ноутбука, медленнее |
| Свой сервер | vLLM, SGLang, llama.cpp server | большие модели, контроль | нужно железо и администрирование |
| Облачный API | OpenAI, Anthropic, OpenRouter и др. | самые сильные модели, без железа | платно, данные уходят провайдеру |
OpenAI-совместимый API
Почти все серверы и провайдеры сегодня говорят на одном языке — на HTTP-API в формате OpenAI. Это де-факто стандарт, и курс опирается именно на него: код урока не знает, где живёт модель, — меняется только адрес в .env.
Основные эндпоинты:
| Эндпоинт | Назначение |
|---|---|
GET /v1/models |
список доступных моделей |
POST /v1/chat/completions |
ответ модели на диалог |
POST /v1/embeddings |
векторные представления текстов |
Анатомия запроса
{
"model": "qwen3:8b",
"messages": [
{"role": "system", "content": "Ты — лаконичный ассистент."},
{"role": "user", "content": "Что такое токен?"}
],
"temperature": 0.7,
"max_tokens": 200,
"stream": false
}messages— история диалога. Роли:system(инструкции),user(пользователь),assistant(прошлые ответы модели),tool(результаты инструментов, M10).temperature— «смелость» выбора следующего токена: 0 — почти детерминированно, 1 — разнообразно (подробно в M08).max_tokens— предел длины ответа.- Дополнительно:
tools(вызов инструментов),response_format(JSON по схеме),stream(потоковая выдача).
API не хранит историю. Каждый запрос содержит весь диалог целиком, и модель читает его заново. «Память» чат-бота — это просто список messages, который приложение отправляет каждый раз. Отсюда растущая стоимость длинных диалогов и целая дисциплина управления контекстом (M09, M16).
Анатомия ответа
{
"model": "qwen3:8b",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "Токен — это ..."},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 27, "completion_tokens": 64, "total_tokens": 91}
}finish_reason— почему модель остановилась:stop(закончила сама),length(упёрлась вmax_tokens),tool_calls(хочет вызвать инструмент).usage— сколько токенов прочитано и сгенерировано. Облачные провайдеры берут деньги именно за токены.
Потоковая выдача (streaming)
При "stream": true сервер не ждёт конца генерации, а отправляет кусочки по мере готовности в формате Server-Sent Events (SSE):
data: {"choices":[{"delta":{"content":"Токен"}}]}
data: {"choices":[{"delta":{"content":" — это"}}]}
data: [DONE]
Каждое событие — строка data: <JSON> и пустая строка после неё; конец потока — data: [DONE]. Пользователь видит первые слова через доли секунды вместо ожидания всего ответа. В упражнении ex03 вы напишете разбор SSE-потока сами.
Токены и контекстное окно
Модель работает не с символами и не со словами, а с токенами — кусочками текста из её словаря (обычно 100–250 тысяч элементов). Частые слова — один токен, редкие разбиваются на несколько. Для русского текста токенов, как правило, больше, чем для английского того же смысла; как устроена токенизация, разберём в M07.
Контекстное окно — максимальное число токенов, которое модель обрабатывает за раз: промпт (весь диалог) плюс ответ. Если сумма больше окна, сервер вернёт ошибку или обрежет начало диалога.
Размер окна задаётся не только моделью, но и сервером. Qwen3 обучена на длинных контекстах, но Ollama по умолчанию выделяет лишь 4096 токенов — для агентов этого мало. В курсе Ollama запускается с OLLAMA_CONTEXT_LENGTH=16384; команда make llm-check проверяет это.
Рассуждающие модели
Современные модели (Qwen3, DeepSeek-R1 и др.) умеют перед ответом «думать вслух»: генерировать скрытую цепочку рассуждений (thinking), а потом — ответ. Это повышает качество на сложных задачах, но стоит времени: простой вопрос «2+2?» может превратиться в 500 токенов размышлений.
Серверы возвращают рассуждения отдельно от ответа: llama.cpp — в поле reasoning_content, Ollama — в поле reasoning. Выключается режим по-разному:
| Сервер | Как выключить thinking |
|---|---|
| Ollama | "reasoning_effort": "none" |
| llama.cpp | "chat_template_kwargs": {"enable_thinking": false} |
Функция mlcourse.no_thinking() возвращает оба параметра сразу — каждый сервер игнорирует чужой. В агентных циклах курса thinking по умолчанию выключен ради скорости; когда он помогает, а когда нет, измерим в M07 и M09.
Скорость и память
Две фазы генерации
Ответ модели рождается в две фазы:
- Prefill — модель читает весь промпт. Токены промпта обрабатываются параллельно, одной большой операцией над матрицами, поэтому эта фаза быстрая: сотни и тысячи токенов в секунду.
- Decode — модель генерирует ответ по одному токену: каждый следующий зависит от предыдущего.
Отсюда две метрики:
- TTFT (time to first token) — время до первого токена ответа, определяется в основном prefill;
- скорость генерации — токенов в секунду в фазе decode.
Почему decode упирается в память
Чтобы сгенерировать один токен, нужно прочитать из памяти все веса модели. Вычислений при этом немного, а данных — гигабайты. Поэтому верхняя граница скорости генерации — это пропускная способность памяти, делённая на размер модели:
\[ \text{ток/с} \;\lesssim\; \frac{\text{пропускная способность памяти, ГБ/с}}{\text{размер весов, ГБ}}. \]
Для qwen3:8b в 4-битной квантизации (файл ≈ 5,2 ГБ) на M1 (≈ 68 ГБ/с):
\[ \frac{68}{5{,}2} \approx 13 \text{ ток/с}. \]
На практике получается около 8 ток/с — реальные системы достигают 50–70 % теоретического предела. Эта простая формула объясняет многое: почему квантизация ускоряет генерацию (весов меньше — читать меньше), почему серверные GPU с памятью HBM быстрее в десятки раз и почему prefill и decode оптимизируют разными способами (M08).
Сколько памяти занимает модель
Веса. Модель из \(N\) параметров при \(b\) битах на параметр занимает
\[ M_\text{веса} = N \cdot \frac{b}{8} \text{ байт}. \]
8 млрд параметров в формате bf16 (16 бит) — 16 ГБ, в 4-битной квантизации — около 5 ГБ (часть слоёв хранится точнее, поэтому в среднем выходит ≈ 5 бит на вес). Квантизация подробно разбирается в M08.
KV-кэш. Чтобы не пересчитывать весь диалог на каждом шаге decode, модель хранит для каждого прочитанного токена ключи и значения (keys, values) механизма внимания во всех слоях:
\[ M_\text{KV} = 2 \cdot L \cdot H_\text{kv} \cdot d_\text{head} \cdot s \cdot T, \]
где \(L\) — число слоёв, \(H_\text{kv}\) — число голов ключей/значений, \(d_\text{head}\) — размерность головы, \(s\) — байт на число (2 для fp16), \(T\) — число токенов в контексте, а множитель 2 — это K и V.
\(L = 36\), \(H_\text{kv} = 8\), \(d_\text{head} = 128\), \(s = 2\): \(2 \cdot 36 \cdot 8 \cdot 128 \cdot 2 = 147\,456\) байт ≈ 144 КиБ на каждый токен. Контекст 16 384 токена — около 2,4 ГБ. Вместе с весами ≈ 7,6 ГБ: на Mac с 16 ГБ помещается, но с запасом не разгуляешься. Что такое ключи, значения и головы внимания, разберём в M06.
Кэш ответов и кассеты
Даже быстрая модель отвечает секунды, медленная — минуты. Перезапускать ноутбук с десятком запросов к модели ради одной правки неудобно, а в публичном CI моделей нет вовсе. Поэтому все клиенты курса (mlcourse.get_client(), get_chat_model() и др.) пропускают запросы через кассеты — записанные пары «запрос → ответ».
- Кассеты урока лежат в
lessons/<урок>/cassettes/и коммитятся в git. - Личный кэш — в
~/.cache/mlcourse/llm/, не коммитится. - Ключ записи — хэш тела запроса. Изменили промпт, модель или температуру — это новый запрос и новый вызов модели.
LLM_CACHE |
Поведение |
|---|---|
on (по умолчанию) |
берёт готовый ответ из кассет или личного кэша; новые ответы — в личный кэш |
off |
всегда настоящий запрос |
record |
как on, но всё использованное сохраняется в кассеты урока |
refresh |
всегда настоящий запрос с перезаписью кассет |
replay |
только кассеты, без сети (так ноутбуки проверяются в CI) |
Кэширование ответов LLM — не только учебный приём. В продакшне кэш экономит деньги и время на повторяющихся запросах, а записанные ответы используются в регрессионных тестах. «Семантический кэш», который узнаёт не только одинаковые, но и похожие запросы, разберём в M24.
В кассету попадают только путь запроса, его тело и тело ответа. Адрес сервера, заголовки и ключи API не сохраняются никогда — кассеты безопасно публиковать.
Учиться вместе с AI-агентом
Coding-агенты вроде OpenCode читают код, запускают команды и правят файлы. Для обучения это обоюдоострый инструмент: агент может мгновенно решить упражнение — и вы ничему не научитесь.
В курсе настроен агент-репетитор (tutor), которому запрещено писать решения. Он объясняет, задаёт наводящие вопросы, подсказывает, где искать ошибку, и запускает тесты. Правила работы с ним:
- Сначала попытка самостоятельно, потом вопрос репетитору.
- Просите объяснить, а не сделать: «почему падает тест?» вместо «исправь».
- Проверяйте ответы агента: модели ошибаются уверенно.
Устройство coding-агентов, настройку OpenCode и написание собственного агента подробно разберём в M21.
Coding-агенту нужен большой контекст: системный промпт и описания инструментов OpenCode занимают 10–12 тысяч токенов ещё до вашего вопроса. С окном 16K агент работает на пределе и может падать с ошибкой exceeds the available context size. Для комфортной работы нужно 32K и больше.
Вопросы для самопроверки
- Чем
pyproject.tomlотличается отuv.lock? Какой из них определяет, что именно будет установлено? - Почему обучение нейросетей в курсе запускается нативно, а не в Docker?
- Вы отправили модели вопрос, потом второй вопрос отдельным запросом. Почему модель не помнит первый вопрос?
- Ответ пришёл с
finish_reason: "length". Что произошло и что делать? - Что такое TTFT и от какой фазы генерации он зависит в первую очередь?
- Оцените верхнюю границу скорости генерации модели размером 20 ГБ на GPU с пропускной способностью памяти 1000 ГБ/с.
- Во сколько раз вырастет KV-кэш, если увеличить контекст с 4K до 32K токенов? А размер весов?
- Почему кассеты не содержат адрес сервера и ключ API и почему это важно для публичного репозитория?
Ответы
pyproject.tomlописывает допустимые диапазоны версий,uv.lock— точные версии всех пакетов с хэшами. Устанавливается то, что записано вuv.lock(uv sync).- Docker Desktop на macOS запускает контейнеры в Linux-виртуалке без доступа к GPU Apple (Metal); нативно PyTorch использует MPS, а MLX и Ollama — Metal.
- API не хранит состояние: каждый запрос самостоятелен. Чтобы модель «помнила», нужно отправлять всю историю в
messages. - Ответ обрезан: модель упёрлась в
max_tokens. Увеличить лимит, попросить ответ короче или проверить, не тратит ли модель токены на рассуждения. - Время до первого токена ответа; определяется в основном фазой prefill — чтением промпта.
- \(1000 / 20 = 50\) ток/с — теоретический предел; реально — около 25–35 ток/с.
- KV-кэш растёт линейно по числу токенов — в 8 раз. Размер весов не меняется.
- В кассету пишутся только путь, тело запроса и тело ответа. Публичный репозиторий видят все, и утёкший ключ позволил бы пользоваться моделью за чужой счёт, а адрес — атаковать сервер.
Что почитать
- Документация uv — разделы Projects и Locking.
- Docker: Get started — образы, контейнеры, volumes, compose.
- Ollama: документация — API, OpenAI-совместимость, переменные окружения.
- OpenAI API Reference: Chat Completions — эталон формата, на который ориентируются остальные серверы.
- Документация OpenCode — агенты, команды, провайдеры.