
Полная версия
Золотая лихорадка ИИ-агентов. Делегирование неопределённости
Если задачи сопоставимого объёма разделяют минуты и сутки, это не загадка, а улика. Время прогона не берётся ниоткуда: раз его не съел объём, его съело то, чего задаче не хватало. Значит, если «чёткая» задача идёт несоразмерно своему объёму, чёткость была иллюзией, и неопределённость просто спряталась. Маска ей для этого не понадобилась: в этих парах есть только задача и время. И время говорит то же, что говорили маски. Улика ещё не показывает, где пряталась неопределённость. Она показывает только, что неопределённость была.
Спрятаться ей было где, и ни одно из этих мест не видно в самой задаче: её можно перечитывать сколько угодно и не заметить ни лишнего слова, ни недостающего. У каждой из трёх суточных задач место было своё.
Сама библиотека и в соседнем сервисе обновилась за минуты, и на ноутбуке тесты прошли. На сервере сборки упал тест, который сравнивает готовую миниатюру с эталонной: миниатюра вышла на едва заметный оттенок темнее. Падал он при каждом запуске, без исключений. ИИ-агент перебирал версии библиотеки, пересобирал, откатывал, собирал снова, клал рядом миниатюры с ноутбука и с сервера и сравнивал их точка за точкой. Каждая пересборка занимала минут двадцать, и за ночь их набралось несколько десятков. Код один, версия одна, исходная картинка одна, а цвет разный, и причины он не находил. К утру стало видно, в чём дело: на сервере под нашей библиотекой работает системная библиотека другой версии, и цвет она считает чуть иначе. Формулировка «обновить библиотеку» была чёткой. Капризным оказалось окружение, в котором её предстояло выполнить, и как оно себя поведёт, не знал никто — ни мы, ни он. Неопределённость пряталась снаружи, в мире. Там же живёт и редкий баг: задача починить его бывает короткой и чёткой, а когда и отчего он случается, из неё не узнать.
С методом API окружение ни при чём. Соседей у него два десятка, и написаны они по-разному: половина ходит к данным через старый слой, половина — через новый; одни сообщают об ошибках в одном виде, другие в другом; где-то длинный список отдают по номерам страниц, где-то — с меткой, откуда продолжать. В команде правило знает каждый: новое пишется только через новый слой, списки отдаются с меткой, а старые методы не трогают, пока их не перепишут целиком. Ни в одном файле оно не записано. Оно живёт в старых замечаниях на ревью, в истории переделок, в привычке всей команды.
ИИ-агент взял за образец ближайшего соседа — старого — и повторил его во всём. Потом стал читать историю соседних файлов, сверять даты и авторов правок, заметил, что за последний год к старому слою не прибавилось ни одного нового обращения, и переписал метод через новый. Потом нашёл два способа отдавать списки, долго сличал их с окружающим кодом и выбрал тот, что встречался чаще, — старый. Утром метод работал, тесты проходили, а первое замечание на ревью было коротким: «у нас так не делают». Сутки ушли на то, чтобы по косвенным следам угадывать правило, которое любой из команды назвал бы за полминуты, — и угадать наполовину. Для нас «по образцу соседних» значило одно, а для того, кто правила не знает, — любое из двух десятков. Правило действует давно, но в репозитории, а не в задаче, и для задачи, ушедшей вечером, его не существовало. Неопределённость пряталась в общем коде — в том, что знают все и не записал никто.
Последнее укрытие ближе всего. «Дать пользователю удалить свою учётную запись» мы пишем, отчётливо видя результат: кнопка в настройках, окно подтверждения, записи больше нет. Ночью ИИ-агент дважды переделал удаление, сначала только помечая запись, потом стирая насовсем; в утреннем diff она исчезает сразу и безвозвратно. Вместе с ней стёрты все комментарии пользователя, и обсуждения, где ему отвечали другие люди, теперь обрываются на полуслове. Общие документы, которые он когда-то создал, удалены тоже, хотя писали в них вдесятером. Читая это, мы уверенно поправляем: удалённую запись месяц можно восстановить, комментарии остаются с подписью «пользователь удалён», это же само собой разумеется. На документах мы запинаемся. Что с ними делать, мы, оказывается, не знаем и сами. Кнопка в этих сутках — малая часть; остальное ушло на то, чего в формулировке не было: сразу или со сроком, стереть или обезличить, чей труд уходит вместе с записью. Часть ответов у нас была, но осталась в голове и до задачи не дошла. Части не было вовсе — было только ощущение, что она есть. Вечером одно от другого не отличалось: всё казалось одинаково понятным. Неопределённость пряталась в голове автора.
Ясность для себя ещё не означает определённости задачи. Определённость — свойство задачи в момент передачи: решения приняты, критерий назначен. Ясность — состояние головы человека, и она может обманывать. Определённость уходит вместе с задачей. Ясность остаётся у нас. В задачах на минуты они совпадали: мы видели результат, и задача несла в себе всё, что нужно, чтобы его получить. В задачах на сутки ясность была ничуть не меньше — такая же спокойная и полная. Изнутри головы пары не различались. Различались задачи.
Чёткость, которую мы чувствовали, была ясностью, и ясностью настоящей: результат мы действительно видели. Определённости не было ни в одной из трёх суточных задач: как поведёт себя сервер сборки, не знал никто; правило о соседях знала команда, но не задача; а что делать с общими документами, не решил никто. Сверх того, что требовал объём, прогон шёл столько, сколько в задаче было неопределённости, а не столько, сколько у нас было ясности. «Чёткая» задача, которая идёт сутки, была ясна только своему автору. Определённой она не была.
2.3. Кажущееся исключение: объём
В пятистах файлах нашего кода стоит вызов, которого в следующем выпуске сетевой библиотеки уже не будет: прежний интерфейс, через который код ходит к чужим сервисам, объявлен устаревшим. Авторы библиотеки приложили руководство по переходу: таблица в две колонки, слева старый вызов, справа новый. Переписать пятьсот файлов под новый API — работа, о которой не спорят на планёрке и которую никто не хочет делать руками: одна и та же правка, повторённая сотни раз, скучная ровно настолько, насколько предсказуемая. Мы отдаём эту работу ИИ-агенту вечером, одной фразой со ссылкой на руководство, и уходим спокойно: думать тут не о чем.
Взглянем на неё такой, какой она видится вечером. Масок здесь нет: никто не называет эту работу сложной и не ждёт, что ИИ-агент придумает что-то своё. Спрятаться неопределённости тоже негде. Что делать, сказано в руководстве, и сказано не нами, а авторами библиотеки, которые знают её лучше всех. Как делать, очевидно: открыть файл, найти старый вызов, заменить по таблице, перейти к следующему. Когда готово, понятно заранее: старых вызовов не осталось ни одного, сборка идёт без ошибок, тесты проходят. Каждый файл по отдельности — дело нескольких минут, и ни в одном нет ничего, над чем стоило бы задуматься. Их просто пятьсот.
И прогон по ним идёт долго: не час и не два, а больше суток. Здесь и звучит возражение, и отмахнуться от него нельзя. При чём тут неопределённость? Незнания в пятистах файлах не больше, чем в одном; больше только самих файлов. Работа долгая не потому, что в ней что-то не решено, а потому, что она большая. Ведь если время прогона соразмеряют с объёмом, то вот он, объём, и время ему соразмерно: пятьсот файлов по нескольку минут и складываются в ночь. Будь файлов пять, прогон кончился бы за полчаса, и никто не заподозрил бы в нём никакого незнания. Вот оно, исключение: долгий прогон, под которым ничего не спрятано.
Но откроем файлы по одному. В первом вызов меняется ровно по таблице, во втором тоже, в десятом тоже. В двенадцатом старый вызов при сбое сети просто возвращал пустой ответ, и код ниже на это рассчитывал: пустота означала «показать то, что сохранилось с прошлого раза». Новый вызов в той же ситуации бросает ошибку. Руководство прямо об этом предупреждает, но что с ошибкой делать, сказать не может: оно описывает библиотеку, а не наш код. Можно поймать её и вернуть пустоту, как раньше. А можно дать ей подняться выше, туда, где её, возможно, давно ждут и ни разу не дождались. Здесь дорога, которая до сих пор не ветвилась, расходится, и дальше нужно выбрать. Такое место и есть развилка.
Дальше развилок становится больше. Старый вызов сам повторял запрос, если соединение обрывалось; новый не повторяет. Где мы только читаем данные, повтор безвреден, и его можно вернуть. Где запрос что-то создаёт на той стороне, повтор может создать это дважды, — и прежнее поведение, возможно, годами было тихой ошибкой, которую теперь можно исправить, а можно бережно перенести. Старый ждал ответа сколько угодно; новый требует назвать срок, и для запроса, ответа на который человек ждёт перед экраном, разумный срок один, а для фоновой выгрузки совсем другой. Старый сам шёл по перенаправлению на другой адрес; новый останавливается и возвращает этот адрес нам. Старый отдавал ответ готовым текстом, сам угадывая кодировку; новый отдаёт сырые байты, и угадывать теперь нам. Ни на один из этих вопросов не отвечает руководство, и ни на один — соседний файл: у каждого места свой код вокруг вызова и свои ожидания.
Пятьсот файлов — это пятьсот маленьких развилок, и чем их больше, тем больше молчаливых решений. Не буквально по одной на файл: где-то развилок нет вовсе, где-то три в одной функции. Но число работает против возражения, а не за него. Эти файлы писали разные люди в разные годы, и каждый по-своему опирался на старое поведение: один рассчитывал на пустой ответ, другой — на повтор, третий о сбоях просто не думал. Каждый следующий файл добавляет не только замену, но и ещё одно место, где старый вызов может не перейти в новый один к одному. Сам по себе такой выбор мал: минута мысли того, кто знает, как этот сервис должен вести себя при сбое. Но в пятистах файлах такие минуты складываются в сотни выборов, разбросанных по разным местам, и ни один из них не прозвучал вслух.
В diff эти выборы не отличить от механических замен: ушёл один вызов, пришёл другой. В итоговом сообщении прогона все они уместятся в одну фразу: старые вызовы заменены в пятистах файлах. На эти развилки и тратится время прогона. Сама замена занимает мгновение; часы уходят на то, чтобы в каждом файле прочитать код вокруг, понять, чего он ждал от старого вызова, выбрать и проверить выбранное.
А если развилок действительно нет? Допустим, новый интерфейс изменил только имена: каждый старый вызов переходит в новый один к одному, поведение осталось прежним до мелочей, и мы знаем это наверняка. Тогда вся работа укладывается в одно правило замены: где стоит это, поставь то. Правило, которое записывают один раз и применяют пятьсот раз, — это скрипт: замена по шаблону или короткая программа, которая разбирает код и переставляет в нём вызовы. Такой скрипт пройдёт по репозиторию за несколько минут, при втором запуске сделает в точности то же, что при первом, и его целиком можно прочитать до запуска. Ему не нужно понимать код вокруг вызова и не нужно ничего выбирать: ему хватает шаблона. Если в задаче нет неопределённости, ей нужен скрипт, а не ИИ-агент.
Механическую замену ИИ-агент сделает тоже, и сделает аккуратно. Но всё, чем он отличается от короткой программы, — умение прочесть незнакомое место, понять, чего от него ждали, и выбрать — в работе, где выбирать нечего, не пригодится ни разу. Отдавая работу ему, а не скрипту, мы зовём того, кто выбирает, и тем самым признаём, что выбирать есть что. ИИ-агент нужен только там, где есть что решать или искать.
Третьего не дано. Можно сказать: в тех нескольких файлах, что мы успели открыть вечером, вызов менялся ровно по таблице; развилок, скорее всего, нет, но за все пятьсот мы не поручимся, потому и отдаём работу ИИ-агенту, а не скрипту. Это и есть неопределённость: на момент передачи не известно, в каких из пятисот файлов дорога одна, а в каких расходится. Знать, что развилок нет, — значит уметь записать правило замены целиком. Если записать его целиком не получается, значит, где-то замена перестаёт быть заменой, и часы прогона уйдут на то, чтобы выяснить, где именно, — файл за файлом. Большая механическая работа бывает либо с развилками, либо под скрипт. Долгой и при этом определённой она не бывает.
Вечером мы видели в этой работе только объём. Но из всего, что в ней было, объём оказался самым коротким: скрипт проходит его за минуты. Всё остальное время забрали развилки, доставшиеся прогону невыбранными. Будь в тех же пятистах файлах вдвое больше развилок, ночь вышла бы длиннее; не будь их вовсе, она сжалась бы до минут скрипта. Возражение говорило: долго, потому что много. Точнее сказать: долго, потому что много развилок. А нерешённая развилка — неопределённость в самом прямом смысле слова: выбор, который отдали вместе с работой, не сделав.
Долгий прогон — всегда неопределённость, и теперь за этим «всегда» стоят пятьсот файлов: двенадцатый с его пустым ответом, запросы, которые нельзя повторять вслепую, срок ожидания, который никто не назвал, кодировка, которую больше некому угадывать. Большая механическая работа не обошла правило, а показала его с другой стороны: здесь неопределённость не пряталась ни под одним словом, она была разложена по файлам. Исключение, разобранное честно, подтверждает правило.
2.4. Названная неопределённость
Раз в два-три дня сервис личных кабинетов перезапускается сам. Ничего не падает с грохотом: память растёт медленно, ровной линией, пока процесс не упирается в предел и его не перезапускают, и пользователи замечают только несколько секунд без ответа. На тестовых данных рост не повторяется, в журналах виден лишь сам перезапуск. Отдавая этот баг ИИ-агенту, мы пишем всё, что знаем, и отдельно — чего не знаем. Знаем, что началось это три недели назад, после выпуска, в который вошло два десятка изменений. Знаем, что в будни память растёт быстрее, чем в выходные. Знаем, что две самые очевидные догадки уже проверены и обе оказались мимо. Не знаем одного: отчего. Ни у кого из нас ответа нет: он есть только у самого сервиса, и получить его можно, лишь расспрашивая сервис. Прогон идёт двое суток. ИИ-агент поднимает сервис на стенде, пропускает через него записанные за неделю запросы, ускорив их во много раз, делает снимки памяти через час работы и через три. Потом откатывает изменения выпуска половинами, затем половинами половин, и смотрит, без каких линия перестаёт ползти вверх. Под конец второго дня подозреваемый остаётся один: таблица в памяти, куда сервис заводит запись для каждого нового сочетания параметров запроса и откуда ничего не удаляет. Исправление после этого — дело минуты.
Старый сервер, на котором годами по расписанию запускались фоновые задания, выводят из работы, и задания переезжают в новый планировщик. Список заданий есть, расписание есть, код каждого лежит в репозитории; сам перенос — работа на час. Но за годы на одном сервере задания срослись с ним и друг с другом, и ни одна из этих связей не видна, пока смотришь на задание по отдельности. Одно берёт файл, который соседнее оставило на локальном диске часом раньше. Другое отсчитывает начало дня по часовому поясу сервера, а не по тому, что указан в настройках. Третье годами держалось на том, что его никогда не запускают дважды одновременно: сервер был один, задание успевало закончиться задолго до следующего запуска, и второму экземпляру взяться было неоткуда. Что такие связи есть, известно наверняка: без них старые серверы не живут. Где они и сколько их, неизвестно, и в задаче так и написано. ИИ-агент запускает задания в новом окружении рядом со старым, следит, что каждое читает и что пишет, несколько дней подряд сравнивает, что получилось там и там, и разбирает каждое расхождение, пока задания в новом планировщике не начнут вести себя так же, как на старом сервере. Связей находится семь, и о двух из них не помнил даже тот, кто когда-то писал эти задания. Вместо часа прогон занимает три дня, и почти всё это время съедают связи, о которых до начала не подозревали.
Страница со списком товаров отвечает за полторы секунды, а должна укладываться в треть секунды, на тех же запросах, что пользователи присылали за последний месяц, и с тем же содержимым. Число названо, замерить его можно в любую минуту. Какими правками к нему прийти — вопрос открытый. Может быть, хватит одного индекса в базе данных. Может быть, на каждый товар в списке уходит отдельный запрос к базе, и таких запросов сотни. Может быть, половину времени съедает не база, а подготовка ответа. А может быть, всё сразу, и каждое изменение даст понемногу. Какая из догадок верна, заранее не узнать: это выясняется только замерами, а сколько даст правка — только когда она сделана. ИИ-агент пробует одно, другое, третье. Индекс даёт немного. Один запрос к базе вместо сотен даёт почти половину. Готовые ответы, отложенные про запас, ускоряют страницу вдвое, но товары в ней встают не в том порядке, и эту попытку приходится откатить. К утру в коде остаются четыре правки из двух с лишним десятков попыток, и страница отвечает за четверть секунды.
Редкий прод-баг, миграция со скрытыми зависимостями, оптимизация с ясной метрикой: здесь долгий прогон законен. Этих прогонов не сократишь, подумав вечером подольше. Сколько ни сиди над задачей, причина перезапусков не придёт в голову, связи между заданиями не проступят в коде, а нужные правки не назовут себя сами. Сэкономить эти часы можно, только согласившись на догадку вместо ответа. Прятать здесь нечего, и красивое слово не нужно: время уходит туда, куда мы его отправили.
Но ни один из трёх прогонов не обходится без того, чего до его начала не знал никто. Это тоже неопределённость — пути и мира. Незнание бывает разным, и у того, с которым работают эти три прогона, два коротких имени. Путь — какими шагами дойти до цели. Мир — как на самом деле ведут себя код, зависимости и окружение, а не как они описаны. Эту часть незнания и можно отдавать машине: это и есть честная работа ИИ-агента. Отдавая её, нельзя сказать заранее, чем кончится прогон, сколько он продлится и найдётся ли вообще то, за чем мы отправили ИИ-агента. Утечка могла оказаться не в нашем коде, а в чужой библиотеке, которую нам не исправить; страница могла упереться в половину секунды, что с ней ни делай. Законная неопределённость остаётся неопределённостью.
Где же тогда граница между таким прогоном и самообманом? Не в задаче. Тот же баг с памятью можно отдать иначе. Вечером уходит та же задача, только без того, что мы о ней знали: без выпуска, после которого всё началось, без будней и выходных, без двух догадок, которые мы уже отбросили. ИИ-агент добросовестно начинает с самых очевидных догадок и тратит первые полсуток на то, чтобы проверить обе заново. Не зная, что память растёт по будням, он гоняет сервис под нагрузкой выходного дня и долго не видит роста. Не зная, когда начался рост, откатывает изменения за полгода, а не за три недели. Каждый такой крюк стоит ему часов. А на второй день, когда в еженедельной сводке надо написать, почему исправления всё ещё нет, мы пишем коротко: там редкий баг. Такой ответ устраивает всех, и нас самих тоже. Фраза звучит как объяснение, но ничего не объясняет. Слово «баг» говорит, что что-то сломано, слово «редкий» — что сломано не каждый раз; отчего, где и с какого выпуска, не говорит ни одно. Опровергнуть её нельзя — баг действительно редкий и действительно там, — и потому её не слышно: это маска, только сделанная из инженерных слов. Под ней та же неопределённость, что и в честном прогоне, — как ведёт себя наш собственный сервис, — и прогон по ней идёт те же двое суток, если не дольше. Баг тот же, и часы не короче.
То же с фразой, которая в словаре самообмана звучала почти научно: «ИИ-агент ищет оптимальное решение». В ней сказано о занятии ИИ-агента и ни слова о нашем незнании: одно заслоняет другое. Но в прогоне со страницей товаров он и правда подбирает шаги, которые заранее были неизвестны. Скажем о нём ту же фразу, назвав незнание: мы не знаем, какими правками прийти к трети секунды, и отдали ему именно это. Занятие прежнее; только под словом «оптимальное» теперь стоит число, а под «ищет» — названное незнание, и маски больше нет. Фраза из оправдания стала отчётом о работе.
Граница, выходит, проходит не по задаче, а по знанию о задаче. Честный прогон отличается от самообмана не тем, что в нём нет незнания, а тем, что его источник назван: мы знаем, чего не знаем, и знаем, почему отдали это машине. Вопрос не в том, отдали ли мы неопределённость, — отдали, раз прогон длинный. Вопрос в том, какую именно, и признаёмся ли мы себе в этом. Честность начинается с того, что мы знаем, какую отдали. Не с удачи: честный прогон может вернуться ни с чем, а прогон под маской — случайно наткнуться на ответ, но удача задним числом не переписывает вечер. И не с короткого срока: прогон с названной неопределённостью тоже может идти сутками.
У работы ИИ-агента в таких прогонах есть своё имя, и это не «он исследует». Этим словом мы называли прогон, у которого не было вопроса: оно обещало знание, не говоря, о чём. Законная работа над путём и миром называется поиском. «Исследует» говорит о том, чем занят ИИ-агент; «ищет причину перезапусков» — о том, какого ответа ждём мы. Искать можно только названное, и у поиска вопрос есть, заданный до начала прогона: отчего растёт память, что задания молча берут друг у друга, какими правками прийти к трети секунды. Защита поиска не отменяет тезис, а уточняет его. Неопределённости в трёх законных прогонах не меньше, чем под любой маской; нет в них только молчания о ней.
Хорошим долгий прогон от этого не становится, как не был и плохим. Он всегда неопределённость, и спрашивать о нём стоит не «сколько», а «какая» и «признана ли». Двое суток под словами «там редкий баг» не отвечают ни на один из этих вопросов. Двое суток над утечкой, отданной вместе с выпуском, буднями, отброшенными догадками и честным «отчего — не знаем», отвечают на оба ещё до начала: неопределённость — в том, как ведёт себя наш сервис, и она записана в самой задаче. Такой поиск — промывка на известной жиле: законная работа ИИ-агента, и тоже неопределённость, только названная.
2.5. Способность и смысл
Способность ИИ-агента работать сутками — реальное достижение, и индустрия справедливо им гордится. Удержать задачу на сутки — значит идти шаг за шагом и ни на одном шаге не потерять, куда идёшь. Длинная работа редко ломается на самом трудном месте. Чаще она рассыпается от мелочей: от забытого условия, от попытки, которую уже делали, от правки, которая молча спорит с правкой шестичасовой давности. ИИ-агент, который не рассыпается, на двадцатом часу помнит, о чём была задача и что из сделанного в начале прогона трогать уже нельзя. Он упирается в ошибку, которая десятый раз подряд всплывает в новом месте, и разбирает её заново, а не бросает работу и не начинает всё сначала. Помнит, что в полночь откатывал похожую правку, и не повторяет её на рассвете. Замечает, что его собственное давнее изменение теперь мешает, возвращается к нему и переделывает. И всё это время сам запускает сборку, сам читает, что она вывела, и сам решает, что делать дальше: подсказок ему не шлёт никто.
Здесь не о чем спорить и нечего умалять. Восхищение, с которым мы смотрим на такую работу, — не наивность: оно видит настоящее умение и видит его верно. Для того, кто смотрит на ИИ-агента, долгая работа значит именно это — что он умеет.
Но для владельца задачи эти сутки значат другое: не что умеет ИИ-агент, а сколько незнания было в задаче в момент передачи. Владелец — не должность, а отношение к результату: это тот, кому результат нужен и кто утром отвечает за слово «принято». Чаще всего это тот же человек, что ночью любовался работой. Человек тот же — меняется вопрос, который он задаёт прогону: ночью — на что способен ИИ-агент, утром — что было в задаче, которую он сам отдал.
Тридцать один час основателя можно прочитать дважды. Первое прочтение — как достижение: всё это время ИИ-агент не выпускал из рук одну задачу, провёл её через двести четырнадцать файлов, выстроил собственный конечный автомат подписки, дошёл до модуля авторизации и не бросил работу на полпути. Второе — как мера задачи. Конечный автомат — это ответ на вопрос, какие у подписки бывают состояния. Правки в модуле авторизации — ответ на вопрос, где кончается биллинг. Даже сам конец прогона — ответ на вопрос, что считать готовым. Во фразе «переведи биллинг на подписки, сделай нормально» этих ответов не было. Их дала ночь. Факты в обоих прочтениях совпадают: ни один не прибавлен и не убран; меняется только то, к чему их относят, — к инструменту или к задаче. Каждый час, который первое прочтение записывает ИИ-агенту в заслугу, второе записывает задаче — как ещё одно нерешённое в ней.









