Привет, Хабр!
Меня зовут Артём, я фаундер Leadl.ai. Мы строим AI-агента для поиска b2b-клиентов, и одна из его ключевых задач это мониторинг чатов и различных источников.
Звучит просто, пока не сталкиваешься с масштабом.

У нас в пуле 20000+ чатов в 15 источниках. Суммарно около 1000 000 000 сообщений в сутки. Из них реально полезных (запросы на услуги, поиск подрядчиков, вакансии) от силы 3-5%. Остальное: флуд, криптоспам, «доброе утро», мемы и бесконечные стикеры.

Задача: вытащить эти 3-5% качественных сообщений. Первой мыслью было отдать всё на откуп большой LLM типа GPT-4o. Посчитали. Среднее сообщение 50 токенов. 100 000 сообщений 50 токенов/сообщение ($10 / 1M токенов) = $50 в день только на input. Добавьте сюда output и prompt — и счёт легко перевалит за $100-150/день или $3000-4500/месяц. Для стартапа это путь в никуда.

Нам нужен был pipeline, который бы отсеивал мусор на ранних этапах, чтобы до дорогого LLM-скоринга доходило не более 5-10% от всего потока. Вот как мы его построили, через какие грабли прошли и что из этого вышло.

Общая архитектура пайплайна

Идея простая: каскадная фильтрация. Каждый следующий этап сложнее и дороже предыдущего, но он работает с уже очищенными данными. Это как в компьютерных играх: сначала пачка слабых мобов, потом мини-босс, и только в конце — финальный рейд-босс (наша LLM).
Вот как выглядит наш пайплайн:
graph TD

A[Raw Message Stream<br>~100,000/day] --> B{Stage 1: Heuristic Filter};

B --> |~70% filtered| C{Stage 2: Lightweight ML Classifier};

C --> |~60% filtered| D{Stage 3: Semantic Search Check};

D --> |~50% filtered| E{Stage 4: LLM Scorer};

E --> |~20% filtered| F{Stage 5: Deduplication};

F --> G[Clean Leads<br>~3,000/day];

B --> H1[Trash Bin 1];

C --> H2[Trash Bin 2];

D --> H3[Trash Bin 3];

E --> H4[Trash Bin 4];

В итоге до LLM доходит всего около 5-8% от первоначального потока. Давайте разберём каждый этап.

Этап 1: Грубый эвристический фильтр

Задача: отсеять самый очевидный мусор с минимальными затратами CPU-времени.

Это первый и самый важный рубеж обороны. Он работает на простых правилах и регулярных выражениях. Дёшево, сердито, невероятно эффективно. Здесь мы отсеиваем около 70% всего потока.

Что уходит в утиль:

  1. Слишком короткие сообщения: всё, что короче условных 30 символов. Прощайте, «+», «ап», «актуально», «добрый день». Конечно, можно случайно отсеять «ищу js-dev», но на практике такие короткие запросы почти не встречаются.

  2. Сообщения от ботов: банальная проверка if message.sender.is_bot.

  3. Медиа и стикеры: if message.media or message.sticker.

  4. Стоп-слова: огромный список слов-триггеров, которые почти никогда не встречаются в целевых сообщениях. Это и «спасибо», «пожалуйста», «созвон», «митинг», и политические термины, и крипто-лексика («airdrop», «листинг», «NFT»).

  5. Спам-ссылки: регулярка на популярные спам-домены и Telegram-каналы.

Вот примитивный пример на Python, иллюстрирующий логику:
STOP_WORDS = {'спасибо', 'пожалуйста', 'добрый', 'утро', 'вечер', ...}

MIN_LENGTH = 30

def heuristic_filter(message_text: str) -> bool:

"""Returns True if the message is likely junk."""

if len(message_text) < MIN_LENGTH:

return True

Простая токенизация и проверка на стоп-слова

words = set(message_text.lower().split())

if words.intersection(STOP_WORDS):

return True

Тут могут быть регулярки для спам-ссылок, и т.д....

return False

Пример

text1 = "добрый день, коллеги! как успехи?"

text2 = "Ищу подрядчика для разработки MVP на Python (Django/FastAPI). Бюджет 150к, сроки до конца месяца."

print(f'Text 1 is junk: {heuristic_filter(text1)}') # -> True

print(f'Text 2 is junk: {heuristic_filter(text2)}') # -> False

Стоимость: почти нулевая.
Результат: отсеиваем ~70% сообщений, которые гарантированно не являются лидами.

Этап 2: Лёгкий ML-классификатор

Задача: из оставшихся ~30 000 сообщений найти те, что похожи на лиды.

После эвристик у нас всё ещё много мусора: осмысленные, но нерелевантные обсуждения, вопросы не по теме и т.д. Здесь в игру вступает ML.

Сначала мы, как и многие, решили: «Возьмём rubert-tiny2 и дообучим на классификацию!». Собрали датасет на 10 000 сообщений (50/50 лиды/не лиды), дообучили, запустили. Получили recall 0.91, но precision 0.54. Это означало, что мы ловили почти все лиды, но в итоговой выборке было 46% мусора. Пользователи бы нас прокляли.

