# Создаём клон Gamma.app: от промпта до AI-агента на LangGraph *Как фронт-энд разработчик из Сбера создал аналог популярного сервиса для генерации презентаций с помощью LangChain и LangGraph* ![Слайд с информацией о спикере](./primakov-article-images/Слайд%20с%20информацией%20о%20спикере.PNG) *[0:00:02 → 0:00:50]* ## Кто создаёт AI-сервисы сегодня? В списке Forbes [топ-50 AI-компаний](https://www.forbes.com/lists/ai50) есть множество знакомых названий: OpenAI, Cursor, Bolt New. Я вот обратил внимание на компанию Speak — они с 2016 года разрабатывают приложение для изучения языков с AI-помощником и попали в топ-50. ![Слайд с компаниями, использующими AI](./primakov-article-images/Слайд%20с%20компаниями,%20использующими%20AI.PNG) *[0:00:50 → 0:02:10]* Но главное открытие: **сейчас создавать AI-приложения можно довольно быстро**. Наши студенты делают продукты не хуже тех, что попадают в топ-рейтинги. ## Gamma.app: что внутри магии? [Gamma.app](https://gamma.app/) — это сервис для создания презентаций. Вы вводите тему, он создаёт план презентации с заголовками и буллетами для каждого слайда, а затем генерирует полную презентацию с изображениями и оформлением. ![Демонстрация работы сервиса gamma.app](./primakov-article-images/Демонстрация%20работы%20сервиса%20gamma.app.PNG) *[0:02:10 → 0:03:12]* Выглядит как волшебство, но давайте разберём, как создаются подобные проекты. ## Декомпозиция задачи Чтобы создать аналог Gamma.app, нужно решить две основные задачи: ![Слайд с задачами для решения](./primakov-article-images/Слайд%20с%20задачами%20для%20решения.PNG) *[0:03:12 → 0:04:15]* 1. **Генерировать план презентации** — структуру с заголовками и основными пунктами 2. **Создавать презентацию** — контент, изображения и оформление на основе плана ## Часть 1: Генерация плана с LangChain ### Промпт-инжиниринг в реальности Простой промпт в GigaChat может создать план презентации, но для продакшена нужен более сложный подход: ![Слайд с промптом для генерации презентации](./primakov-article-images/Слайд%20с%20промптом%20для%20генерации%20презентации.PNG) *[0:05:40 → 0:06:23]* Мой промпт включает: - Инструкции по созданию плана - Описание доступных типов слайдов (титульный, контентный и т.д.) - Требования к формату JSON - Текущую дату (важно для актуальных тем) ### Шаблоны в LangChain LangChain предоставляет механизм шаблонов для удобной работы с промптами: ```typescript const template = ` Создай план презентации на тему {topic}. Учти следующие требования: {requirements} Текущая дата: {current_date} {format_instructions} `; ``` Переменные в фигурных скобках заменяются на реальные значения при выполнении. ### Структурированный вывод с Zod Для надёжной работы с JSON-ответами использую Zod для описания схемы: ![Слайд с описанием JSON схемы](https://via.placeholder.com/800x450/2B6CB0/FFFFFF?text=JSON+Schema+с+Zod%0Apresentation%3A+%7B%0A++title%3A+string%0A++slides%3A+array%0A++imageStyle%3A+string%0A%7D) *[0:06:23 → 0:08:00]* ```typescript const presentationSchema = z.object({ title: z.string().describe("Заголовок презентации"), description: z.string().describe("Описание презентации"), imageStyle: z.string().describe("Общий стиль изображений"), slides: z.array(z.object({ title: z.string().describe("Заголовок слайда"), bullets: z.array(z.string()).describe("Основные пункты"), imagePrompt: z.string().describe("Промпт для генерации изображения"), webSearchQuery: z.string().optional().describe("Запрос для поиска в интернете") })) }); ``` ### Создание цепочки LangChain ![Слайд с описанием работы парсера ответа от LLM](https://via.placeholder.com/800x450/2B6CB0/FFFFFF?text=LangChain+Pipeline%0ATemplate+→+Model+→+Parser+→+JSON) *[0:08:00 → 0:09:25]* ```typescript const parser = StructuredOutputParser.fromZodSchema(presentationSchema); const chain = template.pipe(model).pipe(parser); const result = await chain.invoke({ topic: "Фотографирование котят", format_instructions: parser.getFormatInstructions(), current_date: new Date().toISOString() }); ``` ![Демонстрация распарсенного JSON](https://via.placeholder.com/800x450/2B6CB0/FFFFFF?text=Результат+парсинга%0A%7B%0A++title%3A+%22Фотографирование+котят%22%0A++slides%3A+%5B...%5D%0A%7D) *[0:09:25 → 0:09:55]* ## Часть 2: Создание презентации с LangGraph Для генерации самой презентации простого LangChain недостаточно. Нужны: - Поиск в интернете для актуальной информации - Генерация изображений для каждого слайда - Создание контента с учётом контекста - Условная логика и роутинг ![Демонстрация аналога сервиса gamma.app](https://via.placeholder.com/800x450/4A5568/FFFFFF?text=Клон+Gamma.app%0AГенерация+презентации%0Aпо+готовому+плану) *[0:09:55 → 0:10:57]* Здесь на помощь приходит **LangGraph**. ## Архитектура LangGraph: от нод к агентам ![Слайд с объяснением работы LangGraph](https://via.placeholder.com/800x450/2B6CB0/FFFFFF?text=LangGraph+Architecture%0ANodes+→+Edges+→+State+→+Graph) *[0:10:57 → 0:26:20]* ### Основные концепции LangGraph работает с **нодами** (nodes) — функциями, которые выполняют конкретные задачи: - Каждая нода получает состояние и обновляет его - Между нодами есть рёбра (edges) для управления потоком - Возможен условный роутинг и циклы - Единое состояние передаётся между всеми нодами ### Архитектура моего клона Я создал компактный и эффективный граф из нескольких нод: 1. **Prepare** — подготовка данных и присвоение ID слайдам 2. **Router** — решение, нужен ли веб-поиск 3. **WebSearch** — поиск актуальной информации 4. **Generate Images** — генерация изображений 5. **Generate Content** — создание контента слайдов 6. **Final** — финализация результата ### Нода подготовки данных ```typescript async function prepareNode(state: GraphState): Promise> { const presentation = state.presentation; // Присваиваем ID каждому слайду presentation.slides.forEach((slide, index) => { slide.id = `slide-${index}`; }); return { presentation }; } ``` ### Условный роутинг Для принятия решений использую **Conditional Edge**: ```typescript function routeToWebSearch(state: GraphState): string[] { const sends = []; state.presentation.slides.forEach(slide => { if (slide.webSearchQuery) { sends.push( new Send("webresearch", { slide }) ); } }); return sends.length > 0 ? sends : ["generate"]; } ``` ### Веб-поиск с Tavily Для поиска актуальной информации использую Tavily API: ```typescript async function webSearchNode(data: { slide: Slide }): Promise> { const tavily = new TavilySearchResults({ maxResults: 5, searchDepth: "advanced" }); const results = await tavily.search(data.slide.webSearchQuery); // Экранируем фигурные скобки в коде const cleanResults = results.replace(/\{/g, '{{').replace(/\}/g, '}}'); return { webSearchResults: { [data.slide.id]: cleanResults } }; } ``` **Важный момент**: LangChain воспринимает фигурные скобки как переменные шаблона, поэтому код нужно экранировать двойными скобками. ### Генерация изображений с Kandinsky Для генерации изображений использую российскую нейросеть Kandinsky через API: ```typescript async function generateImageNode(data: { slide: Slide, imageStyle: string }): Promise> { const kandinsky = new KandinskyAPI(); const imagePrompt = `${data.slide.imagePrompt}, ${data.imageStyle}`; const image = await kandinsky.generateImage({ prompt: imagePrompt, negativePrompt: data.slide.negativeImagePrompt, width: data.slide.type === 'title' ? 512 : 768, height: data.slide.type === 'title' ? 512 : 432 }); return { generatedImages: { [data.slide.id]: image.url } }; } ``` ### Генерация контента с сохранением контекста Самая интересная часть — создание контента с учётом предыдущих слайдов: ```typescript async function generateContentNode(state: GraphState): Promise> { const messages = [ { role: 'system', content: 'Ты создаёшь контент для слайдов презентации...' } ]; for (const slide of state.presentation.slides) { // Добавляем запрос на генерацию контента messages.push({ role: 'user', content: `Создай контент для слайда "${slide.title}". Используй найденную информацию: ${state.webSearchResults[slide.id] || ''} Основные пункты: ${slide.bullets.join(', ')}` }); // Получаем ответ от LLM const response = await model.invoke(messages); messages.push({ role: 'assistant', content: response }); // Запрашиваем комментарий для спикера messages.push({ role: 'user', content: 'Добавь комментарий для спикера: что рассказывать по этому слайду?' }); const speakerNotes = await model.invoke(messages); messages.push({ role: 'assistant', content: speakerNotes }); // Сохраняем результат slide.content = response; slide.speakerNotes = speakerNotes; } return { presentation: state.presentation }; } ``` Такой подход **сохраняет контекст** между слайдами, позволяя создавать логически связанные презентации. ### Сборка графа воедино ```typescript const graph = new StateGraph(GraphState) .addNode("prepare", prepareNode) .addNode("webresearch", webSearchNode) .addNode("generateImages", generateImageNode) .addNode("generateContent", generateContentNode) .addNode("final", finalNode) .setEntryPoint("prepare") .addConditionalEdges("prepare", routeToWebSearch) .addEdge("webresearch", "generateImages") .addEdge("generateImages", "generateContent") .addEdge("generateContent", "final") .setFinishPoint("final"); const app = graph.compile(); // Запуск генерации const result = await app.invoke({ presentation: planFromLangChain }); ``` ## Технологический стек - **LangChain** — для простых цепочек обработки - **LangGraph** — для сложных сценариев с условными переходами - **Zod** — для структурированного вывода - **Tavily** — для веб-поиска - **Kandinsky API** — для генерации изображений - **TypeScript** — для типизации и надёжности ## Оптимизация и экономика ### Параллельное выполнение LangGraph позволяет запускать ноды параллельно. Например, генерация изображений для разных слайдов происходит одновременно, что значительно ускоряет процесс. ### Стоимость генерации Примерная стоимость создания презентации из 8 слайдов: - Планирование: ~$0.02 - Веб-поиск: ~$0.01 - Генерация изображений: ~$0.06 - Создание контента: ~$0.02 **Итого: ~$0.11** себестоимость при продажной цене $2-5. ## Практические советы 1. **Экранируйте фигурные скобки** в коде, иначе LangChain будет пытаться их интерпретировать 2. **Используйте Zod** для гарантированной структуры ответов 3. **Сохраняйте контекст** между запросами для связности контента 4. **Тестируйте промпты** на разных темах и языках 5. **Добавляйте текущую дату** для актуальных тем ## Заключение Создание AI-агентов — это **не rocket science**. Фронт-энд разработчик может создать полноценный аналог коммерческого сервиса, используя современные инструменты. Главное — **пробовать и экспериментировать**. Это направление развивается очень быстро, и скоро каждый разработчик будет работать с AI-агентами. ![QR-код для доступа к приложению](https://via.placeholder.com/200x200/2D3748/FFFFFF?text=QR+Code%0AГенератор+презентаций) *QR-код для доступа к демо-версии клона Gamma.app* --- *Статья основана на докладе Александра Примакова, фронт-энд разработчика Сбера с 10-летним опытом в IT. Александр также преподаёт в университетах и успешно запустил курс по созданию AI-агентов для магистрантов КФУ.* ### Полезные ссылки - [LangChain Documentation](https://langchain.com/docs) - [LangGraph Tutorial](https://langchain.com/langgraph) - [Zod Schema Validation](https://zod.dev/) - [Tavily Search API](https://tavily.com/) - [Kandinsky API](https://fusionbrain.ai/) **Хэштеги:** #AI #LangChain #LangGraph #TypeScript #Презентации #Kandinsky #MachineLearning