← назад к разделу

Агент пишет на любом популярном языке одинаково бегло. Но ошибается он на них по-разному, точнее, по-разному ошибка всплывает. Выдуманный метод репозитория на Java не проходит компиляцию, и вы видите его через секунду. Тот же выдуманный метод на Python проходит тесты успешного пути и падает у пользователя через неделю. Агент один и тот же, задача та же, разница только в том, сколько выдумок доживёт до продукта и как быстро вы их поймаете.

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

Где всплывает ошибка агента: компилятор или прод

Возьмём самую частую ошибку агента: он уверенно вызвал метод, которого нет. repository.findByEmailAndActive(email), user.subscription.renew(), client.retry_with_backoff(). Имена правдоподобные, в похожих проектах такие есть, и модель дописала их по памяти.

статические типы агент выдумал метод компилятор: ошибка починено за минуту динамические типы агент выдумал метод тесты: зелёные прод: упало через неделю

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

На языке со строгими типами компилятор не находит метод и останавливает сборку. Ошибка стоит минуту: агент видит сообщение компилятора и сам её чинит, вы об этом чаще всего даже не узнаете. На языке с динамическими типами вызов несуществующего метода — обычный код, пока до него не дошло выполнение. Тесты проверяют успешный путь, где этого вызова нет; в проде через неделю ветка выполняется, и пользователь видит AttributeError: 'NoneType' object has no attribute 'renew'. Ошибка стоит инцидента.

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

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

Пять свойств языка, которые агент усиливает

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

Объём в обучающей выборке

