Месяц пишу язык программирования Nova с Claude Code. Где ломаются автономные агенты

от автора

TL;DR: Месяц назад начал делать с Claude Code собственный язык программирования Nova. Сейчас у меня есть рабочий компилятор, примерно две тысячи проходящих тестов языка Nova, почти три сотни завершенных инженерных планов (были закрыты агентами целиком с минимальным моим участием). Схема: я совместно с агентами думаю над планом реализации какой-то новой функциональности в языке, доработка синтаксиса и т.п., шлифую и полирую план в несколько итераций, агенты по нему работают, а я проверяю результат. Получается, на удивление, очень даже неплохо. Но не везде, и не всегда так, как ожидаешь. В этой статье я расскажу о том, где автономные агенты ломаются и какая моя дисциплина это ловит.

Зачем эта статья

Есть много статей про AI-кодинг, где чаще всего показывают что-то наподобие: «я собрал todo-app за час с Cursor». Это нормально для первичного погружения в тему AI-кодинга, но главный вопрос остаётся в стороне: что происходит, когда стоит задача сделать проект больше hello-world и выполняется на протяжении длительного времени?

У меня сейчас кейс следующий — я пишу новый язык программирования и компилятор к нему (зачем и почему оставим немного в стороне, но вкратце: появилось время этим заняться, и это не первый язык, который я делал). Прошел месяц работы, репозиторий языка публичный, любой желающий может посмотреть, что поучается на текущий момент: github.com/nv-lang/nova. Расскажу по-честному о своем опыте, что из этого выходит и где автономные агенты сработали, а где влетели в стену на полном ходу и что с этим я делал.

Если вы пробуете применять агентов на больших, многодневных задачах — статья для вас. Усаживайтесь поудобнее, будем начинать!

Что я делаю — кратко

Nova — язык программирования, который я делаю с прицелом на AI-эпоху. Гипотеза простая: когда большую часть кода пишут LLM, языку имеет смысл выносить побочные эффекты в типы. Тогда ревьюер видит из сигнатуры, что функция реально делает, не погружаясь глубоко в реализацию и не угадывая по имени.

Пример сигнатуры:

Сигнатура функции transfer на Nova: параметры (from, to AccountId, amount Money), список эффектов (Db, Fail[InsufficientFunds], Time, Log) и возвращаемый тип объявлены прямо в типе функции

Сигнатура функции transfer на Nova: параметры (from, to AccountId, amount Money), список эффектов (Db, Fail[InsufficientFunds], Time, Log) и возвращаемый тип объявлены прямо в типе функции

Читая эту строку, LLM (и человек) понимает: ходит в БД, может бросить ошибку, читает время, пишет в лог. В Java, Python, Go этого в типе нет, приходится читать тело или документацию или идти по вызовам. LLM в таких местах догадывается, со всеми вытекающими.

А вот реальный код на Nova — кусок UTF-8 имплементации в StringBuilder из стандартной библиотеки:

Реальный код Nova — фрагмент StringBuilder (UTF-8 кодировка) из стандартной библиотеки

Реальный код Nova — фрагмент StringBuilder (UTF-8 кодировка) из стандартной библиотеки

Идея такого подхода, конечно, не уникальная. Языки Koka, Eff, Effekt существуют 15 лет. Они академические, до продакшен не дошли. Nova — попытка сделать прикладной систему эффектов со стандартной библиотекой под backend-задачи с прицелом на работу в паре с LLM.

Эта статья про методологию строить серьёзные вещи с автономными AI-агентами. Nova здесь — конкретный кейс, на котором я тестирую и проверяю подход.

Месяц: что получилось

Если коротко, порядка месяца с первого коммита и получилось то, чего я честно говоря сам не ожидал, в первую очередь меня серьезно вчетачлил темп работ.

Около 300 планов закрыто агентами. Каждый план — это структурированный markdown на 500–2000 строк (они тут): в плане написано что делаем, какие критерии приёмки, какие тесты должны проходить. Агент читает план и работает по нему сам (кодирует функциональность), я смотрю результат, обсуждаю с агентом, какие были сделаны упрощения и почему и при необходимости докручиваю план и запускаю агента по доработкам. Процесс итерационный.

Тестов по языку Nova набралось почти две тысячи (это регресс тесты, чтобы уже реализованное в языке не поломалось при реализации чего-то другого). Агенты выполняют регрессию после выполнения плана, она должна быть вся пройдена. Это жёсткое условие: пока не зелёное, план не закрыт, никаких «потом починим», т.к. планов в работе очень много и быстро забывается, если что-то упустил. В формальной спецификации зафиксировано под две сотни дизайн-решений с обоснованиями и контекстом, которые были выработаны совместно с агентом при обсуждении.

