Тайм коды со слайдами **слайд** обо мне [0:00:02 → 0:00:50] **слайд** компании использующие AI [0:00:50 → 0:02:10] **слайд** демонстрация работы сервиса gamma.app [0:02:10 → 0:03:12] **слайд** задачи которые нужно решить чтобы сделать аналог [0:03:12 → 0:04:15] **слайд** план создания презентации в видео строки [0:04:15 → 0:05:14] **слайд** о langChain и langGraph [0:05:15 → 0:05:40] **слайд** промт для генерации презентации [0:05:40 → 0:06:23] **слайд** описание json схемы [0:06:23 → 0:08:00] **слайд** описание работы парсера ответа от LLM [0:08:00 → 0:09:25] **слайд** демонстрация распарсеного json [0:09:25 → 0:09:55] **слайд** демонстрация аналога сервиса gamma.app [0:09:55 → 0:10:57] **слайд** объяснение работы langGraph [0:10:57 → 0:26:20] [0:00:00 → 0:00:01] Смотрим, как они устроены. [0:00:02 → 0:00:03] Пару слов о себе. [0:00:03 → 0:00:09] Меня зовут Александр, около 10 лет опыта в IT и 6 из них это работа в Сбере, уже больше половины получается. [0:00:10 → 0:00:14] В Сбере я фронт-энд разработчик и мой основной инструмент это TypeScript. [0:00:16 → 0:00:20] Но кроме того, что я работаю в Сбере, я еще преподаю в университетах. [0:00:21 → 0:00:28] С коллегами моими используем учебную платформу самописную, на которой я как бы full-stack разработчик получается. [0:00:28 → 0:00:31] Там и фронт-энд, и бэк-энд, и ICD, база данных, в общем, все на мне. [0:00:32 → 0:00:35] И в этом году мы запустили курс по созданию AI-агентов. [0:00:35 → 0:00:45] И успешно с магистрами Казанского федерального университета прошли этот курс, они создали приложение с AI-агентами. [0:00:46 → 0:00:48] Вот, но о чем я хотел поговорить. [0:00:48 → 0:00:52] Хайповые AI-сервисы у Forbes, даже есть список на этот счет. [0:00:52 → 0:00:58] Топ 50 компаний, которые на AI-сервисах основаны. [0:00:58 → 0:01:01] Здесь не очень удобно, потому что они отсортированы по алфавиту. [0:01:01 → 0:01:05] Наверное, интереснее посмотреть по деньгам, если их пересортировать. [0:01:05 → 0:01:09] Вот видно, что OpenAI на первом месте, понятно. [0:01:10 → 0:01:15] Но здесь очень много компаний, которые вы наверняка слышали или пользовались их сервисами. [0:01:22 → 0:01:25] Создатели Bolt New, Cursor, все здесь. [0:01:26 → 0:01:28] Ну, меня заинтересовало. [0:01:28 → 0:01:33] Приложение, которое, точнее, компания, вот чуть ниже, Speak называется тоже. [0:01:34 → 0:01:36] Топ 50 входит. [0:01:36 → 0:01:41] Они разработали приложение для изучения иностранных языков с помощью искусственного интеллекта. [0:01:41 → 0:01:43] С помощью AI-агента, помощника. [0:01:44 → 0:01:47] Я его скачал, попробовал, в принципе, неплохо. [0:01:47 → 0:01:52] Но я вам так скажу, что наши студенты, в принципе, делают не хуже, уж точно, не хуже приложения. [0:01:52 → 0:01:56] Хотя здесь они, например, с 2016 года давно работали над ним. [0:01:57 → 0:01:58] Но вот сейчас. [0:01:58 → 0:02:01] Уже можно довольно быстро создавать такие вещи. [0:02:01 → 0:02:04] Давайте разбираться, как они делаются. [0:02:04 → 0:02:07] Как разрабатываются приложения с AI-агентами. [0:02:07 → 0:02:11] В качестве подобного я решил выбрать GumUp. [0:02:12 → 0:02:13] Вспомним, что такое GumUp. [0:02:13 → 0:02:16] Это приложение для создания презентаций. [0:02:17 → 0:02:20] Вот, вводим, что мы хотим, на какую тему мы хотим презентацию. [0:02:20 → 0:02:26] Она нам создает план презентации, где расписывает про каждый слайд, заголовок. [0:02:26 → 0:02:27] Ну и буллеты, коротко там. [0:02:27 → 0:02:28] Один, два, три просто. [0:02:28 → 0:02:30] О чем слайд должен быть. [0:02:30 → 0:02:34] После создания плана нажимаем создать презентацию и она создается. [0:02:35 → 0:02:37] Просто волшебство какое-то. [0:02:37 → 0:02:39] Каждый слайд появляется последовательно. [0:02:39 → 0:02:42] Для каждого слайда там почти для каждого. [0:02:42 → 0:02:43] Генерируется изображение. [0:02:43 → 0:02:45] Буллеты как-то оформляются. [0:02:47 → 0:02:51] Текст тоже как-то все красиво компонуется. [0:02:52 → 0:02:55] И вуаля, просто вот презентация готова. [0:02:56 → 0:02:56] Сгенерирована. [0:02:56 → 0:02:59] Причем довольно красиво получается. [0:02:59 → 0:03:01] В общем-то он все логично расписал. [0:03:01 → 0:03:09] То, что если хотим учиться фотографировать котят, нам нужно подумать об свете, о настройках камеры, выбор ракурса, в общем, такого. [0:03:09 → 0:03:11] Ну давайте декомпозировать. [0:03:11 → 0:03:12] Как такие вещи делать? [0:03:12 → 0:03:13] Значит, первое и второе. [0:03:13 → 0:03:16] Нам глобально надо решить две задачи. [0:03:16 → 0:03:20] Это генерировать план презентации. [0:03:20 → 0:03:22] Ну и саму презентацию на основе этого плана. [0:03:23 → 0:03:24] Вот такие две части. [0:03:24 → 0:03:26] Как разрабатывать план презентации? [0:03:26 → 0:03:28] Ну самый простой путь просто. [0:03:28 → 0:03:34] Берем, открываем любой чатик с искусственным интеллектом, просим его создать нам план презентации. [0:03:34 → 0:03:36] Довольно несложный промпт. [0:03:36 → 0:03:38] Вот я открыл гигачат, посмотрел. [0:03:38 → 0:03:41] Он мне генерирует, в принципе, примерно то же самое. [0:03:42 → 0:03:46] То есть какие-то слайды, темы для каждого слайда и буллеты. [0:03:47 → 0:03:48] О чем должен быть этот слайд? [0:03:48 → 0:03:51] И здесь, в принципе-то, и план такой же. [0:03:51 → 0:03:53] Может быть, чуть другая последовательность. [0:03:53 → 0:03:55] Подумать о композиции, ракурсе. [0:03:56 → 0:03:57] Настроить камеру, настроить свет. [0:03:57 → 0:04:01] Как-то договориться с котенком о том, что мы его будем фотографировать. [0:04:01 → 0:04:02] Ну, в таком плане. [0:04:04 → 0:04:07] Я разработал приложение, которое похоже на Gama App. [0:04:07 → 0:04:11] Можно сказать, клон Gama App, на основе которого я это все буду рассказывать. [0:04:11 → 0:04:14] И у меня в приложении промпт выглядит чуть больше. [0:04:14 → 0:04:16] Вообще, он выглядит вот так. [0:04:16 → 0:04:17] Это огромная-огромная строка. [0:04:18 → 0:04:21] Не пугайтесь, я сейчас разберу, из чего она состоит. [0:04:21 → 0:04:25] В общем-то, тут есть первая такая часть, где я уговариваю его. [0:04:26 → 0:04:27] Создать план. [0:04:27 → 0:04:28] Объясняю, что мы будем создавать план. [0:04:31 → 0:04:34] И расписываю, какие виды слайдов существуют. [0:04:34 → 0:04:37] Какие бывают, которые моя система готова. [0:04:37 → 0:04:40] Потом воспроизводить титульный слайд, там, контентный и так далее. [0:04:41 → 0:04:45] Потом довольно большая часть, которая относится к форматированию ответа. [0:04:45 → 0:04:49] Я хочу получить ответ, ну, не в формате текста или Markdown. [0:04:49 → 0:04:51] Мне будет удобнее работать с форматом JSON. [0:04:53 → 0:04:55] И здесь описывается этот формат. [0:04:56 → 0:04:59] Ну, и в конце я добавляю еще текущую дату. [0:04:59 → 0:05:04] Нейросетям неплохо бы в промптах добавлять информацию о текущей дате. [0:05:04 → 0:05:08] Чтобы он учитывал и понимал, что если просят что-то новенькое, [0:05:08 → 0:05:10] то нужно сходить в интернет и поискать. [0:05:13 → 0:05:17] Делаются такие вещи, в общем-то, не руками, а с помощью фреймворков. [0:05:17 → 0:05:21] Хотя, безусловно, все это и руками можно создать и отправить, но это неудобно. [0:05:21 → 0:05:24] При масштабировании уже становится понятно. [0:05:25 → 0:05:26] Сложно расти. [0:05:26 → 0:05:30] Нужно пользоваться фреймворками, которые как раз позволяют свое решение масштабировать. [0:05:31 → 0:05:32] Лонгчейн и лонгграф, в общем. [0:05:34 → 0:05:37] Для создания плана презентации нам будет достаточно лонгчейна. [0:05:37 → 0:05:39] А вот для самой презентации уже будем использовать лонгграф. [0:05:40 → 0:05:46] Как с помощью лонгчейна создать вот этот большой-большой промпт для генерации плана? [0:05:46 → 0:05:49] В общем-то, используется для этого шаблоны. [0:05:49 → 0:05:52] В лонгчейне есть такой механизм, как вот шаблоны. [0:05:53 → 0:05:55] Мы тут здесь описываем то, [0:05:55 → 0:05:57] что хотим описать, то, что статично, [0:05:57 → 0:06:01] а все вещи, которые будут потом должны быть заполнены, [0:06:01 → 0:06:04] мы обрамляем вот так, фигурные скобки, и название включаем. [0:06:04 → 0:06:07] То есть потом пользователь к нам придет с названием презентации, [0:06:07 → 0:06:10] и мы вот топик подменим на тему, которую он нам прислал. [0:06:11 → 0:06:13] И так далее, все в шаблоне заменим. [0:06:13 → 0:06:14] Это самое простое. [0:06:14 → 0:06:17] А вот насчет инструкций по форматированию, вот можно видеть, [0:06:17 → 0:06:20] что внизу есть вот такая вещь, формат instructions. [0:06:21 → 0:06:24] Вот ее нужно заполнить этими инструкциями для форматирования. [0:06:24 → 0:06:25] Как они делаются? [0:06:26 → 0:06:30] В общем-то, можно JSON-схему, конечно, положить туда руками, [0:06:30 → 0:06:33] но на разных языках это делается чуть по-разному. [0:06:34 → 0:06:36] Примерно всегда есть какая-то обертка. [0:06:37 → 0:06:40] На TypeScript у нас это Zot библиотечка, [0:06:40 → 0:06:44] которая очень популярна стала для описания форм объектов. [0:06:45 → 0:06:47] И вот здесь я как раз описываю, что у нас есть презентация. [0:06:48 → 0:06:51] Глобального презентации в целом есть какое-то заголовок, [0:06:51 → 0:06:52] какое-то описание. [0:06:52 → 0:06:54] Я это делаю, потому что, ну, по-моему, [0:06:54 → 0:06:58] пользователь может опечататься или как-то несуразно сформулировать тему, [0:06:58 → 0:07:00] которую, может быть, нейросеть решит переформулировать, [0:07:01 → 0:07:02] ну, плюс добавить описание. [0:07:02 → 0:07:04] Ну и когда мы генерируем план презентации, [0:07:04 → 0:07:06] в этот момент неплохо бы добавить что-то общее, [0:07:06 → 0:07:09] что будет актуально для всей презентации. [0:07:09 → 0:07:11] Например, общий стиль изображений. [0:07:11 → 0:07:14] Что гамап вы видели, что у меня, посмотрите, [0:07:14 → 0:07:16] что картинки генерируются в едином стиле, [0:07:16 → 0:07:19] едином и для всей презентации. [0:07:19 → 0:07:22] Вот здесь формируется этот стиль глобальный. [0:07:22 → 0:07:23] Ну, а потом каждый слайд. [0:07:24 → 0:07:25] У слайда должен быть заголовок, [0:07:25 → 0:07:27] у него может быть контентная часть, [0:07:27 → 0:07:30] но на этапе планирования, в общем-то, она не заполняется. [0:07:31 → 0:07:34] Буллеты — это как раз вот массив строк про то, [0:07:34 → 0:07:36] о чем слайд, первый, второй, третий, [0:07:36 → 0:07:38] ну и какие-то еще дополнительные вещи, [0:07:38 → 0:07:40] которые мы можем захотеть. [0:07:40 → 0:07:43] Естественно, мы это описываем не просто набором ключей, [0:07:43 → 0:07:46] а для каждого ключа описываем тип этого ключа [0:07:46 → 0:07:51] и через describe описываем, о чем этот ключ. [0:07:51 → 0:07:53] То есть если title — это строка, понятно, [0:07:54 → 0:07:56] то что это за строка описывается уже через describe, [0:07:56 → 0:07:57] как ее заполнять. [0:07:58 → 0:07:59] Вот. [0:08:00 → 0:08:03] После того, как мы эти вещи применим, [0:08:03 → 0:08:05] мы сможем получить финальный промт. [0:08:05 → 0:08:06] Как это делается? [0:08:06 → 0:08:09] Мы берем, тут пример из документации, [0:08:10 → 0:08:13] мы берем Structured Output Parser, [0:08:14 → 0:08:17] который инициализируем из этой ZOT-схемы. [0:08:17 → 0:08:19] Потом нам нужен промт, [0:08:19 → 0:08:23] в котором мы пометим какое-то место, [0:08:23 → 0:08:27] в которое попадет инструкция по форматированию в конечном итоге. [0:08:27 → 0:08:29] Вот такой формат instructions [0:08:29 → 0:08:32] где-то нужно разместить внутри нашего промта. [0:08:33 → 0:08:35] Затем туда добавляются модели parser, [0:08:35 → 0:08:39] то есть мы формируем chain для long chain. [0:08:39 → 0:08:42] Первым в этой цепочке получается template, [0:08:42 → 0:08:44] потом модель, потом parser, [0:08:44 → 0:08:46] который нам на выходе выдаст как раз JSON. [0:08:47 → 0:08:48] Вот. [0:08:49 → 0:08:51] Инструкции по форматированию получаются, ну, [0:08:51 → 0:08:53] вызовом метода getFormatInstructions. [0:08:53 → 0:08:55] Тут, в общем-то, тоже как бы магии нету. [0:08:55 → 0:08:57] В момент, когда мы запускаем цепочку, [0:08:57 → 0:09:01] мы этот ключ формат instructions заполняем вызовом этого метода [0:09:02 → 0:09:02] getFormatInstructions. [0:09:03 → 0:09:07] То есть getFormatInstructions просто выдает нам конкретное сообщение, [0:09:07 → 0:09:11] что там, пожалуйста, следуй вот такой JSON-схеме, [0:09:11 → 0:09:16] отдай мне ответ, строго соблюдая этот контракт. [0:09:18 → 0:09:20] Ну и, кроме того, можем заполнить дополнительные вещи, [0:09:20 → 0:09:24] то есть в моем случае пользователь приходит с темой презентации, [0:09:24 → 0:09:26] может быть, с каким-то дополнительным описанием, [0:09:26 → 0:09:27] я все это прокидываю в prompt. [0:09:28 → 0:09:29] Выглядит это вот таким образом. [0:09:29 → 0:09:33] Если это вызвать, то искусственный интеллект начинает нам возвращать [0:09:33 → 0:09:36] большую-большую строку, которая внутри содержит JSON. [0:09:38 → 0:09:42] В этом JSON просто вот заполнены те ключи, которые я описывал. [0:09:42 → 0:09:44] Ну, где-то он даже контент заполняет, [0:09:44 → 0:09:46] но потом я в любом случае контент формирую уже отдельно, [0:09:46 → 0:09:49] когда презентация целиком генерируется. [0:09:50 → 0:09:53] Вот. Значит, ну тут примерно то же самое. [0:09:53 → 0:09:54] Профотографирование котят. [0:09:55 → 0:09:57] Посмотрим, как это выглядит у меня. [0:09:59 → 0:10:02] Я закидываю тему презентации, прошу сгенерировать план, [0:10:02 → 0:10:06] и вот начинает прилетать этот JSON, который я показывал, [0:10:06 → 0:10:08] он разбирается, и мы генерируем план. [0:10:09 → 0:10:11] Ну, интересно, что в моем случае я решил, [0:10:12 → 0:10:15] что было бы неплохо ходить в интернет для некоторых тем, [0:10:15 → 0:10:17] а для некоторых слайдов, [0:10:17 → 0:10:20] если искусственный интеллект так решит, [0:10:20 → 0:10:23] если мой AI-агент захочет что-то поискать в интернете. [0:10:23 → 0:10:26] Ну и здесь я генерирую промпт для изображения [0:10:26 → 0:10:28] и негативный промпт для изображения, [0:10:28 → 0:10:30] чтобы потом диффузионной моделькой, собственно, [0:10:30 → 0:10:33] сгенерировать изображение с промптом и негативным промптом. [0:10:34 → 0:10:37] Вот. Затем нажимаем «Создать презентацию». [0:10:37 → 0:10:39] В принципе, с планом мы разобрались. [0:10:39 → 0:10:43] И вот как выглядит создание презентации. [0:10:44 → 0:10:44] Нажимаем. [0:10:46 → 0:10:47] Немножко подвисло. [0:10:51 → 0:10:53] Видео немножко подвисло, к сожалению. [0:10:53 → 0:10:56] Но можно сходить на сайт и посмотреть, [0:10:56 → 0:10:57] как оно там генерируется. [0:10:58 → 0:11:02] Как же генерировать вот такие более сложные вещи, [0:11:02 → 0:11:03] как слайды? [0:11:03 → 0:11:05] Это уже не просто план, где, в принципе, [0:11:05 → 0:11:08] одной текстовкой можно одним запросом обойтись. [0:11:08 → 0:11:10] А здесь вот, когда мы генерируем слайды, [0:11:10 → 0:11:11] каждый слайд — это какой-то контент, [0:11:11 → 0:11:14] тоже заголовок, изображение. [0:11:14 → 0:11:16] Может, надо сходить в интернет, чего-то поискать, [0:11:16 → 0:11:18] и всё это собрать в кучу. [0:11:18 → 0:11:20] В общем, здесь уже более сложные вещи. [0:11:20 → 0:11:21] Здесь нам поможет ланграф. [0:11:22 → 0:11:25] Как раз этот фреймворк позволяет уже более сложные вещи делать. [0:11:27 → 0:11:30] Когда мы пользуемся ланграфом, [0:11:30 → 0:11:32] мы описываем логику в форме нод, [0:11:32 → 0:11:34] в форме узлов логики, [0:11:34 → 0:11:37] каждый из которых является функцией [0:11:37 → 0:11:38] и делает что-то своё. [0:11:39 → 0:11:41] У такого графа будет какая-то входная нода [0:11:41 → 0:11:42] и какая-то выходная нода. [0:11:43 → 0:11:44] И между ними будет, конечно, [0:11:44 → 0:11:46] как-то распределяться логика. [0:11:46 → 0:11:48] То есть мы можем делать что-то последовательно. [0:11:49 → 0:11:50] Каждая нода делает что-то своё. [0:11:51 → 0:11:53] Может быть, генерирует изображение, [0:11:53 → 0:11:54] может быть, входит в интернет, [0:11:54 → 0:11:55] может быть, что-то ещё. [0:11:55 → 0:11:59] Не обязательно, чтобы путь, [0:11:59 → 0:12:00] по которому перемещается логика, [0:12:00 → 0:12:02] был именно таким последовательным. [0:12:03 → 0:12:05] Потому что это, в принципе, делает лангчейн. [0:12:05 → 0:12:07] А для более сложных вещей [0:12:07 → 0:12:09] у нас есть возможность описать роутинг [0:12:09 → 0:12:13] и по каким-то условиям ходить в одни ноды, [0:12:13 → 0:12:14] не ходить в другие. [0:12:14 → 0:12:15] В общем-то, менять путь. [0:12:16 → 0:12:19] При этом мы можем написать логику так, [0:12:19 → 0:12:20] что в конце у нас будет вариант [0:12:21 → 0:12:23] вернуться в самое начало. [0:12:23 → 0:12:24] У нас может быть какой-то [0:12:24 → 0:12:26] дополнительный агент-критик, [0:12:26 → 0:12:28] который посмотрит, что мы сделали, [0:12:28 → 0:12:30] то есть что сделал AI-агент наш. [0:12:30 → 0:12:32] Если ему что-то не понравится, [0:12:32 → 0:12:33] он скажет «переделай» и вернётся [0:12:33 → 0:12:35] на какой-то этап начала, [0:12:36 → 0:12:38] чтобы сделать заново, учитывая правки. [0:12:39 → 0:12:42] Весь этот граф, он работает [0:12:42 → 0:12:44] с единым состоянием. [0:12:44 → 0:12:47] Мы определяем изначальное состояние [0:12:47 → 0:12:48] и потом передаём его между нодами, [0:12:48 → 0:12:50] и каждая нода работает с этим состоянием, [0:12:50 → 0:12:51] что-то там обновляет. [0:12:52 → 0:12:54] По итогу работа AI-агента является [0:12:55 → 0:12:56] заполненное состояние, [0:12:56 → 0:12:58] которое мы потом снаружи считываем [0:12:58 → 0:12:59] и что-то с ним делаем. [0:12:59 → 0:13:00] Например, рисуем презентацию. [0:13:02 → 0:13:03] Когда я разрабатывал... [0:13:03 → 0:13:05] А, бывают более сложные схемы, [0:13:05 → 0:13:06] более интересные даже. [0:13:07 → 0:13:09] Когда у нас есть агент-супервайзер, [0:13:09 → 0:13:12] который сам принимает решение, [0:13:12 → 0:13:13] нужно ли ему воспользоваться [0:13:13 → 0:13:15] другим каким-то агентом, [0:13:15 → 0:13:17] и сам принимает решение, [0:13:17 → 0:13:18] когда ему закончить, [0:13:18 → 0:13:21] например, ему дали задачу нарисовать презентацию, [0:13:21 → 0:13:22] он может подумать так, [0:13:22 → 0:13:24] мне сейчас нужно пойти в агента [0:13:24 → 0:13:25] для поиска в интернете, [0:13:25 → 0:13:26] поискать информацию, [0:13:26 → 0:13:28] потом сходить к художнику нарисовать, [0:13:28 → 0:13:31] потом сходить там и контент нагенерировать. [0:13:31 → 0:13:34] Но при этом он сам принимает все эти решения [0:13:34 → 0:13:35] и самостоятельно решает, [0:13:35 → 0:13:39] куда пойти и какого агента сейчас задействовать. [0:13:39 → 0:13:41] Получается такая мультиагентная система. [0:13:42 → 0:13:44] В моем случае [0:13:44 → 0:13:46] настолько сложная вещь не понадобилась. [0:13:47 → 0:13:49] Я пошел по более простому пути. [0:13:49 → 0:13:50] Я рисовал графы. [0:13:51 → 0:13:52] У меня получилось несколько вариантов [0:13:52 → 0:13:54] в ходе разработки этого клона. [0:13:56 → 0:13:59] И сейчас я вам покажу финальный вариант. [0:13:59 → 0:14:00] Он получился довольно компактным, [0:14:01 → 0:14:03] довольно дешевым в использовании [0:14:03 → 0:14:04] и довольно быстрым. [0:14:05 → 0:14:06] Дело в том, что некоторые ноды [0:14:06 → 0:14:08] можно запускать параллельно, [0:14:08 → 0:14:11] одновременно работая над несколькими задачами. [0:14:11 → 0:14:13] Посмотрим поочередно. [0:14:13 → 0:14:15] Сначала шаг подготовка. [0:14:15 → 0:14:18] В моем случае, что такое подготовка? [0:14:18 → 0:14:19] Я просто прохожусь по слайдам, [0:14:19 → 0:14:21] сгенерированным на этапе планирования [0:14:21 → 0:14:23] и назначаю им ID. [0:14:23 → 0:14:26] Здесь будет неплохо показать, [0:14:26 → 0:14:28] как выглядит нода графа. [0:14:28 → 0:14:29] В общем-то, [0:14:29 → 0:14:31] это функция, [0:14:31 → 0:14:33] которая принимает состояние [0:14:33 → 0:14:35] и она должна обещать вернуть нам [0:14:35 → 0:14:38] какую-то часть состояния на апдейт. [0:14:38 → 0:14:39] Может целиком состояние обновить, [0:14:39 → 0:14:41] может какие-то конкретные ключи. [0:14:42 → 0:14:43] Поэтому, по итогу, [0:14:43 → 0:14:45] мы должны вернуть объект, [0:14:45 → 0:14:47] у которого будут заполнены ключи [0:14:47 → 0:14:49] состояния, в данном случае один ключ [0:14:49 → 0:14:51] Presentation, в котором уже лежит план [0:14:51 → 0:14:53] на этапе генерации. [0:14:54 → 0:14:56] Это планирование мы его создали [0:14:56 → 0:14:57] и когда приходим к генерации, [0:14:57 → 0:14:59] состояние заполняется этим планом. [0:15:00 → 0:15:02] Я прохожусь внутри плана [0:15:02 → 0:15:03] по всем слайдам и просто [0:15:03 → 0:15:04] генерирую ID. [0:15:05 → 0:15:06] Давайте дальше. [0:15:06 → 0:15:09] Нам нужно принять решение, куда пойти. [0:15:09 → 0:15:10] Либо на веб-ресерч, [0:15:10 → 0:15:12] либо на генерацию. [0:15:13 → 0:15:15] То есть нам нужно пройтись [0:15:15 → 0:15:17] по плану, по всем слайдам [0:15:17 → 0:15:19] и найти, есть ли такие слайды, [0:15:19 → 0:15:21] для которых есть промт поиска в интернете. [0:15:21 → 0:15:22] Для этого используется [0:15:23 → 0:15:25] Conditional Edge, так называемый. [0:15:25 → 0:15:27] Что это такое [0:15:27 → 0:15:28] Conditional Edge? [0:15:29 → 0:15:31] Тоже функция, она тоже принимает состояние, [0:15:31 → 0:15:32] но у нее задача вернуть [0:15:32 → 0:15:34] не апдейт состояния, [0:15:34 → 0:15:35] а куда пойти дальше. [0:15:36 → 0:15:38] И здесь я создаю [0:15:40 → 0:15:42] массив, куда можно пойти [0:15:42 → 0:15:44] после работы этой [0:15:44 → 0:15:45] предыдущей ноды. [0:15:46 → 0:15:47] И либо его заполняю, [0:15:48 → 0:15:49] либо, если он не заполнен, мы идем на exit. [0:15:50 → 0:15:51] Exit в данном случае имеется ввиду [0:15:51 → 0:15:53] пойти на to generate, то есть [0:15:53 → 0:15:54] презентацию генерировать, [0:15:54 → 0:15:57] как будто в поиск не потребовался. [0:15:59 → 0:16:00] Массив send-off я создаю [0:16:00 → 0:16:01] пустым, [0:16:01 → 0:16:02] а потом пытаюсь его заполнить. [0:16:02 → 0:16:04] Я пробегаюсь по всем слайдам, [0:16:04 → 0:16:06] ищу слайды, у которых заполнен [0:16:06 → 0:16:07] ключ websearch query. [0:16:08 → 0:16:10] На этапе планирования, [0:16:10 → 0:16:11] напомню, это происходит. [0:16:11 → 0:16:13] И если такой слайд нашелся, [0:16:13 → 0:16:15] то я его помещаю в push, [0:16:16 → 0:16:18] в sends, массив send-off, [0:16:18 → 0:16:19] ну и заполняю [0:16:19 → 0:16:22] с помощью специального класса send. [0:16:23 → 0:16:26] Send передается, какую ноду надо запустить, [0:16:26 → 0:16:28] в данном случае webresearch называется нода, [0:16:28 → 0:16:29] и какие данные в него прокинуть. [0:16:30 → 0:16:31] Можно прокидывать [0:16:31 → 0:16:32] хоть все состояние, [0:16:32 → 0:16:34] но довольно удобно [0:16:34 → 0:16:36] прокидывать только то, что нужно. [0:16:36 → 0:16:38] Например, один слайд, с которым [0:16:38 → 0:16:40] эта нода для research [0:16:40 → 0:16:42] будет работать. Ей, в общем-то, нужен [0:16:42 → 0:16:44] только websearch query, но чтобы сохранить [0:16:44 → 0:16:46] в состоянии информацию, понадобится [0:16:46 → 0:16:48] id-шник слайда, ну, в общем, проще [0:16:48 → 0:16:50] передать туда целиком слайд. [0:16:50 → 0:16:52] Внутри она считает id-шник и [0:16:52 → 0:16:54] websearch query. Как будет [0:16:54 → 0:16:56] websearch query работать? В общем-то, [0:16:56 → 0:16:58] есть разные способы, как [0:16:58 → 0:17:00] поискать в интернете, можно даже [0:17:00 → 0:17:02] самостоятельно написать краулер, [0:17:02 → 0:17:04] какой-нибудь, но я воспользовался [0:17:04 → 0:17:06] удобной утилитой Tavili, [0:17:06 → 0:17:08] которая позволяет искать [0:17:08 → 0:17:09] в интернете. [0:17:10 → 0:17:12] Вот напомню, что в Send мы отправляли [0:17:12 → 0:17:14] один слайд, поэтому [0:17:14 → 0:17:16] здесь функция принимает тоже [0:17:16 → 0:17:17] один слайд. [0:17:18 → 0:17:20] Внутри я инициирую этот [0:17:20 → 0:17:22] инструмент Tavili, [0:17:22 → 0:17:24] ну, а затем просто из слайда [0:17:24 → 0:17:26] беру websearch query и, в общем, [0:17:26 → 0:17:28] иду искать и жду, пока этот [0:17:28 → 0:17:30] инструмент мне найдет то, что нужно. [0:17:31 → 0:17:32] Затем я обновляю [0:17:32 → 0:17:34] состояние, специально [0:17:34 → 0:17:36] созданный для этого ключик [0:17:36 → 0:17:38] websearch-result, и по id-шнику слайда [0:17:38 → 0:17:40] сохраняю информацию, которую нашел в интернете. [0:17:41 → 0:17:42] Интересный моментик, вот с фигурными [0:17:42 → 0:17:44] скобками, если создавать презентации [0:17:44 → 0:17:46] на какие-то темы для программистов, [0:17:46 → 0:17:48] там наверняка будут блоки кода, блоки кода [0:17:48 → 0:17:50] содержат фигурные скобки, а лонгчейн [0:17:50 → 0:17:52] воспринимает фигурные скобки как [0:17:52 → 0:17:54] часть темплейта, которую надо заполнить [0:17:54 → 0:17:56] какими-то данными, и [0:17:56 → 0:17:57] будет ругаться, что [0:17:58 → 0:18:00] вы не предоставили мне данные [0:18:00 → 0:18:02] для заполнения этого ключа. [0:18:03 → 0:18:04] Поэтому я их [0:18:05 → 0:18:06] экранирую, я нахожу фигурные [0:18:06 → 0:18:08] скобки и [0:18:08 → 0:18:10] делаю двойными фигурными скобками, [0:18:10 → 0:18:13] в общем-то это способ заэкранировать [0:18:13 → 0:18:13] их. [0:18:14 → 0:18:16] Вот и все, то есть точно так же, [0:18:17 → 0:18:18] как и в prepare, в общем [0:18:18 → 0:18:20] я что-то делаю и кладу это [0:18:20 → 0:18:22] в состояние. Далее нам [0:18:22 → 0:18:24] надо принять решение, то есть мы, понятно, [0:18:24 → 0:18:26] уже идем на generate, и далее [0:18:26 → 0:18:28] надо понять, хотим ли мы сгенерировать [0:18:28 → 0:18:31] картинки или начинаем контент генерировать. [0:18:32 → 0:18:34] Что здесь происходит? Это тоже [0:18:34 → 0:18:37] роутер, он тоже принимает состояние [0:18:37 → 0:18:38] и должен вернуть, куда идем дальше. [0:18:39 → 0:18:41] Точно так же работает с массивом [0:18:41 → 0:18:42] sendов. Здесь есть [0:18:42 → 0:18:44] вот такой цикл, [0:18:44 → 0:18:46] я пробегаюсь по слайдам, [0:18:47 → 0:18:48] по каждому из них, потом [0:18:48 → 0:18:50] смотрю, есть ли там prompt на генерацию [0:18:50 → 0:18:53] изображения, и массив sendов заполняю [0:18:53 → 0:18:54] опять экземпляром класса [0:18:54 → 0:18:56] send, название нода, куда [0:18:56 → 0:18:58] нужно пойти, и информация, которая [0:18:58 → 0:19:01] нужна для генерации. Это слайд [0:19:01 → 0:19:02] с его image prompt и negative [0:19:02 → 0:19:05] image prompt, и также глобальный [0:19:05 → 0:19:07] стиль изображения, который тоже надо учесть, [0:19:07 → 0:19:09] чтобы во всей презентации [0:19:09 → 0:19:11] и во всех слайдах стили изображения [0:19:11 → 0:19:12] были в едином стиле. [0:19:13 → 0:19:14] Вот. Ну, [0:19:14 → 0:19:16] как генерировать картинки? [0:19:16 → 0:19:18] Я думаю, вы знаете, что есть [0:19:18 → 0:19:20] такая у нас нероссеть Кандинский, [0:19:20 → 0:19:22] которая умеет изображения генерировать, [0:19:22 → 0:19:24] и у нее недавно появилась такая кнопочка API [0:19:24 → 0:19:25] позволяющая [0:19:27 → 0:19:29] взаимодействовать программным способом [0:19:29 → 0:19:31] с Кандинским и генерировать изображения [0:19:31 → 0:19:31] изображения. [0:19:33 → 0:19:34] Ну, не стану [0:19:35 → 0:19:36] врать, в общем, кода получилось много, [0:19:36 → 0:19:38] здесь он вроде бы [0:19:38 → 0:19:40] даже весь. Я написал [0:19:40 → 0:19:42] обертку для работы с API [0:19:44 → 0:19:44] Кандинского, [0:19:44 → 0:19:45] назвал его собачка-броджия [0:19:45 → 0:19:48] это чисто обертка [0:19:48 → 0:19:50] над API, [0:19:50 → 0:19:52] позволяющая чуть более удобно с ней работать, [0:19:52 → 0:19:54] но даже с учетом такой обертки [0:19:55 → 0:19:56] кода получилось много. [0:19:56 → 0:19:58] Моя обертка над API [0:19:59 → 0:19:59] Кандинский [0:20:01 → 0:20:02] насосная, в общем, ее можно найти [0:20:02 → 0:20:03] на Gitverse, [0:20:04 → 0:20:06] если интересно, можете попробовать. [0:20:08 → 0:20:08] Внутри я просто [0:20:08 → 0:20:10] смотрю, что некоторые слайды [0:20:10 → 0:20:11] я хочу квадратные, [0:20:12 → 0:20:14] один к одному, иногда 16 к 9, [0:20:14 → 0:20:16] в зависимости от того, какой слайд. [0:20:16 → 0:20:18] В общем, сохраняю изображение в файловой [0:20:18 → 0:20:20] системе и сохраняю в состоянии [0:20:20 → 0:20:22] ссылку на это изображение. [0:20:23 → 0:20:24] В общем-то и все. [0:20:25 → 0:20:26] Идем дальше. [0:20:26 → 0:20:29] Ну, наконец-то, генерация контента. [0:20:29 → 0:20:30] Как же сгенерировать контент? [0:20:31 → 0:20:32] В общем-то, контент мне нужен, в принципе, [0:20:32 → 0:20:35] в Markdown, который я на UI [0:20:35 → 0:20:36] потом смогу красиво [0:20:37 → 0:20:37] отображать. [0:20:38 → 0:20:40] Я принял такое решение. [0:20:40 → 0:20:42] Но тут есть интересный момент. [0:20:42 → 0:20:44] Когда мы идем генерировать такие уже [0:20:44 → 0:20:46] более сложные вещи, мы будем [0:20:46 → 0:20:48] пользоваться блокчейном, и для начала [0:20:48 → 0:20:50] нам понадобится системный промт, [0:20:50 → 0:20:51] где мы описываем, [0:20:52 → 0:20:53] что мы сейчас будем генерировать. [0:20:53 → 0:20:56] Значит, слайды, контент для слайдов, [0:20:56 → 0:20:58] там внутри, [0:20:59 → 0:21:00] имея в виду, что на слайде [0:21:01 → 0:21:02] очень много текста влезает, [0:21:02 → 0:21:03] нам нужны какие-то буллеты, [0:21:03 → 0:21:05] ну, в общем, какое-то описание, как мы видим [0:21:06 → 0:21:08] генерацию этих контента для слайда. [0:21:09 → 0:21:10] Это системный промт. [0:21:10 → 0:21:12] Потом мы досылаем туда [0:21:13 → 0:21:14] запрос. Вот данные, сгенерируй [0:21:14 → 0:21:15] контент для слайда. [0:21:16 → 0:21:18] То есть, вот результат поиска в интернете [0:21:18 → 0:21:20] мы туда дописываем, [0:21:20 → 0:21:23] о чем у нас был этот слайд [0:21:23 → 0:21:25] на этапе планирования [0:21:25 → 0:21:26] названия, там буллеты, [0:21:26 → 0:21:28] в общем, все, что надо уже для слайда, [0:21:28 → 0:21:30] готовое мы туда складываем, [0:21:30 → 0:21:31] вот, сгенерируй нам, пожалуйста, слайд. [0:21:32 → 0:21:33] Нейросеть нам отвечает. [0:21:33 → 0:21:35] Хорошо, вот контент. И в Markdown, значит, [0:21:35 → 0:21:36] отдает контент. [0:21:38 → 0:21:39] После чего [0:21:39 → 0:21:41] мне хочется пойти немножко дальше [0:21:41 → 0:21:43] и сгенерировать не только контент, [0:21:43 → 0:21:46] но и описание, о чем этот слайд, [0:21:46 → 0:21:47] о чем думала нейросеть, [0:21:48 → 0:21:49] создавая этот слайд, [0:21:49 → 0:21:51] и о чем надо, собственно, рассказывать [0:21:52 → 0:21:54] тому, кто будет [0:21:55 → 0:21:56] презентацию показывать [0:21:56 → 0:21:58] и рассказывать что-то по ней. [0:21:58 → 0:21:59] Поэтому я решил [0:21:59 → 0:22:01] сделать так, что мне нужно пойти [0:22:01 → 0:22:04] в нейросеть и попросить ее сгенерировать. [0:22:04 → 0:22:04] Пожалуйста, теперь [0:22:06 → 0:22:08] комментарий для этого слайда, [0:22:08 → 0:22:10] где опиши, что рассказывать. [0:22:10 → 0:22:12] Я мог бы отдельно [0:22:12 → 0:22:13] создать для этого системный промт, [0:22:13 → 0:22:16] отдельно там запрос сделать, [0:22:16 → 0:22:16] но я делаю так. [0:22:17 → 0:22:19] В массив сообщений, которые [0:22:20 → 0:22:21] мне получился, [0:22:21 → 0:22:23] там системный промт изначальный, [0:22:23 → 0:22:25] первый запрос на генерацию [0:22:26 → 0:22:28] контента я тоже туда дописываю, [0:22:28 → 0:22:29] ответ нейросети хорошо, [0:22:30 → 0:22:32] контент я тоже в этот массив сообщений [0:22:32 → 0:22:34] складываю и далее добавляю [0:22:34 → 0:22:35] еще один запрос. [0:22:35 → 0:22:37] Теперь, пожалуйста, добавь комментарий для спикера. [0:22:39 → 0:22:41] Потом нейросеть не отвечает [0:22:41 → 0:22:43] и это получается уже [0:22:43 → 0:22:46] пятый элемент массива сообщений. [0:22:46 → 0:22:48] То есть я сохраняю себе [0:22:48 → 0:22:49] этот комментарий и далее [0:22:49 → 0:22:52] для следующих слайдов я поступаю точно так же. [0:22:52 → 0:22:53] Я донаращиваю [0:22:54 → 0:22:55] общий массив сообщений [0:22:56 → 0:22:57] при генерации контента [0:22:57 → 0:22:59] для всех слайдов всей презентации. [0:23:00 → 0:23:02] Получается, что у меня как бы [0:23:02 → 0:23:04] есть история общения с нейросетью, [0:23:04 → 0:23:06] где дописываются новые сообщения [0:23:06 → 0:23:07] и ее новые ответы [0:23:07 → 0:23:08] на мои сообщения. [0:23:09 → 0:23:11] Таким образом, если для [0:23:11 → 0:23:13] презентации нужно, чтобы мысль [0:23:13 → 0:23:15] как-то последовательно развивалась, [0:23:15 → 0:23:17] я сохраняю контекст предыдущих слайдов, [0:23:17 → 0:23:19] чтобы следующие при генерации [0:23:19 → 0:23:20] учитывали этот контекст. [0:23:21 → 0:23:23] При общении с нейросетью [0:23:23 → 0:23:24] у нас есть такая возможность [0:23:24 → 0:23:27] отправлять туда массив сообщений. [0:23:27 → 0:23:29] Надо просто помечать, [0:23:29 → 0:23:30] какой из них системный, [0:23:30 → 0:23:31] какой от пользователя, [0:23:31 → 0:23:33] а какой ответ от AI. [0:23:34 → 0:23:36] В общем-то, это и все. [0:23:36 → 0:23:38] И потом у нас остается только [0:23:38 → 0:23:41] собрать все эти ноды воедино. [0:23:41 → 0:23:42] Как это делается? [0:23:43 → 0:23:45] Каждая нода это функция. [0:23:45 → 0:23:47] Мы должны просто добавить [0:23:47 → 0:23:48] в наш State Graph. [0:23:48 → 0:23:50] State Graph прокидывается изначально [0:23:50 → 0:23:52] состояние, с которым мы хотим начать. [0:23:53 → 0:23:55] В этом состоянии можно еще и описать, [0:23:55 → 0:23:56] как именно обновляется каждый ключ. [0:23:57 → 0:23:58] Редюсеры добавить. [0:23:59 → 0:24:00] После чего, [0:24:01 → 0:24:02] получившемуся State Graph, [0:24:02 → 0:24:04] мы добавляем ноды. [0:24:04 → 0:24:06] Мы их как-то должны назвать, [0:24:06 → 0:24:08] как вы видели на графе, [0:24:08 → 0:24:10] и прокинуть функцию. [0:24:12 → 0:24:13] Затем [0:24:13 → 0:24:15] нам нужно добавить [0:24:15 → 0:24:16] описание, как мы будем [0:24:16 → 0:24:18] перемещаться между этими нодами. [0:24:18 → 0:24:20] От какой в какую можно пойти, [0:24:20 → 0:24:22] где начало и где конец. [0:24:23 → 0:24:24] Start это Prepare, [0:24:24 → 0:24:26] а Final это End. [0:24:26 → 0:24:28] Это означает, что работа [0:24:29 → 0:24:30] агента после выполнения [0:24:30 → 0:24:33] ноды Final будет завершена, [0:24:33 → 0:24:34] и он отдаст нам результат [0:24:35 → 0:24:37] генерации презентации. [0:24:37 → 0:24:39] Все, потом это дело компилируем. [0:24:39 → 0:24:40] В общем, работаем [0:24:41 → 0:24:41] с этим. [0:24:43 → 0:24:45] Там уже ничего сложного. [0:24:45 → 0:24:47] Можно посмотреть, [0:24:47 → 0:24:49] вот такая презентация [0:24:49 → 0:24:51] получилась на моем сервисе. [0:24:51 → 0:24:53] Можно тоже сходить [0:24:53 → 0:24:55] и попробовать посмотреть, [0:24:55 → 0:24:56] как они генерируются. [0:24:57 → 0:24:59] Вот такую презентацию [0:24:59 → 0:25:01] я сгенерировал. [0:25:01 → 0:25:03] Как видно, тут примерно [0:25:03 → 0:25:05] о том же. [0:25:05 → 0:25:06] В общем, тоже изображение [0:25:06 → 0:25:07] в едином стиле, [0:25:07 → 0:25:10] и на моем сайте можно [0:25:11 → 0:25:12] проиграть презентацию. [0:25:13 → 0:25:14] В общем, [0:25:15 → 0:25:16] это и все. [0:25:17 → 0:25:18] Основной вывод [0:25:18 → 0:25:20] это нужно пробовать, [0:25:20 → 0:25:22] делать это не какой-то [0:25:22 → 0:25:23] rocket science, [0:25:24 → 0:25:26] не только для ML [0:25:26 → 0:25:28] инженеров, [0:25:28 → 0:25:29] специалистов. [0:25:29 → 0:25:31] Сейчас я фронт-энд разработчик [0:25:31 → 0:25:32] в основном, [0:25:32 → 0:25:34] сделал вот такую вещь. [0:25:35 → 0:25:37] Соответственно, когда объединяются команды, [0:25:37 → 0:25:39] можно делать вещи более сложные [0:25:39 → 0:25:40] и интересные. [0:25:40 → 0:25:43] Это дело развивается очень интенсивно, [0:25:44 → 0:25:46] и всем стоит попробовать [0:25:46 → 0:25:47] залезть, посмотреть, [0:25:47 → 0:25:49] как эти вещи создаются, [0:25:50 → 0:25:51] может быть, создать что-то [0:25:52 → 0:25:53] крутое и интересное. [0:25:53 → 0:25:56] В любом случае, вы все, [0:25:56 → 0:25:58] кто это смотрит, слушает, читает, [0:25:58 → 0:25:59] будете [0:26:00 → 0:26:02] участвовать в разработке [0:26:02 → 0:26:04] чего-то, что связано и агентами. [0:26:04 → 0:26:05] Это уже неизбежно. [0:26:06 → 0:26:08] И я думаю, всем стоит [0:26:08 → 0:26:10] попробовать самостоятельно [0:26:10 → 0:26:11] эту историю поговорить, [0:26:12 → 0:26:14] чтобы, по крайней мере, [0:26:14 → 0:26:15] если даже вам эта идея не нравится, [0:26:16 → 0:26:17] просто быть знакомым [0:26:17 → 0:26:19] с тем, как такие вещи создаются. [0:26:20 → 0:26:21] Вот. [0:26:21 → 0:26:23] По QR-коду доступно [0:26:23 → 0:26:25] собственно мое приложение, [0:26:25 → 0:26:27] которое генерирует презентации, [0:26:27 → 0:26:29] но понятно, что генерация завязана [0:26:29 → 0:26:32] на потребление токенов, [0:26:32 → 0:26:33] которые стоят денег, [0:26:33 → 0:26:35] поэтому какое-то время [0:26:35 → 0:26:36] оно может быть доступно, [0:26:36 → 0:26:38] но потом я отключу. [0:26:39 → 0:26:41] На этом спасибо [0:26:42 → 0:26:43] и всем пока.