Проект: Ио — бот для чатов  Публичный пост
30 июля 2026  222
Ио — бот для чатов
https://t.me/PhoronisBot

Ио — это бот на основе LLM, заточенный под работу в чатах, идеологически похож на Сучару, но более культурный. Функционал, наверное, поскромнее, но в чатах с ней всё равно весело. Ио умеет отвечать на запросы пользователя, распознавать картинки и войсы, учитывать текущий тред, историю чата и информацию о пользователе. Сейчас бот живёт в нескольких десятках чатов.

Расскажу про то, как пилил его кучу лет, сражался с памятью, пытался разобраться в векторных базах и в итоге получил что-то более-менее работающее.

Как появилась идея? Что вдохновило?

Много лет назад существовало околокосмическое комьюнити Alpha Centauri. У него был оффтоп-чат, а в нём жил чат-бот.. Никаких LLM тогда ещё не существовало, бот работал на регулярках, умел искать погоду, банить пользователей, запоминать информацию и прочие приколюхи. Чат вместе с ботом канул в Лету, а люди разбежались по другим чатам. И через какое-то время мне захотелось написать похожего бота.

Прототип из эпохи до LLM

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

Токенайзер работал так: удалял из текста ссылки, разбивал его по знакам препинания на предложения, затем предложения — на слова, переводил их в нижний регистр и применял стеммер.

Например, слова «расскажи» и «рассказать» превращаются в основу «расска», а «погода» и «погоду» — в «погод».
Таким образом, список искомых команд не разрастался до десятков вариантов написания одного и того же. В русском языке есть всякие склонения, падежи и прочие усложнения, поэтому работало это не идеально.

Местами всё равно приходилось писать несколько вариантов
Местами всё равно приходилось писать несколько вариантов

Среди первых функций были прогноз погоды, возможность банить пользователей, распознавание войсов и перевод текстов.

С погодой тоже были сложности. Пользователь мог писать имя города в разных вариантах, поэтому, чтобы определить город, я пользовался сервисом DaData. Сам прогноз брал и до сих пор беру из OpenWeather. Источник не лучший, зато бесплатный.

Ну а для перевода и распознавания войсов использовал сервисы Яндекса. Кружки тоже хотелось распознавать, поэтому пришлось затащить FFmpeg для извлечения аудио из видео.

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

Первые попытки с LLM

В 2023 году начал набирать популярность ChatGPT. Он уже умел в русский язык, имел API — кажется, всё сошлось.

Сначала я попробовал написать отдельного бота. В тот момент удобного способа вызывать функции через LLM ещё не было. Получился примитивный бот на базе GPT-3.5 Turbo. Контекст хранился в оперативной памяти, поэтому после перезапуска ничего не сохранялось. Работал он только внутри треда.

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

С полученным опытом я пошёл переписывать Ио. LLM вызывалась, если пользователь обращался к боту, но ни одна из обычных команд не срабатывала.

В таком виде бот прожил до 2024 года. После этого я добавил нормальную работу с контекстом. Все сообщения хранились в базе данных. Каждое сообщение ссылается на реплай, таким образом строится дерево диалога. Такой подход позволял, например, получить другой ответ на тот же запрос: достаточно было ответить на нужное сообщение в истории, и бот продолжал диалог именно с этой точки, не затрагивая остальные ветки переписки.

В это же время я перешёл на GPT-4o mini и добавил Tool Calling, чтобы модель сама выбирала и вызывала нужные функции. Соответственно все предыдущие костыли с токенайзером больше были не нужны и были выброшены. Заодно была выпилена функция банов пользователей, так как к тому моменту её никто уже не использовал.

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

Поскольку бота можно было свободно добавлять в любые чаты, обработка изображений сначала работала только в доверенных. Даже с этим ограничением бот съедал на моделях около $10–20 в месяц.

Учим бота запоминать информацию

Первая версия памяти