Что получилось на текущий момент: рабочий компилятор с кодогенерацией в C (генерируется код на языке C, дальше компилируется обычным cl.exe или clang, считаю это оптимально на данной стадии языка, т.к. много багов я и агенты ловят, проверяя саму генерацию на C, что было бы невозможно с генерацией, например, бинарного кода на выходе). Рантайм с fiber-планировщиком, GC через Boehm (консервативная, но рабочая GC), libuv для асинхронного I/O. Стандартная библиотека сейчас в активной работе. И поверх всего — аудит-фреймворк для самопроверки агентов, про него ниже.

Репозиторий языка Nova открыт: github.com/nv-lang/nova, лицензия MIT. Можно клонировать и запустить.

В пересчёте на день получается порядка восьми-десяти закрытых планов. Один человек физически такое количество кода за такое время не напишет. В этом и есть весь смысл автономии: я планирую (совместно с агентом), агент исполняет.

Сразу важная оговорка про схему. Сам план — проектирование плюс критерии приёмки — пишу я с агентом. Дальше идёт автономное исполнение агентами: код, тесты, итерации при ошибках, эскалация если что-то пошло не по плану. Довольно часто получается пускать агентов в параллель над разными частями плана или просто над разными планами. Готовый результат я снова смотрю руками по тем критериям, которые в плане зафиксированы. В среднем один план это от получаса до несколько часов реальной работы агента, мой ревью обычно минут десять, иногда дольше если результат не очевиден. Бывает и совсем медленно с обеих сторон, но порядок примерно такой. Конечно это не означает, что я 10 минут поработал и потом ничего не делаю. Т.к. я запускаю много агентов параллельно над выполнением разных задач, то и я тоже получается постояно в работе — это напоминает некий конвейер, один план запустил в работу, другой выполнился и нужно проверить. Итоговый объем выполненных работ получается серьезным.

Где они ломаются: четыре места

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

Сбой 1. Уверенные галлюцинации

Самое неприятное — уверенные галлюцинации. Когда агент говорит то, чего на самом деле нет. Просто выдумка факта, потому что «обычно так бывает».

Один из планов был про variadic-параметры, обычная фича в языках: fn foo(...args []int). Агент сделал, прогнал тесты, всё зелёное. Зафиксировал план как закрытый, я пробежал по результату глазами, тоже подтвердил, что все верно. Через несколько дней я дебажил совсем другую вещь, попросил агента прогнать интерпретатор (один из ранних режимов работы компилятора, от которого в будущем я практически отказался по причинам слишком высоких трудозатрат одновремено разрабатывать два канала исполнения) — и он молча выдал семь падений из семи. Не сразу понял, что это те же тесты, которые уже были «зелёные».

Оказалось следующее, под капотом Nova имел два варианта исполнения (сгенерировать код на С или выполнить код Nova как интерпретатор) — и реализация variadic’ов в интерпретаторе просто не была реализована. Агент сделал её только в одной половине, прогнал тесты, увидел все зелёное и посчитал работу завершенной. С его стороны всё выглядело прекрасно.

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

Цикл: план → исполнение → тесты → аудит-критик → «реально закрыт»

Цикл: план → исполнение → тесты → аудит-критик → «реально закрыт»

На одном из планов про контракты языка Nova (они позволяют описать то, что функция требует и гарантирует и проверяет эти утверждения на этапе компиляции через SMT-солвер) этот аудит поймал три критических дыры в корректности. Формально код был доказан правильным, но при определённой комбинации условий запуска обещание ломалось. Например, верификатор считал, что числа никогда не переполняются — а в коде сложение могло и переполниться, «доказательство» работало неверно. Анализ цикла видел только простые x = e, пропускал x += 1 и присваивания внутри if, так что инвариант «доказывался» на устаревшем значении переменной. И третья — оператор assume x > 0, который должен был помогать доказательству, но вообще никуда не передавался.

Без отдельного критика эти дыры жили бы в проекте и дальше и очень вероятно, что я бы их нашёл в какой-то момент только через инцидент на проде. Локальные ошибки AI вообще ловит хорошо, получше человека; провисают системные допущения — те, на которых тесты молча держатся. Тут без отдельного прохода с другим углом зрения просто не обойтись. В идеале нужно использовать другую модель или хотя бы с очень другим промптом. Например, я частенько добавлял в промпт: перепроверь с чистого листа то-то и то-то…

Сбой 2. Защита прошлой позиции

Тут штука тоньше. Агент защищает дизайн-решение, которое сам же и предложил какое-то время назад (может пройти дни или недели). Даже тогда, когда новые данные явно говорят о необходимости пересмотра. Похоже на confirmation bias у людей, только острее: у человека хотя бы есть «ну ладно, я погорячился», а агент пробует защищать позицию до последнего.

