Современные онлайн-чаты, особенно в Telegram, сталкиваются с серьезными проблемами токсичного контента, спама и оскорблений. Традиционные фильтры по ключевым словам давно устарели: пользователи легко обходят их с помощью замены букв, использования смайликов или специальных символов. В результате ручная модерация отнимает много времени, а автоматическая часто оказывается неточной и излишне жесткой.

Мы уже публиковали туториалы по созданию ботов модераторов:

Но прошлые статьи использовали классические ML-модели, что делало их менее гибкими.

В этой статье я рассмотрю пример, в котором будут использоваться нейронки для выполнения задач модерации.

Большие языковые модели (LLM) способны понимать контекст и смысл сообщения, а не просто искать запрещённые слова. И в отличии от классического ML их не нужно переобучать, достаточно просто изменить промпт. Это делает их идеальным инструментом для интеллектуальной модерации. Вместо примитивных фильтров бот с LLM анализирует семантику текста и принимает взвешенные решения. Однако просто использовать LLM недостаточно — необходимо учитывать уникальные правила, которые могут отличаться для каждого чата. Поэтому мы реализуем гибридный подход:

  • LLM анализирует текст на основе контекста и установленных правил.

  • Бот сверяется с правилами из базы данных для конкретного чата.

  • Система принимает обоснованное решение: оставить сообщение, удалить, забанить пользователя или вынести предупреждение.

Подготовка к разработке бота модератора

Перед началом работы необходимо скачать и установить Python с официального сайта. Рекомендуется использовать версию 3.8 или выше. После установки откройте терминал (командную строку) и введите следующие команды:

pip install aiogram
pip install requests

Эти команды установят необходимые для работы библиотеки. Теперь можно приступить к получению токенов для LLM и бота.

Первым делом создадим бота в Telegram. Для этого перейдите в @BotFather и введите команду /newbot. Следуйте инструкциям: укажите имя бота (например, "AI Moderator") и username (например, "aimoder_amvera_bot").

Теперь необходимо получить токен для доступа к LLM. Регистрируемся в Amvera Cloud и получаем на баланс 111 рублей, чего вполне хватит для теста. После регистрации переходим в раздел LLM. Выбираем одну из четырёх доступных моделей и копирем токен.

Подключаем GPT 5 в Amvera Cloud
Подключаем GPT 5 в Amvera Cloud

Обратите внимание: для использования токена необходимо активировать подписку. В разделе LLM (Preview) найдите кнопку «Подключить пакет токенов» и выберите подходящий тарифный план. Для тестов нам подойдёт бесплатный пакет на 20 000 токенов.

Теперь, имея все необходимые токены, можно приступить к разработке.

Архитектура бота модератора

Как же будет работать система модерации? Процесс модерации организован следующим образом:

  1. Пользователь отправляет сообщение в чат.

  2. Бот перехватывает сообщение и отправляет его текст на проверку в LLM.

  3. Модель анализирует текст на наличие нарушений, основываясь на установленных правилах беседы.

  4. В зависимости от ответа модели, бот принимает решение:

    • Если сообщение нарушает правила, то бот выдает соотвествующее наказание, либо удаляет сообщение.

    • Если в сообщении не будет нарушений, то ничего делать не нужно.

Как вы могли заметить, в Amvera Cloud доступны четыре модели:

  • LLaMA 3 8B — оптимальна по соотношению цены и скорости, подходит для большинства случаев использования.

  • LLaMA 3 70B — более мощная модель для больших чатов и сложных сценариев модерации.

  • GPT 5 и 4.1 — для самых сложных сценариев.

Для тестирования мы будем использовать LLaMA 3 8B как наиболее сбалансированный вариант по минимальной цене.

Пишем бота модератора

Создайте в удобном месте папку для проекта, в которой создайте файл main.py с кодом нашего бота и папку data для хранения базы данных.

Начнём с реализации команды /start и базового функционала:

import sqlite3
import asyncio
import requests
import logging
import re

