Skip to content

Repository files navigation

GraphRAG PoC

Гибридный GraphRAG над одним связным корпусом (Apache Kafka: Git + JIRA + Confluence/KIP): двухконтурный граф знаний в Neo4j (детерминированный скелет + LLM-обогащение с entity resolution), маршрутизируемый гибридный retrieval (вектор + BM25 + обход графа), генерация ответа с цитированием источников. Флагманское демо — «лог с ошибкой → что затронуто».


Как это работает

Пайплайн в четыре слоя. Каждый слой — отдельный модуль в src/graphrag/, переключаемый через config/settings.yaml.

1. Загрузка источников (connectors/)

Три коннектора тянут один связный корпус и нормализуют в единый JSONL:

  • Git (git.py) — код + история коммитов (Java, tree-sitter-парсинг символов).
  • JIRA (jira.py) — тикеты, статусы, связи (дубликаты, «починено в»).
  • Confluence/KIP (confluence.py) — вики-страницы, design-документы.

2. Двухконтурный граф знаний (graph/)

  • Скелет (skeleton.py) — детерминированный: модули, тикеты, страницы, люди и явные рёбра (DEPENDS_ON, MENTIONS, DUPLICATES, FIXED_BY, OWNS). Никакого LLM — воспроизводимо.
  • Обогащение (enrich.py + resolve.py) — LLM-слой поверх: достаёт неявные связи и склеивает дубли сущностей (entity resolution). Отделено от скелета, чтобы граф оставался восстановимым без модели.

3. Маршрутизируемый гибридный retrieval (retrieval/)

  • Роутер (router.py) классифицирует вопрос → маршрут: factual (точечный факт), multihop (обход графа по связям — только если в вопросе назван известный модуль), mixed (всё вместе — дефолт).
  • Гибрид (hybrid.py) объединяет три сигнала: векторный поиск (bge-m3), BM25 (лексический), обход графа (Neo4j). Результаты сливаются и реранжируются (cross-encoder bge-reranker или лексический фолбэк).

4. Генерация с цитированием (generate/answer.py)

LLM формирует ответ строго по извлечённому контексту и проставляет ссылки на источники (JIRA-URL, chunk-id страницы). Если контекста недостаточно — честно воздерживается («на основе контекста ответить невозможно») вместо галлюцинации.

Флагманский сценарий (retrieval/impact.py): лог с ошибкой → упавшие модули → затронутые по DEPENDS_ON → владельцы → тикеты «уже чинили» → страницы вики. Всё со ссылками.


📊 Текущие результаты качества

Замер от 2026-08-02 на n=300, генератор и судья — DeepSeek (deepseek-chat), эмбеддер — bge-m3, reranker — cross-encoder (bge-reranker-v2-m3), multihop full-retrieval вкл., фикс-коммиты и их тело проиндексированы — это прод-конвейер. Полный отчёт: eval/trial/quality_snapshot_report.md, сырьё: eval/trial/quality_snapshot_results.json.

Набор данных: корпус Apache Kafka — Git (~61k записей код+коммиты), JIRA (~1.8k записей), Confluence/KIP (~1.3k записей). Вопросы сгенерированы из этого корпуса автоматически.

На скольких мерили: 300 вопросов из зафиксированного набора (вырос с 104 — на малом n метрики были шумны, особенно по маршрутам). Все метрики здесь reference-free; reference-required (correctness/recall) в этот снимок не входят — им нужен размеченный срез с эталонами.

Метрика Значение Замерено (n/всего) Что значит
Abstention rate 14% (43/300) 300/300 Доля «не знаю». Система честно молчит вместо вранья.
Faithfulness 0.71 257/300 Доля утверждений ответа, подтверждённых контекстом. Двухполюсно: 64×[0–0.2], 172×[0.8–1.0].
Answer relevance 0.57 300/300 Насколько ответ бьёт в вопрос. См. оговорку про honest-refusal ниже.
Context precision 0.69 300/300 Доля релевантных фрагментов в извлечённом контексте.

