[Перевод] Как я программирую при помощи БЯМ

e5992ba47a88f9a87a132211c0f4f099

От переводчика.

Я решил предложить вам перевод этого объёмистого и довольно сложного текста, так как в течение последнего года немало слышал серьёзных успехах больших языковых моделей (БЯМ) в обработке программного кода — в куда большей степени, чем при коммуникации на естественном языке. Например, мой давний знакомый и собеседник Виктор Георгиевич Сиротин @visirokмного пишет в своей Telegram-группе «Материализация идей» об опытах программирования с использованием Copilot. Под катом я помещаю для вас перевод статьи знаменитого и влиятельного инженера из Кремниевой Долины Дэвида Крошо (David Crawshaw), сооснователя и технического директора (CTO) компании Tailscale. Ранее Дэвид более 9 лет работал программистом-исследователем в компании Google и в настоящее время является одним из самых авторитетных практикующих специалистов по языку Go. В частности, именно Дэвид адаптировал Go для платформ iOS и Android. В статье Дэвид делится своими наблюдениями о том, какую работу программист может и должен поручать большим языковым моделям, какие подводные камни есть в этом искусстве, и как оно может развиваться в ближайшие годы. Далее — от автора.

В этом документе я обобщил мой собственный опыт, приобретённый за минувший год и связанный с использованием генеративных моделей при программировании. Не скажу, что это был пассивный процесс. Я намеренно изыскивал возможности воспользоваться БЯМ при программировании, чтобы подробнее их изучить. В результате я теперь постоянно применяю БЯМ при работе и считаю, что в сухом остатке они положительно влияют на мою продуктивность (попробовал вернуться к программированию без них — опыт не понравился).

За этой работой я смог выделить часто повторяющиеся шаги, которые можно оптимизировать, и сейчас мы с единомышленниками разрабатываем инструмент именно для этой цели, ориентированный на работу с Go:  sketch.dev. Это только начало, но пока имеющийся опыт радует.

Контекст

Обычно мне любопытно узнавать о новых технологиях. Поэкспериментировав самую малость с БЯМ, я задумался, можно ли извлечь из них практическую пользу. Технология, которая (хотя бы иногда) способна выдавать отточенные ответы на каверзные вопросы, располагает к себе. Ещё увлекательнее наблюдать, как компьютер пытается по сформулированному запросу написать фрагмент программы — и серьёзно в этом прогрессирует.

На моей памяти был только один технологический переход, субъективно сравнимый с нынешним: дело было в 1995 году, когда мы впервые сконфигурировали локальную сеть с применением рабочего маршрута по умолчанию. У нас в соседней комнате стоял совместно используемый компьютер, на котором действовала программа Trumpet Winsock, и мы заменили его машиной, которая поддерживала маршрутизацию соединения через диал-ап. Тогда в один момент Интернет оказался у меня под рукой. В тот момент казалось ошеломительным иметь бесперебойный доступ к Интернету, казалось, что мы в будущем. Пожалуй, я тогда это ощущал острее многих других, привыкших к Интернету в университетах, поскольку я сразу окунулся в новейшие Интернет-технологии: веб-браузеры, JPEG, миллионы людей. Доступ к мощным БЯМ ощущается примерно так же.

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

Обзор

При повседневном программировании я обычно использую БЯМ в трёх следующих качествах:

1.     Автозавершение. Мне удаётся работать продуктивнее, если модель сама набирает за меня относительно очевидные фрагменты текста. По-видимому, в нынешнем состоянии здесь ещё есть над чем работать, но это тема для отдельного разговора. Даже стандартные решения «с полки» лучше, чем ничего. Я сам в этом убедился, пытаясь от них отказаться. Не проходило и недели, чтобы меня не начало удручать, сколько букв требуется набить хотя бы для того, чтобы получилась модель формального взаимодействия (FIM). Именно эта область более всего располагает к экспериментам.

