Плагин Veynrel 2.2.1 для Obsidian разделяет три действия, которые в типичном «AI-редакторе» слиты в один клик: поиск похожих заметок, получение рекомендации от языковой модели и запись изменений на диск. Между этапами стоят локальные проверки, и именно они решают, попадёт ли правка в файл.
Причина такой архитектуры простая: две заметки могут быть похожи по смыслу, но это не повод объединять их или удалять одну. В тестовом хранилище Obsidian лежат две заметки с названием Cache lifetime: одна в Study/Caching, вторая в Study/Systems. Поиск справедливо считает их близкими. Одинаковый термин не означает одинаковое содержание: в одном файле может лежать определение, в другом пример или важное ограничение, которое нельзя потерять при слиянии.
Автор плагина описывает этот разрыв прямо: получить рекомендацию от LLM гораздо проще, чем безопасно применить её к Markdown. Дальше разбираю, как в Veynrel устроены четыре барьера: каталог фрагментов с ID, планировщик операций, запись с проверкой актуальности файла и откат, который не затирает ручные правки. Разбор опирается на публикацию автора плагина, где приведены код, тесты и описание эксперимента.
Почему применить рекомендацию LLM сложнее, чем её получить
Ответ модели выглядит как готовый текст: «объедините эти две заметки», «добавьте ссылку», «удалите дубль». Применить его к хранилищу means изменить конкретный файл по конкретному пути, не потеряв данные и не перезаписав чужую работу. В последних версиях Veynrel поиск, рекомендация и право изменить файл стали отдельными этапами, и это ключевое архитектурное решение плагина.
Более ранний эксперимент автора показывает цену ошибки на уровне человека, а не кода: он взял 40 синтетических заметок и разобрал десять найденных пар. Девять совпадений показались полезными, но только в четырёх случаях он уверенно создал бы ссылку. Остальные пять требовали прочитать оба текста целиком. Это не бенчмарк качества LLM и не замер точности поиска, а иллюстрация того, что «похоже» и «стоит менять» — разные утверждения.
Чем семантическая близость отличается от уверенности в дубликате
Семантический поиск работает механически: текст делится на осмысленные фрагменты, для них получаются числовые векторы, затем система сравнивает близость представлений заметок. Число на выходе выглядит как оценка, но ею не является. Даже сходство 0.95 означает не «95% уверенности в дубликате», а лишь близость в выбранном векторном пространстве. Поэтому результат поиска в плагине называется кандидатом.
Практический вывод из этой формулировки: кандидат попадает в интерфейс, но не в очередь на запись. В сценарии с Cache lifetime два файла имеют одинаковое имя и разные пути, и до отдельного согласия на анализ модель не получала их содержимое. Сначала пользователь видит пару, затем разрешает отправку текста, и только потом появляется рекомендация.
Контракт refs.v2: как модель ссылается на фрагменты по ID
Контракт refs.v2 убирает из ответа модели сам текст цитаты. Плагин заранее готовит каталог фрагментов: каждый фрагмент получает ID и привязку к конкретной заметке. Модель возвращает только ссылки, а исходный текст в ответ подставляет локальный код. Выдуманный ID или ссылка на фрагмент из другой заметки отклоняются, потому что проверка идёт по каталогу, а не по доверию к строке от LLM.
const fragment = catalog.get(ref.fragmentId);
if (!fragment) {
throw new Error("Неизвестный ID фрагмента: " + ref.fragmentId);
}
if (fragment.notePath !== ref.notePath) {
throw new Error("Фрагмент относится к другой заметке: " + ref.notePath);
}
output.push(fragment.text); // текст берётся из каталога, не из ответа модели
Смысл схемы в разделении ответственности: модель выбирает, какие фрагменты релевантны, код отвечает за дословность. Модель физически не может переписать цитату, потому что цитату она не присылает. Тот же приём, когда LLM извлекает факты, а детерминированный код проверяет их перед решением, разобран в материале про библиотеку solvi.
Что именно проверяет локальный код перед подстановкой текста
- Существование ID в каталоге: ссылка на несуществующий фрагмент обрывает обработку ответа.
- Принадлежность фрагмента нужной заметке: ссылку на чужой файл нельзя протащить через поле notePath.
- Источник текста: в итоговую рекомендацию попадает строка из каталога, а не строка из ответа модели.
Отдельно стоит проговорить, чего здесь нет. Проверка не оценивает, насколько уместен выбранный фрагмент, и не сравнивает смысл цитаты с тезисом. Она отвечает на узкий вопрос: существует ли такой фрагмент и принадлежит ли он этой заметке. Этого достаточно, чтобы отсечь галлюцинации в ссылках, и недостаточно, чтобы гарантировать качество аргумента.
Планировщик: от рекомендации к конкретной операции
Рекомендация остаётся текстом, пока её не перевели в операцию. Планировщик в Veynrel выполняет именно перевод: определяет целевой файл, место вставки или замены и итоговое содержимое. Дальше операция не исполняется сразу, а показывается пользователю вместе с точным diff.
Почему проверки по имени файла недостаточно
В примере с Cache lifetime оба файла называются одинаково, различаются только пути: Study/Caching и Study/Systems. Если планировщик сопоставляет заметку по имени, операция может уйти не в тот файл, и пользователь увидит правку в заметке, которую не выбирал. Проверка по пути снимает неоднозначность.
function pickNote(candidates, targetPath) {
const note = candidates.find((item) => item.path === targetPath);
if (!note) {
throw new Error("Заметка по пути не найдена: " + targetPath);
}
return note;
}
Подтверждение diff закрывает вторую часть риска: даже корректно найденный файл можно исправить не так, как ожидал человек. Пользователь видит пары строк «было» и «стало» и подтверждает конкретный результат. Пример такого подтверждения для пары одноимённых заметок:
- Cache lifetime: 300 секунд
+ Cache lifetime: 300 секунд, сбрасывается при записи (см. Study/Systems)
Запись через Vault.process() и инвариант current === before
Между подготовкой diff и нажатием кнопки проходит время, и файл может измениться: пользователь дописал абзац, синхронизация подтянула правку с другого устройства. Запись в плагине идёт через Vault.process() с инвариантом current === before. Если содержимое файла на диске уже не совпадает с тем, из которого строился diff, запись отклоняется целиком.
await vault.process(file, (current) => {
if (current !== before) {
throw new Error("Файл изменился после подготовки diff, запись отменена");
}
return after;
});
Инвариант превращает гонку в явную ошибку. Без него типичный сценарий такой: LLM предложила заменить второй абзац, пользователь за это время переписал третий, а запись вернула файл к состоянию before и стёрла ручную работу. С проверкой такого не произойдёт: плагин сообщит о конфликте и предложит пересобрать diff по актуальному тексту.
Undo, который не затирает последующие правки
Откат в Veynrel разрешён только при точном совпадении текущего содержимого с записанным after. Если после применения операции в заметке что-то поправили вручную, кнопка отката блокируется: иначе она вернула бы before и уничтожила более поздние изменения.
function canUndo(current, after) {
return current === after;
}
// при false пользователь видит сообщение:
// "Откат недоступен: в заметке есть изменения после записи"
Логика жёсткая и в бытовом смысле неудобная: отменить одну правку, сохранив следующую, плагин не умеет. Зато он не притворяется системой контроля версий. Для полноценной истории нужен git или отдельное хранилище версий, а здесь задача скромнее: не потерять данные при одном шаге отката.
Что показали автотесты с управляемыми сбоями
Проверка построена на воспроизводимых сбоях, а не на удачных прогонах. Содержимое файла подменяют между подготовкой diff и записью, чтобы сработал инвариант current === before. Путь в операции указывают на соседнюю заметку с тем же именем, чтобы проверить сопоставление по полному пути. Undo вызывают после ручной правки, чтобы убедиться, что откат блокируется.
Такие тесты проверяют поведение защит, а не качество модели: они не говорят ничего о том, насколько полезны рекомендации LLM. Это важное различие, потому что зелёный прогон автотестов часто воспринимают как подтверждение корректности всего пайплайна. Здесь он подтверждает только то, что небезопасная операция не пройдёт до записи.
Прогон на DeepSeek через OpenRouter: что прошло валидацию и что осталось за её границей
Отдельный эксперимент автор провёл на DeepSeek через OpenRouter: десять запросов, все десять ответов прошли техническую валидацию. Ни одной выдуманной ссылки, ни одного чужого фрагмента, ни одного расхождения по пути файла. При этом как минимум один тезис оказался подтверждён выбранной цитатой лишь частично: фрагмент существовал, принадлежал нужной заметке, но не доказывал утверждение полностью.
Масштаб эксперимента стоит держать в голове: десять запросов к одной модели через агрегатор. Это не сравнение DeepSeek с другими моделями и не замер надёжности. Подробнее про то, зачем для разбора ответов нужны артефакты по каждому вопросу, а не только итоговый счёт, написано в материале про lm-eval-ledger.
Почему техническая валидация не равна смысловой
Код проверяет: ID существует, фрагмент принадлежит нужной заметке, путь совпадает, diff точен, файл не изменился с момента подготовки. Код не проверяет, что цитата подтверждает тезис, что тезис вообще нужен в заметке и что после правки текст останется связным. Это зона человека.
Именно поэтому подтверждение точного diff остаётся обязательным шагом, а не формальностью. Валидация ответов LLM снижает класс ошибок, связанных с галлюцинациями в ссылках и путях, и не снимает ответственность за смысл. Если цитата подтверждает тезис частично, увидит это только читатель, а не валидатор.
Что из этого подхода переносится на другие инструменты
Архитектура Veynrel собрана из приёмов, которые не привязаны к Obsidian и TypeScript. Их можно перенести в любой инструмент, где LLM предлагает правки в Markdown.
- Каталог фрагментов с ID вместо свободных цитат. Модель выбирает из известного набора, исходный текст подставляет код.
- Проверка целевого файла по пути. Имя заметки не уникально, полный путь уникален.
- Подтверждение точного diff перед записью. Пользователь видит «было» и «стало», а не пересказ изменений.
- Запись с инвариантом current === before. Изменение файла между подготовкой и применением превращается в явную ошибку, а не в потерю данных.
- Undo только при совпадении с after. Откат не должен затирать более поздние ручные правки.
Ограничения у подхода тоже реальные. Каталог фрагментов надо готовить заранее, и его качество напрямую влияет на то, что вообще сможет выбрать модель. Проверки добавляют код, который нужно тестировать на сбоях. Смысловая точность остаётся на пользователе. Для сценариев, где правки идут пачками без присмотра, такая схема не подходит без дополнительного слоя проверок.
Отдельно про демонстрации: шесть скриншотов основного разбора сняты в Obsidian 1.13.7 на синтетических файлах с контролируемым локальным ответом. Они показывают работу интерфейса и защит, а не качество реальной модели. Если нужен инструмент для самого просмотра и редактирования Markdown-папок, стоит посмотреть на MarkMello 0.4.0, но это другой класс задач.
Практический вывод для тех, кто встраивает LLM в редактирование заметок: начните с разделения этапов. Поиск выдаёт кандидатов, модель выбирает ID из каталога, планировщик строит операцию с проверкой пути, запись проходит через API с проверкой актуальности, откат ограничен точным совпадением. Каждый из этих шагов можно реализовать и протестировать отдельно, и именно это делает работу с LLM в личном хранилище предсказуемой.