Полный курс программирования: от основ до создания ИИ-ассистента

Глубокое погружение в программирование для абсолютных новичков. Курс последовательно проведет вас от базовых алгоритмов и структур данных до веб-разработки, автоматизации и создания собственного ИИ.

1. Введение в программирование и базовые типы данных

Введение в программирование и базовые типы данных

Программирование часто окружено ореолом таинственности. Со стороны может показаться, что разработчики пишут непонятные заклинания на черном экране, заставляя машины выполнять сложные задачи. На самом деле программирование — это просто способ общения. Это процесс перевода человеческих идей и логики на язык, который способен понять и выполнить компьютер.

Чтобы пройти путь от абсолютного новичка до создателя собственного ИИ-ассистента, веб-приложений и игр, необходимо начать с самого фундамента. Невозможно построить надежный небоскреб на слабом фундаменте. Точно так же невозможно написать сложную нейронную сеть, не понимая, как компьютер хранит информацию и управляет ею.

Компьютер — это невероятно быстрое, но абсолютно лишенное фантазии устройство. Он делает ровно то, что ему сказано, шаг за шагом. Программа — это и есть набор таких пошаговых инструкций. А чтобы инструкции имели смысл, они должны работать с какими-то данными. Именно поэтому наше погружение в мир IT начинается с изучения того, как программы запоминают информацию и какие виды этой информации существуют.

Что такое переменная

Представьте, что вы переехали в новый дом и раскладываете вещи по коробкам. Чтобы потом быстро найти нужную вещь, вы берете маркер и подписываете каждую коробку: «Книги», «Посуда», «Документы».

В программировании переменная — это такая же подписанная «коробка», но находится она в оперативной памяти компьютера. Это именованный контейнер, в котором программа хранит данные для их дальнейшего использования и изменения.

Когда программа запускается, она запрашивает у операционной системы немного места в памяти. Память компьютера можно представить как гигантский склад с миллионами пронумерованных ячеек. Каждая ячейка имеет свой уникальный адрес, который выглядит примерно так: 0x7fff5fbffc5c. Запоминать такие адреса человеку невероятно сложно. Поэтому программисты придумали переменные — они позволяют дать сложным адресам памяти простые, понятные человеку имена.

> Переменная — это именованная область памяти, предназначенная для хранения данных, которые могут изменяться в процессе выполнения программы.

Анатомия переменной