2.     Поиск. Если у меня возникает вопрос о работе в сложной среде, например: «как мне сделать эту кнопку прозрачной средствами CSS?», то на него мне лучше всего ответит БЯМ, работающая на основе пользовательских данных — o1, sonnet 3.5, т.д. В этом она не только превзойдёт любой старомодный веб-поисковик и даже попытается распарсить ту веб-страницу, на которой я окажусь. (Иногда БЯМ ошибаются. Точно как и люди. Как-то раз я нахлобучил себе на голову туфлю и спросил двухлетнюю дочку, как ей моя шляпа. Она же не поняла шутку и как следует меня отчитала. БЯМ может что-то «перепутать», как и я в тот момент).   

3.     Программирование через чат. Это самая сложная из трёх стратегий. Именно с ней я извлекаю из БЯМ максимум пользы, но, в то же время, именно она меня более всего беспокоит. В данном случае нужно много учиться и постоянно корректировать свой подход к программированию — в принципе, мне это не нравится. Чтобы научиться таким способом извлекать из чата с БЯМ полезную информацию, требуется повозиться не меньше, чем при осваивании логарифмической линейки. Кроме того, меня раздражает непредсказуемость в работе этого сервиса, где постоянно меняется не только поведение самой программы, но и пользовательский интерфейс. Действительно, в долгосрочной перспективе я собираюсь избавиться от программирования через чат, поставить эти модели на службу разработчику, причём, чтобы взаимодействие с ними было не столь отталкивающим. Но пока я стараюсь решать эту проблему поступательно, то есть, научиться обращаться с БЯМ как можно лучше и подумать, в чём такую практику можно доработать.

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

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

Зачем вообще использовать чат

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

Таким образом, вам программирование через чат может и не подойти. Именно та разновидность программирования, которой приходится заниматься мне — это разработка продукта. В общих чертах это попытка предоставить программу пользователю через надёжный интерфейс. Поэтому я много пишу, много выбрасываю и то и дело перехожу из одного контекста в другой. Бывают дни, когда я пишу в основном на TypeScript, иногда — преимущественно на Go. В прошлом месяце я целую неделю просидел в базе кода на C++, исследуя одну идею, и мне как раз представилась возможность изучить, в каком формате на стороне сервера обрабатываются события при работе по протоколу HTTP. Я сразу везде, что-то всё время забываю и переучиваю. Если вы тратите большую часть времени не на написание кода как такового, а на перепроверку, не стал ли криптографический алгоритм после ваших оптимизаций более уязвим для атак по времени, то не думаю, что изложенные здесь мои наблюдения будут вам полезны.

БЯМ с функцией чата лучше всего справляются с вопросами экзаменационного характера

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

1.     Старайтесь не загонять БЯМ в чрезмерно сложные и неоднозначные ситуации, в которых она может запутаться и выдать вам некачественные результаты. Именно поэтому я не преуспел с чатом в рамках моей IDE. У меня в рабочем пространстве царит беспорядок, тот репозиторий, над которым я работаю, по умолчанию слишком велик, и модель в нём постоянно на что-то отвлекается. По состоянию на январь 2025 года люди значительно превосходят БЯМ в умении сосредотачиваться. Вот почему я по-прежнему работаю с БЯМ через браузер — хорошо сформулированные запросы получаются у меня только в случае, если я создаю их с чистого листа.

2.     Просите выполнить работу, которую легко проверить. Как программист, работающий с БЯМ, вы должны быть в состоянии легко прочитать код, который она написала, обдумать его и решить, хорошо ли она справилась с поставленной задачей. Можно попросить БЯМ сделать такие вещи, о которых вы никогда не попросили бы человека. Например, любой человек сочтёт оскорбительным приказ «перепиши все твои новые тесты и добавь в них <промежуточная концепция, которая, предположительно, должна сделать тесты более удобочитаемыми>». Ещё бы, ведь после этого вам придётся несколько дней жаркой перепалки с человеком, а стоит ли такая работа затраченных усилий. БЯМ же сделает такую работу за минуту и совершенно не будет вам перечить. Пользуйтесь тем, что переделать работу почти ничего не стоит.

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

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

