Как я написал JustHTML с помощью coding-агентов

Как я написал JustHTML с помощью coding-агентов

@ai_longreads

История создания HTML5-парсера на Python с использованием AI-агентов, прохождение всех тестов html5lib и извлеченные уроки по эффективной работе с coding-агентами.

Это AI-перевод статьи, сделанный каналом Про AI: Лучшие Статьи и Исследования.


Как я написал JustHTML с помощью coding-агентов

Original Title: How I wrote JustHTML using coding agents | Автор: Emil Stenström | Оригинальный текст

3 декабря 2025

Недавно я выпустил JustHTML — HTML5-парсер на Python. Он проходит 100% тестов из тестового набора html5lib, не имеет внешних зависимостей и включает API для CSS-селекторов. Работа над ним многому меня научила в плане эффективного взаимодействия с coding-агентами.

Я думал, что хорошо знаю HTML, начиная этот проект, но оказалось, что я вообще ничего не знаю, когда дело доходит до парсинга сломанного HTML5-кода. А это — основная часть алгоритма.

Henri Sivonen, который реализовал HTML5-парсер для Firefox, назвал "adoption agency algorithm" (который обрабатывает неправильно вложенные форматирующие элементы) "самой сложной частью построителя дерева". Он включает в себя правило "Noah's Ark" (ограничивающее количество идентичных элементов до 3) и сложные манипуляции со стеком, которые нарушают стандартную модель стека.

Я до сих пор не знаю, как решать эти проблемы. Но у меня всё равно есть парсер, который решает эти проблемы лучше, чем эталонная реализация html5lib. Сила AI! :)

Почему HTML5?

При выборе проекта для разработки с coding-агентами хорошая идея — выбрать тот, для которого уже есть много тестов. HTML5 исключительно хорошо специфицирован, с подробной спецификацией и тысячами тестов для treebuilder и tokenizer, доступных в репозитории `html5lib-tests`.

Когда вы используете coding-агенты автономно, вам нужен способ, чтобы они понимали свой прогресс. Полный набор тестов идеально для этого подходит. Агент может запускать тесты, смотреть, что упало, и итерировать, пока они не пройдут.

Создание парсера (итерации, перезапуски и работа над производительностью)

Написание полноценного HTML5-парсера — это не короткая задача на один раз. Я работал над этим проектом несколько месяцев в свободное время.

Инструменты: я использовал обычный VS Code с Github Copilot в режиме Agent. Я включил автоматическое подтверждение всех команд, а затем добавил черный список команд, которые всегда хотел подтверждать вручную. Я написал инструкцию для агента, которая говорила ему продолжать работать и не останавливаться, чтобы задавать вопросы. Сработало отлично!

Вот процесс, который потребовался, чтобы дойти до результата:

Простейший HTML5-парсер (в качестве базовой линии)

Для начала я попросил агента написать супер-базовый простой HTML5-парсер. Он работал не очень хорошо, но это было начало.

Подключение html5lib-tests (<1% успешных тестов)

Затем я подключил html5lib-tests и увидел, что у нас менее 1% успешно пройденных тестов. Да, эти тесты сложные. Они являются золотым стандартом для HTML5-парсинга, содержащим тысячи крайних случаев.

Итерации до ~30% покрытия (рефакторинг и исправление багов)

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

Рефакторинг в обработчики для каждого тега

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

Достижение 100% покрытия тестов (с лучшими моделями)

Оттуда мы продолжили итерировать до 100% покрытия тестов. Это заняло много времени, и релиз Claude Sonnet 3.7 стал причиной того, что мы вообще чего-то добились.

Бенчмаркинг и обнаружение, что мы в 3 раза медленнее

Разобравшись с корректностью, я настроил benchmark, чтобы протестировать, насколько быстр мой парсер. Я увидел, что был в 3 раза медленнее, чем html5lib, который и так считается медленным.

Переписывание tokenizer на Rust (и едва дотягивание до html5lib)

Поэтому я попробовал очевидный следующий шаг: я позволил агенту переписать tokenizer на Rust, чтобы ускорить работу (примечание: я не знаю Rust). Это сработало, и скорость едва превзошла html5lib. Он создал целый крейт rust_tokenizer с 690 строками Rust-кода в lib.rs, которые я не мог прочитать, но они прошли тесты.

Обнаружение html5ever (быстрый, корректный, Rust)

Пока я искал альтернативы, я нашел `html5ever`, движок парсинга Servo. Он очень корректен и написан с нуля на Rust для достижения высокой скорости.

Вопрос: зачем вообще это создавать?

В этот момент у меня возникла неприятная мысль: зачем миру нужна более медленная версия html5ever на частично Python? В чём вообще смысл всего этого?! Я чуть не удалил весь проект.

Переключение на портирование логики html5ever на Python

