Библиотека · Поддержка клиентов и голосовые агенты

AI Чат на сайте — Crisp + Claude, умный виджет

Строитель55 минОбновлено: октябрь 2026
61 из 105 в библиотеке

Время: ~20 мин теории + 35 мин практики


Суть урока

Посетитель зашёл на сайт в 23:47 в субботу. Увидел кнопку "Написать нам" и написал вопрос. Обычно — тишина до понедельника утром. К тому времени он уже купил у конкурента.

AI чат отвечает через секунду. В любое время. На многих языках. И если не может помочь — честно говорит и записывает данные для звонка оператора утром.

🎨 Образ: Магазин в торговом центре где стоит продавец-консультант. Он не дремлет в углу — он встречает каждого входящего и сразу спрашивает "Чем могу помочь?". Именно это делает чат-виджет с AI: встречает посетителя сайта в момент когда тот готов к разговору.


Ключевые концепции

  • Crisp.chat + Claude — webhook перехватывает сообщение → Claude отвечает → ответ в чат
  • Intercom Fin — встроенный AI агент (проще, дороже)
  • Chatwoot — open-source альтернатива, self-hosted + Claude
  • Умный эскалейшн — Claude решает → если не может → берёт живой человек
  • RAG по документации — Claude знает всё о вашем продукте
  • Аналитика вопросов — Claude находит паттерны → вы улучшаете FAQ

Теория

Почему обычный чат — это не работает

Обычный виджет онлайн-чата работает пока есть оператор. Оператора нет → "Мы offline, оставьте email". Часть посетителей на этом уходит.

Три боли:

  1. Время — вне рабочих часов клиент уходит
  2. Скорость — даже онлайн оператор отвечает за 2-5 минут (занят другим)
  3. Масштаб — один оператор может параллельно вести лишь несколько чатов

AI закрывает все три: отвечает мгновенно, 24/7, параллельно тысяче клиентов.

Crisp.chat + Claude: webhook-интеграция

Crisp — европейский чат-инструмент (часто выбирают ради соответствия GDPR, обычно проще и дешевле Intercom). Бесплатный план позволяет поставить виджет на сайт, а доступ к API и вебхукам зависит от тарифа: смотри условия на сайте Crisp.

Логика работы:

Код
Клиент пишет в чате → Crisp webhook → ваш сервер → Claude API → ответ → Crisp API → клиент видит
javascript
// server.js — Node.js сервер для обработки Crisp webhook
const express = require('express');
const Anthropic = require('@anthropic-ai/sdk');
const { Crisp } = require('crisp-api');

const app = express();
app.use(express.json());

const claude = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY
});

const crisp = new Crisp();
crisp.authenticateTier(
  'plugin',
  process.env.CRISP_API_IDENTIFIER,
  process.env.CRISP_API_KEY
);
// Имена методов библиотеки crisp-api сверяй с её README: они меняются между версиями

// База знаний о продукте
const KNOWLEDGE_BASE = `
Вы помощник компании [Название компании].

ПРОДУКТ:
- Что это: [описание продукта]
- Цена: [цены]
- Как начать: [шаги]

ЧАСТЫЕ ВОПРОСЫ:
- Вопрос 1: Ответ 1
- Вопрос 2: Ответ 2

ЧЕГО НЕ ДЕЛАТЬ:
- Не придумывать функции которых нет
- Не давать скидки самостоятельно
- Не обещать сроки без уточнения

КОГДА ПЕРЕДАТЬ ОПЕРАТОРУ:
- Жалоба на продукт
- Вопрос о возврате денег
- Техническая проблема которую не можете решить
`;

// Хранилище истории разговоров (в продакшне — Redis или БД)
const conversationHistory = new Map();

