javascript

Ну это полный мэтч! Как мы сделали бота для знакомств в чатах

  • суббота, 18 октября 2025 г. в 00:00:07
https://habr.com/ru/articles/957450/

Всем привет, я Иван, продакт-менеджер. И я остою в айтишном чате — человек двести, может, чуть больше. Там всё как обычно: обсуждаем новости, спорим про фреймворки, кидаем мемы.

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

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

С чего всё началось

Когда мы начали думать, каким должен быть бот, оказалось, что задач не три, а целая куча. Но основные — вот:

  • Автоматически выявлять участников с общими интересами. Без бота мы вручную читали интро. Разобрать сотню сообщений занимало около часа — и всё равно половину пропускали.

  • Минимизировать шум. В среднем 80% сообщений в чате — это стикеры, «+1» или мемы. Их нужно отсеивать ещё до анализа.

  • Обеспечить точность мэтчей. Если бот сводит людей без реального совпадения, доверие к нему падает моментально. Мы мерили успех просто: начинается ли диалог после мэтча.

Как мы это собирали

С самого начала было понятно: если бот будет требовать ручного вмешательства, админы его просто возненавидят. Но и полная автоматизация — не вариант, потому что чатам важно доверие. Так что решили искать баланс между «бот всё делает сам» и «модераторы всё равно всё видят».

В итоге сформировались два главных принципа:

  • Максимальная автоматизация. Чтобы бот сам фильтровал сообщения, проверял интро и считал мэтчи.

  • Панель управления для модераторов. Мы сделали панель, где можно увидеть интро, проверить совпадения и при желании вручную подкорректировать пары.

Чтобы собрать рабочий прототип, мы решили не изобретать инфраструктуру с нуля, а взять готовые сервисы и сосредоточиться на логике мэтчинга.

  • Telegram Bot API — стандартный способ интеграции с чатами.

  • Node.js + Express.js — лёгкий backend, быстро поднимать и легко масштабировать.

  • Supabase (Postgres + Auth) — фактически Postgres «как сервис» с готовым API. Мы выбрали его вместо «голого» Postgres, потому что он сразу даёт авторизацию и API для фронта.

  • Redis — очередь для фоновых задач. RabbitMQ тоже рассматривали, но Redis проще интегрируется и быстрее в наших сценариях.

  • React + Lovable — для админки. Тут хотелось быстро собрать UI и не тратить время на деплой.

  • Railway — хостинг backend. Мы пробовали Heroku, но Railway даёт дешевле и гибче по планам.

Supabase и Lovable позволили убрать из задачи настройку базовой инфраструктуры (БД, авторизация, деплой фронта) и сосредоточиться на логике мэтчинга.

Алгоритм работы

1. Подключение

Бот добавляется в групповой чат. При первом подключении он через Telegram Bot API получает метаданные чата и регистрирует его в нашей системе. Панель администратора доступна только модераторам.

Подпись: Панель администратора. Главная.
Подпись: Панель администратора. Главная.

Когда бот попадает в чат, через Telegram Bot API мы получаем метаданные: id чата, название, тип.

{
  "ok": true,
  "result": {
    "id": -10012345,
    "title": "AI Developers Chat",
    "type": "supergroup"
  }
}

2. Фильтрация сообщений

Сначала мы пытались обрабатывать все сообщения, но более 80% оказались шумом (стикеры, мемы, «+1»). Решение — брать только сообщения длиной больше 10 слов.

function isCandidateMessage(text: string, minWords = 10): boolean {
  if (!text) return false;
  const words = text.trim().split(/\s+/).length;
  return words > minWords;
}

3. Проверка на «интро»

Даже длинное сообщение не всегда является самопрезентацией. Поэтому мы добавили дополнительный шаг: проверку с помощью 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 помогает отсеивать мемы и оффтоп, оставляя только те сообщения, где человек действительно представляется. Если проверка пройдена, сообщение сохраняется в базе как интро и сразу готовится к векторизации.

