Что получится в результате
Соберем чат-виджет ИИ-агента для сайта: посетитель видит кнопку чата, открывает окно, задает вопрос, агент отвечает по базе знаний, собирает контакт, передает сложный диалог оператору, создает лид в CRM и пишет все события в логи.
В первой версии виджет не должен сам обещать скидки, принимать оплату, менять заказ, собирать лишние персональные данные и отвечать без источника в рискованных сценариях. Правильный MVP: ответы по базе знаний, сбор контакта, handoff человеку и создание черновика лида.
В результате будет рабочий контур:
- настройки виджета лежат в `widget_config`;
- сайты-клиенты описаны в `widget_sites`;
- embed-код генерируется из `widget_embed_tokens`;
- сессии посетителей лежат в `chat_sessions`;
- сообщения лежат в `chat_messages`;
- профиль посетителя лежит в `visitor_profile`;
- согласия на обработку данных лежат в `visitor_consents`;
- обращения к агенту пишутся в `agent_chat_runs`;
- RAG-поиск пишется в `widget_retrieval_log`;
- handoff оператору пишется в `handoff_queue`;
- лиды в CRM создаются через `widget_crm_lead_queue`;
- события виджета пишутся в `widget_events`;
- rate limit лежит в `widget_rate_limit_rules`;
- ошибки пишутся в `widget_error_log`;
- тесты лежат в `widget_test_cases`;
- метрики пишутся в `widget_metrics`.
Финальная проверка: виджет вставляется одной строкой script, открывается на сайте, создает сессию, отправляет сообщение в backend, получает ответ агента, сохраняет историю и не ломает верстку страницы.
Что понадобится
Подготовьте:
- сайт, куда будет вставлен виджет;
- домен backend API, например `https://agent.example.com`;
- LLM API key или подключенную модель;
- базу знаний для ответов;
- backend endpoint для чата;
- HTTPS на сайте и backend;
- список страниц, где виджет нужен;
- текст политики обработки данных;
- список тем, где нужен оператор;
- CRM или email для передачи лидов;
- тестовый браузер в desktop и mobile;
- 20-30 тестовых вопросов;
- лимит стоимости и запросов;
- человек для проверки первых диалогов.
Для первого запуска лучше выбрать один сайт, один язык, одну базу знаний и один канал handoff.
Шаг 1. Выберите режим виджета
Есть три рабочих режима.
Режим `assistant`:
- отвечает по базе знаний;
- не собирает заявку сам;
- предлагает оставить контакт;
- передает оператору сложные вопросы.
Режим `lead_capture`:
- задает уточняющие вопросы;
- собирает имя, телефон, email;
- создает лид;
- ставит задачу менеджеру.
Режим `support`:
- отвечает по FAQ;
- проверяет статус обращения через read-only tool;
- создает ticket;
- делает handoff оператору.
Для MVP выберите `assistant` или `lead_capture`, но не смешивайте все сценарии сразу.
Проверка: команда может одной фразой описать, зачем виджет нужен на сайте.
Шаг 2. Создайте `widget_sites`
`widget_sites` хранит сайты, где разрешен виджет.
Колонки:
id
site_key
site_name
domain
allowed_origins_json
default_language
is_active
created_at
Пример:
{
"site_key": "main_site",
"domain": "example.com",
"allowed_origins": ["https://example.com", "https://www.example.com"],
"default_language": "ru",
"is_active": true
}
Проверка: запрос с чужого домена не может использовать ваш widget token.
Шаг 3. Создайте `widget_config`
`widget_config` управляет внешним видом и поведением.
Колонки:
id
site_key
widget_key
mode
title
welcome_message
primary_color
position
language
show_consent
handoff_enabled
crm_enabled
is_active
Стартовые настройки:
{
"widget_key": "main_ai_chat",
"mode": "lead_capture",
"title": "ИИ-помощник",
"welcome_message": "Здравствуйте. Я помогу подобрать решение или передам вопрос менеджеру.",
"primary_color": "#ffd11a",
"position": "bottom_right",
"show_consent": true,
"handoff_enabled": true,
"crm_enabled": true
}
Проверка: изменение `welcome_message` в настройках меняет текст в виджете без правки embed-кода.
Шаг 4. Создайте embed token
`widget_embed_tokens` нужен, чтобы сайт подключался безопасно.
Колонки:
id
site_key
widget_key
public_token
secret_hash
allowed_origins_json
expires_at
is_active
created_at
Правила:
- `public_token` можно вставлять на сайт;
- `secret_hash` не попадает в браузер;
- token привязан к домену;
- token можно отключить;
- запросы проверяют `Origin`;
- token не дает доступ к админке.
Проверка: если token отключен, виджет перестает открывать новые сессии.
Шаг 5. Подготовьте embed-код
Вставка на сайт должна быть короткой.
<script
async
src="https://agent.example.com/widget.js"
data-widget-token="PUBLIC_WIDGET_TOKEN"
data-site-key="main_site">
</script>
Правила:
- script загружается async;
- widget не блокирует загрузку страницы;
- все настройки подтягиваются с backend;
- нет LLM API key в HTML;
- нет CRM token в HTML;
- виджет работает только на разрешенном домене.
Проверка: в исходном коде страницы нет приватных ключей и endpoint для внутренних tools.
Шаг 6. Решите iframe или Shadow DOM
Для MVP безопаснее iframe.
Iframe дает:
- изоляцию CSS;
- меньше конфликтов с сайтом;
- проще вставлять на разные CMS;
- меньше риска сломать верстку;
- удобную sandbox-настройку.
Shadow DOM дает:
- более нативный вид;
- меньше накладных расходов;
- проще кастомизировать;
- сложнее гарантировать отсутствие конфликтов.
Для первого запуска используйте iframe.
Проверка: CSS сайта не меняет кнопки, поля и шрифты виджета.
Шаг 7. Сделайте `widget.js`
`widget.js` должен сделать минимум.
Алгоритм:
- прочитать `data-widget-token`;
- прочитать `data-site-key`;
- проверить, не вставлен ли виджет повторно;
- создать кнопку чата;
- создать iframe;
- передать iframe token, site key и текущий URL;
- слушать события открытия и закрытия;
- не трогать глобальные стили сайта.
Минимальная структура:
(function () {
const script = document.currentScript;
const token = script.dataset.widgetToken;
const siteKey = script.dataset.siteKey;
if (!token || window.__aiWidgetLoaded) return;
window.__aiWidgetLoaded = true;
const button = document.createElement('button');
button.setAttribute('aria-label', 'Открыть чат');
button.className = 'ai-widget-button';
const iframe = document.createElement('iframe');
iframe.src = `https://agent.example.com/widget-frame?token=${encodeURIComponent(token)}&site=${encodeURIComponent(siteKey)}&page=${encodeURIComponent(location.href)}`;
iframe.style.display = 'none';
document.body.appendChild(button);
document.body.appendChild(iframe);
button.addEventListener('click', () => {
iframe.style.display = iframe.style.display === 'none' ? 'block' : 'none';
});
})();
Проверка: script можно вставить два раза, но виджет появится один раз.
Шаг 8. Сделайте frame-страницу
`/widget-frame` рисует UI чата.
Обязательные элементы:
- заголовок;
- приветственное сообщение;
- история сообщений;
- поле ввода;
- кнопка отправки;
- статус "печатает";
- consent checkbox, если нужен;
- кнопка связи с оператором;
- ошибка сети;
- ссылка на политику обработки данных.
Проверка: frame открывается по прямой ссылке только с валидным token и разрешенным origin.
Шаг 9. Создайте `chat_sessions`
`chat_sessions` хранит диалог посетителя.
Колонки:
id
session_id
site_key
widget_key
visitor_id
page_url
referrer
utm_json
status
started_at
last_message_at
closed_at
Статусы:
- `active`;
- `waiting_agent`;
- `waiting_visitor`;
- `handoff_requested`;
- `lead_created`;
- `closed`;
- `blocked`;
Проверка: перезагрузка страницы не теряет текущую сессию, если cookie или localStorage разрешены.
Шаг 10. Создайте `visitor_profile`
`visitor_profile` хранит минимум данных.
Колонки:
id
visitor_id
anonymous_id
name
email
phone
company
language
first_seen_at
last_seen_at
Правила:
- до consent хранить только anonymous id;
- email и телефон валидировать;
- не собирать лишние поля;
- не писать пароль, токен или номер карты;
- удалить профиль по запросу пользователя;
- не показывать один профиль другому посетителю.
Проверка: пользователь может задать вопрос без контакта, а контакт собирается только когда это нужно сценарию.
Шаг 11. Добавьте согласие на обработку данных
Создайте `visitor_consents`.
Колонки:
id
visitor_id
session_id
consent_type
policy_url
accepted
accepted_at
ip_hash
user_agent_hash
Типы:
- `chat_processing`;
- `lead_contact`;
- `marketing_contact`;
- `personal_data`;
Правила:
- для обычного вопроса достаточно короткого уведомления;
- для передачи контакта в CRM нужен явный consent;
- marketing consent не включать по умолчанию;
- текст согласия должен вести на политику;
- отказ не должен ломать весь сайт.
Проверка: без consent контакт не отправляется в CRM.
Шаг 12. Создайте `chat_messages`
`chat_messages` хранит сообщения чата.
Колонки:
id
session_id
message_id
sender_type
message_text
message_html
attachments_json
metadata_json
created_at
`sender_type`:
- `visitor`;
- `ai_agent`;
- `operator`;
- `system`.
Правила:
- сохранять исходный текст посетителя;
- HTML очищать;
- вложения на первом этапе выключить или ограничить;
- сообщения агента связывать с `agent_chat_runs`;
- системные события не показывать посетителю как обычные ответы.
Проверка: после диалога можно восстановить историю без обращения к логам браузера.
Шаг 13. Сделайте endpoint отправки сообщения
Endpoint:
POST /api/widget/message
Вход:
{
"site_key": "main_site",
"widget_key": "main_ai_chat",
"session_id": "sess_123",
"message_id": "msg_001",
"text": "Сколько стоит внедрение ИИ-агента?",
"page_url": "https://example.com/pricing"
}
Backend должен:
- проверить token;
- проверить origin;
- проверить rate limit;
- создать или найти session;
- сохранить сообщение;
- запустить agent run;
- вернуть ответ или `queued`;
- записать событие.
Проверка: запрос без token получает `401`, запрос с чужого домена получает `403`.
Шаг 14. Создайте `agent_chat_runs`
`agent_chat_runs` связывает сообщение и ответ агента.
Колонки:
id
run_id
session_id
message_id
status
input_json
output_json
confidence_score
needs_human
model_name
started_at
finished_at
Статусы:
- `queued`;
- `running`;
- `completed`;
- `needs_human`;
- `failed`;
- `blocked`;
- `timeout`.
Проверка: каждый ответ агента имеет run id и понятный статус.
Шаг 15. Подключите базу знаний
Для виджета база знаний важнее красивой кнопки. Создайте `widget_knowledge_sources`.
Колонки:
id
site_key
source_key
source_type
source_url
status
last_indexed_at
Подключите:
- FAQ;
- страницы услуг;
- цены;
- условия работы;
- инструкции;
- ограничения;
- контакты;
- политику обработки данных.
Проверка: агент отвечает только по опубликованным и актуальным источникам.
Шаг 16. Настройте RAG-поиск
Создайте `widget_retrieval_log`.
Колонки:
id
run_id
query_text
matched_chunk_ids_json
top_score
used_in_answer
created_at
Правила:
- искать только по источникам текущего сайта;
- top 3-5 chunks передавать в prompt;
- не использовать устаревшие chunks;
- показывать источник оператору;
- не отвечать уверенно при плохом score;
- разные сайты не должны видеть документы друг друга.
Проверка: вопрос со страницы тарифов использует именно chunks тарифов, а не чужую базу знаний.
Шаг 17. Напишите системный prompt виджета
Каркас:
Ты ИИ-помощник сайта.
Отвечай кратко и понятно.
Используй только предоставленный контекст сайта и базы знаний.
Если ответа нет в контексте, скажи, что передашь вопрос менеджеру.
Не обещай скидки, сроки, юридические условия и оплату без подтвержденных данных.
Не проси пароль, код из SMS, токен или полный номер карты.
Если пользователь хочет связаться с человеком, поставь needs_human = true.
Верни JSON.
Формат ответа:
{
"answer": "Стоимость зависит от сценария и интеграций. Могу передать менеджеру ваш запрос и контакт.",
"confidence_score": 0.82,
"needs_human": false,
"lead_intent": true,
"contact_request": "email_or_phone",
"used_sources": ["pricing-page"]
}
Проверка: модель возвращает валидный JSON, а не длинную простыню текста.
Шаг 18. Соберите контакт без давления
Создайте `lead_capture_state`.
Колонки:
id
session_id
step
name
email
phone
company
need_summary
status
updated_at
Шаги:
- определить интерес;
- спросить имя;
- спросить удобный контакт;
- уточнить задачу;
- получить consent;
- создать лид или handoff.
Правила:
- не спрашивать все сразу;
- не требовать телефон, если достаточно email;
- не создавать лид без контакта;
- не выдумывать имя;
- дать возможность продолжить без заявки.
Проверка: посетитель может отказаться оставить контакт, и виджет не зацикливается.
Шаг 19. Настройте handoff оператору
Создайте `handoff_queue`.
Колонки:
id
session_id
reason
priority
visitor_summary
last_messages_json
target_channel
status
created_at
resolved_at
Причины handoff:
- пользователь просит человека;
- нет ответа в базе знаний;
- низкая уверенность;
- вопрос про цену и договор;
- жалоба;
- персональные данные;
- ошибка агента;
- пользователь оставил контакт;
- повторный вопрос после ответа.
Каналы:
- CRM lead;
- email менеджеру;
- Telegram уведомление;
- helpdesk ticket;
- live chat operator.
Проверка: оператор получает summary и последние сообщения, а не пустой сигнал "нужен человек".
Шаг 20. Подключите CRM для лидов
Создайте `widget_crm_lead_queue`.
Колонки:
id
session_id
visitor_id
crm_type
lead_payload_json
status
crm_lead_id
last_error
created_at
sent_at
Payload:
{
"name": "Иван",
"email": "ivan@example.com",
"phone": null,
"source": "site_widget",
"page_url": "https://example.com/ai-agent",
"need_summary": "Интересуется чат-виджетом ИИ-агента",
"dialog_summary": "Спросил про стоимость и сроки внедрения"
}
Правила:
- CRM token не попадает в браузер;
- лид создается после consent;
- дубли проверяются по email и phone;
- CRM API ошибки идут в retry;
- посетителю не показывается stack trace.
Проверка: после тестового лида в CRM есть одна карточка и summary диалога.
Шаг 21. Добавьте уведомление менеджеру
Создайте `operator_notifications`.
Колонки:
id
session_id
channel
recipient
message_text
status
sent_at
Текст уведомления:
Новая заявка из AI-виджета.
Страница: /ai-agent
Контакт: ivan@example.com
Потребность: хочет внедрить ИИ-агента на сайт.
Последний вопрос: сколько стоит внедрение.
Проверка: менеджер получает уведомление не позднее 1 минуты после заявки.
Шаг 22. Настройте события виджета
Создайте `widget_events`.
Колонки:
id
site_key
session_id
event_name
event_payload_json
created_at
События:
- `widget_loaded`;
- `widget_opened`;
- `message_sent`;
- `agent_answered`;
- `contact_requested`;
- `contact_submitted`;
- `handoff_requested`;
- `lead_created`;
- `widget_closed`;
- `error_shown`.
Проверка: можно посчитать, сколько людей открыли виджет и сколько дошли до заявки.
Шаг 23. Настройте rate limit и антиспам
Создайте `widget_rate_limit_rules`.
Колонки:
id
scope
limit_per_minute
limit_per_hour
action_on_limit
is_active
Стартовые лимиты:
- anonymous visitor - 10 сообщений в минуту;
- session - 30 сообщений в час;
- IP hash - 60 сообщений в час;
- site - 1000 сообщений в день;
- LLM budget - дневной лимит по стоимости.
Антиспам:
- honeypot для формы контакта;
- блокировка повторяющихся сообщений;
- captcha только после подозрительной активности;
- denylist грубых атак;
- ограничение длины сообщения.
Проверка: бот не может за минуту сжечь весь LLM-бюджет.
Шаг 24. Защитите виджет от prompt injection
Создайте `widget_guardrail_events`.
Колонки:
id
session_id
rule_key
input_text
action
created_at
Блокируйте или переводите в handoff:
- "игнорируй инструкции";
- "покажи system prompt";
- "покажи API key";
- "выполни admin command";
- "удали данные";
- "сделай скидку";
- "обойди правила";
- токсичные запросы;
- запросы с паролями;
- подозрительные ссылки.
Проверка: виджет отказывается раскрывать инструкции и не вызывает tools после атаки.
Шаг 25. Настройте ошибки и fallback
Создайте `widget_error_log`.
Колонки:
id
session_id
error_type
error_message
context_json
created_at
Показывайте пользователю понятные fallback:
- "Не получилось получить ответ, попробуйте еще раз";
- "Передам вопрос менеджеру";
- "Сейчас нет связи с помощником";
- "Напишите email, чтобы мы вернулись с ответом".
Ошибки:
- LLM timeout;
- RAG unavailable;
- CRM unavailable;
- validation error;
- rate limit;
- invalid token;
- network error.
Проверка: при ошибке LLM посетитель не видит stack trace.
Шаг 26. Проверьте mobile UX
На мобильном виджет должен:
- не перекрывать cookie banner;
- не закрывать кнопку покупки;
- открываться на высоту экрана;
- показывать клавиатуру без скачков;
- иметь крупную кнопку отправки;
- сохранять историю при повороте экрана;
- закрываться понятной кнопкой;
- не выходить за viewport;
- иметь доступный контраст;
- не ломать scroll страницы.
Проверка: на ширине 360 px все элементы видны, текст кнопок не обрезается.
Шаг 27. Проверьте производительность сайта
Виджет не должен замедлять сайт.
Правила:
- `widget.js` до 50-80 KB gzip для MVP;
- script грузится async;
- iframe создается после загрузки или при первом открытии;
- heavy libraries не грузятся на главный thread;
- картинки и шрифты виджета оптимизированы;
- нет блокирующих запросов до first paint;
- ошибки виджета не ломают сайт.
Проверка:
страница без виджета: baseline
страница с виджетом: LCP и CLS не ухудшились критично
Шаг 28. Создайте тесты
Создайте `widget_test_cases`.
Колонки:
id
case_key
case_group
input_text
expected_behavior
expected_status
is_critical
Минимальные тесты:
- виджет загружается;
- открытие и закрытие;
- отправка простого вопроса;
- ответ по базе знаний;
- вопрос без ответа;
- сбор email;
- отказ оставить контакт;
- создание лида;
- handoff оператору;
- rate limit;
- invalid token;
- чужой origin;
- prompt injection;
- LLM timeout;
- CRM error;
- mobile viewport;
- повторная вставка script;
- reload страницы;
- сохранение истории;
- отсутствие секретов в HTML.
Проверка: critical tests проходят перед production.
Шаг 29. Запустите smoke test на staging
Порядок:
- откройте staging-страницу;
- проверьте, что `widget_loaded` записан;
- откройте чат;
- отправьте вопрос по базе знаний;
- получите ответ с источником;
- задайте вопрос без ответа;
- проверьте handoff;
- оставьте email;
- проверьте consent;
- проверьте CRM lead;
- обновите страницу;
- проверьте сохранение session.
Проверка: путь от открытия виджета до заявки проходит без ручного вмешательства.
Шаг 30. Включите ограниченный production
Первый запуск:
- только на 1-3 страницах;
- только один язык;
- только один сценарий;
- CRM write-back через очередь;
- auto-send клиенту ограничен;
- handoff включен;
- daily budget маленький;
- rate limit включен;
- alert по ошибкам включен;
- первые диалоги проверяет человек.
Проверка: виджет можно выключить через `widget_config.is_active = false` без удаления script с сайта.
Шаг 31. Настройте метрики
Создайте `widget_metrics`.
Колонки:
id
date
site_key
widget_loaded
widget_opened
messages_sent
agent_answers
handoff_count
leads_created
error_count
avg_latency_ms
estimated_cost_usd
created_at
Смотрите:
- open rate;
- message rate;
- lead conversion;
- handoff rate;
- answer latency;
- error rate;
- cost per lead;
- темы без ответа;
- долю mobile;
- жалобы пользователей.
Проверка: через сутки понятно, какие вопросы задают и где агент не справляется.
Шаг 32. Минимальный результат для запуска
MVP готов, если выполнены условия:
- сайт добавлен в `widget_sites`;
- `widget_config` заполнен;
- embed token работает;
- script вставлен одной строкой;
- iframe или Shadow DOM не ломает верстку;
- session создается;
- messages сохраняются;
- consent работает;
- agent run создается;
- RAG ищет по базе знаний сайта;
- ответ возвращается в UI;
- handoff работает;
- CRM lead создается через очередь;
- events пишутся;
- rate limit работает;
- prompt injection блокируется;
- ошибки имеют fallback;
- mobile UX проверен;
- производительность не просела критично;
- smoke test пройден.
Финальная проверка: вставьте script на staging-страницу, пройдите сценарий вопроса, сценарий заявки, сценарий handoff, сценарий ошибки и mobile-проверку. Если все проходит, включайте виджет на ограниченном наборе production-страниц.
Что нельзя автоматизировать в первой версии
В первой версии не автоматизируйте:
- обещание скидок;
- финальные цены без правил;
- прием платежей;
- изменение заказа;
- удаление данных клиента;
- сбор лишних персональных данных;
- отправку marketing-согласия по умолчанию;
- массовую рассылку из виджета;
- ответы без источника на юридические и финансовые вопросы;
- передачу сырых секретов в prompt;
- загрузку любых файлов без проверки;
- доступ к админским endpoints;
- запись в CRM без consent;
- auto-handoff без summary;
- включение на весь сайт без staging.
Сначала виджет должен безопасно отвечать, собирать заявку и передавать сложные случаи человеку. Потом можно добавлять персонализацию, multi-language, live operator и дополнительные tools.
Частые вопросы
Лучше делать виджет через iframe или Shadow DOM?
Для первого запуска лучше iframe: он изолирует стили, меньше конфликтует с сайтом и проще переносится между CMS. Shadow DOM можно рассмотреть, когда нужна глубокая кастомизация.
Можно ли вставить LLM API key прямо в widget.js?
Нет. В браузере должен быть только public widget token. Все вызовы LLM, CRM и базы знаний должны идти через backend.
Что делать, если агент не знает ответ?
Он должен честно сказать, что не нашел подтвержденный ответ, и предложить передать вопрос менеджеру. Уверенные выдумки в виджете быстро портят доверие.
Нужно ли сразу подключать CRM?
Не обязательно. Можно начать с email или Telegram-уведомлений менеджеру. CRM подключайте, когда уже понятны поля лида, consent и правила дедупликации.
Как понять, что виджет готов к сайту?
Он готов, если проходит smoke test: загрузка, открытие, ответ по базе знаний, заявка, handoff, rate limit, mobile UX, fallback при ошибке и отсутствие секретов в HTML.