// Webhook от Crisp
app.post('/crisp-webhook', async (req, res) => {
  const event = req.body;

  // Обрабатываем только новые сообщения от клиентов
  if (event.event !== 'message:send') {
    return res.json({ status: 'ignored' });
  }

  const sessionId = event.data?.session_id;
  const message = event.data?.content;
  const websiteId = event.data?.website_id;
  const from = event.data?.from;  // 'user' или 'operator'

  // Пропускаем сообщения от операторов (чтобы не зациклиться)
  if (from === 'operator') {
    return res.json({ status: 'ignored' });
  }

  console.log(`Новое сообщение [${sessionId}]: ${message}`);

  try {
    // Получаем или создаём историю разговора
    if (!conversationHistory.has(sessionId)) {
      conversationHistory.set(sessionId, []);
    }
    const history = conversationHistory.get(sessionId);

    // Добавляем сообщение пользователя в историю
    history.push({
      role: 'user',
      content: message
    });

    // Держим историю разумного размера (последние 10 сообщений)
    const recentHistory = history.slice(-10);

    // Отправляем в Claude
    const response = await claude.messages.create({
      model: 'claude-sonnet-5-5',
      max_tokens: 500,
      system: KNOWLEDGE_BASE,
      messages: recentHistory
    });

    const aiResponse = response.content.map((b) => (b.type === "text" ? b.text : "")).join("");

    // Сохраняем ответ AI в историю
    history.push({
      role: 'assistant',
      content: aiResponse
    });

    // Определяем нужен ли оператор
    const needsHuman = detectEscalationNeeded(aiResponse, message);

    // Отправляем ответ в Crisp
    await crisp.website.sendMessageInConversation(websiteId, sessionId, {
      type: 'text',
      from: 'operator',
      origin: 'chat',
      content: aiResponse
    });

    // Если нужен оператор — уведомляем
    if (needsHuman) {
      await crisp.website.sendMessageInConversation(websiteId, sessionId, {
        type: 'text',
        from: 'operator',
        origin: 'chat',
        content: '⏳ Сейчас передам вас специалисту. Одну минуту, пожалуйста.'
      });

      // Меняем статус разговора на "требует внимания" (state: pending, ждёт оператора)
      await crisp.website.changeConversationState(
        websiteId,
        sessionId,
        { state: 'pending' }
      );

      console.log(`[${sessionId}] Передан оператору`);
    }

    res.json({ status: 'success', escalated: needsHuman });

  } catch (error) {
    console.error('Ошибка:', error);

    // При ошибке — сразу зовём оператора
    await crisp.website.sendMessageInConversation(websiteId, sessionId, {
      type: 'text',
      from: 'operator',
      origin: 'chat',
      content: 'Извините, сейчас я не могу помочь. Наш специалист скоро свяжется с вами!'
    });

    res.status(500).json({ status: 'error' });
  }
});

function detectEscalationNeeded(aiResponse, userMessage) {
  const escalationPhrases = [
    'передам вас', 'соединю вас', 'специалист поможет',
    'обратитесь к', 'не могу помочь с этим', 'требует ручного'
  ];

  const userEscalationPhrases = [
    'живой человек', 'оператор', 'менеджер', 'не хочу с роботом',
    'позовите человека', 'хочу поговорить с человеком'
  ];

  const inAiResponse = escalationPhrases.some(p =>
    aiResponse.toLowerCase().includes(p)
  );

  const inUserMessage = userEscalationPhrases.some(p =>
    userMessage.toLowerCase().includes(p)
  );

  return inAiResponse || inUserMessage;
}

app.listen(3000, () => {
  console.log('Сервер запущен на порту 3000');
});
bash
# package.json зависимости
npm init -y
npm install express @anthropic-ai/sdk crisp-api dotenv

# .env
ANTHROPIC_API_KEY=sk-ant-...
CRISP_API_IDENTIFIER=your_crisp_id
CRISP_API_KEY=your_crisp_key

# Запуск
node server.js

# Сделать доступным из интернета (для webhook)
npx ngrok http 3000
# Скопируйте URL типа https://abc123.ngrok.io
# В Crisp: раздел вебхуков (названия пунктов меню меняются) → добавить адрес https://abc123.ngrok.io/crisp-webhook

🎨 Образ: Webhook — это как дверной звонок. Crisp нажимает звонок когда клиент пишет. Ваш сервер открывает дверь, думает что ответить (через Claude), и возвращает ответ в Crisp. Всё это за 1-2 секунды.

Intercom Fin: встроенный AI агент

Intercom — более дорогой вариант (платные тарифы, а AI-агент Fin тарифицируется отдельно; цены на сайте Intercom), но со встроенным AI агентом Fin. Не нужно писать код — просто подключаете документацию и Fin начинает отвечать.

Параметр Crisp + Claude Intercom Fin
Цена Тариф Crisp + оплата Claude API Тариф Intercom + оплата Fin (цены на сайтах)
Настройка Нужен код Без кода
Гибкость Полная Ограниченная
Качество ответов Очень высокое Высокое
Аналитика Своя Встроенная
Лучше для Разработчики Нетехнические команды

Chatwoot: open-source + self-hosted

Chatwoot — бесплатная (open-source) альтернатива Intercom которую вы хостите сами. Полный контроль над данными, без платы за лицензию; за сервер и поддержку отвечаете вы.

