DeepDigest
Towards Data Science · · ~17 мин

Вики-Страницы LLM Перегружены Инженерией — Я Заменил Свою На Чистый Компилятор Python

Автор создал конвейер на чистом Python для компиляции текстовых заметок в связанную markdown wiki без использования LLM и внешних API, описав ошибки и их исправления, встреченные в процессе разработки.

Вики-Страницы LLM Перегружены Инженерией — Я Заменил Свою На Чистый Компилятор Python

Большие языковые модели.

Вики‑страницы LLM перегружены — я заменил свой конвейер на чистый компилятор Python.

Для структурирования локальной уценки не нужны агенты. Для этого нужен компилятор.

Emmimal P Alexander

3 июля 2026 г.

Прочитано: 17 мин.

Изображение от автора, сгенерированное с помощью ChatGPT (DALL·E).

TL;DR.

Я создал конвейер на чистом Python, который компилирует папку с необработанными, беспорядочными текстовыми заметками в связанную, отредактированную markdown‑вики. Никаких вызовов LLM, никаких встраиваний, никаких внешних API — только стандартная библиотека.

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

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

Я протестировал весь конвейер с тремя размерами корпусов на двух разных компьютерах (Linux и Windows) и проверил, действительно ли детерминированные выходные данные совпадают на обоих компьютерах. Они действительно совпали.

Ниже приведён полный код, все 17 тестов и неоконченный вывод с терминала, чтобы вы могли повторно запустить всё самостоятельно.

Почему я это написал.

Я попытался создать LLM‑вики в стиле Karpathy: циклы агентов, рекурсивные вызовы LLM, встраивания для всего.

Исходными данными была папка с локальными markdown‑файлами, которая уже была у меня на диске.

И на полпути меня осенило: я платил токены за реорганизацию текста, который у меня уже был.

Поэтому я заменил весь конвейер на чистый компилятор Python.

В этой статье подробно описывается эта система: как превратить папку с необработанными, несогласованно отформатированными текстовыми заметками в связанную markdown‑вики‑страницу — с нулевыми вызовами LLM, нулевыми внешними API и нулевыми зависимостями от сторонних разработчиков. Каждый приведённый ниже тестовый номер является реальным: он был запущен на двух разных компьютерах (контейнер Linux и мой собственный ПК с Windows), и я включил в статью две реальные ошибки, с которыми столкнулся при его создании.

Если вы ищете вики‑компилятор markdown на чистом языке Python, детерминированную альтернативу инструментам базы знаний на основе агентов или практический обзор создания локальной RAG‑альтернативы, то это именно та статья.

Полный код: https://github.com/Emmimal/wiki-compiler/

Мышление компилятора.

Вот рефрейминг, на котором построена остальная часть этой статьи: агент решает, как должна выглядеть ваша вики‑страница, а компилятор гарантирует, как она должна выглядеть.

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

Я хотел, чтобы эта вики‑страница была предсказуемой. В отличие от LLM, который меняет свои выходные данные, компилятор выдаёт один и тот же результат каждый раз, когда вы его запускаете. Такая согласованность важна для моих личных справочных материалов. Я структурировал эту систему таким образом, чтобы страницы markdown действовали как объектные файлы: они создаются из исходного кода и могут быть перестроены по желанию. Я храню отредактированный вручную контент отдельно от разделов, созданных машиной. Вот как настраивается конвейер:

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

<<<CODE_BLOCK_N>>>

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

Почему здесь важны нулевые зависимости, в частности?

Всё в этой кодовой базе выполняется только со стандартной библиотекой Python. Нет преобразователей предложений, нет векторной базы данных, нет HTTP‑клиента для встраиваемого API. Это не самоцель — проверить чистоту кода. Это прямое следствие проблемы, которую решает этот конвейер.

