Telegram-бот на моделях NVIDIA API со стримингом ответов и предпросмотром прямо в сообщении.
По умолчанию — Nemotron 3 Super 120B; модель переключается прямо в чате через /menu.
- Показывает процесс «думания» поэтапно (таймер + предпросмотр ответа)
- Streaming-режим (по токенам)
- Помнит контекст диалога (полная память в личке, короткая в группах)
- Выбор провайдера и модели через меню (/menu)
- Поиск в интернете перед ответом: модель сама решает, когда искать, и приводит источники (нужен
TAVILY_API_KEY) - Викторины (/quiz), умные опросы (/poll), помощь с кодом (/code), задачи (/task)
- Техсобеседование по Go: /interview — вопросы с реальных собеседований (база в
interview.json), режимы «тест с вариантами» и «напиши сам» + разбор правильного ответа - Путь/roadmap по стеку и языкам: /roadmap — уровни с конкретными языками, продвижение и отметка тем
- Настройка стека: /stack (или кнопка «🧰 Стек и языки») — выбрать готовый шаблон или собрать свой стек из языков
- Режим обучения: /learn (или кнопка «🎓 Режим обучения») — бот высылает теорию по уровню, затем тест-викторину и проверяет знания по каждому ответу
- Экспорт для Obsidian: /export — скачивает весь roadmap + сгенерированные задачи/вопросы одним .md файлом
- Прогресс, выбор стека и выбранная модель сохраняются в
progress.json(переживают перезапуск) - При отказе модели (rate limit, таймаут, недоступность) бот повторяет запрос, затем автоматически переходит на запасную модель из меню и помечает это в ответе
- Групповые команды: /context, /summary, /judge
- Работает в личке, по @упоминанию и по reply в группах
Скорость замерена одним и тем же промптом в одном окне, поэтому цифры сравнимы между собой:
ключ в /menu |
модель | ток/с | первый токен |
|---|---|---|---|
super-120b (по умолчанию) |
nvidia/nemotron-3-super-120b-a12b |
115 | 1.3с |
lightning-30b |
nvidia/nemotron-3.5-lightning-30b-a3b |
149 | 4.0с |
ultra-550b |
nvidia/nemotron-3-ultra-550b-a55b |
47 | 3.4с |
inkling |
thinkingmachines/inkling |
36 | 7.5с |
minimax-m3 |
minimaxai/minimax-m3 |
7–17 | 2.7с |
deepseek-flash |
deepseek-ai/deepseek-v4-flash-0731 |
не замерено | не замерено |
Порядок в таблице — не украшение. Первый ключ идёт моделью по умолчанию, а
следующие за текущей уходят в запасные (_fallback_models берёт следующие
MAX_FALLBACKS), поэтому сразу за super-120b стоят два самых быстрых
варианта: подмена модели не должна оборачиваться ожиданием.
Рассуждающих моделей в списке нет намеренно. Такая модель молчит, пока
думает, и первый токен приходит через десятки секунд — в чате это неотличимо от
зависшего бота. Скорость конкретной модели удобно замерить скриптом
test_flash.py, он же покажет, как она раскладывает поток по полям:
python test_flash.py nvidia/nemotron-3-super-120b-a12bdeepseek-flash — с неё бот и начинался, потом её сменили на Qwen. Размышления
у DeepSeek V4 включаются явно (chat_template_kwargs: {thinking: true}),
то есть по умолчанию модель отвечает сразу, без прохода размышлений. В списке
стоит последней намеренно: пока она не проверена на текущем каталоге, в запасные
модели не попадает (_fallback_models берёт первые MAX_FALLBACKS), но в
/menu выбирается.
Проверить, что ключ её видит:
python test_models.py deepseek-ai/deepseek-v4-flash-0731Пропускная способность публичного эндпоинта плавает в зависимости от нагрузки, так что
конкретные значения будут отличаться — важен порядок величин. Список моделей задаётся
словарём PROVIDERS["nvidia"]["models"] в main.py, туда же можно добавить любую другую
модель из каталога (см. list_models.py).
Ответ ждут в чате, поэтому в коде есть места, специально сделанные ради скорости. Ломать их «заодно» не стоит:
- Модель по умолчанию — не рассуждающая. Это самый крупный вклад: рассуждающая молчит перед первым токеном столько же, сколько потом печатает ответ.
- Один общий
httpx.AsyncClientна процесс (http_client()вmain.py,client()вresearch.py). Раньше клиент создавался на каждый запрос, то есть на каждое обращение к модели приходилось полное TLS-рукопожатие, а на один детальный вопрос их уходит до восьми. Теперь соединения переиспользуются. - История в промпте обрезается. Она уходит в модель дважды за сообщение
(маршрутный вызов + ответ), и десять развёрнутых ответов — это десятки тысяч
токенов префилла перед первым токеном. Последний обмен идёт целиком, ответы
постарше режутся до
MAX_OLD_REPLY_CHARS. - Заглушку с таймером правит одна фоновая задача. Цикл чтения токенов только
копит куски: пока он ждал ответа от Telegram на свою правку, он не вычитывал
поток от модели. Просыпается задача по событию, а не по
sleep, иначе готовый ответ ждал конца её сна — до двух лишних секунд на каждом ответе. - Таймауты разнесены по фазам (
request_timeout): соединение обязано устанавливаться заCONNECT_TIMEOUT, аSTREAM_TIMEOUTотмеряется между порциями потока. Одно общее число держало зависший коннект столько же, сколько живую генерацию.
Потолки времени задаются переменными окружения — см. .env.example.
У любой модели есть отсечка знаний, поэтому без поиска она уверенно выдаёт устаревшие данные за свежие. Чтобы этого не происходило:
- в промпт подставляется сегодняшняя дата и указание опираться на поиск, а не на память;
- бот описывает модели инструмент
web_search, и та сама решает, когда им воспользоваться. В описании инструмента явно перечислено, когда искать НЕ надо: теория, код, отладка, математика, переводы, определения, болтовня — на это модель отвечает сама. Незнакомое слово тоже не повод для поиска: бот скажет, что не знает термина, и переспросит. Проверяется скриптомtest_search_trigger.py(14 случаев, бьёт по живому API); - за один вызов модель присылает 2-4 разные формулировки одного вопроса; они уходят в Tavily параллельно, выдача сливается с дедупликацией по URL;
- поиск идёт до 4 кругов: модель уточняет формулировки и углубляется в детали. Формулировки, уже отправленные в поиск, повторно не ищутся;
- пока идёт поиск, в сообщении построчно копится ход работы:
🔍запросы и📄сайты, на которые бот заходит. Правки сообщения троттлятся (не чаще раза в 1.2с), иначе Telegram включает флуд-контроль; - получив выдачу, модель сверяет факты между источниками: официальным источникам (документация, релиз-ноуты) веса больше, чем блогам и агрегаторам, а расхождения она обязана назвать прямо;
- последней строкой ответа идёт оценка: «Достоверность: высокая/средняя/низкая — почему»;
- источники указываются компактной строкой доменов после текста, без отдельного заголовка.
Реализация — в research.py, он не зависит от main.py и получает вызов модели колбэком.
Ограничители заданы константами там же: MAX_ROUNDS, MAX_RESULTS, MAX_CHARS_TOTAL.
Без TAVILY_API_KEY шаг поиска пропускается целиком и бот работает как раньше —
прод не сломается, если ключ забыли прописать в окружении.
Шаг «решить, нужен ли интернет» стоит отдельного обращения к модели — она
отвечает вызовом инструмента вместо текста. На приветствиях и благодарностях он
пропускается: список в SMALL_TALK сверяется с репликой целиком, так что
«привет, найди курс биткоина» по-прежнему идёт через поиск.
Настройка сделана в пользу полноты, а не скорости: ответ подробный и структурированный, разбитый на разделы с таблицами. На практике вопрос вроде «что нового в Go в 2026» занимает 2-3 минуты и даёт около 9000 символов текста.
Ответ всегда приходит одним сообщением. У Telegram лимит 4096 символов, и при превышении он отвергает сообщение целиком, поэтому длинный ответ публикуется на telegra.ph — в чате остаётся начало и ссылка «Читать полностью», которая раскрывается в клиенте окном Instant View.
Ни Telegram, ни Telegraph не поддерживают <table>, поэтому таблицы рисуются
моноширинным блоком. Модуль tables.py выравнивает колонки по ширине и ставит
линию под заголовком:
Версия │ Дата │ GC
───────┼────────────┼──────────
1.26 │ 2026-02-10 │ Green Tea
Если таблица шире экрана (на телефоне это ~42 моноширинных символа), она разворачивается в список «поле: значение» — иначе строки переносятся и вёрстка разъезжается:
▸ Генерические методы
Описание: Методы могут иметь собственные типовые параметры
Источник: go.dev
Код в ячейках — отдельная история. Многострочная ячейка в markdown невозможна,
поэтому модель записывает переносы escape-последовательностями, и без обработки
пример кода приезжает одной нечитаемой строкой с и \". Такой код вынимается
из ячейки и печатается настоящими строками, а таблица принудительно идёт списком:
выровнять многострочную ячейку нельзя. В промпте модель вдобавок просят выносить
код отдельным блоком и не помещать его в таблицы.
Готовые блоки <pre> прячутся от markdown-конвертера и от финальной чистки
пробелов (re.sub(r' * *', ...)) — обе срезают отступы в начале строк, на
которых держится вёрстка.
Tavily вместе с выдачей отдаёт картинки с текстовыми описаниями
(include_images + include_image_descriptions). Описания попадают в результаты
поиска каталогом, и модель сама расставляет маркеры [imgN] там, где картинка
уместна — она видит описание и отсеивает декоративное (на проверке пропустила
«мультяшного хомяка», взяв баннеры релиза).
Картинки живут только на странице Telegraph: в сообщении Telegram изображений в тексте быть не может, поэтому оттуда маркеры вырезаются. Одна картинка показывается один раз, несуществующие номера игнорируются.
Telegraph принимает контент только массивом узлов (на сырой HTML отвечает
CONTENT_FORMAT_INVALID), поэтому в telegraph.py лежит свой конвертер markdown →
узлы: заголовки, списки, блоки кода, ссылки; таблиц у Telegraph нет, они уходят в <pre>.
API публичный, ключ не нужен — аккаунт создаётся на лету. Свой можно зафиксировать
переменной TELEGRAPH_TOKEN.
Видимая часть подбирается замером: md_to_html раздувает markdown неравномерно
(сильнее всего жирный шрифт), поэтому кусок ужимается, пока не влезет вместе
с заголовком, ссылкой и списком источников.
Если полнота важнее времени — крутите MAX_ROUNDS, MAX_QUERIES и MAX_RESULTS
в research.py вверх; если наоборот, вниз. Поэтому поиск и отдан на усмотрение модели,
а не включён на каждое сообщение.
cd numbertree_bot
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # Linux/Mac
pip install -r requirements.txt
cp .env.example .env
# Отредактируйте .env и вставьте ваши токены- Напишите @BotFather
- Отправьте
/newbot - Следуйте инструкциям
- Скопируйте токен в
.envкакTELEGRAM_BOT_TOKEN
- Зарегистрируйтесь на build.nvidia.com
- Создайте API Key (начинается с
nvapi-) - Скопируйте в
.envкакNVIDIA_API_KEY
Endpoint (OpenAI-совместимый) уже настроен в коде:
https://integrate.api.nvidia.com/v1
Если ходите через свой прокси, переопределите базовый адрес переменной NVIDIA_API_BASE.
Проверить, какие модели доступны вашему ключу:
python list_models.py minimax # фильтр по подстроке
python list_models.py --all # весь каталогpython main.pyЧто и когда чинилось — в CHANGELOG.md.
Офлайн, без ключей и без обращений к API:
python test_regressions.py # разметка, счёт собеседования, настройки, команды
python test_interview_routing.py # маршрутизация текста во время интервьюОстальные test_*.py — разведочные скрипты по живому API: им нужен
NVIDIA_API_KEY, и они тратят запросы.
Премиальные эмодзи <tg-emoji> доступны только ботам с дополнительным именем,
купленным на Fragment. Если Telegram отвергает разметку сообщений целиком —
выключите их переменной CUSTOM_EMOJI=0.
Важно про сохранение данных. progress.json — единственный файл, который бот пишет
(настройки модели, выбранный стек, прогресс по уровням). Файловая система контейнера
на Railway эфемерная: без примонтированного тома этот файл стирается при каждом деплое
и при каждом рестарте по расписанию, то есть настройки сбрасываются ежедневно.
Чтобы данные жили:
- В сервисе Railway: Attach volume, точка монтирования — например
/data(в/appмонтировать нельзя: том перекроет код бота). - Добавить переменную окружения
DATA_DIR=/data.
Каталог создаётся автоматически при первом запуске.
docker build -t telegram-bot .Расписание запуска/остановки настроено в .github/workflows/bot-schedule.yml (09:00–01:00 MSK).
- Пользователь пишет вопрос
- Бот отправляет сообщение «Думаю...»
- В процессе генерации бот обновляет сообщение каждые ~300 символов и обновляет таймер каждые 2 секунды
- В финале показывает ответ с HTML-разметкой, счётчиком токенов и заполнением контекста
Размер контекстного окна NVIDIA в API не сообщает (/v1/models отдаёт только id и
владельца) и переполнение не отвергает, поэтому величина для индикатора измерена вручную:
все пять моделей приняли промпт на 115k токенов, super-120b — на 360k. В конфиге стоят
консервативные 128k (PROVIDERS["nvidia"]["context"]) — это ориентир, а не жёсткий предел.
numbertree_bot/
├── main.py # Основной код бота
├── research.py # Веб-поиск (Tavily) и цикл tool calling
├── telegraph.py # Публикация длинных ответов на telegra.ph
├── tables.py # Markdown-таблицы в моноширинный вид
├── interview.json # База вопросов для /interview
├── roadmap.json # Каталог языков + готовые шаблоны стеков (редактируйте под себя)
├── progress.json # Настройки стека и прогресс пользователей (создаётся автоматически)
├── requirements.txt # Зависимости
├── .env.example # Пример конфигурации
└── .env # Ваши секреты (не коммитить в git!)
Конфигурация лежит в roadmap.json рядом с ботом и состоит из двух частей:
languages — каталог языков. У каждого языка:
id— ключ (напримерgo,python)name— отображаемое имяicon— эмодзиstack— описание стекаlevels— путь по языку от нуля. Каждый уровень задаёт:title— название уровняdifficulty— easy/medium/hard (влияет на генерацию задач)languages— под какой язык генерируются задачи/викториныfocus— темы (через запятую), их можно отмечать пройденными в менюtasks— готовые задачи уровня
templates — готовые шаблоны стека. Каждый шаблон — {id, name, icon, description, languages:[id, ...]} — список языков из каталога, чьи уровни идут последовательно. Например «Go · от нуля до мидла» = только путь по Go.
Бот читает roadmap.json при старте. Меняете конфиг — перезапустите бота.
Управление:
/stack(или кнопка «🧰 Стек и языки») — выбрать готовый шаблон (✅) или собрать свой стек, поочерёдно отмечая языки (✅). Кнопка «Сбросить» возвращает демо-режим (все языки каталога)/roadmap(или кнопка «🗺️ Roadmap») — уровень показывает активный путь (по выбранному стеку) с прогрессом «темы пройдено/всего»- Внутри уровня: отметить тему ✅, сгенерировать задачу по стеку 💻, викторину по языку 🧠, перейти к следующему уровню ➡️
/learn— теория + тест по текущему уровню/export(или кнопка «📤 Экспортировать всё») — собирает все уровни + сгенерированные за сессию задачи/викторины в один markdown-файл и отправляет его- Выбранный стек и прогресс сохраняются в
progress.jsonи переживают перезапуск - Сгенерированный контент хранится в памяти (по чату) и сбрасывается при перезапуске
База вопросов с реальных Go-собеседований лежит в interview.json рядом с ботом (источник — nilchan.com). Структура:
categories— категории (горутины, каналы, синхронизация, слайсы/строки, память/GC, PostgreSQL)questions— вопросы. У каждого:category— категорияq— формулировка вопросаtype—mc(тест с вариантами) илиopen(напиши сам)options+correct— варианты и индекс правильного (дляmc)answer— корректный ответ (дляopen)explanation— разбор «как правильно ответить»
Управление:
/interview(или кнопка в меню) — выбрать категорию, случайный вопрос или все вопросы вперемешку- Тест (
mc) — запускается полл с вариантами; после выбора приходит счёт и объяснение - Открытый ответ (
open) или просто напишите ответ текстом — LLM оценит ваш вариант по шкале 0-10 и зачтёт его в счёт сессии (балл 7+ = «верно»). НуженNVIDIA_API_KEY - При завершении (
🏁 Завершитьили слово «завершить/стоп») бот составляет резюме собеседования: итоговый уровень знаний, сильные стороны, что подтянуть и план до middle - Счёт сессии и сброс — через кнопку «🏁 Завершить»