Разбор архитектуры AI-продукта на Python: мультиагентный пайплайн, RAG, честные метрики и 3 ключевых компромисса. Читайте и применяйте!
Я потратил 18 дней на создание AI-продукта, который превращает научные статьи в презентации PowerPoint под конкретную аудиторию. Это не игрушка, а реально работающий сервис на Railway, доступный каждому. Самое интересное — не моменты «заработало», а честные компромиссы, на которые мне пришлось пойти, и моменты, когда я устоял перед соблазном добавить «умное» решение, которое только ухудшило бы ситуацию. Эта статья — о тех решениях.
Doc2Slides принимает PDF и генерирует файл .pptx, адаптированный под четыре аудитории: ребёнок (весёлые аналогии, простые слова), студент (обучающий материал, определения), инженер (техническая глубина, знание предметной области), руководитель (бизнес-фокус, влияние). Магия в том, что одна и та же статья даёт radically разный результат в зависимости от аудитории. Статья по теории компиляторов для ребёнка превращается в «компиляторы — это волшебные помощники», а для руководителя — в «продвижение технологии компиляторов с помощью формальных фреймворков».
Я построил мультиагентный пайплайн вместо одного гигантского промпта. Поток данных: загрузка PDF → парсер (извлекает разделы и метаданные) → суммаризатор (RAG-суммаризация разделов) → планировщик (проектирует структуру слайдов для аудитории) → писатель (генерирует адаптивный контент) → сборщик (создаёт редактируемый .pptx). Каждый агент — независимый узел в графе состояний LangGraph, они разделяют состояние TypedDict и читают/пишут определённые поля. Вот как выглядит определение графа:
from langgraph.graph import StateGraph, END
from app.agents.state import AgentState
from app.agents.parser import parser_agent
from app.agents.summarizer import summarizer_agent
from app.agents.planner import planner_agent
from app.agents.writer import writer_agent
from app.agents.builder import builder_agent
def build_pipeline():
graph = StateGraph(AgentState)
graph.add_node("parser", parser_agent)
graph.add_node("summarizer", summarizer_agent)
graph.add_node("planner", planner_agent)
graph.add_node("writer", writer_agent)
graph.add_node("builder", builder_agent)
graph.set_entry_point("parser")
graph.add_edge("parser", "summarizer")
graph.add_edge("summarizer", "planner")
graph.add_edge("planner", "writer")
graph.add_edge("writer", "builder")
graph.add_edge("builder", END)
return graph.compile()Почему LangGraph, а не последовательная цепочка? Добавление нового агента — это изменение двух строк в графе. В последовательной цепочке добавление шага часто требует рефакторинга предыдущих. Государственный мультиагентный подход масштабируется лучше.
Я создал оценочный стенд, потому что хотел измерять качество, а не просто заявлять о нём. Три типа оценок: парсер (детерминированные проверки ground-truth), RAG (top-K точность на вручную размеченных парах запрос-раздел), суммаризатор (LLM-судья оценивает точность, полноту, ясность). Парсер показал 100% (34/34 проверок), суммаризатор — 4.4/5. Но RAG top-1 точность составила 42%: только 3 из 7 запросов вернули правильный раздел как лучший результат. Моя первая мысль: скрыть число, сообщить top-3 (57%). Вместо этого я опубликовал оба числа и объяснил, почему так вышло. Анализ ошибок выявил реальное ограничение RAG: запрос «как работает генетический алгоритм?» ожидал раздел «Методология», а получил подраздел «3.6 Критерии остановки». Генетические алгоритмы обсуждаются в шести подразделах (3.1–3.6). Векторный поиск возвращает чанк с наивысшим баллом, а не раздел. Для запросов по широким темам подразделы часто обходят родительский раздел, потому что упоминают конкретный термин плотнее. Это известная проблема RAG. Решения включают иерархический поиск, переписывание запросов, выбор раздела через LLM. Ни одно из них пока не реализовано, но я точно знаю, что сломано и почему — это полезнее, чем притворяться, что всё работает. Урок: детерминированные метрики лучше ощущений. Ощущения позволяют убедить себя, что AI умный. Метрики показывают, где он глупый.
Пользователи могут запросить любое количество слайдов от 3 до 50. Когда фактическая плотность контента не соответствует запрошенному числу слайдов, LLM либо раздувает короткие разделы, либо сжимает плотные. Это создаёт лёгкую избыточность при большом числе слайдов. Очевидное решение: распределять слайды на основе количества слов в разделе. Длинный раздел — больше слайдов, короткий — меньше. Я почти построил это, но осознал: количество слов — это не плотность контента. Раздел из 100 слов с тремя разными концепциями должен получить несколько слайдов, а раздел из 2000 слов, болтающий вокруг одной идеи, — один слайд. Количество слов систематически вознаграждало бы многословные разделы и наказывало краткие. Это не фикс, а баг с математикой. Вместо этого я задокументировал компромисс и выпустил без эвристики. В testing_notes.md проекта написано: «Отклонено быстрое решение: использовать количество слов в разделе как прокси для плотности контента. Количество слов — не плотность: короткий раздел может содержать несколько разных идей, а длинный — болтать вокруг одной. Правильное решение отложено: распределение слайдов с учётом контента с помощью LLM, проверенное оценочным стендом. Требует инфраструктурной работы, не подходящей для первой версии». Урок: правильный ответ на вопрос «стоит ли добавить эту эвристику?» часто «нет». Эвристики ощущаются как прогресс. Иногда это анти-прогресс, замаскированный под прагматизм.
Я разрабатывал локально с SQLite, но задеплоил на Railway с PostgreSQL. Миграция заняла одну строку: DATABASE_URL = os.getenv("DATABASE_URL"). Для локальной разработки в .env: DATABASE_URL=sqlite:///./doc2slides.db. Для Railway переменная окружения: DATABASE_URL=postgresql+psycopg2://postgres:xxx@host:5432/railway. Больше ничего не меняется. Модели SQLAlchemy независимы от бэкенда. Это скучная инженерия. Но скучная инженерия позволяет спать по ночам. Когда кто-то спрашивает, как вы обрабатываете миграции БД, ответ — не хитрый хак, а «конфигурация через переменные окружения и паттерн репозитория».
Фронтенд стоит отметить отдельно. Я не использовал фреймворк — только HTML, CSS и ванильный JavaScript в ~500 строк. Ноль шага сборки. Любой может склонировать репозиторий, открыть файл и понять его за 5 минут. Для MVP это фича, а не ограничение.
Я пропустил: аутентификацию пользователей, мультитенантные рабочие пространства, пользовательские шаблоны презентаций, стриминг ответов, очередь задач с Celery/Redis. Причина: MVP. Каждая фича имеет стоимость. Доставка основной ценности (PDF → слайды под аудиторию) важнее, чем доставка всех возможных фич. Для портфолио-проекта ответ «я мог добавить X, но не стал по таким-то причинам» сильнее, чем «я добавил X плохо».
Проект жив, но не закончен. Будущая работа: распределение слайдов с учётом контента (с оценочным стендом), поддержка многоязычных PDF, пользовательские шаблоны презентаций, исправление RAG для иерархических разделов (подраздел → родитель). Если хотите попробовать Doc2Slides: живое демо на web-production-6eded.up.railway.app, код на github.com/manasviboineypally/doc2slides, 60-секундное видео на Loom. Загрузите любой PDF, выберите аудиторию, получите презентацию. Одна и та же статья — radically разный результат в зависимости от того, кому вы представляете.
Прямо сейчас: возьмите свой проект и честно оцените, какие метрики вы используете для измерения качества. Если вы полагаетесь на «ощущения», создайте хотя бы простой оценочный стенд. Затем задокументируйте компромиссы, которые вы приняли, — это будет ценнее, чем любой код. И, наконец, задеплойте свой проект раньше, чем планировали. Деплой вскроет реальные проблемы, о которых вы даже не подозревали.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →