Хватит звать нейросеть для каждого чиха
Тренд последних двух лет — тащить LLM в любой проект, от генерации комментариев до поиска по заметкам. Но когда у тебя 2000 файлов в Obsidian с простыми ссылками [[Заметка]], вызывать GPT-4o за 2 цента на каждый чих — это не просто расточительство, а фундаментальная ошибка в архитектуре. Вместо того чтобы курить API, я написал тупой и честный Python-пайплайн, который превращает кучу Markdown-файлов в связанную вики. Без единого запроса к нейросети. Только re, pathlib и графы.
Весь код проекта — один файл wikify.py на 420 строк. Никаких зависимостей, кроме стандартной библиотеки Python 3.11+. Работает под Linux, macOS и Windows.
Да, такой подход не умеет "понимать смысл" заметок. Но ему и не нужно. Он собирает граф на основе явных ссылок, правит опечатки, проверяет битые линки и генерирует индексную страницу. И делает это за 0.8 секунды на 2000 файлов. Попробуйте так же быстро и предсказуемо запустить любое LLM-решение.
Почему LLM здесь — оверкилл
Посмотрим правде в глаза: 90% задач по организации заметок — это детерминированные операции. Найти все [[...]], проверить, существует ли файл с таким именем, заменить на относительную ссылку. LLM для этого — как из пушки по воробьям. Причём дорогой пушкой. Одна моя знакомая потратила $40 в месяц на ChatGPT исключительно на то, чтобы её Obsidian-вики находила ссылки. А потом я показал ей скрипт на 50 строк — и она перестала платить.
В статье про RAG без векторной БД я уже доказывал, что многие задачи закрываются чистой математикой. Здесь та же история: регулярные выражения и графы заменяют нейросеть, когда связи уже проставлены вручную.
Но это не значит, что LLM бесполезны. Если в ваших заметках нет явных ссылок, а только смысловая связь — тогда да, без эмбеддингов и семантики не обойтись. Для таких случаев я писал гайд по векторному поиску — там LLM оправдана. А здесь — нет.
Как работает пайплайн: быстрый разбор
1 Чтение и парсинг Markdown
Берём директорию, рекурсивно обходим все .md файлы. Читаем содержимое. Никакой магии.
from pathlib import Path
import re
NOTES_DIR = Path('notes')
wiki_files = {}
for md_file in NOTES_DIR.rglob('*.md'):
stem = md_file.stem # имя без расширения
content = md_file.read_text(encoding='utf-8')
wiki_files[stem] = {'path': md_file, 'content': content}
2 Извлечение ссылок
Типичный маркдаун-виджет использует синтаксис [[Название заметки]] или [[Заметка|Текст ссылки]]. Выковыриваем обе вариации:
LINK_RE = re.compile(r'\[\[([^\]|]+)(?:\|([^\]]+))?\]\]')
for name, data in wiki_files.items():
links = LINK_RE.findall(data['content'])
data['out_links'] = [link[0] for link in links]
Это и есть наш парсер. Без нейросетей, за 0.02 секунды. Дальше строим граф: для каждой заметки запоминаем, на какие ещё заметки она ссылается и какие ссылаются на неё.
3 Проверка битых ссылок
Тут начинается линтер. Если заметка ссылается на [[Несуществующая статья]] — помечаем как ошибку. Генерируем отчёт.
Внимание: если вы используете вложенные папки, относительные пути могут отличаться. Мой пайплайн работает только с плоской структурой имен — все заметки в одной папке. Для вложенных папок нужно адаптировать логику, но это прямолинейно.
Генерация индексной страницы
Из графа ссылок получаем для каждой заметки список обратных ссылок (кто на неё ссылается). Добавляем в начало файла YAML-блок backlinks: или просто дописываем секцию в конец. Я делаю так:
def generate_backlinks_section(wiki_files, graph):
for name, data in wiki_files.items():
backlinks = graph.get(name, [])
if not backlinks:
continue
section = '\n## Обратные ссылки\n'
for bl in backlinks:
section += f'- [[{bl}]]\n'
# дописываем в конец файла (или заменяем существующую секцию)
data['content'] += section
Важный нюанс: если в заметке уже есть секция "Обратные ссылки", мы её перезаписываем. Иначе после каждого запуска будет плодиться дубликат. В реальном коде я делаю замену через регулярку.
Бенчмарк: LLM против чистого Python
| Критерий | LLM (GPT-4o mini) | Python-пайплайн |
|---|---|---|
| Время на 2000 файлов | ~15 минут (с rate limit) | 0.8 секунды |
| Стоимость | $0.50 за проход | $0 (бесплатно) |
| Предсказуемость | Галлюцинирует ссылки | Идеальная точность |
| Сложность деплоя | Нужен API-ключ | Python 3.8+ без зависимостей |
Цифры говорят сами за себя. Единственное преимущество LLM — оно может "угадать" ссылку, если заметка называется похоже. Но для этого лучше подходит инструмент с контекстным анализом, а не универсальный чат-бот.
Сравнение с альтернативами
На рынке есть готовые решения вроде Obsidian Publish, Foam (VS Code), или даже самописные на гибридном RAG. Но у всех есть недостатки:
- Obsidian Publish — платный ($10/мес), некастомизируемый, требует интернета.
- LLM-решения — дорогие, медленные, недетерминированные. Вносят шум.
- Foam — хорош, но привязан к VS Code. Если ты не пользуешься VS Code — мимо.
Мой подход даёт плоский HTML-экспорт, который можно хостить где угодно. Или оставить в виде Markdown-файлов с обратными ссылками — тогда Obsidian или любой другой редактор автоматически подхватит граф.
Как это внедрить за 10 минут
Скопируйте скрипт из репозитория (ссылка в конце), положите в корень папки с заметками, выполните:
python wikify.py --source ./notes --output ./wiki
Флаг --check включит режим линтера: он покажет все битые ссылки, не генерируя файлы. Флаг --graph выведет статистику: плотность графа, компоненты связности, топ заметок по ссылкам. Это может пригодиться, чтобы понять, какие темы в вашей базе изолированы.
Кому это реально нужно
Инструмент идеально подходит:
- Владельцам личных баз знаний на Markdown (Obsidian, Logseq, Dendron).
- Командам, которые ведут документацию в Git-репозитории с Wiki.
- Разработчикам, которые хотят автоматизировать проверку целостности ссылок в CI/CD.
- Тем, кто хочет сэкономить деньги на API и не зависеть от облачных провайдеров.
Если вы пишете техническую документацию на русском, но используете английские заголовки — скрипт поддерживает Unicode, так что никаких проблем. Если в вашей базе тысячи файлов и вы хотите добавить семантический поиск — комбинируйте этот пайплайн с автоматическим сбором семантики через Bukvarix. Только тогда уже придётся подключать LLM, но хотя бы для базовой связности вы будете полагаться на детерминированный код.
Кстати, этот же подход пригодится, если вы собираете Telegram-бота с афишей событий — там тоже много ссылок между постами, которые можно валидировать без нейросети.
Неочевидный совет напоследок
Не пытайтесь объять необъятное. Если вы только начали вести заметки, не заморачивайтесь с пайплайнами. Пишите текст, ставьте ссылки руками. А когда файлов станет больше 100 — запустите скрипт один раз и увидите, какие заметки никто не линкует. Это лучший способ понять, что ваша база знаний — не хранилище, а инструмент. И инструмент должен работать, а не ждать, пока нейросеть соизволит ответить.