Каждый второй разработчик сейчас предлагает клиентам «умный чат-бот с ИИ». Каждый второй такой бот на деле — виджет от стороннего SaaS-сервиса: чужой сервер, чужая база данных с перепиской ваших клиентов, ежемесячная подписка и ноль контроля над тем, что именно бот говорит от вашего имени.
Я пошёл другим путём и написал собственного ассистента с нуля — на PHP и SQLite, без единого внешнего фреймворка, с полным контролем над кодом, данными и логикой ответов. Он прямо сейчас работает на этом сайте — можете открыть чат в углу экрана и убедиться сами. В этой статье — как он устроен внутри, зачем я принял именно такие технические решения, и что происходит на сайте в момент, когда посетитель нажимает на кнопку чата.
📑 Содержание
- Одна строка — и виджет живёт на сайте 🔌
- Мозг ассистента — YandexGPT по API, без посредников 🧠
- Почему именно YandexGPT, а не что-то ещё 💸
- Что на самом деле происходит между вопросом и ответом 🛤️
- Контакты — не текстом, а токенами 🔗
- Что видит браузер посетителя — и что нет 🔒
- База данных — файл, а не дорогой сервер 💾
- Живая синхронизация — там, где обычно все останавливаются 🔄
- На стороне посетителя: тихая дозагрузка без единого лишнего клика 👤
- На стороне администратора: список диалогов 📋
- На стороне администратора: просмотр конкретного диалога 💬
- Плашка-приглашение — небольшая деталь, которая работает на конверсию 🚀
- Админ-панель — переписка под контролем, без сторонних сервисов 🛠️
- Поиск на кириллице — и грабли, на которые я наступил 🔎
- Восстановление пароля — письмо с доменной почты ✉️
- Про куки и что вообще хранится у посетителя 🍪
- Зачем всё это разработчику сайтов на Tilda 💡
- Заключение ⚡
🔌 Одна строка — и виджет живёт на сайте
Первое требование, которое я себе поставил: подключение не должно быть головной болью. Никаких билд-шагов, npm, сборщиков. Одна строка перед закрывающим тегом </body>:
<!-- html -->
<script src="https://my-domen.ru/public/embed.js"></script>И всё. Скрипт сам рисует кнопку в правом нижнем углу, сам подгружает настройки (заголовок, приветствие, аватар), сам разворачивает окно чата при клике. На Tilda это буквально копипаст в настройках футера — без доступа к хостингу, без правки шаблонов.
Технически весь клиентский функционал — разметка, стили, обработчики событий, рендеринг сообщений, синхронизация с сервером — живёт в одном файле. Осознанное решение: раз скрипт подключается на сторонние сайты одним тегом, разбивать его на модули означало бы для каждого сайта дополнительный HTTP-запрос и лишнюю точку отказа. Простота встраивания важнее «архитектурной чистоты».
Рис. 1. Вид виджета на сайте в закрытом виде.
🧠 Мозг ассистента — YandexGPT по API, без посредников
Отвечает не «набор готовых фраз по ключевым словам», а настоящая большая языковая модель — YandexGPT, к которой сервер обращается напрямую по HTTPS-API. Никаких прослоек, никаких сторонних платформ-конструкторов ботов — я управляю запросом к модели полностью сам.
Вот упрощённый, но рабочий по сути код клиента, который отправляет запрос и разбирает ответ (ключ API, разумеется, замаскирован — показываю только логику):
/* php */
class YandexClient {
private $apiKey;
private $folderId;
private $modelName;
public function __construct() {
// Ключ и ID каталога читаются из переменных окружения,
// а не хранятся прямо в коде — это отдельный секретный файл,
// недоступный из браузера ни при каком URL.
$this->apiKey = getenv('YANDEX_API_KEY'); // Токен Яндекс: AQVN••••••••••••••••3f8k
$this->folderId = getenv('YANDEX_FOLDER_ID'); // b1g••••••••••••••01u
$this->modelName = getenv('YANDEX_MODEL_NAME');
}
public function sendMessage($messages) {
$url = 'https://llm.api.cloud.yandex.net/foundationModels/v1/completion';
$modelUri = "gpt://{$this->folderId}/{$this->modelName}/latest";
$data = [
'modelUri' => $modelUri,
'completionOptions' => [
'stream' => false,
'temperature' => 0.6, // баланс "живости" и предсказуемости ответа
'maxTokens' => '2000' // жёсткий потолок длины одного ответа
],
'messages' => $messages
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
"Authorization: Api-Key {$this->apiKey}"
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 25);
$response = curl_exec($ch);
$result = json_decode($response, true);
return $result['result']['alternatives'][0]['message']['text'];
}
}💸 Почему именно YandexGPT, а не что-то ещё
Выбор модели — не случайность, а результат вполне прагматичного расчёта.
Во-первых, на старте у Yandex Cloud есть пробный период: новому платёжному аккаунту даётся грант на 4 000 рублей на 60 дней — этого гранта хватает, чтобы полноценно обкатать бота на реальном трафике сайта, снять метрики по расходу токенов и понять экономику проекта ещё до того, как начнёт списываться реальная оплата.
Во-вторых — уже по деньгам после пробного периода. Для задачи «консультант на сайте», где каждый ответ — это несколько абзацев текста, а не многостраничная аналитика, YandexGPT-lite остаётся одним из самых дешёвых по стоимости токена вариантов на рынке. Для такого объёма это означает: даже при активном общении посетителей стоимость обслуживания чата остаётся копеечной по сравнению со стоимостью привлечения самого посетителя на сайт (реклама, SEO) — экономика легко сходится даже без оптимизации промпта под минимальный расход токенов.
В-третьих — данные остаются в российской юрисдикции, что для клиентского сервиса на российском рынке снимает лишние вопросы про хранение персональных данных за рубежом.
🛤️ Что на самом деле происходит между вопросом и ответом
Разберу весь путь одного сообщения по шагам — от нажатия Enter посетителем до текста, который он видит на экране.
✍️ Шаг 1. Клиент собирает и отправляет вопрос.
embed.js берёт текст из поля ввода и отправляет его на сервер вместе с идентификатором сессии — сам текст переписки на клиенте нигде не хранится, только этот id.
🗄️ Шаг 2. Сервер пишет вопрос в базу данных ещё до обращения к модели.
Это важно: если YandexGPT вдруг ответит с ошибкой или таймаутом, вопрос пользователя всё равно не потеряется — он уже сохранён.
🧩 Шаг 3. Сервер собирает контекст для модели — три источника, склеенные в один системный промпт.
/* php */
$systemPrompt = file_get_contents('private/prompt-rules.md') // как отвечать: тон, формат
. file_get_contents('private/knowledge.md') // что известно о бизнесе
. renderContactsAsTokenList(getContacts()); // какие сейчас есть токены контактов🕰️ Шаг 4. К системному промпту добавляется история диалога.
Если с последнего сообщения этой сессии прошло меньше 24 часов — сессия считается «живой», и модель получает предыдущие реплики целиком (до 30 последних сообщений), чтобы помнить контекст. Если сессия «остыла» — контекст начинается заново, как будто это новый разговор.
📡 Шаг 5. Собранный массив сообщений уходит в YandexGPT.
system + история + новый вопрос отправляются тем самым методом sendMessage() выше. Модель не знает заранее, что ответит — генерирует текст токен за токеном на основе всего, что получила в контексте.
📥 Шаг 6. Сырой ответ модели сохраняется в базу как есть, без обработки.
Именно как есть — если модель вставила {{contacts_block}}, в базе останется этот технический токен, а не готовый развёрнутый номер телефона. Это принципиально: на следующем ходу диалога модель должна увидеть в истории свой же вызов токена, а не готовый текст с цифрами — иначе она начинает пересказывать номер своими словами, путая цифры.
🎨 Шаг 7. Только перед показом ответ проходит цепочку обработки.
Снимаются случайные обратные кавычки вокруг токенов → markdown превращается в безопасный HTML (Parsedown в safe-режиме) → токены контактов разворачиваются в реальные кликабельные ссылки → уже в браузере ответ ещё раз прогоняется через DOMPurify перед вставкой в страницу — второй независимый барьер защиты от вредоносного кода в тексте.
📤 Шаг 8. Готовый HTML уходит клиенту.
embed.js вставляет его в окно чата — и вот тот самый ответ, который видит посетитель.
🔗 Контакты — не текстом, а токенами
Вот деталь, которой я особенно горжусь с инженерной точки зрения. Модель никогда не пишет номер телефона или ссылку на Telegram сама — вместо этого она вставляет в ответ технический токен вида {{contacts_block}} или {{phone}}, а уже сервер разворачивает его в готовую кликабельную ссылку:
/* php */
function expandContactTokens(string $html, array $contacts): string {
// {{contacts_block}} → полный список каналов связи,
// {{phone}}, {{telegram}}, {{whatsapp}} → отдельные ссылки
return str_replace(
array_keys($contacts),
array_values($contacts),
$html
);
}Зачем так сложно? Потому что LLM время от времени «доизобретает» цифры в номерах телефонов или путает ссылки — известная особенность любых генеративных моделей. Если контакты меняются (новый номер, другой мессенджер), я правлю их в одном файле — и бот сразу во всех будущих ответах отдаёт актуальные данные, ему физически неоткуда взять другие.
🔒 Что видит браузер посетителя — и что нет
Проект аккуратно разделён на две зоны:
•Публичная часть (public/) — то, что реально отдаётся браузеру: сам скрипт виджета, картинки, два лёгких API-эндпоинта.
•Приватная часть (private/) — системный промпт, база знаний, ключ API, вся логика работы с базой данных. Эта папка физически недоступна из браузера ни при каком URL — на уровне веб-сервера стоит правило Require all denied. Даже если кто-то узнает точный путь к файлу с промптом, сервер ответит «403 Доступ запрещён», а не отдаст содержимое.
Это стандартная практика для серьёзных бэкенд-проектов, но именно в мире «быстрых чат-ботов на коленке» её часто пропускают — секреты и системные инструкции оставляют лежать рядом с публичными файлами.
💾 База данных — файл, а не дорогой сервер
Вся переписка хранится в SQLite — облегчённой базе данных, которая живёт в одном обычном файле, без отдельного сервера СУБД. Для проекта такого масштаба это осознанный плюс: никаких лишних движущихся частей, резервная копия — это просто скопировать один файл.
Маленькая деталь для тех, кто ценит защиту в глубину: файл базы данных называется conversations.db.php, а не conversations.db. Расширение .php — не потому что это программный код, а ровно наоборот: если бы кто-то узнал прямой путь к файлу, веб-сервер попытался бы исполнить его как PHP-скрипт (получит пустоту) вместо того, чтобы просто отдать сырые байты базы данных на скачивание. Простой, но эффективный трюк.
🔄 Живая синхронизация — там, где обычно все останавливаются
Вот сценарий, который легко упустить при проектировании: посетитель открыл сайт в двух вкладках браузера одновременно (так бывает чаще, чем кажется) — обе используют одну и ту же сессию диалога. Он написал вопрос в одной вкладке, а окно чата во второй — так и не узнало об этом, даже спустя долгое время.
Я закрыл этот пробел сразу в трёх местах проекта — на стороне посетителя и на стороне администратора, и у каждого своя логика поведения.
👤 На стороне посетителя: тихая дозагрузка без единого лишнего клика
Пока окно чата открыто, скрипт раз в 15 секунд незаметно сверяется с сервером — не появилось ли новых сообщений в этой же сессии из другой вкладки:
/* javascript */
// Общая сверка с сервером — вызывается и при открытии окна, и по
// таймеру, пока окно остаётся открытым.
async function syncWithServer() {
try {
const res = await fetch(`${CONFIG_URL}?sessionId=${encodeURIComponent(sessionId)}`);
const data = await res.json();
if (data.totalCount > knownMessageCount) {
const missingCount = data.totalCount - knownMessageCount;
const newTail = data.history.slice(-missingCount);
// Автоскролл вниз — только если пользователь и так был
// у нижнего края чата. Если он листает историю выше —
// его текущий вид не трогаем.
const wasAtBottom =
messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < 40;
appendHistoryMessages(newTail);
if (wasAtBottom) messagesEl.scrollTop = messagesEl.scrollHeight;
knownMessageCount = data.totalCount;
}
} catch (e) {
// Тихо игнорируем — не критично, просто не дозагрузили в этот раз.
}
}
// Таймер живёт, только пока окно чата открыто
let pollIntervalId = setInterval(syncWithServer, 15000);
// И гарантированно гасится при закрытии окна —
// незачем слать запросы, пока чат никто не смотрит
function stopPolling() {
clearInterval(pollIntervalId);
pollIntervalId = null;
}Новые сообщения просто тихо появляются в конце переписки — как в обычном мессенджере, никакого баннера, никакого «нажмите, чтобы обновить». Для посетителя это и должно выглядеть максимально естественно.
Рис. 2. Автоматическая дозагрузка диалога без единого лишнего клика.
📋 На стороне администратора: список диалогов
В админ-панели логика уже другая — намеренно. Администратор часто читает или копирует текст переписки в моменте, и внезапная подмена контента у него на экране может сбить с толку. Поэтому вместо тихой дозагрузки — ненавязчивый баннер с приглашением обновить вручную:
/* javascript */
const initial = { sessionCount: <?= $initialMarker['sessionCount'] ?>,
latestAt: <?= json_encode($initialMarker['latestAt']) ?> };
async function checkForUpdates() {
try {
const res = await fetch('check-updates.php');
const data = await res.json();
// Сравниваем со снимком состояния на момент загрузки страницы
if (data.sessionCount !== initial.sessionCount || data.latestAt !== initial.latestAt) {
document.getElementById('u11-update-banner').style.display = 'block';
}
} catch (e) {
// Тихо игнорируем — следующая попытка через 25 секунд
}
}
setInterval(checkForUpdates, 25000);Серверная часть этого опроса — намеренно «дешёвый» запрос, который не тянет за собой всю переписку, а лишь две цифры:
/* php */
// check-updates.php
function getActivityMarker() {
$stmt = getDb()->query(
'SELECT COUNT(DISTINCT session_id) AS session_count,
MAX(created_at) AS latest_at
FROM messages'
);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
echo json_encode(getActivityMarker());
Рис. 4. Вид на стороне администратора: список диалогов.
💬 На стороне администратора: просмотр конкретного диалога
Отдельная и более точная версия того же механизма — когда админ открыл переписку одного конкретного посетителя. Здесь опрос не сканирует всю базу целиком, а считает сообщения только этой сессии:
/* php */
// check-updates.php — с параметром ?session=...
$sessionId = $_GET['session'] ?? null;
if ($sessionId) {
function countSessionMessages($sessionId) {
$stmt = getDb()->prepare('SELECT COUNT(*) FROM messages WHERE session_id = ?');
$stmt->execute([$sessionId]);
return (int) $stmt->fetchColumn();
}
echo json_encode(['messageCount' => countSessionMessages($sessionId)]);
}/* javascript */
const sessionId = <?= json_encode($viewingSessionId) ?>;
const initialMessageCount = <?= (int) $initialMessageCount ?>;
async function checkForSessionUpdates() {
const res = await fetch('check-updates.php?session=' + encodeURIComponent(sessionId));
const data = await res.json();
if (data.messageCount > initialMessageCount) {
document.getElementById('u11-session-update-banner').style.display = 'block';
}
}
setInterval(checkForSessionUpdates, 15000);Интересная деталь: два таймера админки — 25 секунд для списка и 15 секунд для конкретного диалога — никогда не тикают одновременно. Это не результат специальной блокировки, а просто следствие того, что это обычная многостраничная навигация: при переходе со списка в диалог браузер полностью перезагружает страницу, а вместе с ней — уничтожает предыдущий JS-таймер.
Рис. 5. Вид на стороне администратора: просмотр конкретного диалога.
🚀 Плашка-приглашение — небольшая деталь, которая работает на конверсию
Кнопка чата в углу экрана — это хорошо, но люди не всегда её замечают. Поэтому рядом с кнопкой через несколько секунд после появления показывается ненавязчивая плашка-приглашение:
«Помочь с проектом? 🚀»
Она не мигает и не преследует посетителя — появляется один раз за сутки на устройство, «живёт» рядом с кнопкой около 10 секунд и мягко гаснет, если на неё не среагировали. Клик по ней сразу открывает окно чата — как если бы кликнули по самой кнопке.
Логика показа продумана так, чтобы плашка не раздражала при повторных заходах на сайт — весь цикл держится на нескольких константах и одном localStorage-флаге, общем для всех вкладок и страниц сайта:
/* javascript */
const BUBBLE_COOLDOWN_MS = 24 * 60 * 60 * 1000; // не чаще раза в сутки
const BUBBLE_SHOW_DELAY_MS = 3000; // задержка перед показом
const BUBBLE_VISIBLE_WINDOW_MS = 10000; // сколько "живёт" на экране
let bubbleShownAt = null;
let bubblePendingShow = false;
let bubblePermanentlyClosed = false;
function isBubbleEligible() {
const lastShown = Number(localStorage.getItem('u11_bubble_last_shown') || 0);
return !bubblePermanentlyClosed && (Date.now() - lastShown) > BUBBLE_COOLDOWN_MS;
}
function showBubbleNow() {
bubble.classList.add('visible');
bubbleShownAt = Date.now();
localStorage.setItem('u11_bubble_last_shown', String(bubbleShownAt));
// Плашка обязана "прожить" 10 секунд с момента ПЕРВОГО показа,
// даже если пользователь несколько раз проскроллит вверх-вниз
setTimeout(() => {
bubble.classList.remove('visible'); // плавное угасание по CSS-transition
bubblePermanentlyClosed = true;
}, BUBBLE_VISIBLE_WINDOW_MS);
}
function closeBubbleInstantly() {
// Мгновенно, БЕЗ fade-анимации — иначе при клике на 9-й секунде
// плашка "доигрывала" бы исчезновение уже поверх открытого чата
bubble.style.transition = 'none';
bubble.classList.remove('visible');
requestAnimationFrame(() => { bubble.style.transition = ''; });
bubblePermanentlyClosed = true;
}
function handleScroll() {
const buttonVisible = window.scrollY > SCROLL_THRESHOLD;
btn.classList.toggle('visible', buttonVisible);
if (buttonVisible && isBubbleEligible() && !bubbleShownAt) {
setTimeout(() => {
if (buttonVisible) showBubbleNow();
else bubblePendingShow = true; // покажем, как только кнопка снова станет видна
}, BUBBLE_SHOW_DELAY_MS);
}
}
btn.addEventListener('click', closeBubbleInstantly);
bubble.addEventListener('click', () => { closeBubbleInstantly(); openChat(); });Небольшой психологический приём («предложить конкретное действие», а не просто показать иконку) — и заметный прирост CTR по кнопке чата на практике.
Рис. 6. Плашка-приглашение виджета — небольшая деталь, которая работает на конверсию.
🛠️ Админ-панель — переписка под контролем, без сторонних сервисов
Отдельная закрытая панель для меня как владельца:
•список всех диалогов с поиском по содержимому (в том числе на кириллице, регистронезависимо);
•просмотр полной переписки конкретного посетителя;
•ненавязчивый баннер «Появились новые сообщения» — тоже живая синхронизация, только уже на моей стороне: не нужно вручную обновлять страницу, чтобы увидеть, продолжил ли клиент диалог;
•управление доступом других администраторов (с ролями и лимитом аккаунтов — на случай, если в будущем к проекту подключатся коллеги).
Рис. 7. Вид админ-панели с пользователями.
🔎 Поиск на кириллице — и грабли, на которые я наступил
Казалось бы, простая задача — «искать по переписке» — на деле принесла пару неочевидных нюансов.
Первый: встроенная в SQLite функция LOWER() умеет приводить к нижнему регистру только латиницу — «Тест» и «тест» для неё две разные строки. Решение — хранить рядом с каждым сообщением ещё одну колонку, content_lower, уже приведённую к нижнему регистру средствами PHP (mb_strtolower(), который умеет в UTF-8 корректно), и искать именно по ней, тоже предварительно приводя поисковый запрос к нижнему регистру.
Второй нюанс оказался интереснее и всплыл только на реальных данных: в первой версии поиска запрос напрямую выбирал строки из таблицы сообщений — а если посетитель в одном диалоге упоминал искомое слово несколько раз (например, дважды написал «сайт»), тот же самый диалог дублировался в списке результатов несколько раз подряд. Внешне выглядело как баг с базой данных, а на деле — просто отсутствие группировки по диалогу:
/* php */
// Так БЫЛО — один диалог мог попасть в результаты
// столько раз, сколько раз в нём встретилось слово:
$stmt = $db->prepare(
'SELECT session_id, created_at FROM messages
WHERE content_lower LIKE ?'
);
// Так СТАЛО — сначала находим уникальные session_id,
// затем уже собираем сводку по каждому диалогу одним рядом:
$stmt = $db->prepare(
"SELECT session_id, MIN(created_at) AS started_at,
MAX(created_at) AS last_message_at,
COUNT(*) AS messages_count
FROM messages
WHERE session_id IN (
SELECT DISTINCT session_id FROM messages WHERE content_lower LIKE ?
)
GROUP BY session_id
ORDER BY last_message_at DESC"
);Простое исправление, но полезный урок: с базами данных первая «очевидная» версия запроса не всегда учитывает связи между строками — GROUP BY и DISTINCT здесь не формальность, а необходимость.
✉️ Восстановление пароля — письмо с доменной почты
Вход в админку защищён паролем, и на случай, если я его забуду — есть восстановление по email со ссылкой, ограниченной по времени действия. Сама отправка письма — через встроенную функцию PHP mail(), без подключения платных сервисов рассылок:
/* php */
$fromAddress = 'no-reply@' . $domainHost; // например, no-reply@u11.ru
$headers = "From: {$fromAddress}\r\nContent-Type: text/plain; charset=UTF-8\r\n";
// Envelope sender задаётся ЯВНО через параметр -f — без этого письма
// не доходили даже при корректно настроенных SPF и DKIM в DNS: сервер
// подставлял свой технический адрес в служебный MAIL FROM на уровне
// SMTP-диалога, и почтовые провайдеры проверяли SPF именно по нему,
// а не по видимому заголовку «От кого» в самом письме.
$envelopeSender = '-f' . $fromAddress;
mail($toEmail, $subject, $body, $headers, $envelopeSender);Отдельно стоит сказать: одного правильного кода недостаточно. Чтобы письма с доменной почты не улетали в спам (а часто — вообще не доставлялись), в DNS-записях домена обязательно должны быть настроены SPF (список серверов, которым разрешено отправлять почту от имени домена) и DKIM (цифровая подпись письма, подтверждающая, что оно не подделано по дороге). Без этих двух записей даже идеальный код отправки будет проигрывать спам-фильтрам получателя.
По самой ссылке восстановления — не буду приводить код проверки токена: он специально устроен так, чтобы ссылка была одноразовой, ограниченной по времени и непредсказуемой, и раскрывать логику её генерации и валидации в публичной статье — плохая идея с точки зрения безопасности. Важно, что сам функционал есть и работает надёжно.
Вход защищён полноценно: cookie-сессии с истечением, блокировка при переборе пароля, восстановление по email, CSRF-защита форм. Это не игрушечная админка «на один пароль в коде» — а такой же серьёзный уровень защиты, какой я закладываю в проекты клиентов.
Рис. 8. Восстановление пароля через письмо с доменной почты.
🍪 Про куки и что вообще хранится у посетителя
Прозрачность — часть экспертной репутации, поэтому коротко закрою и этот вопрос. У виджета на стороне браузера хранится только техническое необходимое:
•идентификатор текущей сессии диалога (чтобы бот помнил контекст разговора при перезагрузке страницы);
•отметка о том, что плашка-приглашение уже показывалась сегодня (чтобы не показывать её повторно и не раздражать).
Никакой личной информации, никакой рекламной рассылки, никакой продажи данных третьим лицам — это моя собственная инфраструктура, а не чужой сервис, зарабатывающий на данных пользователей.
💡 Зачем всё это разработчику сайтов на Tilda
Я мог бы подключить готовый виджет от стороннего сервиса за 10 минут. Но тогда я не смог бы объяснить клиенту ни одной технической детали того, как он работает — просто пересказал бы маркетинговую страницу разработчика. Написав ассистента самостоятельно, я точно знаю, что происходит с каждым словом, которое говорит бот от лица моей студии, и могу так же спроектировать индивидуальное решение для клиента — с его собственной базой знаний, его тоном общения, его правилами.
Если вам интересно похожее решение для вашего сайта — обычный чат-бот, консультант по каталогу, квалификация заявок перед звонком менеджера — пишите, обсудим, что имеет смысл именно для вашего бизнеса.
⚡ Заключение
ИИ-ассистент на сайте в 2026 году — это не модная игрушка «для галочки», а инструмент, который либо снимает часть нагрузки с менеджера и доводит посетителя до заявки, либо просто висит иконкой в углу экрана без дела. Разница между этими двумя сценариями редко в самом факте наличия чат-бота — чаще в том, кто его настроил: сторонний сервис по шаблону или разработчик, который контролирует каждую строку промпта и знает, что происходит с данными Ваших клиентов.
Если Вы сомневаетесь, нужен ли Вам ИИ-ассистент или его стоит доработать под конкретные задачи — начните с честного разбора того, что у Вас есть сейчас. Иногда решение не в подключении очередного SaaS-виджета, а в написании собственного бота под Вашу базу знаний и Ваш тон общения.
Помогу настроить своего ИИ-ассистента на YandexGPT — с закрытой базой данных, админ-панелью и без ежемесячной платы за чужой сервис.
Готов бесплатно разобрать Ваш сайт и показать, где именно теряются заявки — возможно, ИИ-ассистент решит эту задачу быстрее, чем кажется. Написать можно через u11.ru или в блоге u11.ru/blog, где я разбираю подобные кейсы регулярно.
Можете также пройти опрос или заполнить форму обратной связи ниже. Я обязательно отвечу удобным для Вас способом в течение 15 минут в рабочее время.