Почему мы перестроили нормативные документы в граф, а не просто загрузили в векторную базу

Почему мы перестроили нормативные документы в граф, а не просто загрузили в векторную базу

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

Мы столкнулись с этим на практике, создавая систему для работы с нормативной базой. Сначала задача казалась стандартной: есть документы, есть вопросы — нужен RAG. Но вскоре стало ясно: главная проблема не в генерации. Она в представлении документа так, чтобы retrieval не разрушал его структуру и смысл.

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

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

Где ломается обычный RAG

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

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

Проблема проявлялась так:

  • пользователь спрашивает о конкретном требовании;
  • semantic search находит похожий по формулировке пункт;
  • но рядом есть подпункт, примечание или ссылка, без которых ответ становится некорректным;
  • модель видит только один фрагмент и строит ответ, как будто этого достаточно.

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

Тогда стало ясно: проблема не в «недоумной» модели. Проблема в слишком плоском представлении документа.

Первый поворот: документ перестал быть просто текстом

Вместо «документ = длинная строка» мы начали строить структурную модель. Основная идея: документ состоит не из абзацев, а из адресуемых элементов.

В нашей модели появились:

  • документ как корневая сущность;
  • узлы: разделы, пункты, подпункты;
  • таблицы как отдельные элементы;
  • формулы как отдельные элементы;
  • связи между узлами;
  • термины и их определения.

Сначала документ превращается в иерархию, а потом уже индексируется.

Это был первый важный шаг. Мы перестали индексировать «результат парсинга» и начали работать с нормализованной моделью документа.

Почему одного узла оказалось недостаточно

Даже с иерархией retrieval не стал автоматически хорошим.

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

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

  • увидеть соседний подпункт;
  • подтянуть примечание;
  • учесть таблицу;
  • проверить ссылки на другие документы или пункты.

Retrieval должен возвращать не просто близкий фрагмент, а минимально достаточный контекст.

Мы не стали делать generic chunking по токенам. Вместо этого группировали соседние элементы с общим родителем и уровнем вложенности. Так система понимала, что это логически связанные пункты в одном разделе.

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

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

Почему семантического поиска оказалось недостаточно

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

  • покажи пункт 6.2.2;
  • найди таблицу 7.1;
  • что означает термин «X»;
  • где описан параметр Y;
  • какие нормы связаны с этим пунктом.

Для этого одного vector search недостаточно. Нужны разные режимы retrieval:

  • точечный поиск по номеру пункта;
  • поиск по таблице;
  • лексический поиск;
  • гибридный поиск;
  • терминологический поиск;
  • поиск по кросс-ссылкам.

Retrieval перестаёт быть одной функцией и превращается в набор специализированных инструментов.

Почему один вопрос может запускать несколько поисков

Система не обязана ограничиваться одним способом поиска. Наоборот, один запрос может содержать несколько намерений:

  • упоминание конкретного пункта;
  • ссылка на таблицу;
  • часть запроса в свободной форме;
  • использование термина, требующего проверки в терминологическом слое.

В этом случае retrieval может:

  • выполнить точечный поиск по пункту;
  • параллельно проверить таблицу;
  • запустить keyword search;
  • обратиться к терминологическому слою;
  • объединить результаты в единый контекст.

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

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

Зачем понадобился слой терминов

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

Мы добавили отдельную обработку терминов:

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

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

Момент, когда стало ясно, что без графа не обойтись

Даже иерархия и термины не решили всех проблем.

Некоторые ответы зависят от жёсткой цепочки связей. Например:

  • пункт требует учёта другого пункта;
  • ссылка на внешний документ;
  • таблица актуальна только с описывающим её разделом;
  • ограничение находится не в основном тезисе.

Семантический поиск может найти основной фрагмент, но не гарантирует подтягивание обязательного контекста. Иногда он слишком далёк по смыслу, чтобы попасть в top-k. Иногда формально не релевантен, но без него ответ ошибочен.

Поэтому нам понадобился граф связей.

В нём у узлов есть отношения:

  • mandatory — узел обязательно учитывается вместе с другим;
  • cross — кросс-ссылка на другой фрагмент или документ;
  • связи документ → узел;
  • связи узел → таблица;
  • связи между документами.

Эти связи участвуют в retrieval. Если найден релевантный узел, система проверяет, нужно ли принудительно подтянуть связанные элементы.

В этот момент система перестаёт быть «чатом поверх документов» и становится инженерным инструментом.

Что это изменило на практике

Главное изменение — не в «умности» ответов, а в их устойчивости.

Система стала лучше справляться с задачами, где нужно:

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

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

Что мы из этого вынесли

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

В нашем случае это означало:

  • документ нельзя рассматривать как плоский текст — нужна адресуемая структура;
  • chunking должен учитывать смысловую иерархию, а не только длину;
  • retrieval должен быть многослойным: vector search полезен, но не заменяет точечный поиск, лексические сигналы, термины, связи и композицию стратегий;
  • обязательные связанные нормы должны быть выражены в данных явно — нельзя надеяться, что модель «сама догадается».

Мы начинали с идеи «построим RAG по документам». Но стало ясно: для нормативки этого недостаточно.

Чтобы система работала, пришлось пройти несколько этапов:

  • от плоских фрагментов к иерархическим узлам;
  • от одиночных чанков к структурным группам;
  • от одного векторного поиска к гибридному retrieval;
  • от простого индекса к терминологическому слою;
  • от «похожего текста» к графу обязательных и кросс-ссылок.

В какой-то момент это перестаёт быть «LLM поверх базы» и становится полноценной инженерной системой, где retrieval — не вспомогательная деталь, а ядро качества.

Именно это, как нам кажется, и отделяет демонстрационный RAG от рабочего инструмента по нормативным документам.

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