
Принципиальные ограничения классического векторного поиска при проектировании и развертывании AI‑систем в сложных доменах заставляют разработчиков искать новые архитектурные решения. Одним из таких решений является поиск по графу знаний.
Возможности и ограничения графов знаний в юридическом домене, а также обзор основных фреймворков для построения RAG‑систем, основанных на графах знаний, подробно разобрал ранее в статье: Графы знаний в юридическом домене: как не потерять сложность при построении RAG‑системы (далее — Первая часть).
Рекомендую с ней предварительно ознакомиться. Здесь приведу лишь ключевые выводы, к которым мы пришли по итогам сравнительного анализа:
RAG‑система с правильно настроенным модулем векторного поиска работает как хорошо организованная справочная система, или, образно говоря, как библиотека. Опытный библиотекарь способен мгновенно найти несколько подходящих книг даже по «размытому» описанию обложки, если в описании есть хоть какой‑то смысл или значимые ключевые слова. Такая организация поиска эффективна для быстрого нахождения релевантных фрагментов текста, но не раскрывает взаимосвязей между данными.
Графовый поиск (при правильной организации) позволяет создать полноценную аналитическую систему, которая дает возможность учитывать разнообразные связи в корпусе данных. Например, если граф построен в результате обработки корпуса юридических документов, такая система может учитывать, как условие из одного пункта договора влияет на исполнимость другого договорного условия; прослеживать связь этого условия с внутренними регламентами компании; точно определять, какая редакция закона действовала на момент совершения сделки.
Вместе с тем на текущем этапе необходимо учитывать и существенные ограничения подхода, основанного исключительно на графах знаний:
Сравнительно высокая стоимость автоматической индексации данных.
Зависимость качества построенного графа от качества LLM, обрабатывающей источники данных.
Критическую важность корректных предварительных настроек фреймворков для создания хранилища данных — KGDB (Knowledge Graph DataBase).
Итоговый вывод, к которому мы пришли: в текущих условиях, если вы хотите создать аналитическую систему, основанную на вашей базе знаний, при построении и развертывании RAG‑систем в юридическом домене целесообразно использовать гибридную архитектуру, совмещающую графы знаний с элементами векторного поиска. Такой подход позволяет объединить скорость и (семантическую) гибкость векторного поиска с глубиной анализа данных, структурированных в виде графа.
В этой статье разберем конкретный кейс построения RAG‑системы в сфере недвижимости с помощью фреймворка LightRAG, который основан на гибридной архитектуре.
Кто я?
Меня зовут Сергей Слепухин. Прикладной лингвист и DS с фокусом на юридическом домене: исследую правовую область знаний и решаю практические задачи в этой сфере.
План статьи
кратко разберём архитектуру LightRAG;
покажем три сценария развёртывания фреймворка «из коробки» и границы декларативной настройки;
опишем эксперимент: цели, корпус, технический стек, запуск и индексацию в ноутбуке с доменной спецификой;
проведём формальный топологический анализ построенного графа средствами теории графов и выявим, почему «коробочный» результат текущими средствами требует оптимизации для выбранного домена.
Приведенные в заголовке характеристики являются сравнительными и отталкиваются от «тяжелого» предшественника LightRAG — GraphRAG.
GraphRAG от Microsoft является хронологически первым полноценным фреймворком для автоматического построения графов в контуре RAG‑системы.
Ключевые особенности архитектуры GraphRAG мы рассмотрели в Первой части.
Здесь отметим, что данный фреймворк, несмотря на все ограничения, на сегодняшний день по‑прежнему остается одним из наиболее популярных (Github⭐: 33.1k, Fork: 3.5k) фреймворков, уступая только LightRAG.
GraphRAG сохраняет свои первенство и актуальность в задачах глобального тематического синтеза (QFS), для решения которых требуется способность к global sensemaking (глобальному осмыслению знаний) на больших корпусах текстов.
Эта способность является следствием архитектурного решения — метода кластеризации узлов графа, который позволяет разбивать его первоначальную структуру на иерархические сообщества (Communities) узлов. Для каждого из этих Сообществ на каждом уровне иерархии генерируются краткие текстовые описания (Community Summaries).