Что улучшилось (относительно прежнего снимка n=96: faith 0.59 / relev 0.72 / prec 0.61)

  • Проиндексированы фикс-коммиты и их тело (описание фикса, не дифф). Это подняло faithfulness 0.59 → 0.71 и context precision 0.61 → 0.69; по маршрутам сильнее всего вырос multihop (faith 0.33 → 0.57). Достижимость how-to-фиксов в прод-top_k выросла ×2 (5→10 на замерном срезе): генератор получил реальный контент фикса и стал меньше выдумывать.
  • Answer relevance просела 0.72 → 0.57 — и это в основном НЕ регресс. Получив настоящий контент, модель чаще честно оговаривается «в источнике этого нет» вместо уверенной выдумки; judge relevance штрафует такую честность нулём. По разбору падений relevance бо́льшая часть — именно honest-refusal (faithfulness на этих ответах остаётся высокой), а не ухудшение.

🎯 Где ломается (оцифровано на n=300)

Классификация всех провалов (relevance<0.5 ИЛИ faithfulness<0.5 ИЛИ воздержание) по source_id-якорю:

Доля провалов Доля всех вопросов
Провалы всего 142/300 47%
CONTENT_PROBLEM — источник извлечён, но ответа в нём нет 87% 41%
RETRIEVAL-пробел — источник не найден 13% 6%

Вывод — узкое место найдено и оцифровано: не пайплайн, а жанр корпуса. Ретрив практически выжат (причина лишь 6% провалов). 87% провалов = content_problem: правильный тикет найден, но how-to-решения в его тексте нет — JIRA-тикеты описывают проблемы, коммиты — изменения, а спрашивают how-to-инструкцию. Ещё индексация того же жанра это не закроет; следующий рычаг — источники другого жанра (dev-рассылки, KIP с обоснованием, треды ревью PR).

✅ Рычаг подтверждён: KIP-документы поднимают answer-rate на ~+9 п.п. (устойчиво)

Проверили гипотезу «сменить жанр корпуса → меньше отказов» контролируемым before/after — единственная разница — наличие KIP (Kafka Improvement Proposals, how-to-жанр «как и почему сделать») в графе:

answer-rate
без KIP 55.7%
с KIP (полный KIP-индекс) 63.7%
с KIP (частичный индекс, независимый прогон) 65.3%

Парный лифт (284 общих вопроса): 54.9% → 63.7% = +8.8 п.п., чисто +25 отвеченных (42 «не знаю»→ответ, 17 обратно). KIP реально работают в ретриве: дошли до top-k у 102/128 безответных (на baseline page-чанков в top-k было 0) — семантический матч по телам KIP через bge-m3. Качество не просело: faithfulness 0.977, relevance 0.807, precision 0.813 (судья Qwen).

Устойчивость: два независимых AFTER-прогона (полный и частичный индекс) дали 63.7% и 65.3% — сошлись близко, лифт +8–10 п.п. много выше межпрогонного LLM-шума (~1.5 п.п.). Эффект реален, не артефакт одного прогона.

Тонкость: дешёвый лексический pre-check (совпадение слов в заголовках KIP) давал ЛОЖНЫЙ NO-GO — он недооценивал, потому что система цепляет KIP по смыслу, а не по словам. Урок: лексический прокси нельзя ставить гейтом на семантический пайплайн.

Оговорки: 17 регрессий (KIP иногда вытесняет полезный контекст); 5 из 128 не нашли пары в AFTER (знаменатель чуть усечён). Артефакты и воспроизведение — eval/kip_delta.py, scripts/reproduce_kip_measure.sh.

✅ Секционный чанкинг KIP: +2.4 п.п. (валидно, faithfulness держит)

Идея: резать длинные KIP не слепым окном 800 символов, а по заголовкам разделов (Proposed Changes / Compatibility …), чтобы ответ-несущий раздел был цельным чанком.

