VPN·HUB·CRACK ИИ тех-поддержка на Claude: отвечаем клиентам за копейки · VPN HUB CRACK
Все статьи
Клиенты и бизнес PRO

ИИ тех-поддержка на Claude: отвечаем клиентам за копейки

Обновлено 11 августа 2026 Проверено

ИИ тех-поддержка на Claude: отвечаем клиентам за копейки

Не теория и не «ещё один чат-бот с кнопками». Это готовый движок ИИ-поддержки, который уже крутится на живом VPN-сервисе: отвечает клиентам в мини-аппе, сам сбрасывает устройства, проверяет оплату и подписку, а деньги и споры молча передаёт владельцу. Клиент не догадывается, что общается не с человеком. Стоимость около 100 ₽ в месяц на всю поддержку. Внизу статьи лежит полный рабочий скрипт ai_support.py, промпт и база знаний: забираешь, переписываешь базу под свой VPN, и у тебя круглосуточная поддержка без операторов.


Проблема, которую это закрывает

Поддержка это самая нелюбимая часть VPN-бизнеса. 80% обращений это одно и то же по кругу:

  • «превышено число устройств» → сбросить HWID (привязка подписки к «железу» устройства);
  • «оплатил, а подписки нет» → проверить платёж и подписку;
  • «как подключиться / где ссылка» → дать инструкцию;
  • «не работает на мобильном интернете» → это глушилки, предложить нужный тариф.

📖 Незнакомые слова? Термины простыми словами смотрите в Словаре терминов.

Ты (или твой оператор) отвечаешь на это руками, в любое время суток, месяцами. Живой оператор стоит денег и спит. Обычный бот-с-кнопками бесит клиентов и не решает нестандартное. А клиент, который не получил ответ за 10 минут, уходит и пишет плохой отзыв.

Решение: ИИ, который не просто отвечает текстом, а реально делает действия в твоём сервисе и понимает живую речь. А то, что ИИ решать не должен (деньги, возвраты, споры), он аккуратно передаёт тебе с полной историей диалога.


Что именно ты получаешь (и чем это НЕ чат-бот)

Ключевое отличие от обычных ботов: этот ИИ агент. Он умеет звать функции твоего сервиса. Пять готовых инструментов:

Инструмент Что делает реально
reset_devices Сбрасывает все HWID-устройства клиента при жалобе на лимит (правильный вызов Remnawave)
list_devices Показывает привязанные устройства (модель, платформа)
check_payment Проверяет статус последней оплаты по базе
check_subscription Проверяет, создана ли подписка, и достаёт ссылку
report_to_owner Шлёт тебе баг-репорт (напр. «оплата прошла, а подписка не создалась»), и при этом продолжает вести клиента

Поверх: умная эскалация. ИИ сам понимает, когда пора звать человека: просьба возврата, спор, грубость, «позовите оператора», вопрос не по теме, или он просто не знает точного ответа. В этот момент у тебя в Telegram-группе создаётся отдельный топик на клиента с карточкой (подписка, устройства) и всей историей диалога. Ты отвечаешь прямо в топике, и ответ уходит клиенту в мини-апп. Нажал «удалить», тикет закрыт, ИИ снова на линии.

Логика простая и честная: рутину закрывает ИИ, деньги и конфликты видит владелец. Ты перестаёшь тонуть в «сбросьте устройства», но ни один платёжный спор не проходит мимо тебя.


Почему именно Claude

  • Модель claude-haiku-4-5: быстрая и дешёвая, но при этом отлично держит роль, следует инструкциям и не «фантазирует» фактами, которых нет в базе.
  • Tool use (вызов функций): из коробки. Именно это превращает болталку в агента, который делает работу, а не пересказывает FAQ.
  • Кэширование промпта: стабильная часть (промпт + база знаний + инструменты, ~5200 токенов) кэшируется на стороне Anthropic и на повторных запросах читается в 10 раз дешевле. Отсюда и цена ~11 копеек за ответ.

Итог по деньгам (проверено на боевом трафике): ~80–130 ₽/мес при ~100 тикетах. Пополнил баланс на $5, хватает на 5–6 месяцев. Сравни с зарплатой оператора.


Как это работает (архитектура за 30 секунд)

Клиент пишет в мини-аппе «Поддержка»
        │
        ▼
POST /api/support/ai  ──►  мгновенно отвечает "pending"   (клиент видит «печатает…»)
        │ (ИИ работает В ФОНЕ, не блокирует запрос)
        ▼
generate_reply(): системный промпт + база знаний + КОНТЕКСТ клиента + инструменты → Claude
        │
        ├─ ИИ ответил ───► сохранили ответ ───► клиент забирает поллингом (3.5 сек)
        │
        └─ Нужен человек (деньги / спор / не знает)
                 │
                 ▼
          Отдельный топик на клиента в ТГ-группе (карточка + история диалога)
                 │
                 ▼
          Владелец отвечает в топике ──► ответ уходит клиенту в мини-апп

Два неочевидных, но критичных решения, которые уже сделаны за тебя:

  1. Асинхронность. ИИ с инструментами думает 8–15 секунд. Если ждать ответ синхронно, мобильный Telegram-WebView рвёт запрос по таймауту и клиент видит «Ошибка связи». Поэтому эндпоинт отвечает мгновенно, а ответ клиент забирает поллингом (ускорен до 3.5 сек в первые 45 секунд).
  2. Кэш-промпт. Стабильная часть промпта помечена cache_control: ephemeral и одинакова для всех клиентов → кэш общий на весь сервис. Первый запрос в 5-минутном окне пишет кэш, остальные читают дёшево. Именно это даёт −81% к расходу.

Внедрение с нуля (15 минут)

  1. Ключ Claude API. Заходишь на console.anthropic.com → API Keys → создаёшь ключ (sk-ant-...), кидаешь на баланс $5–10.
  2. Ставишь скрипт. ai_support.py (внизу статьи) это готовый движок: системный промпт, база знаний, 5 инструментов, generate_reply() (цикл tool use + кэширование + зачистка markdown). Кладёшь его в свой бот/бэкенд.
  3. Подключаешь к своему сервису. Пять инструментов это тонкие обёртки над твоей панелью/базой (Remnawave: сброс HWID POST /api/hwid/devices/delete, проверка подписки get_user_by_uuid; оплаты берутся из твоих таблиц транзакций). Подставляешь свои вызовы вместо remnawave_api.* и get_user_keys/....
  4. Фронт + эскалация. Эндпоинт /api/support/ai принимает сообщение, запускает ИИ в фоне, отдаёт pending; фронт мини-аппа опрашивает ответ поллингом. Эскалация создаёт топик владельцу.
  5. Сеть. Сервер должен доставать api.anthropic.com. Из РФ доступ нужен через прокси или зарубежный сервер (у тебя они и так есть).
  6. Включаешь. Вставляешь ключ, переписываешь базу знаний под себя, ставишь ai_support_enabled = true. Готово: ИИ на линии, эскалации падают в твою ТГ-группу.

Всё управление через настройки (ai_api_key, ai_model, ai_kb, ai_support_extra). Никакого редеплоя ради правки ответов.


Как «обучить» ИИ под свой сервис (это и есть 90% успеха)

ИИ-поддержка состоит из двух частей, и «обучение» это работа со второй:

  • Системный промпт (SYSTEM_PROMPT) универсальный (роль, тон, правила, эскалация). Его НЕ трогаешь: он одинаково хорош для любого VPN.
  • База знаний (DEFAULT_KB / настройка ai_kb) это факты про ТВОЙ сервис. Вот её ты переписываешь полностью. Это и есть «дообучение», без всякого файнтюнинга, просто текст.

Что кладёшь в базу знаний (свободный текст, разделами): приложения и точные ссылки под iPhone/Android; тарифы, лимиты, пробный период; частые проблемы и ТОЧНЫЕ решения по шагам («если X → делай Y»); оплата, возвраты, рефералка, заморозка.

Секрет качества. Лучшую базу знаний собирай из реальной истории обращений твоих операторов. Открой прошлые тикеты, выпиши 20–30 самых частых вопросов и то, как именно на них правильно отвечали, и перенеси это в базу дословными формулировками-шагами. Чем конкретнее «если X → делай Y», тем меньше ИИ импровизирует и тем реже дёргает тебя.

Правило простое: всё, что ИИ должен уметь ответить сам, должно быть в базе знаний явно. Чего там нет, он честно эскалирует тебе, а не выдумывает. Это и делает его безопасным: он никогда не пообещает возврат от твоего имени и не сольёт внутрянку.


Готовый скрипт ai_support.py

Полный рабочий движок с боевого сервиса. Внутри: SYSTEM_PROMPT (не трогать), DEFAULT_KB (пример базы знаний на 40+ фактов, переписываешь под себя), 5 инструментов, generate_reply() с циклом tool-use, кэшированием промпта и зачисткой markdown. Обёртки инструментов (reset_client_devices, _subscription_info, _last_payment_info, _report_to_owner) подставляешь под свою панель/БД.

"""ИИ тех-поддержка VPN (config-driven, Claude tool use).
Универсальный промпт + редактируемая БАЗА ЗНАНИЙ (setting ai_kb, иначе DEFAULT_KB).
Инструменты: reset_devices, list_devices, check_payment, check_subscription, report_to_owner.
Эскалация деньги/спор/не знает → оператор. Не раскрывает, что это ИИ."""
import logging
import re
import httpx


def _strip_md(t: str) -> str:
    """Убирает markdown-разметку из ответа ИИ (мини-апп/Telegram её не рендерят)."""
    t = re.sub(r"\*\*(.+?)\*\*", r"\1", t)
    t = re.sub(r"__(.+?)__", r"\1", t)
    t = re.sub(r"(?m)^\s*#{1,6}\s+", "", t)
    t = re.sub(r"(?m)^\s*\*\s+", "• ", t)
    return t.replace("**", "").replace("__", "")

# ── эти импорты подставь под свой проект (панель/БД) ──
from shop_bot.data_manager.remnawave_repository import get_setting
from shop_bot.data_manager.database import get_user_keys, get_db_connection
from shop_bot.modules import remnawave_api

logger = logging.getLogger(__name__)
API_URL = "https://api.anthropic.com/v1/messages"
DEFAULT_MODEL = "claude-haiku-4-5-20251001"