Способность global sensemaking, основанная на иерархически организованных Community Summaries, и сегодня позволяет GraphRAG опережать конкурентов в задачах, которые требуют агрегированного синтеза всех сущностей и связей в сложных доменах.
Например, когда нужно ответить на вопрос: «Каковы основные тренды в судебной практике по спорам в сфере Х за 20 лет?»
Ограничения:
Индексация, основанная на Community Summaries, обходится очень дорого (~$6–7 / 32 000 tokens на GPT-4o, данные на начало 2025).
Отсутствует возможность инкрементальных обновлений (при добавлении в БД новых знаний индекс графа необходимо пересчитывать заново). То есть любое изменение в корпусе (поправка в законе, новое судебное решение) требует полного перестроения индекса.
Отсутствует возможность явной дедупликации сущностей (один и тот же объект с различными наименованиями может существовать как несколько разных узлов).
Таким образом, граф, построенный на юридических знаниях с помощью GraphRAG, на текущем этапе имеет принципиальные ограничения для широкого использования. Высокая стоимость индексации и невозможность инкрементальных обновлений делают его применение в RAG‑системах с постоянно обновляемой базой знаний если не экономически нецелесообразным, то, по крайней мере, ограниченным определенными сценариями использования.
LightRAG – это фреймворк для построения графовых RAG‑систем, разработанный командой из Университета Гонконга (HKUDS, Университет Гонконга, октябрь 2024, Github⭐: 35.3k, Fork: 5k).
Как и GraphRAG, LightRAG строит граф знаний в процессе индексации, извлекая сущности и связи из документов с помощью LLM. Ключевое отличие в том, что LightRAG не строит поверх графа дорогостоящую иерархию сообществ: сущности и связи хранятся как key‑value пары с векторными эмбеддингами, по которым поиск выполняется за один API‑вызов вместо обхода сотен Community reports (summaries).
Таким образом, топология связей между сущностями совмещается с их векторными представлениями, что позволяет системе одновременно понимать и что спрашивает пользователь, и как это связано с остальными знаниями в базе.
LightRAG решает основные проблемы GraphRAG:
кратное сокращение времени / снижение стоимости индексации и поиска за счет отказа от создания и обхода community‑кластеров графа;
инкрементальное обновление индекса: извлеченные из новых текстовых фрагментов сущности и связи дополняют граф инкрементально, без пересчета всего индекса;
встроенная возможность явной дедупликации сущностей.
По данным разработчиков LightRAG при тестировании на большинстве задач (в том числе задач, отнесенных к юридическому домену) фреймворк не только не уступает GraphRAG, но и превосходит его.
С метриками и результатами более подробно можно ознакомиться в статье – LightRAG: Simple and Fast Retrieval‑Augmented Generation.
При этом разница в потреблении ресурсов при росте числа обрабатываемых токенов между LightRAG и GraphRAG кардинальна:

На примере юридического датасета авторы провели прямое измерение: чтобы обработать один запрос, GraphRAG вынужден прогнать через LLM отчеты 610 community‑кластеров второго уровня, по ~1 000 токенов каждый, итого - 610 000 токенов и сотни API‑вызовов. LightRAG справляется с той же задачей, потратив менее 100 токенов и сделав один API‑вызов: модель просто генерирует ключевые слова запроса, а дальше работает векторный поиск по предзаписанным key‑value структурам графа. Разница ~ 6 000 раз.
Каким образом LightRAG удалось сохранить качество при кратном снижении стоимости индексации?

Архитектура состоит из двух логических слоев – индексирования и поиска.

Шаг 1. Extraction Pipeline: извлечение из документов сущностей и отношений.
Документы проходят стандартную предобработку: разбивка на чанки (по умолчанию размер чанков – 1200 токенов с перекрытием – 100 токенов) с последующей векторизацией (embedding‑model по умолчанию – BAAI/bge‑m3,1024 dim). Полученные эмбеддинги автоматически сохраняются в Vector DB Storage.
Extraction Pipeline задается на уровне системных промптов, руководствуясь которыми LLM (по умолчанию – GPT-4o‑mini), итеративно (количество итеративных проходов ограничено на уровне настроек фреймворка в целях экономии токенов), в несколько проходов (их количество ограничено, что позволяет сэкономить, в отличие от GraphRAG), извлекает из чанков сущности (Entities) – узлы будущего графа, и отношения (Relations) между ними – связи между сущностями, который сформируют ребра будущего графа.
Отдельного внимания заслуживает предзаданная типизация сущностей. По умолчанию используется универсальный перечень из 11 категорий:
Person, Creature, Organization, Location, Event, Concept, Method, Content, Data, Artifact, NaturalObject
Все, что не попало в этот список, относится к категории Other.
Если вы работаете со специфическим доменом (юридический домен, как раз, относится к таковым), можно переопределить и задать собственную нативную таксономию.
В нашем эксперименте мы пошли по компромиссному пути: использовали стандартную типологию при индексации и переопределили ее постфактум, на этапе оптимизации графа. К плюсам и минусам такого выбора вернемся в дальнейшем.
Узлы и ребра снабжаются развернутыми текстовыми описаниями – аннотациями, являющимися результатом суммаризации исходного текста той же LLM.
Шаг 2. LLM Profiling for Key Value Pair Generation: профилирование сущностей и связей.
Для каждой извлеченной сущности и каждого отношения выполняется так называемое профилирование: специальная функция генерирует пару {key: value}, где:
key – имя сущности или ключевые слова отношения;
value – summary, агрегирующее релевантный контекст из всех чанков, где эта сущность или отношение встречались.
Сущности, как правило, имеют один ключ, отношения – несколько, чтобы охватить более широкие тематические контексты, в которых данная связь проявляется. Это и закладывает фундамент двухуровневого поиска (см. ниже по тексту).
Значения KV‑пар дополнительно векторизуются. Таким образом формируется гибридный индекс "граф + вектор": топология связей хранится в графе, семантика наименований и описаний – в векторном пространстве, где можно искать по смысловой близости.
Шаг 3. Дедупликация.
Явные дубликаты сущностей автоматически объединяются в единые узлы с сохранением ранее построенных отношений. С помощью LLM формируется единое подробное описание каждой сущности и каждой связи, агрегирующее информацию из всех чанков, где они встретились.
Важная оговорка: автоматическая дедупликация работает только с явными дубликатами – узлами с точным совпадением имен. Случаи вроде "ст. 130" и "статья 130 ГК РФ", или "ООО «Результат»" и "ооо результат", или "Civil Code" и "Гражданский кодекс РФ" LLM на этом этапе, к сожалению, пока не объединит. Этим придется заниматься на отдельном этапе оптимизации.
Создание графа и финальное сохранение.
После трех указанных выше шагов:
все сущности и связи записаны в графовую базу данных (по умолчанию — NetworkX, опционально поддерживаются Neo4j, PostgreSQL, MongoDB);
описания сущностей и связей векторизованы и сохранены в Vector DB Storage.
Это и позволяет искать одновременно: по тексту чанков, по именам и описаниям сущностей, а также по ключевым словам и описаниям самих отношений (идея гибридного индекса).