Не составляет труда добавлять в код новые структуры

Ежедневно приходится идти на зыбкие компромиссы, связанные с тем, чего стоит написать код, прочитать его и отрефакторить. Разберём для примера ситуацию с границами пакетов в Go. В стандартной библиотеке есть пакет «net/http», в котором содержатся некоторые фундаментальные типы. Они предназначены для работы с кодировкой форматов, MIME-типами, т.д. Также здесь находятся HTTP-клиент и HTTP-сервер. Действительно ли это должен быть один пакет, или его можно разделить на несколько? Есть о чём поспорить! Честно говоря, в настоящий момент я не знаю, есть ли верный ответ на этот вопрос. То, что у нас сейчас есть — работает, и, спустя 15 лет работы, я по-прежнему не уверен, могла бы какая-нибудь другая конфигурация пакетов работать лучше.

Вот некоторые преимущества относительно больших пакетов: централизована документация по вызывающим функциям, легче приступать к написанию кода, упрощается рефакторинг, проще делиться вспомогательным кодом, не изобретая специально для этого надёжных интерфейсов (для чего зачастую требуется подтягивать фундаментальные типы из пакета в ещё один листовой пакет, также наполненный типами). Недостатки в том, что большой пакет сложнее читать, поскольку в нём происходит много всего и сразу (попробуйте почитать реализацию клиента net/http, не оступившись и не провалившись на несколько минут в серверный код). Например, есть у меня база кода, где в некоторых фундаментальных типах применяется библиотека C. Но отдельные элементы этой базы кода должны быть в двоичном файле, широко распространяемом по многим платформам, где технически не требуется библиотека на C. Поэтому рассчитывайте на то, что у вас в базе кода будет больше пакетов, чем ожидается, и некоторые из них нужны именно для изоляции библиотеки на C, во избежание cgo в мультиплатформенном двоичном файле.

В данном случае нет однозначно верных решений, мы просто тасуем различные типы работы, которую придётся выполнять инженеру (предстоящую и текущую). Вот как БЯМ влияет на эти компромиссы:

  • Поскольку БЯМ лучше справляются с вопросами, сформулированными в экзаменационном стиле, модели требуется как можно больше мелких пакетов. По ним ей будет проще очертить полный, но при этом обособленный контекст некоторого куска работы. То же справедливо и для человека, именно поэтому мы и пользуемся пакетами. Однако в данном случае мы «расплачиваемся» за размер пакета дополнительной работой по типизации/латанию/систематизации кода, чтобы повысить его удобочитаемость. Когда БЯМ делает эту работу за нас и сама же получает от этого пользу (а это большой кусок работы), акценты в компромиссе смещаются. В качестве бонуса мы получаем более удобочитаемый код. 

  • Сравнительно мелкие и более многочисленные пакеты можно компилировать и тестировать в отрыве от не связанного с ними кода. Это полезно в цикле разработки с применением БЯМ, поскольку в таком случае изменения можно протестировать и скомпилировать ещё до рефакторинга других зависимых пакетов. Среда для тестирования пакетов упрощается.

Пример

Давайте проработаем пример, на котором я в совокупности продемонстрирую некоторые из обсуждённых выше идей.

Напишем инструмент резервуарной выборки для квартилей чисел с плавающей точкой.

Начнём со структуры пакета. Если бы я делал это до применения БЯМ, то мне пришлось бы выбрать тот или иной пакет потоковой статистики, в котором содержалось бы несколько алгоритмов — может быть, по одному на файл. По-видимому, не только мне в голову приходила такая мысль — существует свободно распространяемый пакет quantile, построенный именно по такой модели. Итак, теперь я хочу выделить в собственный пакет лишь этот конкретный алгоритм. Другие варианты или родственные алгоритмы можно вынести в собственный пакет.

