Служебный e-mail вместо рекламной СМС: как мы проверяем разбор решений ФАС

от автора

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

Ссылка на сам проект будет в конце, если кому-то интересно лично ознакомиться.

Во время ручной проверки одной из партий мы встретили решение ФАС, в котором были сразу две правдоподобные подсказки о канале связи. Это дело № 035/05/18-40/2025.

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

Ответ выглядел аккуратно. Цитата существовала в документе. JSON соответствовал схеме. Только карточка дела сообщала не о той рекламе.

Этот false positive хорошо описывает проблему document AI: получить валидный ответ значительно проще, чем получить факт, который можно публиковать. В статье покажем, как мы разделили сбор решений ФАС, извлечение текста, LLM-разбор, детерминированные проверки и ручное решение о публикации. Подход пригодится тем, кто превращает неоднородные документы в структурированные данные и не может позволить модели быть убедительной вместо точной.

TL;DR

  • Pipeline скачать PDF → отправить в LLM → записать JSON не имеет независимой точки контроля и поэтому не годится для публичной базы.

  • Мы просим модель возвращать не только значение, но и дословную цитату; затем отдельно проверяем наличие цитаты в исходном тексте.

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

  • Повторный проход запускается только для подозрительных полей, а не для всего документа.

  • Обработанная партия получает статус awaiting_review. Публичные данные меняются только после ручного approve.

Что именно мы пытались собрать

В публичной базе ФАС есть карточки дел, документы внутри дела и прикреплённые файлы. Для карты практики по статье 18 Закона о рекламе одной карточки недостаточно.

Нам нужны как минимум:

  • исход дела;

  • канал спорной рекламы: СМС, e-mail, звонок, мессенджер или push;

  • конкретный рекламируемый продукт;

  • рекламодатель, рекламораспространитель и другие участники в их фактических ролях;

  • дата распространения рекламы;

  • доказательства согласия, на которые ссылалась сторона, и оценка этих доказательств комиссией;

  • точные фрагменты решения, подтверждающие каждое поле.

На 3 августа 2026 года после технической дедупликации в двух исходных фильтрах было 4 683 уникальные карточки. База ФАС пополняется, документы становятся доступными не одновременно, часть вложений не содержит пригодного текста. Поэтому в интерфейсе мы отдельно показываем все индексированные дела, дела с решением и опубликованные разборы.

Почему карточка дела и решение — разные сущности

В карточке можно увидеть номер, территориальное управление, даты и список материалов. Но вывод о нарушении, роль участника и оценка согласия находятся в самом решении. Иногда в виде текста, иногда в виде текстового файла (pdf, docx).

Есть ещё третий слой, который легко смешать с первыми двумя: административное дело и назначенный штраф. Решение ФАС по рекламе не доказывает само по себе, что штраф был назначен или взыскан. Поэтому мы сохраняем решение и расчётный диапазон возможной санкции как разные факты и не пишем «компания оштрафована», если отдельного источника об этом нет.

Если объединить сущности в одно поле case, система начнёт приписывать документу факты из другого процесса.

Наивная архитектура, которая почти работает

Первый вариант очевиден:

карточка ФАС → вложение → текст → LLM → JSON → публичная карта

На нескольких чистых решениях он действительно работает. В документе прямо написано «направлено СМС-сообщение», рядом приведён текст рекламы, а в конце комиссия формулирует вывод. Модель возвращает нужные поля, демонстрация выглядит убедительно.

Проблемы начинаются на документах, где:

  • e-mail используется для извещения стороны, а реклама пришла по СМС;

  • в одном тексте встречаются рекламодатель, оператор связи, технический отправитель и рекламируемый бренд;

  • дата договора, дата регистрации дела и дата рекламы написаны одинаковым форматом;

  • общая норма закона о предварительном согласии соседствует с описанием реального доказательства;

  • название компании отредактировано или обезличено;

  • текст PDF извлечён с переносами, повторяющимися колонтитулами и разрывами слов.

LLM видит все слова, но не обязана правильно восстановить их процессуальные роли. А строгая JSON-схема гарантирует только форму ответа.

Pipeline, который получился у нас (на текущий момент)

После нескольких циклов QA схема стала такой:

Схема 1. Красная линия отделяет готовый машинный разбор от данных, которые команда разрешила публиковать

Схема 1. Красная линия отделяет готовый машинный разбор от данных, которые команда разрешила публиковать

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

