- BrainTools - https://www.braintools.ru -

Моя идеальная структура заметок уснула. Теперь за порядок отвечает LLM

Моя идеальная структура заметок уснула. Теперь за порядок отвечает LLM - 1

Как всё уснуло

Полгода назад я построил себе в Obsidian продуманную структуру хранилища. PARA-подобная иерархия, аккуратные папки под проекты и области, шаблоны, теги. Я честно верил, что вот теперь заживём.

Прошло несколько месяцев, и структура уснула. Не развалилась, не сломалась, именно уснула. Заметки продолжали появляться, но раскладывать их по местам мне стало банально лень. Каждая новая мысль требовала маленького ритуала: решить, куда её положить, как назвать, с чем связать, какие теги повесить. По отдельности каждое решение занимает секунды, но их десятки в день, и в какой-то момент мозг [1] просто отказывается. Заметки стали оседать одной кучей, а красивая иерархия превратилась в музей.

Самое обидное, что я понимал: дело не в моей исключительной лени. Так происходит у большинства. Более того, многие вообще не начинают вести базу знаний, потому что заранее боятся этого хаоса. «У меня будет свалка из трёхсот файлов, зачем начинать». И из этой личной боли [2] выросла идея плагина.

Тезис

Методы борьбы с хаосом в заметках человечество придумало давно.

Никлас Луман со своим Zettelkasten показал, что атомарные заметки со связями работают лучше толстых конспектов. Одна заметка, одна мысль, и у каждой мысли есть ссылки на соседние. За жизнь он накопил под девяносто тысяч карточек, и картотека реально «отвечала» ему на вопросы. Цена метода: каждый конспект вручную разрезать на фрагменты, каждому придумать заголовок, проставить связи. Каждый раз.

Ник Майло популяризировал MOC, карты контента. Это заметки-хабы: тема, пара слов о ней и ссылки на всё, что к теме относится. В отличие от папок, одна заметка может жить сразу в нескольких картах. Ручная цена: регулярно пересматривать всё хранилище, группировать сотни файлов и обновлять карты по мере появления новых заметок.

Интервальное повторение [3] существует со времён Эббингауза с его кривой забывания [4] и картотеки Лейтнера. Алгоритмы давно отшлифованы в SuperMemo и Anki: карточка показывается ровно тогда, когда ты вот-вот её забудешь. Только вот сами карточки кто-то должен сделать, сформулировать вопросы к собственному тексту и поддерживать колоду.

Проблема у всех этих методов одна. Они требуют ручного труда, причём труда рутинного. Разрезать конспект на атомы, пересмотреть двести заметок и сгруппировать их по темам, превратить текст в карточки для повторения. Это не мышление [5], это разметка. И именно на разметке дисциплина ломается у всех.

А разметка, внезапно, это ровно то, что LLM делают хорошо. Так что идея плагина простая: отдать модели рутину методологий, а человеку оставить то, ради чего заметки вообще ведутся, то есть мышление.

Плагин написан на TypeScript, работает с любым OpenAI-совместимым API: OpenRouter, OpenAI, Groq или локальная Ollama, если не хочется отправлять заметки наружу. В этой статье расскажу про три функции, которые я считаю базовым набором против хаоса. Это трилогия, но не финал, о планах в конце.

Флешкарты: интервальное повторение без ручной нарезки

База знаний, которую никогда не перечитываешь, довольно быстро становится кладбищем. Интервальное повторение решает это давно и хорошо, в Obsidian есть отличный плагин Spaced Repetition, который умеет показывать карточки прямо из заметок. Формат простой: строка вида Вопрос::Ответ, и заметка с тегом #flashcards.

Но писать карточки к собственному конспекту после часа чтения, на это меня не хватало никогда.

Теперь это одна команда. Плагин читает активную заметку, отправляет её модели с промптом, который жёстко фиксирует формат, и дописывает секцию с карточками в конец. Сам текст заметки не трогается вообще.

С форматом, кстати, пришлось повозиться. Первая версия промпта вежливо просила «карточки в формате Вопрос::Ответ», и модель радостно выдавала:

Вопрос::Что такое инкапсуляция
Ответ::Сокрытие внутренней реализации

Формально разделитель на месте, по сути всё сломано: для плагина повторения это две отдельные кривые карточки. Помог явный анти-пример прямо в промпте: блок «так НЕЛЬЗЯ» с этим самым паттерном теперь живёт рядом с правильными примерами.