Путь был кривой и честно об этом: первая версия дала «+2.3 п.п.», но замер был невалиден — разметка разделов так и не доехала в граф (из ~490 KIP-страниц размеченными были 2; билды убивали на эмбеддинге, restore откатывал текст). Мерилось слепое против слепого. После починки конвейера (scripts/rebuild_section_chunks.py строит секционный граф под защитой бэкапа + resume) и пере-замера на здоровом графе, где разметка реально применена (490/571), под гвардом, подтвердившим, что before/after различаются ровно нарезкой:

answer-rate faithfulness
слепой чанкинг 60.9% 0.981
секционный чанкинг 63.3% 0.979

+2.4 п.п. (нетто +7 вопросов: 25 отказ→ответ, 18 обратно), faithfulness держит (−0.002, шум). Эффект скромный (~1.5× шумового пола), но направленный и без размена на faithfulness. Покрытие 86% (81 страница из старого бэкапа без HTML-исходника осталась слепой) — истинный эффект полной нарезки может быть чуть выше. Замер: reserve=0, соседи=0, судья Qwen, 294 общих вопроса.

✅ Neighbor-expansion: работает ПОВЕРХ секций (+1.8 п.п.)

Идея — при попадании KIP-чанка в top-k дотягивать соседние чанки той же страницы (kip_neighbors), чтобы разрезанная процедура собралась целиком. Результат зависит от нарезки:

граф соседи 0→1 faithfulness
блёклый чанкинг +0.4 п.п. (wash, churn 15↔14) держит
секционный чанкинг +1.8 п.п. (63.9→65.6, нетто +5: 20↔15) 0.981→0.978

Соседи и секции дополняют друг друга: сосед блёклого чанка — случайный обрывок (шум), а сосед секционного — цельный соседний РАЗДЕЛ (полезно). Поэтому на блёклом графе рычаг был wash, а на секционном даёт +1.8 п.п. Эффект на кромке шумового пола (слабее секций), но направленный и faithfulness держит. Включён (kip_neighbors: 1) поверх section-чанкинга. Прежние «−10.7 п.п.» были невалидны (раннер не применял флаг → мерил X vs X).

✅ PR-ревью apache/kafka: код-уровневый how-to источник (+6.6 п.п., faithfulness РАСТЁТ)

Диагностик остатка (eval/wide_ceiling.py) показал: 83% «partial»-отказов и большинство «no» — дыры корпуса (ответа нет нигде, даже широким ретривом), причина — жанр: Git+JIRA+KIP знают что чинили, но не как. «Как» живёт в ревью-тредах PR apache/kafka (обсуждение «делай так, это сломает X»). Дешёвый гейт (eval/pr_answer_presence.py, судья Qwen) подтвердил: у 45% отказов ответ реально присутствует в PR-треде (не просто число комментов) → GO.

Новый коннектор (src/graphrag/connectors/github_pr.py) выгружает описание PR + тред ревью через gh api для тикетов-в-графе, узлы PullRequest + chunk:pr:*, связь MENTIONS→Task. Фильтр шума (pr_filter.py) отсекает бот/CI/LGTM. Diff не тащим (он в графе через коммиты). Ceiling-замер (источник для замерного набора, before переиспользован, судья Qwen, гвард):

answer-rate faithfulness
стек section+neighbors 65.7% 0.977
+ PR-ревью 72.3% 0.991

+6.6 п.п. (нетто +19: 35 отказ→ответ, 16 обратно), faithfulness РАСТЁТ (0.977→0.991) — крупнейший рычаг после KIP и без размена: код-уровневые ответы снижают домысливание генератора. Ceiling (верхняя оценка — источник ровно под замерные тикеты); генерализация на полном корпусе — в todo. Гейт U6 (45% answer-presence) верно предсказал GO; гвард по дороге поймал 2 бага (дубль kip_neighbors в конфиге; хрупкий 3-пробный pre-flight → усилен до 12).

🔧 Дисциплина измерений + починка регрессии графа

Оба провала выше — одна причина: замер без проверки, что рычаг реально активен (флаг не долетал до ретрива; разметка — в граф). Появился гвард eval/measure_guard.py: pre-flight probe (падает, если фича не меняет вход) + fingerprint графа/конфига (before/after обязаны различаться ровно тестируемой осью). Тесты — tests/test_measure_guard.py.

