АрхитектураИнженерный журнал клуба
Как мы превратили сайт и Telegram-бот клуба в единую систему людей, мэтчинга и проверенных дайджестов
Архитектурный кейс Клуба Незаменимых: зачем системе три независимых сервиса, как AI принимает решения под контролем кода и что всё это даёт участникам.
Содержание · 11 глав
Закрытый клуб быстро сталкивается с парадоксом: чем больше в нём сильных людей, обсуждений и полезных источников, тем сложнее находить нужное. Ценность уже находится внутри сообщества, но распределена по анкетам, чатам, каналам и внешним публикациям. Если оставить всё как есть, участник должен сам помнить, кто чем занимается, следить за десятками источников и каждый раз вручную отделять важное от шума.
Поэтому мы строили не просто сайт, Telegram-бота и парсер новостей. Мы строили одну цифровую систему клуба, которая решает три связанные задачи:
- 01понимает, кто находится внутри сообщества и чем человек может быть полезен;
- 02помогает участникам находить друг друга по реальному запросу;
- 03собирает внешний информационный поток и превращает его в проверяемый дайджест.
Для участника это выглядит просто: войти через Telegram, заполнить профиль, написать запрос в клубном чате и получить несколько релевантных знакомств; затем открыть дайджест на сайте или в Telegram. Но за этой простотой находится система с разделёнными зонами ответственности, контрактами данных, многоступенчатой фильтрацией, проверкой доказательств и защитой от повторной публикации.
Главная инженерная идеяAI может предлагать решения, но право окончательно принимать их остаётся у проверяемого кода.
Мы начинали не с микросервисов, а с трёх пользовательских проблем
Слово «архитектура» часто вызывает образ схемы из прямоугольников и стрелок. Но хорошая архитектура начинается раньше — с ответа на вопрос, зачем вообще существует система.
В нашем случае было три сценария.
Первый — безопасный вход в клубную среду
Система должна узнать участника через Telegram, проверить его доступ, хранить профиль и учитывать согласие на использование данных для мэтчинга.
Второй — поиск людей по смыслу, а не по должности
Запрос «ищу человека, который запускал B2B-продукт и умеет выстраивать первые продажи» нельзя качественно обработать обычным фильтром по полю «профессия». Нужно понять смысл запроса и сопоставить его с опытом участников.
Третий — информационная навигация
Несколько десятков источников ежедневно публикуют сотни сообщений. Клубу нужен не ещё один поток ссылок, а короткий ответ на три вопроса: что произошло, почему это важно и на каких источниках основан вывод.
Эти сценарии связаны общей ценностью — экономией внимания. Сайт управляет идентичностью и профилем, бот работает в естественной среде общения, а дайджест-сервис превращает внешний шум в сигнал.
Три приложения, но одна система
На верхнем уровне архитектура состоит из трёх независимо разворачиваемых сервисов.
AI предлагает решение. Код проверяет право на публикацию.
| Сервис | Что видит участник | Инженерная ответственность |
|---|---|---|
| Клубный сайт | Вход, профиль, кабинет, архив дайджестов | Авторизация, доступ, анкета, согласия, основная клубная база |
| Telegram-бот | Мэтчи в ответ на #запрос, выпуск дайджеста в чате | Синхронизация доступных профилей, семантический поиск, безопасная публикация |
| Topic Digest | Готовый проверенный выпуск | Сбор источников, дедупликация, кластеризация событий, проверка и ранжирование |
Почему не собрать всё в одном приложении? Потому что у частей разный ритм и разные причины отказа. Сайт обслуживает интерактивные действия пользователя. Бот живёт в длинном Telegram-процессе и должен переживать перезапуски без повторных сообщений. Парсер работает по расписанию, ходит во внешние источники и выполняет тяжёлый редакционный конвейер.
Разделение позволяет обновлять, масштабировать и восстанавливать эти части независимо. При этом мы не строили инфраструктуру ради инфраструктуры. Сервисы используют один управляемый кластер PostgreSQL, но каждый владеет своей областью данных. Обмен идёт через узкие read-only представления — по сути, подготовленные окна в базу, через которые соседний сервис видит только разрешённые поля.
Это прагматичная середина между монолитом и сложной распределённой платформой с брокерами сообщений и десятками сетевых API. Мы получили чёткие границы без лишней операционной нагрузки.
Профиль участника — не анкета, а часть системы доступа
Вход на сайт построен через современный Telegram OIDC-поток. Если перевести термин на обычный язык, Telegram подтверждает личность пользователя, а сайт получает проверяемый цифровой ответ, не запрашивая пароль.
Сам процесс защищён несколькими механизмами:
- одноразовыми параметрами
stateиnonce, которые не дают подменить или повторить попытку входа; - PKCE — дополнительным доказательством, что ответ Telegram получает именно тот браузер, который начал авторизацию;
- проверкой подписи токена, его издателя, получателя и срока действия;
- короткоживущими защищёнными cookies и отдельной подписанной сессией сайта.
После подтверждения личности система проверяет право доступа: активна ли подписка или относится ли пользователь к административной группе. В спорной ситуации действует принцип fail closed: если внешний сервис проверки временно недоступен, система не открывает закрытые данные «на всякий случай».
Но авторизоваться недостаточно. Для полезного мэтчинга нужен качественный профиль: роль, опыт, навыки, интересы и Telegram username, через который с человеком можно связаться. Кроме того, человек должен явно разрешить использовать эти сведения для рекомендаций.
Из основной базы формируется специальное представление member_matching_source. В него попадают только участники, которые:
- завершили онбординг;
- заполнили обязательные поля и хотя бы один навык;
- имеют право доступа к клубу;
- оставили актуальное согласие на мэтчинг.
Бот не читает все пользовательские таблицы и не пытается самостоятельно интерпретировать правила клуба. Он получает уже разрешённую проекцию данных. Если участник отзывает согласие, его профиль исчезает из этого представления и затем — из поискового индекса бота.
Как бот превращает #запрос в несколько полезных знакомств
Пользователь пишет сообщение с точным хештегом #запрос в нужном топике клуба. Бот проверяет не просто наличие похожего текста, а Telegram-entity самого хештега. Это защищает систему от случайных срабатываний в цитатах или обычном разговоре.
Дальше запрос проходит несколько ступеней.
Актуальная локальная копия разрешённых профилей
Примерно раз в пять минут бот синхронизирует подготовленное представление участников. Снимок обновляется транзакционно: система либо видит согласованную новую версию целиком, либо продолжает работать со старой. Для каждого профиля вычисляется хеш содержимого, поэтому повторно индексируются только изменившиеся карточки.
Профиль превращается в вектор
Текст профиля кодируется в embedding — набор чисел, описывающий его смысл. Запрос пользователя кодируется таким же способом. После этого PostgreSQL с расширением pgvector может вычислить, какие профили находятся ближе всего к запросу по смыслу.
Для клубного масштаба мы используем точный cosine search и выбираем около двадцати ближайших кандидатов. При количестве участников до нескольких тысяч это понятнее и надёжнее сложного приближённого индекса: выигрыш в скорости пока не оправдывает дополнительную неопределённость.
AI выбирает не человека, а доказательство
Векторный поиск хорошо сокращает пространство кандидатов, но он не умеет объяснить, почему конкретный человек подходит. Поэтому профили заранее разбиваются на короткие фрагменты-доказательства: например, строку про запуск B2B-продукта или опыт построения команды.
Языковая модель получает запрос, кандидатов и идентификаторы этих фрагментов. В ответ она может вернуть только пару memberId + evidenceId. Она не имеет права сочинять биографию или свободно писать рекомендацию.
Затем обычный код проверяет результат:
- существует ли участник в исходной выборке;
- действительно ли указанный фрагмент принадлежит его профилю;
- не был ли автор запроса предложен самому себе;
- совпадают ли версия embedding-модели, размерность и хеш профиля;
- достаточно ли валидных рекомендаций.
В итоговое сообщение попадает исходная подстрока из анкеты, а не пересказ модели. Обычно бот публикует от трёх до пяти мэтчей. Если AI нарушил формат, система один раз запрашивает исправление; если валидных кандидатов недостаточно, она не заполняет ответ выдуманными людьми.

