
Всем привет! Меня зовут Иван, я руковожу компанией НИИ Крокодил.
Как и многие в IT, я состою в куче рабочих чатов, суммарно там, наверное, пару сотен человек. И каждый такой чат живёт одинаково: миллион сообщений, обсуждения сменяются одно за другим, а любая важная информация улетает вверх и теряется.
Особенно заметно, когда в чат приходят новички и пишут интро. Человек рассказывает, кто он и чем занимается, а через пару минут этот текст уже никто не увидит: его просто смывает потоком сообщений.
В какой-то момент мне стало интересно: а можно ли сделать так, чтобы люди находили друг друга не случайно, а по интересам и задачам? Так и появилась идея бота, который помогает знакомиться и «мэтчить» людей внутри сообщества. Спойлер: у нас получилось.
В этой статье расскажу, как мы с командой его сделали.
С чего всё началось
Когда мы начали думать, каким должен быть бот, оказалось, что задач не три, а целая куча. Но основные:
Автоматически выявлять участников с общими интересами. Без бота мы вручную читали интро. Разобрать сотню сообщений занимало около часа — и всё равно половину пропускали.
Минимизировать шум. В среднем 80% сообщений в чате — это стикеры, «+1» или мемы. Их нужно отсеивать ещё до анализа.
Обеспечить точность мэтчей. Если бот сводит людей без реального совпадения, доверие к нему падает моментально. Мы мерили успех просто: начинается ли диалог после мэтча.
Как мы это собирали
С самого начала было понятно: если бот будет требовать ручного вмешательства, админы его просто возненавидят. Но и полная автоматизация, тоже не вариант, потому что чатам важно доверие. Так что решили искать баланс.
В итоге мы сформировали два главных принципа:
Максимальная автоматизация. Чтобы бот сам фильтровал сообщения, проверял интро и считал мэтчи.
Панель управления для модераторов. Мы сделали панель, где можно увидеть интро, проверить совпадения и при желании вручную подкорректировать пары.
Чтобы собрать рабочий прототип, мы решили не изобретать инфраструктуру с нуля, а взять готовые сервисы и сосредоточиться на логике мэтчинга.
Telegram Bot API — стандартный способ интеграции с чатами.
Node.js + Express.js — лёгкий backend, быстро поднимать и легко масштабировать.
Supabase (Postgres + Auth) — фактически Postgres «как сервис» с готовым API. Мы выбрали его вместо «голого» Postgres, потому что он сразу даёт авторизацию и API для фронта.

Когда бот попадает в чат, через Telegram Bot API мы получаем метаданные: id чата, название, тип.
{ "ok": true, "result": { "id": -10012345, "title": "AI Developers Chat", "type": "supergroup" }}
Фильтрация сообщений
Сначала мы пытались обрабатывать все сообщения, но более 80% оказались шумом (стикеры, мемы, «+1»). Решение — брать только сообщения длиной больше 10 слов.
function isCandidateMessage(text: string, minWords = 10): boolean { if (!text) return false; const words = text.trim().split(/\s+/).length; return words > minWords;}
Проверка на «интро»
Даже длинное сообщение не всегда является самопрезентацией. Поэтому мы добавили дополнительный шаг: проверку с помощью gpt-4o-mini. Мы передаём сообщение в модель с промптом:
You are given a user's message. Determine whether the message can be classified as a self‑introduction — that is, a personal statement where the user talks about themselves, such as their name, background, interests, hobbies, goals, or experiences.
GPT помогает отсеивать мемы и оффтоп, оставляя только те сообщения, где человек действительно представляется. Если проверка пройдена, сообщение сохраняется в базе как интро и сразу готовится к векторизации.