SYSTEM_PROMPT = """Ты — специалист технической поддержки VPN-сервиса. Общаешься с клиентом в чате поддержки.

СТИЛЬ ОБЩЕНИЯ:
- Максимально формально, вежливо и уважительно, на «вы».
- НЕ сообщай и не намекай, что ты ИИ, бот или автоответчик — ты живой сотрудник поддержки. Не придумывай себе личное имя — представляйся просто как поддержка сервиса.
- Отвечай кратко и по делу, конкретными шагами. Без markdown-разметки (никаких **, __, #, обратных кавычек). Эмодзи — умеренно.
- Опирайся на БАЗУ ЗНАНИЙ и КОНТЕКСТ КЛИЕНТА ниже. Сначала сверься с подпиской, сроком и числом устройств клиента.

ИНСТРУМЕНТЫ (используй, когда уместно):
- reset_devices — сбросить все привязанные устройства клиента (при жалобе на лимит/«превышено число устройств»/HWID).
- list_devices — посмотреть подключённые устройства.
- check_payment — проверить статус последней оплаты клиента.
- check_subscription — проверить, создана ли подписка и получить ссылку-подписку.
- report_to_owner — отправить владельцу баг-репорт (например: оплата прошла, но подписка не создалась). При этом ты можешь продолжать отвечать клиенту.

ЭСКАЛАЦИЯ — верни РОВНО одно слово ESCALATE (без других слов), если:
- клиент явно просит живого оператора/человека;
- вопрос про возврат денег/спор/жалоба/угроза/грубость;
- проблема НЕ решается или ты НЕ знаешь точного решения;
- вопрос не по теме поддержки (реклама, спам, сотрудничество).
Когда эскалируешь — клиенту автоматически сообщат, что подключается оператор. Лучше эскалировать, чем выдумать ответ.

ЗАПРЕТЫ:
- Не обещай возвраты, компенсации, скидки, бесплатные дни.
- Не выдумывай фактов, которых нет в базе знаний.
- НИКОГДА не рассказывай о внутреннем устройстве, коде, технологиях сервиса, о том, кто им владеет или управляет.
- Обсуждай ТОЛЬКО вопросы про VPN и аккаунт клиента. На отвлечённые темы вежливо возвращай к теме поддержки; если клиент настойчиво уводит в сторону — верни ESCALATE."""

# ↓↓↓ ЭТО переписываешь под свой сервис — «обучение» ИИ. Пример реальной базы: ↓↓↓
DEFAULT_KB = """СЕРВИС: T-VPN.

ПРИЛОЖЕНИЯ ДЛЯ ПОДКЛЮЧЕНИЯ: Happ (основное) и INCY (запасное).
- Happ для iPhone / Android, INCY для iPhone / Android (точные ссылки — под каждую платформу).

КАК ПОДКЛЮЧИТЬСЯ: 1) установить приложение; 2) нажать «Подключиться» — подписка добавится; 3) подключиться. Локация — АВТОВЫБОР (сервис сам подбирает лучшую). Вручную страну не выбирать.

ТАРИФЫ (в месяц): Pro 150, Reserve+ 200, Maximum 300, Family 400, Family+ 600. Пробный период — 14 дней (один раз на аккаунт).
УСТРОЙСТВА: Pro/Maximum/Reserve+ — 1; Family/Family+ — 5. Сброс — инструментом reset_devices.

ЗАМОРОЗКА: раз в месяц, до 30 дней, управление в мини-аппе.
РЕФЕРАЛКА: 25% с оплат друга на баланс; друг — 15% скидка; вывод от 500 руб (только реферальные).
ТРАФИК: в основном безлимит; на отдельных серверах (БС) — 150 ГБ/мес.

ДИАГНОСТИКА «НЕ ПОДКЛЮЧАЕТСЯ»:
1. Обновить подписку в приложении (потянуть список серверов вниз).
2. Работает по Wi-Fi, но не на мобильном → у оператора глушилки → предложить тариф Maximum («Обход глушилок»).
3. Ошибка «Превышено число устройств» → reset_devices + обновить подписку. «Критическая ошибка ядра Xray» → переустановить/сменить приложение.

ОПЛАТА: СБП, карта, крипта, Telegram Stars. Обработка до 30 минут.
ОПЛАТА НЕ ЗАЧИСЛИЛАСЬ: 1) check_payment; 2) в ожидании <30 мин → подождать, >30 мин → ESCALATE; 3) оплаты нет → попросить чек, ESCALATE; 4) оплата прошла → check_subscription: подписка есть → выдать ссылку + report_to_owner; подписки нет → report_to_owner «баг» и сказать «уже решается».
ВОЗВРАТ: только через поддержку → уточнить претензию → ESCALATE."""

