3 апреля 2026 года Андрей Карпати описал реально работающую систему, где он складывает необработанные исследовательские материалы в папку, показывает их LLM, которая с нуля создает и поддерживает всю взаимосвязанную вики-систему. ИИ пишет статьи, создает обратные ссылки между связанными идеями, классифицирует концепции и постоянно обновляет всю систему по мере поступления новых материалов. Вот промпт, который всё это делает LLM Wiki gist
Его новый рабочий процесс превращает необработанные исследовательские материалы в самоподдерживающуюся вики, базу знаний без RAG, только файлы Markdown и LLM, которая выполняет функции библиотекаря. Вместо того чтобы просматривать необработанные документы по каждому запросу, как в RAG, здесь LLM считывает исходный материал один раз и компилирует его в структурированную, организованную вики-систему. Его исследовательская вики-страница по одной теме разрослась примерно до 100 статей и 400 000 слов.
Мы взяли эту идею и инфраструктурно доработали:
Аспект |
Karpathy LLM Wiki |
Наша реализация |
|---|---|---|
Хранение |
Markdown-файлы в папке |
AlloyDB + pgvector (SQL) |
Связи |
|
Типизированные рёбра графа (11 типов) |
Поиск |
Нет (или grep) |
Векторный + графовый гибрид |
Обновление |
LLM переписывает markdown |
SQL-функции + LLM-классификация из БД |
Доступ |
Локальная папка |
HTTP-сервер с авторизацией |
Мультипользовательность |
Нет |
Роли admin и reader |
Протокол |
— |
MCP (StreamableHTTP + SSE) |
Классификация связей |
LLM при записи |
|
Ключевое отличие: у Карпати wiki — это заметки Obsidian, которые LLM читает и пишет. У нас wiki — это база данных с графом, к которой несколько агентов обращаются через стандартизированный протокол. Карпати решал задачу «один человек + один LLM ведут заметки». Мы решаем задачу «несколько AI-агентов + человек совместно работают с растущей базой знаний». Разные масштабы — разная инфраструктура.
С чего всё начиналось
Домашняя wiki уже давно была назрела, так как проект оказался непрост. Смысл проекта - добавить блоки Titans в стандартную Gemma 3 и посмотреть что из этого выйдет. Технология Titans описана здесь
Конечно, сначала мы реализовали подход Karpaty, добавив векторный поиск по эмбеддингам.
Потом мы добавили граф знаний с типизированными рёбрами. Выбор технологии построения графа потребовал размышлений и занял некоторое время: Как выбирали технологию построения графа Но с ИИ-кодером реализация всегда быстрее, чем выбор технологий. Раз-раз, реализовали и увидели неплохой рост recall:
Метрика |
До (вектор) |
После (вектор + граф) |
|---|---|---|
Avg recall |
46.7% |
68.3% |
Enrichment |
— |
92 концепции найдены графом |
… И серьезный прирост на абстрактных запросах: |
Запрос |
Было |
Стало |
|---|---|---|
«Что общего у MesaNet и Titans» |
0% |
67% |
«Альтернативы Softmax attention» |
0% |
67% |
«Ассоциативное сканирование» |
50% |
100% |
«Дистилляция в TTT» |
67% |
100% |
Граф в общем не нужен на точных запросах. Но там, где человек спрашивает «как связано X и Y», граф находит путь: Линейные RNN → Ассоциативное сканирование → NLM → M3 Optimizer — три прыжка по концепциям, которые векторный поиск не найдёт. Граф – это очень круто. |
И всё было хорошо, пока wiki была личным инструментом. Сервер, который это всё обслуживал, был однопользовательским. stdio-транспорт — процесс запускается, обслуживает одного клиента, умирает. Это агент OpenClaw написал себе такую локальную базу знаний, и у него можно теперь спрашивать в телеграме. Точнее у нее, ее зовут Мнемозина, и она богиня знаний. Но. Хоть и прикольно писать код в телеграме, но иногда хочется вернуться в VS Code, да и проект, ради которого всё вот это, лежит на локальной машине. А значит нужен mcp-server. Stdio-транспорт MCP подразумевает, что сервер — это дочерний процесс клиента. Клиент подключился → сервер родился → клиент отключился → сервер умер. Для CLI-утилиты это нормально, но для сервера знаний, к которому стучатся 3-4 агента одновременно — не работает. Кроме того, чтобы подключить удалённого агента, приходилось пробрасывать порт через SSH-туннель со всеми паролями и явками, или запускать отдельный SSE-прокси. Кстати, запустили такой прокси, нормально отдает наружу. Но без авторизации. Все инструменты — и читающие, и пишущие — доступны любому, кто подключился. Агент-читатель случайно дёрнет graph_classify_edge — и перепишет типы рёбер.
Что изменилось в v3
StreamableHTTP + SSE в одном процессе
Мы перешли на двухтранспортную архитектуру в рамках одного Express-приложения:
POST/GET/DELETE /mcp → StreamableHTTP (новый протокол MCP 2025-11-25) GET /mcp/sse + POST /mcp/messages → SSE (legacy, протокол 2024-11-05)
StreamableHTTP — это свежий стандарт MCP transport. Один endpoint /mcp, на нем три HTTP-метода. Клиент может открыть долговременное соединение, сервер держит сессию в памяти через InMemoryEventStore. Старые клиенты, которые умеют только SSE, подключаются через /mcp/sse — и это тоже работает.
Зачем оставлять два транспорта? Не все MCP-клиенты уже перешли на StreamableHTTP. SSE-legacy гарантирует, что ничего не сломается. Зачем это в нашем камерном проекте? А чтобы было.
Ролевая модель: admin и reader
Два уровня доступа:
Роль |
Инструменты |
Что может |
|---|---|---|
admin |
Все read + |
Чтение + мутации графа |
reader |
|
Только чтение |
Роль определяется при подключении — через Basic Auth. Admin знает пароль админа, reader знает пароль читателя. Инструменты, недоступные роли, просто не регистрируются в MCP-сервере этой сессии. Ограничение здесь на уровне регистрации инструментов, а не runtime-проверки. MCP-клиент получает список доступных инструментов при initialize. Если инструмента нет в списке — он просто не появится в UI, не попадёт в tool_choice модели, не будет случайно вызван.
Кстати интересно получилось: мы дали другому агенту OpenClaw админский доступ, и он сказал, что инструменты изменения wiki он видит, но использовать не имеет права, потому что он простой кодер. То есть, дословно:
Сейчас у меня в подключены 15 инструментов через reader. Если я не прокинул write-инструменты — скорее всего это политика
tools.profile: "coding", которая фильтрует мутационные MCP-инструменты.
Разделение по ролям решается на сервере так:
function buildServer(role) { const server = new McpServer({ name: 'titans-wiki', version: '3.0.0' }); registerReadTools(server); if (role === 'admin') registerAdminTools(server); return server; }
При новом подключении — создаётся инстанс McpServer с нужным набором инструментов, подключается к выбранному транспорту. Всё.
Архитектура v3
┌─────────────────────┐ │ Express + CORS │ │ Port 8000 │ └──────────┬──────────┘ │ ┌──────────▼──────────┐ │ Auth Middleware │ │ Basic / Bearer │ └──────────┬──────────┘ │ ┌───────────────┼───────────────┐ │ │ ┌──────────▼──────────┐ ┌──────────▼──────────┐ │ StreamableHTTP │ │ SSE (legacy) │ │ /mcp │ │ /mcp/sse │ │ POST/GET/DELETE │ │ + /mcp/messages │ └──────────┬──────────┘ └──────────┬──────────┘ │ │ └───────────────┬───────────────┘ │ ┌──────────▼──────────┐ │ buildServer(role) │ │ ┌─ registerRead │ │ └─ registerAdmin │ (только admin) └──────────┬──────────┘ │ ┌──────────▼──────────┐ │ AlloyDB + pgvector │ │ graph_nodes (72) │ │ graph_edges (215) │ │ wiki_pages │ └─────────────────────┘
LLM из SQL: ai.generate() в AlloyDB
Один из самых изящных трюков — классификация рёбер прямо из базы данных:
CREATE OR REPLACE FUNCTION graph_classify_edge( _source_label TEXT, _target_label TEXT, _context TEXT DEFAULT NULL ) ... _result := ai.generate(_prompt); ...
AlloyDB Omni с версии 1.5.2 поддерживает google_ml_integration — можно вызвать Gemini прямо из plpgsql. Один пакетный прогон — и 205 нетипизированных рёбер получили конкретные типы (depends_on, develops, based_on). Стоимость: $0.01 на весь прогон.
Трудности, с которыми столкнулись
Направленность рёбер. MesaNet → Conjugate Gradient Solver — это depends_on? uses? used_in? Пришлось разделить uses (A использует B) и used_in (B применяется в A), плюс depends_on (A не может без B). Двукратный прогон классификации.
Дубли рёбер. После LLM-классификации одна пара страниц имела два ребра: depends_on и mentions. Функция
graph_dedup_edges()оставляет ребро с максимальным весом.authored_by наоборот. Gemini иногда ставила person → paper вместо paper → person. Правило:
authored_byтолько для конкретных работ, не для обзорных тем.Версия AlloyDB. Для
ai.generate()нуженgoogle_ml_integration ≥ 1.5.2. У нас была 1.4.3. Обновление контейнера с 16.8.0 до 16.11.0 с перепривязкой volume — аккуратная операция с данными на борту.
Многопользовательский сценарий: как это работает сейчас
У нас два OpenClaw-агента:
Мнемозина (основной) — подключается с ролью admin, может всё: искать, читать, обновлять граф, классифицировать рёбра.
Полифем (второй агент) — подключается с ролью reader, только читает.
graph_upsert_nodeиgraph_classify_edgeу него просто не появляются в списке инструментов.gemini-cli на локальной машине – reader
Cline в VS-Code – reader
Набор инструментов v3
Read-инструменты (доступны всем):
Инструмент |
Назначение |
|---|---|
|
Чтение содержимого страницы |
|
Связи-[[wikilinks]] из страницы |
|
Страницы по тегу / все теги |
|
Обратные ссылки на страницу |
|
BFS-обход от ноды |
|
Кратчайший путь между концептами |
|
Гибридный retrieval (вектор + граф) |
|
Все рёбра определённого типа |
|
Статистика графа + сироты |
|
Поиск противоречий |
Admin-инструменты (только admin):
Инструмент |
Назначение |
|---|---|
|
Создать/обновить ноду |
|
LLM-классификация типа ребра |
|
Удаление дублей, селф-лупов, разворот directed-ошибок |
Чему мы научились
Граф + вектор > вектор. На абстрактных запросах гибридный retrieval выигрывает у чистого векторного поиска. На точных — не проигрывает. Нет причин не использовать граф, если данные позволяют.
Типы рёбер — это не украшение, а архитектура. 11 типов с весами (depends_on = 0.95, mentions = 0.3) позволяют ранжировать hop-результаты осмысленно, а не просто «найдено на расстоянии 2».
Авторизация на уровне регистрации инструментов надёжнее runtime-проверок. Если инструмент не зарегистрирован — клиент его не видит. Модель его не вызывает. Пользователь его не ждёт.
LLM из SQL — killer feature для энтерпрайз-графов. Нет нужды в промежуточном слое: вызов Gemini прямо из plpgsql через
ai.generate()классифицирует рёбра. Кстати, там можно и эмбеддинги вычислять для строки, чтобы на SQL передавать текст параметром, а не вектор, но иногда этот вектор нужно повторно использовать на клиентеДва транспорта — таков миграционный путь. StreamableHTTP — будущее MCP. SSE — настоящее. Поддержка обоих в одном процессе означает, что клиенты переходят на новый протокол в своём темпе.
Что еще накрутили:
Графовый reasoning: не просто находить путь, но и объяснять его. «MesaNet связан с Titans через 3 хопа, потому что оба используют Surrogate Memory, которая основана на Fast Weight Programmers».
Авто-обнаружение противоречий: при добавлении нового источника проверять, не противоречит ли он существующим утверждениям.
Что еще интересно попробовать:
Версионирование рёбер: отслеживать, когда связь была создана и почему — для растущей wiki это становится критичным.
WebSocket-транспорт: для real-time уведомлений об изменениях графа.
Стек: Node.js, Express, AlloyDB Omni, pgvector, MCP SDK, Vertex AI (text-embedding-004), Gemini 2.5 Flash Lite (edge classification)
P.S. Кому интересно почитать про Titans, Miras, MesaNet и прочие технологии Test-Time Training (TTT), напишите, я дам подключение к этому MCP, он не опубликован на публичных хабах. А всякий научпоп – в канале @veriga_pro_AI
Комментарии (6)

