Все статьи

«Напомним, ранее...»: зачем мы вернули RAG, от которого сами отказались

Мы строим Рерайт-Завод — AI-систему для автоматизации рерайта новостей в региональных СМИ. В первой статье я рассказывала, как мы учим модель писать в стиле конкретного издания. Во второй — как устроен фактчек. Обе статьи — про задачи, которые хотя бы теоретически решают все. Стиль. Проверка фактов. Базовый набор.

А теперь про то, что не делает никто. И что отличает текст, написанный журналистом, от текста, написанного ChatGPT, — мгновенно, с первого абзаца.

Один абзац, который выдаёт машину

Откройте любой региональный портал. Выберите новость про ремонт дороги. С вероятностью 80% где-то в тексте будет:

Напомним, ранее мэрия обещала завершить ремонт участка улицы Ленина до конца 2025 года. Работы были начаты в июне и заморожены в октябре из-за проблем с подрядчиком.

Вот этот блок — бэк. Background. Контекст. То, что превращает новость из голого факта в историю с продолжением.

Журналист помнит, что его издание писало об этом три месяца назад. Помнит, что подрядчик менялся дважды. Помнит, что в комментариях было 200 разъярённых жителей. И добавляет бэк — потому что без него новость бессмысленна. Это не «ремонт дороги». Это очередной провал ремонта дороги, и читатель должен это знать.

AI-рерайтер берёт текст с ТАСС и пересказывает. Всё. У него нет памяти. Нет архива. Нет понимания, что издание уже три раза об этом писало. Текст формально правильный и абсолютно мёртвый.

Именно поэтому редакторы, которые пробовали ChatGPT для рерайта, говорят одно и то же: «Текст нормальный, но какой-то... пустой». Он пустой не потому, что модель плохая. А потому что у неё нет контекста публикации.

Почему это сложная задача

Кажется, что всё просто: загрузи архив издания в векторную базу, найди похожие статьи, вставь ссылку. Десять строк кода, полдня работы.

Нет.

Проблема 1: похожее ≠ связанное. Статья про ремонт улицы Ленина семантически похожа на статью про ремонт проспекта Мира. Embeddings скажут — 0.89 similarity, отличный матч. Только это разные улицы, разные подрядчики, разные скандалы. Бэк, ссылающийся на другую улицу, хуже, чем никакой бэк.

Проблема 2: релевантность по времени. Статья про ту же дорогу, но от 2019 года — это не контекст, это археология. Бэк должен быть свежим. Но «свежий» — понятие относительное. Для новости про губернатора бэк годичной давности нормален. Для новости про погоду — нет.

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

Как мы к этому подходили (и почему RAG вернулся)

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

Он был прав. Для стиля — бессмысленно.

Для бэков — наоборот. Тут нам нужна именно семантическая близость. Новость про ремонт улицы Ленина → найти предыдущие статьи про ремонт улицы Ленина. Новость про нового главу района → найти, что писали про предыдущего. Это ровно та задача, для которой embeddings и создавались.

«Ну вот тут — да. Тут вектора реально нужны. Только не для стиля, а для контента. Разные задачи — разные инструменты.»

Инженерный подход без эго.

Архитектура: три агента вместо одного

Бэк — это не просто «найди похожую статью и вставь ссылку». Это мини-расследование: что писали раньше? Что из этого важно сейчас? Как это сформулировать в одном абзаце?

Мы разбили задачу на трёх агентов:

┌──────────────────────────────────────────────────┐
│              ВХОДЯЩАЯ НОВОСТЬ                     │
│  «В Гатчине возобновили ремонт ул. Чкалова»      │
└─────────────────────┬────────────────────────────┘
                      │
                      ▼
┌──────────────────────────────────────────────────┐
│  АГЕНТ 1: ПОИСК                                   │
│                                                   │
│  Извлекает ключевые сущности:                    │
│  — Гео: Гатчина, ул. Чкалова                    │
│  — Тема: ремонт дороги                           │
│                                                   │
│  Ищет в векторной базе архива издания:            │
│  — По сущностям (точное совпадение)              │
│  — По семантике (cosine similarity > 0.82)       │
│  — Фильтр: не старше 12 месяцев                  │
│                                                   │
│  Результат: 0–5 кандидатов                       │
└─────────────────────┬────────────────────────────┘
                      │
                      ▼
┌──────────────────────────────────────────────────┐
│  АГЕНТ 2: ФИЛЬТРАЦИЯ                              │
│                                                   │
│  Из кандидатов отбирает реально связанные:       │
│  — Та же улица? Тот же объект?                   │
│  — Есть причинно-следственная связь?             │
│  — Добавляет ли контекст, а не шум?              │
│                                                   │
│  Выбрасывает ложные срабатывания:                │
│  ✗ «Ремонт ул. Карла Маркса» (другая улица)     │
│  ✓ «Ремонт ул. Чкалова заморожен» (бинго)       │
│                                                   │
│  Результат: 0–2 релевантных статьи               │
└─────────────────────┬────────────────────────────┘
                      │
                      ▼