Далее рассмотрим, что нам может дать БЯМ. Первый проход получился неплохим. Сформулировал такой промпт, дополнительно уточнив, что он нужен мне на Go, в результате получил quartile_sampler.go:

// QuartileSampler ведёт резервуарную выборку для оценки квартилей потока  
// значений float64. Здесь резервуарная выборка применяется для того, чтобы у 
// нас было репрезентативное множество при обработке больших потоков данных. 
type QuartileSampler struct {  
   reservoir []float64  
   seen      int  
   maxSize   int  
   rng       *rand.Rand  
}

// NewQuartileSampler создаёт новый инструмент выборки, в котором может содержаться до maxSize образцов.  
// Чем больше maxSize, тем более точные оценки мы получаем, но тем больше памяти при этом расходуем.  
func NewQuartileSampler(maxSize int) *QuartileSampler { … }

Базовый интерфейс тоже хорош:

// Добавляет в выборку новое значение.  
func (qs *QuartileSampler) Add(value float64) { … }

// Квартили возвращают оценочные значения для 25-й, 50-й (медиана) и 75-й перцентилей  
// тех данных, что мы пока успели рассмотреть. Если никакие данные добавлены не были, то возвращается (0,0,0).  
func (qs *QuartileSampler) Quartiles() (q1, median, q3 float64) {  
   if len(qs.reservoir) == 0 {  
       return 0, 0, 0  
   }  
    
   // Делаем копию во избежание изменения резервуара  
   sorted := make([]float64, len(qs.reservoir))  
   copy(sorted, qs.reservoir)  
   slices.Sort(sorted)  
    
   n := len(sorted)  
   q1 = percentile(sorted, 0.25)  
   median = percentile(sorted, 0.50)  
   q3 = percentile(sorted, 0.75)  
    
   return q1, median, q3  
}

Отлично! Ещё есть тесты.

Отмечу, что на этом можно было бы и остановиться. Иногда я пользуюсь генерацией кода через БЯМ как своеобразным специализированным поиском. Скажем, меня интересует резервуарная выборка, но при этом я хочу посмотреть, как алгоритм поведёт себя при каком-нибудь странном ограничении, например, при выборке взвешенных по времени данных. Можно не копаться в литературе, а немного откорректировать промпт, так, чтобы в новой реализации отслеживалась свежесть данных. Также можно было бы попросить модель указать в комментариях ссылки на источники, которые я затем мог бы проверить вручную и определить, нашла ли она какие-нибудь солидные исследования, достойные дальнейшей проработки. Бывает, я потрачу минуту, чтобы прочитать сгенерированный код, вижу в нём очевидный приём, о котором сам бы и не подумал, затем отбрасываю этот код и начинаю всё заново. Просто теперь я знаю, что такой приём возможен. Вот почему так сложно оценить материал, сгенерированный БЯМ. Да, иногда она выдаёт плохой код, буксует, совершает что-то невозможное (она навоображала себе такой элемент API Monaco, который я действительно хотел бы когда-нибудь встретить в реальности), просто тратит моё время. Но при этом я могу сэкономить многие часы, вычленив из её выдачи что-то важное, чего я раньше не знал.

Вернёмся к коду. Забавно, что первый вариант кода, предложенного БЯМ, не скомпилировался. Посреди реализации квартилей была такая строка:

n := len(sorted)

Она вполне нормальная, сортирует сегмент, определённый несколькими строками выше. Но это значение так нигде и не используется, поэтому gopls (и компилятор Go в ответ на команду go build) сразу же говорит:

declared and not used: n

Исправить это очень легко. Если я вставлю ошибку обратно в БЯМ, она её исправит. Но в данном случае я читаю код и явственно вижу, что просто могу сам удалить эту строку — что и делаю.  

Переходим к тестам. Результаты меня не удивили. В quartile_sampler_test.go:

func TestQuartileSampler(t *testing.T) {  
   tests := []struct {  
       name     string  
       data     []float64  
       wantQ1   float64  
       wantMed  float64  
       wantQ3   float64  
       epsilon  float64 // допустимая ошибка при сравнении чисел с плавающей точкой  
   }{  
       {  
           name:    "basic sequence",  
           data:    []float64{1, 2, 3, 4, 5, 6, 7},  
           wantQ1:  2,  
           wantMed: 4,  
           wantQ3:  6,  
           epsilon: 0.1,  
       },  
	…  
   }

   for _, tt := range tests {  
       t.Run(tt.name, func(t *testing.T) {  
           qs := NewQuartileSampler(100) // Для точности тестирования используем большой резервуар  
           for _, v := range tt.data {  
               qs.Add(v)  
           }

           q1, med, q3 := qs.Quartiles()  
            
           if math.Abs(q1-tt.wantQ1) > tt.epsilon {  
               t.Errorf("Q1 = %v, want %v (±%v)", q1, tt.wantQ1, tt.epsilon)  
           }  
           if math.Abs(med-tt.wantMed) > tt.epsilon {  
               t.Errorf("Median = %v, want %v (±%v)", med, tt.wantMed, tt.epsilon)  
           }  
           if math.Abs(q3-tt.wantQ3) > tt.epsilon {  
               t.Errorf("Q3 = %v, want %v (±%v)", q3, tt.wantQ3, tt.epsilon)  
           }  
       })  
   }
}

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

Во-первых, БЯМ не прогоняет эти числа через другую реализацию (насколько мне известно. Работая с многоумной БЯМ, сложно с уверенностью сказать, что именно происходит у неё за кулисами). Она сама их вычислила, а БЯМ, как известно, не очень сильны в арифметике. Поэтому тест такого рода целесообразно проводить в случае, если код написан человеком (ведь мы опираемся на вывод, полученный из другого инструмента), либо в случае, если мы решили дедовским методом сами посчитать арифметику, то подобные задачи плохо подходят для БЯМ. 

Вторая проблема — в том, что мы могли бы справиться лучше. Я счастлив жить во времена, когда программисты сами пишут себе тесты, но мы не придерживаемся в коде тестов столь же строгих стандартов, как в боевом коде. Это разумный компромисс, в сутках всё-таки всего 24 часа. Но недостаточную математическую подготовку БЯМ с успехом компенсируют за счёт энтузиазма.

Давайте попробуем ещё более качественный тест.

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

Так у нас получился новый тестовый код:

// referenceQuartiles вычисляет точные квартили для сегмента значений float64  
// при помощи линейной интерполяции, то есть, действует примерно так, как действовал бы инструмент выборки.  
func referenceQuartiles(data []float64) (q1, median, q3 float64) { … }

// compareQuartiles проверяет, действительно ли два набора квартилей отличаются друг от друга в рамках погрешности.  
// Возвращает true, если степень несовпадения остаётся в допустимых рамках, в противном случае возвращает false.  
func compareQuartiles(q1a, meda, q3a, q1b, medb, q3b, epsilon float64) bool { … }

// checkQuartiles – это тестовый помощник, сравнивающий выведенную выборку со справочной 
// реализацией и сообщающий обо всех найденных отличиях.  
func checkQuartiles(t *testing.T, data []float64, epsilon float64) {  
   t.Helper()  
    
   // Получаем справочные значения  
   wantQ1, wantMed, wantQ3 := referenceQuartiles(data)  
    
   // Получаем значения выборки, и для точности используем большой резервуар  
   qs := NewQuartileSampler(1000)  
   for _, v := range data {  
       qs.Add(v)  
   }  
   gotQ1, gotMed, gotQ3 := qs.Quartiles()  
    
   if !compareQuartiles(gotQ1, gotMed, gotQ3, wantQ1, wantMed, wantQ3, epsilon) {  
       t.Errorf("Quartiles mismatch:\ngot  (q1=%v, med=%v, q3=%v)\nwant (q1=%v, med=%v, q3=%v)\nepsilon=%v",  
           gotQ1, gotMed, gotQ3, wantQ1, wantMed, wantQ3, epsilon)  
   }  
}

Далее я переработал исходный тест, показанный выше, так, чтобы в нём использовались checkQuartiles — и у нас получилось что-то новенькое:

func FuzzQuartileSampler(f *testing.F) {  
   // Добавляем корпус значений для посева  
   f.Add([]float64{1, 2, 3, 4, 5})  
    
   f.Fuzz(func(t *testing.T, data []float64) {  
       // При фаззинге используем увеличенную погрешность, поскольку разброс крайних значений может оказаться шире  
       checkQuartiles(t, data, 0.2)  
   })  
}

Это смешно, потому что неверно. У меня работает инструмент gopls, который сразу же сообщает:

fuzzing arguments can only have the following types:
    string, bool, float32, float64,
    int, int8, int16, int32, int64,
    uint, uint8, uint16, uint32, uint64,
    []byte

Если передать эту ошибку обратно БЯМ, она повторно сгенерирует фаззинг-тест, так, что на этот раз он выстраивается вокруг функции func(t *testing.T, data []byte), которая при помощи math.Float64frombits извлекает из сегмента данных числа с плавающей точкой. Взаимодействия такого рода лишний раз стимулируют нас автоматизировать обратную связь от инструментов. Всё, что требовалось модели — это очевидное сообщение об ошибке, на его основе она делала успехи в полезной работе. Я не требовался.

Экспресс-анализ истории наших с БЯМ чатов за последние несколько недель показывает, что более чем в 80% случаев ошибки имеют инструментальный характер. БЯМ может прогрессировать без всяких моих подсказок (но, как я упоминал выше, это ни в коей мере не нельзя считать авторитетным количественным анализом). Примерно в половине случаев модель может решить проблему без моего вмешательства, я выступаю лишь в роли посредника.

Куда мы движемся? Тесты становятся лучше, возможно, даже не столь DRY

Лет 25 назад среди программистов было движение, объединённое призывом «не повторяйся» (don«t repeat yourself, сокращённо DRY). Как часто бывает в случаях с краткими запоминающимися принципами, которые преподаются старшеклассникам, это движение зашло слишком далеко. Чтобы абстрагировать фрагмент кода и создать условия для его повторного использования, приходится нести немало издержек, требуется создавать промежуточные абстракции, которые нужно заучивать. В обособленный таким образом код нужно вносить фичи, благодаря которым этот код становится максимально полезен самому широкому кругу людей. Таким образом, мы впадаем в зависимость от библиотек, наполненных бесполезными деталями, которые мешают сосредоточиться.

В последние 10–15 лет наблюдался гораздо более взвешенный подход к коду: многие программисты усвоили, что лучше повторно реализовать концепцию, если обеспечивать разделяемость выходит дороже, чем написать и поддерживать отдельный код. Я теперь гораздо реже стал писать в код-ревью «этого не стоит делать, разделите реализации» (И это к лучшему, поскольку никому в самом деле не нравится слышать такие вещи, когда вся работа уже сделана). Программисты постепенно совершенствуются в искусстве компромиссов.

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

Я считаю, что эта тенденция лучше всего просматривается на уровне обёрток REST API, специфичных для различных языков. В API любой крупной компании найдутся десятки таких примеров (обычно низкокачественных). Авторы таких обёрток на самом деле не используют своих реализаций в каких-то конкретных целях, а пытаются вписать все сучки и задоринки некоторого API в большой и сложный интерфейс. Даже если это сделано хорошо, на мой взгляд, было бы проще посмотреть документацию REST (обычно это набор curl-команд) и реализовать языковую обёртку для того 1% кода API, который меня действительно интересует. Так значительно сокращается объём материала по API, который приходится изучить заранее, а также упрощается чтение и осмысление кода в дальнейшем — как для меня, так и для других программистов, которым придётся с ним работать.

Например, в рамках моего недавнего проекта на sketch.dev я реализовал на Go обёртку для Gemini API. Даже притом, что существует официальная обёртка на Go, бережно сработанная знатоками языка, которым определённо не всё равно, в неё требуется хорошенько вчитываться, чтобы понять:

$ go doc -all genai | wc -l  
    1155

В первой версии моя упрощённая исходная обёртка состояла всего из 200 строк кода — один метод, три типа. Прочитать всю реализацию — это 20% от работы, требуемой на прочтение документации по официальному пакету. Если же вы решите покопаться в этой реализации, то обнаружите, что она — сама по себе обёртка, в которой заключена другая реализация, состоящая в основном из сгенерированного кода, и в ней полно всяких proto, grpc и т.д. А мне только-то и требовалось выполнить cURL и разобрать объект JSON.

Рано или поздно в проекте наступает момент, когда Gemini становится основой всего проекта, в котором используются почти все до единой возможности Gemini, а выстраивание на основе gRPC хорошо согласуется с использованием системы телеметрии, задействованной где-то ещё в вашей организации, там, где уже следует использовать большую официальную обёртку. Но, как правило, такая работа требует гораздо больше времени, как на подготовительном, так и на рутинном этапе. Учитывая, что нам почти всегда требуется лишь тончайший срез любого API, с которым приходится сегодня работать, такие собственные клиенты, в основном написанные на мощностях GPU, значительно эффективнее при решении любой задачи.

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

Автоматизация всего, что подмечено выше: sketch.dev

Как программист я инстинктивно стараюсь поставить компьютеры себе на службу. Из БЯМ можно извлечь массу пользы, как же перепоручить компьютеру работу с моделью?

Думаю, для решения этой задачи важнее всего не слишком обобщать. Решаешь конкретную задачу, а потом постепенно расширяешься. Поэтому не стоит сразу делать универсальный UI для программирования через чат, добиваясь, чтобы этот интерфейс в равной степени хорошо работал и с COBOL, и с Haskell. Лучше сосредоточиться на конкретном окружении. Я программирую в основном на Go, поэтому мне легко представить такой список для моих коллег по Go:

  • Что-то вроде песочницы для Go, выстроенной вокруг редактирования пакета и выполнения тестов

  • Чтобы в этом был интерфейс для чата, в котором можно редактировать код

  • Небольшая среда на основе UNIX, в которой можно было бы выполнять go get и go test

  • Интеграция с goimport-ами

  • Интеграция с gopls

  • Автоматическая обратная связь от модели: при редактировании модели выполнять go get, go build, go test, получать обратную связь по недостающим пакетам, ошибкам компилятора, отказам тестов, так, чтобы можно было сообщать эту информацию модели и добиваться, чтобы она автоматически исправляла ошибки

Мы с коллегами собрались и написали ранний прототип такого инструмента:  sketch.dev.

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

Иными словами, мы не пытались встраивать goimport-ы в скетч ради того, чтобы ими пользовались люди, а стремились приблизить код Go к виду, готовому для компиляции, и для этого использовали автоматические сигналы. Таким образом компилятор сможет предоставлять БЯМ более качественную обратную связь об ошибках, одновременно управляя моделью. Таким образом,  sketch.dev правильнее понимать как «интегрированную среду для разработки на Go, ориентированную на БЯМ».

Всё это очень свежие разработки, и многое ещё предстоит сделать — например, интегрировать инструмент с git, чтобы можно было загружать для редактирования уже существующие пакеты, а отредактированные результаты затем сбрасывать в ветку. Нужно лучше протестировать обратную связь. Наладить взаимодействие с консолью. (Если получен ответ «запусти sed», то нужно запустить sed, будь ты человек или БЯМ.) Пока мы продолжаем исследования, но уверены, что, если сосредоточиться на создании среды для определённого рода программирования, то получится лучше, чем при попытке создать универсальный инструмент.

© Habrahabr.ru