Тот же разбор вскрыл регрессию прода: у 473/571 KIP-страниц был текст, но не было чанков (следствие убитых билдов) — KIP стал невидим вектор-поиску, answer-rate тихо просел к до-KIP уровню (~54%). Починка — scripts/fill_kip_chunks.py (досоздаёт недостающие чанки, resume-safe, аддитивно): KIP-страниц с чанками 98 → 571/571, KIP-в-контексте восстановлен до 69.7% (сломанный 57.9%, историч. здоровый 68.7%). Здоровый граф забэкаплен под гвардом.

Прим. Интеграционные тесты вайпают Neo4j (conftest: DETACH DELETE). pytest по умолчанию их пропускает (addopts = -m 'not integration'), чтобы обычный прогон не стирал рабочий граф; явный прогон — pytest -m integration. Бэкап/восстановление графа — scripts/neo4j_backup.sh / neo4j_restore.sh.

🧪 Проверка самого судьи: доверенный судья через человеческий gold

Метрики выше судит тот же DeepSeek, что и генерит ответы (само-оценка → круг предвзятости). Чтобы разорвать круг, добавили независимого судью Qwen (build_llm(role="judge")), пере-судили те же ответы и собрали человеческий gold (42 записи: 12 honest-refusal + 30 не-отказов, размечены через eval/gold_ui.py) как якорь-правду. Согласие двух LLM — лишь консистентность; правоту решает человек.

Кто ближе к человеку (MAE к gold, меньше = лучше) — по-метрично, а не одним числом:

метрика DeepSeek Qwen вывод
faithfulness 0.417 0.125 Qwen решительно ближе → доверяем Qwen
answer_relevance (отказы) 0.533 0.354 DeepSeek занижал честные отказы → Qwen ближе
answer_relevance (не-отказы) 0.237 0.282 судьи сопоставимы
context_precision (не-отказы) 0.135 0.205 судьи сопоставимы

Важно: средний MAE по метрикам говорил бы «Qwen глобально лучше», но это маскировало разворот — всё превосходство держится на faithfulness. Поэтому вердикт по каждой метрике: Qwen доверен на faithfulness и на отказной relevance; на не-отказных relevance/precision судьи сопоставимы (глобального превосходства не заявляем — разница мала, n=30, один разметчик).

Исправленный baseline без survivorship. Сравнивать полное среднее Qwen (по не-воздержаниям) с полным DeepSeek (по всем) — разный знаменатель. На общем не-воздержавшемся наборе (оба судьи дали число):

метрика DeepSeek Qwen (доверенный)
faithfulness 0.824 0.985
answer_relevance 0.577 0.745
context_precision 0.694 0.804

Опубликованные выше DeepSeek-цифры (faith 0.71 / relevance 0.57) занижены его же занижением честных отказов — по доверенной оценке faithfulness системы ≈ 0.98. Оговорки: gold мал (42, один разметчик) — вывод директивный. 104 «faith = None» у Qwen оказались 96 честными воздержаниями + 8 транзиентными сбоями (не баг промпта — при пере-судействе JSON валиден; добавлен ретрай). Артефакты: eval/trial/judge_calibration_report.md, eval/trial/trusted_baseline_report.md, eval/cross_judge.py, eval/judge_calibration.py.

⚠️ Оговорки к цифрам (честность метода)

  • Само-оценка (проверена, см. выше): DeepSeek и генерит, и судит. Калибровка против человека показала, что на honest-refusal он занижает faith/relevance — цифры оптимистичны НЕ везде, это потолок метода, а не абсолютная истина. answer relevance особенно занижен на честных «в источнике не указано» (см. выше).
  • n=300 заметно надёжнее прежнего n=96/104: на 3× масштабе метрики не обвалились, малый n был чуть оптимистичен (faith 0.78 на n=104 → 0.71 на n=300).
  • Faithfulness двухполюсна: ответ либо полностью обоснован (172 записи ≈ 1.0), либо явно выдуман (64 записи ≈ 0.0), середины почти нет. Полюс 0.0 — это преимущественно content_problem (нет ответа в источнике) + переобобщение генератора; сокращается не поиском, а контентом.

