Квиз-бот без единой нейросети: как я за 3 дня собрал бота, который сам собирает заявки
Илья Черняк · 26 июля 2026 г.
Пишу по горячим следам. Ко мне пришёл близкий человек с задачей «нужен умный бот на ИИ» — и первое, что я сделал, это отговорил его от ИИ. Через три дня бот жил на сервере и собирал реальные заявки. Ниже — как он устроен, почему без нейросетей, и девять мест, где я споткнулся. Включая последнее, которое не чинится кодом вообще.

Я не разработчик. Claude Code открыл впервые в феврале, до этого максимум лез в настройки роутера. Так что дальше — не гайд от программиста, а разбор от предпринимателя, который собрал живую штуку и теперь показывает, где сам напоролся.
Часть 1. Что болело до бота
Вадим — психолог и коуч, ведёт годовую программу: группа из двенадцати человек, еженедельные встречи, трекинг, командный проект. Плюс он блогер с живой аудиторией и свой кофе-бренд. Человек, у которого дефицита внимания к нему точно нет.
И вот как у него был устроен набор в программу.
Руками. Каждому — лично в личку. Одно и то же объяснение по двадцатому разу. Ответы участников живут в переписке, вперемешку с «привет, как дела» и голосовыми про кофе.
Момент, после которого мне всё стало понятно, — его собственное голосовое. Сначала он сказал: данные собирать не нужно, я же знакомым рассылаю. А через одно сообщение сам себя развернул:
«Если я запомнил и ты запомнил — то куда это должно прилетать? Как я узнаю, чьи это ответы??»
Вот это и есть боль. Не «нет бота» — а ответы есть, а чьи они, непонятно.
Дальше — три вещи, которые превращают ручной набор в мучение:
- Часть людей отвечает голосом. Это его аудитория, они так привыкли. Значит, кто-то должен эти голосовые слушать и конспектировать. Этот кто-то — он сам, вечером.
- Программа требует интейка. Вопросы про запрос, про работу с психотерапевтом, про антидепрессанты. Задавать такое в лоб в личке неловко обеим сторонам.
- Он физически не масштабируется. Двенадцать человек — ещё ок. Поток с Instagram — уже нет.
А у меня была своя боль, не клиентская. К этому моменту я собрал уже кучу разных ботов, и каждый раз — с нуля. Хотелось не «ещё одного бота», а движок: чтобы следующий клиент был папкой с конфигом, а не новым проектом.
Часть 2. Почему я отговорил клиента от ИИ
Вадим пришёл со словами «хочу умного бота». Это нормальное желание — все хотят умного. Я предложил ровно наоборот: кнопочный сценарный автомат, ноль нейросетей в рантайме.
Логика простая. Интейк — это не беседа. Это предсказуемая анкета, где важно, чтобы всем задали одни и те же вопросы в одном и том же порядке. LLM в этом месте — не преимущество, а риск: она начнёт вести диалог, переформулировать, сочувствовать невпопад и однажды выдумает то, чего в программе нет.
Чем расплачиваешься за «неумного» бота:
| Умный на LLM | Сценарный автомат | |
|---|---|---|
| Предсказуемость | «как получится» | шаг в шаг, всегда одинаково |
| Стоимость | токены на каждого лида | ноль |
| Что может выдумать | что угодно | ничего, он не генерирует |
| Отладка | «почему он так ответил?» | видно в конфиге |
Единственное место, где ИИ реально нужен, — расшифровка голосовых. И вот тут я сделал важную для себя оговорку, которую потом повторял клиенту: распознавание речи — это не «умный бот». Бот по-прежнему не ведёт беседу. Он получает голосовое, превращает его в текст и кладёт в анкету. Обещание «без ИИ» осталось честным.
Правило, которое я после этого записал себе: ИИ ставим не туда, где красиво звучит, а туда, где без него задача не решается вообще.
Часть 3. Как он устроен
Движок и клиент — разные вещи
Главное архитектурное решение: сценарий живёт в конфиге, а не в коде.
quizbot/
├── bot/engine/ ← общий движок, один на всех
└── clients/
└── vector/ ← клиент
├── config.yaml ← сценарий: кружки, вопросы, палитра, владелец
├── media/ ← видео-кружки
└── .env ← токен бота (права 600)
Движок читает config.yaml и собирает из него конечный автомат. Новый клиент — это новая папка и одна команда в systemd. Не форк, не копипаста, не «а давай подправим под нового».
Именно поэтому Вадим у меня в проекте называется не «клиент, для которого сделали бота», а первый клиент движка.
Чем платил за каждое решение
| Решение | Почему так |
|---|---|
| aiogram 3.13 | живой асинхронный фреймворк под Telegram, всё нужное из коробки |
| SQLite, а не векторная база | разведка прямым текстом сказала: векторка тут — оверинжиниринг. Данных мало, запросы простые: кто, когда, что ответил |
| Своё хранилище состояния | штатное у aiogram живёт в памяти. Рестарт сервиса — и все, кто был в середине анкеты, теряют шаг. Своё, на SQLite, переживает рестарт |
| AssemblyAI для голоса | pay-as-you-go, примерно 0,25 доллара на сотню заявок. Не подписка |
| Заявка = карточка в Telegram, не Excel | из-за голосовых. В карточке контакт сверху, ответы текстом, и сам голосовой файл рядом — можно послушать интонацию |
| Видео-кружки в сценарии | это лицо Вадима, а не текст от бота. Ни один конструктор квизов, который я нашёл, так не умеет |
systemd-шаблон quizbot@.service | один юнит на всех клиентов. Запуск нового: systemctl enable quizbot@новый_клиент |
Как это выглядит для человека
Двенадцать шагов, и в них зашита вся драматургия:
кружок №1 (привет, ты в «Векторе»)
↓
телефон одной кнопкой + согласие на обработку данных
↓
кружок №2 (сейчас задам вопросы, отвечай честно)
↓
рамка доверия: «дальше личные вопросы, видит только Вадим»
↓
6 вопросов (текстом или голосом, счётчик «Вопрос 3 из 6»)
↓
кружок №3 (спасибо, до встречи)
↓
карточка с заявкой падает Вадиму в Telegram
Обратите внимание на два места, которых не было в первой версии.
Телефон просим сразу после первого кружка, а не в конце. Потому что человек, который бросит анкету на пятом вопросе, всё равно останется контактом — а не растворится.
Рамка доверия перед личными вопросами — отдельный пассивный шаг, который просто показывает текст и идёт дальше. Его подсказал агент, который смотрел на бота глазами UX. Я сначала эту правку пропустил, вернулся и добавил. Без неё вопрос про антидепрессанты прилетает человеку в лоб.
Часть 4. Грабли
Тут самое ценное. Девять штук, честно, по порядку.
1. Аудит четырьмя агентами нашёл то, чего не видел я
Перед запуском я прогнал код через четверых агентов с разными ролями: разработчик смотрел код, тестировщик пытался сломать, дизайнер смотрел на путь человека, а четвёртый — на саму постановку задачи.
Трое сошлись независимо друг от друга на одном месте. В коде отправки заявки стояло вот это:
try:
await bot.send_message(owner_id, card)
except:
pass # ← вот здесь молча умирали заявки
Два сценария, при которых заявка исчезала бесследно. Первый: Telegram отвечает 403, потому что владелец не нажал /start у своего бота (об этом ниже, это отдельная боль). Второй: карточка длиннее 4096 символов — человек ответил развёрнуто, и всё сообщение не ушло.
Клиент бы никогда не сказал «у вас баг». Он бы сказал «ваш бот не работает» — и ушёл.
Починили: нарезка длинной карточки на куски, явный лог вместо pass, функция возвращает результат отправки. И главное — данные заявки лежат в базе до попытки отправки, так что при любом сбое доставки они целы.
Урок, который стоил бы мне клиента:
except: pass— это не обработка ошибки. Это решение никогда о ней не узнать.
2. Ключ от сервиса лежал прямо в исходнике
Тот же аудит нашёл ключ AssemblyAI, вписанный прямо в код. Вынес в .env с правами 600, код теперь падает при старте, если ключа нет, — лучше не запуститься, чем работать наполовину.
Продолжение истории смешнее: при выкатке на сервер чуть не уехал файл stt.py.bak — со старым ключом внутри. Бэкап, который я сам сделал часом раньше. Вычистил.
3. Гонка на семи секундах
Расшифровка голосового занимает около семи секунд. Если человек за это время дописывал что-то ещё — анкета проскакивала шаг вперёд и путалась.
Лечится замком на пользователя: пока обрабатываем один ответ, второй получает вежливое «Секунду…».
4. Скрипт создания бота чуть не стёр все ключи
У меня был скрипт, который создаёт бота через BotFather и записывает токен в .env. Записывает — перезаписывая весь файл целиком. Вместе с ключом AssemblyAI, который там уже лежал.
Поймал до запуска. Новый скрипт меняет только строку с токеном, остальное не трогает, плюс делает бэкап.
5. Бот вешался на стикер
Человек в середине анкеты присылает фото, стикер или кружок — и бот встаёт. Он ждал текст или голос, а пришло другое.
Лечится хендлером-ловушкой, который ловит всё остальное и мягко переспрашивает. Но есть нюанс: регистрировать его надо последним. Зарегистрируешь раньше — он перехватит вообще всё, и бот перестанет слышать нормальные ответы.
6. Сироты в базе
Повторный /start создавал новую заявку, а старая, недозаполненная, оставалась висеть. База обрастала мусором.
Теперь при новом старте предыдущая незавершённая заявка того же человека помечается как брошенная. Завершённые и чужие — не трогаем.
7. Спор про личные вопросы, который решил не я
Двое агентов — тот, что смотрел UX, и тот, что смотрел постановку — независимо сказали одно: вопросы про терапию и антидепрессанты в воронке выглядят грубо.
Правильный ответ оказался не техническим. Аудитория бота — не холодный трафик, а люди, уже пришедшие в программу. Это не воронка, это интейк перед годовой работой, и там такие вопросы нормальны. Развилку закрыл человек, а не код.
Но замечание не пропало зря: добавили рамку доверия перед блоком и возможность пропустить — достаточно написать «пропущу», и в карточке будет честное «(пропущено)».
8. Правил в одном месте, бой жил в другом
Классика. Локальная копия — одна папка, боевая на сервере — другая. Правишь локально, перезапускаешь сервис, проверяешь — старое поведение. Потому что правка до сервера не доехала.
Записал себе отдельным правилом: «active» в systemd не значит «моя правка применилась». Проверять надо не статус сервиса, а сам файл на сервере.
9. Последняя миля оказалась физической
Бот собран. Задеплоен. Сервис зелёный. Тестовые прогоны прошли.
И первая настоящая заявка не долетает до Вадима.
Причина: Telegram не разрешает боту написать человеку первым. Вадим — владелец бота, но он сам ни разу не нажал у него /start. Ни строчкой кода это не решается. Только человек и один клик.
Я теперь закладываю это в план как отдельный пункт: «клиент нажимает /start у своего бота». Десять секунд, разово — и без них вся система бесполезна.
Часть 5. Сколько это заняло
| Когда | Что |
|---|---|
| День 0 | бриф клиенту: премиум-PDF на пять страниц, разбор его голосовых |
| День 2 | ответы на два вопроса клиента: как собираем контакт, как быть с голосовыми |
| День 3, утро | клиент прислал контент: три видео-кружка и шесть вопросов |
| День 3, день | рабочая версия собрана и проверена вживую. Аудит четырьмя агентами. Правки |
| День 3, вечер | боевой бот, деплой на сервер |
| День 3, 16:21 | первый живой незнакомец проходит анкету |
| День 4 | правки клиента выкачены, тёплый отзыв, подтверждён следующий этап |
От брифа до боевого бота — три дня. От контента клиента до работающего бота — один день.
И вот это, пожалуй, главная цифра всей статьи: 90% времени я ждал не код, а контент. Кружки и вопросы. Сборка — день. Ожидание материала — всё остальное.
Что живёт сейчас: сервис крутится на сервере, десять заявок, все со статусом «завершена». То есть люди доходят до конца анкеты, а не отваливаются на середине — включая тех, кто отвечал голосом.
Часть 6. Чем закончилось
Отзыв клиента, голосовым, дословно:
«Спасибо тебе большое за бота. Вроде всё работает, всё хорошо. Сейчас с ребят соберу обратную связь. И потом подготовлю бота — прям основного, куда я смогу с Инстаграма перегонять людей, которые хотят ознакомиться с продуктами. Там больше материала, больше кружочков, ну и ветки будут идти — в зависимости от ответа.»
Тут стоит остановиться. Клиент сам попросил апселл, который мы держали как гипотезу. Мы планировали предложить второй бот — холодный, под трафик из Instagram, с ветвлением по ответам. Не пришлось: он пришёл к этому сам, пройдя через первый.
Это, по-моему, лучший способ продавать следующий этап — сделать предыдущий так, чтобы клиент сам увидел, что дальше.
Его роадмап, в его же порядке приоритета: ветвление по ответам → рассылка всем, кто хоть раз нажал /start → самостоятельная правка вопросов без меня → выгрузка в таблицу.
Что я вынес
- «Без ИИ» — это фича, а не компромисс. Клиент просил умного бота, я отговорил, и оказался прав: для анкеты предсказуемый автомат лучше нейросети. Дешевле, стабильнее и ничего не выдумывает.
- ИИ ставим точечно. Единственное место, где он тут нужен, — расшифровка голоса. Он не ведёт диалог, он снимает с клиента ручную работу по прослушиванию.
- Аудит перед запуском окупается один раз и навсегда. Четыре агента с разными ролями нашли
except: pass, который бы молча съедал заявки. Одна эта находка стоит всего времени на аудит. - Разные роли важнее количества. Ценность дал не «ещё один проверяющий», а то, что четверо смотрели с разных сторон: код, слом, путь человека, сама постановка задачи. Два самых важных замечания пришли не от кода.
- Сразу стройте движок, а не проект. Разница между «бот для Вадима» и «движок, у которого Вадим — первый клиент» — это одна папка с конфигом. Но она решает, будет ли следующий клиент за день или за неделю.
- Данные сохраняем до отправки, а не после. Любая доставка может упасть. Заявка, которая уже в базе, переживёт и 403, и падение сервиса, и мой кривой деплой.
- Последняя миля почти всегда физическая. Код готов, сервис зелёный, а система не работает, потому что человек не нажал одну кнопку. Планируйте и это тоже.
- Узкое место — не разработка. Три дня проекта, из них один — сборка. Остальное — ожидание контента. Если хотите быстрее, начинайте выбивать материал раньше, чем откроете редактор.
Хочешь такого же бота под свою программу, курс или услугу — пиши в личку @magic4e, движок уже готов, вопрос только в твоём сценарии. Подписывайся на @mdkguru — там я по горячим следам показываю, как собираю команду AI-агентов и во что это превращается.
Спасибо Вадиму Прощенко за разрешение рассказать про его кейс и процитировать голосовые. Сделано одним человеком и небольшим флотом агентов. Все баги мои.