Тут же случилась история, которая сбила с меня немного наивности. Я искренне думал, что базовый уровень моделей уже поднялся достаточно и подобные фокусы в прошлом. Промпт к тому моменту был вылизан: правильные примеры, анти-пример, явные запреты. Запускаю на тестовой заметке, получаю аккуратные карточки в идеальном формате. Радуюсь секунды три, пока не вчитываюсь: это карточки про инкапсуляцию и индексы в базах данных. Дословно мои примеры из промпта, а заметка была совершенно о другом. Слабая бесплатная модель просто скопировала few-shot вместо того, чтобы сгенерировать по тексту. Примеры, которые сильной модели помогают понять формат, для слабой оказались магнитом: зачем думать, если ответ уже написан в задании. Переключился на модель поприличнее, и всё заработало как надо, с теми же самыми промптом и примерами. Без драмы, просто факт, с которым приходится жить: качество модели критично, и промптом его не компенсируешь.

Сама генерация со вставкой умещается в несколько строк, вся вспомогательная логика [6] вынесена в общие функции:

async buildFlashcardsContent(content: string, prompt: string) {
  const raw = await callOpenRouter(this.settings, prompt, content);
  const cards = extractFlashcards(raw);
  if (!cards) throw new Error("Некорректный ответ AI");

  const newContent = appendSection(
    content,
    `## Flashcardsn#flashcardsnn${cards}`,
  );
  return { newContent, cardCount: cards.split("n").length };
}

Промпт уходит системным сообщением, текст заметки пользовательским. Ответ прогоняется через санитайзер, о нём ниже, и если карточек не осталось, функция бросает ошибку [7] вместо записи мусора. Дальше appendSection дописывает в конец заметки секцию с тегом #flashcards, по которому плагин Spaced Repetition находит колоду. Эту же функцию использует и команда для активной заметки, и пакетная обработка по фильтрам.

Доверять ответу модели на слово, как вы уже поняли, нельзя, и тот самый санитайзер extractFlashcards выглядит так:

function extractFlashcards(raw: string): string {
  return raw
    .replace(/```[a-zA-Z]*n?/g, "")
    .replace(/```/g, "")
    .split("n")
    .map((line) => line.trim().replace(/^(?:[-*+•]|d+[.)])s+/, "").trim())
    .filter((line) => {
      const sep = line.indexOf("::");
      if (sep === -1) return false;
      return (
        line.slice(0, sep).trim().length > 0 &&
        line.slice(sep + 2).trim().length > 0
      );
    })
    .join("n")
    .trim();
}

Что здесь происходит. Сначала срезаются markdown-обёртки с кодом, которые модели добавляют вопреки любым запретам. Потом с каждой строки снимается нумерация и маркеры списков. И в живых остаются только строки, где по обе стороны от первого :: есть непустой текст, то есть настоящие карточки. Если после фильтра не осталось ничего, плагин считает ответ браком и не трогает заметку. Лучше честная ошибка, чем мусор в файле.

Пример генерации(в будущем планирую добавить систему тегов)

Пример генерации(в будущем планирую добавить систему тегов)

MOC из кластеров: карта, которую не нужно рисовать вручную

Про сам метод я писал выше, здесь про цену. Чтобы построить карты вручную, нужно пересмотреть всё хранилище и сгруппировать сотни файлов. Ровно та работа, от которой моя структура и уснула.

В плагине к тому моменту уже был «глубокий аудит»: map-reduce по хранилищу, где модель сначала анализирует заметки батчами, а потом кластеризует их по темам. Про то, как устроен этот аудит и почему MapReduce, я разбирал в отдельной статье [8]. Кластеры попадали в итоговый отчёт и на этом умирали. Красивый анализ, прочитал, кивнул, закрыл.

И вот тут я получил отдельное удовольствие. У плагина есть локальный индекс, note-index.json, который хранит результаты анализа каждой заметки вместе с mtime файла, чтобы повторный аудит пропускал неизменённое. Мне концепция индекса нравилась с самого начала, есть в таких штуках что-то глубоко правильное. Это во мне ещё со времён увлечения ООП и Java: спроектировать структуру, у которой каждое поле на своём месте, спрятать доступ за менеджером и смотреть, как система складывается сама. Когда понадобилось хранить кластеры, я не стал плодить новые файлы. Индекс уже умел загружаться, сохраняться по dirty-флагу и версионироваться, оставалось добавить одно поле. Фича легла в систему без единого костыля, и это ощущение, когда своя же архитектура принимает новую задачу как родную, для меня половина удовольствия от разработки. Добавить в индекс секцию под кластеры ощущалось как элегантное решение моей же проблемы:

export interface ClusterRecord {
  name: string;
  description: string;
  filePaths: string[];
}

export interface NoteIndexData {
  version: number;
  updatedAt: string;
  notes: Record<string, NoteRecord>;
  clusters?: {
    createdAt: string;
    items: ClusterRecord[];
  };
}

В notes лежат результаты по каждой заметке, ключ это путь файла. Новая секция clusters хранит результат последней кластеризации: тема, краткое описание из reduce-фазы и пути заметок кластера. Поле опциональное, каждый аудит его перезаписывает.