В 2025 году я начитался про RAG (Retrieval-Augmented Generation, метод работы с большим количеством данных), но был ещё не готов разбираться с векторными базами и прочими сложностями. Поэтому решил пойти самым простым путём. Добавил в базу поле metaInfo с JSON, в котором хранилась собранная информация о пользователе. Раз в 10 сообщений обновлял это поле вот таким запросом:


А дальше в каждый запрос добавлял эту информацию с такими приписками:
isHelpful, isInterests и isUseUsername включались случайно, с разной вероятностью. Как оказалось, LLM плохо понимает инструкции вроде «иногда», «если это уместно» и тому подобные, поэтому пришлось использовать такой костыль
isHelpful, isInterests и isUseUsername включались случайно, с разной вероятностью. Как оказалось, LLM плохо понимает инструкции вроде «иногда», «если это уместно» и тому подобные, поэтому пришлось использовать такой костыль

Память заработала, теперь Ио знала что-то о пользователе.



Иногда это позволяло генерировать забавные диалоги.
Иногда это позволяло генерировать забавные диалоги.


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

Эта же пользовательница очень сильно старалась заставить Ио начать писать что-нибудь неприличное. Это вылилось в то, что Ио начала отвечать неадекватно вообще на все её сообщения.


Пришлось добавить возможность очистить память.

Теперь всё-таки векторы

В таком виде бот просуществовал до 2026 года. К этому времени из-за развития агентов порог входа в подобные эксперименты сильно снизился, и мне наконец захотелось попробовать векторные базы. В моём представлении RAG строился просто: взять все сообщения, положить их в векторную базу, а при новом запросе найти похожие и добавить их в контекст. Всё — модель знает историю пользователя и учитывает её в ответах. Как же сильно я ошибался.

Очень быстро выяснилось, что поиск похожих сообщений почти бесполезен. Большинство найденных сообщений были просто кусками старых диалогов без какой-либо долгосрочной ценности. Модели гораздо полезнее знать устойчивые факты о пользователе, чем перечитывать его переписку. Поэтому я продолжил использовать два подхода: векторный поиск по сообщениям и отдельное извлечение фактов.

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

Такая сложность нужна потому, что один и тот же факт модель каждый раз может формулировать немного по-разному. Заодно появилась возможность актуализировать информацию, если она противоречит старым данным. При новом запросе факты пользователя ранжируются по дате и важности, после чего в контекст попадает топ-10.

Мелкие полезные фичи

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

Guest mode. Возможность призвать бота в любой чат и пообщаться с ним там. Это похоже на inline mode, только диалог не ограничивается одним запросом. Скажем спасибо за это террористу Дурову.

Поиск по Википедии. Позволяет модели хоть иногда не косячить в фактах.

Явная память. Возможность напрямую попросить бота что-нибудь запомнить. Это решило проблему, когда для исправления фактов приходилось руками лезть в базу.

Монетизация

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

Какой технологический стек вы использовали? Почему?

Изначально стек был таким: Node.js, TypeScript, Telegraf.js, OpenAI.

Позже с Telegraf.js я перешёл на grammY. У него удобный API, хорошая типизация, быстрая поддержка новых версий Telegram Bot API и много полезных расширений.

В качестве векторной базы изначально взял Qdrant. Для моих масштабов это оказалось оверкилом: незачем поддерживать отдельную БД, когда всё спокойно помещается в PostgreSQL. Поэтому недавно я перешёл на расширение pgvector.

Для работы с LLM сначала перешёл на LangChain, но он оказался слишком сложным. Поэтому в итоге мигрировал на AI SDK от Vercel.

В качестве провайдера сначала использовал OpenAI, затем перешёл на OpenRouter, а потом на RouterAI из-за проблем с оплатой.

Промпты и трейсы запросов храню в Langfuse. Для моих задач это оказалось не лучшим решением, потому что промпт у меня динамически изменяется, а возможностей шаблонов не хватает. В итоге сейчас у меня статическая часть хранится в Langfuse, а динамическая подставляется одним куском. Трейсинг я почти не использую: пока не возникало задач, для которых хотелось бы регулярно туда смотреть.