🔬 Эксперимент: сильнее ли cross-encoder? (recall-гейт → A/B → порог)

Гипотезу «узкое место — ранжирование, а не поиск» проверили строгим парным A/B на тех же 13 вопросах (lexical vs cross-encoder bge-reranker-v2-m3) — с recall-гейтом на входе и guardrail на воздержания. Харнесс: eval/recall_gate.py, eval/ab_eval.py, eval/threshold_calib.py.

  • Recall-гейт: 0.88 — для 7 из 8 воздержавшихся вопросов нужный фрагмент уже в пуле кандидатов до реранка. Значит лимитирует ранжирование, а не поиск. ✅
  • A/B по воздержаниям: 62% → 38% — cross-encoder отвечает на 3 вопроса больше (5 → 8). ✅
  • A/B по качеству отвечённых: not_supported — там, где ответили обе версии, cross-encoder head-to-head не лучше (дельты в основном нули, есть регрессии). Строгий предрегистрированный критерий (строго положительная дельта на ≥ 5 вопросах) не выполнен. ❌
  • Порог отсечения: неубедительно — помогает лишь на уровне шума (порог ~0.01: precision 0.42→0.48); любой осмысленный порог поднимает precision до 0.63 ценой роста воздержаний → guardrail отклоняет. Оставлен выключенным.

Вывод: cross-encoder ценен тем, что отвечает чаще, а не тем, что отвечает качественнее. Разброс между прогонами (воздержания 0.38 ↔ 0.46 на одном конфиге) подтверждает: n=13 + само-судья слишком шумны для тонких выводов — приоритет №1 теперь расширение набора оценки. Артефакты: eval/trial/ab_report.md, eval/trial/recall_gate_results.json, eval/trial/threshold_calib.json.

🎯 Триаж на выросшем наборе: первый твёрдый вердикт

Сделали ровно то, к чему привёл вывод выше — заменили хрупкий критерий на парный перестановочный тест (точен при любом n; eval/ab_eval.py) и вырастили набор (eval/grow_set.py). Тест даёт три честных исхода: «лучше A/B» · «неразрешимо + нужно ~n вопросов» · эквивалентность. Как вердикт сходится с ростом данных (в скобках — совместно-отвечённых):

Метрика n=13 (5) n=46 (21) n=104 (57)
faithfulness −0.20, p=1.0 +0.20, p=0.19 +0.17, p=0.031 → cross-encoder лучше
context_precision −0.03, p=1.0 +0.076, p=0.11 +0.036, p=0.10 (нужно ~159)
answer_relevance −0.02, p=1.0 +0.007, p=0.82 +0.005, p=0.78 (эквивалентно)
  • На n=13 знак эффекта был неверным (все отрицательные) — маленькая выборка не различала даже направление; по мере роста знак стабилизировался.
  • faithfulness взял значимость (p=0.031): cross-encoder реально лучше по верности контексту.
  • answer_relevance — эквивалентность (эффект ≈ 0); context_precision — эффект мал (нужно ~159).

Итог: cross-encoder значимо лучше по faithfulness, эквивалентен по relevance; а инструмент честно говорит «неразрешимо, нужно ~n», а не выдаёт шум за вывод. Артефакты: eval/trial/ab_report_grown.md, eval/trial/ab_results_grown.json.

🔧 Multihop: убран граф-only ретрив (замер 2026-07-23)

Разбор воздержаний на выросшем наборе показал: значимая доля — вопросы с именем модуля, уходящие в маршрут multihop, где пул собирался только из графа (2 узла-модуля), без вектора/BM25 — фактических чанков нет, генератор вынужден воздержаться. Проверка: для всех 13 таких вопросов vector+BM25 достают точный тикет на 1-м месте — ответ в корпусе есть, терялся только из-за маршрута. Фикс: на multihop домешивать вектор+BM25 к графу (retrieval.multihop_full_retrieval, дефолт вкл; срез top-k не вытесняет граф-узлы). Замерили прямым A/B (graph-only vs full, общий cross-encoder, 96 пар; eval/ab_eval.py multihop):

