articles/primakov.md
2025-07-07 16:27:01 +03:00

17 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

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

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

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

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

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

Шаблоны в LangChain

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

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

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

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

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

Слайд с описанием JSON схемы [0:06:23 → 0:08:00]

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

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

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

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

Слайд с объяснением работы LangGraph [0:10:57 → 0:26:20]

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

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

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

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

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

  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 = `slide-${index}`;
  });
  
  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 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:

async function generateImageNode(data: { 
  slide: Slide, 
  imageStyle: string 
}): Promise<Partial<GraphState>> {
  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
    }
  };
}

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

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

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 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-код для доступа к приложению

QR-код для доступа к демо-версии клона Gamma.app


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

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

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