veriga Автор
05.06.2026 16:57Да, именно так — двухэтапный 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.3Rank:
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 символов
Если нужен код, скажите ваш гитхаб аккаунт, я вас добавлю

alterbred
05.06.2026 16:57Сначала показалось, что выдумывал сам что-то похожее...
но у меня всё без Вики и нейросетей...Полной реализации нет, только визуализация вывода
https://www.walks.ru/wm_dr/
(лучше смотреть на большом экране)
flowing_abyss
Спасибо за статью, очень интересный подход. Хотелось бы чуть лучше понять механику
wiki_graph_context.Правильно ли я понимаю, что сначала находятся seed-страницы через vector search, а потом к ним добавляются соседние страницы из графа? Если да, то интересно, как вы дедуплицируете seed/hop-страницы, как выбираете глубину обхода, откуда берутся веса для разных типов рёбер, и что именно отдаёте LLM – полный текст страниц или только релевантные фрагменты?
И ещё вопрос. Планируете ли вы где-нибудь выложить код (например, на GitHub)?
veriga Автор
Да, именно так — двухэтапный 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-функции:
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 символов
Если нужен код, скажите ваш гитхаб аккаунт, я вас добавлю
flowing_abyss
Содержательно. Это именно то, что я хотел узнать. Спасибо за ответ.
Мой GitHub.
veriga Автор
пригласил