Фаза 1: Локальный семантический граф
Доказать, что личный граф знаний с LLM работает лучше, чем просто заметки. Без федерации, без протокола — только локальная магия.
Deliverables
- Python-прототип (reference-имплементация локального узла)
- Плагин для Obsidian (минимальный: извлечение триплетов + чат)
- Тестовый корпус 1000 заметок с размеченными правильными ответами
- Бенчмарк: граф vs полнотекстовый поиск vs чистый LLM
- Публичный репозиторий + документация по установке
Критерий перехода к фазе 2
Метрика 80% точности достигнута на тестовом корпусе, прототип стабильно работает у 5+ beta-тестеров, есть как минимум один контрибьютор извне core-команды. Если метрика не достигнута — продлеваем фазу или пересматриваем подход (возможно, чистый LLM-подход лучше, и FSS не нужен).
Компоненты прототипа
1. Хранилище Markdown + frontmatter
Основа — обычная папка с Markdown-файлами. Каждый файл имеет frontmatter YAML с типизированными свойствами (через Obsidian Properties или вручную). Это позволяет сразу работать с существующими Obsidian-хранилищами без миграции.
---
title: Преступление и наказание
type: book
author: Достоевский Ф.М.
year: 1866
rating: 9
read_at: 2024-03-15
tags: [русская-литература, экзистенциализм]
---
# Преступление и наказание
Прочитал в марте 2024. Главный конфликт — между идеей Раскольникова
и человечностью, которая в нём остаётся...
Frontmatter — первый источник триплетов: модель читает его и сразу получает типизированные свойства без извлечения из текста. Это даёт надёжный baseline для случая, когда пользователь уже структурировал часть данных.
2. Извлекатель триплетов (LLM)
Python-модуль, который для каждой заметки запускает локальный LLM с задачей извлечь триплеты. Используется structured output (Outlines или instructor), что гарантирует JSON строго определённой схемы. Модель: Qwen 2.5 14B через Ollama. На современном ноутбуке — ~3 секунды на заметку.
extracted_triples = [
Triple(
subject="note:prestuplenie",
predicate="written_by",
object="person:dostoevsky",
confidence=0.95
),
Triple(
subject="note:prestuplenie",
predicate="influenced_by",
object="concept:existentialism",
confidence=0.78
),
Triple(
subject="note:prestuplenie",
predicate="main_conflict",
object="идея vs человечность",
confidence=0.82
)
]
Каждый триплет получает confidence — оценку модели, насколько
она уверена в извлечении. Это важно для последующих запросов: триплеты с
confidence < 0.5 помечаются как «предположительные» и не используются в
строгих ответах.
3. Локальный граф (SQLite)
Извлечённые триплеты сохраняются в SQLite с индексами по subject, predicate, object. Это даёт O(log n) поиск по любому полю, чего достаточно для личного графа (10-100k триплетов на 1000 заметок). Граф пересобирается при изменении заметки, с инкрементальным обновлением.
CREATE TABLE triples (
id INTEGER PRIMARY KEY,
subject TEXT NOT NULL,
predicate TEXT NOT NULL,
object TEXT NOT NULL,
object_type TEXT,
source_note TEXT NOT NULL,
confidence REAL,
extracted_at TIMESTAMP,
extracted_by TEXT
);
CREATE INDEX idx_subject ON triples(subject);
CREATE INDEX idx_predicate ON triples(predicate);
CREATE INDEX idx_object ON triples(object);
SQLite выбран осознанно: embedded, без сервера, файл хранится рядом с заметками, при необходимости удаляется и перестраивается за минуты. Для 100k триплетов SQLite справляется за миллисекунды. Если граф вырастет до миллионов — миграция на DuckDB или embedded Postgres, но это не актуально для фазы 1.
4. Векторный индекс (LanceDB)
Параллельно с триплетами строится векторный индекс заметок через bge-m3 (мультиязычные эмбеддинги). Это даёт семантический поиск: «найди заметки про влияние Достоевского на экзистенциализм, даже если слово не используется». LanceDB embedded, хранит векторы в файле рядом с SQLite.
import lancedb
db = lancedb.connect("~/.fss/vectors")
table = db.create_table("notes", data=[
{"id": "note:prestuplenie", "vector": embed(text), "text": text}
])
# Семантический поиск
results = table.search(embed("влияние Достоевского на экзистенциализм"))
.limit(10).to_list()
Векторный индекс не заменяет триплеты, а дополняет их. Триплеты отвечают на точные запросы («все книги Достоевского с рейтингом 8+»), векторы — на смысловые («что я читал про экзистенциализм, даже без этого слова»). Сочетание даёт мощь, недоступную ни одному из подходов по отдельности.
5. Слои запросов (LLM-агент)
Python-модуль, который принимает запрос на естественном языке и собирает ответ, используя все три уровня (текст, триплеты, векторы). Архитектура простая: LLM получает запрос + релевантный контекст (из векторов) + релевантные триплеты (из графа) + инструкции отвечать с указанием источников.
def answer(query: str) -> str:
# 1. Семантический поиск по заметкам
relevant_notes = vector_search(query, limit=5)
# 2. Извлечение сущностей и запрос к графу
entities = llm_extract_entities(query)
triples = graph_query(entities)
# 3. Сборка контекста
context = build_context(relevant_notes, triples)
# 4. Финальный ответ с provenance
answer = llm_generate(query, context, require_sources=True)
return answer
Ключевой момент — require_sources=True: модель обязана указать,
из какой заметки или триплета взято каждое утверждение в ответе. Если модель
не может указать источник, она обязана сказать «не знаю». Это критически
отличается от чистого LLM-чата, где модель может галлюцинировать уверенно.
6. API-сервер (FastAPI)
Локальный HTTP-сервер на порту 8765, который даёт плагину Obsidian (и другим
клиентам) доступ к графу. Три эндпоинта: POST /query (запрос на
естественном языке), POST /reindex (переиндексация заметки),
GET /graph (визуализация графа для отладки).
@app.post("/query")
async def query_endpoint(q: QueryRequest):
answer = await answer_with_provenance(q.text)
return {
"answer": answer.text,
"sources": answer.sources,
"triples_used": answer.triples
}
7. Плагин для Obsidian
Минимальный TypeScript-плагин, который добавляет в Obsidian чат-панель с доступом к локальному графу. Пользователь выделяет текст заметки → «Extract triples» в контекстном меню → плагин вызывает локальный API → триплеты сохраняются в frontmatter. Параллельно — чат для запросов к графу.
Это даёт первый реальный user-facing интерфейс, без необходимости перепрыгивать в отдельное приложение. Целевая аудитория фазы 1 — Obsidian-пользователи, поэтому плагин критичен для adoption.
Метрики и бенчмарк
Главная метрика фазы 1: 80% запросов отвечены точнее, чем полнотекстовый поиск. Измеряется на тестовом корпусе 1000 заметок с 100 размеченными запросами и правильными ответами. Для каждого запроса оцениваем три подхода: полнотекстовый поиск (baseline), чистый LLM (без графа), FSS (граф + LLM).
Оценка — слепая: 3 эксперта оценивают ответы каждого подхода по шкале 1-5, не зная, какой подход какой. FSS считается успешным, если в 80+ случаях из 100 получает оценку выше, чем полнотекстовый поиск. Это жёсткая метрика: если FSS не превосходит baseline заметно, его незачем строить.
Календарь фазы 1
| Месяц | Деливерабл | Метрика |
|---|---|---|
| 1 | Python-прототип без UI: хранилище + LLM + граф | 10 заметок → 100 триплетов |
| 2 | Векторный индекс + слои запросов | 10 тестовых запросов работают |
| 3 | Плагин Obsidian + тестовый корпус 1000 заметок | Бенчмарк запущен |
| 4 | Бенчмарк: FSS vs полнотекстовый vs LLM | Метрика 80% достигнута |
| 5-6 | Beta-тестеры (5+), полировка, публичный релиз | 5+ активных beta-тестеров |
Что может пойти не так
Три главных риска фазы 1. Локальный LLM недостаточно точен: Qwen 2.5 14B может ошибаться в извлечении триплетов, особенно для мета-утверждений («эта заметка напоминает мне ту»). Смягчение: гибридный подход — для confidence < 0.5 используем облачную модель как second opinion. Производительность: 3 секунды на заметку × 1000 заметок = 50 минут на полную индексацию. Смягчение: инкрементальная индексация (только изменившиеся заметки) + фоновый режим. Метрика не достигнута: если FSS не превосходит полнотекстовый поиск, это сигнал, что проекту не нужно расти дальше в текущем виде. Это нормально — лучше узнать на фазе 1, чем на фазе 4.
Не код, не плагин, не бенчмарк — а публичное доказательство, что личный семантический граф работает лучше, чем обычные заметки. Если это доказано, фаза 2 (протокол) имеет смысл. Если нет — пересматриваем концепцию целиком.