Любая переменная состоит из трех ключевых элементов:

  • Имя (идентификатор) — то, как мы обращаемся к переменной в коде.
  • Значение — информация, которая лежит внутри.
  • Тип данных — правило, объясняющее компьютеру, что именно лежит внутри и как с этим можно работать.
  • Рассмотрим пример из реальной жизни. Допустим, мы пишем программу для интернет-магазина. Нам нужно сохранить возраст покупателя. Мы создаем переменную, называем ее userAge и кладем в нее значение 25.

    Теперь каждый раз, когда мы напишем в коде userAge, компьютер пойдет в память, найдет «коробку» с такой надписью и достанет оттуда число 25. Если у покупателя случится день рождения, мы сможем обновить значение в этой же коробке:

    Именно поэтому она называется переменной — ее содержимое может меняться со временем.

    !Визуализация переменных в памяти компьютера

    Правила именования переменных

    Компьютер строг к правилам. Вы не можете назвать переменную как угодно. Существуют универсальные правила, применимые почти ко всем языкам программирования:

    * Имя не может начинаться с цифры. 1player вызовет ошибку, а player1 — сработает отлично. * Нельзя использовать пробелы. Вместо них используют специальные стили написания (о них ниже). * Имена чувствительны к регистру. Переменные score, Score и SCORE — это три совершенно разные коробки. * Нельзя использовать зарезервированные слова языка (например, if, for, while), так как они уже заняты под внутренние команды.

    Чтобы код был читаемым, программисты договорились использовать определенные стили (нотации) для слов, состоящих из нескольких частей:

    camelCase* (верблюжий регистр) — первое слово со строчной буквы, каждое следующее с заглавной. Пример: maxUserScore, shoppingCartTotal. Часто используется в JavaScript и Java. snake_case* (змеиный регистр) — слова пишутся со строчной буквы и разделяются нижним подчеркиванием. Пример: max_user_score, shopping_cart_total. Стандарт для Python.

    Хорошее имя переменной должно отвечать на вопрос «Что здесь хранится?». Имя x = 10 ничего не говорит программисту. А вот discountPercentage = 10 сразу дает понять, что речь идет о десятипроцентной скидке.

    Базовые типы данных

    Мы выяснили, что переменная — это коробка. Но коробки бывают разными. Вы не станете наливать суп в картонную коробку для обуви, а для хранения зимней резины не возьмете шкатулку для драгоценностей.

    Компьютеру критически важно знать тип данных, который хранится в переменной. От этого зависит, сколько места в памяти нужно выделить и какие операции разрешено выполнять с этими данными. Например, числа можно умножать, а тексты — нет.

    Давайте разберем четыре фундаментальных типа данных, которые существуют практически в любом языке программирования.

    1. Целые числа (Integer)

    Целые числа (Integer, часто сокращается до int) — это числа без дробной части. Они могут быть положительными, отрицательными или нулем.

    Зачем они нужны? Для подсчета всего, что нельзя разделить на части. Количество пользователей на сайте, количество жизней в игре, год рождения, количество товаров в корзине.

    Пример из жизни: У вас в корзине 3 яблока. Нельзя положить в корзину 3.14 яблока, если мы считаем их поштучно.

    С целыми числами можно выполнять классические математические операции: сложение, вычитание, умножение и деление.

    Если у нас есть переменная apples = 5 и pears = 3, то общее количество фруктов вычисляется просто: .

    2. Числа с плавающей точкой (Float)

    Числа с плавающей точкой (Float или Double) — это числа, имеющие дробную часть. В программировании дробная часть всегда отделяется точкой, а не запятой.

    Зачем они нужны? Для измерения непрерывных величин: веса, расстояния, температуры, денег.

    Пример из жизни: Цена товара составляет 99.99 долл. Ваш рост — 1.75 метра.

    В физике и геометрии без них не обойтись. Вспомним формулу площади круга:

    Где — площадь, — константа (примерно 3.14159, что является типичным Float), а — радиус.

    Важный подводный камень: Компьютеры хранят дробные числа в двоичной системе счисления, что иногда приводит к неожиданным результатам из-за проблем с точностью. В большинстве языков программирования, если вы попросите компьютер сложить и , вы ожидаете получить . Но компьютер выдаст что-то вроде 0.30000000000000004.

    Это происходит потому, что дробь в двоичной системе является бесконечной периодической дробью (как в десятичной). Компьютер вынужден округлять ее, чтобы уместить в памяти, из-за чего накапливается микроскопическая ошибка. Поэтому при работе с деньгами в серьезных банковских приложениях программисты часто хранят суммы в целых числах (в копейках или центах), чтобы избежать потери точности.

    3. Строки (String)

    Строка (String, часто str) — это любой текст. Буквы, слова, предложения, абзацы или даже пустой текст. Чтобы компьютер понял, что перед ним строка, а не имя переменной или команда, текст всегда заключается в кавычки (одинарные ' ' или двойные " ").

    Зачем они нужны? Для хранения имен, адресов, сообщений в чате, паролей и любого другого текста.

    Пример из жизни: userName = "Алексей", userEmail = "alex@example.com".

    Строки можно «складывать». Этот процесс называется конкатенацией (склеиванием). Если у нас есть firstName = "Иван" и lastName = "Иванов", мы можем сложить их: firstName + " " + lastName. Результатом будет новая строка "Иван Иванов".

    Типичная ошибка новичка: путаница между числами и строками, содержащими цифры. Для компьютера число 5 (Integer) и строка "5" (String) — это совершенно разные вещи. Если вы сложите два числа: . Но если вы сложите две строки: "5" + "5" = "55". Компьютер просто приклеит один символ к другому.

    4. Логический тип (Boolean)

    Логический тип (Boolean, часто bool) — это самый простой тип данных, который может принимать только одно из двух значений: Истина (True) или Ложь (False).

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

    Зачем он нужен? Это основа принятия решений в коде. Любая программа постоянно задает вопросы и действует в зависимости от ответа (Да или Нет).

    Пример из жизни: * Пользователь авторизован? isLoggedIn = True * Товар есть в наличии? inStock = False * Уровень пройден? levelCompleted = True

    Булевы значения часто появляются как результат математических сравнений. Например, выражение компьютер вычислит и превратит в значение True. А выражение превратится в False.

    Преобразование типов (Type Casting)

    В реальных проектах данные часто приходят не в том виде, в котором нам нужно. Например, когда пользователь вводит свой возраст в текстовое поле на сайте, программа получает эти данные как строку "25". Если мы захотим проверить, больше ли возраст 18 лет (выполнить математическое сравнение ), нам нужно сначала превратить строку в число.

    Этот процесс называется преобразованием типов.

    Большинство языков позволяют делать это явно. Например, в Python мы можем сказать: «Возьми строку '25' и сделай из нее целое число».

    Если попытаться преобразовать в число строку, в которой написаны буквы (например, int("Привет")), программа выдаст ошибку и остановит работу, так как компьютер не знает, какое математическое значение соответствует слову «Привет».

    Системы типизации: Статика против Динамики

    Языки программирования по-разному относятся к тому, насколько строго нужно контролировать типы данных в переменных. По этому признаку они делятся на два больших лагеря: со статической и динамической типизацией.

    Статическая типизация

    В языках со статической типизацией (Java, C++, C#) вы обязаны заранее объявить, какой тип данных будет храниться в переменной. Как только коробка создана и подписана, вы не можете положить в нее ничего другого.

    Если вы сказали, что переменная score — это целое число (Integer), вы не сможете позже положить туда строку "Победа". Компьютер выдаст ошибку еще до запуска программы.

    * Плюсы: Высокая надежность. Компьютер находит множество ошибок на этапе написания кода. Программы работают быстрее, так как компьютеру не нужно тратить время на угадывание типа данных в процессе работы. * Минусы: Нужно писать больше кода. Разработка идет медленнее.

    Динамическая типизация

    В языках с динамической типизацией (Python, JavaScript, Ruby) переменные — это универсальные коробки. Вы не указываете тип заранее. Компьютер сам догадывается о типе данных в момент, когда вы кладете туда значение.

    Более того, в процессе работы программы вы можете вытащить из коробки число и положить туда строку.

    * Плюсы: Код пишется быстрее и выглядит лаконичнее. Идеально для новичков и быстрого создания прототипов. * Минусы: Ошибки могут всплыть только во время работы программы. Если вы случайно перезаписали число строкой, а потом попытались умножить ее на 5, программа сломается прямо в руках у пользователя.

    | Характеристика | Статическая типизация (C++, Java) | Динамическая типизация (Python, JS) | | :--- | :--- | :--- | | Объявление типа | Обязательно (программистом) | Автоматически (интерпретатором) | | Изменение типа | Запрещено | Разрешено | | Поиск ошибок | До запуска (на этапе компиляции) | Во время работы программы | | Скорость разработки | Медленнее | Быстрее |

    Как это связано с созданием ИИ-ассистента?

    Возможно, сейчас вы думаете: «Я хочу создавать искусственный интеллект, зачем мне знать про какие-то целые числа и строки?».

    Дело в том, что любая, даже самая сложная система, строится из этих базовых кирпичиков. Давайте заглянем в будущее и посмотрим, как наш финальный проект — ИИ-ассистент — будет использовать базовые типы данных:

  • String (Строки): Когда пользователь пишет запрос («Какая сегодня погода?»), ИИ получает его в виде строки. Ответ ИИ также генерируется и отправляется пользователю в виде строки.
  • Float (Дробные числа): В основе машинного обучения лежат нейронные сети. Нейронная сеть — это огромный набор математических весов и вероятностей. Уверенность ИИ в том, что на картинке изображен кот, может быть равна 0.985 — это типичный Float.
  • Integer (Целые числа): ИИ будет использовать целые числа для подсчета количества обработанных запросов, лимитов API (например, разрешено 100 запросов в день) или определения длины текста.
  • Boolean (Логика): ИИ постоянно проверяет состояния. isMicrophoneOn = True (слушать ли голос?), hasContext = False (помнит ли ИИ предыдущий вопрос?).
  • Понимание того, как данные хранятся и взаимодействуют — это первый и самый важный шаг в программировании. Мы научились создавать «коробки» и раскладывать по ним информацию. В следующих статьях мы узнаем, как заставить компьютер принимать решения на основе этих данных и повторять действия тысячи раз в секунду с помощью циклов.

    10. Клиентская разработка и интерактивность интерфейсов

    Клиентская разработка и интерактивность интерфейсов

    В предыдущей части курса мы создали визуальный каркас для нашего ИИ-ассистента. Используя HTML, мы возвели «стены» интерфейса, а с помощью CSS поклеили «обои» и расставили «мебель». Если открыть созданный файл в браузере, мы увидим красивое поле для ввода текста, кнопку отправки и область для истории сообщений.

    Однако сейчас этот интерфейс абсолютно мертв. Вы можете кликать по кнопке «Отправить» сотни раз, но ничего не произойдет. Текст останется в поле ввода, а новые сообщения не появятся на экране. Наш интерфейс — это красивая фотография автомобиля, у которого нет двигателя.

    Чтобы вдохнуть жизнь в веб-страницу, заставить ее реагировать на действия пользователя и общаться с нашим Python-сервером, нам нужен третий столп веб-технологий — язык программирования JavaScript.

    JavaScript: Нервная система веб-страницы

    JavaScript (JS) — это язык программирования, который изначально был создан специально для работы внутри веб-браузеров. Если HTML определяет структуру (что находится на странице), а CSS определяет внешний вид (как это выглядит), то JavaScript определяет поведение (что страница делает).

    В отличие от Python, который в нашей архитектуре работает на сервере (в скрытой от пользователя среде), JavaScript выполняется прямо на устройстве пользователя — в его браузере (Chrome, Safari, Edge). Это называется клиентской разработкой (Frontend).

    > Важное уточнение: Java и JavaScript — это два совершенно разных языка программирования. Они похожи друг на друга не больше, чем виноград (grape) и грейпфрут (grapefruit). Название JavaScript было выбрано исключительно в маркетинговых целях на заре развития интернета.

    Зачем нам нужен язык программирования прямо в браузере? Представьте, что каждый раз, когда вы ставите лайк в социальной сети, браузеру приходилось бы полностью перезагружать страницу, чтобы показать красное сердечко. Это было бы невероятно медленно. JavaScript позволяет изменять текущую страницу «на лету», без ее перезагрузки.

    DOM: Мост между HTML и JavaScript

    Браузер не читает HTML-код как простой текст, когда дело доходит до программирования. Когда браузер загружает HTML-документ, он преобразует его в сложную внутреннюю структуру, которая называется DOM (Document Object Model — Объектная модель документа).

    DOM — это древовидное представление всей вашей веб-страницы. Каждый HTML-тег становится объектом (узлом дерева), с которым может взаимодействовать JavaScript.

    !Дерево DOM: как браузер преобразует HTML-код в структуру объектов, понятную для JavaScript

    В JavaScript есть глобальный объект document, который представляет всю страницу целиком. Через этот объект мы можем найти любой элемент, изменить его текст, поменять цвет или вообще удалить со страницы.

    Поиск элементов на странице

    Чтобы заставить кнопку работать, нам сначала нужно «схватить» ее с помощью JavaScript. Для этого используется метод querySelector, который позволяет находить элементы точно так же, как мы обращались к ним в CSS.

    В этом коде мы создаем переменные (используя ключевое слово const для констант, значения которых не будут переназначаться) и сохраняем в них ссылки на реальные HTML-элементы. Теперь переменная sendButton — это пульт управления нашей кнопкой.

    Манипуляция элементами

    Получив доступ к элементу, мы можем делать с ним что угодно. Например, мы можем прочитать текст, который пользователь ввел в поле:

    Или мы можем создать совершенно новый HTML-элемент прямо из кода и добавить его на страницу. Именно так наш ИИ-ассистент будет выводить свои ответы:

    Этот процесс называется манипуляцией DOM. Это основа любой интерактивности в интернете.

    События и слушатели: Реакция на действия пользователя

    Программа на Python, которую мы писали ранее, выполнялась линейно: строчка за строчкой, сверху вниз. Клиентский JavaScript работает иначе. Он основан на событийно-ориентированной модели.

    Когда страница загружается, скрипт выполняется один раз, настраивает все необходимое и... засыпает. Он ждет, пока что-то произойдет. Это «что-то» называется событием (Event).

    Событием может быть: * Клик мышью по элементу * Нажатие клавиши на клавиатуре * Прокрутка страницы * Завершение загрузки картинки

    Чтобы отреагировать на событие, мы должны прикрепить к элементу слушатель событий (Event Listener). Это функция, которая будет вызвана браузером автоматически в момент наступления события.

    Давайте оживим нашу кнопку отправки:

    Теперь браузер работает как бдительный охранник. Как только пользователь кликает по кнопке, браузер немедленно запускает функцию handleSendClick.

    !Интерактивная симуляция работы DOM и событий: добавление сообщений в чат

    Асинхронное программирование: Как не заморозить браузер

    Мы научились забирать текст пользователя и показывать его на экране. Но главная задача — отправить этот текст нашему Python-серверу (где живет логика ИИ), дождаться ответа и показать его.

    Здесь мы сталкиваемся с фундаментальной проблемой. JavaScript в браузере работает в один поток (Single Thread). Это значит, что он может делать только одно дело в единицу времени.

    Представьте, что отправка запроса к ИИ и генерация ответа занимает 5 секунд. Если мы напишем код синхронно (как обычно), то на эти 5 секунд JavaScript полностью остановится, ожидая ответа. Вся веб-страница «зависнет»: пользователь не сможет нажимать другие кнопки, прокручивать текст или выделять слова. Браузер будет казаться сломанным.

    Решение этой проблемы — асинхронное программирование.

    > Асинхронность можно сравнить с походом в ресторан быстрого питания. Вы подходите к кассе, делаете заказ и оплачиваете его. Кассир не заставляет вас стоять у кассы, пока жарится котлета (это заблокировало бы всю очередь). Вместо этого вам выдают пейджер с номером заказа. Вы отходите и можете заниматься своими делами (листать ленту в телефоне). Когда бургер готов, пейджер пищит (происходит событие), и вы забираете еду.

    В современном JavaScript для асинхронных операций используются Промисы (Promises — обещания) и синтаксис async / await.

    Fetch API: Общение с сервером

    Для отправки данных по сети браузер предоставляет встроенную функцию fetch() (принести). Она работает асинхронно: отправляет запрос и сразу же возвращает «пейджер» (Промис) — обещание того, что результат будет предоставлен позже.

    Чтобы работать с этим удобно, мы помечаем нашу функцию специальным словом async, а перед вызовом fetch ставим слово await (подождать).

    Слово await говорит браузеру: «Приостанови выполнение конкретно этой функции, пока не придет ответ от сервера. Но саму страницу не блокируй, пусть пользователь продолжает с ней взаимодействовать».

    Давайте напишем функцию для общения с нашим ИИ-ассистентом:

    Разберем этот критически важный кусок кода:

  • URL и Метод: Мы обращаемся по адресу http://localhost:8000/api/chat. Метод POST означает, что мы не просто запрашиваем страницу, а отправляем данные на сервер.
  • Заголовки (Headers): Мы сообщаем серверу, что отправляем данные в формате JSON (Content-Type: application/json).
  • Тело (Body): Мы используем функцию JSON.stringify(), чтобы превратить наш JavaScript-объект в текстовую строку формата JSON. (Вспомните статью про форматы данных: по сети можно передавать только текст или байты, но не объекты из оперативной памяти).
  • Обработка ответа: Когда сервер присылает ответ, мы используем response.json(), чтобы выполнить обратную операцию — превратить текст ответа в удобный объект JavaScript.
  • Обработка ошибок (try/catch): Сеть непредсказуема. Wi-Fi может отключиться в любую секунду. Блок try/catch позволяет программе не сломаться с критической ошибкой, а элегантно обработать проблему.
  • Интеграция: Полный цикл работы интерфейса

    Теперь мы можем объединить работу с DOM, событиями и асинхронными запросами в единый, законченный механизм.

    Вот как выглядит полный цикл работы клиентской части нашего ИИ-ассистента:

  • Пользователь вводит текст и нажимает «Отправить».
  • Срабатывает слушатель события click.
  • JavaScript читает текст из поля ввода.
  • JS создает новый элемент div с сообщением пользователя и добавляет его в DOM (на экран).
  • JS показывает индикатор загрузки (например, текст «ИИ печатает...»).
  • JS вызывает асинхронную функцию fetch для отправки текста на Python-сервер.
  • В это время пользователь может прокручивать историю чата — страница не зависла.
  • Python-сервер обрабатывает запрос, обращается к нейросети и возвращает JSON-ответ.
  • JS получает ответ, удаляет индикатор загрузки.
  • JS создает новый div с ответом ИИ и добавляет его в DOM.
  • | Этап | Технология | Роль в системе | | :--- | :--- | :--- | | Ввод текста | HTML / CSS | Предоставляет визуальное поле и кнопку | | Нажатие кнопки | JS (Events) | Фиксирует действие пользователя | | Отображение текста | JS (DOM) | Динамически меняет структуру страницы | | Отправка на сервер | JS (Fetch API) | Передает данные по сети в фоновом режиме | | Формат передачи | JSON | Универсальный язык общения между JS и Python |

    Проблема управления состоянием (State Management)

    В нашем примере мы брали данные (сообщения) и напрямую вставляли их в HTML (DOM). Для простого чата это работает отлично. Но представьте, что мы разрабатываем сложный интерфейс, где одно и то же сообщение должно отображаться в главном окне чата, в боковой панели последних файлов и в счетчике непрочитанных сообщений в шапке сайта.

    Если мы будем вручную искать каждый из этих элементов через querySelector и обновлять их, наш код быстро превратится в запутанный клубок (так называемый «спагетти-код»).

    В современной веб-разработке эту проблему решают с помощью концепции Состояния (State) и специализированных библиотек (таких как React, Vue или Angular).

    Вместо того чтобы вручную менять DOM, разработчик описывает Состояние (например, массив всех сообщений). Когда в массив добавляется новое сообщение, библиотека (например, React) сама вычисляет, какие части HTML нужно обновить, и делает это автоматически.

    > В профессиональной разработке ИИ-интерфейсов, таких как ChatGPT или Claude, используются именно такие продвинутые инструменты. Они позволяют создавать сложные элементы Generative UI — когда ИИ возвращает не просто текст, а интерактивные графики, таблицы или мини-приложения прямо внутри чата.

    Однако под капотом любого, даже самого сложного фреймворка, работают те же самые фундаментальные механизмы, которые мы разобрали сегодня: DOM, события и асинхронные запросы.

    Теперь наш ИИ-ассистент имеет полноценное «лицо» и «нервную систему». Он готов принимать запросы от пользователей через удобный браузерный интерфейс и передавать их в «мозг» — на наш сервер. В следующих статьях мы углубимся в серверную часть и узнаем, как именно Python принимает эти запросы из интернета, маршрутизирует их и управляет диалогом с помощью баз данных.

    11. Серверная разработка и принципы создания REST API

    Серверная разработка и принципы создания REST API

    В предыдущей статье мы вдохнули жизнь в интерфейс нашего ИИ-ассистента с помощью JavaScript. Мы научились перехватывать действия пользователя, читать введенный текст и отправлять его «куда-то» с помощью функции fetch. Это «куда-то» — не абстрактное облако, а конкретная программа, работающая на удаленном компьютере.

    Сегодня мы переходим на темную сторону луны — в серверную разработку (Backend). Мы узнаем, как сервер принимает запросы из браузера, что такое API, и почему архитектурный стиль REST стал абсолютным стандартом в современной веб-разработке.

    Анатомия веб-приложения: Клиент и Сервер

    Современный интернет построен на клиент-серверной архитектуре. Это фундаментальный принцип разделения обязанностей, который можно сравнить с работой ресторана.

    Клиент (Frontend) — это зал ресторана, меню и посетитель. В мире веб-разработки клиентом выступает браузер (Chrome, Safari) или мобильное приложение. Задача клиента — красиво отобразить информацию и собрать команды от пользователя (нажатия кнопок, ввод текста).

    Сервер (Backend) — это кухня ресторана. Сюда нет доступа обычным посетителям. Здесь хранятся запасы продуктов (базы данных), здесь работают повара (серверная логика), которые по сложным рецептам (алгоритмам) готовят блюда.

    Сервер — это, по сути, обычный компьютер, но без монитора и клавиатуры. Он подключен к быстрому интернету и работает 24/7. На этом компьютере запущена специальная программа (написанная, например, на Python), которая непрерывно «слушает» сеть в ожидании входящих сообщений.

    Но как клиент в зале передает свой заказ на кухню? Ему нужен посредник.

    Что такое API?

    API (Application Programming Interface — Программный интерфейс приложения) — это набор правил и механизмов, с помощью которых одна программа может общаться с другой.

    Возвращаясь к нашей аналогии, API — это официант.

    Вы (клиент) не идете на кухню (сервер), чтобы самостоятельно жарить стейк. Вы не знаете, где лежат ножи и как работает плита. Вместо этого вы смотрите в меню (документацию API), выбираете блюдо и отдаете заказ официанту. Официант относит стандартизированный заказ на кухню, ждет, пока повара все приготовят, и приносит вам готовое блюдо.

    В программировании API скрывает внутреннюю сложность системы. Нашему JavaScript-коду в браузере абсолютно неважно, как именно Python-сервер обращается к нейросети, как он фильтрует текст или сохраняет историю. Браузеру важно лишь одно: «Если я отправлю текст по этому адресу, я получу ответ в таком-то формате».

    !Схема клиент-серверной архитектуры с API в роли посредника

    Эволюция общения: от хаоса к REST

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

    Позже появился протокол SOAP (Simple Object Access Protocol). Он был строгим, надежным, но невероятно громоздким. Для отправки простого сообщения требовалось написать десятки строк сложного XML-кода. SOAP был похож на бюрократическую машину, где для получения справки нужно заполнить стопку бланков.

    В 2000 году ученый Рой Филдинг в своей докторской диссертации предложил новый подход — REST (Representational State Transfer — Передача состояния представления). REST — это не программа и не библиотека. Это архитектурный стиль, набор рекомендаций о том, как правильно проектировать API, используя уже существующие возможности интернета (протокол HTTP).

    REST произвел революцию благодаря своей простоте и логичности. Сегодня более 80% всех веб-сервисов в мире используют именно REST API.

    Фундаментальные принципы REST API

    Чтобы API считался по-настоящему RESTful (соответствующим стилю REST), он должен соблюдать несколько строгих принципов.

    1. Клиент-серверная архитектура

    Мы уже обсудили этот принцип. Клиент и сервер должны быть полностью независимы. Сервер не должен заниматься отрисовкой кнопок, а клиент не должен напрямую управлять базой данных. Это позволяет команде frontend-разработчиков менять дизайн сайта, не ломая при этом серверную логику.

    2. Отсутствие состояния (Stateless)

    Это, пожалуй, самое важное и сложное для новичков правило. Сервер не должен хранить информацию о текущем состоянии клиента между запросами.

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

    > Представьте, что вы звоните в банк. > Подход с состоянием (Stateful): Вы звоните, называете номер паспорта. Оператор вас запоминает. Вы кладете трубку. Звоните снова и говорите: «Переведите мои деньги маме». Оператор помнит, кто вы, и делает перевод. > Подход без состояния (Stateless / REST): Вы звоните и говорите: «Переведите деньги маме». Оператор отвечает: «Кто вы? Я вас не знаю». В REST вы должны каждый раз говорить: «Я Иван Иванов, вот мой паспорт, переведите деньги маме».

    Почему REST требует отсутствия состояния? Ради масштабируемости. Если ваш ИИ-ассистент станет популярным, один сервер не справится с нагрузкой. Вам придется поставить 10 серверов. Если первый запрос пользователя попадет на Сервер №1 (и тот его запомнит), а второй запрос случайно попадет на Сервер №2, произойдет ошибка — Сервер №2 ничего не знает о пользователе. При Stateless-подходе любой сервер может обработать любой запрос, так как вся нужная информация (например, токен авторизации) уже есть внутри самого запроса.

    3. Единый интерфейс и Ресурсы

    В REST API все данные рассматриваются как Ресурсы (Resources). Ресурсом может быть что угодно: пользователь, сообщение в чате, настройки профиля.

    Каждый ресурс имеет свой уникальный адрес (URL). А действия над этим ресурсом определяются стандартными HTTP-методами (глаголами).

    HTTP-методы: Глаголы интернета

    Протокол HTTP, на котором работает весь веб, изначально имеет встроенные методы, указывающие на намерение клиента. REST API использует их для реализации базовых операций CRUD (Create, Read, Update, Delete).

    | HTTP Метод | Операция CRUD | Назначение в REST API | Пример URL | | :--- | :--- | :--- | :--- | | POST | Create (Создать) | Создание нового ресурса на сервере | /api/users | | GET | Read (Прочитать) | Получение данных о ресурсе (без их изменения) | /api/users/123 | | PUT | Update (Обновить) | Полная замена существующего ресурса | /api/users/123 | | PATCH | Update (Обновить) | Частичное изменение ресурса (например, только email) | /api/users/123 | | DELETE | Delete (Удалить) | Удаление ресурса | /api/users/123 |

    Обратите внимание на красоту и лаконичность этого подхода. В плохом (не REST) API разработчики часто создают хаос из адресов: * /get-user-by-id?id=123 * /create-new-user * /deleteUser/123

    В правильном REST API адрес (URL) указывает только на существительное (ресурс), а HTTP-метод указывает на глагол (действие): * GET /api/users/123 (Дай мне пользователя 123) * DELETE /api/users/123 (Удали пользователя 123)

    Концепция Идемпотентности

    В контексте HTTP-методов важно понимать термин идемпотентность. Операция называется идемпотентной, если многократное ее выполнение дает тот же результат, что и однократное.

    Метод GET идемпотентен. Вы можете запросить профиль пользователя 100 раз, и на сервере ничего не изменится. Метод DELETE идемпотентен. Если вы удалите сообщение, оно исчезнет. Если вы отправите команду на удаление этого же сообщения еще 10 раз, сервер просто ответит, что такого сообщения уже нет, но общее состояние системы не ухудшится.

    А вот метод POSTне идемпотентен. Если вы отправите запрос POST /api/messages с текстом «Привет» 5 раз, в базе данных будет создано 5 одинаковых сообщений. Именно поэтому, когда у вас зависает интернет при оплате в магазине, и вы несколько раз яростно кликаете кнопку «Оплатить» (отправляя POST-запросы), с вашей карты могут списать деньги несколько раз.

    !Интерактивный симулятор REST API: отправка запросов и получение ответов

    HTTP Статус-коды: Как сервер отвечает

    Когда официант (API) возвращается с кухни (от сервера), он приносит не только само блюдо (данные), но и чек с результатом обслуживания. В REST API эту роль играют HTTP статус-коды — трехзначные числа, которые мгновенно сообщают клиенту, как прошел запрос.

    Они делятся на 5 классов (по первой цифре):

    * 1xx (Информационные): Запрос принят, процесс продолжается. (Встречаются редко). * 2xx (Успех): * 200 OK — Запрос выполнен успешно (стандартный ответ для GET). * 201 Created — Ресурс успешно создан (стандартный ответ для POST). * 204 No Content — Запрос успешен, но серверу нечего вернуть (часто используется при DELETE). * 3xx (Перенаправление): Ресурс перемещен, клиенту нужно обратиться по другому адресу. * 4xx (Ошибка клиента): Вы (клиент) сделали что-то не так. * 400 Bad Request — Сервер не понял запрос (например, вы забыли указать обязательное поле). * 401 Unauthorized — Вы не авторизованы (нет токена или логина). * 403 Forbidden — Вы авторизованы, но у вас нет прав на это действие (например, попытка удалить чужое сообщение). * 404 Not Found — Запрашиваемый ресурс не существует (знаменитая «Страница не найдена»). * 5xx (Ошибка сервера): Клиент все сделал правильно, но сервер сломался. * 500 Internal Server Error — В коде сервера произошла непредвиденная ошибка (например, деление на ноль в Python). * 503 Service Unavailable — Сервер перегружен или находится на техническом обслуживании.

    Использование правильных статус-кодов — признак профессионального REST API. Если пользователь запрашивает несуществующее сообщение, сервер должен вернуть статус 404, а не статус 200 с текстом «Ой, сообщение не найдено» внутри.

    Формат данных: Почему JSON победил

    Как мы помним из статьи про форматы данных, по сети нельзя передать объекты из оперативной памяти напрямую. Их нужно сериализовать (превратить в текст).

    В эпоху SOAP стандартом был XML. Он выглядел так:

    REST API принес с собой моду на JSON (JavaScript Object Notation). Тот же самый набор данных в JSON выглядит так:

    JSON победил по трем причинам:

  • Он занимает меньше места (нет закрывающих тегов), что экономит трафик.
  • Он легче читается человеком.
  • Он идеально, без дополнительных преобразований, превращается в объекты JavaScript в браузере.
  • Проектирование API для ИИ-ассистента

    Давайте применим полученные знания и спроектируем REST API для нашего ИИ-ассистента. Нам нужно управлять историей диалогов и отправлять новые сообщения.

    Вот как будет выглядеть таблица маршрутов (Endpoints) нашего сервера:

  • GET /api/chats — получить список всех диалогов пользователя.
  • POST /api/chats — создать новый пустой диалог.
  • GET /api/chats/42/messages — получить историю сообщений из диалога №42.
  • POST /api/chats/42/messages — отправить новое сообщение от пользователя в диалог №42.
  • DELETE /api/chats/42 — удалить диалог №42 навсегда.
  • Обратите внимание на вложенность в пунктах 3 и 4. URL /api/chats/42/messages читается как предложение: «Обратись к API, найди чаты, выбери чат 42, и дай мне его сообщения». Это классический ресурсно-ориентированный дизайн.

    Как это выглядит в коде (Python + FastAPI)

    Чтобы демистифицировать серверную магию, давайте посмотрим, как выглядит создание эндпоинта на современном Python-фреймворке FastAPI.

    Фреймворк — это набор готовых инструментов, который берет на себя рутинную работу (чтение HTTP-заголовков, открытие сетевых портов), позволяя разработчику сфокусироваться на бизнес-логике.

    Всего в 20 строках кода мы создали полноценный REST API сервер!

    Когда наш JavaScript из предыдущего урока делает fetch('http://localhost:8000/api/chat', { method: 'POST', ... }), этот запрос летит по сети, попадает в наш Python-скрипт, фреймворк FastAPI находит функцию с декоратором @app.post("/api/chat"), передает в нее текст, выполняет логику и отправляет JSON обратно в браузер.

    Круг замкнулся. Клиент и Сервер успешно пообщались.

    Подводные камни и лучшие практики

    Создать REST API легко. Создать хороший REST API — искусство. Вот несколько правил, которые отличают профессионалов от новичков:

  • Версионирование: Всегда добавляйте версию в URL, например /api/v1/users. Если через год вы решите полностью переделать логику, вы создадите /api/v2/users. Старые мобильные приложения, которые не обновились, продолжат работать с v1, и система не сломается.
  • Пагинация: Если у пользователя 10 000 сообщений в чате, запрос GET /api/messages не должен возвращать их все разом (это «повесит» и сервер, и браузер). Используйте параметры запроса для порционной выдачи: GET /api/messages?limit=50&offset=100 (дай 50 сообщений, пропустив первые 100).
  • Безопасность: Никогда не доверяйте данным от клиента. Если клиент присылает POST /api/users с полем "is_admin": true, сервер обязан проигнорировать это поле, иначе любой хакер сможет стать администратором.
  • В этой статье мы разобрали, как сервер общается с внешним миром через REST API. Мы научились проектировать правильные URL, использовать HTTP-методы по назначению и понимать статус-коды. Однако сейчас наш сервер страдает амнезией: как только программа на Python перезапускается, все данные исчезают из оперативной памяти. В следующей статье мы погрузимся в мир баз данных и узнаем, как надежно хранить информацию десятилетиями.

    12. Реляционные базы данных и основы SQL-запросов

    Реляционные базы данных и основы SQL-запросов

    В конце предыдущего этапа мы создали серверную часть нашего ИИ-ассистента. Сервер научился принимать запросы от браузера через REST API, обрабатывать их и возвращать ответы. Однако у нашей системы есть критический недостаток — она страдает абсолютной амнезией. Как только мы перезапускаем программу на Python, вся история переписки, все настройки пользователей и сохраненные контексты исчезают навсегда, так как они хранились в оперативной памяти компьютера.

    Чтобы ИИ-ассистент мог узнавать вас спустя месяц, помнить предыдущие диалоги и хранить терабайты информации, нам необходимо обеспечить персистентность (постоянство) данных. Для этого в программировании используются базы данных.

    Что такое база данных и почему не Excel?

    База данных (БД) — это организованная структура для хранения, изменения и извлечения информации.

    На первый взгляд может показаться, что обычный файл Excel или Google Таблицы — это и есть база данных. Там тоже есть строки, столбцы и листы. Почему бы нашему серверу просто не записывать историю чатов в .xlsx файл?

    Разница заключается в масштабе, надежности и правилах. Excel — это свободный художник. В одном столбце вы можете написать число 42, ячейкой ниже — слово Привет, а еще ниже закрасить ячейку желтым цветом. Если два человека попытаются одновременно сохранить изменения в одном файле Excel, скорее всего, возникнет конфликт или файл сломается.

    Профессиональные базы данных работают под управлением СУБД (Система управления базами данных). Это строгий бухгалтер и охранник в одном лице. СУБД гарантирует, что:

  • Типы данных строго соблюдаются. Если столбец предназначен для дат, вы физически не сможете записать туда текст.
  • Многопоточность безопасна. Тысячи пользователей могут одновременно писать сообщения в чат, и СУБД аккуратно выстроит их запросы в очередь, ничего не потеряв.
  • Данные защищены от сбоев. Если во время сохранения сообщения отключат электричество, СУБД восстановит данные до целостного состояния при следующем запуске (это свойство называется транзакционностью).
  • Реляционная модель: Искусство связей

    Существуют разные типы баз данных (документные, графовые, ключ-значение), но абсолютным стандартом в индустрии, на котором держится большая часть интернета, являются реляционные базы данных (от английского relation — отношение, связь).

    В реляционной базе данных вся информация хранится в виде двумерных таблиц. Каждая таблица посвящена только одной сущности.

    Представьте, что мы проектируем базу данных для нашего ИИ-ассистента. Новичок мог бы создать одну гигантскую таблицу и записывать туда всё подряд: * Имя пользователя * Email пользователя * Название чата * Текст сообщения * Время отправки

    Если пользователь Иван отправит 100 сообщений, нам придется 100 раз продублировать его Имя и Email в каждой строке. Это приводит к перерасходу памяти. А если Иван решит сменить Email? Нам придется искать и обновлять все 100 строк. Если мы пропустим хотя бы одну, возникнет аномалия данных — система не будет знать, какой Email правильный.

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

    Первичные и внешние ключи

    Чтобы таблицы могли ссылаться друг на друга, нам нужны надежные идентификаторы.

    Первичный ключ (Primary Key, PK) — это уникальный идентификатор записи в таблице. Это как номер паспорта для человека или VIN-номер для автомобиля. Имена могут совпадать, но первичный ключ всегда уникален. Обычно это просто целое число, которое автоматически увеличивается с каждой новой строкой (1, 2, 3...).

    Внешний ключ (Foreign Key, FK) — это столбец в одной таблице, который содержит значение первичного ключа из другой таблицы. Именно он создает ту самую «связь» (реляцию).

    Давайте спроектируем правильную структуру для ИИ-ассистента:

  • Таблица users (Пользователи):
  • - id (Первичный ключ): 1 - name: Иван - email: ivan@mail.com

  • Таблица chats (Диалоги):
  • - id (Первичный ключ): 42 - title: "План тренировок" - user_id (Внешний ключ): 1 (Указывает, что этот чат принадлежит Ивану)

  • Таблица messages (Сообщения):
  • - id (Первичный ключ): 1005 - text: "Напиши мне программу отжиманий" - chat_id (Внешний ключ): 42 (Указывает, в каком чате написано сообщение)

    Теперь, если Иван сменит почту, нам нужно будет обновить ровно одну ячейку в таблице users. Все его чаты и сообщения останутся нетронутыми, так как они связаны по неизменному id.

    !Схема базы данных ИИ-ассистента

    SQL: Язык общения с данными

    Мы поняли, как данные хранятся. Но как нашему Python-серверу сказать базе данных: «Найди мне все сообщения Ивана за вчерашний день»?

    Для этого в 1970-х годах был придуман SQL (Structured Query Language — язык структурированных запросов).

    SQL кардинально отличается от Python или JavaScript. Python — это императивный язык. Вы пишете пошаговую инструкцию: создай массив, запусти цикл, возьми элемент, сравни его, если совпадает — добавь в новый массив.

    SQL — это декларативный язык. Вы не объясняете базе данных, как искать информацию. Вы просто описываете, какой результат вы хотите получить. База данных сама решает, какие алгоритмы применить, чтобы найти данные максимально быстро.

    > Представьте, что вы пришли в ресторан. > Императивный подход: вы идете на кухню, берете сковородку, наливаете масло, кладете мясо, жарите 5 минут. > Декларативный подход (SQL): вы говорите официанту: «Принесите мне стейк средней прожарки». Как именно повар будет его готовить — вас не касается.

    Все операции в базах данных сводятся к четырем базовым действиям, которые образуют аббревиатуру CRUD (Create, Read, Update, Delete). В SQL для них есть четыре главные команды.

    1. CREATE: Добавление данных (INSERT)

    Чтобы добавить новую строку в таблицу, используется команда INSERT INTO.

    Мы указываем имя таблицы (users), перечисляем столбцы, которые хотим заполнить, и передаем значения. Столбец id мы не указываем — СУБД сгенерирует его автоматически.

    2. READ: Чтение данных (SELECT)

    Это самая частая и самая мощная команда в SQL. Аналитики данных могут писать запросы SELECT длиной в сотни строк.

    Базовый синтаксис выглядит так:

    Давайте разберем этот запрос по частям, так как он читается почти как обычный английский текст: * SELECT text, created_at — выбери только колонки с текстом и датой (не тяни из базы лишние данные, экономь память). * FROM messages — из таблицы сообщений. * WHERE chat_id = 42 — отфильтруй только те строки, где идентификатор чата равен 42. * ORDER BY created_at DESC — отсортируй результаты по дате создания по убыванию (от новых к старым). * LIMIT 10 — верни только первые 10 строк (пагинация, чтобы не перегрузить интерфейс).

    3. UPDATE: Обновление данных

    Если пользователь решил отредактировать свое сообщение, мы используем команду UPDATE.

    Критически важное правило: никогда не забывайте блок WHERE в командах UPDATE и DELETE. Если вы напишете просто UPDATE messages SET text = 'Удалено', база данных послушно заменит текст абсолютно всех сообщений в системе на слово «Удалено». Это классическая ошибка новичков, из-за которой компании теряют миллионы.

    4. DELETE: Удаление данных

    Удаление конкретного чата:

    JOIN: Склеиваем данные обратно

    Мы разделили данные на разные таблицы ради порядка. Но что, если нам нужно вывести на экран профиль пользователя и количество его сообщений? Нам нужно объединить данные из таблицы users и таблицы messages в одном запросе.

    Для этого используется операция JOIN (соединение).

    Представьте две стопки карточек. В одной — имена пользователей и их ID. В другой — тексты сообщений и ID их авторов. Операция JOIN берет карточку сообщения, смотрит на ID автора, находит карточку пользователя с таким же ID в другой стопке и склеивает их в одну длинную карточку.

    Самый популярный вид соединения — INNER JOIN (внутреннее соединение). Он возвращает только те строки, для которых нашлось совпадение в обеих таблицах.

    В этом запросе мы говорим: «Возьми таблицу users, присоедини к ней таблицу messages, и правилом для склеивания (ON) считай совпадение первичного ключа пользователя (users.id) и внешнего ключа в сообщении (messages.user_id)».

    Существует также LEFT JOIN (левое соединение). Оно берет ВСЕ строки из первой (левой) таблицы, даже если для них нет совпадений во второй. Если у пользователя Ивана еще нет ни одного сообщения, INNER JOIN вообще не покажет Ивана в результатах. А LEFT JOIN покажет Ивана, но в колонке с текстом сообщения будет пустота (специальное значение NULL).

    !Интерактивный визуализатор SQL JOIN: объединение таблиц пользователей и заказов

    Индексы: Как база данных ищет так быстро?

    Вспомним нашу статью про алгоритмы и вычислительную сложность. Если в таблице messages хранится 10 миллионов строк, а мы пишем запрос WHERE user_id = 5, как база данных найдет нужные строки?

    По умолчанию она будет использовать линейный поиск со сложностью . Она прочтет каждую из 10 миллионов строк и проверит условие. Это займет несколько секунд, что недопустимо для веб-приложения.

    Чтобы ускорить поиск, в базах данных создаются Индексы.

    Индекс в БД работает точно так же, как алфавитный указатель в конце толстой книги. Вместо того чтобы листать всю книгу в поисках термина «Полиморфизм», вы открываете указатель на букву «П», находите слово и видите номер страницы.

    Когда мы создаем индекс для колонки user_id, СУБД строит специальную структуру данных (обычно это B-дерево), которая позволяет находить нужные строки со сложностью . Поиск среди 10 миллионов записей займет не миллионы операций, а всего около 20-30 шагов. Ответ будет получен за миллисекунды.

    Однако у индексов есть цена (тот самый компромисс между временем и памятью). Индекс занимает дополнительное место на жестком диске. Кроме того, при каждом добавлении нового сообщения (INSERT), базе данных приходится не только записывать строку в таблицу, но и обновлять индексное дерево. Поэтому индексировать абсолютно все колонки подряд — плохая идея. Индексы ставят только на те колонки, по которым часто происходит поиск или фильтрация.

    Безопасность: Угроза SQL-инъекций

    Когда мы будем писать код на Python для нашего ИИ-ассистента, мы будем принимать текст от пользователя и вставлять его в SQL-запрос.

    Наивный подход выглядит так (псевдокод):

    Что произойдет, если злоумышленник введет в поле имени следующую строку: ' OR '1'='1?

    Наш Python-код слепо склеит строки, и в базу данных отправится такой запрос:

    База данных проверит условие: «Имя пустое? Нет. ИЛИ единица равна единице? Да, единица всегда равна единице!». Условие становится истинным для абсолютно всех строк в таблице. В результате хакер получит список всех пользователей системы, включая их пароли и личные данные. Это называется SQL-инъекцией — одной из самых старых и опасных уязвимостей в веб-разработке.

    Чтобы этого избежать, современные фреймворки никогда не склеивают SQL-запросы как обычные строки. Они используют параметризованные запросы, где данные отправляются отдельно от самого SQL-кода, и база данных точно знает, что введенный текст — это просто текст, а не часть команды.

    В этой статье мы заложили фундамент работы с данными. Мы поняли, как структурировать информацию с помощью таблиц и связей, и научились базовым командам SQL для управления этими данными. Теперь наш ИИ-ассистент готов обрести долговременную память. В следующей статье мы свяжем наш Python-сервер с реальной базой данных с помощью технологии ORM, которая позволит нам управлять таблицами, вообще не написав ни строчки на чистом SQL.

    13. Взаимодействие клиента и сервера по протоколу HTTP

    Взаимодействие клиента и сервера по протоколу HTTP

    На предыдущих этапах нашего курса мы проделали огромную работу. Мы создали клиентскую часть (фронтенд) с помощью HTML, CSS и JavaScript, научив браузер реагировать на клики пользователя. Затем мы разработали серверную часть (бэкенд) на Python, которая умеет обрабатывать логику, и подключили к ней реляционную базу данных SQL для надежного хранения истории переписки нашего ИИ-ассистента.

    Теперь перед нами стоит фундаментальная задача: как заставить эти два совершенно разных мира общаться друг с другом? Как JavaScript в браузере пользователя из Москвы передает текст сообщения Python-серверу, физически расположенному в дата-центре во Франкфурте?

    Связующим звеном, универсальным языком общения в интернете является HTTP (Hypertext Transfer Protocol — протокол передачи гипертекста). Понимание того, как устроен этот протокол на уровне текста и байтов, отличает обычного кодера, слепо копирующего функции fetch(), от профессионального инженера, способного проектировать надежные и безопасные архитектуры.

    Что такое HTTP и архитектура Клиент-Сервер

    В основе всего интернета лежит архитектурная модель Клиент-Сервер.

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

    Сервер — это мощный компьютер (или программа на нем, например, наш код на FastAPI), который пассивно слушает сеть, ожидая обращений. Получив запрос, сервер обрабатывает его, обращается к базе данных и возвращает ответ.

    HTTP — это набор строгих текстовых правил, по которым строится этот диалог. Он работает на прикладном уровне сетевой модели (поверх транспортного протокола TCP).

    Главная философская особенность HTTP заключается в том, что это протокол без сохранения состояния (Stateless).

    > Представьте себе кассира в ресторане быстрого питания, который страдает абсолютной амнезией. Вы подходите и говорите: «Дайте мне бургер». Он пробивает бургер. Вы не отходите от кассы и через секунду говорите: «И еще колу». Кассир смотрит на вас пустым взглядом и спрашивает: «Какую колу? Кто вы? Вы уже что-то заказывали?».

    Для HTTP каждый новый запрос — это чистый лист. Сервер не помнит предыдущих запросов от того же клиента. Это сделано намеренно: отсутствие памяти позволяет серверам работать невероятно быстро и обслуживать миллионы пользователей одновременно, не тратя оперативную память на хранение контекста каждого диалога. О том, как мы обходим это ограничение для авторизации пользователей, мы поговорим чуть позже.

    !Схема взаимодействия клиента и сервера: браузер отправляет HTTP-запрос, сервер обрабатывает его и возвращает HTTP-ответ

    Анатомия URL: Адрес в интернете

    Прежде чем отправить запрос, клиент должен знать, куда именно его отправлять. Для этого используется URL (Uniform Resource Locator — единообразный локатор ресурса).

    Давайте разберем типичный URL нашего будущего ИИ-ассистента: https://api.gurufy.com:443/v1/chat/messages?user_id=42&sort=desc

    Этот адрес состоит из нескольких критически важных компонентов:

  • Схема (Протокол): https:// — указывает браузеру, на каком «языке» и с каким уровнем шифрования нужно общаться.
  • Хост (Домен): api.gurufy.com — человекочитаемое имя сервера. Система DNS (Domain Name System) переведет это имя в физический IP-адрес (например, 192.168.1.15), чтобы маршрутизаторы знали, куда направить сигнал.
  • Порт: :443 — виртуальная «дверь» на сервере. На одном компьютере могут работать сотни программ. Порт указывает, какой именно программе предназначен запрос. Для HTTP стандартный порт — 80, для безопасного HTTPS — 443. Обычно браузеры скрывают порт, подставляя его автоматически.
  • Путь (Path): /v1/chat/messages — конкретный маршрут внутри нашего приложения. Это аналог пути к файлу в файловой системе, который мы изучали ранее. Он указывает, к какой именно сущности (сообщениям чата) мы обращаемся.
  • Параметры запроса (Query String): ?user_id=42&sort=desc — дополнительные настройки запроса, начинающиеся со знака вопроса и разделенные амперсандом &. Здесь мы просим сервер отфильтровать сообщения для пользователя с ID 42 и отсортировать их по убыванию.
  • Структура HTTP-запроса (Request)

    Когда ваш JavaScript-код вызывает функцию fetch('https://api.gurufy.com/messages'), браузер формирует текстовое сообщение и отправляет его по сети.

    Если мы перехватим этот пакет данных, мы увидим обычный текст. HTTP-запрос состоит из трех частей: стартовой строки, заголовков и тела.

    Вот как выглядит «сырой» HTTP-запрос на создание нового сообщения:

    Давайте разберем его построчно.

    1. Стартовая строка (Start Line)

    Первая строка POST /v1/chat/messages HTTP/1.1 — самая важная. Она содержит: * Метод: POST (что мы хотим сделать). * Путь: /v1/chat/messages (с чем мы хотим это сделать). * Версию протокола: HTTP/1.1 (по каким правилам общаемся).

    2. Заголовки (Headers)

    Далее идут строки в формате Ключ: Значение. Это метаданные запроса — служебная информация для сервера. * Host: Обязательный заголовок, указывающий целевой домен (полезно, если на одном IP-адресе висит несколько сайтов). * User-Agent: Информация о клиенте (какой браузер, какая операционная система). Сервер может использовать это, чтобы отдать мобильную или десктопную версию сайта. * Accept: Клиент говорит: «Я ожидаю ответ в формате JSON». * Content-Type: Клиент предупреждает: «Данные, которые я тебе сейчас отправлю, отформатированы как JSON». * Content-Length: Размер отправляемых данных в байтах, чтобы сервер знал, когда закончить чтение.

    3. Тело запроса (Body)

    После заголовков обязательно идет одна пустая строка. Это сигнал серверу: «Заголовки закончились, дальше идут сами данные».

    В теле находится полезная нагрузка (Payload). В нашем случае это JSON-строка с текстом сообщения для ИИ. У некоторых запросов (например, при простом открытии веб-страницы) тела нет вообще.

    HTTP-методы: Глаголы интернета

    В статье про базы данных мы изучили концепцию CRUD (Create, Read, Update, Delete). HTTP-методы — это прямое отражение этих операций на уровне сети.

    | Метод | Описание | Аналог в SQL (CRUD) | Наличие тела в запросе | | :--- | :--- | :--- | :--- | | GET | Запрашивает данные с сервера. Ничего не меняет. | SELECT (Read) | Нет | | POST | Отправляет данные для создания новой сущности. | INSERT (Create) | Да | | PUT | Полностью заменяет существующую сущность. | UPDATE (Update) | Да | | PATCH | Частично обновляет сущность (например, только имя). | UPDATE (Update) | Да | | DELETE | Удаляет сущность на сервере. | DELETE (Delete) | Обычно нет |

    Концепция Идемпотентности

    Важнейшее свойство HTTP-методов — идемпотентность. Метод называется идемпотентным, если его многократное выполнение дает точно такой же результат на сервере, как и однократное.

    * GET — идемпотентен. Вы можете обновить страницу сайта 100 раз, и на сервере ничего не сломается и не создастся 100 новых записей. * PUT — идемпотентен. Если вы отправите команду «Установить имя пользователя = Иван» 10 раз, имя просто 10 раз перезапишется на «Иван». Итог один. * DELETE — идемпотентен. Если вы удалите сообщение с ID 5, оно исчезнет. Если вы отправите этот запрос еще 5 раз, сервер просто ответит ошибкой, но состояние базы данных не изменится (сообщения как не было, так и нет). * POSTНЕ идемпотентен. Если вы отправите запрос «Списать 1000 рублей» или «Создать заказ» дважды из-за лага интернета, сервер создаст два заказа и спишет 2000 рублей.

    Именно поэтому при оплате картой в интернете вы часто видите предупреждение: «Пожалуйста, не нажимайте кнопку 'Оплатить' дважды». Разработчики защищают неидемпотентный POST-запрос.

    Структура HTTP-ответа (Response)

    Получив наш POST-запрос, Python-сервер обрабатывает его, сохраняет сообщение в базу данных, генерирует ответ от ИИ и отправляет его обратно клиенту.

    Сырой HTTP-ответ выглядит так:

    Структура зеркально похожа на запрос:

  • Стартовая строка (Status Line): HTTP/1.1 201 Created. Содержит версию протокола и код состояния (результат операции).
  • Заголовки: Метаданные от сервера (дата, тип сервера, формат ответа).
  • Пустая строка.
  • Тело ответа: Сам JSON с ответом от ИИ-ассистента.
  • !Интерактивный инспектор HTTP: соберите свой запрос и посмотрите, как формируется сырой текст протокола и какой ответ возвращает сервер

    Коды состояния HTTP (Status Codes)

    Код состояния — это трехзначное число в первой строке ответа. Это способ сервера быстро сообщить браузеру, как всё прошло, не заставляя его читать всё тело ответа.

    Коды делятся на 5 классов (по первой цифре):

    1xx: Информационные (100–199)

    Редкие коды. Означают: «Я получил начало твоего запроса, продолжай отправлять данные».

    2xx: Успех (200–299)

    Всё прошло отлично. * 200 OK: Стандартный ответ для успешных GET-запросов (страница найдена, данные отправлены). * 201 Created: Успешный результат POST-запроса (новая запись успешно создана в базе данных). * 204 No Content: Запрос выполнен успешно, но серверу нечего вернуть в теле ответа (часто используется при успешном DELETE).

    3xx: Перенаправление (300–399)

    Сервер говорит: «То, что ты ищешь, находится по другому адресу». * 301 Moved Permanently: Страница навсегда переехала. Браузер запомнит это и в следующий раз сразу пойдет по новому адресу (важно для SEO). * 302 Found: Временное перенаправление.

    4xx: Ошибка клиента (400–499)

    Сервер говорит: «Ты отправил мне какую-то чушь или у тебя нет прав. Исправь запрос». * 400 Bad Request: Синтаксическая ошибка в запросе (например, сломанный JSON). * 401 Unauthorized: Вы не авторизованы (не передали логин/пароль или токен). * 403 Forbidden: Вы авторизованы, сервер знает кто вы, но у вас нет прав на это действие (например, обычный пользователь пытается удалить чужой чат). * 404 Not Found: Самая известная ошибка интернета. Сервер говорит: «Я существую, но ресурса по указанному URL у меня нет».

    5xx: Ошибка сервера (500–599)

    Сервер говорит: «Запрос был правильный, но я сам сломался при его обработке». * 500 Internal Server Error: В коде на Python произошла ошибка (например, деление на ноль или отвалилась база данных). * 502 Bad Gateway: Проблема с прокси-сервером или балансировщиком нагрузки. * 503 Service Unavailable: Сервер перегружен или находится на техническом обслуживании.

    Управление состоянием: Как сервер нас узнает?

    Мы уже выяснили, что HTTP — протокол без сохранения состояния (Stateless). Но когда вы заходите в свой профиль на сайте, вам не нужно вводить логин и пароль при каждом клике на новую страницу. Как это работает?

    Разработчики придумали «костыли» поверх HTTP, чтобы эмулировать память. Существует два основных подхода:

    1. Cookies (Куки)

    Когда вы впервые вводите логин и пароль, сервер проверяет их в базе данных. Если всё верно, сервер добавляет в HTTP-ответ специальный заголовок: Set-Cookie: session_id=abc123xyz; Secure; HttpOnly

    Браузер видит этот заголовок и сохраняет строку session_id=abc123xyz на жестком диске вашего компьютера. При каждом следующем запросе к этому же домену, браузер автоматически прикрепляет заголовок: Cookie: session_id=abc123xyz

    Сервер читает этот заголовок, ищет abc123xyz в своей оперативной памяти и понимает: «Ага, это Иван, он авторизовался 10 минут назад».

    2. Токены (JWT)

    В современной разработке (особенно для API и мобильных приложений) чаще используют токены. После логина сервер возвращает длинную зашифрованную строку (токен).

    JavaScript на клиенте сохраняет этот токен и вручную прикрепляет его к каждому fetch запросу в специальный заголовок авторизации: Authorization: Bearer eyJhbGciOiJIUzI1NiIsIn...

    В нашем ИИ-ассистенте мы будем использовать именно токен-авторизацию, так как она лучше подходит для REST API.

    Эволюция HTTP и HTTPS

    Протокол не стоит на месте. За десятилетия он прошел несколько стадий эволюции:

    * HTTP/1.0 (1996): Открывал новое TCP-соединение для каждого файла (HTML, картинка, скрипт). Это было невероятно медленно. HTTP/1.1 (1997): Ввел концепцию Keep-Alive*. Одно соединение остается открытым, и по нему скачиваются все файлы страницы по очереди. HTTP/2 (2015): Ввел мультиплексирование*. Теперь файлы передаются не по очереди, а параллельно в виде бинарных потоков внутри одного соединения. Сайты стали загружаться в разы быстрее. * HTTP/3 (2022): Отказался от тяжелого протокола TCP в пользу быстрого UDP (протокол QUIC), что решило проблему потери пакетов в мобильных сетях.

    Безопасность: Почему HTTP стал HTTPS?

    Оригинальный HTTP передает данные в открытом виде. Если вы сидите в публичном Wi-Fi в кафе и отправляете пароль по HTTP, любой хакер в этой же сети может перехватить пакеты и прочитать ваш пароль обычным текстом.

    HTTPS (HTTP Secure) решает эту проблему. Перед тем как отправить HTTP-запрос, клиент и сервер устанавливают зашифрованное TLS-соединение с использованием сложной математики (асимметричной криптографии с открытым и закрытым ключами).

    Даже если хакер перехватит HTTPS-запрос, он увидит лишь бессмысленный набор символов. Расшифровать его может только сервер, владеющий секретным ключом. Сегодня использование HTTPS является абсолютным стандартом — браузеры помечают обычные HTTP-сайты как «Небезопасные».

    Понимание HTTP — это суперсила веб-разработчика. Когда ваш код не работает, вы больше не будете гадать на кофейной гуще. Вы откроете вкладку Network (Сеть) в инструментах разработчика в браузере, посмотрите на сырые заголовки, увидите статус 401 или 500, и точно поймете, на чьей стороне проблема — клиента или сервера. В следующей статье мы применим эти знания на практике и напишем код на Python, который будет принимать и маршрутизировать эти HTTP-запросы.

    14. Автоматизация рутинных задач и написание скриптов

    Автоматизация рутинных задач и написание скриптов

    До этого момента мы рассматривали программирование как процесс создания масштабных систем: мы проектировали архитектуру баз данных, настраивали серверы и связывали их с браузером по протоколу HTTP. Это классическая разработка программного обеспечения (Software Engineering). Однако в реальной жизни программирование часто применяется для решения гораздо более приземленных, но не менее важных задач.

    Представьте, что вам нужно переименовать тысячу фотографий, добавив к каждой дату съемки. Или каждое утро заходить на сайт конкурента, копировать цены на десять товаров и вставлять их в Excel-таблицу. Выполнять это вручную — значит тратить часы на монотонную работу, рискуя совершить ошибку из-за потери концентрации.

    Здесь на сцену выходят скрипты (scripts — сценарии) — небольшие программы, написанные для автоматизации конкретных, узких задач. Если полноценное веб-приложение можно сравнить с огромным заводом, то скрипт — это специализированный робот-манипулятор на конвейере, который делает ровно одно действие, но делает его безупречно и молниеносно.

    Философия автоматизации: когда стоит писать код

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

    Эту концепцию можно выразить простой формулой:

    Где — сэкономленное время, — количество повторений задачи в будущем, — время на одно ручное выполнение, а — время на разработку и отладку скрипта.

    > Если задача занимает 5 минут, но вы делаете её каждый рабочий день, за год набегает более 20 часов рутины. Потратив 2 часа на написание скрипта сегодня, вы сэкономите 18 часов чистой жизни в этом году.

    Однако у автоматизации есть ловушка: иногда программисты тратят дни на автоматизацию задачи, которую нужно было выполнить всего один раз. Умение вовремя остановиться и сделать работу руками — важный навык инженера.

    Взаимодействие с операционной системой

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

    Для этого в Python используются встроенные модули os (Operating System) и shutil (Shell Utilities). Они позволяют создавать папки, перемещать файлы, читать их метаданные и удалять мусор.

    Давайте рассмотрим классическую задачу: у вас есть папка «Загрузки», в которой скопились сотни файлов разных форматов (картинки, документы, архивы). Мы хотим написать скрипт, который автоматически раскидает их по соответствующим папкам.

    Этот код демонстрирует базовый паттерн автоматизации: Сбор данных (получение списка файлов) Анализ (проверка расширения) Действие (перемещение).

    Обратите внимание на использование os.path.join(). Мы не пишем пути вручную через слэши (например, folder + "/" + filename), потому что на Windows используются обратные слэши \, а на macOS и Linux — прямые /. Функция join автоматически подставит правильный разделитель, делая наш скрипт кроссплатформенным.

    !Интерактивный симулятор сортировки файлов

    Сбор данных из интернета: Парсинг и Web Scraping

    Второй уровень автоматизации — выход за пределы локального компьютера. В предыдущей статье мы изучили, как общаться с серверами через REST API, получая аккуратные JSON-ответы. Но что делать, если у нужного вам сайта нет публичного API?

    В этом случае применяется Web Scraping (веб-скрейпинг) — программный сбор данных непосредственно с HTML-страниц, предназначенных для людей.

    Процесс состоит из двух этапов:

  • Скачать HTML-код страницы (например, с помощью библиотеки requests).
  • Найти в этой «каше» из тегов нужную информацию (с помощью парсеров, таких как BeautifulSoup).
  • Представим, что мы хотим написать скрипт, который следит за ценой определенной книги в интернет-магазине.

    Этика и подводные камни скрейпинга

    Скрейпинг — мощный инструмент, но он требует ответственности.

    Во-первых, сайты часто меняют свой дизайн. Если разработчики магазина переименуют класс price-tag в product-price, ваш скрипт мгновенно сломается. В отличие от API, которые поддерживают обратную совместимость, HTML-верстка нестабильна.

    Во-вторых, существует проблема нагрузки. Если ваш скрипт будет запрашивать страницу 100 раз в секунду, сервер магазина может не выдержать (это называется DDoS-атакой). Владельцы сайтов активно защищаются от ботов, блокируя IP-адреса. Поэтому хорошие скрипты всегда делают паузы между запросами (используя функцию time.sleep()) и уважают правила, описанные в файле robots.txt на сервере.

    Планировщики задач: Автоматизация без участия человека

    Скрипт, который нужно запускать вручную, автоматизирует задачу лишь наполовину. Настоящая магия начинается, когда код выполняется сам по заданному расписанию: каждый час, по пятницам в 18:00 или первого числа каждого месяца.

    Для этого используются системные планировщики задач. Они работают в фоновом режиме (как демоны) и запускают указанные программы в нужное время.

    В операционных системах Linux и macOS стандартом является Cron.

    Cron использует специфический, но очень лаконичный синтаксис для настройки расписания. Строка конфигурации состоит из пяти полей, разделенных пробелами, за которыми следует команда для выполнения.

    | Минуты (0-59) | Часы (0-23) | День месяца (1-31) | Месяц (1-12) | День недели (0-7) | Команда | | :--- | :--- | :--- | :--- | :--- | :--- | | 30 | 08 | | | 1-5 | python3 /path/to/script.py |

    В этом примере означает «любое значение». Расписание читается так: Запускать скрипт в 08:30, каждый день месяца, каждый месяц, но только с понедельника (1) по пятницу (5)*.

    Если вы работаете в Windows, аналогичный функционал предоставляет графическая утилита Task Scheduler (Планировщик заданий), где расписание настраивается через удобный интерфейс.

    !Схема базового цикла автоматизации

    Интеграция скриптов в архитектуру ИИ-ассистента

    Теперь давайте свяжем концепцию скриптов с главной целью нашего курса — созданием умного ИИ-ассистента.

    Современные Большие Языковые Модели (LLM), такие как GPT, невероятно умны в анализе текста, но по своей природе они изолированы. Они похожи на гениального мыслителя, запертого в комнате без окон: они могут написать блестящее эссе, но не могут отправить email, прочитать файл с вашего рабочего стола или узнать погоду на улице.

    Чтобы сделать ИИ-ассистента по-настоящему полезным, мы должны дать ему «руки». И этими руками выступают наши Python-скрипты. Эта архитектурная концепция называется Tool Use (Использование инструментов) или Function Calling (Вызов функций).

    Как это работает на практике

  • Намерение (Intent): Пользователь пишет в чат: «Ассистент, проанализируй мой отчет о продажах за май и сделай краткую выжимку».
  • Маршрутизация: ИИ понимает, что у него нет доступа к файлам. Вместо того чтобы генерировать текст, ИИ формирует специальный JSON-ответ: {"action": "read_file", "parameters": {"filename": "sales_may.xlsx"}}.
  • Выполнение скрипта: Наш серверный код на Python перехватывает этот JSON. Он видит команду read_file. Запускается заранее написанный нами скрипт, который находит нужный Excel-файл, извлекает из него сырые данные и переводит их в текстовый формат.
  • Возврат контекста: Скрипт отправляет извлеченный текст обратно в ИИ.
  • Финальный ответ: Теперь ИИ, обладая нужным контекстом, генерирует красивую аналитическую выжимку и отправляет её пользователю.
  • В этой парадигме скрипты перестают быть просто жестко запрограммированными алгоритмами. Они становятся модульными инструментами в арсенале искусственного интеллекта.

    Вы можете написать скрипт для поиска информации в Google, скрипт для добавления события в Google Календарь, скрипт для управления умным домом. Как только вы описываете ИИ-ассистенту, что делает каждый скрипт и какие аргументы он принимает, ассистент начинает сам решать, когда и какой скрипт запустить для выполнения сложной многошаговой задачи пользователя.

    Надежность и обработка ошибок

    Когда вы пишете скрипт для себя и запускаете его вручную, вы можете легко заметить ошибку (например, если пропал интернет). Но когда скрипт работает автоматически по расписанию (через Cron) или вызывается ИИ-ассистентом, он должен быть максимально отказоустойчивым.

    Главное правило автоматизации: Внешний мир непредсказуем.

  • Файл, который скрипт должен прочитать, может быть удален.
  • Сайт, который мы парсим, может вернуть ошибку 500 (Internal Server Error).
  • Жесткий диск может переполниться.
  • Если скрипт просто упадет с ошибкой, вы можете узнать об этом спустя недели, когда важные данные уже будут потеряны. Поэтому критически важно использовать конструкцию try...except для перехвата исключений и вести логирование (запись истории работы скрипта в отдельный текстовый файл).

    В этом примере мы не просто надеемся на лучшее. Мы устанавливаем таймаут (чтобы скрипт не завис навсегда, ожидая ответа от мертвого сервера) и аккуратно записываем любые проблемы в файл automation.log. Если ИИ-ассистент вызовет этот скрипт и произойдет ошибка, скрипт вернет ИИ понятное текстовое сообщение об ошибке, и ассистент сможет извиниться перед пользователем или предложить альтернативное решение.

    Автоматизация — это мост между сухой теорией программирования и реальной пользой. Написав свой первый скрипт, который сэкономит вам хотя бы 10 минут в день, вы почувствуете настоящую суперсилу инженера. В следующих статьях мы углубимся в то, как именно обучить нашего ИИ-ассистента правильно выбирать и запускать эти скрипты, превращая его из простого чат-бота в автономного агента.

    15. Обработка, фильтрация и базовый анализ данных

    Обработка, фильтрация и базовый анализ данных

    В предыдущей статье мы научились писать скрипты и собирать информацию из внешнего мира с помощью парсинга веб-страниц и работы с API. Представьте, что ваш скрипт успешно отработал и скачал цены на десять тысяч товаров из интернет-магазина, или собрал тысячи логов из чата вашего ИИ-ассистента.

    Вы смотрите на этот текстовый файл или базу данных, и возникает проблема: сами по себе эти данные бесполезны. Это просто цифровой шум.

    Сырые данные можно сравнить с нерафинированной нефтью. Чтобы получить из неё полезный продукт (бензин или пластик), нефть нужно очистить, разделить на фракции и переработать. В программировании этот процесс называется Data Pipeline (конвейер данных). Умение превращать хаос сырых данных в структурированную информацию — один из самых ценных навыков любого разработчика, от создателя веб-сайтов до инженера машинного обучения.

    Анатомия грязных данных

    В идеальном мире академических примеров данные всегда приходят в правильном формате. В реальном мире сырые данные (raw data) всегда «грязные».

    Если вы собираете данные от пользователей или парсите сайты, вы неизбежно столкнетесь со следующими проблемами:

  • Пропуски (Missing Values): Пользователь не указал возраст при регистрации, или на сайте не была указана цена товара. В коде это часто выглядит как None, null или пустая строка "".
  • Дубликаты: Из-за сетевой ошибки скрипт скачал одну и ту же страницу дважды.
  • Аномалии (Outliers): Опечатки, из-за которых цена хлеба составляет 1 000 000 рублей, или возраст пользователя равен 150 годам.
  • Несогласованность форматов: Один пользователь ввел телефон как +79991234567, другой как 8 (999) 123-45-67, а третий написал телефон скрыт.
  • Если мы передадим такие данные нашему ИИ-ассистенту или попытаемся построить по ним график, программа либо выдаст ошибку (например, попытается сложить число со строкой), либо выдаст ложный результат.

    > Мусор на входе — мусор на выходе (Garbage In, Garbage Out / GIGO). > > Фундаментальный принцип информатики, означающий, что качество результатов работы любой программы напрямую зависит от качества входных данных.

    Очистка и нормализация

    Первый этап работы с данными — их очистка и нормализация. Нормализация в контексте обработки данных — это приведение всех значений к единому стандарту и формату.

    Рассмотрим практический пример на Python. Допустим, мы спарсили список цен на книги, но из-за особенностей верстки сайта данные пришли в виде строк с лишними пробелами, валютой и пропусками.

    В этом коде мы применили сразу несколько концепций из предыдущих статей: цикл for для перебора, условные операторы if для логики ветвления и блок try...except для безопасной обработки ошибок преобразования типов.

    !Воронка обработки данных: от сырого хаоса к структурированной информации

    Фильтрация данных

    Когда данные очищены, их часто бывает слишком много. Фильтрация — это процесс отбора подмножества данных, которые соответствуют определенным критериям.

    В Python для элегантной фильтрации списков часто используется синтаксис List Comprehension (генератор списков). Это способ создать новый список из существующего, применив условие, записанное в одну строку.

    Допустим, у нас есть список словарей, представляющий пользователей нашего приложения. Мы хотим отфильтровать только тех, кто старше 18 лет и имеет активную подписку.

    Оба подхода делают абсолютно одно и то же, но второй вариант короче и считается более «питоничным» (Pythonic). Фильтрация позволяет нам сузить фокус и передавать на следующие этапы программы только релевантную информацию.

    Базовый анализ: Описательная статистика

    После очистки и фильтрации мы можем начать извлекать из данных смысл. Самый простой способ понять большой массив чисел — использовать описательную статистику (Descriptive Statistics). Она позволяет описать миллионы строк всего несколькими ключевыми метриками.

    Среднее арифметическое (Mean)

    Самая известная метрика. Чтобы найти среднее, нужно сложить все значения и разделить на их количество.

    В математике это записывается формулой:

    Где:

  • — среднее значение.
  • (сигма) — математический знак суммы.
  • — каждое конкретное значение в нашем наборе данных.
  • — общее количество значений.
  • Например, если пользователи оценили работу нашего ИИ-ассистента на 4, 5 и 3 звезды, средний рейтинг составит: .

    Проблема среднего значения и Медиана (Median)

    Среднее арифметическое имеет один критический недостаток: оно крайне чувствительно к выбросам (аномально большим или маленьким значениям).

    Представьте классическую ситуацию: в баре сидят 9 человек, каждый из которых зарабатывает 100 000 рублей в месяц. Средний доход в баре — 100 000 рублей. Внезапно в бар заходит Илон Маск с доходом в миллиарды. Если мы теперь посчитаем среднее арифметическое, получится, что в среднем каждый человек в этом баре — мультимиллионер. Но реальность 9 из 10 человек не изменилась! Среднее значение нас обмануло.

    Для решения этой проблемы используется Медиана.

    Медиана — это число, которое находится ровно посередине отсортированного набора данных. Половина значений находится ниже медианы, а половина — выше.

    Если мы выстроим доходы посетителей бара по возрастанию: 100к, 100к, 100к, 100к, 100к, 100к, 100к, 100к, 100к, 10 000 000к.

    Медиана (число посередине) останется равна 100 000 рублей. Она проигнорировала выброс и показала реальную картину.

    !Интерактивный калькулятор: Среднее значение против Медианы

    Помимо среднего и медианы, базовый анализ всегда включает поиск Минимума и Максимума (для понимания диапазона данных), а также подсчет количества элементов (Count).

    Переход на новый уровень: Библиотека Pandas

    Использовать стандартные списки и циклы for в Python отлично подходит для обработки сотен или тысяч записей. Но когда счет идет на миллионы строк (что является нормой в современном анализе данных), стандартный Python начинает работать слишком медленно.

    Вспомним статью об алгоритмической сложности. Поиск среднего значения через цикл for имеет сложность , где — количество элементов. Каждая итерация цикла в Python требует времени на проверку типов (так как Python — язык с динамической типизацией).

    Для профессиональной работы с данными была создана библиотека Pandas. Это индустриальный стандарт, который под капотом написан на быстром языке C, но предоставляет удобный интерфейс на Python.

    Главная структура данных в Pandas называется DataFrame (фрейм данных). Визуально это очень похоже на таблицу в Excel: есть строки (индексы) и столбцы (колонки).

    Давайте посмотрим, как задачи очистки, фильтрации и анализа выглядят с использованием Pandas:

    Векторизация: секрет скорости Pandas

    Почему код на Pandas работает в десятки раз быстрее? Секрет кроется в концепции векторизации.

    Когда мы пишем df["price"] * 2 (умножить все цены на 2), Pandas не перебирает элементы по одному, как это делает цикл for. Вместо этого он передает весь массив данных (вектор) на уровень процессора, который выполняет математическую операцию над всеми числами практически одновременно, используя оптимизированные инструкции языка C.

    | Характеристика | Стандартный Python (Списки) | Pandas (DataFrame) | | :--- | :--- | :--- | | Скорость на больших данных | Низкая (поэлементный перебор) | Высокая (векторизация на C) | | Удобство работы с таблицами | Низкое (список словарей) | Высокое (встроенные методы) | | Потребление памяти | Высокое (каждый объект хранит метаданные) | Низкое (данные хранятся плотными блоками) | | Порог входа | Низкий (базовый синтаксис) | Средний (нужно учить новые методы) |

    Роль обработки данных в создании ИИ-ассистента

    Может показаться, что анализ данных — это отдельная профессия (Data Analyst), не связанная с разработкой нашего ИИ-ассистента. Но это заблуждение. Современный ИИ не может существовать без качественного конвейера данных.

    Большие Языковые Модели (LLM), такие как GPT, имеют ограничение на объем текста, который они могут «запомнить» за один раз. Это ограничение называется Контекстным окном (Context Window).

    Представьте, что пользователь просит вашего ИИ-ассистента: «Проанализируй базу отзывов о нашем ресторане за год и скажи, на что чаще всего жалуются клиенты».

    Если в базе 50 000 отзывов, вы физически не сможете отправить их все в нейросеть — они не поместятся в контекстное окно, а если и поместятся, запрос будет стоить огромных денег (API нейросетей тарифицируются по количеству слов/токенов).

    Здесь на помощь приходят навыки обработки данных:

  • Очистка: Скрипт удаляет пустые отзывы и спам.
  • Фильтрация: Скрипт оставляет только отзывы с оценкой 1 или 2 звезды (ведь пользователь спросил про жалобы).
  • Агрегация: Если отзывов все еще слишком много, скрипт может сгруппировать их по датам или ключевым словам.
  • Формирование промпта: Только отфильтрованная, самая важная выжимка данных (например, 100 самых длинных негативных отзывов) отправляется в ИИ.
  • Этот архитектурный паттерн, когда мы сначала программно ищем нужные данные в базе, фильтруем их, и только потом отдаем нейросети для генерации красивого ответа, называется RAG (Retrieval-Augmented Generation — Генерация, дополненная поиском). Это фундамент всех современных корпоративных ИИ-систем.

    Умение писать код — это не просто знание синтаксиса. Это умение управлять потоками информации. Очищая данные от мусора и выделяя из них суть с помощью математики и алгоритмов, вы подготавливаете почву для следующего, самого захватывающего этапа нашего курса — машинного обучения и наделения программы зачатками интеллекта.

    16. Основы разработки мобильных приложений и интерфейсов

    Основы разработки мобильных приложений и интерфейсов

    В предыдущих статьях мы научились создавать серверную логику (бэкенд), проектировать базы данных и обрабатывать огромные массивы сырой информации. Наш ИИ-ассистент уже умеет думать, запоминать историю диалогов и анализировать данные. Но пока что он живет исключительно в терминале разработчика или в виде ответов сервера в формате JSON.

    Чтобы продукт стал доступен миллионам людей, ему нужно «лицо» — удобный графический интерфейс. Сегодня смартфоны генерируют более половины всего мирового интернет-трафика. Поэтому умение создавать мобильные приложения — это навык, который превращает невидимый серверный код в осязаемый продукт, лежащий в кармане пользователя.

    Анатомия мобильного приложения

    Мобильное приложение — это программа, спроектированная специально для работы на смартфонах и планшетах. В контексте клиент-серверной архитектуры, которую мы разбирали ранее, мобильное приложение выступает клиентом.

    Оно не выполняет тяжелых вычислений и не хранит терабайты данных. Его главные задачи:

  • Красиво отобразить данные, полученные от сервера.
  • Считать действия пользователя (нажатия, свайпы, голос).
  • Отправить эти действия на сервер через API.
  • Плавно отреагировать на ответ сервера.
  • > Мобильный телефон — это лишь окно в цифровой мир. Вся тяжелая работа выполняется на серверах.

    Представьте ресторан. Мобильное приложение — это красиво оформленное меню и вежливый официант. Бэкенд (наш Python-код) — это кухня с поварами. База данных — это склад продуктов. Официант не готовит еду сам, он лишь принимает заказ, передает его на кухню и приносит готовое блюдо клиенту.

    Нативная и кроссплатформенная разработка

    Исторически сложилось так, что рынок мобильных устройств поделен между двумя гигантами: Apple с операционной системой iOS и Google с операционной системой Android. Это две совершенно разные экосистемы, говорящие на разных языках.

    Перед любым разработчиком встает фундаментальный выбор: как именно создавать приложение?

    Нативная разработка (Native)

    Нативная разработка подразумевает создание приложения специально для одной конкретной платформы с использованием ее «родных» языков программирования и инструментов.

  • Для iOS используется язык Swift и среда разработки Xcode.
  • Для Android используется язык Kotlin (ранее Java) и среда Android Studio.
  • Плюсы этого подхода заключаются в максимальной производительности. Нативный код имеет прямой доступ ко всем аппаратным функциям телефона (камере, GPS, Bluetooth, процессору) без посредников. Приложение работает идеально плавно и выглядит в точности так, как задумали создатели операционной системы.

    Минус очевиден: вам нужно писать код дважды. Нужны две разные команды разработчиков, два разных бюджета и в два раза больше времени на тестирование.

    Кроссплатформенная разработка (Cross-platform)

    Чтобы решить проблему двойной работы, были созданы кроссплатформенные фреймворки. Самые популярные из них сегодня — это React Native (от Meta) и Flutter (от Google).

    Идея заключается в том, что вы пишете код один раз на одном языке (например, на JavaScript для React Native или на Dart для Flutter), а специальный компилятор переводит этот код в приложения, понятные и для iOS, и для Android.

    !Сравнение подходов к разработке мобильных приложений

    Аналогия: нативная разработка — это нанять двух переводчиков, один из которых идеально говорит по-французски, а другой по-японски. Кроссплатформенная разработка — это написать текст на английском и пропустить его через автоматический переводчик.

    Современные кроссплатформенные инструменты достигли такого уровня, что в 90% случаев пользователь не заметит разницы. Для нашего ИИ-ассистента, где интерфейс состоит в основном из текстового чата и кнопок, кроссплатформенный подход (например, Flutter) будет идеальным выбором, экономящим месяцы работы.

    Особенности мобильного UI/UX дизайна

    Создание интерфейса для телефона кардинально отличается от создания веб-сайта для компьютера. Разработчик должен учитывать физические и психологические ограничения мобильной среды.

    UI (User Interface) — это то, как приложение выглядит (цвета, шрифты, отступы). UX (User Experience) — это то, как приложение работает и насколько удобно им пользоваться.

    1. Проблема «Толстого пальца» (Fat Finger Problem)

    На компьютере у нас есть мышь с курсором размером в один пиксель, обеспечивающая хирургическую точность. На телефоне инструмент ввода — это палец пользователя.

    Если кнопки будут слишком маленькими или расположенными слишком близко друг к другу, пользователь будет постоянно промахиваться, что вызовет раздражение. Стандарт Apple гласит: минимальный размер кликабельного элемента должен составлять 44x44 пункта (примерно 7-10 мм на физическом экране).

    2. Дефицит экранного пространства

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

    Для ИИ-ассистента главным элементом является история диалога. Поле ввода текста, кнопка микрофона и кнопка отправки должны быть закреплены внизу (ближе к большому пальцу), а настройки и профиль лучше спрятать в боковое меню или на отдельный экран.

    3. Жесты вместо кликов

    Мобильные интерфейсы опираются на жесты: свайп (смахивание), пинч (щипок для масштабирования), долгое нажатие. Например, чтобы удалить сообщение в чате, пользователю удобнее смахнуть его влево, чем искать маленькую кнопку с иконкой корзины.

    Управление состоянием (State Management)

    Когда мы переходим от статических HTML-страниц к сложным мобильным приложениям, возникает главная архитектурная проблема фронтенда — Управление состоянием.

    Состояние (State) — это вся информация, которую приложение помнит в конкретный момент времени.

    В нашем ИИ-ассистенте состоянием будет:

  • Авторизован ли сейчас пользователь? (True/False)
  • Что он сейчас печатает в поле ввода? (Строка текста)
  • Ждем ли мы сейчас ответ от сервера? (True/False — от этого зависит, показывать ли анимацию загрузки)
  • Список всех сообщений в текущем чате (Массив объектов)
  • В современных фреймворках интерфейс — это функция от состояния. Это значит, что разработчик не меняет интерфейс напрямую (не пишет команды вроде «сделай эту кнопку красной»). Разработчик меняет данные в состоянии, а интерфейс автоматически перерисовывается, чтобы соответствовать новым данным.

    !Интерактивный симулятор управления состоянием чата

    Рассмотрим жизненный цикл отправки сообщения:

  • Пользователь нажимает кнопку «Отправить».
  • Приложение берет текст из состояния inputText и добавляет его в массив messages.
  • Приложение меняет состояние isLoading на True.
  • Интерфейс мгновенно реагирует: текст появляется в чате, а рядом возникает крутящийся индикатор загрузки.
  • Приложение отправляет HTTP POST запрос на наш Python-сервер.
  • Сервер обрабатывает запрос через LLM и возвращает JSON с ответом.
  • Приложение получает ответ, добавляет его в массив messages и меняет isLoading на False.
  • Индикатор загрузки исчезает, появляется текст от ИИ.
  • Этот паттерн гарантирует, что интерфейс всегда синхронизирован с реальными данными, предотвращая баги, когда на экране отображается одно, а в памяти устройства хранится другое.

    Работа с сетью в мобильной среде

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

    Профессиональное мобильное приложение должно быть готово к нестабильной сети. Это достигается несколькими методами:

    Асинхронность

    Как мы обсуждали в статье про JavaScript, сетевые запросы должны быть асинхронными. Если приложение отправит запрос к ИИ-ассистенту и полностью заблокирует свою работу в ожидании ответа, интерфейс «зависнет». Пользователь не сможет даже прокрутить чат. Асинхронный код отправляет запрос в фоновом режиме, позволяя интерфейсу оставаться отзывчивым.

    Кэширование и локальные базы данных

    Чтобы приложение не выглядело пустым при отсутствии интернета, последние загруженные данные сохраняются в памяти телефона. Для этого используются легковесные локальные базы данных, такие как SQLite или Realm.

    Когда пользователь открывает чат с ИИ-ассистентом в авиарежиме, приложение сначала читает историю из локальной базы данных (мгновенно) и показывает ее на экране. И только потом пытается сходить на сервер за новыми сообщениями. Этот подход называется Offline-First (Сначала оффлайн).

    Идемпотентность и повторные попытки (Retries)

    Представьте: пользователь написал длинный запрос ИИ-ассистенту, нажал «Отправить», и в этот момент поезд заехал в туннель. Запрос оборвался.

    Хорошее приложение перехватит ошибку сети и сохранит сообщение в локальную очередь. Как только интернет появится, приложение автоматически попытается отправить запрос снова. Здесь критически важно вспомнить концепцию идемпотентности из статьи про REST API: сервер должен быть спроектирован так, чтобы случайная двойная отправка одного и того же сообщения (если сеть моргнула) не привела к дублированию ответа в базе данных.

    Архитектурные паттерны: MVVM

    По мере роста приложения код может превратиться в запутанный клубок, где логика работы с сетью перемешана с дизайном кнопок. Чтобы этого избежать, мобильные разработчики используют архитектурные паттерны. Самый популярный сегодня — MVVM (Model-View-ViewModel).

  • Model (Модель): Это слой данных. Здесь находятся классы, описывающие структуру сообщения, и код, который делает HTTP-запросы к нашему бэкенду.
  • View (Представление): Это «глупый» визуальный слой. Кнопки, тексты, анимации. View не знает, откуда берутся данные, он умеет только рисовать их на экране и сообщать о кликах.
  • ViewModel (Модель Представления): Это мозг экрана, посредник. ViewModel получает данные из Model, подготавливает их (например, форматирует дату из 2023-10-25T14:30:00Z в понятное Сегодня, 14:30) и передает во View.
  • Разделение ответственности (Separation of Concerns) позволяет нам легко тестировать логику приложения без необходимости запускать графический интерфейс, а также менять дизайн, не трогая сложную бизнес-логику.

    Интеграция всего курса: Путь одного сообщения

    Давайте соберем воедино знания из всех предыдущих статей и проследим путь одного сообщения от мобильного телефона до базы данных и обратно.

  • Клиент (Мобильное приложение): Пользователь нажимает на кнопку микрофона. Нативный API телефона записывает звук. Приложение конвертирует аудио в текст (или отправляет аудиофайл как есть).
  • Сеть (HTTP/REST): Приложение формирует POST-запрос. В тело запроса кладется JSON: {"user_id": 123, "message": "Какая сегодня погода?"}. Запрос летит через интернет.
  • Сервер (FastAPI/Python): Наш бэкенд принимает запрос. Он проверяет токен авторизации пользователя.
  • База данных (SQL): Сервер делает SELECT-запрос в реляционную базу данных, чтобы достать историю предыдущих сообщений этого пользователя (контекст диалога).
  • Обработка данных: Сервер фильтрует историю, отбрасывая слишком старые сообщения, чтобы уложиться в лимит токенов нейросети.
  • ИИ-логика (LLM): Сервер отправляет промпт и историю в API большой языковой модели (например, OpenAI).
  • Автоматизация (Tool Use): LLM понимает, что ей нужна погода. Она просит сервер запустить скрипт парсинга погоды. Сервер выполняет скрипт, получает данные и возвращает их LLM. LLM генерирует финальный ответ.
  • Сохранение (SQL): Сервер сохраняет вопрос пользователя и ответ ИИ в базу данных.
  • Ответ (HTTP): Сервер возвращает мобильному приложению JSON со статусом 200 OK и текстом ответа.
  • Обновление интерфейса (State Management): Мобильное приложение получает JSON, обновляет состояние (State), и на экране пользователя плавно появляется сообщение от ИИ-ассистента.
  • Разработка мобильных приложений — это вершина айсберга. Пользователь видит только красивый интерфейс и плавные анимации, но теперь вы понимаете, какая колоссальная инженерная работа, алгоритмы и структуры данных скрываются под капотом каждого нажатия на экран.

    17. Введение в создание игр и архитектура игрового цикла

    Введение в создание игр и архитектура игрового цикла\n\nВ предыдущих модулях мы научились создавать серверную логику, проектировать базы данных, верстать веб-страницы и разрабатывать мобильные интерфейсы. Все эти системы, несмотря на их сложность, объединяет одна фундаментальная черта: они реактивны. Они ждут, пока пользователь нажмет кнопку, отправит сообщение или сделает свайп, и только после этого выполняют полезную работу. \n\nНо что, если мы хотим создать мир, который живет по собственным законам? Мир, где гравитация постоянно тянет объекты вниз, враги патрулируют территорию, а смена дня и ночи происходит независимо от того, трогает ли игрок клавиатуру. Для создания таких автономных систем требуется совершенно иной архитектурный подход. \n\n## От пакетной обработки к живым мирам\n\nЧтобы понять, почему игры устроены иначе, чем обычные приложения, полезно взглянуть на эволюцию взаимодействия человека и компьютера.\n\nНа заре вычислительной техники программы работали в пакетном режиме (batch mode). Программист загружал стопку перфокарт в машину, нажимал кнопку запуска и уходил пить кофе. Компьютер выполнял вычисления и выдавал результат. Программа не предполагала никакого вмешательства в процессе работы. Сегодня по такому принципу работают скрипты для автоматизации рутинных задач, которые мы писали ранее: они запускаются, обрабатывают данные и завершаются.\n\nС появлением мониторов и клавиатур возникла потребность в мгновенной обратной связи. Появились интерактивные программы, такие как первые текстовые квесты. Они работали по принципу пошагового диалога:\n\n> Вы стоите перед входом в пещеру. Вокруг темный лес.\n> > ВОЙТИ В ПЕЩЕРУ\n> Вы вошли в пещеру. Здесь пахнет сыростью.\n\nКод таких программ представлял собой бесконечный цикл, который блокировал выполнение программы, ожидая ввода пользователя. Как только пользователь вводил команду, программа обрабатывала ее, выдавала текст и снова замирала в ожидании.\n\nСовременные веб-сайты и мобильные приложения работают на основе цикла событий (Event Loop). Под капотом они делают то же самое: приложение загружается и засыпает, ожидая событий (кликов, скроллов, сетевых запросов). Если вы откроете приложение банка и положите телефон на стол, программа не будет потреблять ресурсы процессора — она просто ждет.\n\nИгры ломают эту парадигму. Игра не может позволить себе уснуть. Если игрок отпустил геймпад, игровой мир должен продолжать жить: анимации должны проигрываться, физика — просчитываться, а искусственный интеллект врагов — принимать решения. \n\n## Архитектура игрового цикла (Game Loop)\n\nДля решения задачи непрерывной симуляции был создан паттерн Игровой цикл (Game Loop). Это сердце любой игры, от простейшего Тетриса до современных блокбастеров с фотореалистичной графикой.\n\nИгровой цикл — это бесконечный цикл while, который работает так быстро, как только может, и на каждой итерации выполняет три строго определенные фазы.\n\n### Фаза 1: Обработка ввода (Process Input)\nНа этом этапе игра опрашивает все устройства ввода: клавиатуру, мышь, геймпад, сенсорный экран. В отличие от веб-приложений, где нажатие кнопки мгновенно вызывает функцию (событийно-ориентированная модель), игра просто фиксирует состояние кнопок в данный момент времени. Например, игра записывает: "В эту миллисекунду зажата клавиша W и левая кнопка мыши".\n\n### Фаза 2: Обновление состояния (Update)\nЭто мозг игры. Здесь применяется вся бизнес-логика и математика. Опираясь на данные о вводе из первой фазы, игра обновляет внутреннее состояние мира:\n- Если зажата клавиша W, координаты персонажа изменяются.\n- Просчитывается физика: гравитация тянет объекты вниз, проверяются столкновения (коллизии) между пулями и стенами.\n- Обновляется искусственный интеллект: враг замечает игрока и меняет свое состояние с "Патрулирование" на "Атака".\n- Уменьшаются таймеры (например, время до взрыва гранаты).\n\nВажно понимать, что на этом этапе на экране ничего не меняется. Все вычисления происходят исключительно в оперативной памяти компьютера (в переменных, массивах и объектах классов).\n\n### Фаза 3: Отрисовка (Render)\nТолько после того, как все математические расчеты завершены, игра берет текущее состояние мира и рисует его на экране. Координаты объектов переводятся в пиксели, накладываются текстуры, рассчитывается освещение. Как только картинка готова, она выводится на монитор, и цикл начинается заново.\n\n!Схема архитектуры игрового цикла\n\nОдна полная итерация этого цикла называется кадром (frame). Количество таких итераций, которые компьютер успевает сделать за одну секунду, называется FPS (Frames Per Second — кадры в секунду). Если игра работает при 60 FPS, это значит, что цикл ввода, обновления и отрисовки выполняется 60 раз каждую секунду.\n\n## Проблема скорости процессора\n\nВ раннюю эпоху программирования разработчики писали игры под конкретное железо. Например, игра создавалась для процессора с частотой 1 МГц. Программист знал, что игровой цикл будет выполняться ровно 30 раз в секунду. Исходя из этого, он задавал скорость движения персонажа: "сдвигать корабль на 2 пикселя за каждый кадр".\n\nНо когда выходили новые, более мощные компьютеры, возникала проблема. Новый процессор мог выполнять цикл не 30, а 120 раз в секунду. Поскольку скорость персонажа была жестко привязана к кадрам (2 пикселя за итерацию), на новом компьютере игра работала в 4 раза быстрее. Враги носились по экрану с невероятной скоростью, и играть становилось невозможно. Игра была привязана к тактовой частоте процессора.\n\nЧтобы отвязать скорость игры от мощности железа, программисты ввели концепцию Временного шага (Time step), более известную как Delta Time.\n\n## Математика времени: Delta Time\n\nDelta Time (обозначается как ) — это количество времени, прошедшее с момента завершения предыдущего кадра до текущего кадра. Обычно это значение измеряется в секундах или миллисекундах.\n\nВместо того чтобы задавать скорость в "пикселях за кадр", мы начинаем мыслить физическими величинами: "пиксели в секунду". \n\nВспомним базовую формулу из школьной физики:\n\n\nГде:\n- — пройденное расстояние.\n- — скорость объекта.\n- — время в пути.\n\nВ контексте программирования игрового цикла эта формула адаптируется для вычисления новой позиции объекта на каждом кадре:\n\n\nГде:\n- — текущая координата объекта на экране.\n- — желаемая скорость в пикселях в секунду.\n- — время, прошедшее с прошлого кадра (в секундах).\n\nДавайте рассмотрим конкретный пример с числами. Допустим, мы хотим, чтобы наш персонаж двигался со скоростью 100 пикселей в секунду. Игру запускают на двух разных компьютерах.\n\nКомпьютер А (Слабый, выдает 10 FPS):\nПоскольку игра выдает 10 кадров в секунду, каждый кадр занимает 1/10 секунды. Значит, сек.\nЗа один кадр персонаж сдвинется на: пикселей.\nЗа одну секунду пройдет 10 кадров: пикселей.\n\nКомпьютер Б (Мощный, выдает 50 FPS):\nИгра выдает 50 кадров в секунду, каждый кадр занимает 1/50 секунды. Значит, сек.\nЗа один кадр персонаж сдвинется на: пикселя.\nЗа одну секунду пройдет 50 кадров: пикселей.\n\n!Интерактивная демонстрация влияния Delta Time на скорость движения\n\nРезультат: независимо от того, насколько мощный у пользователя компьютер и сколько FPS он выдает, персонаж за одну реальную секунду пройдет ровно 100 пикселей. На слабом компьютере движение будет выглядеть дерганым (большие скачки по 10 пикселей), а на мощном — идеально плавным (маленькие шажки по 2 пикселя), но скорость игрового процесса останется идентичной.\n\nИспользование — это золотой стандарт индустрии. Это значение передается во все функции обновления (Update) для каждого объекта в игре, чтобы синхронизировать анимации, физику и таймеры с реальным мировым временем.\n\n## Объектно-ориентированный подход в игровом цикле\n\nКак мы обсуждали в статье про Объектно-Ориентированное Программирование (ООП), классы позволяют нам объединять данные (состояние) и методы (поведение). В контексте игр ООП раскрывается в полной мере.\n\nПредставьте, что в нашей игре есть сотни объектов: игрок, враги, летящие пули, аптечки. Писать логику для каждого из них прямо в главном цикле while невозможно — код превратится в нечитаемую кашу.\n\nВместо этого создается базовый класс Entity (Игровая сущность), от которого наследуются все объекты в игре. Главное правило: каждый такой класс обязан иметь метод update(delta_time) и метод render().\n\nГлавный игровой цикл становится невероятно простым и элегантным. Он просто перебирает список всех существующих в мире объектов и вызывает у них соответствующие методы:\n\n\n\nВ этой архитектуре каждый объект сам знает, как себя вести. Класс Bullet (Пуля) в своем методе update будет просто лететь вперед по прямой. Класс Enemy (Враг) будет вычислять расстояние до игрока и двигаться к нему. Главному циклу не нужно знать детали — он работает как дирижер, который просто дает команду "играть" каждому музыканту в оркестре.\n\n## От игр к ИИ-ассистентам: цикл автономного агента\n\nВы можете задаться вопросом: зачем мы изучаем архитектуру игр в курсе, конечная цель которого — создание продвинутого ИИ-ассистента? \n\nДело в том, что современные автономные ИИ-агенты (такие как AutoGPT или сложные системы на базе LLM) строятся на архитектуре, поразительно похожей на игровой цикл. \n\nОбычный чат-бот (как ChatGPT) работает по принципу цикла событий: вы задаете вопрос, он генерирует ответ и засыпает. Но автономный ИИ-агент, которому поручили сложную задачу (например, "проанализируй рынок конкурентов, напиши отчет и отправь его по почте"), должен действовать самостоятельно, без постоянных пинков от пользователя.\n\nДля этого ИИ-агенты используют цикл ReAct (Reason + Act), который является прямой адаптацией игрового цикла:\n\n1. Observe (Ввод): Агент считывает текущее состояние среды. Что сейчас на экране? Какие данные пришли из API? Что ответил веб-сайт?\n2. Reason (Обновление): Агент передает эти данные в языковую модель (LLM). Модель анализирует ситуацию и принимает решение о следующем шаге.\n3. Act (Отрисовка/Действие): Агент выполняет действие: кликает по кнопке на сайте, запускает Python-скрипт или делает запись в базу данных.\n\nЭтот цикл while (goal_not_reached) крутится до тех пор, пока ИИ не решит поставленную задачу. Понимание того, как управлять состоянием в бесконечном цикле, как изолировать логику сущностей и как работать с дельта-временем (чтобы ИИ не спамил запросами к серверу слишком быстро) — это навыки, которые делают из обычного веб-разработчика архитектора сложных автономных систем.\n\nИгровой цикл учит нас главному: как создавать программы, которые не просто реагируют на мир, а живут в нем.\n\n

    18. Интеграция сторонних API и облачных сервисов

    Интеграция сторонних API и облачных сервисов

    В предыдущих модулях мы проделали колоссальную работу: научились писать серверную логику, проектировать базы данных, верстать интерфейсы и даже управлять автономными циклами. Наши программы стали сложными и функциональными. Однако до этого момента они напоминали изолированные острова. Наш сервер общался только с нашей базой данных и нашим клиентом.

    В реальном мире ни одно серьезное приложение не живет в вакууме. Современная разработка строится на принципе делегирования: вместо того чтобы изобретать велосипед, разработчики используют готовые решения от других компаний. Именно здесь на сцену выходят сторонние API и облачные сервисы.

    Что такое сторонний API и зачем он нужен

    Аббревиатура API (Application Programming Interface — программный интерфейс приложения) уже встречалась нам, когда мы создавали собственный сервер. Мы писали REST API, чтобы наш фронтенд мог общаться с нашим бэкендом.

    Сторонний API (Third-party API) — это точно такой же интерфейс, но предоставленный другой компанией, чтобы ваше приложение могло использовать ее технологии, данные или вычислительные мощности.

    Представьте, что вы открываете завод по производству автомобилей. Вы можете попытаться делать абсолютно всё сами: добывать каучук для шин, плавить стекло для окон, шить кожу для сидений. Это займет десятилетия и потребует миллиардных инвестиций. Альтернативный путь — покупать готовые шины у Michelin, стекла у AGC, а электронику у Bosch, сосредоточившись на сборке и дизайне самого автомобиля.

    В программировании работает тот же принцип. Если вы создаете интернет-магазин, вам не нужно становиться банком, чтобы принимать платежи по картам. Вы интегрируете API платежной системы (например, Stripe или ЮKassa). Если вам нужна карта для отслеживания курьера, вы не запускаете собственные спутники — вы подключаете Google Maps API или Яндекс Карты API.

    Преимущества использования сторонних API

    | Критерий | Разработка с нуля | Интеграция стороннего API | | :--- | :--- | :--- | | Время запуска | Месяцы или годы | От нескольких часов до пары недель | | Стоимость | Огромные затраты на зарплаты инженерам | Оплата по факту использования (часто есть бесплатный лимит) | | Поддержка | Вы сами чините все баги и обновляете систему | Провайдер API сам следит за работоспособностью серверов | | Фокус | Распыление внимания на побочные задачи | Концентрация на уникальной бизнес-логике вашего продукта |

    В контексте нашего курса интеграция API — это единственный адекватный способ наделить нашего ИИ-ассистента интеллектом. Обучение собственной большой языковой модели (LLM) уровня ChatGPT требует кластеров из тысяч видеокарт стоимостью в сотни миллионов долларов. Вместо этого мы арендуем «мозг» по API, оплачивая только те слова, которые модель для нас генерирует.

    !Схема взаимодействия: Пользователь отправляет запрос на наш сервер, наш сервер обращается к стороннему API, получает ответ и возвращает его пользователю

    Анатомия интеграции: ключи, токены и безопасность

    Когда вы обращаетесь к чужому серверу, этот сервер должен понимать, кто вы такой, чтобы списать деньги с вашего баланса и убедиться, что у вас есть права на выполнение операции. Этот процесс называется аутентификацией.

    В отличие от пользователей-людей, которые вводят логин и пароль в красивую форму на сайте, программы используют специальные строки символов.

    API-ключи (API Keys)

    Самый простой и распространенный способ аутентификации. API-ключ — это длинная уникальная строка, которая выдается вам при регистрации в сервисе.

    Пример ключа: sk-proj-5b3a9c8d7e6f1a2b3c4d5e6f7a8b9c0d

    Вы просто прикрепляете этот ключ к каждому HTTP-запросу. Обычно это делается через заголовки (Headers). Например, сервис может требовать передавать ключ в заголовке Authorization с приставкой Bearer (предъявитель).

    > Bearer-токен можно сравнить с билетом на концерт. Охраннику на входе неважно, как вас зовут и где вы живете. Ему важно только то, что вы предъявили валидный билет. Кто владеет билетом (токеном), тот и получает доступ.

    Главное правило безопасности: переменные окружения

    Самая частая и фатальная ошибка новичков — жесткое кодирование (hardcoding) API-ключей прямо в тексте программы.

    Если вы сохраните такой код и загрузите его в публичный репозиторий (например, на GitHub), специальные боты-хакеры найдут ваш ключ в течение нескольких секунд. Они тут же начнут использовать его для своих задач (например, для генерации спама через ИИ), и к утру вы можете обнаружить счет от облачного провайдера на десятки тысяч долларов.

    Чтобы этого избежать, ключи хранят в переменных окружения (Environment Variables). Это специальные настройки операционной системы или сервера, которые программа может прочитать во время работы, но которых нет в самом коде.

    В Python для этого используется библиотека python-dotenv и скрытый файл .env, который добавляется в исключения системы контроля версий (файл .gitignore), чтобы он никогда не попал в интернет.

    Пример правильного подхода:

    Файл .env (лежит только на вашем компьютере): OPENAI_API_KEY=sk-proj-секретный_ключ

    Файл main.py (можно безопасно публиковать):

    Практика: Подключение ИИ-модели к нашему коду

    Давайте посмотрим, как выглядит реальный код интеграции на Python. Наша задача — отправить текстовый запрос (промпт) в облачный сервис (например, OpenAI API или YandexGPT) и получить сгенерированный ответ.

    Для отправки HTTP-запросов в Python стандартом индустрии является библиотека requests. Она берет на себя всю черновую работу по формированию правильного текстового HTTP-сообщения, которое мы изучали в модуле про сети.

    Обратите внимание на структуру data. Мы не просто отправляем строку текста. Мы отправляем структурированный JSON (в Python это словарь), где указываем конкретную модель, массив сообщений (историю диалога) и параметры генерации (например, temperature — степень креативности модели).

    !Интерактивный симулятор API-запроса

    Подводные камни интеграций и обработка ошибок

    Когда вы вызываете функцию внутри своей программы (например, calculate_sum(2, 2)), вы уверены, что она выполнится мгновенно. Но когда вы делаете сетевой запрос к стороннему API, вы выходите в непредсказуемый внешний мир.

    Кабель на дне океана может быть поврежден, сервер провайдера может перезагружаться, или ваш интернет может пропасть на секунду. Профессиональный код отличается от любительского тем, что он готов к этим сбоям.

    1. Сетевые задержки и таймауты

    По умолчанию библиотека requests может ждать ответа бесконечно. Если сервер API «зависнет» и не закроет соединение, ваша программа тоже зависнет навсегда, ожидая ответа. Это может привести к полной остановке вашего бэкенда.

    Решение: всегда указывать параметр timeout (время ожидания в секундах).

    2. Лимиты запросов (Rate Limits)

    Ни один облачный сервис не позволит вам отправлять миллион запросов в секунду. Для защиты от перегрузок (и DDoS-атак) провайдеры устанавливают Rate Limits (лимиты частоты). Например: «не более 50 запросов в минуту».

    Если вы превысите этот лимит, сервер перестанет обрабатывать ваши данные и начнет возвращать HTTP статус-код 429 Too Many Requests.

    Правильная архитектура должна уметь обрабатывать 429 ошибку. Обычно в заголовках ответа сервер передает параметр Retry-After, который говорит: «Подожди 5 секунд и попробуй снова». Ваша программа должна «уснуть» на это время и затем автоматически повторить запрос, чтобы пользователь даже не заметил сбоя.

    3. Ошибки авторизации (401 и 403)

    Ошибка 401 Unauthorized означает, что сервер вас не узнал. Чаще всего это происходит, если вы забыли передать токен, передали его с опечаткой или срок действия токена истек.

    Ошибка 403 Forbidden означает, что сервер вас узнал, но у вас нет прав на это действие. Например, ваш API-ключ дает право только на чтение данных, а вы пытаетесь отправить POST-запрос на создание новой записи, или у вас закончились деньги на балансе.

    Экономика облачных API

    Облачные сервисы редко бывают полностью бесплатными. В сфере ИИ-ассистентов тарификация обычно происходит за токены.

    Токен — это кусочек слова. В английском языке 1 токен примерно равен 4 символам (или 0.75 слова). В русском языке из-за особенностей кодировки одно слово может разбиваться на 2-4 токена.

    Провайдеры берут деньги как за входящие токены (ваш промпт), так и за исходящие (ответ модели).

    Расчет стоимости одного запроса можно описать так: Стоимость = (Входящие токены / 1000 × Цена за 1000 входящих) + (Исходящие токены / 1000 × Цена за 1000 исходящих).

    Например, если цена за 1000 входящих токенов составляет 0.15 руб., а за исходящие — 0.60 руб., то запрос длиной 500 токенов и ответ длиной 200 токенов обойдется вам в: (500 / 1000 × 0.15) + (200 / 1000 × 0.60) = 0.075 + 0.12 = 0.195 руб.

    Понимание этой экономики критически важно. Если ваш ИИ-ассистент хранит огромную историю переписки и отправляет ее целиком при каждом новом сообщении пользователя, количество входящих токенов будет расти в геометрической прогрессии, сжигая ваш бюджет. Именно поэтому в предыдущих модулях мы изучали алгоритмы фильтрации и базы данных — чтобы извлекать из истории только самые важные факты перед отправкой их в API.

    Архитектура ИИ-ассистента: собираем пазл

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

  • Клиент (Frontend): Мобильное приложение или веб-сайт, который мы сверстали. Пользователь пишет сообщение «Составь план тренировок» и нажимает кнопку отправки.
  • Наш сервер (Backend): Написанный на Python (FastAPI), он принимает запрос от клиента. Сервер проверяет, авторизован ли пользователь в нашей системе, и достает из базы данных (SQL) историю его предыдущих тренировок.
  • Интеграция (Third-party API): Наш сервер формирует большой промпт, объединяя запрос пользователя и данные из БД. Затем он делает HTTP-запрос к облачному API (например, OpenAI), передавая свой секретный API-ключ.
  • Облако (Cloud): Мощные серверы провайдера прогоняют текст через нейросеть, генерируют ответ и возвращают его нашему серверу в формате JSON.
  • Сохранение (Database): Наш сервер получает ответ, сохраняет его в базу данных, чтобы не забыть контекст для будущих вопросов.
  • Ответ клиенту: Наш сервер отправляет готовый текст обратно на мобильное приложение, где он красиво отрисовывается на экране.
  • В этой схеме наш сервер выступает в роли умного посредника (Proxy). Мы никогда не зашиваем API-ключи от платных нейросетей напрямую в мобильное приложение. Если бы мы это сделали, любой хакер мог бы скачать наше приложение, извлечь ключ и пользоваться нейросетью за наш счет. Клиент общается только с нашим сервером, а уже наш сервер, находясь в безопасной среде, общается с внешним миром.

    Интеграция API — это суперсила современного разработчика. Она позволяет одному программисту за выходные собрать продукт, который еще 10 лет назад требовал бы работы целого отдела инженеров. Понимая принципы HTTP, JSON и безопасной работы с ключами, вы можете подключить к своему приложению машинное зрение, генерацию голоса, отправку SMS, банковские переводы и тысячи других сервисов, создавая по-настоящему масштабные проекты.

    19. Основы работы с большими языковыми моделями (LLM)

    Основы работы с большими языковыми моделями (LLM)

    В предыдущем модуле мы научились выходить за пределы нашего локального сервера и общаться с внешним миром через сторонние API. Мы отправляли текстовые запросы в облако и получали осмысленные ответы. Наш ИИ-ассистент обрел «голос» и способность рассуждать. Но что именно происходит на серверах OpenAI, Google или Яндекса, когда они получают наш запрос?

    Долгое время искусственный интеллект казался магией. Сегодня мы снимем эту завесу тайны. В этой статье мы разберем анатомию Больших Языковых Моделей (Large Language Models, LLM), поймем, как они «мыслят», почему иногда галлюцинируют, и научимся запускать их прямо на своем компьютере, не отправляя данные в облако.

    Иллюзия разума: как работает LLM

    Когда вы общаетесь с ChatGPT, возникает стойкое ощущение, что на другом конце провода сидит эрудированный собеседник, который понимает смысл ваших слов. На самом деле, базовая механика любой языковой модели поразительно проста и прагматична.

    LLM — это гигантский калькулятор вероятностей, чья единственная задача — предсказать следующее слово (токен) в тексте.

    Представьте функцию автодополнения (Т9) в вашем смартфоне. Когда вы печатаете «Я иду в...», телефон предлагает варианты: «магазин», «кино», «школу». Он делает это на основе статистики ваших прошлых сообщений. LLM делает абсолютно то же самое, но на стероидах. Она прочитала почти весь интернет (миллионы книг, статей, форумов и репозиториев кода) и выучила статистические связи между словами в человеческих языках.

    Если вы дадите модели фразу «Мама мыла...», она математически вычислит, что с вероятностью 99% следующим словом должно быть «раму».

    Когда вы задаете сложный вопрос, например: «Напиши код на Python для сортировки списка», модель не «думает» над алгоритмом. Она вычисляет, какое слово статистически чаще всего следует за этой фразой в обучающих данных, затем вычисляет следующее слово с учетом только что сгенерированного, и так далее, пока не сгенерирует специальный невидимый символ «конец текста».

    > Большие языковые модели не обладают сознанием, логикой или пониманием реальности. Они обладают лишь колоссальной статистической картой того, как люди используют язык.

    !Интерактивный симулятор предсказания следующего токена

    Анатомия LLM: Токены, Параметры и Контекст

    Чтобы профессионально работать с нейросетями, необходимо понимать три фундаментальных термина, которые определяют их возможности и стоимость.

    1. Токены: алфавит нейросетей

    Нейросети — это математические модели. Они не умеют работать с буквами (А, Б, В) или словами. Они работают только с числами. Поэтому перед тем, как текст попадет в модель, он проходит процесс токенизации — разбиения на кусочки, каждому из которых присваивается уникальный числовой ID.

    Токен — это фрагмент слова. В английском языке один токен примерно равен 4 символам (или 0.75 слова). Короткие и частые слова (например, apple) могут быть одним токеном. Длинные или редкие слова (например, unbelievable) разбиваются на 2-3 токена (un + believ + able).

    В русском языке ситуация сложнее. Из-за особенностей кодировки кириллицы одно русское слово может разбиваться на 3-5 токенов. Именно поэтому использование облачных API для русского языка часто обходится дороже — вы платите за каждый сгенерированный токен.

    2. Контекстное окно: кратковременная память

    У каждой модели есть строгий лимит на количество текста, которое она может «удержать в голове» за один раз. Этот лимит называется контекстным окном (Context Window).

    Если контекстное окно модели составляет 8 000 токенов (примерно 6 000 слов), это значит, что сумма вашего запроса (промпта) и ответа модели не может превышать этот лимит.

    Что произойдет, если вы загрузите в такую модель книгу на 20 000 слов и попросите сделать краткий пересказ? Модель просто «забудет» начало книги. Она физически не способна обработать данные за пределами своего окна.

    В архитектуре нашего ИИ-ассистента мы решаем эту проблему с помощью баз данных и алгоритмов поиска (RAG — Retrieval-Augmented Generation). Вместо того чтобы отправлять модели всю историю переписки за год, мы с помощью SQL-запросов достаем только те 5-10 сообщений, которые релевантны текущему вопросу пользователя, экономя токены и не переполняя контекстное окно.

    3. Параметры: размер «мозга»

    Когда в новостях пишут: «Выпущена новая модель на 70 миллиардов параметров», речь идет о количестве внутренних связей в нейронной сети.

    Параметры (веса) — это числа, которые модель выучила во время тренировки. Грубо говоря, это синапсы искусственного мозга. Чем больше параметров, тем больше нюансов, фактов и сложных логических связей может запомнить модель.

  • 7B - 14B параметров (миллиардов): Маленькие модели. Отлично работают на обычных компьютерах. Подходят для простых задач: суммаризация текста, базовый перевод, извлечение фактов.
  • 70B - 100B параметров: Средние модели. Требуют мощных серверов. Умеют писать хороший код, анализировать сложные документы, поддерживать глубокий диалог.
  • 1000B+ параметров (как GPT-4): Гигантские модели. Работают только в огромных дата-центрах. Обладают энциклопедическими знаниями и способны к сложным многоступенчатым рассуждениям.
  • !Схема работы Трансформера: от текста к вероятностям

    Революция Трансформеров и Механизм Внимания

    До 2017 года языковые модели читали текст строго последовательно, слово за словом (слева направо). Это приводило к тому, что к концу длинного предложения модель забывала, о чем шла речь в начале.

    В 2017 году исследователи из Google опубликовали статью «Attention Is All You Need», которая перевернула индустрию. Они представили архитектуру Трансформер (Transformer) и механизм Внимания (Self-Attention).

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

    Рассмотрим пример: «Банк одобрил кредит, потому что у клиента была хорошая история. Он находился на другом берегу реки».

    Слово «Банк» имеет два значения (финансовое учреждение и берег). Благодаря механизму внимания, когда модель обрабатывает первое предложение, она связывает слово «Банк» со словами «кредит» и «клиент», понимая контекст. Во втором предложении она связывает слово «берег» со словом «реки». Трансформер не просто предсказывает слова, он строит сложную математическую паутину смыслов.

    Облачные API против Локальных моделей (Open Source)

    До сих пор мы говорили об использовании LLM через сторонние API (например, OpenAI). Это модель Closed Source (закрытый исходный код). Вы не имеете доступа к самой нейросети, вы лишь отправляете ей данные и получаете результат.

    Но существует и другой подход — Local AI Engineering (использование открытых моделей).

    Такие компании, как Meta (модели Llama) или Mistral AI, публикуют веса своих моделей в открытый доступ. Вы можете скачать этот «мозг» (файл размером от 4 до 40 гигабайт) и запустить его прямо на своем сервере или даже мощном ноутбуке.

    Сравнение подходов

    | Критерий | Облачные API (Closed Source) | Локальные модели (Open Source) | | :--- | :--- | :--- | | Приватность данных | Вы отправляете данные чужой компании. Не подходит для коммерческой тайны, медицинских или банковских данных. | 100% приватность. Данные никогда не покидают ваш компьютер или сервер компании. | | Стоимость | Оплата за каждый токен. При миллионах запросов счета могут достигать тысяч долларов. | Бесплатно (вы платите только за электричество и покупку собственного оборудования). | | Оборудование | Достаточно слабого ноутбука, все вычисления происходят на серверах провайдера. | Требуются мощные видеокарты (GPU) с большим объемом видеопамяти (VRAM). | | Контроль | Провайдер может изменить модель, отключить вам доступ или ввести цензуру в любой момент. | Полный контроль. Вы можете дообучать модель (Fine-tuning) под свои специфические задачи. |

    Для нашего ИИ-ассистента выбор зависит от задачи. Если мы делаем бота для помощи с домашними заданиями — облачное API идеально. Но если мы создаем корпоративного ассистента, который будет анализировать секретные финансовые отчеты компании — мы обязаны использовать локальную модель.

    Практика: Запуск локальной LLM на Python

    Давайте посмотрим, как выглядит код для запуска открытой модели прямо на вашем компьютере. Для этого в экосистеме Python существует библиотека transformers от компании Hugging Face (это своеобразный GitHub для нейросетей).

    В этом примере мы загрузим небольшую модель (например, Mistral 7B), передадим ей текст и получим ответ.

    Примечание: для выполнения этого кода в реальности потребуется видеокарта NVIDIA с минимум 8 ГБ видеопамяти, либо мощный процессор (но это будет медленнее).

    Обратите внимание на параметр temperature. Это важнейшая настройка любой LLM.

  • Если temperature = 0.0, модель становится абсолютно детерминированной. Она всегда будет выбирать токен с максимальной математической вероятностью. Ответы будут сухими, точными и однообразными (идеально для написания кода или анализа данных).
  • Если temperature = 0.8 или выше, модель начинает выбирать менее вероятные токены. Ответы становятся креативными, разнообразными, но повышается риск «галлюцинаций» (выдумывания несуществующих фактов).
  • Промпт-инжиниринг: искусство общения с ИИ

    Независимо от того, используете вы облачное API или локальную модель, качество ответа на 90% зависит от того, как вы сформулировали задачу. Этот навык называется Промпт-инжинирингом (Prompt Engineering).

    Многие новички пишут запросы так, будто общаются с человеком, который умеет читать мысли: «Сделай мне сайт про собак». Модель выдаст базовый, скучный HTML-код, потому что она не знает ваших предпочтений.

    Профессиональный промпт строится по фреймворку RTF (Role, Task, Format):

  • Role (Роль): Задайте модели контекст и экспертизу.
  • «Ты — Senior Frontend разработчик с 10-летним опытом создания современных, адаптивных веб-интерфейсов».
  • Task (Задача): Максимально подробно опишите, что нужно сделать, включая ограничения.
  • «Напиши HTML и CSS код для главной страницы приюта для собак. Страница должна содержать шапку, блок 'О нас' и галерею из 3 карточек питомцев. Используй современный минималистичный дизайн. Не используй JavaScript».
  • Format (Формат): Укажите, в каком виде вы хотите получить результат.
  • «Верни результат в виде двух отдельных блоков кода: сначала HTML, затем CSS. Не пиши никаких пояснений до или после кода».

    В архитектуре нашего ИИ-ассистента мы не заставляем пользователя писать такие сложные промпты каждый раз. Мы зашиваем Роль и Формат в серверную логику (в так называемый System Prompt), а от пользователя принимаем только саму Задачу. Наш Python-сервер склеивает их вместе перед отправкой в LLM.

    Собираем всё вместе: место LLM в нашем проекте

    Теперь, когда мы понимаем, как работают языковые модели, давайте посмотрим на итоговую архитектуру нашего ИИ-ассистента, объединяющую все знания из предыдущих модулей курса:

  • Клиент (Frontend/Mobile): Пользователь нажимает кнопку микрофона и говорит: «Напомни, что я просил купить вчера?». Интерфейс переводит голос в текст и отправляет HTTP POST-запрос на наш сервер.
  • Сервер (Backend на Python): Наш сервер принимает запрос. Он обращается к реляционной базе данных (SQL), чтобы найти сообщения пользователя за вчерашний день.
  • Формирование контекста: Сервер берет найденные в БД записи (например, «купить молоко и хлеб») и формирует системный промпт: «Ты полезный ассистент. Используй следующий контекст для ответа: [молоко, хлеб]. Вопрос пользователя: Напомни, что я просил купить вчера?».
  • Обращение к LLM: Сервер отправляет этот промпт через API в облачную модель (или передает локальной модели через библиотеку transformers).
  • Генерация: Трансформер токенизирует текст, прогоняет его через миллиарды параметров, применяет механизм внимания и по одному токену генерирует ответ: «Вчера вы просили купить молоко и хлеб».
  • Ответ: Сервер получает сгенерированный текст, сохраняет его в базу данных (чтобы помнить контекст для будущих вопросов) и отправляет обратно на клиент, где он отображается на экране.
  • Большие языковые модели — это не просто умные чат-боты. Это новый тип вычислительного движка. Если раньше мы писали жесткие алгоритмы (if-else) для обработки текста, то теперь мы можем делегировать понимание неструктурированных данных нейросети. Понимание того, как работают токены, контекстное окно и промпты, делает вас не просто пользователем ИИ, а инженером, способным создавать системы следующего поколения.

    2. Управляющие конструкции и логика ветвлений

    Управляющие конструкции и логика ветвлений

    Представьте, что вы читаете книгу. Вы начинаете с первой страницы, читаете строку за строкой, абзац за абзацем, пока не дойдете до конца. Именно так по умолчанию работает компьютерная программа: она выполняет инструкции строго последовательно, сверху вниз.

    Но что, если бы все программы работали только так? Калькулятор мог бы сложить только два заранее заданных числа и выключиться. Видеоигра заканчивалась бы через секунду после запуска, выполнив линейный набор команд. Наш будущий ИИ-ассистент смог бы сказать только «Привет», не обращая внимания на то, что именно вы у него спросили.

    Чтобы программы стали по-настоящему умными, интерактивными и полезными, они должны уметь принимать решения и повторять действия. Для этого в программировании существуют управляющие конструкции — специальные механизмы, которые позволяют изменять линейный ход выполнения кода. Они превращают программу из глупого исполнителя одной мелодии в разумный механизм, способный адаптироваться к ситуации.

    Основа принятия решений: Логические выражения

    Прежде чем программа сможет выбрать, по какому пути пойти, она должна задать вопрос. В мире компьютеров любой вопрос должен быть сформулирован так, чтобы на него можно было ответить только «Да» или «Нет» (True или False).

    В прошлой статье мы познакомились с логическим типом данных (Boolean). Теперь мы научимся генерировать эти значения с помощью операторов сравнения.

    Операторы сравнения

    Компьютер отлично умеет сравнивать данные. Для этого используются математические символы, которые немного адаптированы под клавиатуру:

    | Оператор в коде | Математическая запись | Значение | Пример (результат) | | :--- | :--- | :--- | :--- | | == | | Равно | 5 == 5 (True) | | != | | Не равно | 10 != 5 (True) | | > | | Больше | 3 > 8 (False) | | < | | Меньше | 4 < 9 (True) | | >= | | Больше или равно | 10 >= 10 (True) | | <= | | Меньше или равно | 5 <= 2 (False) |

    > Критически важное отличие: В программировании один знак равенства = означает присваивание (положить значение в переменную). Два знака равенства == означают сравнение (проверить, равны ли значения). Путаница между = и == — самая частая ошибка в первые недели написания кода.

    Логические операторы (AND, OR, NOT)

    В реальной жизни условия редко бывают простыми. Чтобы получить ипотеку, недостаточно просто быть старше 18 лет. Нужно быть старше 18 лет И иметь стабильный доход. Для объединения нескольких простых условий в одно сложное используются логические операторы.

  • Логическое И (AND): Результат будет True, только если все объединенные условия истинны.
  • Пример: (age >= 18) and (income > 50000)
  • Логическое ИЛИ (OR): Результат будет True, если хотя бы одно из условий истинно.
  • Пример: (day == "Суббота") or (day == "Воскресенье") — проверка на выходной день.
  • Логическое НЕ (NOT): Инвертирует значение. True превращается в False, и наоборот.
  • Пример: not (isBanned) — если пользователь НЕ заблокирован.

    Ветвление: Условный оператор if-else

    Теперь, когда мы умеем задавать вопросы, давайте научим программу действовать в зависимости от ответов. Главный инструмент для этого — конструкция if-else (если-иначе).

    Базовый оператор if

    Оператор if проверяет условие. Если оно истинно (True), выполняется определенный блок кода. Если ложно (False) — программа просто игнорирует этот блок и идет дальше.

    В этом примере, так как , программа зайдет внутрь блока if, выведет сообщение об успехе и спишет деньги. Если бы товар стоил 1500, программа бы молча пропустила блок покупки и сразу вывела «Спасибо за визит».

    Добавляем альтернативу: else

    Часто нам нужно сказать программе: «Если условие выполняется, сделай это, а иначе — сделай что-то другое». Для этого используется ключевое слово else.

    Теперь программа никогда не промолчит. Она выберет ровно один из двух путей.

    !Схема работы условного оператора if-else

    Множественный выбор: else if (elif)

    Жизнь не всегда делится только на черное и белое. Иногда вариантов больше двух. Представьте, что мы пишем логику для оценки температуры на улице. Нам нужно проверить несколько условий подряд. В Python для этого используется слово elif (сокращение от else if), в других языках — else if.

    Как это работает: Компьютер проверяет условия строго сверху вниз. Как только он находит первое подходящее условие (в нашем случае ), он выполняет его блок кода и немедленно покидает всю конструкцию. Он даже не будет проверять блок else.

    Конструкция Switch / Match

    Когда нам нужно сравнить одну переменную с большим количеством конкретных значений (например, определить день недели по его номеру от 1 до 7), писать длинную цепочку if-elif-elif-elif становится утомительно.

    Для таких случаев во многих языках (C++, Java, Go, JavaScript) существует конструкция switch. В Python аналогичная конструкция появилась недавно и называется match.

    Пример логики на языке Go:

    Блок default здесь играет ту же роль, что и else — он срабатывает, если ни один из case не подошел.

    Циклы: Искусство автоматизации

    Ветвления позволяют программе выбирать путь. Но настоящая мощь компьютеров заключается в их способности выполнять рутинную работу с невероятной скоростью, не уставая и не совершая ошибок.

    Если вам нужно вывести на экран числа от 1 до 5, вы можете написать команду вывода пять раз. Но что, если нужно вывести числа от 1 до 10 000? Здесь на помощь приходят циклы.

    Цикл — это управляющая конструкция, которая заставляет блок кода выполняться снова и снова, пока выполняется определенное условие.

    Цикл while (Пока)

    Цикл while работает как зацикленный оператор if. Он говорит компьютеру: «Пока условие истинно, выполняй этот код. Как только код выполнится, вернись в начало и проверь условие снова».

    В этом примере:

  • Компьютер проверяет: ? Да. Съедаем яблоко, остается 2.
  • Возвращается наверх. ? Да. Съедаем яблоко, остается 1.
  • Возвращается наверх. ? Да. Съедаем яблоко, остается 0.
  • Возвращается наверх. ? Нет (False). Цикл завершается, программа идет дальше.
  • !Интерактивная визуализация работы цикла while

    Опасность бесконечного цикла: Если вы забудете изменить переменную внутри цикла (например, уберете строку apples = apples - 1), условие будет оставаться истинным вечно. Программа зависнет, бесконечно печатая «Съедаем одно яблоко...», пока не закончится оперативная память или вы не закроете ее принудительно.

    Цикл for (Для)

    Если while отлично подходит для ситуаций, когда мы не знаем точное количество повторений (например, «пока пользователь не введет правильный пароль»), то цикл for создан для случаев, когда количество шагов известно заранее, или когда нам нужно перебрать элементы в коллекции (например, буквы в строке или товары в корзине).

    Пример вывода чисел от 0 до 4 в Python:

    Здесь i — это переменная-счетчик, которая автоматически создается циклом и на каждом шаге (итерации) принимает новое значение: 0, затем 1, 2, 3 и 4.

    Управление потоком внутри цикла: break и continue

    Иногда нам нужно вмешаться в работу цикла прямо в процессе его выполнения. Для этого существуют две специальные команды.

    Оператор break (Прервать)

    Команда break работает как стоп-кран. Она немедленно прерывает выполнение всего цикла, независимо от того, истинно ли еще условие.

    Представьте, что вы ищете конкретную книгу на полке из 100 книг. Вы перебираете их одну за другой (это цикл). Как только вы находите нужную, вам нет смысла просматривать оставшиеся 60 книг. Вы берете книгу и прекращаете поиск.

    Оператор continue (Продолжить)

    Команда continue работает мягче. Она прерывает только текущий шаг (итерацию) цикла и заставляет программу сразу перейти к следующему шагу.

    Аналогия: вы проверяете яблоки в корзине. Если яблоко гнилое, вы не прекращаете проверку всей корзины (break), вы просто выбрасываете это конкретное яблоко и берете следующее (continue).

    Результат работы этого кода выведет: 1, 2, 4, 5. Число 3 было пропущено.

    Как это связано с созданием ИИ-ассистента?

    Управляющие конструкции — это нервная система любой программы, включая искусственный интеллект. Без них ИИ был бы просто статичным хранилищем данных. Давайте посмотрим, как логика ветвлений и циклы оживляют нашего будущего ассистента:

  • Бесконечный цикл ожидания (while):
  • Ваш голосовой помощник (например, Siri или Алиса) не выключается после одного ответа. В его основе лежит бесконечный цикл while True:, который постоянно слушает микрофон. Он прервется только если вы выключите устройство.

  • Распознавание намерений (if-elif-else):
  • Когда ИИ перевел ваш голос в текст, ему нужно понять, что делать дальше. * if intent == "погода": -> запустить функцию поиска погоды. * elif intent == "будильник": -> установить таймер. * else: -> отправить запрос в большую языковую модель (LLM) для генерации свободного ответа.

  • Обработка данных (for):
  • Если вы попросите ИИ: «Прочитай мои последние 5 писем», он получит список писем с сервера и использует цикл for email in emails:, чтобы по очереди озвучить каждое из них.

  • Фильтрация контента (continue / break):
  • Если ИИ анализирует длинный текст в поисках конкретного факта, он может использовать break, как только найдет ответ, чтобы сэкономить вычислительные ресурсы. А если он читает комментарии и натыкается на спам, он использует continue, чтобы пропустить его и перейти к следующему.

    Освоив переменные и управляющие конструкции, вы получили в свои руки базовый алфавит программирования. Вы уже можете писать программы, которые решают математические задачи, автоматизируют расчеты и принимают решения. В следующей статье мы научимся упаковывать эти блоки кода в удобные, переиспользуемые инструменты — функции.

    20. Проектирование и создание собственного ИИ-ассистента

    Проектирование и создание собственного ИИ-ассистента

    На протяжении предыдущих модулей мы изучали программирование как набор изолированных инструментов. Мы разобрали, как хранить данные в переменных, как управлять логикой с помощью циклов и ветвлений, как проектировать архитектуру через классы, сохранять информацию в базы данных и общаться с внешним миром по сети. Теперь пришло время объединить все эти технологии в единую, сложную и интеллектуальную систему.

    Создание собственного ИИ-ассистента — это кульминация базового курса программирования. Это проект, в котором классический детерминированный код (где всё предсказуемо) встречается с вероятностной природой нейросетей (где результат генерируется на лету).

    Эволюция: от Чат-бота к ИИ-Агенту

    Прежде чем проектировать систему, необходимо провести четкую границу между обычным чат-ботом и полноценным ИИ-агентом.

    Классический чат-бот работает по жестко заданному сценарию (дереву решений). Он получает сообщение, ищет ключевые слова и отправляет заранее заготовленный ответ. Если пользователь отклоняется от сценария, бот ломается и выдает ошибку. Это похоже на автомат с напитками: вы нажимаете конкретную кнопку и получаете конкретную банку.

    ИИ-агент устроен принципиально иначе. Он понимает контекст диалога, способен самостоятельно принимать решения и выполнять цепочку действий для достижения цели. Агент не просто отвечает на вопросы — он использует инструменты. Если чат-бот — это автомат с напитками, то ИИ-агент — это бариста, который выслушает ваши пожелания, уточнит детали, смешает ингредиенты и подаст уникальный кофе.

    Ключевые отличия ИИ-агента: * Адаптивность: Агент не ломается при смене темы. Он обращается к базе знаний и формулирует ответ на лету. * Автономные действия: Агент может создавать события в календаре, отправлять письма или искать информацию в интернете прямо во время диалога. * Удержание контекста: Агент помнит, о чем вы говорили пять сообщений назад, и учитывает это при ответе.

    Анатомия системы: как технологии работают вместе

    Чтобы ИИ-ассистент работал автономно и надежно, нам потребуется выстроить многоуровневую архитектуру. В ней каждый изученный нами ранее инструмент выполняет свою уникальную роль.

  • Клиентская часть (Frontend / Mobile): Это «лицо» и «уши» нашего ассистента. Веб-страница на HTML/CSS/JS или мобильное приложение, где пользователь вводит текст или записывает голосовое сообщение. Клиент отвечает только за отображение данных и отправку HTTP-запросов на сервер.
  • Серверная логика (Backend на Python): Это «нервная система». Сервер принимает запросы от клиента, проверяет права доступа, управляет потоком данных и координирует работу всех остальных узлов. Именно здесь живут наши классы, функции и бизнес-логика.
  • База данных (SQL): Это «долгосрочная память». Здесь хранятся профили пользователей, настройки и, самое главное, история диалогов. Без базы данных ассистент страдал бы тяжелой формой амнезии, забывая вас после каждого сообщения.
  • Языковая модель (LLM API): Это «мозг». Сервер отправляет сюда текст, а модель возвращает осмысленный ответ или команду к действию.
  • !Архитектура ИИ-ассистента: взаимодействие клиента, сервера, базы данных и языковой модели

    Проектирование личности: Системный промпт

    Любой ИИ-ассистент начинается с базовой инструкции — системного промпта (System Prompt). Это скрытый от пользователя текст, который отправляется в языковую модель при каждом запросе. Он задает роль, тон общения и строгие границы дозволенного.

    Плохой системный промпт: «Ты полезный помощник. Отвечай на вопросы пользователя». С таким промптом модель будет вести себя непредсказуемо, давать слишком длинные ответы и легко поддастся на провокации пользователя (например, просьбу написать вредоносный код).

    Профессиональный системный промпт строится как должностная инструкция для сотрудника:

    > Ты — Jarvis, персональный ИИ-ассистент для продуктивности. Твоя главная цель — помогать пользователю планировать день, писать код и анализировать данные. > > ПРАВИЛА: > 1. Отвечай кратко и по делу. Не используй вводные фразы вроде «Конечно, я помогу». > 2. Если ты не знаешь ответа, прямо скажи об этом. Не выдумывай факты. > 3. Тон общения: профессиональный, но дружелюбный. Обращайся к пользователю на «ты». > 4. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО обсуждать политику, религию и давать медицинские советы.

    В коде нашего Python-сервера этот промпт хранится как константа и всегда вставляется в самое начало массива сообщений перед отправкой в API.

    Управление памятью: Базы данных и Контекстное окно

    Как мы помним из предыдущей статьи, у любой языковой модели есть лимит кратковременной памяти — контекстное окно (например, 8000 токенов). Если мы будем отправлять в модель всю историю переписки за год при каждом запросе, мы не только мгновенно исчерпаем лимит, но и разоримся на оплате API.

    Здесь на помощь приходят реляционные базы данных (SQL) и алгоритмы фильтрации.

    Когда пользователь отправляет новое сообщение, наш Python-сервер выполняет следующие шаги:

  • Сохраняет новое сообщение пользователя в таблицу messages в базе данных.
  • Делает SQL-запрос SELECT, чтобы достать последние 10 сообщений из этого конкретного диалога (сортировка по времени).
  • Формирует из этих 10 сообщений JSON-массив.
  • Добавляет системный промпт в начало массива.
  • Отправляет этот компактный пакет данных в LLM.
  • Получает ответ от LLM, отправляет его пользователю и параллельно сохраняет в ту же таблицу messages.
  • Таким образом, мы создаем иллюзию бесконечной памяти. Модель всегда видит актуальный контекст беседы, а старые сообщения безопасно лежат на жестком диске сервера, не расходуя дорогие токены.

    Интеграция с внешним миром: Вызов функций (Tool Use)

    Сама по себе языковая модель заперта в цифровой коробке. Она не может узнать текущую погоду, потому что ее знания ограничены датой окончания тренировки. Она не может поставить будильник, потому что у нее нет рук.

    Чтобы сделать ассистента по-настоящему полезным, мы должны дать ему инструменты. В инженерии ИИ эта концепция называется Function Calling (Вызов функций) или Tool Use.

    Механика работы выглядит как магия, но под капотом это строгий обмен JSON-файлами и использование словарей Python.

    Как ИИ «нажимает кнопки»

    Вместе с текстовым запросом мы отправляем в LLM специальный JSON-объект — описание функций, которые есть на нашем сервере. Мы как бы говорим модели: «Смотри, у меня есть функция get_weather(city). Если пользователю нужна погода, не пытайся угадать ее, а просто скажи мне вызвать эту функцию».

    Если пользователь пишет: «Какая сейчас погода в Токио?», модель понимает, что ей не хватает знаний. Вместо обычного текстового ответа, она генерирует JSON-команду:

    Наш Python-сервер получает этот JSON. С помощью условных конструкций (if/elif) или словарей-маршрутизаторов он понимает, что нужно сделать:

    Получив реальные данные (например, «+15 градусов, дождь»), модель наконец-то генерирует человеческий ответ: «В Токио сейчас +15°C и идет дождь. Не забудьте зонт!».

    Именно так ИИ-ассистенты бронируют билеты, управляют умным домом и пишут письма. Модель лишь принимает логическое решение, а физическую работу (выполнение кода) делает наш классический Python-сервер.

    Архитектура агента: Цикл ReAct

    Когда задачи становятся сложными, одного вызова функции недостаточно. Что если пользователь просит: «Узнай погоду в Париже на завтра и, если там будет дождь, перенеси мою утреннюю пробежку в календаре на вечер»?

    Для решения таких задач используется архитектурный паттерн ReAct (Reason + Act / Рассуждение + Действие). Это бесконечный цикл while, внутри которого агент шаг за шагом приближается к цели.

    Цикл состоит из трех фаз:

  • Thought (Мысль): Модель анализирует текущую ситуацию и решает, что делать дальше.
  • Action (Действие): Модель выбирает инструмент и передает ему аргументы.
  • Observation (Наблюдение): Сервер выполняет действие и возвращает результат модели.
  • !Интерактивная симуляция цикла ReAct: от получения задачи до финального ответа

    Для сложного запроса выше цикл ReAct будет выглядеть так:

    * Thought 1: Мне нужно узнать погоду в Париже на завтра. * Action 1: Вызов инструмента get_weather(city="Paris", date="tomorrow"). * Observation 1: Сервер возвращает: «Завтра в Париже сильный дождь». * Thought 2: Идет дождь. Значит, мне нужно найти утреннюю пробежку в календаре и перенести ее на вечер. * Action 2: Вызов инструмента update_calendar(event="пробежка", new_time="19:00"). * Observation 2: Сервер возвращает: «Событие успешно обновлено». * Thought 3: Все условия выполнены. Я могу ответить пользователю. * Финальный ответ: «Я проверил погоду — завтра в Париже ожидается сильный дождь. Поэтому я перенес вашу утреннюю пробежку на 19:00».

    Этот цикл прерывается оператором break только тогда, когда модель решает, что задача полностью выполнена.

    Обработка ошибок и отказоустойчивость

    В реальном мире сети обрываются, API сторонних сервисов падают, а языковые модели иногда «галлюцинируют» (выдают неверный формат данных). Профессиональный программист всегда проектирует систему с учетом возможных сбоев.

    Если LLM вместо правильного JSON вернула нечитаемый текст, наш сервер не должен упасть с фатальной ошибкой. Мы используем конструкцию try...except для перехвата ошибки парсинга. Более того, мы можем программно отправить ошибку обратно в LLM с сообщением: «Ты вернул невалидный JSON. Пожалуйста, исправь синтаксическую ошибку и отправь снова». Удивительно, но современные модели отлично понимают свои ошибки и исправляют их со второй попытки.

    Два пути разработки: No-Code против собственного кода

    Сегодня индустрия предлагает два принципиально разных подхода к созданию ИИ-ассистентов.

    Путь No-Code (Без кода): Платформы вроде кастомных GPT внутри ChatGPT или специализированные конструкторы (BotHelp, различные платформы для создания ботов в Telegram) позволяют собрать ассистента за 30 минут. Вы просто пишете системный промпт в текстовое поле, загружаете пару PDF-файлов для базы знаний и нажимаете «Опубликовать». Плюсы: Мгновенный запуск, не нужны серверы. Минусы: Вы полностью зависите от платформы. Вы не контролируете данные пользователей, не можете внедрить сложную бизнес-логику или нестандартные интеграции с внутренними корпоративными системами.

    Путь Code (Собственная разработка через API): Это путь, к которому вас готовил этот курс. Вы арендуете сервер, поднимаете базу данных PostgreSQL, пишете логику на Python (FastAPI) и общаетесь с LLM через Assistants API или напрямую отправляя HTTP-запросы. Плюсы: Абсолютный контроль. Вы можете запустить локальную бесплатную модель (например, Llama 3) для полной конфиденциальности. Вы можете написать любые инструменты (Tools) — от парсинга сайтов до управления промышленным оборудованием. Минусы: Требует глубоких знаний архитектуры, сетей и программирования.

    Сборка воедино: Путь одного сообщения

    Давайте проследим путь данных в нашей готовой системе от начала до конца. Это резюмирует весь курс программирования.

  • Пользователь открывает веб-страницу (HTML/CSS) и нажимает кнопку «Отправить». JavaScript перехватывает событие клика (Event Listener).
  • JavaScript формирует JSON с текстом сообщения и отправляет асинхронный HTTP POST-запрос (Fetch API) на наш сервер.
  • Python-сервер (FastAPI) принимает запрос. Он извлекает токен авторизации из заголовков и проверяет, кто именно к нам пришел.
  • Сервер делает SQL-запрос SELECT в базу данных, чтобы достать историю переписки этого пользователя.
  • Python объединяет системный промпт, историю из БД и новое сообщение в один большой список словарей.
  • Сервер делает свой собственный HTTP-запрос к API языковой модели (например, OpenAI), передавая этот список и описание доступных функций (Tools).
  • Модель анализирует текст. Если нужен инструмент, она возвращает JSON с командой. Сервер выполняет скрипт (например, поиск в Google) и возвращает результат модели (Цикл ReAct).
  • Модель генерирует финальный текстовый ответ.
  • Сервер сохраняет этот ответ в базу данных (SQL INSERT), чтобы запомнить его для будущих вопросов.
  • Сервер отправляет HTTP-ответ (статус 200 OK) обратно в браузер.
  • JavaScript получает ответ, находит нужный <div> в DOM-дереве и отрисовывает сообщение на экране.
  • Весь этот процесс занимает около двух секунд.

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

    3. Функции, область видимости и декомпозиция кода

    Функции, область видимости и декомпозиция кода

    Представьте, что вы решили испечь торт. Если вы делаете это впервые, вам нужна подробная инструкция: возьмите 200 граммов муки, разбейте три яйца, добавьте сахар, перемешайте, поставьте в духовку на 40 минут при температуре 180 градусов.

    Но если вы профессиональный шеф-повар и руководите кухней ресторана, вы не будете диктовать су-шефу каждый шаг. Вы просто скажете: «Приготовь бисквит». Ваш помощник уже знает, какая последовательность действий скрывается за словом «бисквит».

    В предыдущих статьях мы научились писать базовые инструкции для компьютера: создавать переменные, проверять условия с помощью ветвлений и повторять действия в циклах. Мы писали код строка за строкой, как подробный рецепт для новичка. Но по мере роста программы такой подход перестает работать. Если код вашего ИИ-ассистента будет состоять из 10 000 строк, идущих подряд, вы сами перестанете понимать, как он работает, уже через неделю.

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

    Функции: Фабрики внутри программы

    Функция — это именованный блок кода, который решает одну конкретную задачу. Вы можете написать этот блок один раз, а затем вызывать его по имени в любом месте программы столько раз, сколько потребуется.

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

    Анатомия функции

    Давайте разберем создание функции на примере языка Python. Допустим, нам нужно рассчитывать финальную стоимость товара с учетом налога.

    Разберем этот механизм по деталям:

  • Объявление (Определение): Слово def (от английского define — определить) говорит компьютеру: «Сейчас я буду создавать новую функцию».
  • Имя функции: calculate_price. Имя должно быть глаголом, отражающим суть действия. Оно подчиняется тем же правилам, что и имена переменных (обычно используется snake_case или camelCase).
  • Параметры (Входные данные): В скобках указаны base_price и tax_rate. Это переменные, которые функция ожидает получить извне для своей работы. Их может быть сколько угодно, или не быть вообще.
  • Тело функции: Блок кода с отступом. Здесь происходит вся магия — вычисления, проверки, циклы.
  • Возврат результата: Ключевое слово return отдает результат работы обратно в ту часть программы, которая вызвала функцию.
  • Вызов функции

    Сама по себе написанная функция ничего не делает. Она как выключенный станок на заводе. Чтобы она заработала, ее нужно вызвать и передать ей аргументы (конкретные значения для параметров).

    > Критически важное отличие: Начинающие часто путают вывод на экран (print) и возврат значения (return). > print просто показывает текст на мониторе для человека. Программа забывает это значение в ту же секунду. > return передает данные обратно в программу, чтобы компьютер мог использовать их для дальнейших математических расчетов или логических операций.

    Зачем нужны функции?

    Использование функций опирается на один из главных принципов программирования — DRY (Don't Repeat Yourself — Не повторяй себя).

    Если вам нужно сделать одно и то же действие в трех разных местах программы, вы не должны копировать и вставлять код три раза. Вы должны вынести это действие в функцию и вызвать ее трижды.

    Почему это важно? Представьте, что формула расчета налога изменилась. Если вы скопировали код в 50 мест, вам придется искать и исправлять его 50 раз. Вы обязательно где-то ошибетесь. Если вы использовали функцию, вам нужно изменить ровно одну строку внутри этой функции, и изменения мгновенно применятся ко всей программе.

    Область видимости: Кто кого видит?

    Когда вы начинаете разбивать программу на функции, возникает важный вопрос: как переменные внутри функций взаимодействуют с остальной программой? Здесь в игру вступает концепция области видимости (Scope).

    Область видимости — это правило, определяющее, в какой части программы существует и доступна конкретная переменная.

    Локальная область видимости

    Переменные, созданные внутри функции, называются локальными. Они рождаются в момент вызова функции и навсегда уничтожаются в момент ее завершения. Внешний мир о них ничего не знает.

    Переменная message заперта внутри функции greet_user. Это сделано специально. Благодаря этому правилу, над одной программой могут работать десять разных программистов. Каждый из них может создать внутри своей функции переменную с именем result или index, и они никогда не перепутаются и не сломают код друг друга.

    Глобальная область видимости

    Переменные, созданные в основном теле программы (вне любых функций), называются глобальными. Они видны и доступны из любой точки программы, включая внутренности функций.

    !Схема области видимости переменных

    Проблема затенения (Shadowing)

    Что произойдет, если мы создадим локальную переменную с таким же именем, как у глобальной?

    Результат работы этого кода будет таким: Внутри функции статус: online Снаружи функции статус: offline

    Внутри функции login мы не изменили глобальную переменную. Мы создали ее локального «клона». Локальная переменная перекрыла (затенила) собой глобальную на время работы функции. Как только функция завершилась, клон исчез, и мы снова увидели оригинальную глобальную переменную со значением "offline".

    > Золотое правило архитектуры: Старайтесь избегать использования глобальных переменных для хранения изменяемых данных. Передавайте данные в функции через аргументы и получайте результат через return. Глобальные переменные делают код непредсказуемым: любая функция может незаметно изменить их, и найти причину ошибки будет невероятно сложно.

    Декомпозиция: Искусство разделять и властвовать

    Теперь, когда мы понимаем механику функций, давайте поговорим о том, как правильно их применять.

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

    Почему декомпозиция критически важна?

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

    Декомпозиция решает сразу несколько проблем:

  • Снижение когнитивной нагрузки: Человеческий мозг может одновременно удерживать в рабочей памяти 5–7 объектов. Разбив задачу на части, вы можете сфокусироваться на решении одной маленькой проблемы за раз.
  • Командная работа: Если задача разделена на независимые функции, разные программисты могут писать их параллельно.
  • Тестирование: Гораздо проще проверить работу маленькой функции, которая делает только математический расчет, чем тестировать огромный блок кода, который одновременно скачивает данные, считает их и рисует график.
  • Принцип единой ответственности (Separation of Concerns)

    Главный критерий хорошей декомпозиции — внутренняя связность (cohesion) и слабое зацепление (coupling).

    Простыми словами: каждая функция должна делать только одну вещь, но делать ее хорошо. Она не должна лезть в дела других функций.

    Рассмотрим плохой пример. Нам нужно зарегистрировать пользователя:

    Эта функция нарушает принцип единой ответственности. Если завтра мы захотим изменить текст приветственного письма, нам придется лезть в логику регистрации. Если мы захотим использовать проверку пароля при смене пароля (а не только при регистрации), мы не сможем этого сделать, потому что проверка намертво вшита в регистрацию.

    Сделаем правильную декомпозицию:

    Теперь наш код читается как обычный текст на английском языке. Каждая функция независима. Мы можем легко взять is_password_valid и использовать ее в любой другой части программы.

    Как это работает в ИИ-ассистенте?

    Давайте посмотрим, как функции и декомпозиция применяются при создании реального проекта — вашего будущего ИИ-ассистента.

    Глобальная задача: «Сделать так, чтобы ИИ отвечал на голосовые запросы». Это слишком сложно. Применяем декомпозицию сверху вниз:

    Уровень 1: Главный цикл (Оркестратор) Программа постоянно слушает пользователя, понимает его и отвечает.

    Уровень 2: Разделение на крупные модули (Функции)

  • record_audio() — включает микрофон и записывает звук.
  • speech_to_text(audio) — отправляет звук в нейросеть и возвращает текст.
  • analyze_intent(text) — определяет, чего хочет пользователь (узнать погоду, поставить будильник, просто поболтать).
  • execute_command(intent) — выполняет нужное действие.
  • text_to_speech(response) — озвучивает результат.
  • Уровень 3: Детализация модулей Возьмем функцию execute_command(intent). Внутри нее будет своя декомпозиция. Если намерение — узнать погоду, она вызовет функцию get_weather(city).

    Функция get_weather(city) в свою очередь разобьется на:

  • build_api_url(city)
  • fetch_data_from_server(url)
  • parse_temperature(data)
  • Благодаря такому подходу, если завтра сломается сервис погоды, вам не придется переписывать весь искусственный интеллект. Вы точно будете знать, что проблема находится в маленькой, изолированной функции fetch_data_from_server, и исправите только ее.

    Освоив функции, области видимости и принципы декомпозиции, вы перешли от написания простых скриптов к проектированию архитектуры программного обеспечения. Вы научились создавать переиспользуемые инструменты и управлять сложностью кода. В следующей статье мы сделаем еще один шаг к профессиональной разработке и изучим структуры данных — способы эффективно хранить и организовывать огромные массивы информации, с которыми предстоит работать нашему ИИ.

    4. Базовые структуры данных: массивы, списки и словари

    Базовые структуры данных: массивы, списки и словари

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

    Представьте, что вы пишете программу для школы. Сохранить имя одного ученика легко: student_name = "Иван". Но что, если в школе 1000 учеников? Создавать тысячу переменных от student_1 до student_1000 — это путь в никуда. Такой код невозможно читать, поддерживать и использовать в циклах.

    Здесь на сцену выходят структуры данных — специализированные форматы для организации, хранения и обработки информации в компьютере. Если переменная — это один ящик стола, то структуры данных — это картотеки, стеллажи и автоматизированные склады.

    В этой статье мы разберем четыре фундаментальные структуры данных, которые используются в 99% программ: массивы (списки), кортежи, словари и множества. Мы поймем, как они работают «под капотом», и научимся выбирать правильный инструмент для каждой задачи.

    Массивы и Списки: Порядок превыше всего

    Массив (Array) — это структура данных, которая хранит набор элементов одного типа в строго определенном порядке.

    Самая точная аналогия из реальной жизни — это таблетница на неделю или многоквартирный дом. В многоквартирном доме квартиры расположены последовательно, и у каждой есть свой уникальный номер. Чтобы найти нужную квартиру, вам не нужно стучаться в каждую дверь — достаточно знать ее номер.

    Как массивы работают в памяти компьютера

    Чтобы понять, почему массивы работают именно так, нужно заглянуть внутрь оперативной памяти (RAM). Представьте память как бесконечную ленту из ячеек. Когда вы создаете классический массив из 5 целых чисел, компьютер находит на этой ленте 5 свободных ячеек, идущих строго подряд, и бронирует их.

    Благодаря тому, что ячейки идут подряд, компьютер может мгновенно найти любой элемент с помощью простой математической формулы:

    Где: * — искомый адрес в памяти. * — адрес начала массива (первой ячейки). * — порядковый номер элемента. * — размер одного элемента в байтах.

    Именно из этой формулы вытекает главное правило программирования, которое так часто сбивает с толку новичков: индексация начинается с нуля.

    > Если нам нужен самый первый элемент массива, его смещение относительно начала равно нулю. Подставив в формулу, мы получим . Компьютер сразу обращается к началу массива.

    Списки (Lists) в современных языках

    Классические массивы (как в языках C или Java) имеют жесткое ограничение: их размер нужно указывать заранее. Если вы создали массив на 10 элементов, вы не сможете положить туда 11-й.

    В современных высокоуровневых языках, таких как Python или JavaScript, чаще используются динамические массивы, которые в Python называются списками (Lists). Они умеют автоматически расширяться, когда в них заканчивается место, и могут хранить данные разных типов одновременно.

    Давайте посмотрим, как работать со списками на практике:

    Главная опасность списков: IndexError

    Самая частая ошибка при работе со списками — попытка обратиться к элементу, которого не существует. Если в списке 4 элемента, их индексы: 0, 1, 2 и 3.

    Если вы напишете print(shopping_list[4]), программа немедленно завершится с ошибкой IndexError: list index out of range (Индекс списка выходит за пределы допустимого диапазона). Компьютер буквально говорит вам: «Я попытался вычислить адрес памяти по твоей формуле, но там чужие данные, я туда не пойду».

    Кортежи (Tuples): Неизменяемые списки

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

    В коде кортежи обычно обозначаются круглыми скобками:

    Зачем нужна структура данных, которую нельзя менять?

  • Защита от ошибок (Безопасность): Если вы передаете важные данные (например, настройки подключения к базе данных или координаты) в чужую функцию, вы хотите быть уверены, что эта функция случайно их не перезапишет.
  • Производительность: Поскольку размер кортежа фиксирован навсегда, компьютер оптимизирует его в памяти. Кортежи работают немного быстрее списков и занимают меньше места.
  • Словари (Dictionaries): Поиск по смыслу

    Списки отлично подходят, когда важен порядок (например, очередь задач или история сообщений). Но что, если нам нужно найти информацию по какому-то признаку?

    Представьте, что у вас есть список из миллиона пользователей. Вам нужно найти возраст пользователя с логином "alex_99". Если использовать список, компьютеру придется проверять каждого пользователя по очереди (от нулевого до миллионного), пока он не найдет нужного. Это невероятно медленно.

    Для таких задач используются словари (Dictionaries, в других языках — Hash Maps или Ассоциативные массивы).

    Словарь — это неупорядоченная коллекция, которая хранит данные в виде пар «Ключ: Значение» (Key-Value).

    Аналогия — обычный толковый словарь или телефонная книга. Вы не читаете словарь с первой страницы, чтобы найти слово «Программирование». Вы используете само слово (Ключ), чтобы мгновенно найти его определение (Значение).

    !Визуальное сравнение структур данных: массив (слева) и словарь (справа).

    Как словари работают «под капотом»: Магия хеширования

    Словари обеспечивают мгновенный поиск благодаря процессу, который называется хеширование.

    Когда вы добавляете в словарь ключ (например, строку "alex_99"), компьютер пропускает этот текст через специальную математическую функцию — хеш-функцию. Эта функция превращает любой текст в уникальное число. Это число и становится скрытым индексом в памяти компьютера.

    Поэтому, когда вы запрашиваете данные по ключу "alex_99", компьютер снова вычисляет хеш, мгновенно получает точный адрес в памяти и забирает значение. Ему не нужно перебирать другие элементы. Поиск среди 10 элементов и среди 10 миллионов элементов займет одинаковое время!

    Практика работы со словарями

    В Python словари создаются с помощью фигурных скобок {}:

    Главное правило ключей

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

    Почему? Потому что если вы используете список в качестве ключа, а потом измените этот список, его хеш (математическое число) изменится. Компьютер навсегда «потеряет» дорогу к значению, которое было привязано к этому ключу.

    Если вы попытаетесь обратиться к ключу, которого нет в словаре (например, user_profile["phone"]), вы получите ошибку KeyError. Чтобы избежать этого, профессионалы используют безопасные методы получения данных, например метод .get(), который возвращает пустоту (None), если ключа нет, вместо того чтобы обрушить программу.

    Множества (Sets): Математическая уникальность

    Четвертая базовая структура — множество (Set). Это неупорядоченная коллекция, в которой каждый элемент уникален.

    Представьте мешок, в который вы складываете шарики с номерами. Если вы попытаетесь положить в мешок шарик с номером 5, а там уже есть пятерка, вторая просто растворится в воздухе. В множестве не может быть дубликатов.

    Множества невероятно полезны для двух задач:

  • Быстрое удаление дубликатов из любых данных.
  • Проверка вхождения. Проверить, есть ли элемент в множестве, компьютер может так же мгновенно, как и в словаре (множества тоже используют хеширование под капотом).
  • Архитектура данных: Собираем всё вместе для ИИ-ассистента

    В реальных проектах структуры данных редко используются поодиночке. Вся мощь программирования раскрывается, когда мы начинаем вкладывать их друг в друга, создавая сложные деревья информации.

    Давайте посмотрим, как может выглядеть внутренняя память (состояние) нашего будущего ИИ-ассистента. Нам нужно хранить информацию о пользователе, его настройках и историю диалога.

    Разберем этот пример:

  • Главная структура ai_state — это словарь. Он позволяет нам быстро обращаться к разным блокам памяти по именам (ключам).
  • Внутри ключа user лежит еще один словарь. Это логично, так как у пользователя есть разные атрибуты.
  • Настройки сервера server_config — это кортеж. Мы не хотим, чтобы ИИ случайно изменил IP-адрес во время работы.
  • История чата chat_history — это список. Здесь критически важен хронологический порядок. Мы должны знать, что было сказано сначала, а что потом. А каждый элемент этого списка — словарь, описывающий конкретное сообщение.
  • Если нам нужно получить текст последнего сообщения от пользователя, мы напишем такой код:

    Понимание того, как комбинировать списки и словари — это 80% успеха при работе с данными в современном вебе. Именно в таком формате (он называется JSON) общаются между собой серверы, мобильные приложения и нейросети.

    Освоив структуры данных, вы научились не просто писать команды, но и правильно организовывать информацию. Вы перешли от работы с отдельными кирпичами к проектированию целых зданий. В следующей статье мы объединим наши знания о функциях и структурах данных, чтобы изучить алгоритмы — пошаговые рецепты решения сложных вычислительных задач, таких как сортировка и поиск.

    5. Основы алгоритмов и оценка вычислительной сложности

    Основы алгоритмов и оценка вычислительной сложности

    Данные собраны, разложены по полочкам списков и упакованы в словари. В предыдущих статьях мы заложили фундамент: научились хранить информацию и управлять базовым потоком программы. Но сами по себе данные мертвы. Их нужно искать, фильтровать, сортировать и преобразовывать в полезный результат.

    Представьте, что вы создали телефонную книгу на миллион контактов. Сохранить их в массив — это только половина дела. Если пользователь захочет найти номер мамы, а ваша программа будет искать его десять минут, зависая и нагревая телефон, пользователь удалит ваше приложение.

    Разница между «программой, которая просто работает» и «программой, которой приятно пользоваться», кроется в алгоритмах. Умение писать эффективные алгоритмы — это то, что отличает профессионального инженера от начинающего кодера.

    Что такое алгоритм?

    Алгоритм — это конечная последовательность точных шагов, приводящая к решению конкретной задачи.

    Рецепт приготовления яичницы, инструкция по сборке шкафа из IKEA, маршрут навигатора от дома до работы — всё это алгоритмы из реальной жизни. В программировании алгоритм берет входные данные (например, неотсортированный список чисел), выполняет над ними математические или логические операции и выдает результат (отсортированный список).

    Хороший алгоритм должен обладать тремя свойствами:

  • Конечность: он должен когда-нибудь завершиться, а не уйти в бесконечный цикл.
  • Детерминированность: при одних и тех же входных данных он всегда должен выдавать один и тот же результат.
  • Эффективность: он должен решать задачу за разумное время и не потреблять всю доступную память компьютера.
  • Именно третье свойство — эффективность — становится главным камнем преткновения по мере роста ваших проектов.

    Иллюзия времени: почему секунды лгут

    Допустим, вы написали алгоритм поиска нужного товара в каталоге интернет-магазина. Вы запускаете его на своем мощном игровом ноутбуке, и он находит товар за секунды. Вы довольны и выгружаете код на сервер.

    Но сервер — это дешевая виртуальная машина, которая параллельно обрабатывает запросы еще тысячи пользователей. На сервере ваш алгоритм выполняется уже секунд. А если запустить его на старом смартфоне пользователя, он займет секунд.

    > Время в секундах — это плохая метрика для оценки кода. Оно зависит от мощности процессора, загруженности оперативной памяти, фоновых процессов и даже температуры в комнате, где стоит сервер.

    Чтобы оценивать алгоритмы объективно, программисты и математики договорились измерять не время, а количество базовых операций, которые алгоритм должен выполнить для достижения результата. Базовой операцией считается простое действие: сравнение двух чисел, присваивание значения переменной или математическое вычисление.

    О-большое (Big O Notation): язык эффективности

    Для описания того, как растет количество операций при увеличении объема данных, используется математическая концепция Big O Notation (О-большое).

    Буква означает Order of the function (порядок функции). Внутри скобок пишется математическое выражение, которое показывает зависимость количества операций от , где — это размер входных данных (например, количество элементов в массиве).

    Big O всегда описывает худший сценарий (Worst-case scenario). Программисты — пессимисты. Мы всегда предполагаем, что нужный нам элемент находится в самом конце списка или вообще отсутствует.

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

    1. Константная сложность:

    Алгоритм работает за , если время его выполнения не зависит от объема данных. Ему нужно одинаковое количество операций и для элементов, и для миллионов.

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

    2. Линейная сложность:

    Алгоритм работает за , если количество операций растет прямо пропорционально количеству данных. Если данных стало в раз больше, алгоритм будет работать в раз дольше.

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

    3. Квадратичная сложность:

    Это опасная зона. При сложности увеличение данных в раз приводит к замедлению работы в раз ().

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

    Если в списке имен, компьютер сделает сравнений. Для современных процессоров это терпимо. Но если имен будет , потребуется миллиардов операций. Программа надолго «зависнет».

    4. Логарифмическая сложность:

    Это «магическая» сложность, к которой стремятся все разработчики. При время выполнения растет крайне медленно, даже если объем данных увеличивается астрономически.

    Что такое логарифм в контексте программирования? Простыми словами: логарифм — это количество раз, которое нужно разделить число пополам, чтобы получить единицу.

    * Если , то делим пополам: . Потребовалось шага. Значит, . * Если , потребуется шагов. * Если , потребуется всего около шагов!

    !Сравнение графиков вычислительной сложности: как растет время выполнения при увеличении объема данных.

    Чтобы понять, как достичь такой невероятной скорости, давайте столкнем лбами два алгоритма поиска.

    Битва алгоритмов: Линейный поиск против Бинарного

    Представьте, что вы играете с другом в игру «Угадай число». Друг загадал число от до . После каждой вашей попытки он отвечает: «Больше», «Меньше» или «Угадал».

    Подход 1: Линейный поиск (Глупый метод)

    Вы начинаете перебирать все числа подряд: — Это ? — Больше. — Это ? — Больше. — Это ?

    Если друг загадал число , вам придется сделать попыток. В терминах программирования это алгоритм со сложностью . Мы просто идем по массиву от начала до конца.

    Подход 2: Бинарный поиск (Умный метод)

    Вы используете логику и делите диапазон пополам: — Это ? — Больше. (Вы мгновенно отбрасываете все числа от до ). — Это ? — Меньше. (Отбрасываем от до ). — Это ?

    При таком подходе, даже если число загадано в диапазоне от до , вы гарантированно угадаете его максимум за попыток. Это и есть алгоритм со сложностью .

    !Интерактивная визуализация: сравнение линейного и бинарного поиска на массиве чисел.

    Как написать бинарный поиск в коде

    У бинарного поиска есть одно критически важное условие: данные должны быть заранее отсортированы. Вы не сможете искать слово в словаре методом деления пополам, если страницы перепутаны случайным образом.

    Давайте напишем код бинарного поиска на Python. Мы будем использовать концепцию «двух указателей» — переменных, которые обозначают границы зоны поиска.

    Разберем магию этого кода:

  • Мы создаем «окно» поиска с помощью переменных left и right.
  • В цикле while мы находим середину этого окна.
  • Сравниваем элемент в середине с тем, что мы ищем (target).
  • Если не угадали, мы сдвигаем одну из границ так, чтобы отсечь ровно половину оставшегося массива.
  • Цикл повторяется, пока мы не найдем элемент или пока границы не схлопнутся (что означает отсутствие элемента).
  • Компромисс между временем и памятью (Space-Time Tradeoff)

    До сих пор мы говорили только о времени — Time Complexity. Но у алгоритмов есть и вторая характеристика — Space Complexity (пространственная сложность). Она оценивает, сколько дополнительной оперативной памяти требует алгоритм для своей работы.

    Она тоже измеряется с помощью Big O Notation: * по памяти означает, что алгоритм использует фиксированное количество переменных (как наш binary_search, которому нужны только left, right и mid). * по памяти означает, что алгоритму нужно создать копию массива или выделить память, пропорциональную размеру входных данных.

    В программировании существует негласный закон сохранения: вы часто можете ускорить алгоритм, пожертвовав оперативной памятью, и наоборот.

    | Подход | Время | Память | Суть | | :--- | :--- | :--- | :--- | | Экономия памяти | Долго | Мало | Вычисляем данные на лету каждый раз, когда они нужны. Процессор работает на 100%, память свободна. | | Экономия времени | Быстро | Много | Вычисляем данные один раз и сохраняем результат в словарь (кэширование). При следующем запросе отдаем мгновенно за . |

    В современной разработке оперативная память стоит дешево, а время пользователя — бесценно. Поэтому в 90% случаев инженеры выбирают алгоритмы, которые работают быстрее, даже если они «съедают» больше мегабайт.

    Архитектура ИИ-ассистента: Алгоритмы в действии

    Как эти абстрактные концепции помогут нам в создании нашего ИИ-ассистента?

    Представьте, что ваш ИИ должен анализировать историю сообщений пользователя, чтобы понимать контекст беседы. Если история состоит из сообщений, подойдет любой алгоритм. Но что, если пользователь общается с ИИ уже год, и в базе сообщений?

  • Поиск по ключевым словам: Если ИИ нужно найти, когда пользователь в последний раз упоминал слово «билеты», линейный поиск по всем сообщениям заставит ИИ «задуматься» на несколько секунд.
  • Оптимизация через структуры данных: Мы можем создать специальный словарь (индекс), где ключами будут слова, а значениями — списки ID сообщений. Тогда поиск сообщений со словом «билеты» займет времени! Мы пожертвовали памятью (создали огромный словарь-индекс), но получили мгновенный ответ.
  • Сортировка задач: Если ИИ работает как планировщик, ему нужно постоянно держать задачи отсортированными по времени. Добавление новой задачи в отсортированный список с помощью бинарного поиска места для вставки займет , что позволит ИИ работать без задержек.
  • Подводим итоги

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

    Мы разобрали, что такое Big O Notation, почему вложенные циклы — это потенциальная угроза производительности, и как алгоритм «разделяй и властвуй» в виде бинарного поиска способен находить иголку в стоге сена за считанные шаги.

    Теперь, когда мы умеем хранить данные и эффективно ими манипулировать, наши программы становятся слишком сложными для простых функций. В следующей статье мы совершим концептуальный скачок и познакомимся с Объектно-Ориентированным Программированием (ООП) — подходом, который позволит нам моделировать реальный мир в коде и создать архитектуру ИИ-ассистента на профессиональном уровне.

    6. Объектно-ориентированное программирование: классы и объекты

    Объектно-ориентированное программирование: классы и объекты

    В предыдущих статьях мы проделали огромный путь. Мы научились сохранять данные в переменные, управлять потоком программы с помощью циклов и условий, упаковывать логику в функции и даже оценивать эффективность алгоритмов с помощью О-большого.

    Если сравнить программирование со строительством, то сейчас вы умеете делать отличные кирпичи, замешивать крепкий раствор и возводить ровные стены. Но что, если нам нужно построить не сарай, а небоскреб? Или спроектировать умный дом?

    Когда программа разрастается до тысяч строк кода, обычные функции и разрозненные словари перестают справляться. Код превращается в запутанный клубок (программисты называют это «спагетти-кодом»), где изменение одной переменной ломает работу функции на другом конце программы.

    Чтобы управлять сложностью реальных проектов, инженеры придумали Объектно-Ориентированное Программирование (ООП). Это не просто новый синтаксис. Это совершенно иной способ мышления и моделирования реальности в коде.

    Парадигма ООП: от действий к сущностям

    До сих пор мы писали код в процедурном стиле. Мы думали категориями действий: «взять данные», «отсортировать список», «вывести на экран». Данные жили отдельно (в переменных и словарях), а функции, которые их обрабатывали, — отдельно.

    В ООП мы начинаем думать категориями сущностей (объектов). Мы объединяем данные и функции, которые с ними работают, в единые независимые блоки.

    > Объектно-ориентированное программирование — это парадигма разработки, в которой программа рассматривается как набор взаимодействующих друг с другом объектов, каждый из которых обладает собственным состоянием и поведением.

    Давайте сравним два подхода на примере банковского счета.

    | Характеристика | Процедурный подход | Объектно-ориентированный подход | | :--- | :--- | :--- | | Фокус | На функциях (глаголах) | На объектах (существительных) | | Хранение данных | Данные передаются из функции в функцию | Данные хранятся внутри самого объекта | | Пример кода | withdraw_money(account_balance, 100) | my_account.withdraw(100) | | Масштабируемость | Сложно отслеживать, кто изменил данные | Легко контролировать доступ к данным |

    В ООП банковский счет — это самостоятельный объект. Он сам «знает», сколько на нем денег, и сам «умеет» списывать или начислять средства.

    Чтобы создать такой объект, нам понадобятся два фундаментальных понятия: Класс и Объект.

    Классы и Объекты: Чертёж и Дом

    Эти два термина часто путают, но разница между ними принципиальна.

    Класс — это абстрактный шаблон, чертёж или инструкция. Он описывает, какими свойствами будет обладать сущность и что она сможет делать. Сам по себе класс не занимает память под данные и не существует в физическом мире программы как активная единица.

    Объект (экземпляр класса) — это конкретная реализация этого чертежа в оперативной памяти компьютера.

    Представьте завод по производству автомобилей. Инженеры создали чертёж новой модели. На чертеже указано, что у машины должны быть цвет, двигатель и количество дверей, а также она должна уметь заводиться и тормозить. Этот чертёж — Класс.

    Когда конвейер запускается, с него сходят реальные автомобили. Один красный, другой синий. У одного двигатель литра, у другого . Каждый сошедший с конвейера автомобиль — это Объект. Вы не можете сесть в чертёж и поехать на работу. Вы едете на объекте.

    !Схема: Класс как чертёж и Объекты как готовые изделия

    Анатомия Класса: Атрибуты и Методы

    Любой класс состоит из двух главных компонентов:

  • Атрибуты (Состояние) — это переменные, привязанные к классу. Они хранят данные, описывающие объект. Для автомобиля это color, max_speed, is_engine_on.
  • Методы (Поведение) — это функции, привязанные к классу. Они определяют, что объект может делать, и могут изменять его атрибуты. Для автомобиля это start_engine(), brake().
  • Давайте напишем наш первый класс на языке Python. Мы создадим шаблон для пользователя нашего будущего ИИ-ассистента.

    Мы создали чертёж. Теперь давайте создадим конкретные объекты (экземпляры) этого класса.

    Пока что наши пользователи абсолютно одинаковые и скучные. У них нет имен, нет возраста. Чтобы объекты могли хранить уникальные данные, нам нужен специальный механизм инициализации.

    Конструктор и магия слова self

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

    В Python конструктор всегда называется __init__ (от слова initialize — инициализировать). Двойные подчеркивания означают, что это системный, «магический» метод.

    Давайте улучшим наш класс User:

    Теперь создадим объекты с уникальными данными:

    Что такое self и зачем он нужен?

    Слово self (в некоторых языках, например в JavaScript или C++, используется слово this) — это самый большой камень преткновения для новичков.

    > self — это ссылка объекта на самого себя. Это способ объекта сказать: «Возьми моё имя», «Измени мой статус».

    Когда мы вызываем maria.introduce(), под капотом Python делает следующее: он берет класс User, находит там функцию introduce и передает объект maria в качестве первого аргумента.

    То есть вызов maria.introduce() невидимо для вас превращается в User.introduce(maria). Именно поэтому в определении метода def introduce(self): мы обязаны указать self первым параметром. Он принимает тот самый объект, который вызвал метод.

    Если бы мы не использовали self.name, а просто написали name, Python подумал бы, что мы ищем обычную локальную переменную внутри функции, которая исчезнет после завершения работы функции. self гарантирует, что данные сохранятся в «памяти» самого объекта.

    Взаимодействие объектов

    В реальных программах объекты редко существуют в вакууме. Они общаются друг с другом, передают данные и вызывают методы друг друга.

    Представьте, что мы пишем интернет-магазин. У нас будут классы Product (Товар) и Cart (Корзина). Корзина должна уметь принимать в себя товары.

    В этом примере метод add_product принимает не просто строку или число, а целый объект класса Product. Это демонстрирует мощь ООП: мы оперируем крупными, логически завершенными концепциями реального мира.

    Проектирование ИИ-ассистента: Архитектура на классах

    Давайте вернемся к нашей главной цели — созданию ИИ-ассистента. Если бы мы писали его на функциях, у нас были бы десятки разрозненных списков: история сообщений, настройки пользователей, ключи API.

    С помощью ООП мы можем спроектировать красивую и масштабируемую архитектуру. Разделим систему на логические блоки (классы).

    1. Класс Message (Сообщение)

    Каждое сообщение в чате — это не просто текст. У него есть автор (пользователь или ИИ), время отправки и сам текст.

    2. Класс DialogManager (Менеджер диалога)

    Этот объект будет хранить историю конкретной беседы и управлять ею. Если пользователь захочет очистить историю, это сделает менеджер.

    3. Класс AIAssistant (Сам ассистент)

    Это главный класс, который объединяет всё вместе. Он имеет имя, настройки «характера» (системный промпт) и свой собственный менеджер диалогов.

    Посмотрите, как элегантно теперь выглядит запуск нашего приложения:

    Каждый ассистент имеет свою изолированную историю сообщений. Сообщения, отправленные Джарвису, не попадут в память Йоды, потому что у каждого объекта AIAssistant есть свой собственный объект DialogManager. Это и есть магия инкапсуляции состояния, которую дает ООП.

    Частые ошибки новичков в ООП

    Переход от процедурного мышления к объектному дается не сразу. Вот три главные ловушки, в которые попадают начинающие разработчики:

  • Забытый self: Самая частая ошибка в Python. Если вы напишете def my_method(): внутри класса, а потом попытаетесь вызвать obj.my_method(), программа выдаст ошибку. Метод всегда должен принимать self первым аргументом.
  • Божественный объект (God Object): Это антипаттерн (плохая практика), когда программист создает один гигантский класс App или System, который делает вообще всё: и скачивает файлы, и рисует кнопки, и работает с базой данных. Класс должен отвечать за одну конкретную сущность. Разделяйте логику!
  • ООП ради ООП: Не всё в мире должно быть классом. Если вам нужно написать простой скрипт для переименования пяти файлов в папке, обычная функция справится с этим быстрее и понятнее. Используйте ООП там, где есть состояние (данные, которые меняются со временем) и сложное поведение.
  • Подводим итоги

    Объектно-ориентированное программирование позволяет нам моделировать реальный мир. Мы узнали, что: * Класс — это чертёж, описывающий структуру и поведение. * Объект — это конкретный экземпляр, созданный по этому чертежу. * Атрибуты хранят состояние объекта, а Методы определяют его действия. * Конструктор (__init__) настраивает объект при его рождении. * self позволяет объекту обращаться к собственным данным.

    Мы заложили фундамент архитектуры нашего ИИ-ассистента. Но ООП скрывает в себе еще больше возможностей. Что, если мы захотим создать VoiceAssistant (Голосового ассистента), который умеет всё то же самое, что и обычный, но плюс еще синтезирует речь? Неужели придется копировать весь код класса AIAssistant?

    Нет! В следующей статье мы познакомимся с тремя китами продвинутого ООП: Наследованием, Инкапсуляцией и Полиморфизмом. Мы научимся создавать иерархии классов, защищать данные от случайного изменения и писать по-настоящему профессиональный код.

    7. Продвинутые концепции архитектуры и паттерны проектирования

    Продвинутые концепции архитектуры и паттерны проектирования

    В предыдущих материалах мы заложили прочный фундамент: научились мыслить объектами, создавать классы и управлять состоянием программы. Мы поняли, что Объектно-Ориентированное Программирование (ООП) — это способ моделирования реального мира в коде.

    Однако создание одного или двух классов — это лишь начало пути. Когда проект разрастается до десятков тысяч строк кода, а над ним работает целая команда инженеров, простого умения писать классы становится недостаточно. Возникают новые вызовы: как сделать так, чтобы изменение одной части системы не ломало другую? Как избежать дублирования логики? Как правильно организовать взаимодействие между десятками объектов?

    Чтобы ответить на эти вопросы, инженеры десятилетиями вырабатывали стандарты, правила и шаблоны. В этом материале мы совершим переход от написания простого кода к проектированию архитектуры. Мы разберем три кита продвинутого ООП, познакомимся с правилами хорошего тона (SOLID), изучим популярные паттерны проектирования и поймем, как строится архитектура современных ИИ-ассистентов.

    Три кита продвинутого ООП

    Классы и объекты — это базовые инструменты. Настоящая мощь объектно-ориентированного подхода раскрывается через три фундаментальные концепции: инкапсуляцию, наследование и полиморфизм.

    1. Инкапсуляция: защита данных и скрытие сложности

    Инкапсуляция — это механизм объединения данных и методов, работающих с этими данными, в единый объект, а также скрытие деталей внутренней реализации от внешнего мира.

    Представьте себе современную кофемашину. У нее есть сложный внутренний механизм: бойлер для нагрева воды, помпа для создания давления, жернова для помола зерен. Но для вас, как для пользователя, доступен только простой интерфейс — несколько кнопок на панели. Вы нажимаете кнопку «Эспрессо», и машина сама знает, в какой последовательности активировать внутренние узлы.

    Если бы инкапсуляции не было, вам пришлось бы вручную включать нагреватель, следить за температурой и отключать помпу. Одно неверное действие — и машина сломана.

    В программировании инкапсуляция защищает объект от некорректного использования. Мы разделяем интерфейс (то, что доступно другим частям программы) и реализацию (то, что скрыто внутри).

    В этом примере мы не можем напрямую изменить __balance. Мы обязаны использовать метод deposit, который содержит логику проверки (нельзя положить отрицательную сумму). Это гарантирует, что состояние объекта всегда остается корректным.

    2. Наследование: переиспользование и иерархия

    Наследование позволяет создать новый класс на основе уже существующего. Новый класс (дочерний) автоматически получает все атрибуты и методы старого класса (родительского), но может добавлять свои собственные или изменять унаследованные.

    Вернемся к нашему ИИ-ассистенту. Допустим, у нас есть базовый класс AIAssistant, который умеет принимать текст и генерировать текстовый ответ. Теперь мы хотим создать VoiceAssistant — голосового помощника. Нам не нужно писать весь код с нуля!

    Наследование реализует важнейший принцип программирования — DRY (Don't Repeat Yourself — Не повторяйся). Если мы найдем ошибку в логике генерации ответа, мы исправим ее только в одном месте (в родительском классе), и она автоматически исправится во всех дочерних классах.

    3. Полиморфизм: один интерфейс, множество реализаций

    Слово полиморфизм происходит от греческого «множество форм». В программировании это означает способность разных объектов одинаково реагировать на один и тот же вызов метода, даже если внутри они реализованы совершенно по-разному.

    Представьте универсальный пульт дистанционного управления. Кнопка «Включить» работает и для телевизора, и для кондиционера, и для аудиосистемы. Пульту не нужно знать, как именно устроена электроника внутри каждого прибора. Он просто посылает сигнал «Включить», а каждый прибор реагирует на него по-своему.

    Полиморфизм делает систему невероятно гибкой. Мы можем добавлять новые типы ассистентов (например, HologramAssistant), и нам не придется переписывать функцию broadcast_alert. Она продолжит работать с новыми классами, если они поддерживают нужный интерфейс.

    Принципы SOLID: правила хорошего тона

    Даже используя ООП, можно написать запутанный и хрупкий код. Чтобы этого избежать, Роберт Мартин (известный как «Дядя Боб») сформулировал пять принципов объектно-ориентированного проектирования, известных под акронимом SOLID.

    Разберем два самых важных принципа, которые критически необходимы на старте.

    SRP: Принцип единственной ответственности (Single Responsibility Principle)

    > Класс должен иметь только одну причину для изменения.

    Это означает, что каждый класс должен отвечать только за одну конкретную задачу. Если ваш класс AIAssistant одновременно генерирует ответы, сохраняет их в базу данных, форматирует текст для экрана и отправляет email-уведомления — это «Божественный объект» (God Object). Это антипаттерн.

    Если изменится формат базы данных, вам придется менять класс ассистента. Если изменится дизайн экрана — снова менять класс ассистента.

    Правильный подход: Разделить логику. Класс AIAssistant только думает. Класс DatabaseManager только сохраняет. Класс UIFormatter только рисует интерфейс.

    OCP: Принцип открытости/закрытости (Open/Closed Principle)

    > Программные сущности должны быть открыты для расширения, но закрыты для модификации.

    Вы должны иметь возможность добавлять новый функционал, не переписывая существующий (и уже протестированный) код.

    Если в вашем коде есть огромная конструкция if-elif-else, которая проверяет тип пользователя (обычный, премиум, админ) для расчета скидки, то при добавлении статуса «VIP» вам придется вскрывать и менять эту функцию. По принципу OCP, лучше использовать полиморфизм: создать базовый класс User с методом get_discount(), и переопределить этот метод в дочерних классах PremiumUser и AdminUser. Тогда добавление VIPUser потребует просто создания нового класса, старый код останется нетронутым.

    Паттерны проектирования: проверенные решения

    Когда инженеры по всему миру начали применять ООП, они заметили, что сталкиваются с одними и теми же архитектурными проблемами. Чтобы не изобретать велосипед каждый раз, были собраны паттерны (шаблоны) проектирования — типичные способы решения часто встречающихся задач.

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

    Без использования паттернов сложность системы растет экспоненциально. Если у вас есть компонентов, которые хаотично общаются друг с другом, количество связей между ними можно описать формулой . При 10 компонентах это 45 связей, при 100 — почти 5000! Паттерны упорядочивают этот хаос.

    Классически паттерны делятся на три группы:

    | Тип паттерна | За что отвечает | Примеры | | :--- | :--- | :--- | | Порождающие | Безопасное и гибкое создание объектов | Singleton (Одиночка), Factory (Фабрика), Builder (Строитель) | | Структурные | Организация связей и построение иерархий | Facade (Фасад), Adapter (Адаптер), Decorator (Декоратор) | | Поведенческие | Эффективная коммуникация между объектами | Observer (Наблюдатель), Strategy (Стратегия), Command (Команда) |

    Давайте рассмотрим два популярных паттерна на примерах.

    Паттерн Singleton (Одиночка)

    Проблема: В системе есть объект, который должен существовать строго в единственном экземпляре. Например, подключение к базе данных. Если каждый класс будет создавать свое подключение, база данных не выдержит нагрузки и упадет.

    Решение: Паттерн Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Если объект уже создан, класс возвращает существующий, а не создает новый.

    Паттерн Observer (Наблюдатель)

    Проблема: У вас есть объект (например, менеджер сообщений чата), и несколько других объектов (счетчик непрочитанных, окно уведомлений, звуковой плеер), которые должны реагировать на появление нового сообщения. Жестко прописывать вызов каждого из них внутри менеджера — нарушение принципа SRP.

    Решение: Менеджер сообщений становится «Издателем». Остальные объекты — «Подписчиками» (Наблюдателями). Когда приходит сообщение, Издатель просто кричит: «У меня обновление!», и все подписанные объекты сами решают, что им делать (один проигрывает звук, другой меняет цифру на иконке).

    Архитектура программного обеспечения: взгляд сверху

    Если классы — это кирпичи, а паттерны — это типовые комнаты, то архитектура — это генеральный план всего небоскреба. Архитектура определяет, как крупные модули программы (база данных, серверная логика, пользовательский интерфейс) взаимодействуют друг с другом.

    Самый известный архитектурный паттерн, который используется в веб-разработке и создании приложений — это MVC (Model-View-Controller).

    !Схема архитектуры MVC (Model-View-Controller)

    Архитектура MVC разделяет приложение на три независимых слоя:

  • Model (Модель): Отвечает за данные и бизнес-логику. Модель знает, как сохранить пользователя в базу данных, как рассчитать налог или как обновить историю диалога. Она ничего не знает о том, как эти данные выглядят на экране.
  • View (Представление): Отвечает за визуализацию. Это кнопки, текст, графики, HTML-страницы. Представление берет данные из Модели и красиво их отрисовывает, но само не принимает никаких решений.
  • Controller (Контроллер): Связующее звено. Он принимает действия от пользователя (нажатие кнопки, ввод текста), обрабатывает их, дает команду Модели обновить данные, а затем говорит Представлению обновить экран.
  • Аналогия из жизни: Ресторан. * Модель — это повара на кухне. Они работают с ингредиентами (данными) и готовят блюда (результат). * Представление — это красивая подача блюда на тарелке и интерьер зала. * Контроллер — это официант. Он принимает ваш заказ (ввод пользователя), передает его на кухню (Модели), а затем выносит готовое блюдо в зал (Представлению).

    Такое разделение позволяет frontend-разработчикам (создателям интерфейсов) и backend-разработчикам (создателям серверной логики) работать параллельно, не мешая друг другу.

    Архитектура ИИ-ассистентов: Workflow против Agent

    Теперь применим наши знания к созданию ИИ-ассистента. В современной разработке систем на базе больших языковых моделей (LLM) выделяют два принципиально разных архитектурных подхода: Workflow (Конвейер) и Agent (Агент).

    Подход Workflow (Конвейер)

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

    Пример конвейера для обработки email-сообщения:

  • Шаг 1 (Код): Получить письмо.
  • Шаг 2 (LLM): Проанализировать тональность письма (позитивное/негативное).
  • Шаг 3 (Код): Если негативное — переслать менеджеру. Если позитивное — идти дальше.
  • Шаг 4 (LLM): Сгенерировать шаблон ответа с благодарностью.
  • Шаг 5 (Код): Отправить ответ.
  • В этой архитектуре контроль потока (Control Flow) находится в руках нашего кода. Мы точно знаем, что произойдет. Это дешево, быстро и легко тестируется.

    Подход Agent (Агент)

    Агентская архитектура — это совершенно иной уровень абстракции. Здесь мы передаем контроль потока самой нейросети. Мы не пишем жесткий алгоритм. Мы даем ИИ Цель и набор Инструментов (Tools), например: доступ к поисковику, калькулятор, доступ к базе данных.

    Агент работает в цикле, который называется ReAct (Reasoning and Acting — Рассуждение и Действие):

  • Мысль (Thought): ИИ анализирует цель. «Пользователь просит забронировать отель в Париже на завтра. Сначала мне нужно узнать дату на завтра».
  • Действие (Action): ИИ решает использовать инструмент get_current_date().
  • Наблюдение (Observation): ИИ получает результат от инструмента (например, 15 мая).
  • Мысль 2: «Отлично, теперь мне нужно найти отели на 16 мая через инструмент поиска».
  • Действие 2: Вызов инструмента search_hotels(location="Paris", date="16 May").
  • Этот цикл продолжается до тех пор, пока Агент не решит, что собрал достаточно информации для финального ответа.

    Агентская архитектура невероятно гибкая. Агент может сам исправить свою ошибку (если поиск не дал результатов, он переформулирует запрос). Но за эту гибкость мы платим высокую цену: агенты потребляют много токенов (денег), работают медленнее и могут «зациклиться», если не справятся с логикой.

    Финальные мысли

    Путь от переменных и циклов до архитектуры ИИ-агентов — это путь от тактики к стратегии.

    Мы изучили, как инкапсуляция защищает наши объекты, как наследование избавляет от дублирования, а полиморфизм делает системы гибкими. Мы поняли, что принципы SOLID и паттерны проектирования — это не просто скучная теория, а выстраданные индустрией правила выживания в сложных проектах. Наконец, мы увидели, как архитектурные концепции, такие как MVC и Agent-based системы, позволяют строить масштабируемые и умные приложения.

    В следующих материалах мы перейдем к практической реализации этих концепций: научимся работать с сетью, подключать базы данных и интегрировать реальные API нейросетей в наш код, чтобы наш ИИ-ассистент наконец-то обрел голос и разум.

    8. Работа с файловой системой и форматами данных

    Сохранение состояния: Работа с файловой системой и форматами данных

    В предыдущих материалах мы научились создавать сложные архитектуры с помощью классов, объектов и паттернов проектирования. Наш ИИ-ассистент обрел структуру: у него есть менеджер диалогов, анализатор текста и генератор ответов. Однако у текущей версии программы есть один фатальный недостаток — амнезия.

    Как только выполнение скрипта завершается или компьютер перезагружается, все объекты уничтожаются сборщиком мусора, а оперативная память очищается. Вся история переписки, настройки пользователя и накопленный контекст исчезают навсегда.

    Чтобы программа могла «помнить» информацию между запусками, данные необходимо перенести из энергозависимой оперативной памяти (RAM) на постоянный носитель (жесткий диск или SSD). В этом материале мы разберем, как программы взаимодействуют с операционной системой для сохранения данных, изучим универсальные форматы обмена информацией и научим нашего ИИ-ассистента вести постоянный журнал диалогов.

    Анатомия файловой системы

    Файловая система (ФС) — это подсистема операционной системы, которая отвечает за организацию, хранение и именование данных на физических носителях.

    Если жесткий диск — это огромный пустой склад, то файловая система — это набор стеллажей, коробок и подробный каталог, указывающий, где именно лежит каждая вещь. Без файловой системы данные представляли бы собой сплошную нечитаемую кашу из нулей и единиц.

    На логическом уровне (то, как это видит программист и пользователь) файловая система представляет собой перевернутое дерево.

    !Схема файловой системы и путей

    В самом верху находится корневой каталог (root). В системах на базе Linux и macOS он обозначается одним слешем /. В Windows корнем обычно является логический диск, например C:\.

    От корня отходят ветви — директории (папки), внутри которых могут находиться другие директории или конечные листья — файлы.

    Абсолютные и относительные пути

    Чтобы программа могла найти нужный файл, ей необходимо передать точный адрес — путь (path). Понимание разницы между абсолютными и относительными путями — один из главных камней преткновения для начинающих разработчиков.

    Абсолютный путь — это полный адрес файла, начиная от самого корня файловой системы. Он уникален и всегда указывает на одно и то же место, независимо от того, откуда запущена ваша программа.

    Пример абсолютного пути (macOS/Linux): /Users/developer/projects/ai_assistant/config.json

    Пример абсолютного пути (Windows): C:\Users\developer\projects\ai_assistant\config.json

    Относительный путь — это маршрут к файлу, который строится относительно текущего рабочего каталога (Current Working Directory, CWD). Рабочий каталог — это папка, из которой был запущен ваш скрипт.

    Для навигации по относительным путям используются два специальных символа: * . (одна точка) — означает «текущая директория». * .. (две точки) — означает «подняться на один уровень вверх» (в родительскую директорию).

    Представьте, что вы находитесь в торговом центре. Абсолютный путь — это GPS-координаты магазина. Относительный путь — это инструкция: «выйди из текущего павильона, поверни направо и поднимись на второй этаж».

    Если ваш скрипт запущен из папки ai_assistant, то относительный путь к файлу конфигурации будет выглядеть просто: ./config.json (или просто config.json).

    Если же файл лежит в папке settings, которая находится на одном уровне с вашей текущей папкой, путь будет таким: ../settings/config.json (подняться наверх, зайти в settings, взять файл).

    > Использование относительных путей делает ваш код переносимым. Если вы отправите проект с абсолютными путями (например, C:\Users\Ivan\...) другому разработчику, код у него сломается, так как на его компьютере нет пользователя Ivan. Относительные пути гарантируют, что проект будет работать на любой машине. > > Документация Python по работе с путями

    Базовые операции ввода-вывода (I/O)

    Взаимодействие с файлами в любой операционной системе сводится к трем базовым шагам:

  • Открыть файл (получить к нему доступ).
  • Прочитать данные или записать новые.
  • Закрыть файл (освободить ресурсы).
  • В Python для открытия файлов используется встроенная функция open(). Она обращается к ядру операционной системы с системным вызовом и возвращает файловый дескриптор — специальный объект, через который программа может управлять файлом.

    Режимы открытия файлов

    Функция open() принимает два основных аргумента: путь к файлу и режим работы (mode). Режим сообщает операционной системе, что именно вы собираетесь делать, чтобы ОС могла правильно распределить права доступа.

    Основные режимы: r (read*) — чтение. Файл должен существовать, иначе программа выдаст ошибку. Это режим по умолчанию. w (write*) — запись. Осторожно! Если файл уже существует, этот режим полностью сотрет его содержимое перед записью. Если файла нет — он будет создан. a (append*) — добавление. Данные будут записываться в самый конец файла, не удаляя старое содержимое. Идеально для логов и истории сообщений. b (binary*) — бинарный режим. Добавляется к основным (например, rb или wb) для работы с нетекстовыми данными (картинки, аудио, скомпилированный код).

    Проблема утечки ресурсов и контекстные менеджеры

    Рассмотрим классический, но устаревший способ работы с файлом:

    Почему этот код опасен? Если между открытием файла и его закрытием произойдет ошибка (например, закончится место на диске или программа попытается разделить на ноль), выполнение скрипта прервется, и строка file.close() никогда не выполнится.

    Незакрытый файл остается заблокированным операционной системой. Другие программы не смогут его изменить или удалить. Кроме того, данные могут не сохраниться на диск, так как ОС часто кэширует запись в оперативной памяти для ускорения работы, и физическая запись происходит именно в момент вызова close().

    Чтобы решить эту проблему элегантно, в Python существуют контекстные менеджеры — конструкция with.

    Конструкция with гарантирует, что файл будет закрыт при выходе из блока кода в любом случае — даже если внутри произойдет критическая ошибка и программа упадет. Под капотом контекстный менеджер использует магические методы ООП __enter__ и __exit__, о которых мы говорили в предыдущих статьях.

    Кодировки: почему текст превращается в иероглифы

    Обратите внимание на параметр encoding="utf-8" в примере выше. Компьютер не понимает букв, он понимает только числа. Кодировка — это таблица соответствия, которая говорит компьютеру, какое число соответствует какой букве.

    Исторически операционные системы использовали разные кодировки (например, Windows-1251 для русского языка в Windows). Если вы запишете файл в одной кодировке, а прочитаете в другой, вы получите кракозябры (Mojibake) — бессмысленный набор символов.

    Сегодня мировым стандартом является UTF-8 — универсальная кодировка, поддерживающая практически все языки мира, включая эмодзи. Всегда явно указывайте encoding="utf-8" при работе с текстовыми файлами, чтобы ваш ИИ-ассистент корректно сохранял и читал сообщения на любом языке.

    Форматы данных: от простого текста к структурированному JSON

    Мы научились сохранять строки текста. Но что, если нам нужно сохранить сложный объект? Например, профиль пользователя с его именем, возрастом, списком интересов и историей запросов.

    Если мы просто запишем это в текстовый файл построчно, нам придется писать сложный код для обратного чтения: понимать, на какой строке находится возраст, а где начинается список интересов.

    Для решения этой задачи были придуманы стандартизированные форматы сериализации данных. Сериализация — это процесс перевода сложной структуры данных (например, объектов или словарей в памяти) в плоскую строку байтов для сохранения или передачи по сети.

    Формат CSV (Comma-Separated Values)

    CSV — это текстовый формат для хранения табличных данных. Каждая строка файла — это строка таблицы, а колонки разделены запятыми (или точками с запятой).

    CSV отлично подходит для выгрузки статистики, логов или датасетов для машинного обучения. Однако он плохо справляется с вложенными данными (например, если у сообщения есть массив прикрепленных файлов).

    Формат JSON (JavaScript Object Notation)

    JSON — это абсолютный король современного программирования. Изначально созданный для языка JavaScript, он стал универсальным стандартом обмена данными между серверами, браузерами, мобильными приложениями и базами данных.

    JSON идеально ложится на базовые структуры данных, которые мы изучали ранее: словари (объекты) и списки (массивы).

    В Python для работы с этим форматом есть встроенный модуль json. Он предоставляет две главные функции: * json.dumps() (dump string) — превращает Python-словарь в строку JSON (сериализация). * json.loads() (load string) — превращает строку JSON обратно в Python-словарь (десериализация).

    Для работы напрямую с файлами используются функции json.dump() и json.load() (без буквы 's' на конце).

    Пример сохранения настроек ИИ-ассистента:

    Продвинутая работа с путями: Объектно-ориентированный подход

    Долгое время в Python для работы с путями использовался модуль os.path. Пути представляли собой обычные строки, и их приходилось склеивать специальными функциями:

    Этот подход работал, но противоречил принципам ООП. Строка — это просто текст, у нее нет методов для проверки существования файла или создания папки.

    В современных версиях Python появился модуль pathlib, который превращает пути в полноценные объекты.

    Использование pathlib делает код чище, безопаснее и полностью независимым от операционной системы (модуль сам подставит правильные слеши для Windows или Linux).

    Практика: Модуль памяти для ИИ-ассистента

    Давайте объединим все изученные концепции (ООП, контекстные менеджеры, JSON и pathlib) и напишем класс, который будет отвечать за долговременную память нашего ИИ-ассистента.

    Этот класс инкапсулирует всю сложную логику работы с файловой системой. Остальной код ИИ-ассистента может просто вызывать метод add_message, не задумываясь о том, как именно открываются файлы, где они лежат и в каком формате сохраняются.

    Итоги архитектурного этапа

    Понимание работы с файловой системой — это мост между абстрактным кодом в оперативной памяти и реальным миром устойчивых данных. Мы изучили, как операционная система управляет доступом к файлам, почему важно использовать контекстные менеджеры для предотвращения утечек ресурсов, и как форматы вроде JSON позволяют сохранять сложные объекты.

    Теперь наш ИИ-ассистент обладает долговременной памятью. В следующих материалах мы выйдем за пределы одного компьютера: научимся работать с сетью, отправлять HTTP-запросы и интегрировать нашего ассистента с реальными API мощных нейросетей.

    9. Основы веб-технологий: структура и верстка страниц

    От терминала к браузеру: Как устроен современный веб

    До этого момента наш ИИ-ассистент жил исключительно в черном окне терминала. Он научился запоминать контекст диалога, сохранять данные в файлы формата JSON и использовать объектно-ориентированную архитектуру. Однако обычные пользователи не запускают скрипты через командную строку. Они открывают браузер.

    Чтобы сделать программу доступной миллионам людей, ей нужен графический интерфейс. В современном мире самым универсальным способом создания интерфейсов стали веб-технологии. Понимание того, как строятся веб-страницы, необходимо не только для создания сайтов, но и для написания парсеров (программ, собирающих данные из интернета), с которыми в будущем будет работать наш ИИ.

    Клиент-серверная архитектура: Диалог двух компьютеров

    Прежде чем писать код интерфейса, необходимо понять, как данные попадают на экран пользователя. Весь интернет построен на базовой концепции взаимодействия двух сторон: клиента и сервера.

    Клиент — это программа, которая запрашивает информацию. В повседневной жизни клиентом выступает ваш веб-браузер (Chrome, Safari) или мобильное приложение.

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

    > Представьте поход в ресторан. Вы (клиент) изучаете меню и делаете заказ. Официант (интернет-протокол HTTP) передает ваш заказ на кухню. Повар (сервер) готовит блюдо по рецепту (выполняет серверный код) и через официанта возвращает вам готовую еду (веб-страницу).

    Когда вы вводите адрес сайта, сервер отправляет вашему браузеру набор текстовых файлов. Браузер читает эти файлы и превращает их в красивую картинку с кнопками и анимациями. Главные из этих файлов написаны на языках HTML и CSS.

    HTML: Каркас и фундамент страницы

    HTML (HyperText Markup Language, язык гипертекстовой разметки) — это не язык программирования. В нем нет переменных, циклов или логических ветвлений. Это язык разметки, который объясняет браузеру, где находится заголовок, где абзац текста, а где картинка.

    Если сравнить веб-страницу со зданием, то HTML — это бетонный фундамент, несущие стены и перекрытия.

    Анатомия HTML-тегов

    Основа HTML — это теги. Тег работает как контейнер, который оборачивает контент и придает ему смысл. Большинство тегов парные: они имеют открывающую и закрывающую часть.

    Синтаксис выглядит так: открывающий_тег контент закрывающий_тег

    Например, чтобы создать абзац текста, используется тег p (от слова paragraph):

    Теги могут иметь атрибуты — дополнительные настройки, которые пишутся внутри открывающего тега. Например, тегу ссылки a (anchor) нужен атрибут href, чтобы знать, куда именно вести пользователя:

    Дерево документа и вложенность

    HTML-документ имеет строгую иерархию, напоминающую матрешку. Один тег может находиться внутри другого. Это называется вложенностью.

    Базовая структура любой веб-страницы выглядит так:

    Разберем ключевые элементы: * DOCTYPE сообщает браузеру, что используется современный стандарт HTML5. * html — корневой элемент, оборачивающий всю страницу. * head — технический раздел (мозг страницы). Здесь лежат настройки, название вкладки и ссылки на стили. Пользователь не видит содержимое этого блока. * body — тело страницы. Все, что находится здесь (текст, кнопки, изображения), отображается на экране.

    !Анатомия веб-страницы: от каркаса к дизайну

    Семантическая разметка: Почему это критично для ИИ

    На заре интернета разработчики строили сайты, используя универсальные блоки (теги div), которые не несли никакого смысла. Страница превращалась в бесконечную простыню одинаковых контейнеров.

    Современный стандарт требует использования семантических тегов — элементов, название которых говорит об их предназначении: * header — шапка сайта (логотип, навигация). * main — главное уникальное содержимое страницы. * article — самостоятельная статья или пост. * footer — подвал сайта (копирайты, контакты).

    Зачем это нужно? Во-первых, для поисковых систем (SEO). Во-вторых, для программ экранного доступа, которые читают сайты незрячим людям.

    В-третьих, это критически важно для нашего ИИ-ассистента. Когда мы научим его собирать информацию из интернета, ему будет гораздо проще найти нужный текст внутри тега article, чем пытаться угадать, в каком из сотен безымянных div спрятана суть статьи.

    CSS: Дизайн, цвета и макет

    Если HTML — это голые бетонные стены, то CSS (Cascading Style Sheets, каскадные таблицы стилей) — это обои, краска, мебель и освещение. CSS отвечает за то, как элементы выглядят: их цвет, размер, отступы и расположение на экране.

    Как работает CSS

    CSS состоит из селекторов и правил. Селектор указывает браузеру, какой именно HTML-элемент мы хотим изменить, а правило описывает, как его изменить.

    В этом примере селектор p находит все абзацы на странице и применяет к ним синий цвет текста и размер шрифта 16 пикселей.

    Классы: Точечное управление дизайном

    Применять стили ко всем абзацам сразу — плохая идея. В интерфейсе чата сообщения пользователя должны выглядеть иначе, чем ответы ИИ. Для этого в HTML существуют атрибуты class.

    Класс — это метка, которую можно повесить на любой тег, чтобы затем обратиться к ней из CSS. В CSS перед именем класса ставится точка.

    HTML:

    CSS:

    Блочная модель (Box Model)

    Самая важная концепция в CSS — это блочная модель. Браузер воспринимает абсолютно каждый HTML-элемент как прямоугольную коробку.

    У этой коробки есть четыре уровня:

  • Content (Контент) — сам текст или картинка.
  • Padding (Внутренний отступ) — пустое пространство между контентом и границей коробки. Делает кнопки "пухлыми".
  • Border (Граница) — рамка вокруг элемента.
  • Margin (Внешний отступ) — пустое пространство снаружи рамки, отодвигающее соседние элементы.
  • | Свойство | Аналогия из жизни | Зачем используется | | :--- | :--- | :--- | | Padding | Поролон внутри коробки с хрупким грузом | Чтобы текст не прилипал к краям кнопки или карточки | | Border | Стенки самой картонной коробки | Для визуального выделения элемента (рамка) | | Margin | Расстояние между двумя коробками на складе | Чтобы элементы не слипались друг с другом на странице |

    Практика: Верстка интерфейса для ИИ-ассистента

    Давайте объединим HTML и CSS, чтобы создать базовый макет для нашего чата. Мы используем современную технологию CSS Flexbox, которая позволяет легко выстраивать элементы в ряд или колонку.

    Этот код создает логичную структуру: шапка с названием, основная область с историей переписки и подвал с полем ввода. Благодаря семантическим тегам header, main и footer, структура понятна не только нам, но и браузеру.

    В следующих этапах курса мы изучим язык JavaScript. Именно он вдохнет жизнь в эту статичную картинку: заставит кнопку "Отправить" реагировать на клик, забирать текст из поля ввода, отправлять его на наш Python-сервер и динамически добавлять ответ ИИ на экран.