П/ВИН

v0-german: генератор КП и диагностик для бизнес-школы

·9 мин чтения
v0-german: генератор КП и диагностик для бизнес-школы

Представь ситуацию: менеджер только что завершил двухчасовой диагностический звонок с потенциальным клиентом. Впереди — ещё несколько встреч в этот день. И ему нужно вручную написать отчёт о текущей ситуации клиента, коммерческое предложение с правильным тарифом и персонализированную дорожную карту на несколько месяцев вперёд. Это легко может занять 2-3 часа. Умножь на количество диагностик в неделю — и получишь огромную дыру в продуктивности команды.

Именно такую задачу мы решали в проекте v0-german — платформе для бизнес-школы Германа Юна. В итоге получился полноценный AI-генератор документов, который превращает аудиозапись диагностического звонка в три готовых PDF за несколько минут.

Контекст: воронка продаж бизнес-школы

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

Диагностика — ключевой момент. Это живой разговор, где куратор выясняет текущее состояние бизнеса клиента: оборот, структуру отделов продаж, проблемы с дебиторкой, ситуацию с наймом и адаптацией сотрудников. После звонка нужно подготовить три документа:

  1. Отчёт диагностики — «Точка А» клиента: где он сейчас, какие системные проблемы, ключевые выводы
  2. Коммерческое предложение — с подходящим тарифом, обоснованием и программой
  3. Дорожная карта — персонализированный план работы по месяцам

Раньше всё это писалось вручную. Теперь — генерируется.

Архитектура решения

Проект построен на Next.js 14 с App Router, интеграцией OpenAI API (Whisper + GPT-4o) и Supabase в качестве базы данных. PDF-генерация реализована через @react-pdf/renderer — это позволяет получать настоящие векторные PDF без Chromium и Puppeteer.

Визард на странице /generator проведёт менеджера через четыре шага:

  • Шаг 1 — данные клиента + выбор тарифа
  • Шаг 2 — загрузка аудио или вставка транскрипции
  • Шаг 3 — просмотр и редактирование сгенерированных итогов
  • Шаг 4 — скачивание трёх PDF-документов

Сессии сохраняются в Supabase в таблице generator_sessions, что позволяет вернуться к любому клиенту позже и при необходимости провести аудит качества генерации.

Проблема первая: галлюцинации Whisper

Одна из первых боевых проблем — загадочная фраза «Спасибо за субтитры Алексею Дубровскому!», которая появлялась в транскрибациях. Это известная галлюцинация модели Whisper: когда на входе тишина, шум или неразборчивая речь, модель начинает генерировать популярные фразы из обучающих данных. В русскоязычном корпусе таких «меток» от переводчиков субтитров — огромное количество.

Решение простое, но неочевидное — параметр prompt в запросе к Whisper. Он задаёт контекст записи и резко снижает вероятность галлюцинаций:

const transcription = await openai.audio.transcriptions.create({
  file: audioFile,
  model: 'whisper-1',
  language: 'ru',
  prompt: 'Запись диагностического звонка с владельцем бизнеса. Обсуждаются продажи, финансы, управление персоналом.',
});

Одновременно подняли лимит загружаемых файлов с 100 МБ до 500 МБ — двухчасовые записи звонков весят прилично, и старый лимит регулярно блокировал работу. В Next.js App Router это решается через конфиг сегмента роута:

export const config = {
  api: {
    bodyParser: false,
  },
};
 
// next.config.js
module.exports = {
  experimental: {
    serverActions: {
      bodySizeLimit: '500mb',
    },
  },
};

При этом бэкенд уже умел разбивать файлы на чанки по 20 МБ для Whisper API — просто нужно было убрать фронтендовый барьер.

Проблема вторая: AI додумывает факты

Это самая серьёзная проблема, которую мы обнаружили при первых боевых тестах. При проверке итогов диагностики реального клиента выяснилось: GPT добавлял детали, которых не было в транскрипции. Конкретный пример — в цитате клиента появилась фраза «всё делается кустарным способом», хотя в реальной транскрипции этих слов не было. Клиент говорил о задержках и проблемах с арендой цеха — AI «дополнил» его мысль.

Для бизнес-документов это недопустимо. Если в КП написано что-то, чего клиент не говорил — это может разрушить доверие в самый важный момент продажи.

Решение двухуровневое.

Уровень 1: переписать промпт с жёсткими ограничениями

Исходный промпт был сформулирован как «составь отчёт на основе транскрипции». Новый промпт начинается с абсолютных запретов:

const DIAGNOSTIC_PROMPT = `
ТЫ — СТРОГИЙ АНАЛИТИК. ТВОЯ ЕДИНСТВЕННАЯ ЗАДАЧА — ИЗВЛЕЧЬ ФАКТЫ ИЗ ТРАНСКРИПЦИИ.
 
КРИТИЧЕСКИЕ ПРАВИЛА (нарушение = провал задачи):
1. ИСПОЛЬЗУЙ ТОЛЬКО то, что прямо сказано в транскрипции
2. ЗАПРЕЩЕНО добавлять выводы, которых нет в тексте
3. ЗАПРЕЩЕНО использовать слова клиента в другом контексте
4. Если информации недостаточно — напиши «не упоминалось в разговоре»
5. Цитаты — только дословные, из транскрипции
 
Если ты нарушишь эти правила — ты предашь свою роль аналитика.
`;

Жёсткая формулировка «предашь свою роль» — не случайна. Это техника из prompt engineering, которая активирует у модели механизм самоконтроля.

Уровень 2: ревьюер-агент

После первичной генерации документа к нему применяется второй вызов к API — агент-ревьюер. Он получает сгенерированный JSON и исходную транскрипцию, проверяет каждое поле на наличие в тексте и исправляет несоответствия:

// app/api/generator/generate-docs/route.ts
const generated = await generateDiagnosticDocs(transcription, clientData);
 
// Ревью-агент проверяет факты
const reviewed = await reviewGeneratedContent({
  generated,
  transcription,
  prompt: REVIEWER_PROMPT,
});
 
return NextResponse.json(reviewed);

Ревьюер работает на модели GPT-4o и получает конкретную инструкцию: «Найди все утверждения в generated, которых нет в transcription, и замени их либо точными цитатами, либо пометкой [не упоминалось]».

В git-истории видно отдельный коммит на переход ревьюера на gpt-5.4: chore: use gpt-5.4 for diagnostic reviewer agent — это позволило улучшить точность проверки фактов при сохранении скорости.

Обновление тарифов: от трёх к пяти

В процессе работы над продуктом изменилась сама продуктовая линейка школы. Исходные три тарифа (250к / 750к / 1М) были заменены на пять, отражающих реальную сегментацию клиентов:

| Тариф | Цена/мес | Срок | Итого | Для кого | |-------|----------|------|-------|----------| | Старт | 100 000 ₽ | 3 мес | 300 000 ₽ | Оборот до 40-50 млн | | Мастермайнд | 100 000 ₽ | 9 мес | 900 000 ₽ | Продолжение Старта | | Система + Продажи | 250 000 ₽ | 3 мес | 750 000 ₽ | Оборот 100-700 млн | | Бережливое производство | 250 000 ₽ | 3 мес | 750 000 ₽ | Производственный бизнес | | Мастермайнд Про | 250 000 ₽ | 9 мес | 2 250 000 ₽ | Крупный бизнес после курса |

Обновление прошло в трёх местах одновременно: промпты для AI-генерации КП в lib/generator/prompts.ts, данные в Supabase (таблица тарифов в схеме kp) и отображение на сайте. При этом в интерфейсе выбора тарифа менеджером добавили развёрнутые описания — не просто название и цена, а кому подходит тариф и что в него входит. Это важно: менеджер должен понимать, что предлагать конкретному клиенту.

Производительность: кэширование и bulk-экспорт

Отдельным блоком работ стала оптимизация экспорта PDF. В git-истории видно несколько итераций:

2aeccde refactor: pre-cached PDF downloads instead of on-the-fly generation
5b493e3 feat: async bulk PDF export with real progress bar for /20files
b6a84ac feat: retry failed PDFs up to 3 times with backoff
e34c4c4 fix: keep bulk zip download cache-only

Суть проблемы: генерация PDF «на лету» при каждом клике на скачивание — медленная и ненадёжная операция. Особенно для bulk-экспорта раздела /20files, где нужно сгенерировать и запаковать в ZIP несколько десятков документов.

Решение — предварительное кэширование. PDF генерируются асинхронно в фоне и сохраняются на диск. При клике на скачивание отдаётся уже готовый файл. Для bulk ZIP добавили реальный прогресс-бар с отображением текущего статуса генерации.

Одна из нетривиальных проблем — место хранения кэша. Изначально использовался cwd() (текущая директория процесса), что ломалось в Docker-контейнере при деплое. Переехали на os.tmpdir(), потом выяснилось, что /tmp не персистентен между перезапусками контейнера, и в итоге настроили writable runtime директорию вне tmp:

import path from 'path';
import os from 'os';
 
// Было:
const cacheDir = path.join(process.cwd(), '.cache', 'pdfs');
 
// Стало:
const cacheDir = path.join('/var/data', 'pdf-cache');
// Монтируется как persistent volume в Dokploy

Добавили retry-логику с экспоненциальным backoff для неудачных генераций — до 3 попыток с возрастающей паузой между ними.

Деплой и инфраструктура

Проект деплоится через Dokploy (self-hosted PaaS). В истории коммитов видна одна характерная проблема:

9b85a01 fix: use SHA-tagged image to force Dokploy fresh pull
211fa8f fix: deploy via SSH pull+force-update with keepalive

Dokploy иногда кэшировал старый Docker-образ и не подтягивал свежий при деплое. Решение — тегировать образ SHA коммита вместо latest, что гарантирует pull нового образа:

# В CI/CD pipeline
docker build -t app:${GITHUB_SHA} .
docker push registry/app:${GITHUB_SHA}

Проблема с keepalive возникла из-за того, что SSH-соединение разрывалось при долгих операциях pull на медленном канале. Добавили ServerAliveInterval в SSH-конфиг и флаг --timeout для docker pull.

Промпты в базе данных, а не в коде

Важное архитектурное решение — промпты для AI хранятся в Supabase, а не захардкожены в коде. Это позволяет обновлять инструкции без деплоя:

// app/api/generator/generate-docs/route.ts
const { data: settings } = await supabase
  .from('settings')
  .select('value')
  .eq('key', 'diagnostic_doc_prompt')
  .single();
 
const prompt = settings?.value !== 'USE_DEFAULT'
  ? settings.value
  : DEFAULT_DIAGNOSTIC_PROMPT;

Для разных типов документов — разные ключи: diagnostic_prompt для резюме встречи (используется в build-kp) и diagnostic_doc_prompt для итогов диагностики. Это позволяет тонко настраивать каждый тип документа через админ-панель.

Результаты

После нескольких недель боевого использования картина следующая:

  • Время подготовки документов: с 2-3 часов ручной работы до 5-7 минут в генераторе
  • Галлюцинации Whisper: полностью устранены после добавления контекстного промпта
  • Выдумки GPT: двухуровневая защита (жёсткий промпт + ревьюер-агент) снизила количество недостоверных фактов до минимума
  • Лимит файлов: поддержка записей до 500 МБ и длительностью 2+ часа
  • Стабильность деплоя: решена проблема с кэшированием Docker-образов

Выводы и уроки

Первый урок, который стоит запомнить: AI-галлюцинации в бизнес-документах — не технический баг, а продуктовый риск. Если клиент увидит в КП слова, которых он не говорил, доверие разрушится мгновенно. Простой жёсткий промпт с явными запретами работает значительно лучше, чем мягкие инструкции. А добавление второго агента-ревьюера — это страховочная сетка, которая ловит то, что проскочило сквозь первый промпт.

Второй урок: хранить промпты в базе данных, а не в коде — правильное решение с первого дня. Когда бизнес-логика меняется (а она меняется постоянно), обновить запись в Supabase через админку на порядок быстрее, чем делать коммит и деплой. Особенно критично это для документов, где качество напрямую влияет на конверсию в продажу.

Третий урок касается инфраструктуры: PDF-генерация должна быть асинхронной и кэшированной. Генерировать PDF по запросу пользователя — медленно и ненадёжно. Правильный паттерн — генерировать в фоне, хранить результат, отдавать по запросу. Retry с backoff обязателен для любых AI-операций: Whisper и GPT API иногда отвечают с задержкой или возвращают ошибку, и без retry пользователь получает сломанный UX.

Четвёртый урок — про Docker и персистентность: знай, куда пишешь данные в контейнере. cwd() ломается при деплое, /tmp не переживает перезапуск, process.cwd()/.cache может не иметь прав на запись. Единственное надёжное место — явно смонтированный persistent volume. Это элементарно, но на это наступают снова и снова.

В целом проект v0-german — хороший пример того, как AI-автоматизация реально экономит время, если правильно управлять качеством генерации. Ключевая ошибка большинства подобных проектов — доверять AI безоговорочно. Правильный подход: AI генерирует черновик, второй AI проверяет факты, и только потом документ идёт к пользователю. Это удваивает стоимость API-вызовов, но полностью оправдывает себя, когда документ используется в реальных продажах.


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

Паша Вин
Паша Вин

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.

Связанный проект

CJM Designer