После того как вы уберёте вызовы LLM, останется лишь выполнить синтаксический анализ текста, манипулировать строками и обходить графы по словарю в памяти. Именно для таких задач и были созданы структуры данных re, os и обычный Python. Усложнение зависимостей здесь не повысит корректность работы — лишь создаст проблемы с установкой и добавит ещё один элемент, который может сломаться по причинам, никак не связанным с вашими заметками. Если у вас когда‑нибудь зависала установка pip в Windows из‑за того, что для вашей версии Python не было доступно скомпилированное колесо для библиотеки машинного обучения, вы уже понимаете, почему важно, чтобы система «просто запускалась».

Проблема с вики‑сайтами, управляемыми агентами

Идея использовать LLM для создания и поддержки персональной вики‑страницы не нова, и это не моя идея. Она получила серьёзное развитие после того, как Андрей Карпати описал эту закономерность в широко распространённом посте. В нём он объяснил, что тратит меньше средств из своего бюджета на написание кода и больше — на создание структурированных, устойчивых баз знаний на основе своих исследовательских заметок. Затем он опубликовал общедоступный «файл идей», где более подробно описал архитектуру и провёл явное сравнение процесса с компиляцией: вводятся исходные тексты, появляется структурированная вики‑страница с перекрёстными ссылками, а компиляцией занимается LLM [1][2].

Я считаю, что структура компиляции совершенно правильная. Просто я не думаю, что LLM должен быть компилятором.

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

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

Задержка: каждый цикл «чтение — принятие решения — запись» — это обмен данными по сети, если вы используете размещённую модель, или реальные вычислительные затраты, даже если вы запускаете что‑то локально. Для работы, которая в основном сводится к реструктуризации существующего текста, такая задержка бессмысленна.

Недетерминированность: это то, что меня действительно обеспокоило. Я дважды прогнал одну и ту же папку через ранний прототип на основе агента и получил две разные структуры ссылок. В исходных файлах ничего не изменилось. Просто оба раза модель немного по‑разному оценивала то, что считается связанным. В программном обеспечении это порой выглядит забавно. Но когда речь идёт об источнике информации, это становится проблемой.

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

Шаг 1: Средство извлечения метаданных регулярных выражений

В папках с заметками настоящий беспорядок. В одних файлах используется заголовок #, в других — строка в верхнем регистре, а в третьих вообще нет заголовка. Метаданные, такие как «created:» или «aliases:», могут отсутствовать или быть спрятаны в середине файла. Любой экстрактор, который ожидает последовательного форматирования, ломается, когда сталкивается с реальным файлом, поэтому я создал свой, чтобы разобраться с этим беспорядком.

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

Это значит, что конвейер не прерывает работу из‑за неправильного оформления — он просто обрабатывает то, что находит. Это самая скучная часть проекта, но она убирает большую часть беспорядка, поэтому я потратил больше всего времени на то, чтобы этот этап работал как надо.

Шаг 2: построение графиков.

На этом этапе обрабатываются ссылки между заметками. Здесь я столкнулся с проблемой производительности.

Моя первая версия запускала отдельное регулярное выражение для каждой сущности в отношении каждого другого файла. Это был подход O(n^2). Со 100 файлами всё было в порядке. С 1000 файлами это занимало 4,4 секунды. При обработке 5000 файлов требовалось 107 секунд. Я думал, что отсутствие вызова API обеспечит быструю работу кода по умолчанию. Я ошибался. Алгоритм важнее, чем отсутствие сетевых вызовов.

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

Результаты резко изменились. Время просмотра 1000 файлов сократилось с 4,4 секунд до менее чем 50 миллисекунд. Время просмотра 5000 файлов уменьшилось со 107 секунд до менее чем секунды. Эти цифры получены в ходе раннего тестирования в моей среде разработки Linux — ещё до того, как я запустил конвейер в Windows. Поэтому не стоит ожидать, что они будут точно совпадать с показателями Windows, которые приведены дальше в статье; реальные цифры для Windows есть в разделе «Тесты» ниже. Цифры важны, поэтому вот фактический прогресс:

