articles/primakov.md
Primakov Alexandr Alexandrovich 76351e365e восстановил часть
2025-07-10 19:14:50 +03:00

16 KiB
Raw Blame History

Создаём клон Gamma.app: от промпта до AI-агента на LangGraph

Как фронт-энд разработчик из Сбера создал аналог популярного сервиса для генерации презентаций с помощью LangChain и LangGraph

Слайд с информацией о спикере [0:00:02 → 0:00:50]

Кто создаёт AI-сервисы сегодня?

В списке Forbes топ-50 AI-компаний есть множество знакомых названий: OpenAI, Cursor, Bolt New. Я вот обратил внимание на компанию Speak — они с 2016 года разрабатывают приложение для изучения языков с AI-помощником и попали в топ-50.

Слайд с компаниями, использующими AI [0:00:50 → 0:02:10]

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

Gamma.app: что внутри магии?

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

Демонстрация работы сервиса gamma.app [0:02:10 → 0:03:12]

Выглядит как волшебство, но давайте разберём, как создаются подобные проекты.

Декомпозиция задачи

Чтобы создать аналог Gamma.app, нужно решить две основные задачи:

Слайд с задачами для решения [0:03:12 → 0:04:15]

  1. Генерировать план презентации — структуру с заголовками и основными пунктами
  2. Создавать презентацию — контент, изображения и оформление на основе плана

Часть 1: Генерация плана с LangChain

Промпт-инжиниринг в реальности

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

Слайд с промптом для генерации презентации [0:05:40 → 0:06:23]

Мой промпт включает:

  • Инструкции по созданию плана
  • Описание доступных типов слайдов (титульный, контентный и т.д.)
  • Требования к формату JSON
  • Текущую дату (важно для актуальных тем)

Шаблоны в LangChain

LangChain предоставляет механизм шаблонов для удобной работы с промптами:

import { PromptTemplate } from "@langchain/core/prompts";

const promptTemplate = PromptTemplate.fromTemplate(
  `Создай план презентации на тему {topic}.
Учти следующие требования: {requirements}
Текущая дата: {current_date}
{format_instructions}`
);

await promptTemplate.invoke({
  topic: "cats"
  requirements: '...',
  current_date: new Date().toISOString(),
  format_instructions: '...'
});

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

Структурированный вывод с Zod

Для надёжной работы с JSON-ответами использую Zod для описания схемы:

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("Запрос для поиска в интернете")
  })).describe('Список слайдов презентации')
});

Создание цепочки LangChain

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 [0:09:25 → 0:09:55]

Часть 2: Создание презентации с LangGraph

Для генерации самой презентации простого LangChain недостаточно. Нужны:

  • Поиск в интернете для актуальной информации
  • Генерация изображений для каждого слайда
  • Создание контента с учётом контекста
  • Условная логика и роутинг

Демонстрация аналога сервиса gamma.app план Демонстрация аналога сервиса gamma.app слайды

Здесь на помощь приходит LangGraph.

Архитектура LangGraph: от нод к агентам

Основные концепции

LangGraph работает с нодами (nodes) — функциями, которые выполняют конкретные задачи:

  • Каждая нода получает состояние и обновляет его
  • Между нодами есть рёбра (edges) для управления потоком
  • Возможен условный роутинг и циклы
  • Единое состояние передаётся между всеми нодами

Архитектура моего клона

Слайд с графом клона gamma.app

Я создал компактный и эффективный граф из нескольких нод:

  1. Prepare — подготовка данных и присвоение ID слайдам
  2. Router — решение, нужен ли веб-поиск
  3. WebSearch — поиск актуальной информации
  4. Generate Images — генерация изображений
  5. Generate Content — создание контента слайдов
  6. Final — финализация результата

Нода подготовки данных

async function prepareNode(state: GraphState): Promise<Partial<GraphState>> {
  const presentation = state.presentation;
  
  // Присваиваем ID каждому слайду
  presentation.slides.forEach((slide, index) => {
    slide.id = uuidv4();
  });
  
  return { presentation };
}