Шаг 1. Собираем оба фильтра и не считаем пересечение дважды

В базе ФАС дела по нашей теме встречаются как минимум в двух выдачах: «Статья 18» и «Часть 1 статьи 18». Одна карточка может попасть в обе, потому что (крайне вероятно) загружется это все вручную сотрудниками ФАС.

Сначала мы сохраняем принадлежность к каждому фильтру, а не уничтожаем её при объединении. Затем ищем точные дубли по тройке:

нормализованный номер дела+ территориальное управление+ дата регистрации

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

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

Шаг 2. Извлекаем текст, но сохраняем исходный файл

Решения опубликованы в PDF, DOC, DOCX и иногда TXT. Мы ограничиваем размер вложения, сохраняем файл атомарно и только затем извлекаем текст:

  • PDF — через pdftotext с сохранением расположения;

  • DOC — через antiword;

  • DOCX — из word/document.xml;

  • TXT — как обычный UTF-8.

Текст нормализуется, но исходное вложение остаётся рядом. Это позволяет повторить извлечение другим инструментом и проверить место, где потерялась таблица или сломался перенос.

Документ с несколькими десятками символов не отправляется модели как полноценное решение. Он получает отдельный статус «текст недоступен».

Этот принцип пригодился и в других местах: отсутствие данных рассматривается как состояние pipeline, а не значение по умолчанию.

Шаг 3. Просим факты вместе с доказательствами

Модель возвращает структуру примерно такого вида:

{  "outcome": {    "value": "violation",    "quote": "Признать СМС-рекламу ... ненадлежащей"  },  "channels": [    {      "value": "sms",      "quote": "Признать СМС-рекламу"    }  ],  "consentEvidence": [    {      "value": "Предварительное согласие не подтверждено",      "assessment": "missing",      "quote": "без предварительного согласия абонента"    }  ]}

Цитата должна быть непрерывным фрагментом исходного текста. После ответа мы нормализуем пробелы, кавычки и ё/е, а затем независимо проверяем, что цитата действительно присутствует в документе. Не найденная цитата удаляется вместе с фактом.

Это не защищает от неправильной, но настоящей цитаты. Служебный e-mail как раз существовал в источнике. Однако проверка цитат отсекает другой класс ошибок: пересказ, склейку двух фрагментов и придуманное моделью подтверждение.

Шаг 4. Проверяем инварианты вне модели

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

Например:

  • дата рекламы не должна быть позже даты регистрации дела;

  • дата договора связи не должна становиться датой спорного сообщения;

  • ИНН извлекается из цифр после явной метки, а не из ОГРН;

  • сайт, упомянутый только для отправки или отслеживания документа, не становится рекламируемым брендом;

  • общая цитата статьи 18 не считается доказательством отсутствующего согласия;

  • при прямой формулировке «признать СМС-рекламу» каналом должна быть СМС, даже если выше встречалась электронная почта.

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

Тот самый false positive: e-mail победил СМС

В исходном решении рядом находятся три коротких фрагмента:

«путём направления 09.11.2024 г. СМС – рекламы»«на его адрес электронной почты, указанный в ЕГРИП»«РЕШИЛА: Признать СМС – рекламу»

Мы убрали из примера имя, адрес и содержание рекламного сообщения: для воспроизведения ошибки они не нужны. Regression-тест сохраняет тот же конфликт и отдельно добавляет обезличенного предпринимателя в роли участника, чьи действия проверяла комиссия.

Черновой ответ:

{  "channels": ["email"],  "entityRole": "other"}

Проверенный ответ:

{  "channels": ["sms"],  "entityRole": "respondent"}
Схема 2. Обе цитаты существуют в документе, но описывают разные процессы

Схема 2. Обе цитаты существуют в документе, но описывают разные процессы

Почему модель ошиблась? Фраза про e-mail находится в чистом повествовательном предложении и буквально описывает направление документа. А формулировка комиссии одновременно содержит канал, объект рекламы, дату, отсутствие согласия и итог. Без явного приоритета модель выбрала ближайший узнаваемый канал связи, хотя это был канал процессуальной коммуникации.

Исправление состоит из двух частей.

Первая — выделять резолютивную часть и проверять прямую конструкцию Признать СМС-рекламу. Если она есть, значение и цитата берутся оттуда.

Вторая — нормализовать роль лица по формулировке ФАС. Конструкция «лицом, в действиях которого содержатся признаки нарушения, установлен…» не должна оставлять участника в нейтральной роли other.

После исправления мы добавили regression-тест, в котором одновременно присутствуют служебный e-mail, СМС-реклама и обезличенный предприниматель. Отдельные тесты на email и sms не поймали бы конфликт приоритетов.

Здесь для нас важен общий вывод: тестировать нужно не словарь сущностей, а совместное присутствие конкурирующих правдоподобных фактов.

Шаг 5. Второй проход получает только спорные поля

Если автоматические проверки находят противоречие, мы не просим другую модель перечитать документ «внимательнее» целиком. Система определяет поля, требующие разбора: например, channels, consentEvidence и entities.

Во второй запрос попадают:

  • текущий черновик;

  • резолютивная часть;

  • фрагменты вокруг тематических маркеров;

  • перечень только спорных полей;

  • более жёсткие определения канала, продукта и доказательства согласия.

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

Повторный запрос всё равно не считается арбитром. Он лишь создаёт ещё одно проверяемое предложение.

Шаг 6. Партия — единица контроля

Решения обрабатываются партиями с лимитами:

  • максимальное число документов;

  • общий бюджет токенов;

  • не более двух попыток на документ;

  • предел ошибок;

  • предел времени выполнения.

Перед каждым документом pipeline резервирует ожидаемый бюджет. При достижении лимита партия завершается контролируемо.

Для ручной проверки выбираются:

  • все ошибки и неопределённые исходы;

  • результаты с низкой уверенностью;

  • документы с повторной попыткой или дорогим разбором;

  • детерминированная выборка успешно обработанных дел.

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

Почему awaiting_review важнее красивого confidence

У партии нет перехода analysis completed → published. После обработки она попадает в awaiting_review.

Схема 3. Ручное решение как отдельный переход состояния

Схема 3. Ручное решение как отдельный переход состояния

Псевдокод границы выглядит так:

const draft = await analyzeBatch(documents)const qa = buildQaReport(draft)saveDraft(draft, { status: "awaiting_review" })if (humanDecision === "approve") {  publishCompletedItems(draft)} else {  keepPublicDataUnchanged()}

В production это видно снаружи. На момент среза восемь решений имели статус «разбор готов и проверяется перед публикацией». Они не учитывались в 1 553 опубликованных разборах.

Мы сознательно не используем confidence модели как кнопку публикации. high означает оценку самой модели внутри заданного контекста. Статус approved означает, что результат прошёл процесс, за который отвечает команда.

Что мы пока не автоматизируем полностью

Роль организации

Одна компания может быть оператором связи, рекламораспространителем, рекламодателем, рекламируемым брендом или просто участником переписки. Совпадение названия недостаточно для вывода о роли.

Доказательство согласия

Фраза «реклама допускается при наличии предварительного согласия» — норма закона. Анкета, договор, скриншот или запись из личного кабинета — доказательство, представленное стороной. Ещё отдельно нужно понять, приняла ли его комиссия. Три сущности нельзя свести к одному булевому consent: false.

Факт штрафа

Мы можем показать применимый диапазон по типу субъекта и дате, но решение ФАС не подтверждает вынесение постановления по КоАП. Поэтому система не превращает потенциальную санкцию в «назначенный штраф».

Неполный источник

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

Что можно забрать в любой document-AI pipeline

Наш текущий чек-лист выглядит так.

  1. Храните исходный файл и производный текст отдельно.

  2. Определите сущности источника до подключения LLM.

  3. Просите модель возвращать значение вместе с минимальной дословной цитатой.

  4. Проверяйте цитату независимо от модели.

  5. Формализуйте приоритеты разделов документа.

  6. Добавляйте regression-тест на совместное присутствие конкурирующих фактов.

  7. Перезапускайте только подозрительные поля.

  8. Ограничивайте партию по времени, ошибкам и бюджету.

  9. Храните черновой результат отдельно от публичного.

  10. Делайте человеческое решение явным состоянием системы.

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

Мы продолжаем добавлять решения в карту практики ФАС. У каждой опубликованной карточки можно открыть исходные материалы и проверить, на чём основан разбор.

Если вы строили похожий pipeline, то буду признателен за обратную связь и если подсветите ваши лайфхаки. Например, какое состояние или правило остановки лучше всего защищало вас от убедительного, но неправильного результата модели?

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