TOOLS = [
    {"name": "reset_devices", "description": "Сбросить (отвязать) ВСЕ устройства клиента. При жалобе на лимит/HWID/«превышено число устройств».",
     "input_schema": {"type": "object", "properties": {}, "required": []}},
    {"name": "list_devices", "description": "Показать текущие привязанные устройства клиента (только чтение).",
     "input_schema": {"type": "object", "properties": {}, "required": []}},
    {"name": "check_payment", "description": "Проверить статус последней оплаты клиента (прошла/в ожидании/нет).",
     "input_schema": {"type": "object", "properties": {}, "required": []}},
    {"name": "check_subscription", "description": "Проверить, создана ли подписка клиента, есть ли она в системе, и получить ссылку-подписку.",
     "input_schema": {"type": "object", "properties": {}, "required": []}},
    {"name": "report_to_owner", "description": "Отправить владельцу баг-репорт/сообщение о проблеме. Клиенту при этом можно продолжать отвечать.",
     "input_schema": {"type": "object", "properties": {"note": {"type": "string", "description": "краткое описание проблемы для владельца"}}, "required": ["note"]}},
]


def _model() -> str:
    return (get_setting("ai_model") or DEFAULT_MODEL).strip() or DEFAULT_MODEL


def _kb() -> str:
    return (get_setting("ai_kb") or "").strip() or DEFAULT_KB


def build_user_context(user_id: int) -> str:
    """Короткая справка о клиенте для модели: тариф, срок, grace. Подставь свои поля."""
    try:
        keys = get_user_keys(int(user_id)) or []
        real = [k for k in keys if str(k.get("host_name")) != "GRACE"]
        pool = real or keys
        if not pool:
            return "У клиента НЕТ активной подписки (не покупал или истекла)."
        pick = max(pool, key=lambda k: str(k.get("expire_at") or ""))
        exp = str(pick.get("expire_at") or "")[:10]
        grace = (int(pick.get("grace") or 0) == 1) or (str(pick.get("host_name")) == "GRACE")
        host = pick.get("host_name")
        gtxt = " Клиент в Grace-режиме (подписка на исходе)." if grace else ""
        return f"Подписка клиента: тариф {host}, действует до {exp}.{gtxt}"
    except Exception:
        return "Статус подписки клиента неизвестен."


def has_human_reply(history: list) -> bool:
    """Отвечал ли уже живой оператор (ответы ИИ помечаются 🤖) — тогда ИИ молчит."""
    for sender, content in history:
        if sender == "admin" and not str(content or "").startswith("🤖"):
            return True
    return False


def _client_keys(user_id: int) -> list:
    keys = get_user_keys(int(user_id)) or []
    return [(k.get("remnawave_user_uuid"), k.get("host_name"))
            for k in keys if k.get("remnawave_user_uuid") and str(k.get("host_name")) != "GRACE"]


async def get_client_devices(user_id: int) -> list:
    out = []
    for uuid, host in _client_keys(user_id):
        try:
            out += await remnawave_api.get_user_devices(uuid, host_name=host)
        except Exception:
            pass
    return out


async def reset_client_devices(user_id: int) -> int:
    total = 0
    for uuid, host in _client_keys(user_id):
        try:
            total += await remnawave_api.reset_all_devices(uuid, host_name=host)
        except Exception as e:
            logger.warning("reset_client_devices: %s", e)
    return total


def _last_payment_info(user_id: int) -> dict:
    conn = get_db_connection()
    try:
        r = conn.execute(
            "SELECT status, amount_rub, created_date, payment_method FROM transactions "
            "WHERE user_id=? ORDER BY created_date DESC LIMIT 1", (user_id,)).fetchone()
        try:
            pend = conn.execute("SELECT COUNT(*) FROM pending_transactions WHERE user_id=?", (user_id,)).fetchone()[0]
        except Exception:
            pend = 0
    finally:
        conn.close()
    if r:
        return {"last_status": r[0], "amount": r[1], "date": str(r[2])[:16], "method": r[3], "pending": pend}
    return {"last_status": None, "pending": pend}