Условный роутинг

Для принятия решений использую Conditional Edge:

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:

async function webSearchNode(data: { slide: Slide }): Promise<Partial<GraphState>> {
  const client = await tavily({ apiKey: process.env['TAVILY_API_KEY'] })

  const researchResult = await client.search(slide.webResearchQuery, {
    maxResults: 3,
  })

  return {
    webResearchResult: {
      [slide.id as string]: researchResult.results
        .map(r => r.content?.replace(/\{/g, '{{').replace(/\}/g, '}}'))
        .join('\n\n')
    }
  };
}

Важный момент: LangChain воспринимает фигурные скобки как переменные шаблона, поэтому код нужно экранировать двойными скобками.

Генерация изображений с Kandinsky

Для генерации изображений использую российскую нейросеть Kandinsky через API:

Для работы с api я написал простенькую обёртку для упрощения работы @brojs/kandinsky

async function generateImageNode(data: { 
  slide: Slide, 
  imageStyle: string 
}): Promise<Partial<GraphState>> {
  const kandinsky = new KandinskyAPI();
  const { imagePrompt, imageNegativePrompt } = slide

  const imageSize = {
    width: data.slide.type === 'title' ? 512 : 768,
    height: data.slide.type === 'title' ? 512 : 432
  }

  const images = await generateKandinskyImage({
    imagePrompt,
    imagesStyle,
    ...imageSize,
    imageNegativePrompt,
  })
  
  return {
    generatedImages: {
      [data.slide.id]: image.url
    }
  };
}

Генерация контента с сохранением контекста

Самая интересная часть — создание контента с учётом предыдущих слайдов:

async function generateContentNode(state: GraphState): Promise<Partial<GraphState>> {
  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 };
}

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

Сборка графа воедино

const builder = new StateGraph(agentState)
  .addNode("prepare", prepareData)
  .addNode('webResearch', webResearch)
  .addNode('toGenerate', () => ({}))
  .addNode('final', state => state)
  .addNode('imageGenerator', imageGenerator)
  .addNode('generateSlideContent', generateSlideContent)

builder
  .addEdge(START, "prepare")
  .addConditionalEdges("prepare", afterPrepareRouter)
  .addConditionalEdges("toGenerate", generateImagesAndFirstContent)
  .addConditionalEdges("generateSlideContent", toGenerateNextSlideContent)
  .addEdge('webResearch', 'toGenerate')
  .addEdge('final', END);

const app = builder.compile();

// Запуск генерации
const result = await app.invoke({ 
  presentation: planFromLangChain 
});

Технологический стек

  • LangChain — для простых цепочек обработки
  • LangGraph — для сложных сценариев с условными переходами
  • Zod — для структурированного вывода
  • Tavily — для веб-поиска
  • Kandinsky API — для генерации изображений
  • TypeScript — для типизации и надёжности

Оптимизация и экономика

Параллельное выполнение

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

Стоимость генерации

Примерная стоимость создания презентации из 8 слайдов около 20-30 Р

Практические советы

  1. Экранируйте фигурные скобки в коде, иначе LangChain будет пытаться их интерпретировать
  2. Используйте Zod для гарантированной структуры ответов
  3. Сохраняйте контекст между запросами для связности контента
  4. Тестируйте промпты на разных темах и языках
  5. Добавляйте текущую дату для актуальных тем

Заключение

Создание AI-агентов — это не rocket science. Фронт-энд разработчик может создать полноценный аналог коммерческого сервиса, используя современные инструменты.

Главное — пробовать и экспериментировать. Это направление развивается очень быстро, и скоро каждый разработчик будет работать с AI-агентами.

Ссыылка на рабочий прототип


Статья основана на докладе Александра Примакова, фронт-энд разработчика Сбера с 10-летним опытом в IT. Александр также преподаёт в университетах и успешно запустил курс по созданию AI-агентов для магистрантов КФУ.

Полезные ссылки

Хэштеги: #AI #LangChain #LangGraph #TypeScript #Презентации #Kandinsky #MachineLearning