add article

This commit is contained in:
2025-07-07 16:27:01 +03:00
parent 65eab4986f
commit 6d077d05e9
4 changed files with 2922 additions and 0 deletions
+353
View File
@@ -0,0 +1,353 @@
# Создаём клон Gamma.app: от промпта до AI-агента на LangGraph
*Как фронт-энд разработчик из Сбера создал аналог популярного сервиса для генерации презентаций с помощью LangChain и LangGraph*
![Слайд с информацией о спикере](https://via.placeholder.com/800x450/2D3748/FFFFFF?text=Александр+Примаков%0A10+лет+в+IT%0A6+лет+в+Сбере)
*[0:00:02 → 0:00:50]*
## Кто создаёт AI-сервисы сегодня?
В списке Forbes топ-50 AI-компаний есть множество знакомых названий: OpenAI, Cursor, Bolt New. Особенно интересна компания Speak — они с 2016 года разрабатывают приложение для изучения языков с AI-помощником и попали в топ-50.
![Слайд с компаниями, использующими AI](https://via.placeholder.com/800x450/2D3748/FFFFFF?text=Forbes+Top+50+AI+Companies%0AOpenAI%2C+Cursor%2C+Bolt+New%2C+Speak)
*[0:00:50 → 0:02:10]*
Но главное открытие: **сейчас создавать AI-приложения можно довольно быстро**. Наши студенты делают продукты не хуже тех, что попадают в топ-рейтинги.
## Gamma.app: что внутри магии?
Gamma.app — это сервис для создания презентаций. Вы вводите тему, он создаёт план презентации с заголовками и буллетами для каждого слайда, а затем генерирует полную презентацию с изображениями и оформлением.
![Демонстрация работы сервиса gamma.app](https://via.placeholder.com/800x450/4A5568/FFFFFF?text=Gamma.app+Demo%0AВводим+тему+→+План+→+Презентация)
*[0:02:10 → 0:03:12]*
Выглядит как волшебство, но давайте разберём, как это работает изнутри.
## Декомпозиция задачи
Чтобы создать аналог Gamma.app, нужно решить две основные задачи:
![Слайд с задачами для решения](https://via.placeholder.com/800x450/2B6CB0/FFFFFF?text=Задачи%0A1.+Генерация+плана+презентации%0A2.+Создание+презентации+по+плану)
*[0:03:12 → 0:04:15]*
1. **Генерировать план презентации** — структуру с заголовками и основными пунктами
2. **Создавать презентацию** — контент, изображения и оформление на основе плана
## Часть 1: Генерация плана с LangChain
### Промпт-инжиниринг в реальности
Простой промпт в ChatGPT может создать план презентации, но для продакшена нужен более сложный подход:
![Слайд с промптом для генерации презентации](https://via.placeholder.com/800x450/2B6CB0/FFFFFF?text=Промпт+для+генерации%0A•+Уговариваем+создать+план%0A•+Описываем+типы+слайдов%0A•+Форматирование+в+JSON%0A•+Текущая+дата)
*[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<Partial<GraphState>> {
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<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:
```typescript
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
}
};
}
```
### Генерация контента с сохранением контекста
Самая интересная часть — создание контента с учётом предыдущих слайдов:
```typescript
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 };
}
```
Такой подход **сохраняет контекст** между слайдами, позволяя создавать логически связанные презентации.
### Сборка графа воедино
```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