Блог

RAG для англоязычного права перенесли на русское. Что сломалось

10 мин

Пайплайн, собранный под англоязычный корпус DIFC, на российских НПА развалился в четырёх местах: токенизация номеров законов, BM25, OCR и границы чанков. Разбор с замерами на воспроизводимом стенде.

Пайплайн переносится на русский примерно наполовину. Архитектура — постраничный ранкинг, гибридный поиск, реранкер, структурированный вывод — переезжает без единой правки. Ломается всё, что касается языка: токенизация номеров законов, BM25-слой, обязательность OCR и границы чанков. Ниже — что именно, с цифрами со стенда.

Откуда данные

Мы участвовали в соревновании по RAG на корпусе права Дубайского международного финансового центра (DIFC) на платформе agentic-challenge.ai. Корпус английский, около 590 страниц.

Результат — 24 место из 355 команд, финальный скор 0.578. Не победа, но верхние 7% — и, что важнее для этой статьи, вся наша специализация до этого была русскоязычной. Именно поэтому перенос обратно на российское право оказался таким показательным: мы точно знали, как пайплайн ведёт себя на английском, и могли отделить языковые проблемы от архитектурных.

Задача: на входе вопрос по корпусу, на выходе JSON с ответом, флагом «можно ли ответить» и списком страниц-источников. Два класса вопросов — deterministic (boolean, число, имя, дата; сверяются exact match) и free_text (что решил суд, в чём суть нормы).

Метрика составная, и её устройство определило стратегию:

Total = S × G × T × F
  S = точность ответа (детерминированные + свободный текст)
  G = F-beta, β = 2.5, по страницам  (grounding)
  T = наличие телеметрии
  F = коэффициент за скорость первого токена

β = 2.5 означает, что recall по страницам весит сильно больше precision. Добавить лишнюю страницу почти бесплатно, потерять правильную — дорого. Дальше мы это использовали везде.

Стек на соревновании: text-embedding-3-large (1024d), rank_bm25, FAISS IndexFlatIP, реранкер BAAI/bge-reranker-v2-m3, Claude Sonnet 4 на deterministic и Gemini Flash / GPT-4.1 на free_text.

После соревнования мы перенесли этот пайплайн на российское право — уже как продукт.

Что перенеслось без единой правки

Это скучная, но важная часть: архитектурные решения оказались языконезависимыми.

  • Постраничный ранкинг. Одна страница PDF — один чанк. Основа точного цитирования: модель ссылается на страницу, которую можно открыть и проверить.
  • Гибридный поиск dense + BM25 со слиянием через RRF.
  • Реранкер поверх объединённой выдачи, а не поверх каждой ветки отдельно.
  • Структурированный вывод через function calling вместо парсинга JSON из текста.
  • Детерминированный постпроцессинг вместо надежды на промпт.
  • Мета-индекс для прямого lookup по номеру статьи или дела.
  • Пиннинг конкретного провайдера модели после успешного прогона.

Что сломалось на русском

Токенизация — главная неочевидная проблема

Стандартный токенайзер режет «44-ФЗ» на «44» и «фз». После этого BM25 плывёт: запрос «что в 44-ФЗ про закупки» матчит любой документ, где встречается число 44.

В английском аналога нет — «Section 44» так не разваливается, потому что номер и слово изначально разделены пробелом и не образуют единого токена-идентификатора.

Решение: детектим в запросе номер закона регуляркой и ставим точный keyword-фильтр по полю law_number, а не надеемся на BM25. Две оговорки, без которых оно ломается:

  • фильтр ставится только при однозначности. «Сравни 44-ФЗ и 223-ФЗ» — не ставится;
  • если фильтр дал ноль результатов — повтор без него. Иначе recall проседает на опечатках и редких номерах.

BM25-слой переписан целиком

rank_bm25 на английском → Elasticsearch с собственным русским анализатором. Что в нём:

  • char-фильтр ё → е. В законах вперемешку «зачётный» и «зачетный»; без нормализации это разные токены, и половина запросов по «рассрочкам» не находит «рассрочек».
  • Snowball russian stemmer.
  • Кастомный стоп-лист = стандартный русский минус не, без, до, после. Это не косметика: удаление этих слов меняет правовой смысл нормы, а стандартный список их выбрасывает.
  • Синонимы только на search-time: гк → гражданский кодекс, ооо → общество с ограниченной ответственностью, ст → статья. Не раздувают индекс и правятся без переиндексации.