Обработка 100 файлов, 1000 файлов, 5000 файлов:
* научное парное регулярное выражение (первая версия) — ~46 мс, ~4400 мс, ~107 000 мс;
* комбинированное чередование регулярных выражений (промежуточное) — ~12 мс, ~597 мс, ~14 000 мс;
* сопоставление слов с индексированными фразами (окончательное) — ~2 мс, ~33 мс, ~492 мс.

Стоит обратить внимание на среднюю строку моего теста — это была моя первая попытка исправить ошибку, и она не удалась. Я попытался объединить имена всех объектов в один большой шаблон чередования (Name1|Name2|Name3…). Хотя это сократило количество объектов регулярных выражений, базовому движку всё равно приходилось проверять полный список в каждой сканируемой позиции. При 5000 объектах это по‑прежнему давало квадратичное поведение, замаскированное под линейное. Средство сопоставления с индексацией слов оказалось единственной версией, которая реально устраняла сложность, а не просто скрывала её за более быстрым постоянным коэффициентом.

Вот как на самом деле выглядит результирующий график для трёх реальных объектов из моего тестового корпуса:

<<<CODE_BLOCK_N>>>

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

Определение здесь строго лексическое, а не семантическое. Оно совпадает только в том случае, если в тексте встречается точное название. Он не понимает, что «компания» и «мой работодатель» могут обозначать одно и то же. Это серьёзное ограничение, и позже в статье я расскажу о фактических затратах, связанных с этим.

Шаг 3: Рерайтер с учётом разделов.

На этом этапе создаётся фактическая уценка. Это не анализатор абстрактного синтаксического дерева. Он не обходит никакие деревья — просто выполняет целевую замену строк между определёнными тегами ## Heading. Поэтому я называю его рерайтером с учётом разделов.

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

Я не просто полагался на то, что это сработает, — я протестировал систему. Я вручную добавил заметку на сгенерированную страницу, изменил исходный код и запустил компилятор. Я убедился, что заметка осталась на месте, а другие разделы обновились. Несколько дней спустя я повторил тот же тест на компьютере с Windows — результат оказался таким же.

Вот как это выглядит от начала до конца на одном реальном объекте из моего тестового корпуса:

RAW SOURCE (raw_notes/attention_mechanism.txt)
------------------------------------------------
ATTENTION MECHANISM
created: 2026-02-27

A common mistake is tuning Attention Mechanism without first
checking Learning Rate Schedule.

This section needs a cleaner example before it is considered final.
COMPILED OUTPUT (compiled_wiki/attention_mechanism.md)
------------------------------------------------
# Attention Mechanism

## Metadata
- created: 2026-02-27
- aliases: none
- source: raw_notes/attention_mechanism.txt

## Related
- [[Learning Rate Schedule]]

## Referenced By
- [[Gradient Descent]]

## Body
A common mistake is tuning Attention Mechanism without first
checking Learning Rate Schedule.

This section needs a cleaner example before it is considered final.

## Notes
_(add your own notes here -- preserved on recompile)_

Ничто в файле raw не указывало компилятору на то, что Gradient Descent ссылается на эту страницу. Эта ссылка была добавлена автоматически, потому что в собственном raw‑примечании Gradient Descent в основном тексте упоминается «Механизм внимания», и графостроитель это заметил. Вот и весь конвейер в одном конкретном примере: беспорядочный ввод, структурированный вывод с перекрёстными ссылками и нулевой ручной привязкой.

Шаг 4: Линтер (и вторая ошибка).

Линтер прост: он обрабатывает выходные данные, помечает неработающие ссылки и выявляет страницы, на которые больше никто не ссылается. Люди могут ошибочно принять это за шаг «оценки» LLM, но это не так. Это простая структурная проверка с фиксированными правилами.

В моей первой версии была серьёзная ошибка. Я упоминаю об этом, потому что такую вещь можно упустить, если не написать тест, который действительно исследует логику. Линтер подсчитывал входящие ссылки, сканируя каждую [[ссылку]] в файле. Проблема в том, что раздел, на который ссылаются, тоже содержит [[ссылки]]. Эти ссылки отслеживают страницы, которые указывают на текущий файл, а не страницы, на которые указывает сам файл. Мой линтер учитывал их как исходящие ссылки, из‑за чего количество ссылок для каждой страницы увеличивалось.