- Бот срабатывает на Telegram-entity хештега, а не на похожую подстроку в тексте.
- Username участника — единственный контакт в ответе. Здесь он закрыт: согласие на мэтчинг не равно согласию на публикацию.
- Фрагмент-доказательство. Эту строку подставил код дословно из анкеты — модель вернула только идентификатор фрагмента.
memberId + evidenceId, а текст после тире взят из профилей как есть. Сверьте с проверками выше — каждая из них выполняется до того, как сообщение уйдёт в чат.Почему одного векторного поиска недостаточно
Векторный поиск иногда воспринимают как готовое решение: загрузили тексты, нашли ближайшие и показали пользователю. На практике близость по смыслу — только первый сигнал.
Представим два профиля. В одном человек прямо пишет: «запустил B2B SaaS и вывел первые продажи». В другом часто встречаются слова «продукт», «продажи» и «B2B», но это список интересов без практического опыта. Для embedding оба текста могут оказаться близкими к запросу. Для полезного знакомства они не равноценны.
Поэтому система сочетает три разных типа логики:
- RULES
Строгие правила отвечают за доступ, согласие, актуальность и отсутствие дублей.
- VECTORS
Векторная математика быстро находит смыслово близких кандидатов.
- LANGUAGE
Языковая модель сравнивает запрос с конкретными фрагментами опыта.
Сильная сторона здесь не в одной «умной» модели, а в композиции. Каждый инструмент делает то, что умеет лучше всего, а окончательный результат можно проверить.
Дайджест начинается не с генерации текста, а со сбора фактов
Отдельный сервис Topic Digest работает как редакционный микросервис. Один экземпляр отвечает за одну тему и по расписанию собирает материалы за 24 часа.
В текущей конфигурации он следит за 23 Telegram-каналами и 13 веб-источниками. Источники разделены на discovery — места, где мы замечаем новый сигнал, — и primary, то есть первичные документы и официальные публикации, которыми можно подтвердить событие.
Telegram-каналы читаются последовательно и с паузами, чтобы не упираться в лимиты платформы. Веб-источники можно обрабатывать параллельно. Любой вход затем превращается в единый SourceDocument: сервису дальше неважно, пришёл текст из Telegram, RSS или сайта.
До первого обращения к AI выполняется дешёвая и детерминированная очистка:
- нормализация формата;
- удаление повторов по URL;
- удаление точных копий по SHA-256 хешу нормализованного текста;
- проверка временного окна и обязательных полей.
Это важная экономическая и качественная деталь: дорогая модель не должна тратить контекст на дубли и технический мусор.
Как система решает, что действительно важно
Редакционная логика разбита на два AI-прохода, а между ними и после них работают строгие алгоритмы. Общая форма пути — на схеме ниже, а дальше в главе каждый шаг разобран отдельно.
Первый проход: первичный отбор
Документы отправляются в модель пачками. Для каждого материала она оценивает релевантность теме, силу сигнала и тип публикации. Рекламные сообщения и обычные пересказы отбрасываются. В дальнейшую обработку проходят материалы с релевантностью не ниже установленного порога.
Если модель вернула сломанный ответ для целой пачки, система не выбрасывает день целиком. Она один раз повторяет запрос, а затем рекурсивно делит пачку пополам, локализуя проблемный документ. Это похоже на поиск неисправной лампы в гирлянде: вместо замены всей цепи мы сужаем участок сбоя.
Кластеризация: несколько публикаций превращаются в одно событие
Одна новость может появиться в десяти каналах. Если считать каждую ссылку отдельным событием, репосты искусственно поднимут её важность и заполнят весь выпуск.
Поэтому система строит идентичность события из трёх частей:
Тексты нормализуются, а близость subject и change сравнивается по пересечению смысловых токенов. Для объединения нужен тот же actor и достаточная средняя похожесть. Внутри кластера материалы ранжируются по релевантности, силе сигнала, наличию первичного источника и количеству независимых издателей.
Так десять пересказов не становятся десятью новостями. Они становятся одним событием с набором подтверждений.
Безопасное обогащение
Если материал содержит ссылку на первоисточник, сервис может загрузить её и добавить факты. Но он не ищет произвольные подтверждения в открытом интернете: разрешены только ссылки, уже присутствующие во входных документах.
Каждый адрес проходит защиту от SSRF — класса атак, при котором внешняя ссылка пытается заставить сервер обратиться во внутреннюю сеть. Проверяются протокол, DNS, редиректы, частные и loopback-адреса, размер ответа и время загрузки. То есть «прочитать ссылку» для сервера — это отдельная контролируемая операция, а не обычный fetch.
Второй проход: синтез события
На втором проходе модель получает уже не сырой поток, а кластер материалов. Она формирует кандидата события: заголовок, краткое содержание, объяснение важности, затронутые группы, сущности, теги, цитату и ссылки на доказательства.
После этого код снова проверяет результат:
- каждая ссылка должна происходить из разрешённого набора;
- цитата должна дословно встречаться в исходном документе;
- теги и сущности должны быть заявлены корректно;
- размеры полей и числовые оценки должны находиться в допустимых границах;
- события не должны дублировать друг друга между кластерами.
Модель может разделить один кластер на несколько событий или признать, что подтверждённого события нет. Но она не может незаметно добавить новый источник или выдумать цитату.