Крупные языки (Python, JavaScript, Java, TypeScript, Go, C#) представлены в обучающих данных на порядки больше, чем редкие (Haskell, Erlang, Ada). Чем больше данных, тем точнее модель воспроизводит идиомы и библиотеки, тем меньше выдумок. Это самый очевидный критерий, но один он ничего не решает: иначе Python был бы безусловным лидером, а на практике он проигрывает Java именно на следующем свойстве.

Строгость типов

Языки со статическими типами (Java, Kotlin, TypeScript, Rust, C#, Go) ловят выдумки на этапе сборки, и это разобрано выше. Языки с динамическими (Python, JavaScript, Ruby) обнаруживают их во время работы. Отсюда парадокс: Python, один из самых представленных языков в обучающих данных, для продукта хуже Java, потому что агент пишет на нём не меньше выдумок, а живут они дольше.

Сила соглашений

Чем меньше способов «как правильно», тем меньше расхождений между сессиями агента. У Go один способ форматировать код, один способ разрешать зависимости, один способ обрабатывать ошибки. У Scala или Ruby каждый второй проект изобретает свои встроенные мини-языки, и агент в каждой сессии выбирает свой.

Java здесь посередине: Spring Boot задаёт сильные соглашения, но без него и без явного свода правил получаются разнокалиберные сервисы. Это лечится не языком, а исполняемым стандартом: правилами, которые агент применяет на каждом изменении. TypeScript с фреймворком (Angular, NestJS) ведёт себя как Java со Spring; чистый Node.js без фреймворка даёт больше шума.

Явность важнее краткости

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

Инструментарий

Линтеры, форматтеры, статический анализ, проверка типов, тесты архитектуры. Чем строже инструменты, тем быстрее и точнее красный сигнал при приёмке. Java тут вне конкуренции: Checkstyle, SpotBugs, ArchUnit, анализ уровня IntelliJ. TypeScript близко: ESLint с правилами по типам, Prettier, tsc. Python догоняет (mypy, ruff, Pydantic), но экосистема разрозненна: каждая команда выбирает свой набор, и агент не знает заранее, какой.

Явный код, а не короткий

Что в тезисе «меньше кода» правда. Шаблонный код, написанный руками, — реальная нагрузка: агент пишет тысячный геттер, тратит токены, иногда промахивается; кодогенерация (Lombok в Java, data class в Kotlin, dataclass в Python, встраивание структур в Go) снимает это начисто. Длинный файл с противоречиями — тоже нагрузка: чем больше и шумнее контекст, тем хуже модель находит в нём нужное. Дело не в «усталости», которой у модели нет, а в разбавлении важного.

Что в этом тезисе миф. Точность агента зависит от явности, а не от краткости. Явные аннотации и типы (@Transactional, @PreAuthorize, декораторы в NestJS, Protocol в Python) делают код длиннее, но каждое решение видно локально: агент прочитал двадцать строк обработчика и понял всё. Пять строк на Haskell выглядят элегантно, но половина поведения выводится из типов в других файлах, и агент, следуя по цепочке, путается чаще.

Хуже всего краткость через метапрограммирование. Вызов User.find_by_email_and_active(email, true) в Rails короче явного запроса, но такого метода в проекте никто не писал: фреймворк достраивает его на лету из имени через механизм method_missing, который перехватывает обращение к несуществующему методу. Агент генерирует похожие вызовы, и одни работают, другие нет, а узнать, какие, можно только выполнением. На Java тот же вызов был бы repository.findByEmailAndActive(...): длиннее, но агент видит, объявлен ли метод в интерфейсе, а компилятор проверяет.

Отсюда формула: агент любит явный код и минимум шаблонного повторения. Кодогенерация на этапе компиляции, когда исходник короткий, а семантика явная, — хорошо. Динамическая магия во время работы, когда исходник короткий, а поведение скрыто, — плохо. Стеки, которые попадают в это «сладкое место»:

  • Java с Lombok, records и сгенерированным jOOQ — исходника в разы меньше, чем у «голой» Java, а типы и аннотации на виду;
  • Kotlin со Spring — data class и nullable-типы делают то же нативно, чуть короче и чуть менее прозрачно;
  • Go — стандартная библиотека и соглашения намеренно скучные, вариантов мало, агент предсказуем;
  • TypeScript с NestJS — декораторы делают то же, что аннотации Spring;
  • Python с Pydantic и mypy — BaseModel и строгие подсказки типов приближают поведение к статическим языкам.

Что не работает: встроенные мини-языки и метапрограммирование (неявные преобразования в Scala, method_missing в Ruby, метаклассы в Python) и конфигурация, размазанная между кодом и YAML: источник правды становится не найти ни человеку, ни агенту.

Если язык уже выбран

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

Python. Первым делом проверка типов как блокирующая ступень: подсказки типов на границах модулей и mypy в CI, который не пропускает изменение с ошибкой. Это то, что превращает выдуманный метод из инцидента в красную строку до просмотра кода. Вторым — Pydantic для всех входных и выходных моделей: агент получает явную схему вместо словарей, и половина «а что тут лежит» исчезает. Третьим — ruff с настройками строже стандартных, в pre-commit и в CI. Тесты на бизнес-логику — не «покрытие 80%», а каждая бизнес-функция с ожидаемым значением из требования. Полное покрытие типами старого кода не нужно: включайте проверку для новых файлов и для тех, которые агент трогает.

JavaScript. Первый шаг — TypeScript, и это стоит дешевле, чем кажется: файлы переименовывают постепенно, а tsc в режиме проверки без сборки работает и на смешанном проекте. Если TypeScript невозможен, типы в JSDoc плюс тот же tsc в режиме проверки. Второй шаг — ESLint с набором strict-type-checked из typescript-eslint: он проверяет правила с учётом типов, а не только по тексту. Третий — pre-commit через Husky и lint-staged.

C++. Современный диалект (C++17 и новее) и запрет на сырые new/delete; clang-tidy в строгом режиме; санитайзеры адресов и потоков на всех прогонах тестов; заголовки подключать явно, а не рассчитывать, что символ приедет транзитивно через чужой include, иначе ни человек, ни агент не поймут, откуда он.

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

Когда язык не главное

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

  • срок неделя и продукт умрёт через квартал. Прототип для проверки гипотезы пишут на том, что быстрее у вас лично; выдумки агента здесь дешевле, чем день на настройку инструментов;
  • домен простой. Сервис, который принимает форму и кладёт в базу, одинаков на любом языке; строгие типы не спасают там, где спасать нечего;
  • интеграция с готовой экосистемой. Обучение моделей — Python, потому что там библиотеки; мобильный клиент — Swift или Kotlin, потому что так устроена платформа. Язык здесь выбран за вас, и правильный ответ — достройка инструментов, а не спор;
  • команда знает один язык. Смена языка ради агента стоит дороже, чем настройка проверок для языка, который команда знает. Выбор языка «под агента» — для нового продукта, а не для живого.

Java или Go

Раньше выбор между ними во многом диктовала скорость набора: Python быстрее всех, Spring Boot догонял за счёт кодогенерации, Go тормозил на многословной обработке ошибок, «голая» Java без фреймворков была реально медленной, отсюда и стереотип. Когда пишет агент, разница в скорости набора стёрта: все три пишутся примерно за одинаковое время. Остаётся то, что от скорости набора не зависит.

Что сравниваемJava (Spring, Lombok)Go
Сложность доменной моделиrecords, sealed types, дженерики, инструменты для DDDпростая система типов, дженерики недавно, агрегаты получаются костыльными
СоглашенияSpring Boot доминирует, но без свода правил разнобойодин способ всего: gofmt, обработка ошибок, modules
Глубина анализа кодаIntelliJ, ArchUnit, SpotBugsvet, golangci-lint — хорошо, но менее глубоко
Старт и памятьJVM прогревается секундыбинарь стартует мгновенно
Регулируемые отраслистандарт в банках и госкомпанияхместами требует обоснования

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

Самый частый вариант на практике гибридный: Java или Kotlin для доменных сервисов (заказы, каталог, платежи), Go для периферии (шлюз, проксирование, лёгкие точки входа). Вопрос «Java или Go» — не «или», а «где что».

Несколько языков в одном продукте

Полный стек на одном языке (TypeScript на клиенте и сервере) проще держать в голове и в правилах агента, чем зоопарк, и это честный аргумент. Но у него есть цена, о которой забывают: серверный TypeScript без фреймворка даёт больше шума, чем Java со Spring, и «один язык» на практике означает «Node.js в бэкенде», а не «Java во фронтенде». Для продукта с толстым доменом это плохая сделка: доменная логика получает более слабый стек ради того, чтобы фронтенд и бэкенд совпали по синтаксису.

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

  • один основной язык для бизнес-логики, остальные — для специализированных задач с чёткой границей (Python для машинного обучения, Go для сетевого ввода-вывода);
  • один набор архитектурных принципов поверх всех языков: код разный, устройство одно;
  • правила для агента — под каждый язык отдельно. Это инвестиция, но на большой команде она окупается.

Что не влияет

  • Возраст языка. Java старше Rust на двадцать лет и дружелюбнее к агенту: больше данных, сильнее соглашения, мощнее инструменты.
  • Производительность. Агент пишет одинаково корректный код на «быстром» Rust и на «медленном» Python; скорость работы программы — отдельный вопрос.
  • «Современность». Scala и Haskell современнее Java по системе типов, но агент на них слабее: данных меньше, соглашения разрознены.
  • Личные предпочтения. Важны для найма и удержания, к дружелюбию к агенту отношения не имеют.

Что меняется в правилах агента при смене языка

Методология не привязана к языку: устройство сервиса (операция, обработчик, репозиторий), спецификация как код, исполняемые правила ревью применимы на Java, Kotlin, Go, TypeScript и Python одинаково. Меняется слой под ней, и полезно знать, какой именно.

Остаётся тем же: правило зависимостей между слоями, форма контракта операции, список крайних случаев, которые проверяют, порядок приёмки. Меняется: чем ловится нарушение (ArchUnit на Java, dependency-cruiser в TypeScript, go vet и линтер на Go, import-linter в Python), какие типы объявляют границу (records и sealed интерфейсы, data class, структуры и интерфейсы, Pydantic-модели), как оформлена ошибка (исключение, Result, error в возвращаемом значении). Правила для агента пишут ровно на этот слой: «инвариант живёт в агрегате» одно для всех, «агрегат это record с приватным конструктором» — только для Java.

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

Коротко

  • Агент ошибается на всех языках одинаково, а всплывает ошибка по-разному: на статическом языке — в компиляторе за минуту, на динамическом — в проде через неделю.
  • Приёмка кода от агента начинается с красного сигнала, а не с чтения: сборка, проверка типов, линтер, тесты. Строгий язык даёт этот набор из коробки, динамический требует собрать его руками.
  • Пять свойств, которые агент усиливает: объём в обучающих данных, строгость типов, сила соглашений, явность против краткости, инструментарий.
  • Явный код, а не короткий: кодогенерация на этапе компиляции — хорошо, динамическая магия во время работы (method_missing, неявные преобразования, метаклассы) — плохо.
  • Язык уже Python — не переписывать: mypy как блокирующая ступень, Pydantic на все модели, ruff, тесты с ожидаемым значением из требования. JavaScript — TypeScript постепенно, ESLint с проверкой по типам.
  • Язык не главное, когда продукт короткоживущий, домен простой, экосистема диктует язык или команда знает один.
  • Java — для сложного домена, большой команды и регулируемых отраслей; Go — для сети, шлюзов и быстрого старта; на практике чаще гибрид.
  • Один язык на клиент и сервер упрощает правила агента, но платит за это слабым серверным стеком; правила пишут под каждый язык отдельно, принципы одни.

Что почитать дальше