Результат? В моём тестовом корпусе из 100 файлов, где было 13 подтверждённых сирот, ошибка линтера показала ноль. Это был не просто недоучёт — линтер показал, что вики идеально связана, хотя на самом деле это было не так. Я бы отправил эту ошибку в работу, если бы не перепроверил логику по второму источнику достоверности.

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

related_text = _extract_section(text, "Related")
for match in LINK_RE.finditer(related_text):
 target_slug = _slugify(match.group(1))
 if target_slug in incoming_count:
 incoming_count[target_slug] += 1

После этого изменения количество потерянных элементов в линтере в точности совпадало с количеством элементов в построителе графиков — каждый раз. Он работает независимо от размера корпуса. Я добавил для этого регрессионный тест, названный в честь ошибки, чтобы она никогда не могла незаметно вернуться.

Полный набор тестов

У меня есть 17 тестов, использующих только stdlib unittest: они охватывают каждый этап, а также полный сквозной конвейер. Вот репрезентативный фрагмент:

test_linter_does_not_miscount_referenced_by ... ok
test_human_notes_preserved_across_recompile ... ok
test_recompile_is_idempotent_on_compiler_owned_sections ... ok
test_deterministic_output ... ok

Ran 17 tests in 0.020s
OK

Тест testlinterdoesnotmiscountreferencedby — это регрессионный тест на ошибку, с которой я только что ознакомился. Это самое неудачное название в файле, но оно самое важное.

Я выбрал 17-ю версию не только потому, что это казалось правильным — структура важна. На каждом этапе есть свои изолированные тесты: в них используются созданные вручную объекты Entity вместо синтетического генератора. Я сделал это так, чтобы ошибка указывала ровно на один этап. Я не хочу тратить час на отладку полного конвейера только для того, чтобы найти опечатку в одной строке.

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

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

Тест: Две машины, одинаковые цифры

Я запустил полный конвейер в трёх разных масштабах — как в контейнере Linux, так и на своём локальном компьютере с Windows 10, используя одно и то же начальное значение, чтобы исходный материал оставался идентичным.

Вывод терминала, raw:

Filesextractgraphrewritelintкомпилировать полный конвейер10022.8 мс3.1 мс59.4 мс86.0 мс85.4 мс171.4 мс131,000261,5 мс47.1 мс605.5 мс883.9 мс914.1 мс1,798,0 мс1335,0001,398,4 мс625.6 мс3, 446,7 мс6,972,5 мс5,470,6 мс12,443,1 мс644

FULL PIPELINE TIME BY STAGE AT 5,000 FILES (12.44s total)
 ============================================================

 extract [==] 1.40s (11%)
 graph [=] 0.63s ( 5%)
 rewrite [=======] 3.45s (28%)
 lint [==============] 6.97s (56%)

 ============================================================

На сегодняшний день редактирование — самый дорогой этап. При 5000 файлах он стоит дороже, чем этапы извлечения, построения графиков и перезаписи, вместе взятые.

Это меня удивило. Lint обладает самой простой логикой во всём конвейере — он просто открывает каждый файл один раз, чтобы проверить два небольших раздела с помощью регулярных выражений. Узким местом является не код, а дисковый ввод‑вывод. Lint перечитывает каждый файл заново, и Windows здесь работает значительно медленнее, чем Linux. Вероятно, этому способствует Защитник Windows, который проверяет каждый файл по мере его открытия, хотя я не проверял это напрямую по собственным журналам Defender.

В graph builder нет дискового ввода‑вывода, поэтому он лучше масштабируется, но не идеально линейно.

Переход от 100 к 1000 файлам — десятикратный скачок в данных — занял в 15,2 раза больше времени. Переход с 1000 файлов на 5000 (ещё одно увеличение в 5 раз) занял в 13,3 раза больше времени. Это реальная, измеримая стоимость того, что поисковику с индексацией слов приходится проверять больше имён‑кандидатов на каждый токен по мере роста списка сущностей, и это означает, что масштабирование не совсем линейное.

