Comments 13
Спасибо за статью, очень интересный подход. Хотелось бы чуть лучше понять механику wiki_graph_context.
Правильно ли я понимаю, что сначала находятся seed-страницы через vector search, а потом к ним добавляются соседние страницы из графа? Если да, то интересно, как вы дедуплицируете seed/hop-страницы, как выбираете глубину обхода, откуда берутся веса для разных типов рёбер, и что именно отдаёте LLM – полный текст страниц или только релевантные фрагменты?
И ещё вопрос. Планируете ли вы где-нибудь выложить код (например, на GitHub)?
Да, именно так — двухэтапный retrieval. Вот механика:
Seed: Запрос - эмбеддинг - cosine similarity находит top-K (стоит хардкодом 5) страниц из graph_nodes. Это seed-ноды.
Hop : Каждая seed-нода раскрывается через graph_edges — прямые соседи (hop1) и соседи соседей (hop2). Глубина по умолчанию 1.
Дедупликация: SELECT DISTINCT ON (wp.path) — если одна и та же страница пришла и как seed, и как hop, остаётся seed (приоритет по relevance: seed > hop1 > hop2).
Веса: заранее заданная таблица внутри SQL-функции:
depends_on = 0.95, develops = 0.9, part_of = 0.85,
based_on = 0.8, alternative_to = 0.75, contradicts = 0.7,
authored_by = 0.5, tagged = 0.4, mentions = 0.3
Rank: rank_score = similarity × edge_weight / (depth + 1). У seed-нод rank = чистый cosine (edge_weight=1, depth=0). У hop-нод — discount по типу ребра и глубине.
Контент для LLM: страницы, обрезанные по relevance:
Общий лимит max_chars = 8000 символов на весь результат.
seed: left(content, max_chars / top_k)
hop1: 500 символов
hop2: 200 символов
Если нужен код, скажите ваш гитхаб аккаунт, я вас добавлю
Содержательно. Это именно то, что я хотел узнать. Спасибо за ответ.
Мой GitHub.
Да, именно так — двухэтапный retrieval. Вот механика:
Seed: Запрос - эмбеддинг - cosine similarity находит top-K (стоит хардкодом 5) страниц из graph_nodes. Это seed-ноды.
Hop : Каждая seed-нода раскрывается через graph_edges — прямые соседи (hop1) и соседи соседей (hop2). Глубина по умолчанию 1.
Дедупликация: SELECT DISTINCT ON (wp.path) — если одна и та же страница пришла и как seed, и как hop, остаётся seed (приоритет по relevance: seed > hop1 > hop2).
Веса: заранее заданная таблица внутри SQL-функции:
depends_on = 0.95, develops = 0.9, part_of = 0.85,
based_on = 0.8, alternative_to = 0.75, contradicts = 0.7,
authored_by = 0.5, tagged = 0.4, mentions = 0.3
Rank: rank_score = similarity × edge_weight / (depth + 1). У seed-нод rank = чистый cosine (edge_weight=1, depth=0). У hop-нод — discount по типу ребра и глубине.
Контент для LLM: страницы, обрезанные по relevance:
Общий лимит max_chars = 8000 символов на весь результат.
seed: left(content, max_chars / top_k)
hop1: 500 символов
hop2: 200 символов
Если нужен код, скажите ваш гитхаб аккаунт, я вас добавлю
Сначала показалось, что выдумывал сам что-то похожее...
но у меня всё без Вики и нейросетей...
Полной реализации нет, только визуализация вывода
https://www.walks.ru/wm_dr/
(лучше смотреть на большом экране)
Понравилось, как вы классифицируете связи через ai.generate() прямо в SQL, это красиво. Но это же LLM, она каждый раз отвечает по разному. Прогоните на новой модели и часть связей сменит тип, не угадаешь что выберет «зависит от» или «использует» поставит.
Поэтому версионирование рёбер, которое у вас пока в планах, я бы не откладывал, раз типы расставляет модель, надо хранить не только когда связь создана, но и какой моделью. Иначе граф будет плавать от прогона к прогону, и те самые 46 -> 68% уже не получатся.
Я в своем решении, запоминаю ответ модельки и переклассифицирую только вручную, когда понадобится, а не на каждом проходе.
И отдельный плюс за то, что права разруливаются через сам список инструментов, а не проверками внутри. Чего в списке нет, того агент не вызовет, даже случайно. Пожалуй стырю подход)
вообще правильнее было бы делать полноценные triples subject-predicat-object на входе и писать классическую онтологию, но это уже вообще другая задача. Здесь связи классифицируются только чтобы расставить веса для ранжирования выдачи, и эти веса не вносят очень большого вклада. Больше урона здесь наносит то, что все связи направленные, а функция классификации выбирает концепты как source и target в порядке их следования, и только потом начинает классифицировать связи между ними конечным списком классов отношений. То есть, если название технологии попадет в source а автор попадет в target, то связь "authored_by" там ляжет правильно, а если они зайдут в функцию наоборот, то связи "author_of" там нет.
Ну это всё будем допиливать, пока и так ищется, просто хопов в контекст попадает больше, токены лишние тратятся
Если возможно то хотелось бы тоже взглянуть на код, очень интересна эта тема.
Мой гитхаб - https://github.com/nickkovdev
Добавил.
Интересно, а как получилось, что у меня два ваших аккаунта в гитхабе отображается на один ник?
Спасибо! Пробежался по коду очень много интересных моментов, попробую применить на свою личную систему заметок.
А на счёт моего двойного аккаунта, если честно я сам не уверен как так вышло, у меня был в какой-то момент гх аккаунт учебный, личный и рабочий. Как-то неправильно выполнился merge аккаунтов и вот так теперь происходит.
Интересная статья, спасибо. Можно и мне посмотреть код? https://github.com/alukos
Wiki-MCP-Server с распределённым графом знаний и авторизацией