┌──────────────────────────────────────────────────┐
│  АГЕНТ 3: ГЕНЕРАЦИЯ БЭКА                          │
│                                                   │
│  Если найдены релевантные статьи:                │
│  → Формулирует бэк в стиле издания               │
│  → Вставляет ссылки на предыдущие материалы      │
│  → 1–2 предложения, не больше                    │
│                                                   │
│  Если ничего не найдено:                         │
│  → Не генерирует бэк вообще                      │
│  → Лучше ничего, чем выдумка                     │
│                                                   │
│  Результат:                                       │
│  «Напомним, в октябре 2025 года ремонт            │
│   ул. Чкалова был заморожен из-за проблем        │
│   с подрядчиком (наш материал).»                  │
└──────────────────────────────────────────────────┘

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

Разделение на поиск → фильтрацию → генерацию позволяет на каждом шаге контролировать качество. Если агент поиска нашёл 5 кандидатов, а агент фильтрации отбросил все 5 — бэка не будет. И это правильный результат.

Embeddings: что работает, а что нет

Мы используем text-embedding-3-small от OpenAI. Не потому что это лучшая модель для русского текста (это не так). А потому что дёшево ($0.02 за миллион токенов), быстро и достаточно хорошо для нашей задачи.

Пробовали text-embedding-3-large. Разница в качестве для русского текста — процентов 5–7 по нашим тестам. Разница в цене — ×6,5. Не стоит.

Что мешает: русский текст с географическими названиями. Embeddings не различают «Гатчина» и «Гатчинский район». Для новостного контекста это критично — новость про город и новость про район могут быть совершенно разными историями. Поэтому агент поиска работает в два прохода: сначала извлекает сущности (NER) и фильтрует по точному совпадению, потом дополняет семантическим поиском.

Порог similarity: 0.82. Подобрали эмпирически на 500 парах «новость — бэк». Ниже 0.82 — слишком много мусора. Выше 0.87 — пропускаем релевантные статьи, где формулировки отличаются.

Проблема архива (она же — проблема курицы и яйца)

Бэки работают, когда у издания есть архив в векторной базе. Архив появляется, когда издание загружает статьи. Издание загружает статьи, когда видит результат. Результат видно, когда работают бэки.

Мы решаем это так: при подключении клиент загружает 100–300 статей для обучения стилю. Эти же статьи идут в векторную базу для бэков. Две задачи — одна загрузка.

В процессе работы каждый сгенерированный и опубликованный рерайт тоже попадает в архив. База растёт органически. Через месяц работы у издания, которое публикует 15 новостей в день, в базе уже 450+ текстов. Бэки становятся точнее с каждым днём.

Холодный старт: первые 2–3 недели бэков мало, потому что база тонкая. Мы не генерируем фейковые бэки, чтобы «показать возможности». Если релевантной статьи нет — бэка не будет. Лучше так, чем врать.

Зачем это вообще нужно (цифры)

Для читателя: бэк даёт контекст, без которого новость непонятна. «Возобновили ремонт» — ну и что? А «возобновили ремонт, который заморозили полгода назад после скандала с подрядчиком» — это история. По нашим тестам на трёх изданиях, средняя глубина прочтения статей с бэками — на 23% выше.

Для SEO: бэк содержит внутреннюю ссылку на предыдущий материал. Это перелинковка, которую так любят поисковики.

Для редактора: не нужно вспоминать «а мы об этом писали?» и лезть в архив. Система находит сама. Экономия — 3–5 минут на материал. При 15 материалах в день — час в сутки.

Для бизнеса: бэк со ссылкой — это повторный трафик на старый контент. Мы видели рост pageviews на 8–12% у изданий, которые системно делают бэки. Больше pageviews — больше показов рекламы.

Чего мы не делаем

Не выдумываем контекст. Если в архиве нет связанных статей — бэка не будет. Агент генерации получает только реальные статьи из базы или ничего.

Не лезем во внешние источники. Бэк ссылается только на материалы самого издания. «Напомним, мы писали...» — а не «напомним, кто-то где-то писал».

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

Итого: что получилось

Три статьи — три слоя системы:

  1. Стиль — декомпозиция на аспекты, профиль издания, генерация «как свой».
  2. Фактчек — агент-верификатор, сравнение с источником.
  3. Контекст — embeddings + RAG + три агента, бэки со ссылками на собственный архив.

ChatGPT не знает стиль издания. Не проверяет факты по источнику. Не помнит, что издание писало вчера. Потому что это не его задача. Его задача — сгенерировать текст по запросу. А задача инструмента для редакции — сгенерировать текст, который можно опубликовать. Разница — как между ножом и скальпелем. Режут оба, но в операционную с кухонным не пускают.


Статья написана с помощью AI-системы «Рерайт-Завод». Публикуется также на Habr.