AI-портал для аутстаффинга: внутренний кейс ITS
Как мы за неделю собрали AI-агента для подбора инженеров из 50 партнёрских Telegram-чатов. Стек: Django + Telethon + gpt-4o-mini + o3-mini. $40/мес на проде.
Содержание
- TL;DR
- Что такое AI-портал для аутстаффинга
- Контекст: почему ручной процесс перестал масштабироваться
- Почему это раньше не автоматизировали
- Что рассматривали и почему отбросили
- Архитектура AI-портала
- Telethon-воркер
- Экстракция: набор узких агентов на gpt-4o-mini
- Матчинг: o3-mini с 8-dimension scoring
- Хранение и Django-дашборд
- Деплой и доступ
- Цифры на проде
- Что мы получили "побочкой"
- Чему мы научились (что бы сделали иначе)
- Когда такой подход применим у вас
- Когда такой подход НЕ работает
- FAQ
- Итог
AI-агент для подбора кандидатов и автоматизации аутстаффинга — внутренний инструмент, который мы собрали в ITS за неделю и который сейчас держит поток позиций из ~50 партнёрских Telegram-чатов на одном операторе вместо CEO. Стек предсказуемый: Django + Telethon + LangChain + связка gpt-4o-mini (структурирование) и o3-mini (reasoning-матчинг). Бюджет на проде — около $40 в месяц. Ниже — что собрали, как устроена архитектура, какие цифры на проде и в каких случаях такой подход применим у вас.
TL;DR
- Задача: автоматизировать обработку позиций из ~50 партнёрских Telegram-каналов и подбор инженеров из bench.
- Стек: Django 5.1 + SQLAlchemy + SQLite, Telethon (user-сессия, не Bot API), LangChain поверх OpenAI (
gpt-4o-miniна экстракцию,o3-miniна матчинг), TaskIQ + Redis на фоновые задачи, Caddy + oauth2-proxy + Keycloak на доступ. - Бюджет: один VPS ~30/мес на токены = ~$40/мес на проде.
- Объём: ~1500 позиций и ~200 откликов в месяц через одного оператора вместо CEO.
- Скорость сборки: 3 часовых созвона на планирование, неделя вайбкодинга силами одного senior-инженера.
- Ключевой принцип: финальное решение по отклику остаётся за человеком — это сознательный выбор, не ограничение.
Что такое AI-портал для аутстаффинга
AI-портал для аутстаффинга — внутренняя система, которая принимает поток позиций из мессенджеров и почты в свободной форме, структурирует их через LLM-экстракцию, сопоставляет с базой инженеров через reasoning-модель и предлагает оператору готовые пары "позиция — кандидат" с обоснованием по нескольким критериям. Финальный отклик отправляет человек, не агент.
Это не "AI-рекрутер", который заменяет человека, и не RPA-робот, который скроллит чаты. Это инструмент, который снимает с оператора рутинную часть (мониторинг, экстракция, первичный матчинг) и оставляет ему то, что требует суждения: финальное "да/нет" и коммуникацию с партнёром.
Контекст: почему ручной процесс перестал масштабироваться
У ITS аутстафф-направление: даём инженеров на внешние проекты партнёрам. Источник позиций — закрытые Telegram-каналы, в которых другие IT-компании постят роли, не закрытые своими силами. Канал — это поток сообщений в свободной форме: где-то с требованиями, где-то без, где-то на английском, где-то вперемешку, иногда со ставкой, иногда без.
До автоматизации процесс держался на одном человеке — CEO. Не потому что он любил Telegram, а потому что для адекватной оценки позиции нужно одновременно держать в голове состав bench, текущую загрузку каждого инженера, исторический контекст по партнёру и интуицию по адекватности роли. Делегировать это младшему сотруднику не получалось: формальные критерии не покрывают "позиция со ставкой ниже рынка от партнёра, который всегда платит вовремя — соглашаемся ради отношений".
Узкое горлышко выглядело так: на сезонных пиках поток позиций обгонял оператора, и часть просто пропускалась. Не "обрабатывалась хуже", а пропускалась — потому что физически некому было прочитать.
Почему это раньше не автоматизировали
Если бы вы спросили нас в 2023-м, мы бы ответили: "теоретически можно RPA-роботом скроллить чат, но что дальше делать с описанием — непонятно".
Главная проблема была в матчинге. Подбор кандидата под позицию — это не SQL-запрос. Описание позиции — свободный текст. CV кандидата — свободный текст. Между ними нет общего ключа: "Senior Python Developer, 5+ years" и "разработчик с опытом в финтехе на FastAPI" формально про одно и то же, но любой регулярный матчер тут ломается.
В 2024-м у нас впервые появилось ощущение, что LLM-модели стали достаточно умными, чтобы делать этот матчинг не хуже живого оператора, и достаточно дешёвыми, чтобы это можно было гонять на потоке. До появления reasoning-моделей вроде o1/o3-mini подобный матчинг ломался на edge-кейсах, и ROI автоматизации был отрицательным: ручные правки съедали выигрыш в скорости.
Что рассматривали и почему отбросили
Bot API. Очевидное первое решение. Не подошло: в большинстве партнёрских чатов бот не имеет прав на чтение истории, и пригласить его требует разрешения админа. Telethon с user-сессией таких ограничений не знает.
Регулярки и эвристики на экстракции. Тестировали на выборке. Базовые поля (название роли, грейд) — точность ~70%, составные (требования, длительность, локация в свободной форме) — резко падает. Edge-кейсы съедали ровно столько времени оператора, сколько мы хотели сэкономить.
Эмбеддинги + cosine similarity для матчинга. Дёшево, но проигрывает на нюансах: "5 лет коммерческого Go" против "разработчик с pet-проектом на Go" эмбеддинги ставят слишком близко. На дорогих B2B-позициях это много false positives.
LLM-only архитектура (одна большая модель на всё). Дорого и медленно. Экстракция структурированных полей — задача на дешёвую модель, reasoning-матчинг — на дорогую. Смешивать — дороже без выигрыша в качестве.
В итоге пришли к двухконтурной архитектуре: набор узкоспециализированных дешёвых экстракторов на gpt-4o-mini плюс один reasoning-агент на o3-mini для матчинга свежих позиций.
Архитектура AI-портала
flowchart LR
A[Партнёрские TG-каналы] -->|Telethon NewMessage<br/>user session| B[Bot worker]
B --> Q[(TaskIQ + Redis)]
Q --> X[Extractor agents:<br/>title / skills / rate / ...<br/>gpt-4o-mini]
X --> DB[(SQLAlchemy + SQLite<br/>Position, Engineer, Match)]
DB --> M[Semantic matcher<br/>o3-mini, 8-dim scoring]
M --> DB
DB --> P[Django dashboard<br/>unmanaged ORM, Bootstrap 5]
P --> O[Оператор]
O -->|Отклик через портал| A2[Партнёр]
P --> Auth[oauth2-proxy → Keycloak OIDC]
Net[Caddy reverse proxy] --> P
Двухконтурная архитектура: дешёвые узкие экстракторы — на структурирование, дорогая reasoning-модель — только на матчинг свежих позиций.
Telethon-воркер
User-сессия Telethon, подписка на events.NewMessage(incoming=True) для списка каналов из конфига. Каждое новое сообщение нормализуется, кладётся в БД и порождает фоновую задачу TaskIQ на экстракцию.
client = TelegramClient(TG_SESSION, TG_API_ID, TG_API_HASH)
@client.on(events.NewMessage(incoming=True))
async def on_message(event):
nm = await normalize(event)
await process_position.kiq(nm.model_dump())
Из неприятного: FloodWaitError от Telethon обрабатываем явным await asyncio.sleep, а не пропускаем offset; .session-файл регулярно бэкапим (потеря — повторная авторизация через SMS); подписку на events.MessageEdited пока не сделали — партнёры иногда правят сообщение через 30 минут после публикации, это в TODO.
Экстракция: набор узких агентов на gpt-4o-mini
Здесь reasoning не нужен. Вместо одного "большого" экстрактора у нас набор специализированных агентов под отдельные поля — каждый со своим коротким промптом и температурой 0.0:
TITLE_EXTRACTOR— нормализованное название роли;SKILLS_EXTRACTOR— primary/secondary стек, с разделением "требуется" vs "упомянуто";RATE_EXTRACTOR— ставка, валюта, период (час/день/месяц);EXT_POSITION_ID_EXTRACTOR— внешний ID, если партнёр его указывает;- ещё несколько на категорию, условия оплаты, опыт.
Через LangChain каждый агент получает только релевантный кусок текста и возвращает строго заданное поле. Паттерн "много мелких агентов вместо одного большого" даёт фокус (промпты короткие и обозримые), стоимость (на полях достаточно дешёвой модели) и контролируемость (промпт можно править, не ломая остальные).
Матчинг: o3-mini с 8-dimension scoring
Сюда уходит только то, что прошло экстракцию и помечено как валидная позиция. Системный промпт матчера — один из самых длинных в проекте: вместо короткого "найди подходящих" мы прописали взвешенную модель оценки по 8 измерениям с явными весами:
| Dimension | Вес |
|---|---|
| job_title_alignment | 25% |
| skills_technical | 25% |
| rate_compatibility | 20% |
| location_match | 15% |
| seniority_level | 8% |
| domain_experience | 4% |
| soft_requirements | 2% |
| culture_fit | 1% |
Плюс жёсткие "предохранители" в самом промпте: если job_title_alignment < 0.5, то overall_match_score MUST be < 0.6; если skills_technical < 0.5 — < 0.65; и так далее. Без этих правил reasoning-модели охотно компенсируют слабое попадание по одному критерию сильным по другому, и итоговый score становится оптимистичнее, чем стоит.
На выходе матчер возвращает overall_match_score, recommendation, dimension_scores, strengths, concerns, verdict, interview_questions. Свободный текст в concerns/strengths сознательно оставлен — оператор видит логику решения и за 5 секунд понимает, согласен он с моделью или нет.
Хранение и Django-дашборд
Production-данные пишутся через SQLAlchemy + Alembic: миграции, типобезопасный domain-слой, никакой Django ORM на write-пути. outstaff.db — SQLite в WAL-режиме. На наш объём (БД меньше гигабайта, один воркер на запись) этого хватает с запасом.
Поверх той же базы поднят Django 5.1 dashboard — но через unmanaged-модели, отзеркаленные 1-в-1 со схемой SQLAlchemy. Это компромисс: получили готовый Django admin и шаблоны на Bootstrap 5, не дублируя миграции и не создавая два источника правды для схемы.
Главный экран — список свежих позиций с топ-кандидатами, скором по 8 измерениям, рисками и кнопками "одобрить и отправить" / "отклонить" / "переподобрать". Все действия логируются в отдельную таблицу — это даёт и аналитику по каналам, и аудит.
Деплой и доступ
Один VPS, Docker Compose, Caddy как reverse proxy и автоматический TLS. Аутентификация в дашборд — через oauth2-proxy → Keycloak (OIDC): команда логинится корпоративным SSO, отдельных паролей под Django нет.
Если у вас есть похожий процесс — поток сообщений в свободной форме плюс одно узкое горлышко в виде живого человека, — мы можем бесплатно посмотреть на сценарий и сказать, есть ли смысл его автоматизировать. Обсудить с командой ITS.
Цифры на проде
| Метрика | До | После |
|---|---|---|
| Кто держит процесс | CEO | Один оператор |
| Покрытие чатов | ~30 каналов вручную | ~50 каналов автоматически, 24/7 |
| Время от позиции до отклика | от 2 часов до "пропустили" | около 15 минут, без пропусков |
| Прямые расходы | — | ~10 хостинг + $30 токены) |
| Объём в месяц | "сколько успел" | ~1500 позиций, ~200 откликов |
| Размер кодовой базы | — | ~10 300 строк Python + ~1 800 HTML/JS |
Цифры по точности матчинга мы пока не публикуем — внутренние замеры есть, но методология не готова к внешнему обсуждению. Правило простое: метрика без замера — не метрика.
Главный бенефит, который не ложится в таблицу: фокус CEO. Он перестал быть бутылочным горлышком и переключился на то, что действительно требует его участия — стратегические партнёрства, найм, продукт.
Что мы получили "побочкой"
Когда переписываешь ручной процесс в систему, обычно получаешь больше, чем планировал:
- Аналитика по каналам. Раньше мы не знали, какой канал даёт больше всего качественных позиций. Теперь это видно в портале и помогает решать, какие чаты держать подключёнными.
- История взаимодействий. Партнёр через полгода спрашивает: "вы у нас по такой-то позиции в феврале откликались?" — раньше CEO рылся в личке, сейчас открываем портал и видим всё.
- Учёт фидбека от инженеров. Раньше "не хочу embedded-системы, готов только на удалёнку, занят до конца квартала" жило в голове CEO. Сейчас — в портале, и матчер учитывает это на этапе подбора.
Чему мы научились (что бы сделали иначе)
- Один большой "промпт-всемогутер" не сработал. Пришлось разбить экстракцию на узкие шаги: отдельный агент на название, отдельный — на стек, отдельный — на ставку. Так короче промпты, проще править и дешевле по токенам.
- Жёсткие правила для матчера понадобились явно. Без MANDATORY RULES reasoning-модель компенсировала слабое попадание по роли сильным попаданием по стеку, и финальный score становился оптимистичнее, чем стоило бы.
- Финальный шаг сразу решили оставить за человеком. Соблазн сделать авто-отклик при высоком score был, но в B2B цена ложноположительного отклика выше, чем выигрыш в скорости. Это решение мы менять не планируем.
- CV-база — главное узкое место. Модель хороша ровно настолько, насколько структурирована информация об инженерах. Половина усилий следующего квартала — разметка bench-профилей.
Когда такой подход применим у вас
Логика воспроизводится в любой нише, где сходятся четыре условия:
- Источник данных — мессенджер, почта или CRM в свободной форме. Если у вас уже структурированный ATS со стандартными формами вакансий — LLM-экстракция добавит шума, а не точности.
- Один человек (часто фаундер или операционный директор) держит процесс в голове, и это узкое горлышко. Без этой боли проект не окупится.
- Решение требует двух шагов: структурировать вход и сравнить с базой. Не reasoning через весь домен, не сложный workflow с десятками веток.
- Цена ошибки терпимая, и финальное решение можно оставить за человеком. Если ошибка стоит сделки на $200K — лучше потратить больше времени на проектирование, чем спешить.
Если все четыре условия выполняются — есть шанс, что вы соберёте свою версию за похожие деньги и сроки. Если хотя бы одно ломается — лучше начать с разбора процесса, а не с выбора стека.
Когда такой подход НЕ работает
Чтобы не звучало как "стройте AI-агентов везде":
- Цена ошибки высокая, а human-in-the-loop недоступен — не делайте. Авто-действие в B2B без оператора — стабильно плохая идея.
- Входные данные хорошо структурированы. ATS со стандартными формами вакансий не нуждаются в LLM-экстракции — добавите шум и стоимость.
- Объём — десятки сообщений в день. Ручная работа дешевле инфраструктуры.
- Матчинг строится на жёстких правилах (точное совпадение сертификатов, лицензий) — LLM добавит шума, а не точности.
В ITS мы обычно начинаем такие внутренние и клиентские проекты с разметки процесса по этим четырём вопросам — раньше выбора стека или модели. Большая часть "это нельзя автоматизировать" на проверке оказывается "это можно автоматизировать, если оставить финальное решение за человеком" — что сильно меняет картину.
FAQ
Сколько стоит собрать AI-портал для подбора кандидатов? В нашем случае — около 35–40 часов работы senior-инженера за неделю плюс ~10 хостинг + ~$30 токены OpenAI). Сборка с нуля под другую нишу займёт сопоставимое время, если вход — Telegram/email и матчинг укладывается в двухконтурную схему "экстракция + reasoning".
Зачем o3-mini для матчинга, если есть более дешёвые модели?
Мы пробовали gpt-4o-mini и для матчинга — точность падает на нюансах вроде "человек 2 года писал на Go, остальное — Python; подходит на Senior Go?". На объёме это даёт десятки лишних некорректных откликов в месяц, что ломает доверие партнёров. Разница в стоимости — порядка $0.05–0.10 на матчинг — на 200 откликов в месяц это считаные доллары.
Почему SQLite, а не Postgres?
Один воркер на запись, один пользователь UI на чтение, объём данных меньше гигабайта. SQLite в WAL-режиме держит это с запасом и не требует отдельного контейнера, бэкапа сложнее cp database.db backup.db или мониторинга. Когда упрёмся в лимиты — мигрируем на Postgres за вечер.
Почему Django, если основной слой данных — SQLAlchemy? Нужен был дашборд с минимальным фронтенд-усилием. Django + admin + готовые шаблоны на Bootstrap покрыли это за день. Чтобы не поддерживать две схемы, Django-модели отражают SQLAlchemy-схему как unmanaged. Единая истина — миграции Alembic.
Почему Telethon, а не Bot API? В большинстве партнёрских Telegram-чатов бот не имеет прав на чтение истории, а пригласить его требует согласия админа. Telethon с user-сессией работает от имени реального аккаунта, который уже состоит в этих чатах, и таких ограничений не знает. Telegram официально разрешает автоматизацию через MTProto в рамках общих rate limits.
Можно ли это сделать без инженера, на n8n или Make? Парсинг и экстракцию — да. Матчинг через LLM с приличной точностью — затруднительно: нужна работа с контекстом CV-базы, дедупликация, история, фидбек-петля. На low-code уровень MVP вы получите, но он быстро упрётся в потолок: либо в стоимость токенов на каждый прогон, либо в качество подбора.
Как обходим Telegram TOS для user-аккаунтов? Никак не обходим. Telegram явно разрешает автоматизацию через Telethon/MTProto при выполнении общих rate limits и ToS. Партнёрские чаты — это закрытые сообщества, в которые мы приглашены как участники.
Где здесь самое узкое место сейчас? Качество CV-базы. Модель хороша ровно настолько, насколько структурированы данные о наших инженерах. Половина усилий за следующий квартал уходит на разметку bench-профилей: явные теги по стеку, история фидбека от партнёров, доступность.
Сколько времени реально ушло у инженера на сборку? Примерно 35–40 часов чистой работы за неделю. Без переработок, без героизма. Большая часть — не код, а переписывание промптов и борьба с edge-кейсами в чатах.
Можно ли заменить o3-mini на Claude или DeepSeek?
Архитектурно — да, это смена 1–2 строк через LangChain. Главное — повторно прогнать промпт матчера на отложенной выборке, потому что MANDATORY RULES чувствительны к тому, как конкретная модель интерпретирует "MUST" в инструкциях.
Итог
3 созвона на планирование. Неделя работы senior-инженера. ~$40/мес на проде. CEO, которому больше не нужно жить в Telegram. Один оператор вместо одного фаундера.
Главный для нас урок не про код, а про то, что граница "это можно автоматизировать" сильно сдвинулась за последние полтора года. Задачи, которые казались слишком завязанными на человеческое суждение, сейчас стоят $30/мес токенов, если правильно их декомпозировать на структурирование вход + reasoning-сравнение.
Самое неудобное: мы откладывали этот проект полтора года, потому что "ну такое же не автоматизируешь". Возможно, у вас в компании есть похожая задача, которую тоже пора пересмотреть.
Если хотите обсудить, как AI-агенты лягут на ваши процессы — напишите нам. Разберём задачу, покажем похожие реализации и предложим формат пилота. Для первых 3 компаний с релевантным сценарием готовы провести бесплатный технический разбор и помочь спроектировать MVP под ваш процесс.
Контакты:
- Telegram: @savchuda
- WhatsApp: +351 930 495 062
- Max: написать в Max
- Email: presales@its.xyz
- LinkedIn: Дмитрий Савчук
