Гибридный GraphRAG над одним связным корпусом (Apache Kafka: Git + JIRA + Confluence/KIP): двухконтурный граф знаний в Neo4j (детерминированный скелет + LLM-обогащение с entity resolution), маршрутизируемый гибридный retrieval (вектор + BM25 + обход графа), генерация ответа с цитированием источников. Флагманское демо — «лог с ошибкой → что затронуто».
Пайплайн в четыре слоя. Каждый слой — отдельный модуль в src/graphrag/, переключаемый через config/settings.yaml.
Три коннектора тянут один связный корпус и нормализуют в единый JSONL:
- Git (
git.py) — код + история коммитов (Java, tree-sitter-парсинг символов). - JIRA (
jira.py) — тикеты, статусы, связи (дубликаты, «починено в»). - Confluence/KIP (
confluence.py) — вики-страницы, design-документы.
- Скелет (
skeleton.py) — детерминированный: модули, тикеты, страницы, люди и явные рёбра (DEPENDS_ON,MENTIONS,DUPLICATES,FIXED_BY,OWNS). Никакого LLM — воспроизводимо. - Обогащение (
enrich.py+resolve.py) — LLM-слой поверх: достаёт неявные связи и склеивает дубли сущностей (entity resolution). Отделено от скелета, чтобы граф оставался восстановимым без модели.
- Роутер (
router.py) классифицирует вопрос → маршрут:factual(точечный факт),multihop(обход графа по связям — только если в вопросе назван известный модуль),mixed(всё вместе — дефолт). - Гибрид (
hybrid.py) объединяет три сигнала: векторный поиск (bge-m3), BM25 (лексический), обход графа (Neo4j). Результаты сливаются и реранжируются (cross-encoder bge-reranker или лексический фолбэк).
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 | Доля релевантных фрагментов в извлечённом контексте. |
- Проиндексированы фикс-коммиты и их тело (описание фикса, не дифф). Это подняло 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 на этих ответах остаётся высокой), а не ухудшение.
Классификация всех провалов (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).
Проверили гипотезу «сменить жанр корпуса → меньше отказов» контролируемым 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 не слепым окном 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 общих вопроса.
Идея — при попадании 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).
Диагностик остатка (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.
Метрики выше судит тот же 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 (нет ответа в источнике) + переобобщение генератора; сокращается не поиском, а контентом.
Гипотезу «узкое место — ранжирование, а не поиск» проверили строгим парным 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, где пул собирался только из графа (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.
- 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. Тесты идут на оффлайн-фолбэках без загрузки моделей.
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-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).
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).