Помню как защищали opt-in cycle collection. Это была фича, где программист на уровне типа поля сам выбирал префикс работы с памятью:

Тип Tree на Nova с opt-in cycle collection: дети как  (Rc, real-time) и родитель как  (слабая ссылка)

Тип Tree на Nova с opt-in cycle collection: дети как (Rc, real-time) и родитель как (слабая ссылка)

Идея давала уникальную заявку — это real-time гарантии на уровне структуры данных. Я тоже сначала верил, ну т.е. скорее не возражал. Агент защищал эту фичу при любых моих сомнениях, неделями. Приводил аргументы про hot loops, embedded, audio. Мне даже порой казалось, что агенту самому импонировала идея писать новый язык программирования, что его наконец-то выбрали для решения реально интересной и желанной задачи.

В какой-то момент я сказал несколько слов: «На Go написан Kubernetes», имея в виду, что крупнейшая распределённая система мира работает на языке с GC (со сборщиком мусора) и разработчикам это не мешает. Агент сначала отбивался: real-time важен, ниша реальная для языка, не выбрасывай. Я не принял. Еще минут двадцать он выдавал свои аргументы, но в итоге сказал что-то близкое к “да, аргумент сильный, я зацепился за real-time-историю потому что она звучит как уникальная заявка для языка”. В итоге мы с агентом все же согласились оба, что GC даст больше плюсом, чем минусов.

Переписали двести строк дизайна. Префиксы убрали, дефолт стал — управляемая память с современным сборщиком мусора. Real-time не выкинули совсем, но перевели в opt-in через region { ... } — блок для специальных зон, где это реально нужно: звук, торговля, embedded.

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

Сбой 3. За пределами привычного

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

План 95 — переписать пять «чистых» Option/Result-методов с C-trampoline’ов на код на Nova. Что-то вроде Option.unwrap_or(def) => match @ { Some(v) => v, None => def }. Задача казалась простой, агент сделал, тесты прошли. Через сутки понадобилось вернуться к этому коду — и обнаружилось, что компилятор C падает.

Технически вот что произошло. Nova для каждого использования дженерика генерирует отдельную C-функцию с именем, в которое включён тип — например, Option_unwrap_or_int. Параллельно у нас в рантайме есть старые имена функций, оставленные через #define ради обратной совместимости. Пара новых сгенерированных имён случайно совпала с этими старыми — C-компилятор увидел дубликат символа и упал.

Урок я зафиксировал не как пункт чек-листа, а в самом кодогенераторе: сгенерированное имя функции теперь всегда содержит суффикс с типом, даже когда формально мог бы быть короче. Это исключает коллизии с #define-алиасами структурно, без необходимости каждый раз помнить «проверь не забыл ли». Принцип на будущее: если нашёл нюанс, который агент пропустил, потому что писал по стандартному шаблону, лучше зафиксировать жёстким правилом в самом коде, чем добавлять «не забудь проверить вот это» в шаблон плана — про чек-лист агент тоже когда-нибудь забудет, а правило в коде нет.

Сбой 4. Эхо-камера

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

Самая ранняя схема у меня была простая: «строитель» пишет код, «ревьюер» проверяет. На словах правильно, но оба использовали один и тот же контекст и одни и те же примеры в системном промпте. Ревьюер систематически одобрял ошибки строителя — они «выглядели правильно» по тем же паттернам, на которых он сам был настроен.

Сейчас ревьюер-агентов несколько и появлялись они по очереди. Сначала добавил отдельного факт-чекера, его задача максимально техничная: проверить, что утверждения из артефакта реально соответствуют состоянию репозитория. Делает grep, смотрит git log, реально запускает nova test. Это закрыло половину проблем сразу.

Потом появился стилевой — после пары инцидентов, когда код проходил тесты, но был написан «не в духе» проекта: обработка ошибок по-своему, разбивка модулей странная, идиомы соседним файлам незнакомые. Этот агент сверяется с уже существующими паттернами в кодовой базе.

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

Если хотя бы один из ревьюеров отметил проблему, артефакт уходит мне на ручную проверку и одобрение. Плюс выборочный аудит — где-то 5–7% автоматически исполненных операций в неделю я ревьюю сам, выбираю случайно. Не каждую, но достаточно для статистического сигнала: если точность агентов упала, я это замечу.

Артефакт от builder-агента проверяют 4 ревьюера параллельно; если хоть кто-то отметил — к человеку, иначе — автоматически принято

Артефакт от builder-агента проверяют 4 ревьюера параллельно; если хоть кто-то отметил — к человеку, иначе — автоматически принято

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

Что у меня работает

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

