Всем привет! Меня зовут Иван, я руковожу компанией НИИ Крокодил.

Как и многие в 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! ❤️
Доверили бы алгоритму подобрать вам собеседника? Интересно узнать, что вы об этом думаете.