Как я собрал AI-руководителя отдела продаж в Instagram Direct - и почему самое сложное оказалось не AI
Илья Черняк · 5 июня 2026 г.
Построил не чат-бота, а AI-руководителя отдела продаж в Instagram Direct. Самое сложное оказалось не AI - а просто прочитать входящее сообщение через код.
Неделю назад я сидел перед терминалом и понимал, что застрял. Не на нейросетях - они заработали за день. Не на архитектуре - SQLite и пара API, ничего космического. Я застрял на том, чтобы просто прочитать входящее сообщение в Instagram Direct через код. Просто прочитать. Это стоило мне больше нервов, чем вся остальная система вместе взятая.
У меня есть клиент с активным Instagram-аккаунтом. Каждый день - десятки входящих в Direct: “сколько стоит?”, ответы на сторис, фотки товаров, голосовые. К вечеру горячий покупатель, который написал утром, тонет под слоем мусора. Владелец не успевает. Деньги утекают. Классика.
Я решил посмотреть на Direct не как на мессенджер, а как на отдел продаж. А если это отдел - ему нужен руководитель. Только не живой (дорого, не масштабируется), а AI-система, которая сортирует входящих, подсказывает кому писать прямо сейчас и готовит черновик ответа в голосе владельца.
Пишу с пылу с жару, пока помню каждую команду. Это не “как правильно по документации” - это как реально собрал, с граблями.
Четыре такта, а не “сели и закодили”
Такт 1. Разведка. Прежде чем писать код, я прогнал рынок IG-DM/CRM-инструментов. Нашел позиционный разрыв. Существующие решения - это либо чат-боты с ветвлениями, либо CRM без понимания контента. Никто не совмещает ось намерения покупателя + детекторы ситуаций + чтение медиа (фото, голосовые, видео) + смысловой поиск по истории + AI-черновики в голосе владельца. Отсюда родился месседж: это не чат-бот, а AI-руководитель отдела продаж.
Такт 2. Дизайн. Через Claude Design (claude.ai) я создал полный гайд - знак, палитру, типографику, - лендинг и CSS-скин дашборда. Скин забрал как сырье (CSS-токены + классы) и портировал в серверный движок. Это дало единый визуальный язык до того, как написана первая строчка бизнес-логики.
Такт 3. Ядро. Построил архитектуру на тесном по RAM VPS Hetzner - без единой тяжелой ML-зависимости. Все “дорогое” (LLM, эмбеддинги, мультимодальность) вынесено в облачные API.
Такт 4. Продуктизация. Превратил внутренний инструмент в продукт с публичным демо, логин-гейтом и лендингом.
Самое сложное: как вообще принимать и отправлять Instagram DM
Прежде чем обсуждать AI - надо разобраться с тем, без чего вся система бессмысленна: программный доступ к личным сообщениям Instagram. Это оказалось самой тяжелой и самой неочевидной частью проекта. Не нейросети, не архитектура, а буквально “как читать и писать DM через код”.
По-настоящему сложное в интеграции с Instagram DM - это не AI, а получить авторизованный доступ на чтение и запись личных сообщений. AI - просто API-вызовы. Вся кровь - в транспортном слое.
В прошлой статье я описывал официальный путь: свое приложение в Meta for Developers, System User Token, прохождение App Review. App Review я прошел. Но прав именно на личные сообщения он мне так и не дал. Постить, читать и отвечать на комментарии - пожалуйста. А вот залезть в Direct через официальный API - это отдельная стена, выше предыдущей. Для этого проекта я нашел путь короче - в обход этой стены вообще. Расскажу.
Официальный путь - стена. Чтобы получить программный доступ именно к личным сообщениям, нужен отдельный раунд App Review под разрешение instagram_manage_messages: бизнес-верификация компании, скринкасты с демонстрацией каждого сценария, обоснования, недели переписки с ревьюерами и частые отказы без внятных причин. Для одного аккаунта и быстрых итераций это оверкилл, который съедает недели до первого прочитанного сообщения.
Прорыв - Zernio. Вместо того чтобы становиться собственным приложением Meta, я взял сервис, который уже является одобренным Meta Tech Provider. Это Zernio, и у него есть бесплатный тариф для личных сообщений. Подключаешь аккаунт через его OAuth-flow (одобрение Meta лежит на Zernio, не на тебе) - и сразу получаешь программный доступ к DM без собственного app review. Самая тяжелая часть всего проекта схлопнулась до 15 минут настройки.
Прием сообщений: вебхук
Zernio шлет вебхук на мой эндпоинт при каждом событии: message.received, message.sent, message.failed. Первичная настройка - handshake верификации в стиле Meta: GET-запрос с параметром hub.challenge, я возвращаю challenge обратно (доказываю владение эндпоинтом). Каждый POST подписан заголовком с signature - проверяю, что запрос действительно от Zernio, а не подделка.
Жесткое ограничение: вебхук работает по принципу fire-and-forget с ретраями. Если не ответить 200 за несколько секунд - Zernio начинает ретраить и заваливает дубликатами. Поэтому хендлер устроен так:
# Сразу 200, тяжёлая работа — в фоновом потоке
def handle_webhook(request):
verify_signature(request)
payload = parse(request)
send_response(200)
threading.Thread(target=process, args=(payload,)).start()
В пейлоаде message.received приходят sender: {id, username}, текст и attachments (медиа-вложения).
Отправка сообщений
POST /inbox/conversations/{id}/messages
Content-Type: application/json
Authorization: Bearer <token>
{"accountId": "...", "message": "текст ответа"}
Длинные ответы режу под лимит длины IG DM (около 1000 символов) и шлю кусками с паузами - иначе Instagram вернет HTTP 400 и сообщение молча потеряется.
Чтение истории и дедуп
GET /inbox/conversations/{id}/messages?accountId=<id>
Возвращает всю переписку, включая ручные ответы самого владельца (приходят как исходящие от “You”). Это критично: бот видит, что владелец уже ответил клиенту вручную, и не переотвечает.
Ограничение Meta - окно 24 часа. Meta разрешает писать пользователю только в течение 24 часов после его последнего входящего сообщения. Вне окна нужны спецтеги или human-agent-флаг. Поэтому система заточена отвечать быстро - и эскалировать владельцу, если не уверена в ответе.
Архитектура: что внутри и почему так
Ключевое ограничение - VPS Hetzner с тесной оперативкой. Никакого PyTorch, никаких локальных векторных баз данных, минимум зависимостей. Все тяжелое считает облако, сервер только оркестрирует.
“Мозг” - Claude (Anthropic). Формирует ответ в голосе владельца - не шаблонный “Здравствуйте, ваш запрос принят”, а живой текст, как если бы владелец писал сам. Исходящие по умолчанию не улетают клиенту напрямую - уходят на подтверждение через Telegram-бот. Владелец в Telegram видит черновик, может отредактировать текст прямо в реплае или нажать “отправить как есть”. Это human-in-the-loop: AI готовит, человек решает. Ни одно сообщение не уходит в Direct без ведома владельца.
Память и CRM на SQLite. Выбрал SQLite именно из-за ограничений RAM - ни ORM, ни внешний процесс не нужен. Внутри:
- полная история диалога (зеркалит тред из Zernio)
- дедуп “уже отвечено” (включая ручные ответы владельца)
- карточки лидов: сегмент, телефон, этап воронки
- полнотекстовый поиск через FTS
Ось намерения. На каждого лида Gemini 2.5 Flash ставит метку: “готов делегировать покупку” / “выбирает сам” / “просто смотрит”. К метке прилагается цитата-обоснование из переписки и уровень уверенности. Классификация запускается автоматически по таймеру и при каждом новом сообщении. Gemini стоит копейки и хорошо работает с короткими контекстами - идеальный классификатор.
Детекторы - готовые рабочие списки. Вместо бесконечного скролла диалогов - конкретные очереди с приоритетами:
- “горячие без ответа” - кто написал и ждет
- “хотят купить, но нет телефона” - надо дожать
- “молчат три дня” - пора реактивировать
Открываешь дашборд утром - и сразу видишь, кому писать прямо сейчас. Не “все 47 диалогов”, а три конкретных имени с контекстом.
Чтение медиа. Фото, голосовые, видео, ответы на сторис - все прогоняется через Gemini 2.5 Flash и превращается в текст. Результат кешируется по идентификатору ассета. Бот “понимает” не только текст, но и фотографии товаров, голосовые вопросы, видео-демонстрации.
Смысловой поиск. gemini-embedding-001 (768 измерений) дает поиск по смыслу: “кто спрашивал про доставку”, “кто упоминал оптовый заказ”. Без локальной векторной БД - косинусное расстояние считается прямо в Python. При сотнях лидов хватает с запасом.
Goal-config. Акцент воронки - что сейчас важнее: собрать телефоны, закрыть на визит, предложить акцию - задается живым конфигом:
{
"goal": "collect_phone",
"priority_label": "нет телефона",
"nudge": "Спросить номер для связи"
}
Меняешь JSON-файл - мозг перестраивается без перезапуска.
Дашборд на stdlib. Написан на чистом http.server из стандартной библиотеки Python. Ни Flask, ни Django, ни Streamlit - ради экономии RAM. Показывает лиды, детекторы, drill-down в полный тред, композер ответа с гейтом подтверждения, кнопку AI-черновика, переключение этапов воронки. Обогащение профилей (число подписчиков, верификация) подтягивается из Zernio.
Продуктизация: один движок, три режима
Чтобы превратить внутренний инструмент в продукт, я сделал движок context-driven через переменные окружения: бренд, имя владельца, лейблы оси намерения, режим read-only, базовый путь. Один и тот же код рендерит и реальную рабочую панель, и публичное демо.
Демо-база. Выдуманная ниша, фейковые лиды, 16 диалогов. Режим read-only: отправка отключена, баннер “это демо” виден сразу. Реальных данных нет.
Логин-гейт. Раньше доступ был по секретной ссылке с токеном в URL. Быстро, но для продукта не годится - ссылка утекает в историю браузера, в Slack-переписку, в скриншоты. Сделал нормальную форму логин/пароль. Сессия хранится в куке: значение - хэш от связки логин:пароль, пароль в открытом виде нигде не лежит, кука переживает рестарт сервера. В итоге три режима доступа уживаются в одном движке: открытое демо / старый токен для обратной совместимости / форма входа.
Развертывание
Два systemd-юнита: один на демо (read-only, демо-база, открытый доступ), второй на реальную панель (за логином). Существующий прод не трогал - выкатил рядом.
# /etc/systemd/system/dm-dashboard-demo.service (simplified)
[Service]
Environment="DASH_MODE=demo"
Environment="DASH_DB=/opt/dm-dashboard/demo.db"
ExecStart=/opt/dm-dashboard/venv/bin/python3 dashboard.py
Лендинг отдается статикой через Caddy.
DNS. Домен продукта висел на “магазинных” NS-серверах регистратора, где API управления зоной недоступен. Через API reg.ru переключил на стандартные NS, добавил A-записи, вычистил паразитные “парковочные” записи.
Автономный вотчер DNS. Маленький скрипт-демон поллил DNS и, как только домен зарезолвился на нужный IP, сам активировал прокси-запись и спровоцировал выпуск TLS-сертификата через Let’s Encrypt. Важный инсайт: Let’s Encrypt проверяет домен через авторитетные серверы, поэтому сертификат можно выпустить раньше, чем обновятся кэши публичных резолверов.
Грабли
Семь конкретных мест, где я споткнулся. Каждое стоило от получаса до целого вечера.
1. systemd и пробелы в значениях. Environment=KEY=value with spaces молча берет только первое слово. Часами дебажил “почему переменная обрезана”. Решение:
Environment="KEY=value with spaces"
Кавычки обязательны, если значение содержит пробелы или спецсимволы.
2. Модуль БД смотрел не на ту env-переменную. Один из модулей хранилища был привязан к другой переменной для пути к базе, чем остальные. Демо чуть не стало читать реальную базу с данными клиента. Урок: проверяй, к какому env реально привязан путь к БД в каждом модуле, а не только в главном.
3. CSS-скин грузится относительно скрипта. Скин подключается по пути dirname(__file__). Деплой на сервер обязан копировать CSS рядом с Python-файлом, иначе дашборд падает на старте с невнятной ошибкой.
4. API reg.ru с IP-whitelist. Я пытался рулить DNS с макбука - Access Denied. API reg.ru работает только с IP из белого списка. У меня в списке был только IP сервера. Все DNS-операции - с сервера через SSH.
5. Дев-артефакты на проде. На лендинге остался переключатель вариантов заголовка (A/B/C) от Claude Design и скрипт обфускации email. Посетитель видит три кнопки, которых быть не должно. Я заметил это только когда показывал демо живому человеку. Урок: перед продом - полный аудит лендинга на дев-мусор. Открой в инкогнито и пройди глазами каждый элемент, как будто видишь сайт впервые.
6. Лимит длины IG DM - около 1000 символов. Сообщения длиннее возвращают HTTP 400 и молча теряются. Я узнал об этом, когда клиент не получил ответ. Решение: нарезать длинные ответы на куски и отправлять с паузами.
7. Вебхук обязан отвечать мгновенно. Zernio ретраит при задержке - получаешь дубликаты входящих. Единственный надежный паттерн: сразу 200, вся логика в фоновом потоке.
Стек, чтобы повторить
- Python stdlib (
http.server) - дашборд и API - SQLite + FTS - память диалогов и CRM
- Claude (Anthropic) - “мозг”: генерация ответов в голосе владельца
- Gemini 2.5 Flash - классификация намерения и чтение медиа (фото, голосовые, видео)
- gemini-embedding-001 (768 измерений) - смысловой поиск по истории
- Zernio (бесплатный тариф) - транспорт Instagram DM: вебхуки, отправка, история треда; Meta Tech Provider, пропуск app review
- Telegram-бот - human-in-the-loop (подтверждение исходящих)
- Hetzner + Caddy + systemd + Let’s Encrypt - хостинг, реверс-прокси, TLS
- API reg.ru - управление DNS-зоной
- Claude Design (claude.ai) - бренд, лендинг, CSS-скин
Что в итоге
Я не разработчик. Claude Code открыл впервые в феврале - четыре месяца назад. Но этот проект показал мне простую вещь: сложное - не AI. Claude, Gemini, эмбеддинги заработали за день, это просто API-вызовы. Всю кровь стоил транспортный слой - авторизованный доступ на чтение и запись Instagram Direct. Как обошел стену Meta App Review - разобрано выше.
Вся тяжелая магия живет в облачных API. Сервер делает только оркестрацию: принял вебхук, дернул API, положил результат в SQLite, отдал HTML. На практике это значит, что такую систему можно поднять на самом дешевом VPS - мой укладывается в пару гигабайт RAM.
Главный сдвиг не технический, а концептуальный: Direct - это не мессенджер, а отдел продаж. Если это отдел - ему нужен руководитель. AI справляется, если правильно разделить, что считает облако, а что делает сервер.
Вопросы и показать свой первый рабочий прототип - в личку @magic4e. Подписывайся на @mdkguru - там я каждый день показываю, как собираю команду AI-агентов и во что это превращается. Спор на 300 000 в самом разгаре.