Основная модель сейчас — Gemini 3.6 Flash, бюджетная — Gemini 2.5 Flash Lite. Для работы с фактами, суммаризации и распознавания картинок использую Nex-N2-mini.

Векторы изначально генерировал через OpenRouter, но с переходом на RouterAI генерация эмбеддингов стала занимать слишком много времени — от 10 до 60 секунд. Бот векторизует текущее сообщение при каждом запросе, чтобы найти подходящий контекст, что создаёт ощущение, что бот тормозит. Поэтому я поднял text-embeddings-inference с multilingual-e5-small. Качество пока не сравнивал, зато эмбеддинги генерируются быстро даже на небольшом сервере.

Отдельная история — инфраструктура. Сначала всё работало через Docker Compose, но при каждом деплое нужно было руками заходить на сервер, стягивать изменения и пересобирать бота. Это, конечно, можно было автоматизировать, но мне было лень. Поэтому потом бот переехал в Coolify. Там появились автоматическая сборка, развёртывание и бэкапы. Но сборка сильно нагружала сервер и могла вообще положить его на пару минут. Плюс возникали проблемы с обновлением базы данных.

Поэтому решил перелезть на k3s. Теперь GitHub Actions собирает образ бота, после чего он деплоится в k3s. Бэкапы отправляются в S3-хранилище на домашнем NAS. В планах дома поднять ещё один k3s и развернуть там мониторинг, чтобы было красиво. Тем более что с агентами это делать стало очень просто и быстро (хотя есть риск, что очередной агент когда-нибудь снесёт мне и базу, и бэкапы).

Как вы запускались и искали первых пользователей?

Добавил бота в чаты, в которых был админом. В чатах, где я не был админом, призывал его через Guest mode и показывал, что он умеет. Правда, помогало это не всегда. Часть пользователей, увидев, как работает бот, добавили его в другие чаты. Так он постепенно и расползся по нескольким десяткам чатов.

С какими самыми неожиданными трудностями пришлось столкнуться?

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

Сколько потратили и заработали? Есть идеи как это можно монетизировать?

Расходы $10 на сервер и $10–30 на LLM. Сумма сильно зависит от моих косяков, активности пользователей и того, насколько дешёвую модель удаётся использовать. Ну и моё время — бесценно.
По доходам всё грустно. Монетизацию только прикрутил, пока есть всего одна покупка на $4.

На самом деле подписку я делал не столько ради заработка, сколько чтобы перестать ежемесячно оплачивать чужие бесконечные эксперименты с ботом. И да, с таймингами очень угадал, с текущими новостями есть шансы, что монетизацией из РФ будет вообще нельзя пользоваться.

Какие планы на будущее?

Запилить мониторинг. Починить баги. Реализовать веб-поиск, ибо сейчас есть только поиск по Википедии. Доработать инфраструктуру: сейчас бот во время деплоя несколько минут недоступен.

Нужны ли какие-то советы или помощь Клуба?

Предложения новых фичей не помешали бы. Ну и бот в теории опенсорсный, так что если кому интересно, заходите жамкнуть звёздочку на GitHub. Правда, предупреждаю: там лютый говнокод вперемешку с вайбкодом.

Какой совет вы бы сами могли дать идущим по вашим стопам?

Не лениться — это главный ограничивающий фактор) Ну и помнить, что векторные базы — не золотая пуля.

Ио до сих пор остаётся для меня большим полигоном для экспериментов. Многие новые идеи из мира LLM и инфраструктуры я сначала пробую именно на нём. Пока проект продолжает приносить удовольствие — значит, всё было не зря.

Связанные посты
Откомментируйте первым 👇

😎

Автор поста открыл его для большого интернета, но комментирование и движухи доступны только участникам Клуба

Что вообще здесь происходит?


Войти  или  Вступить в Клуб