Клиентский бот с нейросетью кажется простой задачей: принять сообщение, отправить его модели и вернуть ответ. Первый прототип действительно можно собрать за вечер. Проблемы начинаются после запуска.
История диалога растёт, расходы увеличиваются незаметно, в группе бот отвечает на чужие сообщения, а один активный пользователь способен занять все доступные ресурсы. Если API-ключ случайно попал во фронтенд или открытый репозиторий, экономику проекта уже можно не считать.
Ниже — практическая схема бота-ассистента, который отвечает клиентам по текстовой базе знаний. В примере используются Python, aiogram и OpenAI-совместимый шлюз NeuroZal API. Отдельно разберём вариант без кода через n8n, стоимость 1000 запросов и ограничения, которые лучше учесть до публикации ссылки на бота.
Что именно собираем
Задача — сделать чат-бота для бизнеса, который:
- принимает личные сообщения в Telegram;
- отвечает по базе знаний компании;
- помнит несколько последних реплик;
- не реагирует на каждое сообщение в групповом чате;
- ограничивает число обращений одного пользователя;
- не выполняет параллельно несколько запросов от одного человека;
- хранит ключи только на сервере;
- корректно сообщает о временной ошибке модели.
Такой бот подходит для ответов на частые вопросы, внутренней базы инструкций, первичного сбора обращений и помощи сотрудникам.
Он не должен без проверки обещать клиенту возврат денег, наличие товара, срок поставки или результат услуги. Для таких действий нужна отдельная бизнес-логика: запрос к учётной системе, проверка полномочий и подтверждение операции.
Как устроено подключение к моделям
NeuroZal API — единый OpenAI-совместимый API-шлюз к моделям OpenAI, Anthropic, Google, xAI, DeepSeek и других поставщиков.
Адрес подключения:
https://api.neurozal.ru/v1
Он совместим с OpenAI SDK. Для Claude Code поддерживается Anthropic-совместимый маршрут /v1/messages, для Codex CLI — /v1/responses.
Регистрация и личный кабинет находятся на neurozal.ru. В кабинете создаётся ключ вида sk-nz-..., отображаются остаток токенов, статистика запросов и расходы. Регистрация занимает минуту — код из письма на этом шаге не нужен. Новым клиентам при регистрации начисляется 35 ₽ на баланс: это стартовая квота для тестов.
В каталоге 37 рабочих моделей. Среди них есть GPT-5.6, GPT-5.5, GPT-4.1, GPT-6 Astra, GPT-4o-mini, Claude Opus 5 и 4.8, Claude Sonnet 5 и 4.6, Gemini 3.8 Flash, Gemini 3.1 и 2.5 Pro, Grok 4.5–4.7, DeepSeek V4, Qwen, GLM 5.3 и Kimi K3.
Для первого запуска разумно взять недорогую модель, проверить качество на реальных вопросах и только потом сравнивать её с более тяжёлыми вариантами.
Обязательный дисклеймер: это API-ключ для программ, редакторов кода, ботов и автоматизации. Это не аккаунт ChatGPT или Claude с логином и паролем. Ключ не подключается в мобильном приложении ChatGPT и не заменяет подписку Plus. Модель выбирает пользователь: один ключ открывает весь доступный список моделей, а не одну конкретную модель.
Подготовка проекта на Python
Понадобятся Python, токен Telegram-бота и API-ключ NeuroZal.
Установка библиотек:
pip install aiogram openai
Секреты передаём через переменные окружения:
export TELEGRAM_BOT_TOKEN="токен_бота"
export NEUROZAL_API_KEY="sk-nz-..."
export BOT_USERNAME="имя_бота_без_собаки"
export MODEL="gpt-4o-mini"
export MAX_REQUESTS_PER_USER_DAY="50"
Число запросов на пользователя здесь не является лимитом сервиса. Это настройка конкретного проекта. Её нужно выбирать по бюджету и сценарию использования.
Базу знаний сохраним в файле knowledge.txt. Каждый самостоятельный фрагмент отделяем пустой строкой:
Компания работает с понедельника по пятницу.
Для оформления обращения клиент сообщает номер заказа.
Срок и стоимость доставки проверяются по данным учётной системы.
В демонстрационном варианте используется простой поиск по совпадающим словам. Для небольшой базы частых вопросов этого достаточно. Для большого каталога документов понадобится отдельный поиск по фрагментам, но принцип вызова модели останется тем же.
Рабочий пример на aiogram
import os
import re
import asyncio
import logging
from collections import defaultdict, deque
from datetime import date
from aiogram import Bot, Dispatcher, F
from aiogram.enums import ChatType
from aiogram.filters import CommandStart
from aiogram.types import Message
from openai import (
AsyncOpenAI,
APIConnectionError,
APITimeoutError,
APIStatusError,
RateLimitError,
)
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
API_KEY = os.environ["NEUROZAL_API_KEY"]
BOT_USERNAME = os.getenv("BOT_USERNAME", "").lower()
MODEL = os.getenv("MODEL", "gpt-4o-mini")
MAX_REQUESTS_DAY = int(os.getenv("MAX_REQUESTS_PER_USER_DAY", "50"))
MAX_HISTORY_CHARS = int(os.getenv("MAX_HISTORY_CHARS", "12000"))
MAX_MESSAGE_CHARS = int(os.getenv("MAX_MESSAGE_CHARS", "4000"))
bot = Bot(BOT_TOKEN)
dp = Dispatcher()
client = AsyncOpenAI(
api_key=API_KEY,
base_url="https://api.neurozal.ru/v1",
timeout=45.0,
)
logging.basicConfig(level=logging.INFO)
with open("knowledge.txt", "r", encoding="utf-8") as file:
KNOWLEDGE = [
part.strip()
for part in file.read().split("\n\n")
if part.strip()
]
histories = defaultdict(lambda: deque(maxlen=12))
locks = defaultdict(asyncio.Lock)
usage = {}
def find_context(question: str, limit: int = 3) -> str:
words = set(re.findall(r"[а-яёa-z0-9]+", question.lower()))
ranked = []
for part in KNOWLEDGE:
part_words = set(re.findall(r"[а-яёa-z0-9]+", part.lower()))
score = len(words & part_words)
if score:
ranked.append((score, part))
ranked.sort(key=lambda item: item[0], reverse=True)
return "\n\n".join(part for _, part in ranked[:limit])
def trim_history(user_id: int) -> None:
history = histories[user_id]
while sum(len(item["content"]) for item in history) > MAX_HISTORY_CHARS:
if len(history) < 2:
history.clear()
break
history.popleft()
history.popleft()
def allow_request(user_id: int) -> bool:
today = date.today()
saved_day, count = usage.get(user_id, (today, 0))
if saved_day != today:
saved_day, count = today, 0
if count >= MAX_REQUESTS_DAY:
usage[user_id] = (saved_day, count)
return False
usage[user_id] = (saved_day, count + 1)
return True
def group_message_allowed(message: Message) -> bool:
if message.chat.type == ChatType.PRIVATE:
return True
if not BOT_USERNAME or not message.text:
return False
return f"@{BOT_USERNAME}" in message.text.lower()
@dp.message(CommandStart())
async def start_handler(message: Message) -> None:
await message.answer(
"Напишите вопрос. Я постараюсь найти ответ в базе знаний."
)
@dp.message(F.text)
async def text_handler(message: Message) -> None:
if not message.from_user or not group_message_allowed(message):
return
user_id = message.from_user.id
text = message.text.strip()
if BOT_USERNAME:
text = re.sub(
rf"@{re.escape(BOT_USERNAME)}",
"",
text,
flags=re.IGNORECASE,
).strip()
if not text:
return
if len(text) > MAX_MESSAGE_CHARS:
await message.answer("Сообщение слишком длинное. Сократите вопрос.")
return
async with locks[user_id]:
if not allow_request(user_id):
await message.answer(
"Лимит обращений на сегодня исчерпан."
)
return
context = find_context(text)
history = histories[user_id]
messages = [
{
"role": "system",
"content": (
"Ты ассистент компании. Отвечай только на основании "
"переданного контекста. Если ответа нет, прямо скажи, "
"что вопрос нужно передать сотруднику.\n\n"
f"Контекст:\n{context or 'Подходящий фрагмент не найден.'}"
),
},
*list(history),
{"role": "user", "content": text},
]
try:
response = await client.chat.completions.create(
model=MODEL,
messages=messages,
max_tokens=600,
)
answer = response.choices[0].message.content
if not answer:
answer = "Не удалось сформировать ответ."
history.append({"role": "user", "content": text})
history.append({"role": "assistant", "content": answer})
trim_history(user_id)
await message.answer(answer)
except (RateLimitError, APITimeoutError, APIConnectionError):
logging.exception("Временная ошибка API")
await message.answer(
"Сервис временно недоступен. Попробуйте позже."
)
except APIStatusError:
logging.exception("Ошибка ответа API")
await message.answer(
"Не удалось обработать запрос. Попробуйте позже."
)
except Exception:
logging.exception("Необработанная ошибка")
await message.answer(
"Произошла внутренняя ошибка."
)
async def main() -> None:
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Код решает базовые эксплуатационные задачи.
Ключи читаются из окружения и не отправляются пользователю. На каждого пользователя создаётся отдельная блокировка, поэтому пять быстрых сообщений не запускают пять параллельных генераций. Суточный счётчик ограничивает расходы, а история обрезается по объёму и числу сообщений.
В группах бот отвечает только при явном упоминании. Это защищает от ситуации, когда модель реагирует на весь поток чата.
Хранилища в примере находятся в памяти процесса. После перезапуска история и счётчики обнулятся. Для рабочего проекта с несколькими процессами их нужно вынести во внешнее хранилище.
Сколько стоит 1000 запросов
Стоимость зависит не от количества сообщений как такового, а от числа входных и выходных токенов.
Возьмём расчётный сценарий:
- 1000 запросов в месяц;
- на каждый запрос приходится 2000 входных токенов с инструкцией, контекстом и историей;
- ответ занимает 500 выходных токенов;
- за месяц получается 2 млн входных и 0,5 млн выходных токенов.
Это пример для сравнения, а не прогноз конкретного бота. Короткие ответы могут стоить дешевле, длинная база знаний и разросшаяся история — дороже.
| Модель | Цена входа за 1 млн | Цена выхода за 1 млн | Вход за месяц | Выход за месяц | Итого |
|---|---|---|---|---|---|
| gpt-4o-mini | 13 ₽ | 53 ₽ | 26 ₽ | 26,50 ₽ | 52,50 ₽ |
| gemini-3.8-flash | 67 ₽ | 334 ₽ | 134 ₽ | 167 ₽ | 301 ₽ |
| claude-sonnet-4.6 | 267 ₽ | 1335 ₽ | 534 ₽ | 667,50 ₽ | 1201,50 ₽ |
| claude-opus-4.8 | 445 ₽ | 2225 ₽ | 890 ₽ | 1112,50 ₽ | 2002,50 ₽ |
| gpt-6-astra | 890 ₽ | 4450 ₽ | 1780 ₽ | 2225 ₽ | 4005 ₽ |
Разница между gpt-4o-mini и gpt-6-astra в этом сценарии — более чем в 76 раз. Поэтому ставить тяжёлую модель на каждый вопрос о графике работы экономически бессмысленно.
Если под 2000 токенов понимать общий объём запроса и ответа без известного разделения, точную сумму назвать нельзя. Для gpt-4o-mini стоимость 2 млн токенов окажется между 26 и 106 ₽, а для gpt-6-astra — между 1780 и 8900 ₽. Реальная цифра зависит от доли ответа.
Оплата в NeuroZal API производится в рублях картой РФ. Пополнение — без комиссии, абонентской платы нет. Оплачиваются использованные токены, остаток не сгорает в конце месяца.
Вариант без кода через n8n
Если не хочется поддерживать Python-приложение, базовую схему можно собрать в n8n:
- Нода
Telegram Triggerпринимает сообщение. - Нода проверки отбрасывает группы, команды и сообщения без нужного упоминания.
- Нода хранилища получает историю пользователя и проверяет его лимит.
- Нода
HTTP Requestотправляет запрос к шлюзу. - Следующая нода сохраняет ответ и обновляет счётчик.
- Telegram-нода отправляет результат пользователю.
- Планировщик очищает устаревшую историю или обнуляет периодические лимиты.
Параметры запроса:
POST https://api.neurozal.ru/v1/chat/completions
Authorization: Bearer sk-nz-...
Content-Type: application/json
Тело:
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": "Отвечай только по переданной базе знаний."
},
{
"role": "user",
"content": "Текст вопроса"
}
]
}
API-ключ нужно хранить в защищённых учётных данных n8n, а не вставлять в публичную схему. Планировщик полезен не только для очистки истории: через него можно проверять зависшие обращения и формировать внутреннюю статистику.
Минус n8n проявляется, когда логика усложняется. Фильтрация документов, параллельные обращения, права сотрудников и нестандартная обработка ошибок быстро превращают схему в большое полотно. Для короткого сценария это удобно, для сложного продукта код обычно легче тестировать и сопровождать.
Главные грабли после запуска
История диалога незаметно съедает баланс
Каждый новый запрос обычно включает предыдущие реплики. Десятое сообщение оплачивает часть текста, который уже передавался девять раз.
Ограничения по числу реплик недостаточно: одно сообщение может быть коротким, другое — содержать длинный документ. Нужен подсчёт токенов для выбранной модели либо жёсткое ограничение размера истории с запасом.
Практичная схема — хранить несколько последних обменов, старую часть периодически сокращать и передавать только подходящие фрагменты базы знаний.
В группах бот отвечает на всё подряд
Без фильтра модель может реагировать на обычные разговоры участников. Это раздражает людей и расходует баланс.
Разрешайте ответы только при упоминании имени бота, конкретной команде или ответе на его сообщение. Для рабочих групп дополнительно полезен список разрешённых чатов.
API-ключ нельзя держать во фронтенде
Ключ нельзя помещать в браузерный код, мобильное приложение, открытую конфигурацию или публичный репозиторий. Пользователь сможет извлечь его и отправлять запросы за ваш счёт.
Запрос к модели должен идти через сервер, n8n или другую контролируемую среду. Если ключ раскрылся, его нужно заменить в личном кабинете, а не надеяться, что никто не заметил.
Нужен лимит на пользователя
Один человек может отправить сотни сообщений вручную или запустить автоматическую отправку. Минимальная защита — ограничение числа обращений, контроль параллельных запросов и ограничение длины текста.
Значения нужно выбирать по экономике проекта. Универсального числа нет: внутренний бот для пяти сотрудников и публичный помощник магазина имеют разный профиль нагрузки.
Честные минусы и ограничения
Бот с базой знаний не гарантирует фактическую точность. Если контекст неполный или противоречивый, модель может сформулировать убедительный, но неподходящий ответ. Критичные действия нужно подтверждать через бизнес-системы или передавать сотруднику.
Простой поиск по словам плохо работает с большой базой и разными формулировками одного вопроса. По мере роста документов потребуется более точный механизм отбора фрагментов.
Память и лимиты из примера не подходят для нескольких экземпляров приложения: каждый процесс будет видеть собственные данные. Понадобится общее хранилище.
Расход зависит от модели. Claude Opus и GPT-5.6 тратят баланс заметно быстрее, чем Gemini Flash и DeepSeek. Дешёвые модели — не подмена, а осознанный выбор для рутинных запросов. Качество лучше проверять на наборе реальных диалогов, а не по одному удачному ответу.
Если возникнут вопросы по подключению или оплате, поддержка доступна в чате магазина или сервиса.
Чек-лист перед запуском
- [ ] Ключ NeuroZal хранится в переменной окружения или защищённом хранилище.
- [ ] Токен Telegram-бота не находится в репозитории.
- [ ] Выбрана модель и рассчитан месячный бюджет.
- [ ] Ограничены длина сообщения и размер истории.
- [ ] Настроен лимит запросов на пользователя.
- [ ] Параллельные запросы одного пользователя блокируются.
- [ ] В группах действует фильтр по упоминанию или команде.
- [ ] Ошибки API записываются в журнал без показа технических деталей клиенту.
- [ ] База знаний разбита на небольшие самостоятельные фрагменты.
- [ ] Бот честно передаёт вопрос человеку, если в базе нет ответа.
- [ ] Статистика запросов и расходы регулярно проверяются в кабинете.
- [ ] На тестовых диалогах проверены цена, качество и спорные ответы.
Практический итог
Для первого чат-бота не нужен отдельный ключ на каждую модель. Один ключ NeuroZal API позволяет переключать модели в настройке приложения и сравнивать их на одинаковых вопросах.
Начните с gpt-4o-mini или другой недорогой модели, ограничьте историю и задайте пользовательские лимиты. После этого соберите несколько десятков реальных обращений и проверьте, где качества не хватает. Тяжёлую модель лучше подключать точечно, а не оплачивать её на каждом простом запросе.
Если такая архитектура подходит под вашу задачу, можно создать ключ в кабинете neurozal.ru, пополнить баланс на тестовую сумму и сначала прогнать закрытый сценарий на своей команде. Это дешевле и безопаснее, чем исправлять ограничения уже после публичного запуска.