Бот для комитета подбора арендаторов: правила, уроки из отказов, честные замеры
Агентство коммерческой недвижимости каждый день проводит «комитет»: брокеры смотрят новые объекты и решают, каким арендаторам их предложить. Раньше это делали вручную в Telegram-чате, а выполнение обещаний сверяли по памяти. С июля 2026 года я веду для агентства бота, который проводит этот комитет вместе с людьми. В бою он с 2 сентября, а с конца октября работает на сервере заказчика.
Ниже — какие решения сработали, какие цифры я считаю честными и что пришлось выбросить.
Задача и ограничения
Комитет устроен так. Сотрудник выкладывает новый объект: помещение, площадь, цена, район. Брокеры подбирают к нему заявки арендаторов из CRM и обещают отправить коммерческое предложение. На следующий день руководитель проверяет, кто отправил, а кто нет.
Что требовалось от бота:
- подбирать заявки к объекту из базы примерно в 23 тысячи заявок;
- понимать ответы брокеров в чате, в личных сообщениях, голосом и на записях встреч;
- сверять обещания со статусами в CRM и выпускать отчёт «выполнено / не выполнено» со штрафами;
- работать в Telegram, где уже живёт комитет, а не заставлять людей переходить в новый интерфейс.
Ограничения: CRM с плохо документированным API, данные заказчика, бюджет на модель — копейки на объект, а не рубли.
Решение 1. Правила до модели и после неё
Первый соблазн — отдать модели объект и всю базу заявок. Это дорого и плохо работает: модель путает районы и забывает про площадь.
Я разделил работу. Детерминированный фильтр из 10 правил отсекает заявки до модели: тип сделки, назначение, площадь, бюджет, район, географические зоны, этаж, вытяжка и ещё несколько условий. Модель получает короткий список и выбирает кандидатов и ниши. Затем тот же список правил проверяет её ответ, поэтому предложить заявку, которую правила запрещают, модель не может.
Детали, которые оказались важными:
- Площадь не симметрична. Арендатор, который ищет 100 м², охотнее посмотрит 180 м², чем 70 м². Допуск снизу — 20%, сверху — вдвое. Асимметрию выбрал замером на отказах брокеров.
- Район сравнивается по словам, а не по подстроке. Иначе «ЗАО» находится внутри «СЗАО».
- Вердикт по ответу брокера выносит код. Модель извлекает из реплики сущности: бренд, номер заявки, отказ. Решает код, а бренды сводятся к одному написанию через нечёткое сравнение и транслитерацию.
Решение 2. Уроки из отказов вместо дообучения
Брокеры отказываются от предложенных заявок и пишут причины: «уже смотрели», «ищут только центр», «нужен первый этаж». Дообучать модель на этом дорого и медленно. Я сделал проще:
- отказы с причинами попадают в промпт короткими уроками;
- в карточке заявки модель видит итог: «отклоняли на N объектах из M»;
- заявка, которую отвергли на трёх и более объектах, уходит в конец списка. Отказы «по месту» («нет вытяжки») не считаются: это свойство помещения, а не заявки.
Замер. 15 последних объектов, по два прогона на каждый вариант, три варианта:
| Вариант | Отвергнутые брокерами заявки в выдаче |
|---|---|
| без отзывов | 70% |
| отзывы без понижения | 67% |
| отзывы с понижением | 51% |
По объектам понижение оказалось лучше одних отзывов в 10 случаях и хуже в одном (знаковый критерий, p = 0,006). Отдельно проверил на разметке руководителя комитета, что хорошие кандидаты при этом не вытесняются.
Чего замер не показывает: часть отзывов оставлена позже проверяемого объекта, поэтому на старых объектах эффект может быть завышен. Так и написано в отчёте заказчику.
Решение 3. Код добирает то, что модель теряет
Часть решений комитет принимает голосом на встрече: «по заявке такой-то отправляем». Запись встречи бот режет на куски, распознаёт и превращает в черновик назначений для администраторов.
Модель на длинной расшифровке пропускает номера. На одной записи прозвучало 69 номеров заявок: модель нашла 50, ещё 19 добрал код поиском номеров в тексте, потеряно 0. Обратная защита тоже нужна: если в куске записи тишина, код не даёт модели «додумать» решения.
Распознавание речи выбирал сравнением на реальной записи комитета. Сущностей обе системы находили поровну, но у одной не было пунктуации и почти не было латиницы, а латиница встречается в названиях сетей-арендаторов. Выбрал вторую.
Решение 4. Модель — по замеру, провайдер — по доступности
Сравнил все чат-модели Yandex, линейку GigaChat и Gemini на одинаковых объектах. В бою осталась Gemini Flash. Позже доступ к ней перевёл на российского посредника: оплата идёт с юрлица заказчика, запросы уходят из РФ без прокси, а модель и промпт остались прежними.
Переезд на сервер заказчика
До конца октября бот жил на моём сервере. 30 октября перенёс его на российский сервер заказчика: systemd-служба, nginx с HTTPS, внешний сторож с алертами. Простой составил около минуты.
Telegram с российского сервера напрямую не открывается, поэтому бот ходит к нему через SSH-туннель на зарубежный сервер заказчика. В день переезда туннель не поднимался. Оказалось, что адрес зарубежного сервера точечно заблокирован из России: ping проходил, TCP на всех портах молчал, а соседние адреса той же подсети отвечали. Заказчик добавил серверу второй адрес, через него туннель заработал, и переезд прошёл в тот же день.
Что не сработало и чего я не утверждаю
- Все замеры до 2 сентября я выбросил. В августе бот читал статусы заявок в CRM неверно: подбор шёл по необработанным заявкам, а активные он не видел вовсе. Цифры качества того периода красивые, но бессмысленные. Ошибку нашёл живой комитет: из 11 брендов, которые люди назначили на один объект, до модели дошли только два.
- Совпадение с брокерами — не метрика качества. В августе бот совпадал с решениями брокеров на 18% доступного. Эту цифру я использовал для диагностики: 43% промахов давало правило площади, 23% — потолок длины списка, 14% — выбор модели. Метрикой качества её не считаю.
- Голос ведущего как канал обучения не прижился. По ТЗ руководитель надиктовывал уроки голосом. За два месяца этим не воспользовались ни разу, и ветку я удалил. Голосом остались ответы брокеров и записи встреч.
- Точность подбора в процентах не заявляю. Есть замеры на конкретных наборах, постоянной метрики в бою нет.
Цифры проекта
- 400+ коммитов, 1 900+ автотестов;
- база CRM — около 20 тысяч объектов и 23 тысяч заявок;
- 10 жёстких правил фильтра, уроки из отказов, понижение повторно отвергнутых заявок;
- в бою с 2 сентября 2026 года, на сервере заказчика с 30 октября.
Стек: Python 3.12, FastAPI, aiogram 3, SQLAlchemy (async) + SQLite, APScheduler, Gemini Flash через российского посредника, rapidfuzz, ffmpeg, API CRM INTRUM, API 2ГИС, systemd, nginx. Разработка — через Claude Code: спецификация, тикеты, тесты, ревью.