Вместо того чтобы бросить, я подумал о написании Python-интерфейса к html5ever, но решил, что мне не нравится возня с библиотекой, требующей установки бинарных файлов. Поэтому я вернулся к чистому Python, но с более быстрым подходом: что если портировать логику html5ever на Python? Разве это не должно быть быстрее существующих Python-библиотек? Я решил выбросить всю предыдущую работу.

Перезапуск с нуля (снова)

Итак, я начал заново с <1% покрытия тестов и итерировал с тем же набором тестов вплоть до 100%. На этот раз я попросил агента сверяться с кодовой базой на Rust в самом начале. Это была утомительная работа, делать одно и то же снова.

Всё ещё медленнее, чем html5lib

К сожалению, я запустил benchmark на новой кодовой базе и обнаружил, что она всё ещё медленнее, чем html5lib.

Профилирование, реальные бенчмарки и микро-оптимизации

Поэтому я переключился на работу над производительностью: я написал несколько новых инструментов для агентов, простой профилировщик и скрейпер, который собрал датасет из 100 тысяч популярных веб-страниц для реального бенчмаркинга. Мне удалось снизить скорость ниже целевой с помощью Python-микро-оптимизаций, но только при использовании только что вышедшего Gemini 3 Pro (который невероятен) для итеративного запуска benchmark и профилировщика. Никакая другая модель не достигла прогресса в бенчмарках.

Удаление непротестированного кода (покрытие как скальпель)

Позже, по наитию, я запустил `coverage` на кодовой базе и обнаружил, что большие части кода были "непротестированными". Но это было наоборот, потому что я уже знал, что тесты покрывают всё важное. Так что строки без покрытия тестами можно было удалить! Я сказал агенту начать удалять код для достижения 100% покрытия тестами, что было интересной сменой ролей. Эти удаления на самом деле ускорили код так же сильно, как и микро-оптимизации.

Фаззинг для поиска крашей и укрепления парсера

После удаления кода я забеспокоился, что удалил слишком много и упустил граничные случаи. Поэтому я попросил агента написать html5 fuzzer, который очень старался генерировать HTML, ломающий парсер.

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

Сравнение с другими парсерами (насколько редки 100%)

Чтобы проверить реальность достижения 100%, я запустил тесты html5lib против других парсеров. Я обнаружил, что ни один другой парсер не проходит 90% покрытия, а lxml, один из самых популярных Python-парсеров, находится на уровне 1%. Эталонная реализация, сам html5lib, на уровне 88%. Может быть, это всё-таки сложная задача?

Выпуск в качестве библиотеки (CI, релизы, selector API)

Наконец, чтобы сделать это хорошей библиотекой, я попросил агента настроить CI, релизы через GitHub, query API, написать README и так далее.

Решил переименовать библиотеку из turbohtml в justhtml, чтобы никого не обманывать, что это самая быстрая библиотека, и вместо этого сосредоточиться на ощущении, что всё просто работает.

Что делал агент и что делал я

После написания парсера я всё ещё не знаю HTML5 как следует. Агент написал его для меня. Я направлял его в вопросах дизайна API и исправлял плохие решения на высоком уровне, но он сделал ВСЮ черновую работу и написал весь код.

Я обрабатывал все git-коммиты самостоятельно, проверяя код по мере его поступления. Я не понимал все алгоритмические решения, но понимал, когда он делал что-то не так.

По мере улучшения моделей я видел устойчивый рост покрытия тестов. Gemini — самая умная модель с точки зрения одноразового решения, в то время как Claude Opus лучше всего итерирует путь к хорошему решению.

Практические советы по работе с coding-агентами

Выбирайте проекты с существующими тестами

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

Пишите инструкции для агента

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

Включите автоматическое подтверждение (с чёрным списком)

Автоматическое подтверждение команд ускоряет работу, но добавьте чёрный список для опасных команд (например, удаление файлов).

Проверяйте весь код

Даже если вы не понимаете каждую строку, проверяйте всё. Вы поймёте, когда что-то пойдёт не так.

Используйте лучшие модели для сложных задач

Более новые модели значительно лучше. Claude Sonnet 3.7 была критична для достижения 100% покрытия.

Будьте готовы к перезапускам

Иногда лучший путь вперёд — начать заново. Не бойтесь выбросить работу, если вы нашли лучший подход.

Используйте профилирование и бенчмарки

Для задач по производительности дайте агенту инструменты для измерения прогресса. Gemini 3 Pro был единственным, кто справился с итеративными микро-оптимизациями.

Используйте coverage в обратную сторону

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

Стоило ли оно того (и что значило "быстро")?

Да. JustHTML — это около 3000 строк Python с более чем 8500 проходящими тестами. Я не смог бы написать это так быстро без агента.

Но "быстро" не означает "без размышлений". Я потратил много времени на проверку кода, принятие дизайнерских решений и направление агента в правильном направлении. Агент делал набор текста; я делал размышление.

Это, вероятно, правильное разделение труда.


Подпишитесь на канал и каждый день читайте лучшие материалы про AI переведенные на русский!

Нашли интересную статью для перевода? Пришлите нашему боту: @ai_longreads_bot

Report Page