Сигнал graph-only full
Воздержания на multihop-подмножестве (n=13) 13/13 2/13 ✅ 11 восстановлено
Воздержания на всём наборе 32% 23% ✅ −9 пунктов
context_precision новоотвеченных 0.83 ✅ правильные источники поднялись
answer_relevance новоотвеченных 0.81 ✅ ответы по теме
faithfulness новоотвеченных 0.33 ⚠️ ответы слабо подкреплены

Вывод: ретрив-фикс закрыл воздержания-от-маршрутизации (правильные тикеты в top-5, precision 0.83), но новые ответы неверны по faithfulness (0.33) — вопросы «как обойти/ исправить», а корпус содержит описание бага, не how-to-фикс; генератор переобобщает. Correctness-гейт (faithfulness/precision новоотвеченных) поймал это — иначе «32%→23%» читалось бы как чистая победа. Следующий рычаг качества — генератор / how-to-покрытие корпуса, не ранжирование. Артефакты: eval/trial/multihop_ab_report.md, eval/trial/multihop_ab_results.json.


Быстрый старт

# 1. Зависимости (ядро + dev). ML-модели (bge-m3) — по надобности: --extra ml
uv sync

# 2. Секреты
cp .env.example .env   # заполнить NEO4J_PASSWORD, LLM_API_KEY

# 3. Neo4j
docker compose up -d

# 4. Проверка
uv run graphrag info
uv run graphrag health     # связь с Neo4j
uv run pytest -q

Секреты. .env в .gitignore — реальный ключ API в репозиторий не попадает. В git только шаблон .env.example с плейсхолдером. Neo4j слушает только loopback (127.0.0.1), пароль из .env.

Воспроизводимость и бэкап графа