Запись в индекс выглядит так. Схема поднялась с версии 2 до 3, а у менеджера индекса появилась пара методов:

const SCHEMA_VERSION = 3; // было 2, добавилась секция кластеров

setClusters(items: ClusterRecord[]): void {
  this.data.clusters = {
    createdAt: new Date().toISOString(),
    items,
  };
  this.dirty = true;
}

getClusters(): ClusterRecord[] {
  return this.data.clusters?.items ?? [];
}

Метод setClusters вызывается в движке аудита сразу после reduce-фазы, и кластеры уезжают на диск вместе с остальным индексом одним сохранением. А команда генерации MOC потом просто спрашивает getClusters и не знает ничего ни про аудит, ни про LLM. Bump версии решает совместимость честно: несовпадающий индекс отбрасывается и строится заново, без миграций.

Дальше всё очевидно. Отдельная команда читает кластеры из индекса и создаёт по заметке на кластер: frontmatter с type: moc, заголовок-тема, описание (из аудита, а если его нет, один короткий запрос к модели) и ссылки на все заметки кластера, по полному пути, чтобы не промахиваться при одинаковых именах файлов в разных папках.

Один нюанс, который вылез на ревью: перезаписывать при повторной генерации можно только свои MOC. Плагин проверяет frontmatter существующего файла, и если там нет type: moc, значит это рукописная заметка пользователя, и она получает суффикс к имени вместо перезаписи. Чужие файлы трогать нельзя даже случайно.

Атомизация: Zettelkasten без ножниц

Из всей классики Zettelkasten самый требовательный к дисциплине. У Лумана это работало, потому что он резал мысли на атомы прямо в момент записи, десятилетиями. У меня же типичная заметка после вечера чтения это простыня из пяти перемешанных идей.

Команда «Разбить заметку на атомарные» отдаёт нарезку модели. От неё требуется строго структурированный ответ, JSON вида {"atoms": [{"title": "...", "body": "..."}]}, где каждый атом это одна самостоятельная идея. Если модель считает, что заметка уже атомарна, она возвращает пустой массив, и плагин честно сообщает об этом, не создавая файлов.

Со строгим JSON от LLM есть известная беда: модели оборачивают его в markdown, пишут вступления и дописывают в конце что-нибудь вроде «Надеюсь, помог!». Парсер, который переваривает всё это, в плагине один и используется всеми функциями:

export function extractJSON<T>(raw: string): T {
  const cleaned = raw
    .replace(/```jsonn?/gi, "")
    .replace(/```n?/g, "")
    .trim();
  const jsonStart = Math.min(
    ...[cleaned.indexOf("{"), cleaned.indexOf("[")].filter((i) => i >= 0),
  );
  if (!Number.isFinite(jsonStart)) throw new Error("JSON не найден в ответе");

  const jsonStr = cleaned.slice(jsonStart);
  try {
    return JSON.parse(jsonStr) as T;
  } catch (err) {
    // модель могла дописать текст после JSON, срезаем хвост
    const lastBrace = Math.max(
      jsonStr.lastIndexOf("}"),
      jsonStr.lastIndexOf("]"),
    );
    if (lastBrace === -1) throw err;
    return JSON.parse(jsonStr.slice(0, lastBrace + 1)) as T;
  }
}

Логика такая: снять обёртки, найти первую открывающую скобку и попробовать распарсить всё от неё. Если парсинг упал, вероятно, после JSON идёт человеческий текст, тогда хвост срезается до последней закрывающей скобки и попытка повторяется. Сверху этого ещё стоит ретрай на весь цикл «запрос плюс парсинг» и обработка случая, когда модель возвращает голый массив вместо объекта. Звучит параноидально, но каждый из этих случаев я видел вживую.

Два решения, которыми я доволен. Первое: оригинал не удаляется и не переписывается. В его конец дописывается секция со ссылками на созданные атомы, и бывшая простыня превращается в хаб, точку входа в тему. Второе: атомы по умолчанию создаются в той же папке, где лежит оригинал. Если у человека хранилище организовано по тематическим папкам, плагин не имеет права ломать эту организацию, стаскивая всё в одну кучу. Общая папка для атомов есть, но как опция в настройках.

Мелочи, на которые напоролся

Дохлые бесплатные модели OpenRouter. В настройках дефолтом стояла конкретная бесплатная модель, и у меня она прекрасно работала. А у новых пользователей API отвечал 404: бесплатные модели на OpenRouter приходят и уходят, и захардкоженный идентификатор со временем превращается в тыкву. Перешёл на openrouter/free, это авто-роутинг, который сам направляет запрос на живую бесплатную модель. Заодно добавил в настройки подгрузку актуального списка бесплатных моделей.