async def _subscription_info(user_id: int) -> dict:
    keys = get_user_keys(int(user_id)) or []
    real = [k for k in keys if str(k.get("host_name")) != "GRACE"]
    if not real:
        return {"has": False}
    k = max(real, key=lambda x: str(x.get("expire_at") or ""))
    uuid, host = k.get("remnawave_user_uuid"), k.get("host_name")
    in_remna = None
    try:
        if uuid:
            in_remna = bool(await remnawave_api.get_user_by_uuid(uuid, host_name=host))
    except Exception:
        in_remna = None
    return {"has": True, "link": k.get("subscription_url") or "", "in_remnawave": in_remna,
            "expire": str(k.get("expire_at") or "")[:10]}


async def _report_to_owner(user_id, note: str) -> bool:
    """Шлёт баг-репорт владельцу в ТГ. Подставь свой способ уведомления."""
    try:
        token = get_setting("support_bot_token")
        if not token:
            return False
        from shop_bot.data_manager.database import get_notify_target
        import html as _h
        chat_id, thr = get_notify_target("alerts")
        if not chat_id:
            return False
        from aiogram import Bot
        bot = Bot(token=token)
        try:
            txt = ("🐞 <b>Баг-репорт от поддержки</b>\n"
                   f"👤 Клиент: <code>{user_id}</code>\n📋 {_h.escape(str(note)[:800])}")
            await bot.send_message(int(chat_id), txt, parse_mode="HTML", message_thread_id=thr)
        finally:
            await bot.session.close()
        return True
    except Exception as e:
        logger.warning("report_to_owner error: %s", e)
        return False


async def _exec_tool(name: str, user_id, tool_input: dict) -> tuple:
    """(текст-результат для модели, короткая пометка действия|None)."""
    try:
        if name == "reset_devices":
            n = await reset_client_devices(user_id)
            return ("Сброшено устройств: %d. Клиент может подключиться заново." % n, "сбросил устройств: %d" % n)
        if name == "list_devices":
            devs = await get_client_devices(user_id)
            if not devs:
                return "У клиента нет привязанных устройств.", None
            desc = "; ".join("%s (%s)" % (d.get("deviceModel", "?"), d.get("platform", "?")) for d in devs[:10])
            return "Устройств: %d%s" % (len(devs), desc), None
        if name == "check_payment":
            p = _last_payment_info(user_id)
            if not p.get("last_status"):
                return ("Оплаченных транзакций нет." + (" В ожидании: %d." % p["pending"] if p.get("pending") else ""), None)
            return ("Последняя оплата: статус=%s, сумма=%s руб, дата=%s, способ=%s, в ожидании=%s"
                    % (p.get("last_status"), p.get("amount"), p.get("date"), p.get("method"), p.get("pending")), None)
        if name == "check_subscription":
            s = await _subscription_info(user_id)
            if not s.get("has"):
                return "Подписка у клиента НЕ найдена (ключей нет).", None
            inr = s.get("in_remnawave")
            inr_s = "да" if inr is True else ("нет" if inr is False else "неизвестно")
            return ("Подписка есть: до %s, в системе Remnawave=%s, ссылка=%s"
                    % (s.get("expire"), inr_s, s.get("link") or "нет"), None)
        if name == "report_to_owner":
            note = (tool_input or {}).get("note") or "без описания"
            ok = await _report_to_owner(user_id, note)
            return ("Баг-репорт отправлен владельцу." if ok else "Не удалось отправить баг-репорт.", "🐞 баг-репорт владельцу")
    except Exception as e:
        return "Ошибка выполнения инструмента: %s" % e, None
    return "Неизвестный инструмент.", None


