Типичный бизнес-сайт — это витрина. Посетитель пришёл, почитал, ушёл. Если повезло — заполнил форму. Но между «посетитель на сайте» и «лид в CRM» — пропасть: кто-то должен ответить ночью, кто-то — выяснить что нужно, кто-то — квалифицировать и передать продажнику. Мы зашили все эти «кто-то» в одну систему из четырёх компонентов: 1 397 строк PHP, 602 строки JS, 451 строка NLP-движка, и 935 строк CRM-адаптера. Работает 24/7 на локальной LLM без единого внешнего API.
Почему не OpenAI / DeepSeek / облачные API
Первое что спрашивают: «Почему не взяли OpenAI API и не закончили за день?» Ответ — три причины:
- 152-ФЗ. Персональные данные клиентов (имя, телефон) не должны покидать периметр компании. Облачные API — это передача PII третьей стороне. Юридически — серый зона. Технически — риск.
- Стоимость. 1000 диалогов в день через OpenAI API = $50–150/сутки. Выделенный GPU-сервер на Selektel = фиксированная стоимость в месяц, независимо от нагрузки.
- Контроль. System-prompt, температура, контекст — на своём сервере ты контролируешь всё. Никаких «модель обновилась и теперь отвечает по-другому».
Локальная LLM на выделенном сервере Selektel — это когда ты платишь за железо, а не за токены. И данные клиентов не пересекают границу РФ.
Архитектура: четыре компонента
Система спроектирована под shared hosting — никаких Docker, Redis, WebSocket. Файловые сессии, PHP 8.2 без фреймворка, JSON как БД. Четыре компонента:
1. chat_widget.js — 602 строки чистого JS
IIFE-паттерн, ни одной зависимости. Подключается на любую страницу одной строкой: <script src="chat_widget.js"></script>. Инжектит CSS (injectCSS), строит DOM чата, управляет состоянием.
Три режима работы (стейт-машина): ai → waiting_human → human. Polling каждые 3 секунды в режиме ожидания оператора. Auto-reconnect при обрыве соединения. Персистентность через localStorage — перезагрузка страницы не теряет сессию.
2. chat_api.php — 1 397 строк, 12 endpoints
Точка входа для виджета. Маршрутизация через ?action=...:
- health — проверка статуса
- lead — захват контакта (имя, телефон/email), создание Person в CRM
- chat — AI-диалог: поиск по KB → system prompt → LLM → извлечение сигналов
- poll — ожидание ответа оператора (3 сек интервал)
- end — завершение сессии с транскриптом
- admin* — 6 endpoints для панели оператора (сессии, аналитика, флаги)
3. kb_engine.php — 451 строка, NLP-поиск
Движок базы знаний. Токенизирует запрос пользователя (униграммы + биграммы), фильтрует русские стоп-слова, ищет совпадения по 10 секциям KB. Веса: keyword match (×4), title (×3), content (×1), FAQ question (×2).
Он же управляет стадиями разговора и извлекает BANT-сигналы из ответов пользователя — budget, authority, timeline, need_clarity. И он же считает lead_score.
4. crm_adapter.php — 935 строк, dual-write в Twenty + Bitrix24
Единый PHP-мост к GraphQL API Twenty CRM. Транслирует 9 методов Bitrix24 (crm.lead.add, crm.deal.add, crm.timeline.comment.add, tasks.task.add, и др.) в GraphQL-мутации Twenty. Режим CRM_MODE=dual — параллельная запись в обе CRM с авто-fallback.
6 стадий разговора
Это не чат-бот «вопрос-ответ». Это диалоговая машина с памятью состояния. Каждая стадия имеет:
- Инструкцию для LLM — что говорить, какой тон, какие вопросы задавать
- Правила перехода — regex-паттерны на ответе пользователя
- Защиту от зацикливания — форсированный переход при N сообщений
| Стадия | Триггер перехода | Форсирование |
|---|---|---|
| 1. Discovery | ≥2 проблемных ключевика | 6 сообщений |
| 2. Qualification | ≥3 BANT-сигналов | 12 сообщений |
| 3. Solution Matching | Возражение или интерес | — |
| 4. Objection Handling | Возражение отработано | — |
| 5. Consultation Offer | Новый вопрос → рестарт | — |
| 6. Human Handoff | «позови оператора» | — |
BANT-скоринг — автоматическая квалификация
Пока пользователь общается с ботом, kb_engine в фоне извлекает сигналы из каждого ответа. Никаких «оцените лид от 1 до 10» — система сама понимает кто перед ней.
Формула score:
// kb_engine.php:366 — функция kb_calculate_score() $score = 0; if (!empty($signals['budget_indicators'])) $score += 3; // бюджет if (!empty($signals['authority_indicators'])) $score += 3; // ЛПР if (!empty($signals['timeline_indicators'])) $score += 2; // срочность $score += ($signals['need_clarity'] === 'high') ? 2 : 0; $score += ($signals['need_clarity'] === 'medium') ? 1 : 0; if ($score >= 7) return 'hot'; if ($score >= 4) return 'warm'; return 'cold';
Паттерны для детекции бюджета ищут не только цифры с «₽», но и косвенные признаки: «бесплатный пилот», «недорого», «эконом». Authority детектит «директор», «я принимаю решение», «собственник», «руководитель отдела». Timeline ловит «срочно», «вчера», «на этой неделе».
Human Handoff — когда бот зовёт человека
Самое тонкое место любого AI-чата — понять когда заткнуться и позвать оператора. Мы сделали двухуровневую систему.
Уровень 1 — триггеры пользователя:
- «позови оператора», «нужен человек», «переключи на менеджера», «хочу живого»
Уровень 2 — защита от ложных срабатываний:
Перед проверкой триггеров — regex-фильтр негации: «не надо оператора», «без менеджера», «не нужен человек». Если негация найдена — handoff не активируется. Простая но критичная деталь, которую забывают 90% чат-ботов.
После handoff оператор заходит в chat_admin.html (888 строк, SPA) — видит все активные сессии, полную историю разговора, AI-подсказки ответов, флаги (good/bad/missing_knowledge).
Интеграция с CRM — лид не теряется
Каждый захваченный контакт мгновенно создаётся в двух CRM одновременно:
- Twenty CRM (основная) — Person с телефоном/email, Opportunity для горячих лидов, Notes с историей стадий
- Bitrix24 (legacy fallback) — Lead с кастомными полями (UF_CRM_LEAD_SCORE, UF_CRM_STAGE)
Дедупликация: перед созданием — поиск по телефону (local digits, последние 7–10 цифр) и email. Если контакт уже есть — добавляется Note «Повторное обращение через чат» вместо создания дубля.
Стадии разговора и изменения lead_score синхронизируются в реальном времени: каждый переход этапа → Note в Twenty с деталями. Рост score → Note с историей. Hot/Warm + consultation_offer → автоматическое создание Opportunity.
Почему PHP и файлы, а не нормальная БД
Хостинг Nichost — shared, без PostgreSQL, без Redis. Всё состояние системы — JSON-файлы с LOCK_EX. Это накладывает ограничения, но и даёт плюсы:
- Ноль devops. Загрузил PHP-файлы через FTP — работает. Никаких миграций БД, бэкапов, репликаций.
- Прозрачность. Любую сессию можно открыть и прочитать глазами —
session_{uuid}.json. - Скорость. file_get_contents/file_put_contents быстрее чем SQL-запрос для одиночных чтений/записей.
Минус — нет конкурентного доступа. Но для сценария «один пользователь — одна сессия» это не проблема.
Что дальше
Сейчас система работает в режиме dual-write (Twenty + Bitrix24). Следующий шаг — отключить Bitrix24 совсем и оставить только Twenty. Дальше — голосовой интерфейс (звонок на сайт → AI-оператор) и исходящие цепочки (бот сам инициирует диалог после заявки).
Главное что мы поняли за время разработки: AI-чат на сайте — это не «прикрутить ChatGPT и готово». Это инженерная задача: стейт-машина, NLP-поиск, BANT-квалификация, CRM-интеграция, комплаенс. Каждый слой по отдельности простой. Собранные вместе — система которая реально заменяет человека на первой линии.