Exit-интервью на LLM: что пользователи говорят в момент закрытия пейволла (схема, промпты, грабли)

TL;DR. Когда игрок закрывает окно оплаты в моей игре, с ним заговаривает отдельный LLM-бот — короткое custdev-интервью по книге «Спроси маму», максимум 5 реплик. За 2 недели: 59 сессий, 20 ответили (34%). Главная находка: заметная часть людей шла из поиска со словом «бесплатно» в моём же title. Ниже отдам вам архитектуру, промпты и код-каркас под любой проект — и расскажу, что это дало.

Два слова о продукте, чтобы вы понимали цитаты

Я делаю онлайн-версию «Лилы» — настольной игры самопознания с ИИ ведущим. Игрок формулирует личный запрос, бросает кубик, ИИ-ведущий ведёт партию по полю и задаёт вопросы. Фаза формулировки запроса бесплатная; дальше игра просит оплату. Классический freemium с пейволлом — на его месте у вас может быть корзина, тариф или подписка, схема ниже от этого не меняется.

Момент, который все теряют

Пользователь дошёл до пейволла и закрыл его. В аналитике это событие paywall_dismissed. Причин там нет.

Опросник в этот момент у меня не работал: человек раздражён, галочки «дорого / не сейчас» ставит не глядя. Мои письма «почему вы не купили?» собирал почти ноль ответов.

А человек ещё на странице. И у него есть причина — прямо сейчас и она горячая!

Я сделал так: закрыл модалку — открывается отдельный чат. Не игровой. Другой визуальный стиль, другая роль: «Не как ведущий игры — просто интересно, что сейчас произошло?»

Панель разговора с Ведущим-исследователем после закрытия пейволла

Панель разговора с Ведущим-исследователем после закрытия пейволла

Отвечают 34%. Для custdev-канала с нулевой стоимостью привлечения респондента это круто.

Правила CustDev-а я взял из книги «Спроси маму»

Роб Фитцпатрик, «The Mom Test». Суть: мнения бесполезны, факты бесценны. Пять правил из книги я перенёс прямо в системный промпт:

  1. Спрашивать о случившемся, не о будущем. «Что произошло, когда открылось окно?» — да. «Купили бы вы, если…» — мусор: о будущем люди врут, не со зла.

  2. «Не сейчас» — вежливое «нет». В заметке для меня это не «тёплый лид».

  3. За «нет денег» копать жизнь, а не защищать цену. Что человек уже делал со своей темой? Платил психологу? Покупал курсы? Это данные о вашем рынке.

  4. Комплимент — форма отказа. «Всё понравилось» → одно тёплое слово и назад к фактам.

  5. Не вести клиента. Не пересказывать ответы «правильными словами». Ваша догадка — не факт.

Шестое правило я выстрадал сам. В первой версии бот начинал с «я ничего не продаю и не уговариваю». Убрал: фраза «я не продаю» звучит ровно наоборот — как продажа. Рамку задаёт первый вопрос. После правки доля ответивших не изменилась, а тон их ответов стал спокойнее.

И финальный вопрос каждого интервью, тоже из книги: «О чём мне стоило спросить, но я не спросил?» Он приносит непрошеное. Одна из игравших ответила: «Вы рано про оплату спрашиваете — технически можно позже, когда человек уже много выложит о себе». Готовая продуктовая гипотеза ;)

Как это устроено

Поток простой:

закрыл пейволл → панель ResearchChat → POST /api/games/:id/research
  action=start   → создать/возобновить сессию, стрим первого вопроса
  action=message → реплика игрока (кап 5) → стрим ответа
  action=end     → завершение → LLM-заметка + транскрипт → мне в Telegram

Решения, которые потом спасли:

  • Отдельные таблицы и отдельный эндпоинт. Research-диалог не пишется в игровой корпус сообщений и не трогает freemium-гейт. Данные интервью не загрязняют данные продукта.

  • Одна сессия на объект, idempotent. INSERT … ON CONFLICT DO NOTHING по subject_id (у меня это id игры). Повторное закрытие модалки возобновляет тот же диалог.

  • Кап реплик — в транзакции. Инкремент счётчика с гардом user_replies < 5 и вставка реплики в одной транзакции. Row lock закрывает гонку параллельных отправок; без него потолок пробивается, а при падении вставки вы теряете последнюю реплику, хотя счётчик уже съеден.

  • Никакого крона. Брошенные сессии закрывает попутный sweep: любой research-запрос fire-and-forget добивает чужие диалоги, где 30 минут тишины:

export async function sweepStaleSessions(): Promise<void> {
  const cutoff = new Date(Date.now() - 30 * 60 * 1000);
  const stale = await db.select({ id: sessions.id }).from(sessions)
    .where(and(
      eq(sessions.status, "active"),
      lt(sessions.lastActivityAt, cutoff),
    ))
    .limit(10);
  for (const s of stale) {
    await completeSession(s.id, "stale").catch(logWarn);
  }
}
// в роуте: void sweepStaleSessions().catch(() => {});

У меня процесс долгоживущий (Docker), поэтому fire-and-forget живёт спокойно. В serverless заворачивайте вызов в waitUntil — там он умирает вместе с воркером.

  • Заметка — вторым LLM-вызовом. По завершении сессии транскрипт прогоняется через промпт-аналитик. На выходе JSON: summary, signal (причина не-оплаты одним словом), payment_tech_issue, emotion, tried_before, quotes (только дословные цитаты), insight.

Заметка exit-ресёрча в Telegram: сигнал, инсайт и цитата игрока

Заметка exit-ресёрча в Telegram: сигнал, инсайт и цитата игрока
  • Канарейка техфейлов. Поле payment_tech_issue триггерит отдельный срочный алерт — я не жду, пока дочитаю ленту заметок. Один такой алерт уже окупил всю фичу: «Евро, закончились международные карты» — человек хотел заплатить в рублях и не смог. Заметность выбора валюты я починил за 20минут.

  • Пересылка — с атомарным клеймом. forwarded_at IS NULL → NOW() перед отправкой; при фейле клейм снимается. Дублей нет, «типа отправленных» нет.

Каркас — забирайте под свой проект

Минимум, который переносится куда угодно. Стек мой — TypeScript + Postgres + LLM с fallback’ом; подставьте свой.