OCR стал обязательным

Русские сканы — решения Верховного суда — без OCR просто выпадали из индекса. Не «искались хуже», а отсутствовали.

Ввели инвариант: ноль нормативных документов с нулём чанков, и проверяем его автоматическим аудитом корпуса. Включение OCR вернуло 19 решений ВС, которых в индексе не было вообще.

Типология источников

Российская правовая система различает авторитетные источники (НПА) и рекомендательные (практика, доктрина). В DIFC прямого аналога не было.

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

Границы чанков, а не их размер

Ожидаемый вопрос — «какой оптимальный размер чанка для русского». У нас на него нет ответа: мы режем не по размеру, а по странице.

Русская специфика оказалась в границах:

  • разрыв страницы в PDF — физический артефакт, он регулярно попадает в середину предложения. Чанк дотягивается в начало следующей страницы до первой настоящей точки, чтобы не эмбеддить обрубок вида «…при условии, что сми»;
  • список сокращений, после которых точка не является концом предложения (ст., п., г., руб.), пришлось собирать под юридический корпус отдельно;
  • статья кодекса регулярно разорвана между страницами — отсюда постатейная сборка поверх страничного индекса.

Единственный измеренный рычаг, связанный с объёмом: бюджет контекста 10 000 → 14 000 токенов дал +3.4 п.п. попадания нормы в контекст.

Три вещи, которые сработали лучше ожидаемого

Постатейная сборка (small-to-big). Ищем по страницам, а в модель отдаём целую статью, а не обрубок страницы. Целостность статьи выросла с 0.15 до 0.891 на тестовой выборке и с 0.29 до 0.882 на отложенной. Попадание в цель при этом не просело, а слегка выросло: 0.949 → 0.974.

Честный отказ при слабых источниках. Один порог по скору топ-результата, откалиброванный по Юдену. Доля корректных отказов на негативных вопросах выросла с 0.09 до 0.73 на тесте и с 0.17 до 0.67 на отложенной — при этом ни один позитивный вопрос не начал отказываться. Соотношение цены и эффекта лучшее из всего, что мы делали: это изменение одной константы в конфиге.

Нормализация ё → е. Двухстрочный char-фильтр.

Что не сработало, хотя по статьям должно было

Постатейная сборка «в лоб» активно вредила

Три подхода подряд, все на тестовой выборке при базовой линии 0.949:

| Подход | Попадание нормы в контекст | |---|---| | Заменить страницы на статьи глобально | 0.526 | | Только для кодексов, с откатом на страницы | 0.795 | | Топ-1 статья | 0.641 |

Мы честно записали «small-to-big не работает» и пошли дальше.

Оба вывода оказались артефактами наших собственных ошибок.

Первая: мерили не ту метрику. Попадание в контекст слепо к целостности статьи — оно не видит, что статья подана рваной.

Вторая, и она интереснее: разворачивали статьи по списку элементов из парсера, а он смешивает страницы тела статьи со страницами, которые её просто цитируют. У ст. 395 ГК в элементах числились страницы 223, 342, 375, 440 — а заголовок статьи только на 223. То есть прототип раскрывал не те статьи.

Чистый детект по заголовкам «Статья N» в самом тексте плюс новая метрика целостности перевернули вывод на противоположный.

Мораль, ради которой стоило это писать: отрицательный результат в RAG почти всегда надо сначала проверить на дефект метрики, а уже потом ему верить.

Структурные фиксы парсера оказались нейтральны для поиска

Починили извлечение заголовков, вычистили редакционный шум, поправили классификацию. Ожидали роста качества поиска. Получили 0.932 → 0.915 — в пределах шума и скорее вниз.

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

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

Глобальный порог не разделяет коллизии, и это доказуемо

Есть класс негативных вопросов про несуществующие в корпусе законы, номер которых совпадает с существующим. Распределения скоров у них инвертированы: у коллизии 0.927 против медианы 0.926 у настоящих попаданий.

Любой порог, который отсекает коллизию, убивает recall. Это не «надо подобрать порог получше» — это «порогом в принципе не решается». Нужен отдельный entity-gate по номеру и токенам названия.

Сложный промпт ухудшил результат

