Skeleton Indexing (KDD 2025) + HippoRAG 2 (ICML 2025) + VectorCypher + Datalog Reasoning + 10 итераций оптимизации
TL;DR
Я реализовал Graph RAG систему, которая комбинирует 5 техник из свежих научных статей в единый пайплайн с декларативным reasoning-движком, полной провенансной трассировкой и типизированным API. Результат: 174/180 (96.7%) на билингвальном бенчмарке из 30 вопросов, оценённых в 6 режимах retrieval. Три режима достигли 100%. Ноль persistent failures.
GitHub: vpakspace/agentic-graph-rag
Проблема: почему обычный RAG недостаточен
Классический RAG — "разбей документ на чанки, сделай embeddings, найди похожие" — работает для простых фактоидных вопросов. Но он ломается на:
Вопросах о связях: "Как метод X связан с компонентом Y?" — ответ разбросан по разным чанкам
Multi-hop рассуждениях: "Что произойдёт, если изменить A, учитывая что A влияет на B, а B на C?"
Глобальных вопросах: "Перечисли все 7 архитектурных решений" — ответ в 7 разных местах документа
Кросс-языковых запросах: русский вопрос о концепциях из английского документа
Моя цель — система, которая справляется со всеми этими типами вопросов, а не только с простыми.
Архитектура: 5 техник из 2025 года
1. Skeleton Indexing (KET-RAG, KDD 2025)
Проблема: извлечение сущностей из всех чанков — дорого (O(n) вызовов LLM).
Решение: строим KNN-граф по embeddings чанков → PageRank → извлекаем сущности только из top-25% "скелетных" чанков. Периферийные чанки привязываем через keyword matching.
Chunks → KNN Graph → PageRank → Top-β Skeletal (full extraction) → Peripheral (keyword linking only)
Результат: 75% меньше вызовов LLM при сопоставимом качестве. Это не трюк — это математика: PageRank выделяет чанки, которые наиболее "центральны" в семантическом пространстве документа.
2. Dual-Node Structure (HippoRAG 2, ICML 2025)
Проблема: обычный GraphRAG теряет контекст полных пассажей. Обычный RAG теряет связи между сущностями.
Решение: два типа узлов в Neo4j:
PhraseNode — сущность (имя, тип, PageRank score, embedding)
PassageNode — полный текст чанка (контент, embedding)
MENTIONED_IN — связывает сущности с пассажами
RELATED_TO — ко-вхождения между сущностями
Это даёт и навигацию по графу (через PhraseNode), и полный контекст (через PassageNode).
3. VectorCypher Retrieval
Гибридный retrieval в три фазы, вдохновлённый VectorCypherRetriever из Neo4j GraphRAG:
Vector Index → находим ближайшие PhraseNode через cosine similarity
Cypher Traversal → расширяем через RELATED_TO (до 3 хопов)
PassageNode Collection → собираем связанные пассажи → GraphContext
Ключевой инсайт: cosine re-ranking по реальным embeddings PassageNode из Neo4j бьёт RRF-фьюжн.
4. Agentic Router с Self-Correction
Три уровня маршрутизации с каскадным fallback:
Tier | Метод | Confidence | Описание |
|---|---|---|---|
1 | Mangle (Datalog) | 0.7 | 65 билингвальных ключевых слов |
2 | LLM (GPT-4o-mini) | 0.85 | Классификация нейросетью |
3 | Pattern (regex) | 0.5 | Regex-паттерны как fallback |
Если качество retrieval ниже порога (relevance < 2.0 из 5), система эскалирует по цепочке инструментов:
vector_search → cypher_traverse → hybrid_search → comprehensive_search → full_document_read
Каждая попытка перефразирует запрос через LLM. Лучшие результаты отслеживаются по всем попыткам.
5. PyMangle — Datalog-движок на Python
Полная реимплементация Google Mangle (2,919 строк):
Lark-based парсер с кастомной грамматикой
Semi-naive evaluation со стратифицированным отрицанием
35+ встроенных функций (арифметика, строки, списки, словари)
Temporal evaluation
Filter pushdown для внешних предикатов
Три файла правил:
routing.mg— маршрутизация запросов (65 ключевых слов)access.mg— RBAC (role inheritance + permit/deny)graph.mg— граф-инференс (reachable, common_neighbor, evidence)
% Транзитивное замыкание по графу reachable(X, Y, 1) :- edge(X, R, Y). reachable(X, Z, D) :- reachable(X, Y, D1), edge(Y, R, Z), D = fn:plus(D1, 1), D < 5. % Общие соседи двух сущностей common_neighbor(A, B, N) :- edge(A, R1, N), edge(B, R2, N), A != B.
Бенчмарк: от 38% до 96.7% за 10 итераций
Дизайн бенчмарка
30 вопросов: 7 simple, 7 relation, 6 multi_hop, 6 global, 4 temporal
2 документа: Doc1 (русский, граф знаний) + Doc2 (английский, архитектура SCL)
6 режимов retrieval: vector, cypher, hybrid, agent_pattern, agent_llm, agent_mangle
180 оценок (30 × 6) через hybrid judge: embedding similarity + keyword overlap + LLM-as-judge
Эволюция результатов
v3: 38% ████░░░░░░░░░░░░░░░░ Baseline (вопросы на EN, документы на RU) v4: 67% █████████░░░░░░░░░░░ +29pp — вопросы на RU (language match!) v5: 73% ██████████░░░░░░░░░░ +6pp — comprehensive_search для global v10: 65% █████████░░░░░░░░░░░ -8pp — добавили 15 новых вопросов v11: 80% ████████████░░░░░░░░ +15pp — enumeration prompt v12: 93% ██████████████████░░ +13pp — hybrid judge v14: 96.7%███████████████████░ +3.7pp — semantic judge
Финальные результаты (v14)
Режим | Результат | |
|---|---|---|
Vector | 30/30 (100%) | Чистый embedding search |
Hybrid | 30/30 (100%) | Vector + Graph |
Agent (Mangle) | 30/30 (100%) | Datalog правила |
Agent (LLM) | 29/30 (96%) | GPT-4o-mini роутер |
Agent (Pattern) | 28/30 (93%) | Regex паттерны |
Cypher | 27/30 (90%) | Граф-траверсал |
Итого | 174/180 (96.7%) | 0 persistent failures |
10 уроков оптимизации
1. Язык вопросов = язык документов (+29pp)
Самое большое улучшение за всю историю проекта. Вопросы на английском о русском документе давали 38%. Переключение на русские вопросы — 67%. Embeddings хорошо справляются с кросс-языковым поиском, но LLM-генератор теряет контекст.
2. Failures — это не retrieval, а generation + evaluation
Ключевой инсайт v11: для глобальных вопросов ВСЕ нужные ключевые слова находились в top-30 чанков. Проблема была в том, что генератор не перечислял все пункты, а judge обрезал ответ до 500 символов.
3. CoT-промпт для judge — катастрофа
Попытка сделать judge "умнее" через Chain-of-Thought ("перечисли найденные ключевые слова → посчитай → выдай вердикт") вызвала регрессию с 144/180 до 48/180. GPT-4o-mini буквально искал английские строки в русском тексте. Простой промпт "match CONCEPTS, not strings" работает в 3 раза лучше.
4. Cosine re-ranking бьёт RRF
Hybrid search с Reciprocal Rank Fusion давал худшие результаты, чем cosine re-ranking по реальным embeddings из Neo4j. RRF хорош для combining разных сигналов, но когда оба сигнала — embedding-based, прямое cosine similarity точнее.
5. Embedding similarity для judge (threshold 0.65)
Для вопросов с reference answer: cosine similarity между ответом системы и эталоном >= 0.65 → auto-PASS. Калибровка: правильный ответ ~0.677, неправильный (другой документ) ~0.570. Порог 0.65 идеально разделяет.
6. Кросс-языковая маршрутизация
Русский вопрос о концепциях из английского документа (Doc2/SCL) ломает vector_search — он возвращает Doc1. Решение: детектируем кросс-языковой глобальный запрос → напрямую full_document_read вместо vector_search.
7. Comprehensive search размывает результаты
comprehensive_search (multi-query fan-out) генерирует N подзапросов → каждый через vector_search → RRF merge. Но если все подзапросы возвращают Doc1, то единственный full_document_read результат для Doc2 тонет в RRF-merge.
8. Self-correction loop должен сохранять лучшее
Ранний баг: каждая попытка перезаписывала предыдущие результаты. Если attempt 1 дал score 2.5, а attempt 2 — score 1.8, система возвращала 1.8. Фикс: трекаем best_results и best_score по всем попыткам.
9. Enumeration prompt — специальный формат
Для глобальных вопросов ("перечисли все...") обычный prompt генерирует текст, а не список. Специальный enumeration prompt: "Output a numbered list. Scan ALL chunks. Do not stop early."
10. Judge limit 500 → 2000 символов
Обрезка ответа до 500 символов для judge убивала enumeration-ответы (7 пунктов ~ 1500 символов). Увеличение до 2000 — мгновенный +5pp.
Typed API и провенанс
Каждый запрос создаёт PipelineTrace:
{ "trace_id": "tr_abc123def456", "router_step": { "method": "mangle", "decision": {"query_type": "simple", "suggested_tool": "vector_search"} }, "tool_steps": [{ "tool_name": "vector_search", "results_count": 10, "relevance_score": 3.2, "duration_ms": 150 }], "escalation_steps": [], "generator_step": { "model": "gpt-4o-mini", "confidence": 0.82 }, "total_duration_ms": 1800 }
API: FastAPI REST (/api/v1/) + FastMCP (SSE/MCP) — и для REST-клиентов, и для AI-агентов.
Цифры проекта
Метрика | Значение |
|---|---|
Python LOC | 16,206 (118 файлов) |
Тесты | 586 (320 core + 108 PyMangle + 158 rag-core) |
Зависимости | 26 пакетов |
Итерации бенчмарка | 10 (v2 → v14) |
Файлы результатов | 15 JSON (~4.7 MB) |
Mangle-правила | 111 строк (3 файла) |
Классы | 36 (14 data models, 8 services, 6 config, 4 reasoning) |
Что дальше
Personalized PageRank для query-focused графового обхода
Human evaluation в дополнение к LLM-as-judge
Streaming ответы в Streamlit UI
Больше Mangle-правил — temporal reasoning, conflict resolution
Стек
Если вам интересны детали реализации или вы хотите обсудить Graph RAG — пишите в комментариях или открывайте issue на GitHub.