Граф строится из data/intermediate/*.jsonl (graphrag build --index), но полный эмбеддинг корпуса — ~1.5ч на CPU (bge-m3). Чтобы не терять граф (интеграционные тесты вайпают Neo4j) и восстанавливать за секунды:

scripts/neo4j_backup.sh baseline     # тар bind-mount тома neo4j_data -> backups/
scripts/neo4j_restore.sh backups/neo4j_baseline_*.tar.gz

Полный KIP before/after замер воспроизводится одним скриптом (гейтированный, фазы по очереди):

scripts/reproduce_kip_measure.sh     # rebuild граф -> BEFORE -> ingest KIP -> AFTER -> kip_delta

Результаты замера — eval/trial/kip_delta_report.md и снимки quality_snapshot_results_{before,after}.json.

Рантайм под слабый GPU/CPU

  • LLM — provider-agnostic: llm.provider: api (дев, OpenAI-совместимый эндпоинт вроде DeepSeek) или ollama (локальный swap-in).
  • Эмбеддер: embeddings.provider: sentence_transformers (bge-m3, CPU) или hashing (оффлайн/тесты).
  • Reranker: cross_encoder (bge-reranker) или lexical (оффлайн, без загрузки модели).

Всё переключается в config/settings.yaml. Тесты идут на оффлайн-фолбэках без загрузки моделей.

Демо «лог → impact»

docker compose up -d
uv run python examples/seed_demo.py                       # демо-граф без корпуса
uv run graphrag log-impact examples/logs/broker_unavailable.log

Выдаёт: упавшие модули → затронутые (обход DEPENDS_ON) → владельцы → тикеты «уже чинили» со ссылками → страницы вики со ссылками.

seed_demo.py сеет расширенный демо-граф (10 модулей с DAG зависимостей, 12 тикетов со связями DUPLICATES/FIXED_BY, люди, KIP-страницы). Из него — демо-golden set для eval:

uv run python examples/build_golden.py   # -> eval/golden_demo.json (12 пар вопрос→узлы)
uv run graphrag eval                      # retrieval-метрики (нужен вектор-индекс)

⚠️ Integration-тесты ходят в тот же dev-Neo4j и чистят базу. После pytest демо-граф надо пересеять (seed_demo.py).

Веб-интерфейс (Gradio)

Тонкий Gradio-UI над сервисным слоем (src/graphrag/service.py), теми же функциями, что и CLI. Вкладки: Ask, Log → Impact, Health & Info.

uv sync --extra ui        # поставить gradio
docker compose up -d      # Neo4j; для Ask — LLM_API_KEY в .env или provider: ollama
uv run graphrag serve-ui  # http://127.0.0.1:7860

Опции: --host, --port, --share (публичная ссылка Gradio).

Команды CLI

graphrag info | health          # конфиг, связь с Neo4j
graphrag ingest [--jira --confluence]   # выгрузка источников -> JSONL
graphrag build [--index]        # скелет графа + векторный индекс
graphrag enrich [--resolve]     # LLM-обогащение + склейка дублей
graphrag log-impact <file>      # «лог с ошибкой -> что затронуто»
graphrag ask "<вопрос>"         # ответ с цитированием (маршрут-осознанный)
graphrag eval                   # golden set + retrieval-метрики
graphrag ru-validate            # bge-m3 vs multilingual-e5 на RU-срезе
graphrag sync                   # инкрементальный ре-sync (пересчёт затронутого)
graphrag eval-quality           # массовая оценка качества ответов + отчёт
graphrag serve-ui               # Gradio-интерфейс (extra ui)

Массовая оценка качества (методика)

graphrag eval-quality генерит вопросы из корпуса, прогоняет их через ask и оценивает ответы стандартными RAG-метриками, складывая разобранный отчёт:

uv run graphrag eval-quality --n 200 --slice-size 40   # -> eval/quality_report.md
  • Reference-free (на всём объёме, эталон не нужен): faithfulness, answer relevance, context precision.
  • Reference-required (на LLM-размеченном срезе): answer correctness, context recall.
  • Retrieval P/R/F1 — отдельной секцией на графовом golden set (другая популяция вопросов, напрямую с метриками выше не сопоставимо).

Отчёт даёт распределения (не только среднее), разбивки по маршруту и типу источника, долю воздержаний (воздержание ≠ сбой судьи) и примеры провалов. Семантика оценок: None = судья не смог оценить (исключается из среднего, не считается нулём); воздержание генератора = ответа по существу нет (тоже N/A, а не ноль).

Статус

Пайплайн реализован целиком, 275 тестов зелёные. Покрыто:

  • коннекторы (Git/JIRA/Confluence) → скелет графа → векторный индекс → сценарий «лог → impact» → генерация с цитированием;
  • гибридный retrieval с маршрутизацией → LLM-обогащение + entity resolution;
  • golden set + метрики качества → RU-валидация эмбеддера → инкрементальный ре-sync.

Валидировано на реальном корпусе Apache Kafka с живым LLM (DeepSeek). Текущее качество и ограничения — в разделе «Текущие результаты» выше.

Известные ограничения

  • Узкое место — жанр корпуса, не пайплайн (оцифровано на n=300). Ретрив практически выжан: причина лишь 6% провалов. 87% провалов = content_problem — правильный тикет найден, но how-to-решения в его тексте нет (JIRA = проблемы, коммиты = изменения ≠ how-to-инструкция). Рычаг подтверждён: добавление KIP (how-to-жанр) подняло answer-rate на ~+9 п.п. (55.7%→63.7%, устойчиво по двум прогонам) на контролируемом before/after (см. «Текущие результаты» выше). Следующие жанровые источники — dev-рассылки, треды ревью PR.
  • Retrieval precision — историческое узкое место; закрыто индексацией фикс-коммитов и их тела (faith 0.59→0.71, multihop 0.33→0.57; см. «Текущие результаты» выше).
  • Судья невоспроизводим при temperature=0; само-оценка одной моделью завышает цифры; answer relevance дополнительно занижена на честных «в источнике не указано» (honest-refusal).

About

Гибридный GraphRAG PoC над корпусом Apache Kafka (код+тикеты+вики): граф Neo4j, маршрутизируемый retrieval (вектор+BM25+граф), генерация с цитированием

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages