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

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

@ai_longreads

Армин Ронахер размышляет о том, почему нам нужны новые языки программирования, оптимизированные для агентного кодинга, и какие характеристики делают язык удобным для LLM.

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


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

A Language For Agents Автор: Armin Ronacher Оригинальный текст:

В прошлом году я впервые задумался о том, как будут выглядеть языки программирования в эпоху агентного (agentic) подхода к разработке. Сначала мне казалось, что огромный корпус существующего кода закрепит позиции нынешних языков, но теперь я думаю иначе. Хочу поделиться своими мыслями о том, почему мы увидим больше новых языков программирования и почему здесь открывается пространство для интересных инноваций. И если кто-то захочет создать такой язык — вот к чему, на мой взгляд, стоит стремиться!

Почему новые языки работают

Работает ли агент значительно лучше на языке, который есть в его весах? Очевидно, да. Но есть менее очевидные факторы, влияющие на успешность агента: качество инструментария вокруг языка и скорость его изменений.

Zig, похоже, слабо представлен в весах (по крайней мере в моделях, которые я использовал) и при этом быстро меняется. Сочетание не оптимальное, но терпимое: можно программировать даже на новой версии Zig, если указать агенту на нужную документацию. Но это не идеально.

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

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

Главная причина, по которой новые языки могут сработать — стоимость написания кода резко падает. В результате широта экосистемы становится менее важной. Теперь я регулярно выбираю JavaScript там, где раньше использовал Python. Не потому что люблю его или экосистема лучше, а потому что агент гораздо лучше справляется с TypeScript.

Вот как это работает: если в выбранном языке не хватает нужной функциональности, я просто указываю агенту на библиотеку из другого языка и прошу сделать порт. Конкретный пример: недавно я написал Ethernet-драйвер на JavaScript для реализации хост-контроллера нашей песочницы. Реализации есть на Rust, C и Go, но мне нужно было что-то подключаемое и настраиваемое на JavaScript. Проще было попросить агента переписать, чем заставить работать систему сборки и дистрибуции с нативной привязкой.

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

Зачем нужен новый язык?

Почему вообще стоит думать о новом языке? Дело в том, что многие современные языки создавались с предположением, что набирать код на клавиатуре тяжело, поэтому мы жертвовали чем-то ради краткости. Например, многие языки — особенно современные — сильно полагаются на вывод типов, чтобы не приходилось их прописывать. Обратная сторона: теперь нужен LSP или сообщения об ошибках компилятора, чтобы понять тип выражения. Агенты тоже с этим борются, и это раздражает при ревью пул-реквестов, где сложные операции могут затруднить понимание реальных типов. Полностью динамические языки ещё хуже в этом плане.

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

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

Так что аргументы за новый язык сводятся к следующему: учитывая фундаментальные изменения в том, кто программирует и какова стоимость кода, стоит как минимум рассмотреть эту идею.

Чего хотят агенты

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

Вот что, на мой взгляд, останется актуальным ещё долго.

Контекст без LSP

Language Server Protocol позволяет IDE получать семантическую информацию о том, что находится под курсором или что подставлять при автодополнении. Отличная система, но с одной особенностью, сложной для агентов: LSP должен работать.

Есть ситуации, когда агент просто не запустит LSP — не из-за технических ограничений, а потому что он ленится и пропустит этот шаг, если можно. Если дать ему пример из документации, нет простого способа запустить LSP, потому что это может быть неполный сниппет. Если указать на репозиторий GitHub и он скачает отдельные файлы, он просто посмотрит на код. Он не настроит LSP для получения информации о типах.

Язык, который не разделяется на два опыта (с-LSP и без-LSP), будет полезен агентам, потому что даёт единый способ работы в гораздо большем числе ситуаций.

Скобки: фигурные, квадратные и круглые

Мне, как Python-разработчику, больно это говорить, но отступы через пробелы — проблема. Эффективность работы с пробелами на уровне токенов (tokens) сложна, и язык со значащими пробелами труднее для LLM. Это особенно заметно при попытке заставить LLM делать точечные правки без вспомогательных инструментов. Часто они намеренно игнорируют пробелы, добавляют маркеры для включения или отключения кода, а затем полагаются на форматтер для исправления отступов.

С другой стороны, скобки без разделения пробелами тоже могут вызывать проблемы. В зависимости от токенизатора, последовательности закрывающих скобок могут разбиваться на токены неожиданным образом (похоже на проблему подсчёта букв в «strawberry»), и LLM легко ошибается в Lisp или Scheme, теряя счёт закрывающих скобок. Решаемо в будущих LLM? Конечно, но это было сложно и для людей без инструментов.

Контекст потока выполнения, но явный

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

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

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

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

Результаты вместо исключений

Агенты боятся исключений. Не уверен, насколько это решаемо с помощью RL (Reinforcement Learning, обучение с подкреплением), но сейчас агенты пытаются ловить всё что можно, логировать и делать плохое восстановление. Учитывая, как мало информации о путях ошибок, это понятно. Проверяемые исключения — один подход, но они распространяются по всей цепочке вызовов и не улучшают ситуацию радикально. Даже если они становятся подсказками, где линтер отслеживает возможные ошибки, много мест вызова требуют корректировки. И как с автораспространением предложенным для контекстных данных, это может быть не лучшим решением.

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

Минимальные диффы и построчное чтение

Сейчас агенты читают файлы в память построчно, поэтому часто захватывают куски, пересекающие многострочные строки. Простой способ увидеть проблему: заставьте агента работать с 2000-строчным файлом, содержащим длинные встроенные строки кода — по сути генератор кода. Агент иногда редактирует внутри многострочной строки, думая, что это реальный код, когда это просто встроенный код в многострочной строке. Для многострочных строк единственный язык с хорошим решением, который я знаю — Zig, но его синтаксис с префиксами непривычен большинству.

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

Делайте грепабельным

Что действительно хорошо в Go — обычно нельзя импортировать символы из другого пакета в область видимости без префикса имени пакета при каждом использовании. Например: context.Context вместо Context. Есть обходные пути (алиасы импортов и dot-imports), но они относительно редки и обычно не одобряются.

Это радикально помогает агенту понимать, на что он смотрит. В целом, делать код находимым через самые базовые инструменты — отлично: работает с внешними неиндексированными файлами и означает меньше ложных срабатываний для автоматизации на базе генерируемого на лету кода (например, вызовы sed, perl).

Локальное рассуждение

Многое из сказанного сводится к: агенты очень любят локальное рассуждение (local reasoning). Они хотят работать по частям, потому что часто работают с несколькими загруженными файлами в контексте и не имеют пространственного понимания кодовой базы. Они полагаются на внешние инструменты вроде grep для поиска, и всё, что сложно грепнуть или прячет информацию в другом месте — проблемно.

Сборка с учётом зависимостей

Что делает агентов успешными или провальными во многих языках — качество инструментов сборки. Многие языки затрудняют определение того, что реально нужно пересобрать или перетестировать из-за слишком многих перекрёстных ссылок. Go здесь хорош: запрещает циклические зависимости между пакетами (import cycles), пакеты имеют чёткую структуру, результаты тестов кэшируются.

Что агенты ненавидят

Макросы

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

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

Реэкспорты и barrel-файлы

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

И не обязательно быть слишком строгим. Go идёт этим путём, но не в крайности. Любой файл в директории может определить функцию, что не оптимально, но достаточно быстро находится и не нужно искать слишком далеко. Работает, потому что пакеты вынуждены быть достаточно маленькими, чтобы всё находилось grep'ом.

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

Алиасинг

Агенты часто ненавидят алиасы. Можно даже заставить их жаловаться на это в thinking-блоках, если дать им рефакторить что-то с кучей алиасов. В идеале язык поощряет хорошее именование и как результат не поощряет алиасинг при импорте.

Флейки-тесты и расхождение окружений разработки

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

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

Множественные условия сбоя

В идеальном мире у агента есть одна команда, которая линтит, компилирует и сообщает, всё ли в порядке. Может быть, ещё одна для запуска нужных тестов. На практике большинство окружений работают не так. Например, в TypeScript часто можно запустить код, хотя проверка типов провалилась. Это может запутать агента. Аналогично разные настройки бандлера могут привести к успеху в одном месте, но провалу в CI с немного другой конфигурацией. Чем унифицированнее инструментарий, тем лучше.

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

Увидим ли мы новые языки?

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

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

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


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

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

Report Page