Подпись: Панель администратора
Подпись: Панель администратора

4. Векторизация

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

Мы остановились на модели 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).

5. Подбор пар

Когда появляется новое интро, мы запускаем вычисление его сходства со всеми сохранёнными в базе. Эта операция тяжёлая, поэтому задачи ставим в очередь и обрабатываем воркерами в фоне.

Результаты пишем в таблицу 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: совпадений много и они достаточно точные.

6. Ручной мэтчинг

В админке можно ввести двух пользователей и получить коэффициент совместимости. Если расчёт уже был, результат достаётся из кеша в БД.

Инженерные заметки из продакшена

Когда мы запустили проект, стало по��ятно: сама идея мэтчинга работает, но без инженерных костылей и доработок всё упиралось в потолок буквально через пару часов. Мы перепробовали кучу вариантов — и вот что в итоге оказалось критичным.

  • Очереди и фоновые воркеры. Интро сыпались десятками в минуту, и каждое нужно было сравнить с тысячами других. Первые тесты просто клали API. В итоге вынесли всё в отдельный контур — Redis и пул воркеров. Очередь сглаживает пики, а воркеры считают батчами. После оптимизаций задержка мэтча стабилизировалась в районе 1–2 секунд.

  • Кеш и идемпотентность. Все коэффициенты сходства сохраняются в таблице matches. Уникальный индекс по паре user_a/user_b исключает дубли и делает систему предсказуемой: одна и та же пара не считается повторно.

  • Масштабирование. Архитектуру разделили на независимые куски — API, фронт и воркеры. Когда чат оживает и нагрузка растёт, просто поднимаем больше воркеров.

  • Надёжность. Для воркеров настроен retry с экспоненциальным backoff: если модель эмбеддингов временно недоступна, задача не теряется и автоматически уходит на повтор.

  • Мониторинг. Следим за глубиной очереди, временем обработки батчей и latency моделей. Это помогает прогнозировать узкие места и вовремя реагировать на деградации.

  • Приватность. В базе храним только интро, прошедшие фильтр. Поток всех сообщений не сохраняем — и нагрузка ниже, и вопросов по приватности нет.

Что пришлось поменять

  • Убрали полное логирование сообщений — храним только интро.

  • Снизили порог cosine similarity с 0.85 до 0.75.

  • Убрали stopwords-фильтр — он ломал интро со смешанным языком

Результаты

  • Мэтчи появляются через несколько секунд после публикации интро.

  • 70% мэтчей по отзывам участников привели к новым диалогам.

  • Админам больше не нужно вручную подбирать, кто с кем «сойдётся».

    Подпись: Панель администратора. Раздел аналитики.
    Подпись: Панель администратора. Раздел аналитики.

Что дальше

MVP уже ожил и показывает, что идея работает. Теперь самое интересное — развивать логику мэтчинга. Мы внутри собрали целый список идей:

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

  • Контекст общения. Пока мы смотрим только на интро, но люди ведь раскрываются в диалогах. Если учитывать стиль общения, мэтчи станут ещё точнее.

  • Фидбэк. Когда мэтч «зашёл», бот должен это понимать и учитывать в будущем.

  • «Событийные» мэтчи. Например, найти собеседника, который тоже едет на ту же конференцию или живёт в твоём городе.

  • API для других сообществ. Чтобы админы могли прикрутить движок мэтчинга в свои чаты или корпоративные платформы.

А дальше — перейти от «пары для диалога» к мини-группам по интересам. Мы уже пробовали это в тестах: Марго из банка и Влад — дизайнер, оба писали, что любят фантастику. В обычном чате их интро бы утонуло среди мемов, но бот их соединил — и они до сих пор общаются.

Мы не строили большую теорию знакомств. Просто собрали пайплайн: фильтр → векторизация → поиск по сходству. Пару десятков строчек кода — и вдруг в чате появляются новые друзья.

Если бы вам предложили — доверили бы алгоритму подобрать вам собеседника? Интересно узнать, что вы об этом думаете.