- BrainTools - https://www.braintools.ru -
TL;DR. Когда игрок закрывает окно оплаты в моей игре, с ним заговаривает отдельный LLM-бот — короткое custdev-интервью по книге «Спроси маму», максимум 5 реплик. За 2 недели: 59 сессий, 20 ответили (34%). Главная находка: заметная часть людей шла из поиска со словом «бесплатно» в моём же title. Ниже отдам вам архитектуру, промпты и код-каркас под любой проект — и расскажу, что это дало.
Я делаю онлайн-версию «Лилы» — настольной игры самопознания с AI ведущим. Игрок формулирует личный запрос, бросает кубик, ИИ-ведущий ведёт партию по полю и задаёт вопросы. Фаза формулировки запроса бесплатная; дальше игра просит оплату. Классический freemium с пейволлом — на его месте у вас может быть корзина, тариф или подписка, схема ниже от этого не меняется.
Пользователь дошёл до пейволла и закрыл его. В аналитике это событие paywall_dismissed. Причин там нет.
Опросник в этот момент у меня не работал: человек раздражён, галочки «дорого / не сейчас» ставит не глядя. Мои письма «почему вы не купили?» собирал почти ноль ответов.
А человек ещё на странице. И у него есть причина — прямо сейчас и она горячая!
Я сделал так: закрыл модалку — открывается отдельный чат. Не игровой. Другой визуальный стиль, другая роль: «Не как ведущий игры — просто интересно, что сейчас произошло?»
Отвечают 34%. Для custdev-канала с нулевой стоимостью привлечения респондента это круто.
Роб Фитцпатрик, «The Mom Test». Суть: мнения бесполезны, факты бесценны. Пять правил из книги я перенёс прямо в системный промпт:
Спрашивать о случившемся, не о будущем. «Что произошло, когда открылось окно?» — да. «Купили бы вы, если…» — мусор: о будущем люди врут, не со зла.
«Не сейчас» — вежливое «нет». В заметке для меня это не «тёплый лид».
За «нет денег» копать жизнь, а не защищать цену. Что человек уже делал со своей темой? Платил психологу? Покупал курсы? Это данные о вашем рынке.
Комплимент — форма отказа. «Всё понравилось» → одно тёплое слово и назад к фактам.
Не вести клиента. Не пересказывать ответы «правильными словами». Ваша догадка — не факт.
Шестое правило я выстрадал сам. В первой версии бот начинал с «я ничего не продаю и не уговариваю». Убрал: фраза «я не продаю» звучит ровно наоборот – как продажа. Рамку задаёт первый вопрос. После правки доля ответивших не изменилась, а тон их ответов стал спокойнее.
И финальный вопрос каждого интервью, тоже из книги: «О чём мне стоило спросить, но я не спросил?» Он приносит непрошеное. Одна из игравших ответила: «Вы рано про оплату спрашиваете — технически можно позже, когда человек уже много выложит о себе». Готовая продуктовая гипотеза ;)
Поток простой:
закрыл пейволл → панель 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.
Канарейка техфейлов. Поле 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% вам ничего не должны.
Раскладка причин по 20 ответившим:
«Ожидал бесплатно» — 7. Доминанта [1] с отрывом. Ещё в одном ответе та же нота: злость, «что ничего не бывает бесплатно».
Нет денег — 3.
Ценность не успела раскрыться до пейволла — 3.
Техфейл оплаты — 1.
Философское «за деньги это не купишь» — 2.
Прочее — единицы.
И одна обратная: «Почему так дёшево. Ценность игры выше» :)
20 интервью — это уже сигнал, не статистика. Гипотезы отсюда проверяются метриками, ниже покажу как.
Теперь главное. Откуда «бесплатно»? Смотрю атрибуцию: все 59 сессий — поиск и прямые заходы, ни одной с моих лендингов (срез чисто органический). Иду в git своей же посадочной — и вот оно: некоторое время назад SEO-проход добавил в title главной «играть бесплатно». Запрос частотный, CTR вырос. Поиск приводил людей на обещание, которое продукт выполняет только наполовину: бесплатна первая фаза, дальше пейволл.
Аналитика показывала мне это как «низкая конверсия органики». Причину назвала одна реплика из exit-интервью: «я думала игра бесплатная, так как гуглила именно бесплатно». Другие формулировки того же: «в середине игры изменились правила», «ощущение обмана».
Что я с этим сделал: title уже переписан — теперь он обещает ровно то, что есть. Проверяю метрикой: доля сигнала «ожидал бесплатно» в новых exit-интервью ушла.
Я строил этот инструмент, чтобы понять чужие возражения. Первым делом он вскрыл мою собственную ошибку [2]. Лучшее доказательство, что это работает.
Подойдёт, если у вашего продукта есть момент отказа, в который пользователь ещё на странице — пейволл, брошенная корзина, отмена подписки, — а интервьюировать каждого руками вы физически не можете.
Не подойдёт как замена живым интервью на этапе поиска продукта: бот хорош в горячем «почему нет» здесь и сейчас, глубинные интервью делать лучше живым общением.
Себестоимость: Час вайбкода, две таблицы, один эндпоинт, два промпта, вебхук. Данные бесценны :)
Автор: PsyGab
Источник [3]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/33721
URLs in this post:
[1] Доминанта: http://www.braintools.ru/article/9378
[2] ошибку: http://www.braintools.ru/article/4192
[3] Источник: https://habr.com/ru/articles/1064468/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1064468
Нажмите здесь для печати.