XML-теги, workflow, self-check: 0.805 у простого промпта против 0.775 у сложного.

ColBERT не дал ничего

На 590 страницах результаты идентичны FAISS + BGE. На малом корпусе любой нормальный ретривер находит нужное.

Провайдерская маршрутизация эмбеддингов ненадёжна

OpenRouter не сортирует эмбеддинги по реальной латентности: sort:latency залипает на одном провайдере, order не соблюдается при включённых fallbacks. Плюс провайдеры деградируют за минуты — у нас Nebius прошёл путь 45 мс → HTTP 403 примерно за 10 минут.

Пришлось самим пинговать кандидатов однотокенным запросом и пинить быстрейшего живого, с перепроверкой по таймеру и мгновенно при отказе. Латентность запроса: 3994 мс → 707 мс.

Почему у нас нет графа

Ожидаемый вопрос, раз речь о праве: где Graph RAG.

Его нет, и это осознанно. Класс задач, где граф выигрывает, узкий и реальный — отсылочные и бланкетные нормы:

Запрос «ответственность поставщика за недопоставку» — поиск находит ст. 511 ГК (восполнение недопоставленного). Но сама статья говорит: покупатель вправе отказаться в порядке ст. 523. Без ст. 523 ответ неполный, а по векторной близости она в топ не попадёт — там другая лексика.

Ключевое: бо́льшую часть этого эффекта берёт одношаговое раскрытие ссылок, а не граф. Цель ссылки хранится в том же формате, что и список элементов чанка, поэтому раскрытие — это скалярный фильтр в векторной базе. Без графовой СУБД, без обхода и без переэмбеддинга: метаданные обновляются на месте. Реранкер потом сам отсеет нерелевантное.

Полноценный граф нужен там, где нужен обратный и многошаговый обход: «какие статьи ссылаются на ст. 15», цепочки ст. 506 → ст. 523 → ст. 450. Сценарии реальные, но редкие.

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

Как это мерилось

Без этого раздела все цифры выше — разговор.

Стенд: корпус 109 документов / 9082 чанка. Эталонный набор 122 вопроса: 105 с ответом и 17 негативных. Разбит на тюнинговую выборку 89 и замороженную отложенную 33. Все решения валидировались на отложенной, а не на той, по которой крутили ручки.

Дневник из 15 записей с точками отката — каждое решение откатывается одной командой. Версионирование окупилось: одну версию мы отреверсили как чистый размен.

Что означают метрики:

  • попадание нормы в контекст — дошёл ли дословный текст правильной нормы до модели. Отличается от recall@k: цель может быть найдена поиском, но срезана бюджетом контекста, и recall этого не покажет;
  • целостность статьи — доля случаев, когда модель получила статью целиком, а не фрагмент;
  • корректные отказы на негативах — доля вопросов без ответа в корпусе, где система честно отказалась вместо правдоподобной выдумки.

Что бы делали заново

Порядок, в котором стоит собирать такую систему. Граф — не первая фаза, а последняя:

  1. корректная доменная обработка документов и постатейная сборка;
  2. гибридный поиск с квотами по типу источника плюс реранкинг;
  3. проверка цитат после генерации;
  4. стенд с эталонным набором и замороженной отложенной выборкой — до любых оптимизаций. Иначе не отличить улучшение от шума, мы это прошли на своей шкуре;
  5. одношаговое раскрытие ссылок скалярным фильтром;
  6. и только если пятое упрётся в потолок — граф. Хранилище наращивать по порядку: рекурсивные запросы в PostgreSQL → NetworkX в памяти → отдельная графовая СУБД только на масштабе от миллиона рёбер.
ragnlpпоиск
Автор
Миргалим Абитов
Ведущий разработчик

Собираю поисковые системы по корпусам документов: гибридный retrieval с реранкингом, доменная обработка PDF, постатейная реконструкция нормативных актов, обязательное цитирование с автоматической проверкой ссылок против источника. Отдельно — воспроизводимые стенды оценки качества поиска: без замеров на замороженной выборке улучшение неотличимо от шума. Помимо RAG — высоконагруженные сервисы и ML. 24 место из 355 команд в соревновании по RAG на англоязычном корпусе права DIFC.

Решаете похожую задачу? Расскажем, как это устроено у нас, и оценим применимость к вашему случаю.

Посмотреть услугу