Количество потерянных файлов (13, 133, 644) оставалось одинаковым при каждом запуске, независимо от операционной системы. Это не совпадение. В этом весь смысл создания этого компилятора, а не агента: выходные данные детерминированы. Они не меняются. Меняется только время на настенных часах, что отражает лишь особенности аппаратного обеспечения и операционной системы, а не алгоритма.

Эти бесхозные цифры — статистика подключения из синтетического набора данных, а не показатель качества. Я проверил их независимо друг от друга: graph builder и linter вычисляют статус потерянного кода совершенно разными путями, и они согласуются во всех масштабах, которые я тестировал.

Что это означает для реального использования: при 5000 notes полная перекомпиляция занимает около 12 секунд на стандартном оборудовании Windows — нулевые затраты на токены и сетевые вызовы. Если вы работаете в масштабе большинства персональных баз знаний (от нескольких сотен до пары тысяч заметок), полная перекомпиляция завершается менее чем за две секунды.

Где это приводит к сбоям

Неструктурированные или крайне противоречивые исходные данные. Мой extractor обрабатывает два стиля заголовков и некоторые необязательные метаданные — это то, что требовалось для моего тестового корпуса. Если вы создадите папку с хаотичным, искажённым текстом или многоязычными заметками без какой‑либо структуры, одно только регулярное выражение не поможет. Вам понадобится значительно более сложный уровень извлечения.

Семантическая привязка. Это важный момент. Мой графический конструктор использует точное соответствие названий. Если в одной заметке говорится о «градиентном спуске», а в другой описывается «этап оптимизации» без использования буквальной фразы, они не будут связаны. Ничто в этом конвейере не понимает смысла. Это честная граница того, что может сделать детерминированный компилятор. Если бы я хотел расширить это, я бы добавил семантический слой в качестве чётко отделённого улучшения — я бы не стал включать его в основной детерминированный путь.

Суть не в том, что LLM — неподходящий инструмент для создания персональной wiki. Дело в том, что они не подходят для выполнения 90 процентов работы, которая носит чисто механический характер, и, возможно, являются правильным инструментом для 10 процентов задач, требующих реального понимания смысла текста, а не просто соответствия его написанию.

Заключительная мысль

Если ваши входные данные детерминированы, то и ваш конвейер должен быть таким же. В противном случае вы просто добавляете случайность туда, где её не было.

Это аргумент всей статьи. Не для каждой задачи управления знаниями требуется агентный цикл. Иногда вам просто нужен синтаксический анализатор, график и линтер, которые расскажут вам правду о ваших собственных результатах, включая те части, где они неверны.

Полный исходный код, включая генератор, extractor, graph builder, rewriter, linter, benchmark harness и все 17 тестов, находится по адресу: https://github.com/Emmimal/wiki-compiler/

Ресурсы и цитаты

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

[1] Андрей Карпати, оригинальный пост, описывающий персональные базы знаний, управляемые LLM, X, апрель 2026 года. https://x.com/karpathy/status/2039805659525644595

[2] Андрей Карпати, файл идей «LLM Wiki» (GitHub Gist), апрель 2026 г., описывающий шаблон wiki-as-compiled-artifact, на который ссылаются в этой статье. https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f

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

Автор:

Эммимал П. Александер

Смотрите все от Эммимал Пи Александер

Глубокие погружения, Управление знаниями, машинное обучение, Python, разработка программного обеспечения

Поделитесь этой статьей

Поделитесь на Facebook

Поделитесь на LinkedIn

Поделитесь на X.

Towards Data Science — публикация сообщества. Поделитесь своими идеями с нашей глобальной аудиторией и зарабатывайте с помощью авторской платёжной программы TDS.

Пишите для TDS.

// оригинал
Towards Data Science ↗ Читать оригинал
5 просмотров
// поделиться Telegram VK