ZenAi: динамические воронки, качество бота и message debounce
Date: 2026-02-28 — 2026-03-01
Project: ZenAi — ИИ-менеджер для Telegram (C:\Tools\ZenAi)
Duration context: два дня интенсивной работы (архитектурная переделка + 15+ фиксов + новые фичи)
Что было сделано
Динамические воронки — замена хардкодного FSM
- Полностью заменён хардкодный FSM (9 enum-состояний) на data-driven движок с шагами из БД.
- 4 новые таблицы:
funnels,funnel_steps,funnel_fields,funnel_kb_links. FunnelEngineвsrc/funnel/engine.py— проверка полноты шага, переход на следующий, промпт из step.prompt, tool schema, completion %.DialogServiceполностью переписан: загрузка воронки → промпт из step.prompt + system_prompt → structured JSON от LLM ({"message", "extracted_fields", "step_complete"}) → извлечение полей → переход между шагами → on_complete.- RAG retriever фильтрует по
document_ids, привязанным к конкретной воронке. - Funnels CRUD API: воронки, шаги, поля, KB-привязки, дублирование.
- Admin: полный 4-вкладочный редактор воронок (основное, шаги с полями, база знаний, завершение).
- Per-funnel LLM settings: модель и температура переопределяют глобальные.
- Миграция с seed-данными: воронка “ZenetiQ Health” с 6 шагами и полями.
Продажная воронка v2
- Вторая воронка «Продажная воронка v2» — 6 шагов (init → qualification → contraindication → presentation → objection_handling → summary), 17 полей.
- Prompt-driven ветвление по
intent_type(weight/glow/health/complex/partner). - Маршрутизация противопоказаний: щитовидка → MOTS-c, серьёзные → к врачу.
- Матрица возражений в промпте: цена, побочки, регистрация, врач против, доверие.
- Структурированный отчёт менеджеру:
client_readiness(hot/warm/cold),next_step, текстовыйmanager_report.
Circle-back механизм — без допроса
- Бот больше не блокируется на обязательных полях. Если клиент игнорирует вопрос — двигается дальше.
- Перед терминальным шагом
get_missing_required_fields()собирает всё незаполненное и мягко доспрашивает. _generate_gap_filling()— генерирует естественный вопрос вместо повторения того же вопроса.- Три бага найдены и починены уже в продакшне: terminal-поля попадали в circle-back,
Boolean falseсчитался как “не заполнено” (is not Noneвместо truthiness), completion_pct учитывал terminal step.
Качество бота — 10+ фиксов промптов и поведения
- Зацикливание на имени — поле
nameстало необязательным, промпт не настаивает. - Перескакивание шагов — при
step_complete=trueдобавлен второй LLM-вызов с промптом нового шага. - Повторное представление — правила в промпте: не повторять приветствие, не задавать вопросы из будущих шагов.
- Многословность — строгие инструкции: макс. 2-3 предложения, без “Отлично!/Замечательно!”, не повторять имя клиента.
- Галлюцинации продуктов — строгая инструкция: рекомендовать ТОЛЬКО из KB, никогда не выдумывать названия/цены.
- Read receipts —
send_read_acknowledge()при получении сообщения (двойные галочки). - Бот говорит от лица менеджера — убраны фразы “другой менеджер”, бот всегда говорит как единственный менеджер.
- Em dashes и «кавычки» — post-processing LLM-вывода: замена
—на-,«»на"". Чистка промптов и KB от em dashes.
Typing emulation и уведомления
- Бот показывает «печатает…» пока LLM обрабатывает сообщение — Telethon
action("typing")context manager. - Глобальный toggle + per-funnel override (По умолчанию / Включена / Выключена).
NotificationService— три канала при завершении воронки: Telegram Bot API, Webhook (POST), Email (aiosmtplib SMTP).- Страница «Уведомления» в админке с кнопками «Тест» для каждого канала.
- Расширена вкладка «Завершение» в редакторе воронки — блоки: system_alert, telegram_notify, webhook_url, email_notify.
Fallback LLM
- При недоступности основной модели (HTTP ошибка, timeout, connection error) бот переключается на fallback.
_call_llm()— retry: 1 попытка + 1 повтор через 1 сек + fallback модель.- Глобальная настройка
llm_fallback_model+ per-funnel override. - UI: Autocomplete «Резервная модель» в настройках OpenRouter.
Message debounce buffer
- Серия быстрых сообщений буферизируется в Redis с настраиваемым окном тишины (дефолт 40 сек).
buffer_incoming_message()— сохраняетmessage_idв Redis-лист, сбрасывает debounce-таймер.flush_debounce_buffers()— cron каждые 10 сек, обрабатывает буферы с истёкшим таймером._process_batched_messages()— склейка текстов + единый LLM-вызов + один ответ.- Настройка
message_debounce_secondsв UI (0 = отключено). - Catchup пропускает конверсации с активным буфером.
Вкладка «Лиды» + фильтры
GET /api/v1/leads/— пагинация, фильтры (is_ready,funnel_id,search), сортировка.- Admin:
LeadList(Datagrid с прогресс-барами, чипами статусов) +LeadShow(карточки: контакт, воронка, собранные данные, FSM). - Отображение воронки в шапке диалога и лид-карточке.
- Выбор воронки при «Запустить ИИ» / «Перезапустить ИИ».
Потеря сообщений при деплое
- Idempotency-ключ ставился с полным TTL (24ч) ДО обработки. При kill контейнера — сообщение навсегда блокировалось.
- Исправлено: короткий TTL-лок (5 мин) → обработка → продление до 24ч.
- Catchup окно увеличено с 5 до 30 минут.
UX админки
- Индикация несохранённых изменений в редакторе воронки: чип «не сохранён», оранжевая кнопка, жёлтая точка на вкладке, предупреждение при уходе.
- KB reindex переведён на синхронный вызов вместо сломанной ARQ-очереди.
- Кнопка «Переиндексировать» для опубликованных KB-документов.
Проблемы и решения
Проблема: бот зацикливается — допрашивает клиента
Симптомы: бот спрашивал «как к вам обращаться?» по 3 раза, блокируя продвижение по воронке.
Решение: Двойной удар — поле name стало is_required=false, промпт переписан: если клиент игнорирует вопрос, бот отвечает на то что спросили и двигается дальше. Позже реализован полноценный circle-back: бот собирает пропущенное перед финальным шагом, не блокируясь ни на одном обязательном поле.
Проблема: structured JSON от LLM — boolean false = “не заполнено”
Симптомы: бот бесконечно возвращался к вопросу о противопоказаниях, хотя клиент ответил «нет».
Решение: collected.get(field.name) возвращает False для boolean полей — falsy в Python. Заменено на is not None в трёх методах: check_step_complete(), compute_completion_pct(), get_missing_required_fields().
Проблема: LLM генерирует em dashes и «кавычки»
Симптомы: в сообщениях бота появляются —, «, » — нетипично для переписки в Telegram.
Решение: post-processing в _post_process_message(): — → -, «» → "". Также вычищены все em dashes из промптов, KB-документов и docs.
Проблема: KB reindex через ARQ не работает
Симптомы: publish endpoint пушил задачу как raw JSON в redis.lpush("arq:queue", ...), но ARQ использует msgpack и слушает очередь "default". 21 stale job, worker никогда не подбирал задачу.
Решение: reindex запускается синхронно через await _run_reindex() прямо в publish endpoint. Просто и надёжно.
Проблема: 502 Bad Gateway после пересборки контейнеров
Симптомы: nginx в admin-контейнере кешировал IP API-контейнера. После docker compose build IP менялся → 502.
Решение: resolver 127.0.0.11 valid=5s + переменная $api_upstream для динамического DNS в nginx.conf.
Ключевые решения
- Data-driven воронки вместо enum FSM — шаги и поля в БД, конфигурируются через админку. LLM управляет извлечением полей и переходами, а не хардкод. Позволяет создавать новые воронки без кода.
- Structured JSON output от LLM —
{"message", "extracted_fields", "step_complete"}вместо свободного текста. Надёжный парсинг полей, детерминированные переходы. - Circle-back вместо жёсткого гейта — бот не блокирует клиента на обязательных полях. Клиентский опыт важнее полноты данных. Всё пропущенное доспрашивается перед финальным шагом.
- Message debounce в Redis — буферизация с таймером тишины решает проблему серий сообщений (клиент шлёт 3-5 коротких подряд). Один LLM-вызов вместо пяти — экономия токенов и когерентный ответ.
- Short TTL lock вместо полного TTL — idempotency-ключ с коротким TTL (5 мин) предотвращает потерю сообщений при kill контейнера. Лок автоматически истекает, catchup подхватывает.
Источники и ссылки
Из сессии
- OpenRouter API — провайдер LLM API для structured output
- aiosmtplib docs — async SMTP для email-уведомлений
- Дизайн-документы:
docs/plans/2026-02-28-dynamic-funnels-design.md,docs/funnel-final-proposal.md
Дополнительный контекст
- n8n: Debounce Telegram messages + OpenAI — аналогичный паттерн debounce для чат-ботов: буферизация серии → один LLM-вызов. Валидирует архитектурное решение ZenAi.
- Reddit: Debounce for chat agents — обсуждение паттерна: таймер молчания, все ранние executions exit early, только финальный доходит до LLM.
- React Admin — Data Provider — документация по кастомным провайдерам для нестандартных API
Что дальше
- Интеграционные тесты с реальными Telegram-аккаунтами.
- amoCRM credentials и маппинг полей воронки → CRM.
- A/B тестирование промптов воронки на реальных клиентах.
- Мониторинг конверсии по шагам воронки (аналитика в админке).
- Security и stability remediation (план в
docs/security-and-stability-remediation-plan-2026-02-28.md).