python
# Интеграция Chatwoot + Claude через Python
import anthropic
import requests
import os

claude_client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

CHATWOOT_URL = os.environ["CHATWOOT_URL"]  # https://your-chatwoot.com
CHATWOOT_TOKEN = os.environ["CHATWOOT_TOKEN"]  # User Access Token
CHATWOOT_ACCOUNT_ID = os.environ["CHATWOOT_ACCOUNT_ID"]

def send_message_to_chatwoot(
    conversation_id: int,
    message: str,
    message_type: str = "outgoing"
) -> dict:
    """Отправляет сообщение в разговор Chatwoot"""

    response = requests.post(
        f"{CHATWOOT_URL}/api/v1/accounts/{CHATWOOT_ACCOUNT_ID}/conversations/{conversation_id}/messages",
        headers={
            "api_access_token": CHATWOOT_TOKEN,
            "Content-Type": "application/json"
        },
        json={
            "content": message,
            "message_type": message_type,
            "private": False  # Видно клиенту
        }
    )
    return response.json()


def handle_chatwoot_webhook(webhook_data: dict) -> dict:
    """Обрабатывает входящий webhook от Chatwoot"""

    event_type = webhook_data.get("event")

    # Только входящие сообщения от клиентов
    if event_type != "message_created":
        return {"status": "ignored"}

    message = webhook_data.get("content", "")
    conversation_id = webhook_data.get("conversation", {}).get("id")
    message_type = webhook_data.get("message_type")

    # Пропускаем исходящие (от операторов/AI)
    if message_type != "incoming":
        return {"status": "ignored"}

    # Claude отвечает
    response = claude_client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=400,
        system="Ты помощник компании. Отвечай по теме продукта. Если не знаешь — скажи честно.",
        messages=[{"role": "user", "content": message}]
    )

    ai_answer = "".join(b.text for b in response.content if b.type == "text")

    # Отправляем ответ в Chatwoot
    send_message_to_chatwoot(conversation_id, ai_answer)

    return {"status": "success"}

Умный эскалейшн: когда передавать человеку

Ключевой момент любого AI чата — знать когда остановиться. Попытка AI отвечать на всё часто хуже, чем быстрая передача оператору.

python
def should_escalate_to_human(
    user_message: str,
    ai_response: str,
    conversation_turns: int
) -> tuple[bool, str]:
    """
    Определяет нужна ли передача оператору.
    Возвращает (нужна_передача, причина)
    """

    # Явные запросы на живого человека
    human_requests = [
        "хочу поговорить с человеком", "позовите оператора",
        "живой менеджер", "не хочу с роботом", "это бот?",
        "соедините с сотрудником"
    ]
    if any(phrase in user_message.lower() for phrase in human_requests):
        return True, "клиент запросил живого оператора"

    # Эмоционально заряженные сообщения
    anger_signals = [
        "это безобразие", "верните деньги", "подам в суд",
        "это мошенничество", "отвратительно", "требую"
    ]
    if any(signal in user_message.lower() for signal in anger_signals):
        return True, "клиент выражает сильное недовольство"

    # AI признаёт что не может помочь
    ai_cant_help = [
        "не могу помочь", "не в моей компетенции",
        "передам специалисту", "уточните у оператора"
    ]
    if any(phrase in ai_response.lower() for phrase in ai_cant_help):
        return True, "AI не смог ответить на вопрос"

    # Слишком долгий разговор без решения (5+ ходов)
    if conversation_turns >= 5:
        claude_check = claude_client.messages.create(
            model="claude-haiku-4-5",  # Быстрая дешёвая модель для проверки
            max_tokens=50,
            messages=[{
                "role": "user",
                "content": f"Проблема клиента решена? Ответь только: да/нет. Сообщение: {user_message}"
            }]
        )
        if "нет" in "".join(b.text for b in claude_check.content if b.type == "text").lower():
            return True, "проблема не решена за 5 сообщений"

    return False, ""

RAG: Claude знает всё о вашем продукте

Без базы знаний Claude будет выдумывать факты о вашем продукте. С базой — отвечает точно.

python
# Простая RAG система для чат-виджета
# В продакшне: Pinecone / Chroma / Qdrant вместо списка строк