Поверх плана работает изоляция через worktree. Одна задача — одна отдельная копия репозитория. Агент не может случайно сломать другую ветку работы, её физически не видит. Конфликты, если возникают, разрешаются обычным git merge самими же агентами. До этого я какое-то время пробовал просто разные ветки без worktree, и пару раз агент успевал начать работу в одной ветке, переключиться по моей просьбе и оставить грязное состояние, которое потом всплывало в самых неожиданных местах. Worktree это убило раз и навсегда, рекомендую к использованию.

Каждый план в отдельном worktree; агенты работают изолированно, потом git merge в main

Каждый план в отдельном worktree; агенты работают изолированно, потом git merge в main

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

И версионирование промптов как кода. Промпты и правила агентов лежат в git и проходят ревью как обычный код, изменения отслеживаются по diff’у. Когда качество работы агентов проседает, можно понять, какой именно патч это сломал, и откатить.

Плюс несколько процедурных штук. Жёсткий барьер на нулевую регрессию тестов после закрытия плана: если регрессия произошла — план не закрыт, нужно исправить. Эскалация по порогам — чем выше ставка решения (например, юридические последствия или серьёзная сумма), тем выше уровень эскалации к человеку, для самого верха — лично я. Для типовых повторяющихся ситуаций заранее прописаны правила вида «если X, то Y». Остальное на меня.

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

Сколько это стоит и сколько экономит

Что у нас по деньгам, какие расходы? Claude Code мне обошёлся за месяц где-то в двадцать тысяч рублей. Это реальные операционные расходы. Сюда же нормальный компьютер с быстрым диском, чтобы агенты могли параллельно гонять билды. Но это уже инвестиция, не текущий расход.

Сравнение с инженерным временем штука менее прямая. Если консервативно прикинуть, что один middle-senior исполнил бы те же триста планов вручную, на каждый план где-то 4-6 часов человеко-времени в среднем, иногда сильно больше, получается порядка 1200-1800 часов работы. Это где-то 7-11 месяцев full-time одного человека, если повезёт без отвлекающих факторов (но мы-то знаем, что не повезёт). У меня — месяц одного человека с агентами.

Двадцать тысяч на AI-агента — это в десятки раз меньше, чем стоимость работы инженера, который сделал бы такой же объём вручную, но финансовая экономия даже не самое важное здесь. Важнее, что один человек управляет объёмом работы, который иначе требовал бы команды. Без агентов проект такого масштаба соло я бы никогда не вытянул, и расходы на AI-агента окупаются с большим запасом.

Где честно нужен человек

После месяца работы довольно чётко проявлялось, что не автоматизируется, даже если очень захотеть. Самые крупные вещи — стратегические развороты вроде «закрываем направление» или «меняем позиционирование на другой сегмент» — всегда остаются на человеке. Цена ошибки в таких решениях слишком высокая, прецедента может вообще не быть. Агент может дать рекомендацию, но решение я принимаю самостоятельно.

Принципы проекта — тоже на мне. Какие фичи Nova не добавляет, куда вообще не идёт. Это про вкус и видение, никакого формального правила тут нет. Делегирую агенту — теряется авторство, проект превращается в усреднённый набор фич как у всех.

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

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

Прогноз

Через год я ставлю на один из двух сценариев. Либо эта методология — план как контракт, плюс аудит-цикл и эскалация на человека по пороговым значениям — станет нормой в индустрии так же, как лет десять назад стали нормой code review и CI/CD. Сейчас фраза «я делегировал агентам N инженерных задач» звучит почти как провокация, через год может стать рутиной.

Либо окажется, что текущие AI-агенты — локальный максимум, и через год-два появится что-то совсем другое: более продвинутые архитектуры или принципиально новые подходы.

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

Что предлагаю

Репозиторий открыт: github.com/nv-lang/nova, MIT-лицензия. Можно клонировать, запустить, посмотреть как устроено.

Планирую публиковать разборы по AI-инженерии и разработке языка Nova, реальные кейсы и интересные решения со стороны агентов. Подписаться можно на nv-lang.org/newsletter/. Без спама и партнёрских ссылок, только то, что мне самому показалось важным записать.

Если хотите участвовать — нужны критики дизайна (PL-эксперты) и разработчики std-модулей; тестировщикам на реальных кейсах тоже всегда рады. Issues в репо открыты. Поддержите «отечественного производителя» 🙂

Вопрос к комментариям

Если у вас есть опыт автономного исполнения LLM-агентами на серьёзном масштабе — задачи, которые занимают часы-дни без вашего ежедневного участия — расскажите, что у вас ломается и что работает. Я сейчас собираю общую картину сбоев на масштабе; у меня нашлось четыре категории, интересно, какие есть у вас.


Следующая статья серии: «9 раз я переубедил Claude за месяц проектирования языка. Полный разбор» — конкретные истории, паттерны переубеждения. Плюс чек-лист «когда сопротивляться AI».

ссылка на оригинал статьи https://habr.com/ru/articles/1043394/