async def generate_reply(user_context: str, history: list, user_id=None) -> tuple:
    """Главная функция. Возвращает (reply_text|None, escalate_bool, actions_list).
    reply_text=None + escalate=True → зови оператора."""
    key = (get_setting("ai_api_key") or "").strip()
    if not key:
        return None, True, []

    # история диалога → сообщения Claude (склейка подряд идущих ролей, срез markdown-пометки ИИ)
    messages = []
    for sender, content in history:
        if not content or sender == "note":
            continue
        role = "user" if sender == "user" else "assistant"
        c = str(content)
        if c.startswith("🤖 "):
            c = c[2:].strip()
        if messages and messages[-1]["role"] == role:
            messages[-1]["content"] += "\n" + c
        else:
            messages.append({"role": role, "content": c})
    while messages and messages[0]["role"] != "user":
        messages.pop(0)
    if not messages or messages[-1]["role"] != "user":
        return None, True, []

    if user_id:
        try:
            _devs = await get_client_devices(user_id)
            user_context += " Подключено устройств сейчас: %d." % len(_devs)
        except Exception:
            pass

    # Стабильный блок (правила + база знаний) кэшируется → чтение в 10x дешевле.
    # Контекст клиента — отдельным НЕкэшируемым блоком, чтобы кэш был общий на всех клиентов.
    system_stable = SYSTEM_PROMPT
    extra = (get_setting("ai_support_extra") or "").strip()
    if extra:
        system_stable += "\n\nДОПОЛНИТЕЛЬНО ОТ ВЛАДЕЛЬЦА:\n" + extra
    system_stable += "\n\n=== БАЗА ЗНАНИЙ ===\n" + _kb()
    system_blocks = [
        {"type": "text", "text": system_stable, "cache_control": {"type": "ephemeral"}},
        {"type": "text", "text": "\n\n=== КОНТЕКСТ КЛИЕНТА ===\n" + user_context},
    ]

    tools = TOOLS if user_id else []
    actions = []
    headers = {"x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json"}
    try:
        async with httpx.AsyncClient(timeout=60) as client:
            for _ in range(6):   # цикл tool-use: ИИ может звать инструменты несколько раз
                body = {"model": _model(), "max_tokens": 700, "system": system_blocks, "messages": messages}
                if tools:
                    body["tools"] = tools
                r = await client.post(API_URL, json=body, headers=headers)
                if r.status_code != 200:
                    logger.warning("AI support HTTP %s: %s", r.status_code, r.text[:200])
                    return None, True, actions
                data = r.json()
                content = data.get("content", [])
                if data.get("stop_reason") == "tool_use":
                    messages.append({"role": "assistant", "content": content})
                    results = []
                    for b in content:
                        if b.get("type") == "tool_use":
                            res, act = await _exec_tool(b.get("name"), user_id, b.get("input") or {})
                            if act:
                                actions.append(act)
                            results.append({"type": "tool_result", "tool_use_id": b.get("id"), "content": res})
                    messages.append({"role": "user", "content": results})
                    continue
                text = "".join(b.get("text", "") for b in content if b.get("type") == "text").strip()
                if not text or "ESCALATE" in text.upper():
                    return None, True, actions   # ИИ решил звать оператора
                return _strip_md(text), False, actions
    except Exception as e:
        logger.warning("AI support call error: %s", e)
        return None, True, actions
    return None, True, actions

Экономика: почему это окупается сразу

Живой оператор Обычный бот-FAQ ИИ на Claude
Режим работы по графику, спит 24/7 24/7
Понимает живую речь да нет да
Делает действия (сброс, проверка) да, руками нет да, сам
Деньги/споры сам теряет отдаёт владельцу с историей
Стоимость зарплата «бесплатно», но бесит ~100 ₽/мес

Один сэкономленный час твоего времени в месяц уже окупает Claude на полгода вперёд. А довольный клиент, получивший решение за минуту в 3 часа ночи, это удержание и хороший отзыв.

Совместимо с любой панелью (примеры на Remnawave). Нужен только ключ Claude API и сервер, который достаёт api.anthropic.com. Модель claude-haiku-4-5 актуальная и дешёвая; при желании легко переключить на более мощную одной строкой.