К тому же, даже rubert-tiny2 на CPU давал ощутимую задержку. Нам нужно было что-то быстрее.

Вернулись к старому доброму TF-IDF + LogisticRegression из scikit-learn. Он обучается за секунды, инференс — миллисекунды. И что самое смешное, после настройки гиперпараметров и чистки словаря он дал precision: 0.82 и recall: 0.78 на нашей тестовой выборке. Да, мы теряем ~22% потенциальных лидов, но зато 82% того, что модель назвала лидом, — действительно лид. Это приемлемый компромисс для второго этапа.

Псевдокод пайплайна обучения

from sklearn.feature_extraction.text import TfidfVectorizer

from sklearn.linear_model import LogisticRegression

from sklearn.pipeline import Pipeline

X_train, y_train - размеченные данные

pipeline = Pipeline([

('tfidf', TfidfVectorizer(ngram_range=(1, 2), max_features=15000)),

('clf', LogisticRegression(C=5, solver='liblinear'))

])

pipeline.fit(X_train, y_train)

Теперь pipeline.predict(new_messages) работает очень быстро

и возвращает 0 (мусор) или 1 (похоже на лид)

Стоимость: очень низкая, инференс на CPU занимает единицы миллисекунд на сообщение.
Результат: отсеиваем ещё ~60% от оставшегося потока. После этого этапа у нас остаётся примерно 12 000 сообщений в день.

Этап 3: Семантическая проверка через Vector DB

Задача: проверить гипотезу классификатора. Насколько «лид» похож на уже известные нам хорошие лиды?

Этот этап — наша «вторая линия обороны разума». Классификатор может ошибаться. Например, сообщение «обсуждаем поиск подрядчиков» он может счесть за лид, хотя это мета-обсуждение.

Здесь мы используем семантический поиск. У нас есть база из нескольких тысяч эталонных, вручную проверенных лидов. Каждое сообщение из этой базы превращено в вектор (эмбеддинг) с помощью Sentence-BERT и хранится в вектороной СУБД (мы используем Qdrant).

Когда сообщение проходит ML-классификатор, мы не отправляем его сразу в LLM. Мы делаем следующее:

  1. Превращаем текст сообщения в вектор с помощью той же S-BERT модели.

  2. Ищем в Qdrant 3 самых близких по косинусному расстоянию вектора из нашей эталонной базы.

  3. Смотрим на расстояние до ближайшего соседа. Если оно слишком большое (например, distance > 0.4), то, скорее всего, наше сообщение-кандидат не очень-то и похоже на настоящие лиды, даже если классификатор так решил. Мы либо отбрасываем его, либо сильно понижаем его приоритет.

Это помогает отсеять семантические аномалии и делает систему более устойчивой.

-- Псевдо-запрос к Vector DB

SELECT

lead_text,

cosine_similarity(embedding, <vector_of_new_message>)

FROM

golden_leads_collection

ORDER BY

cosine_similarity DESC

LIMIT 3;


Ты — AI-асессор. Твоя задача — проанализировать сообщение из Telegram-чата и извлечь информацию о запросе на услуги. Ответь в формате JSON.

Сообщение:

"""

{message_text}

"""

Проанализируй сообщение и верни JSON со следующими полями:

  • is_lead (boolean): true, если это явный поиск исполнителя/подрядчика/сотрудника.

  • lead_type (string): одно из ["project", "vacancy", "task", "other"].

  • score (integer, 0-100): насколько это качественный и понятный лид. 100 — есть бюджет, сроки, чёткое ТЗ. 0 — не лид.

  • summary (string): краткое описание запроса на 1-2 предложения.

  • stack (array of strings): список технологий и инструментов.

  • budget (string): бюджет, если указан.

Если это не лид (is_lead: false), все остальные поля, кроме score=0, должны быть null.

Такой промпт с JSON mode работает отлично. Мы получаем структурированные данные, которые дальше легко обрабатывать и отправлять пользователям.

Стоимость: высокая (относительно предыдущих шагов).
Результат: отсеиваются последние нерелевантные сообщения, а все целевые — обогащаются данными. На выходе остаётся ~3000-4000 чистых, структурированных лидов в сутки.

Этап 5: Дедупликация

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

  • Берём summary, сгенерированный LLM.

  • Считаем хэш от этого summary.

  • Храним хэши за последние N часов (например, 24).

  • Если хэш нового лида уже есть в базе — пропускаем его.

Для обработки небольших изменений в тексте («ищу дизайнера» vs «ищем дизайнера в команду») можно использовать нечёткое сравнение строк (например, расстояние Левенштейна), но на практике простого хэширования summary от LLM хватает в 90% случаев.

Что в итоге?

Этап

Вход, сообщ./день

Выход, сообщ./день

% отсева

Примерная стоимость/1000 сообщ.

1. Эвристики

100,000

30,000

70%

~$0.001

2. ML-классификатор

30,000

12,000

60%

~$0.05

