От 0.034 до 0.791 и обратно: Legal RAG, 17 итераций и стена масштабирования

От 0.034 до 0.791 и обратно: Legal RAG, 17 итераций и стена масштабирования

Участие в юридическом AI-челлендже ARLC 2026 стало погружением в RAG с нуля. Задача — построить пайплайн для поиска ответов в судебных решениях и законах DIFC, с извлечением точных ссылок. Участвовал соло, с Claude Code в качестве ассистента.

Начало: от 0.034 до 0.791 за 5 дней

Первая подача показала скор 0.034 и grounding всего 0.05. При этом точность ответов (Det) была высокой — 0.857. Но поскольку grounding — множитель, общий скор оставался близким к нулю.

Три попытки ушли на поиск ошибки. Проблема оказалась в четырёх символах: в doc_id случайно включалось расширение .pdf. После исправления — doc_id = filename.replace('.pdf', '') — grounding вырос с 0.05 до 0.55, а скор — до 0.438.

Урок: сначала валидируй формат вывода. Ни один ретривал или fine-tuning не поможет, если submission не соответствует ожидаемой структуре.

Архитектура пайплайна

Ingestion: парсинг PDF

Обрабатываются как цифровые, так и сканированные PDF. Решено индексировать на уровне страниц, а не чанков — потому что grounding оценивается по (doc_id, page_number). Проблема чанков — они могут пересекать границы страниц, что ломает маппинг.

Длинные страницы (до 1500+ символов) сжимаются с помощью context distillation — остаются только релевантные абзацы. Это экономит ~200 токенов на запрос.

Гибридный поиск: BM25 + embeddings + RRF

BM25 хорошо ловит точные совпадения (номера статей, дел), но слаб в переформулировках. Embeddings (all-MiniLM-L6-v2) — наоборот. Объединение через Reciprocal Rank Fusion (k=60) даёт синергию без нормализации скоров.

Индексация 300 документов занимает ~10 секунд, работает локально.

Document routing для comparison-вопросов

Вопросы вроде «В каком из дел CFI 001/2020 и CFI 002/2021 судья был назначен раньше?» требуют контекста из двух документов. Обычный ретривал может найти только один.

Решение: отдельный routing-индекс по первым двум страницам каждого документа. При обнаружении двух номеров дел — делается dual query, результаты чередуются (round-robin), чтобы оба дела попали в контекст даже при малом max_pages.

Cross-encoder reranking

Топ-30 страниц из гибридного поиска пересортируются с помощью cross-encoder (ms-marco-MiniLM-L-6-v2). Жёсткий лимит в 30 — из-за TTFT: 1500 пар «запрос-страница» могут занять несколько секунд, что вызывает штраф.

Ключевая фича — priority=True для важных страниц (например, определение статьи). Это гарантирует, что они не выпадут из контекста, даже если релевантность низкая.

Типизированные ответы: детерминированные fast-paths

Для 30–40% типизированных вопросов (числа, даты, булевы) ответ извлекается без LLM — через регулярные выражения и логику. Это экономит ~700ms на TTFT и 0 токенов на API.

Примеры:

  • Извлечение номера закона по шаблону DIFC Law No. X of YYYY
  • Сравнение дат или сумм — если оба значения найдены, LLM не нужен

Adversarial detection

Некоторые вопросы — ловушки: спрашивают о вещах, которых нет в DIFC-праве (например, присяжные). Ответ — null для типизированных, и стандартный fallback для free_text:

“There is no information on this question in the provided documents.”

Попытка заменить fallback на доменный текст («DIFC Courts do not use jury trials…») привела к падению Asst с 0.720 до 0.640 — gold ожидал именно generic-ответ.

Context distillation и LLM-обработка

Context distillation отбирает только релевантные абзацы из страницы. Среднее сжатие — с 1500 до 900 символов. Экономия — ~200 input-токенов.

Выбор модели

Haiku для типизированных и простых free_text. Sonnet — только для сложных free_text (30% вопросов). Выигрыш в Asst +0.10, что даёт +0.026 к общему скору при G=0.86. Потери на TTFT — ~0.009. Чистый прирост — +0.017.

Type-specific промпты

Каждый тип — отдельный промпт. Например, для чисел добавлено: «NOT the article number» — чтобы LLM не отвечал номер статьи вместо значения.

Chain-of-Thought для сравнений

Для сравнительных булевых вопросов — CoT: сначала извлечение фактов, потом сравнение. Снижает ошибки на ~15%.

Парсинг цитат

LLM возвращает индексы релевантных блоков (страниц). Они проверяются на точность — чтобы не было галлюцинаций.

Post-LLM verification: ловим ошибки

LLM может:

  • Ответить числом из другого контекста
  • Смешать номер статьи с её значением («What is the notice period under Article 14?» → 14 вместо 30)
  • Сказать «true», даже если DIFC Law не действует в Лондоне

Все эти случаи перехватываются post-LLM проверками.

Evidence-based grounding: главный компонент

Grounding — множитель. Если G=0, весь скор=0. Система выбора страниц — трёхуровневая:

Уровень 1: Article index pages

Для вопросов про статьи — сначала ищется страница, где статья определена (заголовок «Article 14»), а не просто упомянута. Это дало +0.065 к G (v12→v13).

Уровень 2: LLM citations (CITE)

Если article index не дал результата — используются цитаты из ответа LLM.

Уровень 3: Evidence verification

Финальная проверка: содержит ли страница и ответ, и ссылку на статью? Такая страница — почти наверняка gold.

Smart article continuation

Если статья разбита на две страницы — следующая добавляется только если она продолжает ту же статью (проверка по заголовку). Иначе можно попасть в Article 15. Это дало +0.037 к G (v13→v14).

Page caps по типам

Ограничения по количеству страниц в grounding:

  • boolean (не comparison) — максимум 2
  • comparison — до 4

Опыт показал: меньше страниц — выше G. v11: увеличение лимита до 4 дало +43% страниц, но G упал на 0.059. v14: уменьшение до 2 — G вырос на 0.037.

Математика grounding: β=2.5 под микроскопом

Формула F-beta с β=2.5 взвешивает recall в 6.25 раз сильнее precision. Кажется — добавляй больше страниц. Но практика показала обратное.

Сценарии

Пропущена золотая страница (Gold=2, ты цитируешь 1): G падает на 74%. Одна пропущенная — катастрофа.

Лишняя страница (Gold=1, ты цитируешь 2): G падает на 12%.

Две лишних (Gold=1, ты цитируешь 3): G падает на 22%.

Вывод: precision > volume. Лучше недоцитировать, чем перецитировать.

История итераций: 17 версий, 3 регрессии

Ключевые изменения:

  • v4: исправление .pdf — G ×11
  • v6: гибридный поиск — +0.089 G
  • v7: adversarial detection — +0.129 Det
  • v10: evidence overhaul — +0.044 G
  • v13: article index first — +0.065 G
  • v14: smart continuation + caps — +0.037 G

Три регрессии — три урока

v9: добавление соседних страниц — G упал на 0.098. Следующая страница часто — другая статья.

v11: больше страниц = лучше? Нет. +43% страниц → G упал на 0.059. Шум перевесил сигнал.

v15: доменные override-ответы — Asst упал на 0.080. Gold ожидал generic-текст.

Что работает, а что нет

Что работает

  • Page-level retrieval (не чанки)
  • Гибридный BM25 + embeddings (+0.089 G)
  • Article index first, CITE second (+0.065 G)
  • Evidence verify (answer + article ref) (+0.044 G)
  • Smart article continuation (+0.037 G)
  • Adversarial detection (+0.129 Det)
  • Post-LLM verification (+0.042 Det)
  • Type-specific page caps (+0.037 G)
  • Deterministic fast-paths (+0.03 F)
  • Context distillation (экономия токенов)

Что не работает

  • Больше страниц в grounding — шум > сигнал
  • Adjacent page retention — ловит чужие статьи
  • Domain-specific fallback — gold ожидает generic
  • Post-LLM boolean overrides — например, «Assistant Registrar» ≠ judge
  • Изменение free_text промпта — Asst падает
  • Sonnet для boolean — «рассуждает», а не извлекает
  • Batch-изменения — невозможно отследить, что помогло

