Агрегатор новостей заработал, когда я перестал доверять ИИ

от автора

С чего всё началось

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

Открываю один ресурс, второй, третий — реклама, баннеры, всплывающие окна. Не успеешь нажать на крестик, можно вообще улететь в неизвестном направлении. Вроде ищешь события, а натыкаешься на блогеров, экспертов, мнения, и особенно бесят душераздирающие истории представителей богемной тусовки.

Дальше по привычке открываю Телеграм. Там у меня десятки каналов, и в каждом события дублируются раз восемь в восьми разных пересказах. А по‑настоящему важного среди всего этого набирается три‑четыре штуки, в лучшем случае.

Мне не хватало не ещё одного источника — их и так достаточно. Не хватало того, чтобы кто‑то прошёлся по всему этому за меня: убрал повторы, выкинул лишнее и выдал короткий список по важности. Без экспертов, желтухи, рекламы, баннеров — чистая событийная лента.

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

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

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

Логично рождается вопрос — агрегаторов и так много, зачем ещё один? Дело в том, что классический агрегатор — это, по сути, список ссылок: пять изданий написали про одно происшествие, вот тебе пять ссылок. Мне же хотелось, чтобы кто‑то прочитал всё про это событие за меня, сверил все версии между собой и оставил только то, что подтверждено и самое главное. Это уже не совсем агрегатор в привычном смысле, скорее автоматическая сводка фактов — я и пытался построить именно её.

Как это работает

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

Поэтому на входе стоит довольно жёсткий фильтр. «Эксперт рассказал», «психолог объяснил» — в мусорку. Заявление блогера, какие шмотки надеты у звезды сериалов, интервью, прогнозы и гадания — до ленты не доходит. Ничего не имею против таких жанров, просто им тут не место. Когда я впервые посмотрел, сколько этот фильтр выбрасывает за день, мне заплохело — сколько шлака засоряло мой мозг. 

Не сразу дошло и до другой вещи: событие должно быть законченным. Пример из спорта — после первого периода счёт 1:0, но через час эта цифра уже никому не интересна. Закончился матч — вот тогда есть событие с итогом, который больше не изменится. То же самое с материалами вида «главное за день»: это не одно событие, а подборка нескольких, собранная для удобства редакции. Для моей ленты подобные сводки доставляли большие проблемы в процессе кластеризации, и от них необходимо избавится на входе.

Звучит, наверное, банально, но именно эта дотошность на входе и делает ленту потом читаемой. Тишину на выходе даёт не оформление, а фильтр на входе.

Дальше начинается самая трудная часть: понять, какие статьи рассказывают про одно и то же событие. Пять изданий вполне могут описать один пожар совершенно разными словами, и по ключевым словам это не поймать, я проверял. А вот по смыслу ловится: отдельная модель читает текст и определяет, о чём он на самом деле, поэтому, к примеру, «возгорание на складе» и «пожар в ангаре» по итогу оказываются рядом. Работает модель детерминировано, то есть на один и тот же текст всегда выдаёт один и тот же результат.

Далее правило, на котором держится вся система, и оно же является ее главным фильтром: событие публикуется, только если о нём написали минимум два независимых источника. Никакого рейтинга и сложных формул — просто счётчик: два разных издания написали — публикуем, одно — ждём. Одинокая новость иногда подтверждается, а иногда может быть ложной, и выпускать её в эфир без подтверждения другим источником рискованно.

Потом включается ИИ‑слой, который формулирует один нейтральный текст о событии по материалам всех источников. Он попадает в ленту с учётом значимости и свежести. Процесс повторяется несколько раз в сутки. В телегу на канал мониторинга регулярно приходят отчеты о здоровье системы, тем самым контроль работы ресурса сведён к минимуму, а лента живет своей жизнью.

Какая роль у ИИ

Здесь обычно возникает главный вопрос: «то есть, у тебя новости нейросеть пишет?» Нет, не пишет — она их формулирует по уже собранным и проверенным фактам, и это разные вещи: что попадёт в ленту, решают простые правила без всякого ИИ.