Векторизация
Чтобы сравнивать интро между собой, нужно перевести текст в числовое представление. Для этого используем эмбеддинги — векторы фиксированной длины, которые отражают смысл текста.
Мы остановились на модели text-embedding-3-large (размерность 3072). В продакшене она показала стабильные результаты: похожие тексты действительно получали близкие вектора. Это критично — если модель «шумит», мэтчинг разваливается.
В базе мы храним эмбеддинги прямо в Postgres с расширением [pgvector].
При вставке интро мы сразу считаем эмбеддинг и сохраняем его:
export const createEmbedding = async (text) => { const embedding = await openai.embeddings.create({ model: "text-embedding-3-large", input: text, encoding_format: "float", }); return embedding.data[0].embedding;};
Дальше для сравнения используем косинусное расстояние (<=> в pgvector).
Подбор пар
Когда поя��ляется новое интро, мы запускаем вычисление его сходства со всеми сохранёнными в базе. Эта операция тяжёлая, поэтому задачи ставим в очередь и обрабатываем воркерами в фоне.
Результаты пишем в таблицу matches, а при удачном совпадении бот шлёт уведомление в чат — это мотивирует других участников тоже оставить интро.
static async CreateMatch(match) { // check if match already exists const { data: existingMatch, error: existingMatchError } = await this.GetMatch(match.firstIntroUuid, match.secondIntroUuid); if (existingMatch) return { data: existingMatch, error: null }; const similarity = await IntrosService.GetSimilarity(match.firstIntroUuid, match.secondIntroUuid); let { data, error } = await supabase .from('matches') .insert({ first_intro_uuid: match.firstIntroUuid, second_intro_uuid: match.secondIntroUuid, similarity: similarity }) .select() .maybeSingle() return { data, error }; }
Мы экспериментировали с порогами:
Порог Т | Мэтчей/день | Релевантность |
0.85 | 5 | ~90% |
0.75 | 48 | 70-75% |
0.70 | 76 | ~50-55% |
Оптимум нашли на 0.75: совпадений много и они достаточно точные.
Ручной мэтчинг
В админке можно ввести двух пользователей и получить коэффициент совместимости. Если расчёт уже был, результат достаётся из кеша в БД.
Инженерные заметки из продакшена
Мы наломали кучу дров и вот что поняли:
Очереди и фоновые воркеры. Интро сыпались десятками в минуту, и каждое нужно было сравнить с тысячами других. Первые тесты просто клали API. В итоге вынесли всё в отдельный контур — Redis и пул воркеров. Очередь сглаживает пики, а воркеры считают батчами. После оптимизаций задержка мэтча стабилизировалась в районе 1–2 секунд.
Кеш и идемпотентность. Все коэффициенты сходства сохраняются в таблице
matches. Уникальный индекс по пареuser_a/user_bисключает дубли и делает систему предсказуемой: одна и та же пара не считается повторно.Масштабирование. Архитектуру разделили на независимые куски — API, фронт и воркеры. Когда чат оживает и нагрузка растёт, просто поднимаем больше воркеров.
Надёжность. Для воркеров настроен retry с экспоненциальным backoff: если модель эмбеддингов временно недоступна, задача не теряется и автоматически уходит на повтор.
Мониторинг. Следим за глубиной очереди, временем обработки батчей и latency моделей. Это помогает прогнозировать узкие места и вовремя реагировать на деградации.
Приватность. В базе храним только интро, прошедшие фильтр. Поток всех сообщений не сохраняем и нагрузка ниже, и вопросов по приватности нет.
Что пришлось поменять
Убрали полное логирование сообщений, храним только интро.
Снизили порог cosine similarity с 0.85 до 0.75.
Убрали stopwords-фильтр — он ломал интро со смешанным языком
Результаты
Мэтчи появляются через несколько секунд после публикации интро.
70% мэтчей по отзывам участников привели к новым диалогам.
Админам больше не нужно вручную подбирать, кто с кем «сойдётся».


Что дальше
MVP уже ожил и показывает, что идея работает. Мы хотим дальше развивать логику мэтчинга. Уже собрали целый список идей:
Темы мэтчей. Хочется, чтобы бот не просто «сводил», а показывал, почему людям стоит познакомиться. Например по хобби, по карьере или увлечению.
Контекст общения. Пока мы смотрим только на интро, но люди ведь раскрываются в диалогах. Если учитывать стиль общения, мэтчи станут ещё точнее.
Фидбэк. Когда мэтч «зашёл», бот должен это понимать и учитывать в будущем.
«Событийные» мэтчи. Например, найти собеседника, который тоже едет на ту же конференцию или живёт в твоём городе.
API для других сообществ. Чтобы админы могли прикрутить движок мэтчинга в свои чаты или корпоративные платформы.
Хотим перейти от «пары для диалога» к мини-группам по интересам. Мы уже пробовали это в тестах: Марго работает в банке и Влад — дизайнер, оба писали, что любят фантастику. В обычном чате их интро бы утонуло среди мемов, но бот их соединил и они до сих пор общаются.
Мы не строили большую теорию знакомств. Просто собрали пайплайн: фильтр → GPT → эмбеддинги → поиск по сходству. Несколько десятков строчек кода — и It’s a match! ❤️
Доверили бы алгоритму подобрать вам собеседника? Интересно узнать, что вы об этом думаете.
