Ну это полный мэтч! Как мы сделали бота для знакомств в чатах
- суббота, 18 октября 2025 г. в 00:00:07
Всем привет, я Иван, продакт-менеджер. И я остою в айтишном чате — человек двести, может, чуть больше. Там всё как обычно: обсуждаем новости, спорим про фреймворки, кидаем мемы.
Сообщений очень много, и когда новички приходят, пишут интро о себе — через пару минут их уже никто не видит, всё уходит в ленту. В какой-то момент стало интересно: можно ли эту проблему решить алгоритмом?
Так появилась идея бота, который помогает людям знакомиться по интересам, а не случайно. В этой статье я расскажу, как мы с командой его сделали.
Когда мы начали думать, каким должен быть бот, оказалось, что задач не три, а целая куча. Но основные — вот:
Автоматически выявлять участников с общими интересами. Без бота мы вручную читали интро. Разобрать сотню сообщений занимало около часа — и всё равно половину пропускали.
Минимизировать шум. В среднем 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"
}
}
Сначала мы пытались обрабатывать все сообщения, но более 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 для других сообществ. Чтобы админы могли прикрутить движок мэтчинга в свои чаты или корпоративные платформы.
А дальше — перейти от «пары для диалога» к мини-группам по интересам. Мы уже пробовали это в тестах: Марго из банка и Влад — дизайнер, оба писали, что любят фантастику. В обычном чате их интро бы утонуло среди мемов, но бот их соединил — и они до сих пор общаются.
Мы не строили большую теорию знакомств. Просто собрали пайплайн: фильтр → векторизация → поиск по сходству. Пару десятков строчек кода — и вдруг в чате появляются новые друзья.
Если бы вам предложили — доверили бы алгоритму подобрать вам собеседника? Интересно узнать, что вы об этом думаете.