CREATE TABLE research_sessions (
  id           serial PRIMARY KEY,
  subject_id   int  NOT NULL UNIQUE,     -- игра/заказ/корзина — ваш объект
  user_replies int  NOT NULL DEFAULT 0,
  status       text NOT NULL DEFAULT 'active',  -- active|completed
  forwarded_at timestamptz,
  last_activity_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE research_messages (
  id serial PRIMARY KEY,
  session_id int NOT NULL REFERENCES research_sessions(id),
  role text NOT NULL,         -- assistant|user
  content text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);
const REPLY_CAP = 5;

// action=message. s — сессия из БД, content — текст реплики пользователя.
// Кап и вставка — одна транзакция, иначе гонка.
const row = await db.transaction(async (tx) => {
  const [r] = await tx.update(sessions)
    .set({ userReplies: sql`user_replies + 1`, lastActivityAt: new Date() })
    .where(and(
      eq(sessions.id, s.id),
      eq(sessions.status, "active"),
      lt(sessions.userReplies, REPLY_CAP),
    ))
    .returning();
  if (!r) return null;                    // потолок ИЛИ сессия уже закрыта
  await tx.insert(messages).values({ sessionId: s.id, role: "user", content });
  return r;
});
if (!row) return json({ error: "capped_or_closed" }, 409);

Системный промпт — шаблон с тремя дырками. Всё в фигурных скобках замените под себя:

Ты — {голос вашего продукта: у меня — ведущий игры, у вас — ассистент
магазина, поддержка сервиса}. Пользователь только что {момент отказа:
закрыл пейволл / бросил корзину / отменил подписку} и остался на странице.
Ты выходишь из обычной роли: тебе по-человечески интересно, что он только
что пережил. Ты ничего не предлагаешь и не уговариваешь — но НЕ произноси
этого вслух: рамку задают твои вопросы.

КОНТЕКСТ: {что пользователь видел и делал до отказа, коротко. У меня —
первые слова игрока и его последние 2-3 реплики. У вас — карточка товара,
состав корзины, тариф, история тикета. НЕ вся история: боту хватит сцены,
целиком он начнёт её пересказывать}

ПОНЯТЬ ПО ПОРЯДКУ:
1. Момент «только что»: что увидел, что почувствовал, что сделал. Его словами.
2. Что продукт уже дал ему к этому моменту.
3. Что он делал со своей задачей раньше, до вас, и во что это ему обходилось.

ПРАВИЛА: один короткий вопрос за раз · о случившемся, не о будущем ·
«не сейчас» = «нет», не дожимать · за «дорого» копать жизнь, не защищать
цену · похвала = одно тёплое слово и назад к фактам · не пересказывать
его ответы своими словами · техническая проблема оплаты = главный сигнал,
уточнить детали · два односложных ответа подряд — завершай тепло ·
финал: «о чём мне стоило спросить, но я не спросил?»

Промпт заметки — второй вызов, вход = транскрипт:

Ты — аналитик custdev-интервью. Составь честную заметку для основателя.
Не додумывай: нет причины — так и пиши. Судить только по словам
пользователя. «Подумаю» = вежливое «нет», не интерес.
JSON: {"summary": "...", "signal": "price|timing|got_enough|trust|
tech_payment|unclear_offer|no_answer|other", "payment_tech_issue": bool,
"emotion": "...", "tried_before": "yes|no|unknown",
"quotes": ["дословно, не редактируя"], "insight": "..."}

Дальше — вебхук в Telegram/Slack: заметка сверху, сырой транскрипт снизу.

Про данные. Диалог уходит в LLM-провайдера и в Telegram без имени, почты и каких-либо идентификаторов — только текст реплик и номер сессии; это покрыто политикой обработки данных сервиса. В цитатах здесь я дополнительно изменил формулировки до неузнаваемости конкретного человека — смысл не тронут.

Грабли

Злость на бота. «Зачем я с роботами начала общение — позорно». Человек пришёл играть, наткнулся на пейволл, а тут ещё разговоры. Это честная цена формата, и я её принял. Ответ — архитектурный: жёсткий кап, завершение после двух односложных ответов, ноль попыток удержать или допродать. И кризис-протокол: если в ответах тяжёлое — бот бросает исследование и ведёт к живой помощи.

Бот-интервьюер не универсален. Части людей проще выдать боту больше, чем человеку: ниже порог стыда. Часть принципиально не разговаривает с «роботами». 34% — те, кому формат подошёл. Остальные 66% вам ничего не должны.

Что это дало за 2 недели

Раскладка причин по 20 ответившим:

  • «Ожидал бесплатно» — 7. Доминанта с отрывом. Ещё в одном ответе та же нота: злость, «что ничего не бывает бесплатно».

  • Нет денег — 3.

  • Ценность не успела раскрыться до пейволла — 3.

  • Техфейл оплаты — 1.

  • Философское «за деньги это не купишь» — 2.

  • Прочее — единицы.

  • И одна обратная: «Почему так дёшево. Ценность игры выше» :)

20 интервью — это уже сигнал, не статистика. Гипотезы отсюда проверяются метриками, ниже покажу как.

Теперь главное. Откуда «бесплатно»? Смотрю атрибуцию: все 59 сессий — поиск и прямые заходы, ни одной с моих лендингов (срез чисто органический). Иду в git своей же посадочной — и вот оно: некоторое время назад SEO-проход добавил в title главной «играть бесплатно». Запрос частотный, CTR вырос. Поиск приводил людей на обещание, которое продукт выполняет только наполовину: бесплатна первая фаза, дальше пейволл.

Аналитика показывала мне это как «низкая конверсия органики». Причину назвала одна реплика из exit-интервью: «я думала игра бесплатная, так как гуглила именно бесплатно». Другие формулировки того же: «в середине игры изменились правила», «ощущение обмана».

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

Я строил этот инструмент, чтобы понять чужие возражения. Первым делом он вскрыл мою собственную ошибку. Лучшее доказательство, что это работает.

Когда это стоит делать вам

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

Не подойдёт как замена живым интервью на этапе поиска продукта: бот хорош в горячем «почему нет» здесь и сейчас, глубинные интервью делать лучше живым общением.

Себестоимость: Час вайбкода, две таблицы, один эндпоинт, два промпта, вебхук. Данные бесценны :)

Я делаю онлайн-версию игры «Лила» с ИИ-ведущим — quantumgame.love. Пейволл, о котором речь выше, оттуда.

Автор: PsyGab

Источник

Оставить комментарий