- Уровень доказательности и тип материала. Их посчитал код после второго прохода, а не проставила модель.
- «Почему важно» — обязательное поле контракта синтеза, а не свободный текст: без него кандидат не проходит проверку.
- Цитата. Если она не встречается в исходном документе дословно, событие не попадает в выпуск.
- Источник цитаты берётся только из ссылок, которые уже были во входных документах кластера.
Уровень доказательности
Отдельный алгоритм оценивает не популярность новости, а силу подтверждения:
- первичный источник даёт максимальный уровень;
- два и более независимых источника дают высокий уровень;
- единственный вторичный источник даёт средний уровень;
- чистая аналитика без фактического подтверждения остаётся на минимальном.


Независимость определяется не только доменом. Учитываются источник, URL, точное содержимое и издатель; при этом разные Telegram-каналы считаются отдельными издателями. Слух не превращается в факт только из-за высокого общего балла.
Ранжирование
Каждое проверенное событие получает оценку от 0 до 100. Она складывается из шести измерений:
score = ( impact × 30
+ novelty × 20
+ scope × 15
+ evidence × 15
+ actionability × 10
+ priorityFit × 10 ) / 4Вес влияния самый большой: дайджест должен отвечать не на вопрос «что чаще публиковали?», а на вопрос «что действительно меняет картину для участника?».
После сортировки события распределяются по трём редакционным слоям:
Main
Главные факты и исследования с сильными подтверждениями и итоговым баллом от 75.
Radar
Достойные внимания сигналы с баллом от 50.
Focus
Запасной компактный блок из лучших проверенных событий, если Main и Radar оказались пустыми.
Если ни одного проверенного события нет, сервис не выпускает «пустую умную статью». Отсутствие выпуска лучше, чем уверенный текст без доказательств.
Контракты данных связывают сервисы лучше, чем знание чужих таблиц
Главный результат дайджест-сервиса — не Telegram-сообщение и не HTML. Это неизменяемый объект PublishedDigest v3.
{
topic: "club-ai",
sections: ["main", "radar", "focus"],
sources: "resolved",
evidence: "validated",
publish: "once"
}Версия в названии означает контракт: обе стороны заранее договорились, какие поля обязательны, как устроены секции и что является источником. Перед публикацией объект становится самодостаточным: в нём уже разрешены названия источников и подготовлены публичные ссылки. Внутренние промпты, сырые документы, диагностические предупреждения и технические оценки наружу не выходят.
Готовый выпуск записывается в схему digest один раз. Комбинация темы и даты уникальна: первый успешный выпуск дня побеждает, повторный запуск возвращает его идентификатор, а не создаёт дубль.
Сайт и бот читают разные представления одной канонической записи. Каждый потребитель повторно валидирует контракт v3 на своей стороне. Благодаря этому:
- парсер считает работу завершённой после надёжной записи в базу;
- сайт может показать выпуск независимо от состояния Telegram;
- бот может повторять доставку, не запуская заново AI-конвейер;
- изменение внутренней схемы парсера не ломает потребителей.

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