Сброс индекса при смене схемы. Тот самый bump версии со 2 на 3 означает, что у существующих пользователей накопленный анализ заметок отбрасывается и глубокий аудит придётся прогнать заново. Для пет-проекта это осознанный размен, писать миграции для локального кэша дороже, чем один повторный прогон. Но осадочек остался: следующее поле в индекс я буду добавлять так, чтобы старая версия его просто игнорировала.

Коллизии имён атомов. Заголовки атомов придумывает модель, а мой санитайзер вычищает из них символы, запрещённые в именах файлов. И выяснилось, что разные заголовки после чистки спокойно схлопываются в одно имя, а macOS и Windows вдобавок не различают регистр и юникод-формы. Лечится скучно, но надёжно: ключи путей нормализуются в NFC и нижний регистр, а при коллизии имя получает суффикс, Заметка-2, Заметка-3 и дальше.

Новое распределение ролей

Все три функции устроены по одной схеме. Метод придуман людьми давно, польза его доказана, а буксует он на рутинной разметке. Эту разметку забирает модель. Человеку остаётся то, что автоматизировать нельзя и не нужно: писать заметки, думать над карточками при повторении, ходить по карте и находить неожиданные связи.

Есть и второй слой, про будущее. Чистая, атомарная, связанная база знаний это не только удобство для человека. Это идеальный контекст для ИИ. Чем лучше структурировано хранилище, тем точнее любой будущий ассистент сможет отвечать на вопросы по вашим собственным знаниям. Порядок в заметках становится инвестицией в то, как вы будете работать с ИИ дальше.

Честные ограничения

Уровень модели решает. На слабых бесплатных моделях возможно всё: копирование примеров из промпта вы уже видели, а ещё бывают подписи «Вопрос»/«Ответ» вопреки прямому запрету, выдуманные поля в JSON и вежливые эссе вместо структурированного ответа. Санитайзеры и ретраи ловят часть этого, но не заменяют нормальную модель. Рабочий минимум сейчас это средние модели уровня недорогих API, локальная Ollama на мелкой 3B-модели годится поиграться, но формат она держит через раз.

LLM галлюцинирует связи. Кластеризация иногда объединяет заметки по поверхностному признаку: заметка про рецепт может уехать в кластер «Продуктивность» только потому, что в ней встретилось слово «эффективно». А MOC-описание темы модель пишет уверенно даже тогда, когда обобщает то, чего в заметках нет. Поэтому всё сгенерированное стоит просматривать глазами, плагин создаёт черновик порядка, а не истину. Черновик, впрочем, экономит часы.

Атомизация режет не всегда удачно. Иногда модель дробит цельное рассуждение пополам, и второй атом без первого повисает в воздухе, а иногда наоборот склеивает две смежные идеи в один толстый атом. Именно поэтому оригинал остаётся нетронутым: неудачную нарезку можно просто удалить и попробовать снова, ничего не потеряв.

Что дальше

Трилогия из этой статьи, флешкарты, MOC и атомизация, это базовый набор, скелет. Общая цель шире: плагин, который умеет приводить хранилище к порядку целиком, по понятным шаблонам, а не по кусочкам. Кое-что из этого уже работает в виде аудита и пакетной обработки, кое-что пока в набросках. Обещать конкретику не буду, но направление, кажется, правильное: пусть страх [9] хаоса перестанет быть причиной не начинать.

Что на данный момент есть в плагине

Что на данный момент есть в плагине
Моя идеальная структура заметок уснула. Теперь за порядок отвечает LLM - 4

Плагин доступен в каталоге Obsidian: https://community.obsidian.md/plugins/ai-knowledge-hub [10]. Исходники: https://github.com/zinverno/obsidian-ai-hub [11]. Буду рад issue, критике промптов и историям про то, как у вас уснула ваша структура.

Автор: Ziverpup

Источник [12]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/32631

URLs in this post:

[1] мозг: http://www.braintools.ru/parts-of-the-brain

[2] боли: http://www.braintools.ru/article/9901

[3] повторение: http://www.braintools.ru/article/4012

[4] забывания: http://www.braintools.ru/article/3931

[5] мышление: http://www.braintools.ru/thinking

[6] логика: http://www.braintools.ru/article/7640

[7] ошибку: http://www.braintools.ru/article/4192

[8] статье: https://habr.com/ru/articles/1053366/

[9] страх: http://www.braintools.ru/article/6134

[10] https://community.obsidian.md/plugins/ai-knowledge-hub: https://community.obsidian.md/plugins/ai-knowledge-hub

[11] https://github.com/zinverno/obsidian-ai-hub: https://github.com/zinverno/obsidian-ai-hub

[12] Источник: https://habr.com/ru/articles/1055456/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1055456

www.BrainTools.ru

Rambler's Top100