Работа с Claude Code: ускорение и компромиссы

Весь код (~3000 строк) написан с помощью Claude Code. 17 итераций за 5 дней — без него было бы 3–5.

Преимущества

  • Скорость: 3 минуты на итерацию против часа вручную
  • Рефакторинг при изменениях архитектуры
  • Память: хранит контекст, знает историю
  • Автоматическое версионирование и changelog
  • Код-ревью перед подачей
  • Исследование подходов: сам предлагал RRF, cross-encoder, distillation

Где нужен человек

  • Приоритизация: Det или G?
  • Знание evaluation protocol — например, что «Assistant Registrar» не считается судьёй
  • Интерпретация провалов — v11: решение было не «добавить страниц», а «убрать шум»

Компромисс: скорость ×5–10, но понимание кода хуже. Баг с .pdf мог быть найден раньше.

Финал: стена масштабирования

Warmup: 30 документов, 100 вопросов. Финал: 303 документа, 900 вопросов, 2 попытки.

Первая подача (F-v1): 0.457 — падение на 42%.

Причины

  • Retrieval dilution: документ «DIFC Courts Rules» (537 страниц) загрязняет результаты по юридическим терминам
  • Disambiguation failure: «Consultation Paper No. 3» — 7 документов, BM25 выбирает по плотности терминов
  • Law number regex: ловит вопросы про штрафы, возвращает номер закона вместо суммы (~27 ошибок)
  • 93 zero-page ответа: из них 89 — adversarial free_text с пустыми цитатами
  • Case number leakage: LLM возвращает номер дела вместо количества истцов
  • 2 пустых документа: сканы без OCR

F-v2: 8 фиксов, -0.008 к скору

Исправления:

  1. Law number guard — исключить вопросы со словами fine/fee/penalty
  2. Free_text zero-page — цитировать top-1 страницу для adversarial
  3. Case number leakage — post-LLM проверка
  4. Consultation paper disambiguation — matching по названию
  5. Document diversity — максимум 5 страниц на документ
  6. OCR — pytesseract для пустых PDF
  7. Party count boost — Sonnet и max_pages=5
  8. CP routing — titles в routing-индекс

Результат: Det и Asst выросли, но G упал. Причина — добавление цитат в adversarial-ответы. Gold ожидает пустые страницы. Любая цитата = шум → падение precision → падение G.

Одно неверное предположение оценки стоило дороже, чем 7 правильных фиксов.

Уроки

Для warmup

  • Сначала — валидация формата. Unit-тест на submission.
  • Одно изменение — одна подача. Иначе нет ablation.
  • Собери eval-сет. Даже 10 вопросов с ground truth сэкономят подачи.

Для финала

  • Подай v14 как baseline. Не трать первую попытку на отладку.
  • Иерархический retrieval. Сначала определи документ, потом ищи в нём. Flat search не масштабируется.
  • Протестируй evaluation protocol. Один тест с пустыми цитатами показал бы, что G=1.0 при пустом списке.
  • Синтетический scaling test. Дублируй warmup-документы — проверь, как падает retrieval.

Главные выводы

  1. Grounding определяет всё. G — множитель. Даже идеальные ответы не спасут при G=0.
  2. Precision > recall, даже при β=2.5. Каждая лишняя страница — −10–22%. Лучше недоцитировать.
  3. Domain guardrails бьют general intelligence. «Assistant Registrar» ≠ judge. Эти правила видны только из результатов.
  4. Prompt engineering хрупок. Изменение формулировки — Asst −0.080. «Если работает — не трогай».
  5. Масштаб всё меняет. Pipeline с 30 документов теряет 42% на 300. Retrieval — фундамент.
  6. Evaluation protocol — часть задачи. Неверное предположение о G для edge cases — фатально.

Затраты: 88 USD (API), 5 дней. Код — на GitHub. Результаты финала — в оценке. Но путь от 0.034 до 0.791 — уже победа.

Читать оригинал