При обработке запроса LLM выделяет из него два набора ключевых слов:
низкоуровневые (low_level_keywords) – конкретные именованные сущности и факты (например, торговый павильон, ст. 130 ГК РФ, ООО Верево);
высокоуровневые (high_level_keywords) – обобщенные темы и концепции (например, квалификация объекта недвижимости, вещь, судебная инстанция).
В этом суть того, что авторы называют "dual‑level retrieval system": система одновременно понимает, что спрашивает пользователь, и о чем этот вопрос в более широком контексте..
Дальнейшая логика поиска зависит от выбранного режима:
Режим | Что делает | Когда применять |
| стандартный векторный поиск по чанкам, без обхода графа | Baseline |
| обход ближайшего окружения (локальный уровень) – конкретных сущностей из низкоуровневых ключей | точечные фактологические вопросы |
| "высокоуровневый" поиск по всему графу: обход по общим концепциям и отношениям, связанным в кластеры | аналитические и обобщающие запросы |
| объединение | многоаспектные сложные вопросы |
| интеграция полного графового поиска ( | наиболее полный режим |
В наиболее полном режиме mix фактически выполняются три параллельных процесса: обход графа от выявленных конкретных сущностей, обход от связанных концептов и стандартный векторный поиск по чанкам. Полученные результаты объединяются в единый контекст, передаваемый LLM для генерации ответа.
Поддержка инкрементальных обновлений графа
Отдельно стоит выделить алгоритм инкрементального обновления базы знаний. Новые документы добавляются в существующий граф без полной переиндексации, что очень важно при работе с регулярно обновляемым корпусами юридических текстов.
Команда блога LearnOpenCV, сделавшая, на мой взгляд, один из лучших англоязычных разборов фреймворка, подготовила краткую визуальную демонстрацию работы пайплайна, как раз на юридическом корпусе:
📺 LightRAG Workflow: Legal Doc Analysis, Graph Indexing, Dual‑Level Retrieval
Для большинства доменов и значительной доли практических несложных задач LightRAG можно запустить сходу, с минимальными конфигурационными настройками, задав переменные окружения в.env‑файле.
Фреймворк поставляется с Docker‑образом и готовым docker‑compose.yml, который описывает, как одновременно запустить все нужные компоненты системы: сам сервер LightRAG, его веб‑интерфейс, REST API для программного доступа и опционально, если требуется масштабируемое решение в production‑сценариях — внешние базы данных (Neo4j для графа, PostgreSQL с расширением pgvector для эмбеддингов; Milvus, FAISS или другую векторную БД).
Для того, чтобы потестить систему эти внешние базы можно не подключать вовсе: LightRAG по умолчанию использует встроенные легковесные хранилища (NetworkX для графа, NanoVectorDB для векторов), которых достаточно для прототипирования и работы с небольшими корпусами.
Веб‑интерфейс LightRAG закрывает основные пользовательские сценарии: загрузку документов и мониторинг их обработки, визуализацию графа знаний, а также выполнение запросов с выбором режима поиска.
Минимальные требования:
Параметр | Минимум | Рекомендуется |
CPU | 4 ядра (i5 / Ryzen 5) | 8+ ядер |
RAM | 8 GB | 16+ GB |
Диск | 10 GB | 50+ GB SSD |
В общем, подойдет любой современный бюджетный VPS сервер, в качестве базовой LLM предлагается использовать GPT-4o-mini.
Поддержка Ollama
Ollama позволяет работать с локальными LLM без отправки данных во внешние API. Эта опция особенно актуальна для тех, кому важна конфиденциальность. Например, при работе с юридическими или медицинскими документами.
Переключение задается одной переменной в .env (LLM_BINDING=ollama плюс адрес локального сервера Ollama и название модели). Аналогично можно настроить локальный эмбеддер.
Здесь не буду останавливаться подробно, так как это отдельный пласт, со своими нюансами выбора модели под доступную видеопамять, оптимизации скорости и качества извлечения сущностей.
Важно лишь упомянуть один практический и экономически значимый нюанс: для качественного извлечения сущностей графа авторы LightRAG рекомендуют LLM не менее 32B параметров с контекстом ≥ 32K токенов. Это означает, что для серьезной работы с Ollama понадобится видеокарта уровня NVIDIA RTX 4090 (24 ГБ VRAM) или кластер с нескольким GPU попроще.
1. Локально
Чтобы протестировать фреймворк локально, без написания кода и без каких‑либо доработок, кроме.env‑файла с базовыми настройками (выбор LLM‑провайдера, API‑ключи к LLM и эмбеддеру, при желании – параметры чанкинга и хранилищ), достаточно выполнить в терминале следующую последовательность команд:
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
# cоздаём личную копию файла настроек из шаблона
cp env.example .env # Git Bash / macOS / Linux
# открываем .env и вписываем API-ключи к LLM и эмбеддеру
notepad .env # Windows
# nano .env # Linux/macOS
# поднимаем всю связку контейнеров (сервер + WebUI + хранилища)
docker compose up -dДля выполнения этих команд требуется Docker Desktop (Windows/macOS) или Docker Engine (Linux).
После этого по адресу http://localhost:9621/webui/ открывается полноценный веб‑интерфейс. Подходит для прототипирования и работы с небольшими корпусами.
2. Удаленный сервер через Docker
Логика та же, что и при локальном запуске, только команды выполняются не на вашем ноутбуке, а на удаленной машине через SSH‑сессию.
Здесь уже, конечно, потребуется дополнительно поработать с сетевыми настройками сервера и параметрами доступа, чтобы запущенный сервис (LightRAG‑инстанс) был доступен извне и при этом защищен от посторонних, а это уже задача базового системного администрирования.
3. Через self‑hosted PaaS
Если не хочется заниматься системным администрированием и возиться с терминалом, можно развернуть с помощью self‑hosted PaaS (Platform‑as‑a-Service), например, Dokploy.
После установки такой платформы на свой сервер все управление идет через веб‑интерфейс в браузере.
Упрощенно сценарий развертки выглядит так:
В панели Dokploy подключить GitHub‑репозиторий LightRAG (просто вставить ссылку https://github.com/HKUDS/LightRAG).
Заполнить переменные окружения через веб‑интерфейс (а не в «сыром».env‑файле).
Указать свой домен.
Нажать Deploy.
Логика та же, что и при локальном запуске, только команды выполняются не на вашем ноутбуке, а на удаленной машине через SSH‑сессию.
После развертки можно сразу использовать интуитивно понятный веб‑интерфейс.
WebUI содержит четыре вкладки:
Documents – управление документами;
Knowledge Graph – интерактивная визуализация графа знаний с фильтрами по типам сущностей, поиском по именам узлов, отображением свойств;
Retrieval – собственно, чат с выбором режима поиска (local / global / hybrid / naive / mix ), настройкой параметров (top_k, chunk_top_k, max_total_tokens), возможностью включения реранкера, передачей user_prompt для управления стилем ответа;
API – автоматически генерируемая интерактивная документация (Swagger UI) всех REST-эндпоинтов сервера. В браузере видно, какие запросы поддерживает LightRAG (загрузка документов, поиск, выгрузка графа), какие параметры они принимают.


Ограниченность "коробочных" решений
Ограниченность вытекает из самой природы "коробочного" подхода, особенно когда система изначально не была "заточена" под специфику конкретного домена.
Рабочий LightRAG-инстанс с дефолтными промптами, дефолтной типологией сущностей и декларативной настройкой через .env, WebUI и REST API не может стать антихрупким решением в любом домене.
В общем, как показал наш эксперимент на реальных "юридических" данных, без программирования и понимания домена все еще не обойтись :)
Об этом – в следующем разделе.
Цель эксперимента – обзорно‑исследовательская.
Раскладывается на две практические задачи:
Протестировать запуск фреймворка на дефолтных параметрах с теми же данными, что использовались в предыдущем эксперименте.
Понять, на каких реальных задачах юридического домена граф знаний дает не просто прирост качества по сравнению с векторным RAG, а, в принципе, позволяет их решать, то есть где ограничения векторного RAG принципиальны.
Здесь же стоит обозначить две методологические рамки, в которые пришлось вписаться, чтобы сравнение чисто векторной и векторно‑графовой систем было корректным.
LightRAG не интегрирован в инфраструктуру LangChain. Ensemble Vector RAG из статьи Law & Practice Ensemble RAG. Как создать ассистента, помогающего решать многоаспектные юридические задачи был построен именно вокруг LangChain, что давало ряд возможностей по гибкой настройке пайплайна «из коробки».
В LightRAG же системе передается сам вопрос пользователя и небольшой объект настроек (режим поиска, число извлекаемых сущностей и чанков, бюджет контекста, опциональная системная инструкция). Все промежуточные стадии (двухуровневое извлечение ключевых слов, обход графа, векторный поиск по чанкам, слияние результатов и генерация финального текста) выполняются внутри фреймворка, и вмешаться в них на уровне отдельных компонентов так же гибко, как в LCEL‑пайплайне, нельзя.
Количественная оценка с помощью RAGAS, на мой взгляд, применительно к графовым RAG‑системам в юридическом домене является ограниченной и не всегда репрезентативной.
Метрики для оценки полноты и качества извлеченного системой контекста (context_precision и context_recall) спроектированы для парадигмы а‑ля «чанки текста как атомарные единицы извлечения». Контекст, который собирает LightRAG в режиме mix (наиболее эффективном для юридического домена), гетерогенен по природе: это смесь сущностей графа, описаний ребер и текстовых чанков. Что считать «релевантным контекстом» в случае ребра графа, описание которого лишь косвенно связано с запросом, но при этом играет роль логического моста между двумя несомненно релевантными сущностями?
Практические следствия:
Векторная часть фреймворка в эксперименте используется "из коробки", без специальной донастройки ретривера.
Оценка проведена на репрезентативных запросах, которые либо прямо соотносятся с базой знаний, либо специально подобраны для проверки гипотезы о том, что графовый подход справляется там, где векторный принципиально ограничен.
При этом, забегая немного вперед, с графовой частью пришлось поработать "руками": "коробочной" конфигурации для серьезной работы с юридическим доменом ожидаемо оказалось недостаточно.
Для чистоты сравнительного тестирования мы продолжим работать с тем же корпусом, что использовался в эксперименте с Ensemble Vector RAG.
В качестве источников знаний использовал неструктурированные (но специально подобранные и отобранные) текстовые данные в машиночитаемом формате, которые условно обозначим как "юридические тексты в сфере недвижимого имущества":
Тексты законов: в данном случае, учитывая стоимость индексации и особенности постобработки графа при оптимизации, необходимостью визуализации и демонстрации построенного графа, было решено ограничиться основным и наиболее репрезентативным источником – Гражданским кодексом Российской Федерации (части 1, 2).
Тексты решений Верховного Суда Российской Федерации: 110 релевантных текстов, в которых рассматриваются вопросы неоднозначной квалификации объектов имущества, за период: 2006–2023.
Компонент | Решение | Особенности |
LLM (индексация и поиск) | GPT-4o‑mini (API через VseLLM) | Использовался встроенный кеш LightRAG, чтобы минимизировать бюджет на повторные вызовы API |
Эмбеддинг‑модель | LaBSE (cointegrated/LaBSE‑en‑ru, 768 dim) | Данная модель использовалась для сопоставления результатов эксперимента с Ensemble Vector RAG. В режиме ознакомления с фреймворком рекомендую использовать дефолтную BAAI/bge‑m3. |
Графовый фреймворк | LightRAG + NetworkX | Графовое хранилище по умолчанию: после индексации граф доступен как объект NetworkX для "ручной" оптимизации. |
В общем, конфигурация самая базовая.
Если использовать дефолтную конфигурацию (OpenAI-эмбеддер, официальный эндпоинт OpenAI, без доменной специфики), минимальный рабочий код в ноутбуке может выглядеть так:
!pip install lightrag-hku nest_asyncio -q
import os, nest_asyncio
os.environ["OPENAI_API_KEY"] = "sk-..." # ваш ключ OpenAI
# Патч асинхронности: «оборачиваем» уже запущенный в ноутбуке event loop
nest_asyncio.apply()
from lightrag import LightRAG, QueryParam
from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed
from lightrag.utils import EmbeddingFunc
docs = ["...txt_1...", "... txt_2..."] # docs: List[str]
rag = LightRAG(
working_dir="./rag_storage",
llm_model_func=gpt_4o_mini_complete, # встроенный OpenAI-вызов
embedding_func=EmbeddingFunc(
embedding_dim=1536, # размерность text-embedding-3-small
max_token_size=8192,
func=openai_embed # встроенный OpenAI-эмбеддер
)
)
await rag.initialize_storages()
await rag.ainsert(docs)Особенности, на которые стоит обратить внимание.
1. Необходимость патча асинхронности.
Один из базовых архитектурных принципов фреймворка – асинхронность. Ключевые методы существуют в двух вариантах:
асинхронные корутины: ainsert(), aquery(), initialize_storages() и
синхронные обертки над ними: insert(), query().
В интерактивной среде (Jupyter, Kaggle и Colab) управляющий цикл event loop уже запущен самим ядром ноутбука. А стандартные способы запуска асинхронного кода рассчитаны на то, что цикл они создают сами, "с нуля". Натыкаясь на уже работающий цикл, они падают с ошибкой:
RuntimeError: This event loop is already running
Решение – патч nest_asyncio: перед первым вызовом методов LightRAG он "оборачивает" уже работающий event loop, позволяя запускать в нем новые корутины. Реализован в коде выше.
2. Подключение эмбеддеров не из дефолтного набора.
LightRAG на текущий момент поддерживает openai_embed– встроенную функцию‑обертку для эмбеддеров OpenAI (по умолчанию text‑embedding-3-small, 1536 dim), а также модели через Ollama.
Если вы хотите использовать эмбеддер не из дефолтного набора, а, например, загрузить веса модели из HuggingFace, придется написать асинхронную обертку над такой моделью. Для этого в LightRAG предусмотрен специальный класс‑адаптер EmbeddingFunc.
Я использовал LaBSE от HuggingFace. Если вы захотите взять, например, современную и эффективную для русского языка GigaEmbeddings (Sber), которую можно скачать с HuggingFace, придется проделать аналогичные шаги:
!pip install lightrag-hku pyvis networkx nest_asyncio \
sentence-transformers langchain-huggingface -q
from langchain_huggingface import HuggingFaceEmbeddings
from lightrag.utils import EmbeddingFunc
import numpy as np
embedding_model = HuggingFaceEmbeddings(
model_name="cointegrated/LaBSE-en-ru",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
async def labse_embedding_func(texts: list[str]) -> np.ndarray:
embeddings = embedding_model.embed_documents(texts)
return np.array(embeddings)3. Привязка встроенных функций к OpenAI.
LightRAG также имеет встроенный gpt_4o_mini_complete, но он жестко привязан к официальному эндпоинту OpenAI. Для оплаты в рублях из России использовал VseLLM – российский OpenAI-совместимый прокси (base_url переопределяем самостоятельно через openai_complete_if_cache, как в коде ниже):
from lightrag.llm.openai import openai_complete_if_cache
async def gpt_4o_mini_via_vsellm(prompt, system_prompt=None, history_messages=[], **kwargs):
return await openai_complete_if_cache(
"gpt-4o-mini",
prompt,
system_prompt=system_prompt,
history_messages=history_messages,
base_url="https://api.vsellm.ru/v1",
temperature=0.1, # низкая для стабильности извлечения
max_tokens=3000, # запас на extraction-фазу
**kwargs
)Несколько технических нюансов:
openai_complete_if_cache– встроенная функция‑обертка LightRAG, которая автоматически кеширует ответы LLM: для экономии на повторных вызовах на идентичных фрагментах;
max_tokens=3000– с запасом, потому как на этапе extraction LLM генерирует не только список сущностей, но и развернутые описания каждой из них.
Инициализация LightRAG-инстанса и запуск индексации.
Собираем все вместе:
from lightrag import LightRAG
WORKING_DIR = "./lightrag_labse_index"
os.makedirs(WORKING_DIR, exist_ok=True)
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=gpt_4o_mini_via_vsellm,
chunk_token_size=1500, # по умолчанию 1200
chunk_overlap_token_size=300, # по умолчанию 100
llm_model_max_async=4,
embedding_func=EmbeddingFunc(
embedding_dim=768, # размерность LaBSE
max_token_size=512, # лимит LaBSE
func=labse_embedding_func # асинхронная обертка
)
)
# Инициализация хранилищ и собственно индексация
await rag.initialize_storages()
await rag.ainsert(full_corpus) Вызов метода ainsert запускает pipeline индексации: LightRAG разбивает корпус на чанки, прогоняет каждый через GPT-4o‑mini для извлечения сущностей и связей, профилирует их через LLM‑описания, векторизует через LaBSE и сохраняет одновременно в трех хранилищах: графовом (NetworkX), key‑value (JSON) и векторном (NanoVectorDB).
На моем корпусе – ГК РФ (части 1 и 2) + 110 решений ВС РФ, построение графа заняло около 5–6 часов. Разброс связан с тем, что индексация выполнялась не за один проход, а инкрементально, в несколько сессий:
Корпус | LLM | Объем (сим.) | Время |
ГК РФ (ч. 1–2) + 110 решений ВС РФ | GPT-4o‑mini (через прокси VseLLM) | 2 645 413 | ~ 5–6 ч |
Время определяется не только объемом корпуса, но и инфраструктурой LLM‑вызовов: латентность облачного прокси VseLLM, параллелизм (4 одновременных запроса) и (незначительно) вычисление LaBSE‑эмбеддингов на CPU.
Итак, что мы получили из "коробки", подкорректировав пару дефолтных параметров.
Топ-10 сущностей графа по числу связей:
# | Сущность | Тип | Связей |
1 | Гражданский кодекс РФ | document | 560 |
2 | Суд | organization | 118 |
3 | Верховный Суд РФ | organization | 91 |
4 | Юридическое лицо | concept | 82 |
5 | Земельный участок | naturalobject | 80 |
6 | Общество | organization | 68 |
7 | Лицо | concept | 67 |
8 | Гражданин | person | 64 |
9 | Кредитор | person | 55 |
10 | Покупатель | person | 53 |
Набор сущностей, на первый взгляд, выглядит, в целом, адекватно: центральный узел с наибольшим числом связей – Гражданский кодекс РФ формирует "ядро" графа. Наиболее репрезентативный пример недвижимого имущества – Земельный участок, входит и в число вершин с наибольшей степенью. Однако внимательный юрист, изучив таблицу, отметит, что количество статей в первых двух частях ГК РФ более 560 (статьи – наиболее очевидные вершины графа, с которыми должен быть связан центральный узел). Также универсальная типология сущностей для юридического домена выглядит, мягко говоря, странно и ситуативно.
Распределение сущностей по типам:
Тип сущности | Количество | Тип сущности | Количество |
concept | 2233 | law | 85 |
organization | 814 | naturalobject | 63 |
content | 626 | method | 30 |
person | 515 | event | 27 |
data | 227 | UNKNOWN | 26 |
artifact | 205 | other | 18 |
location | 166 | article | 12 |
document | 156 | прочие (20 типов) | ≤ 8 каждый |
Видим, что большинство сущностей LightRAG отнес к универсальным категориям, при этом более 50% сущностей — как будто бы, к случайным типам, которые, очевидно, пересекаются с универсальными типами: так, к типу article отнесены всего 18 сущностей, при том, что в первых двух частях ГК РФ суммарно более 1100 статей.
При этом абсолютным лидером является размытый concept (2233, почти половина всех узлов), а юридически значимые типы теряются среди несогласованных меток‑синонимов: law, article, legalreference, legalarticle, legaldocument, legal_document, legislation, legalprovision, contract– все это, по сути, правовые нормы, понятия, термины и документы.
Визуализация графа

Граф визуально выглядит адекватно: плотное ядро с расходящимися "лучами" связей, аккуратное кольцо периферийных узлов, цветовая кодировка вершин создает ощущение разнообразия различных типов сущностей.
Двуязычная подпись на всплывающей подсказке – «Договор Транспортной Экспедиции outlines the obligations of the...» – наглядный пример той самой проблемы смешения русского и английского при извлечении, о которой шла речь выше.
Визуальная убедительность графа и его пригодность для поиска – разные вещи.
Эстетика отображения определяется работой алгоритма визуализации, тогда как пригодность графа для retrieval определяется его топологическими характеристиками: связностью, распределением степеней вершин, плотностью и так далее.
А чтобы измерить эти характеристики, без теории графов уже не обойтись.

Полученный граф знаний с точки зрения теории графов – это простой неориентированный размеченный и взвешенный граф:
где – множество вершин (сущностей),
– множество ребер, которые являются неупорядоченными парами вершин (связей, отношений). Каждой вершине и каждому ребру сопоставлен набор атрибутов (тип сущности, описание, вес связи).
Интерес для практического анализа представляли следующие характеристики:
1. Связность: разбиение графа на компоненты связности (максимальные по включению связные подграфы) – {
} и доля вершин в наибольшей компоненте связности
, где
– множество вершин этой компоненты.
Что показывает?
При поиске по графу система начинает его обход от найденных по запросу сущностей и движется по ребрам к связанным сущностям. Если граф распадается на множество изолированных "островов", то все, что лежит вне той компоненты, куда система попала, для нее попросту не существует, что, негативно влияет на качество поиска.
2. Средняя степень вершин графа.
где – степень вершины (число инцидентных ей ребер).
Является следствием из известной леммы о рукопожатиях, в соответствии с которой сумма степеней всех вершин неориентированного (конечного) графа всегда равна удвоенному числу его ребер.
Средняя степень указывает на тип его структуры, точкой отсчета является значение , что вытекает из свойств таких структур, как деревья и леса:
значение , что в точности соответствует простому циклу;
для дерева (связного графа без циклов) число ребер равно , откуда
: средняя степень строго меньше двух и приближается к двойке снизу с ростом графа, не достигая ее;
для леса (ациклического графа), содержащего – компоненты связности:
, откуда
;
означает, что граф содержит циклы; чем сильнее
превосходит двойку, тем больше циклов и тем дальше граф от древовидно‑лесной структуры в сторону плотно связной сети.
Средняя степень принимает значения в диапазоне , где нижняя граница достигается на графе из изолированных вершин (
), верхняя – на полном графе
.
Значение меньше двух свидетельствует об ацикличности и фрагментированности (граф распадается на деревья), значение больше двух – о наличии циклов.
Что показывает?
Средняя степень вершин графа показывает, сколько в среднем ребер отходит от одной сущности, то есть насколько связным является типичный узел. Соответственно, чем выше средняя степень вершин графа, тем больше альтернативных маршрутов между узлами и тем более полный контекст граф способен собрать в ответ на запрос.
С другой стороны, средняя степень – это усредненная характеристика, и она (как и любое среднее) может скрывать неоднородность структуры: несколько вершин с очень высокой степенью (например, центральный узел «Гражданский кодекс РФ») могут "вытягивать" среднее вверх, тогда как основная масса узлов остается почти изолированной. Формально приемлемое при этом скрывает фактическую разреженность графа. Особенно уязвимы древовидные и несвязные структуры: в них удаление единственного ребра‑моста разрывает граф на изолированные компоненты, и часть знаний становится недостижимой при обходе.
3. Плотность графа – отношение фактического числа ребер к максимально возможному при данном числе вершин. Для простого неориентированного графа максимум ребер достигается на полном графе и равен
, откуда
Согласно классическому определению, максимальная плотность равна 1 (для полных графов), минимальная – 0 (для графа без ребер). Граф с малым числом ребер называют разреженным, с числом ребер, близким к максимально возможному – плотным.
Что показывает?
Плотность можно охарактеризовать как "коэффициент использования потенциала связности", простыми словами – насколько граф близок к ситуации, когда все связано со всем. Сама по себе низкая плотность для большого корпуса текстов нормальна: сущности из разных правовых источников и отраслей права и не должны быть сильно связаны. Практически значима не абсолютная величина , а ее динамика до и после оптимизации, если таковая выполняется: рост плотности – доказательство того, что переработка графа, образно говоря, действительно уплотнила смысловые связи, а не просто переставила вершины.
4. Доля изолированных вершин
Изолированной называется вершина со степенью нуль, то есть вершина, не являющаяся концом ни для одного ребра. Рассматриваемая характеристика — доля таких вершин в графе:
Изолированные вершины образуют одноэлементные компоненты связности и не лежат ни на одном маршруте графа.
Что показывает?
Изолированная вершина – это сущность, которую модель извлекла из текста, но не связала ни с чем. Соответственно обход по ребрам до нее никогда не доходит, в контекст ответа она не попадает. Поэтому высокая доля изолированных вершин – не очень нехорошо для поиска: знания формально присутствуют в базе, но недостижимы при обходе графа.
Технически анализ выполнялся напрямую над внутренним графовым объектом LightRAG – экземпляром класса networkx.Graph.
Ниже приведен пример функции, выполняющей такой топологический анализ на микроуровне (вершины и ребра) и макроуровне (компоненты связности):
import networkx as nx
def analyze_graph_topology(rag):
"""
Топологический аудит графа знаний LightRAG.
Микроуровень — характеристики вершин и ребер;
макроуровень — компоненты связности.
Возвращает словарь измеренных характеристик.
"""
# rag - экземпляр LightRAG; ._graph - внутренний объект networkx.Graph
G = rag.chunk_entity_relation_graph._graph
V = G.number_of_nodes() # |V|
E = G.number_of_edges() # |E|
if V == 0:
print("Граф пуст — проверьте working_dir.")
return None
degrees = dict(G.degree())
sum_degrees = sum(degrees.values()) # Σ deg(v)
avg_degree = sum_degrees / V # ⟨k⟩ = 2|E| / |V|
density = nx.density(G) # ρ = 2|E| / (|V|(|V|-1))
print("=== Микроуровень (вершины и ребра) ===")
print(f"Вершин |V|: {V}")
print(f"Ребер |E|: {E}")
print(f"Сумма степеней Σ deg(v): {sum_degrees}")
print(f"Средняя степень ⟨k⟩: {avg_degree:.2f}")
print(f"Плотность ρ: {density:.5f}")
# Лемма о рукопожатиях как валидатор: Σ deg(v) == 2|E|.
# Расхождение означало бы наличие кратных ребер или петель: архитектура фреймворка этого не предусматривает.
if not G.is_directed():
ok = (sum_degrees == 2 * E)
print(f"Лемма о рукопожатиях: "
f"{'выполнена' if ok else 'НАРУШЕНА (есть кратные ребра/петли)'}")
# Аномальные вершины
isolated = list(nx.isolates(G)) # степень 0
pendant = [n for n, d in degrees.items() if d == 1] # степень 1
print("\n=== Аномальные вершины ===")
print(f"Изолированные (deg = 0): {len(isolated)} "
f"({len(isolated) / V * 100:.1f}%)")
print(f"Висячие (deg = 1): {len(pendant)} "
f"({len(pendant) / V * 100:.1f}%)")
# Макроуровень – компоненты связности
if G.is_directed():
components = list(nx.weakly_connected_components(G))
comp_label = "слабосвязные компоненты"
else:
components = list(nx.connected_components(G))
comp_label = "компоненты связности"
sizes = sorted((len(c) for c in components), reverse=True)
largest_cc = sizes[0] if sizes else 0
coverage = largest_cc / V * 100
print(f"\n=== Макроуровень ({comp_label}) ===")
print(f"Число компонент: {len(components)}")
print(f"Наибольшая компонента: {largest_cc} вершин")
print(f"Доля в наибольшей компон.: {coverage:.1f}%")
return {
"V": V, "E": E,
"sum_degrees": sum_degrees,
"avg_degree": avg_degree,
"density": density,
"isolated_frac": len(isolated) / V,
"pendant_frac": len(pendant) / V,
"components": len(components),
"largest_cc": largest_cc,
"largest_cc_frac": largest_cc / V,
}После первичной индексации получился граф со следующими характеристиками:
Характеристика | Значение | Что означает |
Количество вершин | 5261 | количество извлеченных сущностей |
Количество ребер | 5773 | количество связей |
Средняя степень | 2,19 | у среднего узла чуть больше двух связей |
Плотность | 0,00042 | реализовано 0,04% потенциальных связей |
Количество изолированных вершин | 1389, 26,4% | сущности со степенью 0 |
Количество висячих вершин | 2013, 38,3% | сущности со степенью 1 |
Количество компонент связности | 1535 | на сколько изолированных частей ("островов") распался граф |
Количество вершин в наибольшей компоненте | 3398, 64,6% | доля вершин в связном ядре графа |
Средняя степень вершин графа составила 2,19: граф едва вышел за пределы древовидно-лесной структуры.
Для того, чтобы понять, какое число ребер графа образуют циклы, используем цикломатическое число – характеристика графа, которая показывает минимальное количество ребер, которые нужно удалить, чтобы превратить граф в дерево (для связного графа) или в лес (для несвязного графа), то есть добиться отсутствия циклов.
Формально – количество независимых циклов в графе, то есть число ребер сверх минимального древовидного каркаса; для дерева
. В нашем случае:
То есть из 5773 ребер лишь 2047 образуют циклы. Граф структурно гораздо ближе к лесу, чем к связной сети, что, опять же, совершенно нормально для юридического домена.
Плотность – 0,00042 сама по себе ни о чем не говорит. Для юридического корпуса текстов низкая плотность является нормой.
Распределение вершин по степеням:
26,4% вершин изолированы (степень 0): каждая четвертая извлеченная сущность недостижима при любом обходе (скорее всего, среди них немало персон, инициалов, различных номеров и прочих артефактов, которые изолированы относительно сущностей Гражданского кодекса);
38,3% висячих вершин (степень 1): удаление единственного ребра, соединяющего висячую вершину с графом, мгновенно превращает ее в изолированную (это может быть как следствием дефекта индексации: сущность, которую LLM извлекла и привязала ровно к одному контексту, не распознав её множественных связей, так и следствием дизайна выборки: полный текст ГК РФ против узкого среза практики).
Микроуровень (64,7% вершин со степенью ≤ 1) и макроуровень (наибольшая компонента 64,6%) указывают, по сути, на одно и то же с разных сторон: граф фрагментирован, и довольно значительная часть знаний будет недостижима при поиске.
Топ-10 узлов за пределами наибольшей компоненты:
# | Сущность | Тип | Связей |
1 | Supreme Court of the Russian Federation | organization | 28 |
2 | Vladimir Alexandrovich Siplyov | person | 8 |
3 | Result LLC | organization | 7 |
4 | Договор продажи предприятия | concept | 6 |
5 | Статья 970 | concept | 6 |
6 | Объект незавершенного строительства | concept | 6 |
7 | Supreme Arbitration Court of the RF | organization | 5 |
8 | Н.С. | person | 4 |
9 | Белоусова Н.С. | person | 4 |
10 | Federal Law No. 159-FZ | concept | 4 |
Анализ узлов, расположенных за пределами наибольшей компоненты, показывает, что природа фрагментации графа неоднородна.
Часть изолированных сущностей объяснима составом корпуса: при асимметричной выборке (ГК РФ против узкого тематического среза практики) многие нормы кодекса закономерно не активируются и остаются вне ядра.
Но наряду с этим анализ показал и очевидные дефекты автоматического извлечения:
двуязычное расщепление: центральные сущности (Верховный Суд, ВАС РФ, ключевые нормы) присутствуют в графе дважды – в русско‑ и англоязычном вариантах, причем англоязычные дубликаты с десятками связей образуют целые параллельные подграфы, оторванные от русскоязычного ядра;
нормы ГК РФ (ст. 970), которые по содержанию должны находиться в ядре, не были связаны LLM с центральным узлом и оказались в отдельных компонентах;
наличие "шума": инициалы отдельно от фамилии, несуществующие или англоязычные именованные сущности, номера дел и так далее.
Граф без тонкой настройки построить получилось, и построенный на нем поиск как-то справляется с тривиальными задачами из домена. Не стал здесь приводить примеры, потому что анализ показал: очевидно, без оптимизации не обойтись 🛠️
О том, как и что в итоге получилось и что не получилось, читайте дальше 🚀
Спасибо за внимание! Надеюсь, было интересно, готов ответить на вопросы в комментариях.