class SimpleRAG:
    def __init__(self, documents: list[dict]):
        """
        documents: [{"title": "...", "content": "...", "category": "..."}]
        """
        self.documents = documents
        self.client = anthropic.Anthropic()

    def find_relevant(self, query: str, top_k: int = 3) -> list[dict]:
        """Находит релевантные документы (упрощённо через Claude)"""

        # В реальной системе: векторный поиск
        # Здесь: Claude выбирает из небольшого списка

        titles = [f"{i}. {d['title']}" for i, d in enumerate(self.documents)]
        titles_text = "\n".join(titles)

        response = self.client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=100,
            messages=[{
                "role": "user",
                "content": f"""Вопрос: "{query}"

Какие документы наиболее релевантны? Укажи только номера через запятую (например: 0,2):
{titles_text}"""
            }]
        )

        try:
            indices = [int(i.strip()) for i in "".join(b.text for b in response.content if b.type == "text").split(",")]
            return [self.documents[i] for i in indices[:top_k] if i < len(self.documents)]
        except Exception:
            return self.documents[:top_k]  # Fallback

    def answer_with_context(self, question: str) -> str:
        """Отвечает на вопрос с контекстом из базы знаний"""

        relevant_docs = self.find_relevant(question)
        context = "\n\n".join([
            f"### {d['title']}\n{d['content']}"
            for d in relevant_docs
        ])

        response = self.client.messages.create(
            model="claude-sonnet-5-5",
            max_tokens=300,
            messages=[{
                "role": "user",
                "content": f"""Используй ТОЛЬКО информацию из базы знаний.
Если ответа нет в базе — честно скажи "не знаю" и предложи написать в поддержку.

БАЗА ЗНАНИЙ:
{context}

ВОПРОС: {question}"""
            }]
        )

        return "".join(b.text for b in response.content if b.type == "text")


# Пример настройки
kb = SimpleRAG([
    {
        "title": "Тарифы",
        "category": "pricing",
        "content": "Базовый план: 990₽/мес (до 5 пользователей). Стандарт: 2900₽/мес (до 20 пользователей). Бизнес: 7900₽/мес (безлимит). Скидка 20% при оплате за год."
    },
    {
        "title": "Интеграции",
        "category": "technical",
        "content": "Работает с: 1С, amoCRM, Битрикс24, Google Sheets. API доступно на всех тарифах. Webhooks — с тарифа Стандарт."
    },
    {
        "title": "Безопасность",
        "category": "technical",
        "content": "Данные хранятся на серверах в России. Шифрование AES-256. SOC2 Type II. Резервные копии каждые 6 часов."
    }
])

# Тест
questions = [
    "Сколько стоит безлимитный тариф?",
    "Есть ли интеграция с amoCRM?",
    "Можно ли работать с вами из Европы?"
]

for q in questions:
    answer = kb.answer_with_context(q)
    print(f"Q: {q}")
    print(f"A: {answer}\n")

Аналитика: что спрашивают чаще всего

python
def analyze_chat_questions(chat_logs: list[str]) -> dict:
    """Анализирует вопросы из чатов и находит паттерны"""

    client = anthropic.Anthropic()

    combined = "\n---\n".join(chat_logs[:50])

    response = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=800,
        messages=[{
            "role": "user",
            "content": f"""Проанализируй вопросы клиентов из чат-виджета.

ЛОГИ ЧАТОВ:
{combined}

Нужно:
1. **Топ-10 тем** которые спрашивают чаще всего (с примерными % от всех вопросов)
2. **Вопросы без ответа** — где AI говорил "не знаю" (что добавить в FAQ)
3. **Вопросы на оператора** — что слишком сложно для AI
4. **Временны́е паттерны** — когда активнее всего пишут
5. **Рекомендации** — что изменить на сайте чтобы люди находили ответы сами

Это данные для продуктовой команды — пиши конкретно."""
        }]
    )

    return "".join(b.text for b in response.content if b.type == "text")

Практика

Задача: настроить Crisp + Claude webhook за 35 минут

Шаг 1: Регистрация в Crisp (5 мин)

bash
# 1. Идём на crisp.chat
# 2. Создаём аккаунт (бесплатно)
# 3. Добавляем сайт: Settings → Websites → Add
# 4. Копируем код виджета и вставляем на сайт перед </body>

# Для тестирования без реального сайта — в Crisp есть тестовый чат

Шаг 2: Получаем API credentials (5 мин)

bash
# В Crisp Dashboard:
# Profile → API Access Tokens → Create new token
# Сохраняем:
# - Identifier (как логин)
# - Key (как пароль)
# - Website ID (в URL: app.crisp.chat/website/WEBSITE_ID/)

Шаг 3: Запускаем локальный сервер (15 мин)