from contextlib import suppress
from datetime import datetime, timedelta
from aiogram import Bot, Dispatcher, F
from aiogram.filters import Command, CommandObject
from aiogram.types import Message, ChatPermissions
from aiogram.client.default import DefaultBotProperties
from aiogram.exceptions import TelegramBadRequest

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")

URL = "https://kong-proxy.yc.amvera.ru/api/v1/models/llama"
BOT_TOKEN = "" # ТОКЕН ВАШЕГО БОТА
AUTH_TOKEN = "" # ТОКЕН LLM В ФОРМАТЕ Bearer XXXXXXXX

logger = logging.getLogger(__name__)
connection = sqlite3.connect("/data/database.db", check_same_thread=False)
cursor = connection.cursor()
bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode="Markdown"))
dp = Dispatcher()

cursor.execute("CREATE TABLE IF NOT EXISTS rules (id INTEGER PRIMARY KEY AUTOINCREMENT, rules TEXT, group_id BIGINT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
cursor.execute("CREATE TABLE IF NOT EXISTS warnings (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, group_id INTEGER NOT NULL, reason TEXT NOT NULL, moderator_id INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
connection.commit()

@dp.message(Command("start"))
async def start(message: Message):
    await message.answer("Привет! Я бот-модератор, основанный на LLM LLaMA 8b")

if __name__ == "__main__":
    asyncio.run(dp.start_polling(bot))

Теперь добавим базовую интеграцию с API Amvera, где бот будет проверять каждое сообщение и принимать решение об удалении или оставлении:

@dp.message(F.text)
async def on_message(message: Message):
    headers = {"X-Auth-Token": str(AUTH_TOKEN), "Content-Type": "application/json"}
    data = {"model": "llama8b", "messages": [{"role": "user", "text": "Ты модератор, проверяющий сообщения участников чата. Отвечай строго по форме: Оставить/Удалить"}, {"role": "system", "text": message.text}]}

    response = requests.post(URL, headers=headers, json=data, timeout=10)
    result = response.json()
    decision = result['result']['alternatives'][0]['message']['text'].lower()
    if decision == "удалить":
        await message.delete()
        logger.info(f"УДАЛЕНО | @{message.from_user.username} (ID: {message.from_user.id}) | Текст: \"{message.text}\"")
    elif decision == "оставить":
        logger.info(f"ОСТАВЛЕНО | @{message.from_user.username} (ID: {message.from_user.id}) | Текст: \"{message.text}\"")
    else:
        logger.warning(f"ОШИБКА | Текст: \"{message.text}\" | Ответ: {result}")

Отлично! У нас есть основа, от которой мы можем отталкиваться в дальнейшем. Приступим к реализации полноценной логики модерации.

Добавим три команды для управления правилами группы. Команда /addrules - добавляет или обновляет правила:

@dp.message(Command("addrules"))
async def add_rules(message: Message):
    if message.chat.type not in ["group", "supergroup"]:
        await message.answer("Эта команда работает только в групповых чатах!")
        return

    rules = message.text.split(' ', 1)
    if len(rules) < 2:
        await message.answer("Пожалуйста, укажите правила. Пример:\n/addrules Не использовать мат в чате")
        return

    rules = rules[1].strip()
    cursor.execute("SELECT id FROM rules WHERE group_id = ?", (message.chat.id,))
    if cursor.fetchone():
        cursor.execute("UPDATE rules SET rules = ?, created_at = ? WHERE group_id = ?", (rules, datetime.now(), message.chat.id))
        await message.answer("Правила успешно обновлены!")
    else:
        cursor.execute("INSERT INTO rules (rules, group_id) VALUES (?, ?)", (rules, message.chat.id))
        await message.answer("Правила успешно добавлены!")
    connection.commit()

Команда /rules - отображает текущие правила группы:

@dp.message(Command("rules"))
async def show_rules(message: Message):
    if message.chat.type not in ["group", "supergroup"]:
        await message.answer("Эта команда работает только в групповых чатах!")
        return

    cursor.execute("SELECT rules FROM rules WHERE group_id = ?", (message.chat.id,))
    if cursor.fetchone():
        await message.answer(f"Правила этой группы:\n\n{cursor.fetchone()[0]}")
    else:
        await message.answer("Правила для этой группы еще не установлены. Используйте /addrules чтобы добавить правила.")

И команда /clearrules - удаляет текущие правила группы:

@dp.message(Command("clearrules"))
async def clear_rules(message: Message):
    if message.chat.type not in ["group", "supergroup"]:
        await message.answer("Эта команда работает только в групповых чатах!")
        return

    cursor.execute("DELETE FROM rules WHERE group_id = ?", (message.chat.id,))
    connection.commit()
    if cursor.rowcount > 0:
        await message.answer("Правила успешно удалены!")
    else:
        await message.answer("Правила для этой группы не найдены.")

Отлично! Теперь у нас есть функционал управления правилами:

Добавление правил модерации
Добавление правил модерации

Можем приступить к разработке основной логики модерации.

Для начала усовершенствуем обработку сообщений. Добавим более строгий и детализированный промпт, где чётко определим процедуру проверки, формат ответа и примеры наказаний для случаев, когда правила в группе не установлены:

@dp.message(F.text)
async def on_message(message: Message):
    member = await bot.get_chat_member(message.chat.id, message.from_user.id)
    if member.status in ['administrator', 'creator']:
        return

    cursor.execute("SELECT rules FROM rules WHERE group_id = ?", (message.chat.id,))
    rules_result = cursor.fetchone()
    rules = rules_result[0] if rules_result else "Стандартные правила поведения в чатах"
    headers = {"X-Auth-Token": str(AUTH_TOKEN), "Content-Type": "application/json"}
    data = { "model": "llama8b", "messages": [ {"role": "system", "text": f"""Ты — строгий модератор чата. Анализируй сообщения на основе правил этой группы.

ПРАВИЛА ДАННОЙ ГРУППЫ:
{rules}

ПРОЦЕДУРА АНАЛИЗА:
1. ОЧЕНЬ внимательно прочитай правила группы выше
2. Сравни сообщение пользователя с этими правилами
3. Если есть нарушение - определи какое именно правило нарушено
4. Назначь наказание согласно предусмотренному в правилах
5. Если в правилах не указано конкретное наказание - примени стандартное для типа нарушения

ФОРМАТ ОТВЕТА ТОЛЬКО ОДНА СТРОКА БЕЗ КАВЫЧЕК:
Удалить, действие: [бан/мут/кик/предупреждение], длительность: [секунды], причина: [конкретное правило и нарушение]  - ЕСЛИ НАРУШЕНИЕ
Оставить, причина: -  - ЕСЛИ НЕТ НАРУШЕНИЯ

ПРИМЕРЫ ОТВЕТОВ ДЛЯ РАЗНЫХ ПРАВИЛ:
Если в правилах: "За оскорбления - бан"
Сообщение: "иди на хуй" → "Удалить, действие: бан, длительность: 0, причина: оскорбление (нарушение правила про оскорбления)"
Если в правилах: "Спам - мут на 1 час"
Сообщение: "купите товар" → "Удалить, действие: мут, длительность: 3600, причина: спам (нарушение правила про рекламу)"
Если в правилах: "Флуд - предупреждение"
Сообщение: "спам спам спам" → "Удалить, действие: предупреждение, длительность: 0, причина: флуд (нарушение правила про флуд)"
Если нарушение не описано в правилах явно, но противорит духу правил:
Сообщение: "угрозы" → "Удалить, действие: бан, длительность: 0, причина: угрозы (нарушение общего правила поведения)"
Сообщение без нарушений: "привет" → "Оставить, причина: -"
Сообщение для анализа:"""
            },
            {
                "role": "user", "text": message.text
            }
        ], "temperature": 0.1, "max_tokens": 70
    }

    response = requests.post(URL, headers=headers, json=data, timeout=10)
    result = response.json()
    decision = result['result']['alternatives'][0]['message']['text'].lower().strip()

    delete_match = re.match(r"удалить,\s*действие:\s*(\w+),\s*длительность:\s*(\d+),\s*причина:\s*(.+)",decision)
    leave_match = re.match(r"оставить,\s*причина:\s*-", decision)

    if delete_match:
        action = delete_match.group(1)
        duration = int(delete_match.group(2))
        reason = delete_match.group(3).strip()
        await apply_punish(message, action, duration, reason)
        await message.delete()
        logger.info(f"УДАЛЕНО | Правила: {rules[:50]}... | Действие: {action} | Длительность: {duration}с | Пользователь: @{message.from_user.username} | Причина: {reason} | Текст: {message.text}")
    elif leave_match:
        logger.info(f"ОСТАВЛЕНО | @{message.from_user.username} | Правила: {rules[:30]}... | Текст: {message.text}")
    else:
        logger.warning(f"НЕПОНЯТНЫЙ ОТВЕТ: '{decision}' | Правила: {rules[:30]}... | Текст: {message.text}")

Теперь реализуем асинхронную функцию apply_punish, которая отвечает за применение наказаний:

async def apply_punish(message: Message, action: str, duration: int, reason: str) -> str:
    user_id = message.from_user.id
    chat_id = message.chat.id

    if action == "бан":
        await message.bot.ban_chat_member(chat_id, user_id)
        text = "Бан (навсегда)"

    elif action == "мут":
        until_date = datetime.now() + timedelta(seconds=duration)
        await message.bot.restrict_chat_member(chat_id, user_id, until_date = until_date, permissions = ChatPermissions(can_send_messages=False, can_send_media_messages=False, can_send_other_messages=False, can_add_web_page_previews=False))
        if duration == 0:
            time_str = "навсегда"
        elif duration < 60:
            time_str = f"{duration} с"
        elif duration < 3600:
            time_str = f"{duration//60} м"
        elif duration < 86400:
            time_str = f"{duration // 3600} ч"
        else:
            days = duration // 86400
            time_str = f"{days} дн"
        text = f"Мут ({time_str})"

    elif action == "кик":
        await message.bot.ban_chat_member(chat_id, user_id, until_date=datetime.now() + timedelta(seconds=30))
        text = "Кик"

    elif action == "предупреждение":
        cursor.execute("INSERT INTO warnings (user_id, group_id, reason, moderator_id) VALUES (?, ?, ?, ?)", (user_id, chat_id, reason, message.bot.id))
        connection.commit()
        text = "Предупреждение"
    return text

Можем попробовать запустить бота. Вводим команду:

python main.py

В консоли мы увидим следующий вывод:

Вывод в консоли
Вывод в консоли

Это означает, что бот успешно запустился и готов к работе. Создадим тестовую группу, добавим бота с правами администратора и пригласим пользователя для тестирования нарушений.

Вот так выглядит вывод в логах, когда сообщение было оставлено:

Вывод времени
Вывод времени

А так выглядит вывод при обнаружении нарушения правил:

Как можно видеть, бот успешно заблокировал пользователя и удалил его сообщение:

Пример работы бота модератора
Пример работы бота модератора

За кадром мы также протестировали другие виды наказаний, и все они работают корректно.

Однако ограничиваться только автоматической выдачей наказаний нельзя, поэтому добавим команды для ручного управления: /ban,/unban, /mute и /unmute.

Для начала реализуем функцию для парсинга времени:

def pars_time(time: str | None) -> datetime | None:
    if not time:
        return None

    match = re.match(r"(\d+)([a-z])", time.lower())
    if match:
        value = int(match.group(1))
        unit = match.group(2)
        match unit:
            case "h": delta = timedelta(hours=value)
            case "d": delta = timedelta(days=value)
            case _: return None
    else:
        return None

    new = datetime.now() + delta
    return new

Теперь последовательно добавим команды модерации:

Команда /ban - блокировка пользователя:

@dp.message(Command("ban"))
async def mute(message: Message, command: CommandObject | None = None) -> None:
    if message.chat.type not in ["group", "supergroup"]:
        await message.answer("Эта команда работает только в групповых чатах!")
        return

    reply = message.reply_to_message
    if not reply:
        return None

    date = pars_time(command.args)
    with suppress(TelegramBadRequest):
        await bot.ban_chat_member(chat_id=message.chat.зid, user_id=reply.from_user.id, until_date=date)
        await message.answer(f"Модератор {message.from_user.mention_markdown(message.from_user.username)} заблокировал пользователя {reply.from_user.mention_markdown(reply.from_user.username)}")

Команда /unban - разблокировка пользователя:

@dp.message(Command("unban"))
async def mute(message: Message):
    if message.chat.type not in ["group", "supergroup"]:
        await message.answer("Эта команда работает только в групповых чатах!")
        return

    reply = message.reply_to_message
    if not reply:
        return None

    with suppress(TelegramBadRequest):
        await bot.unban_chat_member(chat_id=message.chat.id, user_id=reply.from_user.id)
        await message.answer(f"Модератор {message.from_user.mention_markdown(message.from_user.username)} разблокировал пользователя {reply.from_user.mention_markdown(reply.from_user.username)}")

Команда /mute - ограничение возможности отправки сообщений:

@dp.message(Command("mute"))
async def mute(message: Message, command: CommandObject | None = None) -> None:
    if message.chat.type not in ["group", "supergroup"]:
        await message.answer("Эта команда работает только в групповых чатах!")
        return

    reply = message.reply_to_message
    if not reply:
        return None

    date = pars_time(command.args)
    with suppress(TelegramBadRequest):
        await bot.restrict_chat_member(chat_id=message.chat.id, user_id=reply.from_user.id, until_date=date, permissions=ChatPermissions(can_send_messages=False))
        await message.answer(f"Модератор {message.from_user.mention_markdown(message.from_user.username)} выдал мут пользователю {reply.from_user.mention_markdown(reply.from_user.username)}")

Команда /unmute - снятие ограничений:

@dp.message(Command("unmute"))
async def mute(message: Message, command: CommandObject | None = None) -> None:
    if message.chat.type not in ["group", "supergroup"]:
        await message.answer("Эта команда работает только в групповых чатах!")
        return

    reply = message.reply_to_message
    if not reply:
        return None

    with suppress(TelegramBadRequest):
        await bot.restrict_chat_member(chat_id=message.chat.id, user_id=reply.from_user.id, permissions=ChatPermissions(can_send_messages=True))
        await message.answer(f"Модератор {message.from_user.mention_markdown(message.from_user.username)} снял мут пользователю {reply.from_user.mention_markdown(reply.from_user.username)}")

Сохраняем наш файл и проверяем работу, предварительно запустив бота командой:

python main.py

Проверим выдачу и снятие мута:

Снятие мута
Снятие мута

И проверим выдачу и снятие блокировки:

Разблокировка
Разблокировка

Отлично! Наш бот полностью готов к использованию. Осталось задеплоить его на хостинг Amvera Cloud.

Деплой бота модератора

Развернём наш скрипт для модерации в Amvera, воспользовавшись встроенным CI/CD. В отличии от VPS, Amvera предоставляет движек приложений, максимально упрощающий развёртывание и администрирование проекта.

Мы рассмотрим 2 способа деплоя:

  • Через веб-интерфейс Amvera

  • Через Git push

Для начала создадим файл зависимостей requirements.txt:

aiogram==3.21.0
requests==2.32.4

Это минимальный вариант файла — дополнительные зависимости будут установлены автоматически. Вы также можете создать полный файл зависимостей с помощью команды:

pip freeze

Теперь создадим конфигурационный файл amvera.yml:

version: null
meta:
  environment: python
  toolchain:
    name: pip
    version: "3.12"
build:
  requirementsPath: requirements.txt
run:
  scriptName: main.py
  command: null
  persistenceMount: /data
  containerPort: 80

Сделать это можно в разделе Конфигурация, выбрав нужные параметры.

Также обязательно убедитесь, что в файле main.py используется правильный путь к базе данных:

connection = sqlite3.connect("/data/database.db", check_same_thread=False)

Это критически важно, так как при неправильном пути бот не сможет найти базу данных и не запустится.

Теперь приступим непосредственно к деплою. Начнём с деплоя через веб-интерфейс.

Идем на сайт Amvera Cloud заходим в свой личный кабиент и идем в раздел Приложения. Создайте новый проект, укажите название и выберите подходящий тарифный план. На этапе загрузки данных выберите опцию "Через интерфейс" и загрузите необходимые файлы (не включая папку data).

На этапе настройки переменных окружения вы можете добавить переменные для токена бота и токена LLM. Для этого предварительно удалите из файла main.py прямые указания токенов:

BOT_TOKEN = "" # ТОКЕН ВАШЕГО БОТА
AUTH_TOKEN = "" # ТОКЕН LLM В ФОРМАТЕ Bearer XXXXXXXX

Теперь рассмотрим вариант деплоя через Git.

Откройте терминал в папке с проектом и выполните команду:

git init

Создайте приложение, но на этапе загрузки файлов нажмите "Отменить".

Перейдите в раздел созданного приложения и в разделе "Репозиторий" найдите команду "Подключить к существующему репозиторию". Скопируйте и выполните ее в терминале. Команда будет иметь следующий формат:

git remote add amvera https://git.amvera.ru/имя пользователя/имя проекта;

Эта команда подключит удаленный репозиторий проекта к вашему локальному репозиторию. Теперь добавьте файлы в отслеживаемые:

git add amvera.yml main.py requirements.txt

Выполните коммит изменений:

git commit -m "Обновление 1.0"

Теперь отправьте изменения на сервер Amvera (сборка начнется автоматически):

git push amvera master

Появится запрос на ввод учетных данных:

Введите имя пользователя и пароль от вашего аккаунта Amvera Cloud.

Отлично! Бот успешно запустился в обоих случаях:

Запущено через git
Запущено через git
Запущено через интерфейс
Запущено через интерфейс

На этом наш деплой законечен!

Заключение

Мы успешно реализовали интеллектуального Telegram-бота-модератора, который использует LLM Amvera Cloud для семантического анализа сообщений. В отличие от примитивных фильтров по ключевым словам, наша система способна понимать контекст, учитывать специфические правила каждого чата и автоматически применять адекватные санкции к нарушителям.

Такой подход позволяет:

  • Снизить нагрузку на администраторов

  • Повысить качество модерации

  • Гибко адаптировать поведение бота под разные сообщества

  • Обеспечить проактивное выявление нарушений до эскалации конфликтов

На практике это означает, что администраторам больше не нужно постоянно мониторить чат вручную и разбирать каждый спорный случай — бот берет эту рутинную работу на себя, позволяя модераторам сосредоточиться на стратегических задачах. В результате чат становится более безопасным и комфортным пространством, а пользователи — более дисциплинированными, зная, что нарушения не останутся незамеченными.

И главное, в сравнении с классическим ML, модель не нужно переобучать, достаточно просто изменить промт.

Код проекта доступен по ссылке на GitHub.


Релевантные материалы:

Комментарии (2)


  1. dimonier
    11.09.2025 05:39

    Спасибо!

    Инструкции по созданию бота через botfather уже несколько утомили, но в общем статья полезная: увидел, как временно ограничивать пользователя.


  1. Arctell
    11.09.2025 05:39

    Я сделал проще... Ловим реакции нууу пальцы вниз например по порогу удаляем , в живом чатике люди справляются им приятно они не наблюдатели а участники