Содержание ↓
Пять вопросов, на которые раньше отвечали по памяти
Руководителю маркетинга каждый месяц задают одни и те же пять вопросов: что произошло на рынке, что со спросом, кто занимает эфир, почему сдвинулась тональность и сколько рекламного давления у нас и у остальных игроков. В радаре под каждый вопрос отведён свой экран, и все пять открываются на одном выбранном месяце.
Руками ответ собирается долго. Аналитик листает ленты СМИ и телеграм-каналы, выгружает отзывы, сводит их в таблицу и пишет выводы. Прочитать всё нельзя, поэтому выводом становится то, что попалось на глаза. Если два человека считают одну долю голоса по-своему, числа расходятся, и совещание уходит в спор о методике.
Как устроен конвейер
Сбор идёт ночью по системному cron на сервере. Коллекторы забирают RSS-ленты СМИ, посты телеграм-каналов, отзывы из магазинов приложений и с сайтов-отзовиков. Рядом живут поисковый спрос из Wordstat (о нём отдельный кейс) и выгрузки рекламного давления, которые загружаются файлом.
На входе стоит фильтр по теме. Он держится на словаре корней отраслевых терминов. В условной сети клиник корень «стоматолог» поймает и «стоматологию», и «стоматологов», так что падежи перечислять не нужно. Блок-лист офтопа поначалу искал подстроку, и слово «мода» находилось внутри «чемодана», а «кино» — внутри чьей-то фамилии. Теперь он работает по границе слова. Для прохода нужны два разных понятия из словаря, причём одно слово засчитывается одним свидетельством. Без этого правила слово, которое входит сразу в два понятия, набирало оба и проскакивало порог в одиночку.
Дальше каждый материал получает эмбеддинг. Жадная кластеризация с порогом косинусной близости 0,82 склеивает пересказы одной истории в сюжет. Сюжетам от трёх материалов короткий заголовок пишет модель, одиночкам название собирает код из их же текста. Стадию сюжета (новый, разгоняется, растёт, стабилен, затухает) код ставит по порогам из конфигурации.
Все экраны месяца собираются из одного расчёта, и после сборки он замораживается снимком. Сдвиги ловят детекторы CUSUM и EWMA на дневных рядах. Изменение тональности раскладывается по сюжетам. Вклад сюжета — это его вес, умноженный на тон, за текущий месяц за вычетом того же произведения за прошлый. В сумме вклады дают ровно общий сдвиг, и за этим тождеством следит тест. Долю голоса в СМИ я считаю по внешней прессе: собственные каналы брендов в знаменатель не входят.
С любого числа можно спуститься на три уровня. Сначала цитаты со ссылками на первоисточник, потом разбор по составляющим, в самом низу методология, где записаны формула и окно расчёта со всеми оговорками. Доказательства подгружаются по клику, чтобы страница не тянула сырьё при каждом открытии. Текущий уровень разбора выгружается в Excel, и ячейки, которые начинаются с «=», «+», «-» или «@», получают апостроф — защита от формул в чужом тексте.
Откуда берутся числа на экране
На экранах радара нет ни одного числа, которое придумала модель. Её работа — поставить тональность категорией, решить, относится ли материал к рынку, назвать сюжет и написать связный абзац. Считается всё в TypeScript или SQL. Пришёл я к этому после метрики, которую «оценивала» модель. От прогона к прогону она гуляла и всё время завышалась, а однажды вышла очень красивой. При проверке она оказалась, по сути, фейком.
С недельным отчётом так. Код готовит четыре фактшита — по СМИ, отзывам, поисковому спросу и позициям в выдаче. В каждом лежат факты и список чисел, которые разрешено упоминать. По ним модель пишет четыре секции и резюме: около пяти вызовов, раньше уходило 17–24. Если в тексте всплыло число не из списка, секция перегенерируется один раз. Со второй попытки не вышло — на её место встаёт шаблонный текст из тех же фактов с пометкой. JSON от модели сверяется со схемой, и при кривом ответе секция честно остаётся пустой, а в логе появляется строка.
Если экран врёт, я его выключаю. Так с витрины ушёл прогноз: числа у него были недостоверные, а решение по ним принять могли. Код я оставил. Экран вернётся, когда прогноз сверят с тем, что случилось на самом деле.
Что получает маркетолог
Недельный отчёт приходит в понедельник утром. Сводку на выверенных числах я сознательно поставил в нём выше AI-текста: любую цифру в сводке можно раскрыть до строк, из которых она посчитана. Прозу так проверить нечем.
Отчёт для руководства собирается раз в месяц. После закрытия месяца его числа замораживаются, потому что корпус СМИ пополняется задним числом и без заморозки тот же июль пересчитывался бы при каждом открытии. Заморозить месяц можно только после паспорта полноты. В нём семь классов проверок: окно периода, покрытие, валидность, согласованность с базой, симметрия сбора, стабильность и связность отчёта с самим собой. Месяц, собранный с дырами, паспорт не пройдёт.
В презентациях свои строгости. Если в блоке есть данные, под ним должна стоять сноска с методом, и тест это проверяет. Пропуск в ряду на графике остаётся пропуском. Средний рейтинг отзывов идёт с 95-процентным интервалом: на паре десятков отзывов разница в 0,3 звезды статистически неотличима. Контекст радара подтягивается и в производство кампаний, так что бриф и матрица сегментов видят свежие сюжеты рынка.
Где мы спотыкались
Перед массовым прогоном нового промта по нескольким сотням публикаций мы проверили его на четырёх живых постах и прочитали ответы глазами. Нашлось два дефекта: модель вопреки запрету притянула нужную тему к посту про аренду квартир, а у половины источников в базе был только анонс на 130–300 знаков. Массовый прогон заполнил бы поле догадками по заголовкам.
Один и тот же отзыв лежал в сюжете пятнадцать раз. Ключ дедупликации включал автора и дату. Автор часто пустой, а в Postgres два NULL друг другу не равны, поэтому конфликт не срабатывал. Дата тоже плыла: площадка пишет «неделю назад», и каждый сбор пересчитывал её от своего дня. Помог хэш от площадки и текста.
Сторож свежести знал про один источник, хотя о запусках отчитывалось больше десятка. Несколько потоков молчали неделями, а единственный алерт, доходивший до Telegram, был ложным: он считал отклонёнными сигналы, которые система скрывает сама. О качестве самого детектора мы судили по горстке наблюдений.
На июльских страницах абзац начинался словами «В июне». Месяц попал в фактшит строкой, и модель честно пересказала то, что ей дали. Теперь месяц передаётся параметром. Заодно в код добавили предложный падеж месяца: раньше там были только формы для «за июль» и «до июля», и в текст пролезало «за июня».
Ещё два урока касаются данных. База через API отдаёт не больше 1000 строк на запрос, и дашборды упирались в подозрительно круглые 1000 публикаций — лечится постраничной выборкой и счётом в базе. А однажды сверка чисел сошлась, хотя два представления одного источника расходились в 17 раз. С тех пор для каждого источника я спрашиваю, что именно доказывает сверка.
Если собирать заново
Я бы снова взял два источника и словарь терминов. Следом таблица чисел из SQL-запроса, а текст от модели в самом конце, когда таблице уже можно верить. Список источников под свою категорию удобно собирать на консультации, там же решаем, какие проверки поставить до первого отчёта. Для условной сети клиник он вышел бы коротким: отзывы в картах и в приложении записи, городские СМИ, пара профильных телеграм-каналов. Словарь соберётся из названий услуг и симптомов.
- Claude Code
- Next.js 15
- TypeScript
- Supabase (Postgres + pgvector)
- OpenAI gpt-4.1-mini
- text-embedding-3-small
- cron на сервере
- Telegram-алерты