3. Семантический поиск

12,000

6,000

50%

~$0.20

4. LLM-скоринг

6,000

~3,500

~40%

~$8.00

Итоговая экономия: вместо того чтобы обрабатывать 100 000 сообщений с помощью LLM (стоимость ~ 800), мы обрабатываем только 6 000. Общая стоимость процессинга 100k сообщений получается в районе $0.001*100 + $0.05*30 + $0.20*12 + $8*6 = $0.1 + $1.5 + $2.4 + $48 ≈ **52**.

Разница — более чем в 10 раз. Это позволяет нам держать цену на продукт адекватной.

Что бы мы сделали иначе?

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

  2. Больше внимания разметке. Качество данных для классификатора важнее архитектуры модели. Наш первый провал с precision был именно из-за криво размеченного датасета.
    3. Семантический поиск — не панацея. Мы думали, что он сможет заменить классификатор, но нет. Он отлично работает как дополнительный слой проверки, но в качестве основного фильтра он медленнее и дороже простого ML.

    Надеюсь, наш опыт был полезен. Если вы решаете похожие задачи фильтрации UGC-контента — расскажите в комментариях, как устроен ваш пайплайн. Особенно интересно, кто и как борется с неявными запросами и какие подходы к дедупликации использует.

Комментарии (12)


  1. Triton5
    16.03.2026 13:55

    "Задача: вытащить эти 3-5% качественных сообщений. Первой мыслью было отдать всё на откуп большой LLM типа GPT-4o. Посчитали. Среднее сообщение 50 токенов. 100 000 сообщений 50 токенов/сообщение ($10 / 1M токенов) = $50 в день только на input. Добавьте сюда output и prompt — и счёт легко перевалит за $100-150/день или $3000-4500/месяц. Для стартапа это путь в никуда. "

    Это неверные расчёты.

    OpenAI: GPT-4o
     $2.50/M input tokens
     $10/M output tokens
    То есть по input цена меньше в 4 раза, не $50 а $12,5. То есть получается не $100-150/день а $70-100/день и $2100-3000/месяц.

    Господа, мне почему-то кажется, что для ваших целей с головой хватит GPT-4o-mini - модель от OpenAI с гарантированной поддержкой Structured Output:

    OpenAI: GPT-4o-mini
    $0.15/M input tokens
    $0.60/M output tokens
    То есть по input и output дешевле в 16,6 раз, что приводит к $4-10/день в день и соответственно к $126-$180 в месяц, что уже как бы ОК:)

    Далее, для довольно-таки простой задачи очистки сырых данных может быть достаточно и более дешёвых моделей, но конечно же тоже с гарантированной поддержкой Structured Output:

    OpenAI: gpt-oss-120b
    $0.039/M input tokens
    $0.19/M output tokens
    это в 64 раза дешевле по input и в 52 раза по output , что приводит к $35-$55 в месяц


    OpenAI: gpt-oss-20b
    $0.03/M input tokens
    $0.14/M output tokens
    это в 83 раза дешевле по input и в 71 раза по output , что приводит к $25-$40 в месяц

    Я не утверждаю, что деньги валяются на дороге и их нужно бросать в монитор, но реальные цены несколько корректируют картину мира:)


    1. Triton5
      16.03.2026 13:55

      Ну и за что минусите, хочу узнать?


      1. Artem_leadl Автор
        16.03.2026 13:55

        Не понял


      1. Artem_leadl Автор
        16.03.2026 13:55

        мы в месяц сжираем порядка 7млрд токенов


    1. sunnybear
      16.03.2026 13:55

      Есть бесплатные китайские. Непонятно, можно ли на конвейере использовать, но стоимость обработки сейчас уже сильно ниже


      1. Artem_leadl Автор
        16.03.2026 13:55

        Мы пробовали deepseek он нагрузку не вывозит, но скорит хорошо.
        А ты про какие слышал?
        Просто мы токены пожираем огромным количеством


        1. sunnybear
          16.03.2026 13:55

          Qwen flash / glm flash хорошо классифицируют


          1. Artem_leadl Автор
            16.03.2026 13:55

            кайф, тестанем, спасибо


    1. Artem_leadl Автор
      16.03.2026 13:55

      Мы на GPT сидели с самого начала, чуть не разорились, сейчас ушли на Grok 4 fast


      1. Triton5
        16.03.2026 13:55

        При объёмах 7 млрд токенов/мес стоит запросить Enterprise-тариф у любого провайдера — они часто дают скидку 20–40% при годовом контракте, к этому также прилагается персональный менеджер, приоритетные ответы, гарантии uptime, latency, throughput , приоритет в очереди при высокой нагрузке на серверы .

        Напишите письма с темой "Enterprise API Access Request — 7B tokens/month" к разным провайдерам, в том числе и в DeepSeek (или как его там правильно:)


        1. Artem_leadl Автор
          16.03.2026 13:55

          не знал про такую опцию, пошел писать всем, спасибо большое


  1. eps
    16.03.2026 13:55

    И причём тут хаб CSS?