- Недоступен один источник
- Дайджест продолжает работу с оставшимися материалами и помечает запуск как partial. Технические подробности сбоя остаются во внутренней диагностике, но сам факт неполноты виден участнику: в шапке выпуска указано, сколько источников из скольких удалось прочитать.
- Нет подтверждённых событий
- Выпуск не создаётся. Генерация текста не считается самоцелью.
- Модель вернула неверную структуру
- Результат отклоняется, запрос ограниченно повторяется или проблемная пачка дробится. Строка, не прошедшая схему, не попадает дальше «по доверию».
- Перезапустился бот
- Публикации хранятся в durable outbox — таблице заданий на отправку. Рабочий процесс берёт запись во временную аренду, отправляет сообщение и сохраняет Telegram message id. При ошибке используются увеличивающиеся паузы: от нескольких секунд до получаса. Перезапуск не теряет задачу и не запускает генерацию заново.
- Один и тот же Telegram update пришёл повторно
- Уникальные ключи и идемпотентные операции не дают создать второй ответ. Идемпотентность означает простое свойство: повтор безопасной команды приводит к тому же результату, а не умножает побочные эффекты.
- База временно занята
- Повторяются только транзиентные ошибки PostgreSQL — например, конфликт транзакций или временная блокировка. Логические ошибки не маскируются бесконечными retry.
- Не работает проверка доступа
- Закрытые данные не открываются автоматически. Для систем, содержащих профили людей, безопасный отказ важнее показателя «ответили любой ценой».
Инфраструктура: достаточно сложная, чтобы быть надёжной, и достаточно простая, чтобы ею управлять
Все три сервиса упакованы в Docker-контейнеры и разворачиваются независимо. В качестве общей инфраструктурной основы используются App Platform и Managed PostgreSQL в приватной сети.
В базе разделены роли:
- миграционная роль может менять структуру схемы;
- runtime-роли приложений получают только права, необходимые в обычной работе;
- межсервисные представления доступны на чтение;
- произвольные обращения к чужим таблицам запрещены.
Это уменьшает радиус повреждения. Ошибка в боте не должна дать ему право менять анкеты пользователей, а парсеру не нужна возможность читать клубные профили.
Трафик к PostgreSQL, AI-провайдеру и обычным веб-источникам идёт напрямую по предназначенным маршрутам. Отдельный userspace-прокси применяется только там, где он действительно нужен для Telegram: OIDC-запросов сайта, Bot API и клиентского доступа парсера. Если прокси настроен, но не работает, соответствующий Telegram-контур закрывается, а не незаметно переходит на неожиданный маршрут.
У сервисов есть health-проверки, структурированные логи и ограничение чувствительных данных в сообщениях. Для такой платформы наблюдаемость — это не огромный экран с графиками, а способность быстро ответить: какой источник упал, на каком этапе остановился выпуск, была ли публикация повторена и какую версию контракта прочитал потребитель.
Отдельный слой — тесты. Мы проверяем не только функции по одной, но и границы системы:
правила отбора, скоринг и нормализация
PostgreSQL и pgvector
PublishedDigest v3 и профили мэтчинга
перезапуск, повторная доставка и отзыв согласия
«золотой» редакционный набор, на котором измеряются точность отбора, полнота и отсутствие дублей
Это особенно важно для AI-части: модель вероятностна, поэтому качество нельзя доказывать одним красивым примером. Нужны повторяемые критерии и набор случаев, на которых регрессия становится видимой.
Что в итоге получает участник — и почему это больше, чем набор фич
С технической стороны здесь много деталей: OIDC и PKCE, pgvector, версионированные контракты, immutable-записи, SSRF-защита, outbox, кластеризация и взвешенный скоринг. Но участнику не нужно знать эти слова, чтобы почувствовать результат.
Он получает три вещи.
Контекст о людях
Не безликий список участников, а возможность сформулировать задачу человеческим языком и получить несколько объяснимых знакомств.
Контекст о происходящем
Не бесконечную ленту ссылок, а события, собранные из повторяющихся публикаций, проверенные по источникам и отсортированные по реальной важности.
Доверие к механике
Его данные участвуют в мэтчинге только с согласия; рекомендацию можно связать с конкретной строкой профиля; новость — с конкретной цитатой и источником; повторный запуск не создаёт повторный выпуск.
Именно это мы считаем главной ценностью клубной технологии. Она не пытается заменить общение между людьми и не превращает сообщество в алгоритмическую ленту. Наоборот, система убирает механическую работу вокруг общения: помогает быстрее найти человека, заметить важное событие и понять, почему результат оказался перед тобой.
Мы не просто «прикрутили AI» к сайту и Telegram. Мы встроили вероятностные модели в инженерный контур, где доступ задаётся политиками, данные передаются через контракты, выводы сопровождаются доказательствами, а сбои имеют заранее определённое поведение.
Поэтому снаружи всё выглядит просто. Участник задаёт вопрос — клуб помогает найти нужного человека. Открывает дайджест — видит несколько действительно важных событий. А внутри работает система, которая делает эту простоту надёжной.