bash
mkdir crisp-claude-bot && cd crisp-claude-bot
npm init -y
npm install express @anthropic-ai/sdk dotenv ngrok

# Создаём .env
cat > .env << EOF
ANTHROPIC_API_KEY=sk-ant-your-key
CRISP_API_IDENTIFIER=your-identifier
CRISP_API_KEY=your-key
CRISP_WEBSITE_ID=your-website-id
PORT=3000
EOF
javascript
// bot.js — минимальная рабочая версия
require('dotenv').config();
const express = require('express');
const Anthropic = require('@anthropic-ai/sdk');

const app = express();
app.use(express.json());

const claude = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

// Простейший webhook без истории (для теста)
app.post('/webhook', async (req, res) => {
  const { event, data } = req.body;

  if (event !== 'message:send' || data?.from !== 'user') {
    return res.json({ ok: true });
  }

  const question = data?.content;
  const sessionId = data?.session_id;
  const websiteId = data?.website_id;

  try {
    const response = await claude.messages.create({
      model: 'claude-sonnet-5-5',
      max_tokens: 250,
      system: 'Ты помощник на сайте. Отвечай кратко и по делу. Если не знаешь — скажи честно.',
      messages: [{ role: 'user', content: question }]
    });

    const answer = response.content.map((b) => (b.type === "text" ? b.text : "")).join("");

    // Отправляем ответ через Crisp REST API
    await fetch(
      `https://api.crisp.chat/v1/website/${websiteId}/conversation/${sessionId}/message`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Basic ${Buffer.from(
            `${process.env.CRISP_API_IDENTIFIER}:${process.env.CRISP_API_KEY}`
          ).toString('base64')}`,
          'Content-Type': 'application/json',
          'X-Crisp-Tier': 'plugin'
        },
        body: JSON.stringify({
          type: 'text',
          content: answer,
          from: 'operator',
          origin: 'chat'
        })
      }
    );

    console.log(`[${sessionId}] Q: ${question.substring(0,50)}... → ответил`);
    res.json({ ok: true });

  } catch (err) {
    console.error(err);
    res.status(500).json({ error: err.message });
  }
});

app.listen(process.env.PORT, () =>
  console.log(`Бот запущен на порту ${process.env.PORT}`)
);
bash
# Запускаем сервер
node bot.js

# В другом терминале — открываем туннель
npx ngrok http 3000
# Копируем URL: https://abc123.ngrok.io

Шаг 4: Настраиваем webhook в Crisp (5 мин)

Код
В Crisp Dashboard:
Раздел вебхуков в настройках (названия пунктов меню Crisp меняются)
→ Endpoint URL: https://abc123.ngrok.io/webhook
→ Events: message:send ✅
→ Save

Шаг 5: Тестируем (5 мин)

Напиши в чат
Открываем тестовый чат Crisp (кнопка "Preview" в Dashboard)
Пишем любой вопрос
Смотрим на ответ AI
Проверяем логи в терминале

Инструменты и ресурсы

  • Crisp.chat — crisp.chat (есть бесплатный план; доступ к API и вебхукам зависит от тарифа, смотри на сайте)
  • Chatwoot — chatwoot.com (open-source, self-hosted бесплатно)
  • Intercom — intercom.com (платно, со встроенным AI-агентом Fin; цены на сайте)
  • Tawk.to — tawk.to (бесплатный чат; AI-функции и условия смотри на сайте)
  • ngrok — ngrok.com (туннель для тестирования webhook локально)
  • Anthropic API — названия моделей в коде даны на октябрь 2026; актуальные цены и версии: Актуальное сейчас

Ключевые выводы

Чат на сайте — это не "ещё один канал поддержки". Это точка принятия решения. Клиент пишет в чат в момент когда готов купить или готов уйти. AI который отвечает за секунду в 23:47 даёт шанс удержать уходящего посетителя.

Эскалейшн важнее ответов. Лучший AI-чат — тот который быстро понимает когда нужен человек и передаёт без задержки. Клиент который получил "не знаю, передаю специалисту" — доволен. Клиент которому AI выдумал несуществующую функцию — потерян навсегда.

Аналитика — скрытый бонус. Сотни разговоров = сотни единиц обратной связи. Claude анализирует паттерны за минуты и показывает что нужно улучшить в продукте, сайте и FAQ. Это хороший дополнительный источник к обычным исследованиям пользователей.


Следующий урок

→ Payments & Billing — Stripe MCP, подписки, webhooks

Отметка хранится только в этом браузере и никуда не отправляется. Мой прогресс