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

358 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Создаём клон 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
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 для описания схемы:
```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("Запрос для поиска в интернете")
})).describe('Список слайдов презентации')
});
```
### Создание цепочки LangChain
```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](./primakov-article-images/json-generation.gif)
*[0:09:25 → 0:09:55]*
## Часть 2: Создание презентации с LangGraph
Для генерации самой презентации простого LangChain недостаточно. Нужны:
- Поиск в интернете для актуальной информации
- Генерация изображений для каждого слайда
- Создание контента с учётом контекста
- Условная логика и роутинг
![Демонстрация аналога сервиса gamma.app план](./primakov-article-images/bro-gamma-plan.gif)
![Демонстрация аналога сервиса gamma.app слайды](./primakov-article-images/bro-gamma-slides.gif)
Здесь на помощь приходит **LangGraph**.
## Архитектура LangGraph: от нод к агентам
### Основные концепции
LangGraph работает с **нодами** (nodes) — функциями, которые выполняют конкретные задачи:
- Каждая нода получает состояние и обновляет его
- Между нодами есть рёбра (edges) для управления потоком
- Возможен условный роутинг и циклы
- Единое состояние передаётся между всеми нодами
### Архитектура моего клона
![Слайд с графом клона gamma.app](./primakov-article-images/Слайд%20с%20графом%20клона%20gamma.app.PNG)
Я создал компактный и эффективный граф из нескольких нод:
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 = uuidv4();
});
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 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](https://gitverse.ru/primakov.a.a/kandinsky-js)
```typescript
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
}
};
}
```
### Генерация контента с сохранением контекста
Самая интересная часть — создание контента с учётом предыдущих слайдов:
```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 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-агентами.
[Ссыылка на рабочий прототип](https://platform.bro-js.ru/bro-gamma)
---
*Статья основана на докладе Александра Примакова, фронт-энд разработчика Сбера с 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