ИИ в системе задействован в несколько этапов, но у каждого узкая задача и жёсткие ограничения: один этап пересказывает уже подтверждённое событие обычным языком, после чего текст проходит проверку обычным кодом, без ИИ — совпадают ли числа с источниками, не пролезли ли речевые штампы и типичные для нейросетей обороты, по которым такой текст легко узнать. Если что‑то не так, событие уходит на доработку, и подключается следующий слой LLM — уже не с чистого листа, а с конкретным списком претензий, которые нужно исправить. Исправил, прошёл проверку заново — публикуется. Не смог — событие в ленту просто не попадает. Выдумка в итоговой формулировке — это главный ред флаг.

Второй слой ИИ в роли контролёра — плохая идея

Дальше опишу проблемы, с которыми столкнулся.

В один из моментов пришла идея, а пусть вторая модель проверяет качество текстов, которые пишет первая. Одна пишет, другая оценивает, идея на тот момент казалось прорывной.

На практике вторая модель почти всегда что‑то находила, даже там, где текст был в полном порядке, и каждый раз обоснования находки объясняла уверенно. После нескольких дней войны с проверочным слоем LLM я понял, что ничего в работе слоя изменить не смогу.

Поэтому данный слой пришлось отключить и взять себе на заметку — нельзя задавать ИИ вопросы на мнение. Спросишь «оцени качество текста» — получишь прекрасно обоснованную ерунду. Срабатывают вопросы не на мнение, а вопросы, где ответ можно проверить: совпало или нет, есть или нет. Мнение у модели найдётся всегда, и она его будет отстаивать крайне убедительно.

В итоге завел список оборотов и штампов, которые в итоговую формулировку попасть не должны. Каждый новый текст сверяется со списком и в случае появления запретного оборота, кластер уходит на переформулировку ИИ с конкретной задачей — исправить найденный оборот.

Когда два разных события склеиваются в одно

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

Цифры, которых не было ни в одном источнике

В один из дней сел перечитать случайную выборку опубликованных текстов и обнаружил, что заметная часть содержит цифры, которых не было ни в одном источнике. При этом модель не врала грубо, а как будто уточняла: источники писали «более ста» — она выдавала точное число, дату никто не называл — модель подставляла дату, как потом понял, из своих знаний.

Виноват тут оказался я сам: модель получала от меня обрезанные до 1000 символов тексты источников, по сути, первый абзац. Модели не хватало фактуры, и она сама додумывала картину из своих знаний. Косяк в коде я обнаружил, и с тех пор она получает полные тексты источников, а каждое число в готовом тексте автоматически сверяется с источниками детерминированным правилом. Если число не совпадает — текст уходит не на лёгкую правку формулировки, а на более серьёзную переформулировку: модель заново смотрит на полные тексты источников, чтобы взять оттуда правильное число.

Слово «сегодня»

Существуют слова, которые по прошествии времени теряют свой смысл: «сегодня», «вчера», «завтра». Текст написан вечером со словом «сегодня», открываешь статью утром — и «сегодня» уже означает другой день. Данную неточность в тексте также необходимо устранять автоматически.

Решил это тем же способом, что и раньше — добавил слова в список, который код автоматически удаляет из текста перед публикацией.

Просто вырезать слово оказалось недостаточно, иногда после исключения повисал голый предлог — например, «перенесена на.» — и получался брак другого порядка. Поэтому после вырезания результат дополнительно проверяется, и если что‑то сломалось, текст уходит на переформулировку целиком.

Итог

RuPress работает на rupress.info

Главный вывод, к которому пришёл за время работы над лентой: чтобы ИИ приносил пользу, доверять ему нужно по минимуму. Все решения, которые работали — это места, где я сузил модели задачу до одной конкретной операции и обложил её проверками. А неудачи — места, где я дал ей свободу и рассчитывал, что она разберётся сама. Не разобралась ни разу. Зато в узкой роли, под простыми правилами, справляется неплохо.

ссылка на оригинал статьи https://habr.com/ru/articles/1059364/