Через тернии к агентам

   
 

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

Грустная история или как потратить все токены впустую

Когда случился прорыв в агентах (стало понятно, что они теперь не просто могут, а могут эффективно), я начал гонять их и в бровь и в гриву. Взял OpenCode (в основном потому, что начинал с бесплатных всяких моделек, но потом понравилось), и начал гонять агентскую разработку на петпроектах. И через какое-то время наткнулся на Oh-My-Opencode, который обещал за счет специализированных агентов (Сизифус и компания), решать задачи эффективно. И первые заходы были многообещающими, ultrawork выглядит классно, гораздо лучше чем предыдущие мои попытки workflow через plan + build. Однако радость была недолгой.

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

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

Да я знаю, “допустил компактификацию - проиграл”, особенно на тогдашних модельках с коротким контекстом (это сейчас 1М никого не убедишь, а тогда доступный контекст был сильно меньше). Но проблема была в том, что агент как будто забывал договоренности, достигнутые ранее, как будто ее и не было.

А её и не было. Точнее, она была — но жила только в нашем с ним разговоре, в прокрутке чата, которая к тому моменту уже не помещалась в окно. Договорённости нигде не были зафиксированы, поэтому они попросту не пережили длину задачи. Контекст протух, а вместе с ним растворились и все решения, которые я по наивности считал принятыми.

И вот тут я поймал важную мысль. То, что мы называем «агентской разработкой», на бо́льшую часть состоит вовсе не из агента. Агент — это ядро, языковая модель, и она в этой истории ни в чём не виновата. Виноват конвейер вокруг неё, которого попросту не было (или в случае с oh-my-opencode был, но недокрученный). Провал случился не потому, что модель «слабовата», а потому, что не было контекста — зафиксированных договорённостей, переживающих время, — и не было проверки — момента, где кто-то сверил бы сделанное с тем, о чём договаривались.

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

flowchart TD K["Контекст (спеки, SDD, AGENTS.md)"] --> P["Конвейер агентской разработки"] C["Цикл проверки (actor-critic)"] --> P P --> R["Результат нужного качества"]

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

Харнес, вокруг которого зря ломают копья

Харнес — это та обвязка, через которую вы разговариваете с моделью. Я работаю с двумя: OpenCode (отличная штука для китайских моделей) и ClaudeCode (отличная штука, если вы работаете с моделями Anthropic). Про остальные ничего сказать не могу, потому что не щупал, а гнать напраслину не хочу.

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

Отсюда контринтуитивный вывод. Выбор между OpenCode и ClaudeCode (или еще каким) слабо влияет на конечный результат. А значит, привычный спор «каким харнесом писать с агентами» — это не тот спор. Он греет душу, но приближает нас к цели. Настоящий вопрос не «каким харнесом, а «какой процесс»: что вы кладёте на вход и как проверяете выход. Именно туда я и предлагаю перенести фокус, и весь остаток статьи — про процесс, а не про инструмент.

Одна ремарка, чтобы закрыть тему обвязки и больше к ней не возвращаться. MCP и скиллы — это конкретные умения, которыми вы доукомплектовываете агента: сходить в такую-то систему, уметь такую-то операцию. Именно так с ними и надо работать. Но оркестрацию — то, кто кого запускает, кто кого проверяет, как устроен поток работы — на них строить не стоит (тут я говорю не строго, потому что, например, команды ClaudeCode которые осуществляют оркестрацию - это тоже своего рода скиллы, но скиллы особенные). Оркестрацию лучше строить через агентов. Умения — это про «что агент может», оркестрация — про «как агенты взаимодействуют», и путать эти уровни не надо. На этом про инструменты всё, идем к нашим китам.

Кит первый: контекст

Начнём с фундамента, потому что без него не стоит и всё остальное.

Самый первый и совершенно необходимый слой контекста — это статические файлы, вроде AGENTS.md или CLAUDE.md. Туда кладут стек, команды сборки и тестов, конвенции проекта, стиль именования, устройство репозитория — всё то, что верно для проекта в целом и меняется медленно. Это тот минимум, без которого агент будет заново открывать Америку в каждой сессии: искать, как тут запускаются тесты, гадать, каким образом у вас принято раскладывать файлы. Такой файл нужен, он работает, и с него надо начинать, и начинать обязательно! Тут я никаких откровений не обещаю.

Откровение начинается с вопроса: а чего в этих файлах принципиально нет?

А нет в них того, что привязано к конкретной задаче. Assumptions, из которых вы исходите именно сейчас. Требования, которые вам поставили под эту фичу. Вводные, которые вы получили от продакта в устном разговоре. И, что важнее всего, причины принятых решений — почему выбран этот подход, а не соседний, от чего вы сознательно отказались и почему. Всё это в AGENTS.md не положишь: оно живёт не проектом, а задачей, и через неделю сменится другим. По природе своей это знание задачное, а не проектное.

И где же оно тогда живёт? В голове разработчика! В лучшем случае — обрывками в тикетах вашего трекера задач. А значит, чаще всего оно попросту забывается — тут никакой магии нет. Именно эту боль я и описал в прологе: договорённости, которые нигде не зафиксированы, не переживают длину задачи. Они испаряются из окна контекста агента ровно так же, как испаряются из памяти человека через месяц после релиза. Мы потеряли их не из-за слабости модели, а потому что не было места, где они бы хранились в форме, пригодной и для агента, и для нас.

И такое место на самом деле существует и называется спекой.

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

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

flowchart TD O["Оркестратор (чистый контекст)"] O -->|делегирует черновую работу| S["Субагент (засоряет свой контекст: файлы, варианты, поиск)"] S -->|"отдаёт дистиллят (ответ, факт, вывод)"| O S -.->|замусоренное окно выбрасывается| X["выброшено"]

Оркестратор делегирует черновую работу субагенту. Субагент засоряет собственное окно, но возвращает только дистиллят — контекст оркестратора при этом не страдает.

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

Смотрите, как это устроено. У оркестратора — главного агента, ведущего задачу, — контекст должен оставаться чистым: в нём только суть, только нить рассуждения, только договорённости. А вся черновая работа — перелопатить двадцать файлов, чтобы понять, где объявлена функция; прочитать половину модуля, чтобы восстановить логику; перебрать варианты и отбросить неудачные — отдаётся субагенту. Субагент засоряет своё окно этой чёрной работой до отказа, а наверх отдаёт дистиллят: короткий ответ, вывод, найденный факт. Его замусоренное окно после этого можно выбросить. А контекст оркестратора при этом не пострадал ни на строчку — он получил чистый результат, не увидев грязи, в которой этот результат добывался.

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

Я использую слова “грязь” и “дистиллят”, но не стоит понимать их буквально. “Дистиллят” может быть “грязью” другого уровня, это просто удачные на мой взгляд метафоры.

Теперь сделаем следующий шаг и превратим спеку из документа в метод. Пока спека просто лежит рядом и её при желании можно почитать — это документ, не более. Но её можно поставить иначе: сделать спеку входом реализации. Не сопровождением постфактум, не отпиской «для галочки», а тем артефактом, который ведёт работу агента с самого начала и до конца. Агент не пишет код, а потом описывает, что написал; агент читает спеку и по ней реализует. Вот в этом развороте и состоит разница между спекой как документом и SDD — spec-driven development — как практикой. В первом случае спека описывает прошлое, во втором — направляет будущее. Практика начинается там, где артефакт из свидетеля превращается в руководителя.

И тут я обязан быть честным, иначе получается слишком глянцево. Спека — не панацея, и сама она бывает замусорена. Стоит поручить черновик спеки агенту, и он с энтузиазмом впишет туда требования, которых никто не заявлял: додумает граничные случаи, притащит фичи из соседних проектов, нафантазирует ограничения из воздуха — знакомо, не правда ли? Спека, раздутая домыслами, — это отдельная боль, и она хуже отсутствия спеки, потому что домысел, записанный уверенным тоном, выглядит как требование. Кто-то должен это отфильтровать: отделить заявленное от придуманного. Либо человек глазами, либо — что интереснее — отдельный actor-critic цикл дистилляции, где один агент пишет, а другой вычищает.

Но это уже не про контекст. Это про цикл. И это ровно тот мост, по которому мы переходим ко второму киту.

Кит второй: актор-критик цикл

Если первый кит отвечает на вопрос «что кладём на вход», то вторая — на вопрос «как проверяем выход». И начать я хочу не с механики, а с того, как вообще устроены определения агентов, потому что именно здесь прячется главная аналогия статьи.

Хорошая архитектура ПО стоит на нескольких принципах, и оказывается, что определения агентов подчиняются ровно тем же. Я вижу тут три аспекта.

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

Второй — разделение ролей. Один агент — одна ответственность. Это буквально SRP, только для агентов. Агент, который и пишет спеку, и реализует код, и сам себя проверяет, — это тот же божественный класс, что и в плохом коде: он делает всё и не делает ничего хорошо. Гораздо честнее развести роли: этот пишет, тот проверяет, третий дистиллирует. Тот кто хоть раз был руководителем увидит тут формирование правильной оргштатной структуры.

Третий — ограничение прав и инструментов. И это не мера безопасности, а часть дизайна. Критики — read-only: они читают и выносят вердикт, но ничего не меняют. Исполнители — пишут. Разведение прав по ролям — это то, что делает роли настоящими, а не номинальными. Критик, у которого есть право дописать код, слишком легко поддастся соблазну «сам исправлю» вместо «отправлю на доработку» — и перестанет быть критиком. Из этих трёх аспектов, кстати, прямо растут мотивы для критиков: и боль спеки с домыслами, и боль недостоверного отчёта лечатся именно наличием отдельной проверяющей роли с правильно урезанными правами (и может быть даже другой моделью).

flowchart LR OR["Оркестратор"] OR -->|назначает| A["Актор (исполнитель)"] OR -->|назначает| CR["Критик(и) (read-only)"] A -->|производит артефакт| ART["Артефакт (код, спека, текст, промпт!)"] ART -->|читает реальный результат| CR CR -->|замечания / апрув| OR

Механика actor-critic: актор производит артефакт, критик читает реальный результат (а не отчёт актора), замечания возвращаются актору. Цикл продолжается до апрува.

Теперь сама механика. Она называется actor-critic, и я беру этот термин как метафору, без всякой математики из обучения с подкреплением. Устроено всё так: оркестратор берёт задачу, назначает одного агента актором — исполнителем, — а других агентов критиками — проверяющими. Актор делает работу и рапортует: готово. И вот тут ключевой момент, ради которого всё и затевалось: критик не доверяет отчёту актора. Совсем. Отчёт «сделано» — это не доказательство, а заявка. Критик идёт и читает реальный результат — код, а не рассказ о коде. Оркестратор гоняет этот цикл: актор доработал по замечаниям, критики перечитали реальный результат, и так по кругу. Разработка живыми людьми с обязательным code-review в Pull/Merge Request’ах это именно тот же самый актор-критик цикл.

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

А теперь — кульминация. Потому что самое интересное в actor-critic не в том, что он есть, а в том, что одна и та же механика работает на трёх уровнях сразу.

flowchart TD subgraph L1["Уровень 1: спека"] A1["Актор пишет спеку"] --> C1["Критик дистиллирует: убирает домыслы"] C1 --> A1 end subgraph L2["Уровень 2: код"] A2["Актор реализует по спеке"] --> C2["Критики проверяют реальный код"] C2 --> A2 end subgraph L3["Уровень 3: агенты"] A3["Актор переписывает промпт агента"] --> C3["Критик проверяет новое определение"] C3 --> A3 end L1 --> L2 L2 --> L3 SES["Накопленные сессии (боли, систематические ошибки)"] -->|входные требования к улучшению промптов| L3

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

Подход актор-критик - фрактальный!

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

Уровень второй — код. Актор реализует по спеке, критики проверяют реальный результат: читают код, гоняют его в голове, ищут расхождение с задачей. Также часть критиков могут вообще инициировать QA-пайплайн (автотесты и тд). Классический контроль качества, только встроенный в конвейер, а не приделанный сбоку.

Уровень третий — и вот тут, надеюсь, у вас щёлкнет — сами агенты. Определения и промпты агентов, которыми вы всё это делаете, — это тоже артефакт. Такой же текст, как спека, такой же продукт, как код. А раз это артефакт, его можно прогнать через тот же цикл. Механика тут такая. Вы эксплуатируете свой набор агентов, сессий сто-двести, и по ходу собираете боли: вот здесь агент систематически ошибается, вот тут критик пропускает целый класс проблем, вот там оркестратор теряет нить. Эти накопленные боли становятся входными требованиями. Промпты агентов вы подаёте архитектору и в actor-critic цикл — ровно как обычный артефакт: актор переписывает определение, критик проверяет. И конвейер чинит сам себя, встроенным в себя же паттерном.

Вот это для меня и есть главная мысль всей статьи. Конвейер получается самоулучшающимся (читатели Элияху Гольдратта вспомнят процесс непрерывного улучшения). Он чинит не только код, который вы им пишете, но и собственный инструмент, которым пишете. Это редкое свойство: система, которая улучшает не только свой продукт, но и себя.

И заметьте, что это свойство не случайно. Механика actor-critic вообще не привязана к природе артефакта. Ей всё равно, что перед ней — спека, код или промпт агента; ей нужен артефакт, автор и проверяющий, а больше ничего. Именно поэтому она одинаково переносится на все три уровня. А раз она переносится, значит, перед нами не приём, заточенный под одну задачу, а метод. Рекурсивность (фрактальность) здесь — не фокус, а признак того, что мы имеем дело с методом, применимым к самому себе.

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

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

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

Синтез: два кита в работе

Я пришёл к этому не сразу, и честно скажу, с чего начинал. Точкой входа для меня стал obra/superpowers и, в частности, скилл brainstorming. Именно оттуда я вытащил первые паттерны мышления о том, как вообще разговаривать с агентом не как с автодополнением, а как с исполнителем, которому нужен контекст и проверка. Дальше я стал писать собственные определения агентов — под свои роли, под свой процесс. А потом пришёл к openspec с его жёсткими артефактами, где спека перестала быть вольным текстом и стала структурой со своими правилами. Путь шёл от чужих паттернов мышления через свои определения агентов к дисциплине артефактов. Каждый шаг добавлял жёсткости — и, как ни странно, вместе с жёсткостью добавлял свободы, потому что рутина уходила в конвейер.

А теперь — зачем всё это, давайте на примере. Кейс успеха я вижу в трёх гранях, и они разные.

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

Грань вторая — ретроспективная объяснимость. Накопленные спеки дают возможность через полгода ответить на вопрос «почему сделано так, а не иначе». Без артефактов этот ответ обычно утрачен — он уходит вместе с памятью участников, увольнениями, забыванием. А спека помнит: вот исходные требования, вот отвергнутые варианты, вот причина выбора. Это то знание, которое в обычной разработке испаряется первым, а здесь остаётся зафиксированным. И что более важно - лежит рядом с кодом, и не противоречит ему, потому что механизм openspec при работе поправит старые спеки если требования изменятся.

Грань третья — и она мне нравится больше всех — спеки как модель продукта. Когда я завожу новую фичу, агент опирается не на одну спеку, а на весь корпус накопленных. Он видит состав продукта целиком — из чего тот собран, какие фичи уже есть, — и заранее определяет, с какими из существующих новая фича может конфликтовать. Этот анализ совместимости люди регулярно упускают: держать в голове весь продукт мало кто может, и новая фича то и дело ломает старую в неожиданном месте. А агент, у которого перед глазами корпус спек, ловит конфликт до того, как он стал багом. И вот отсюда главное: ценность спек накопительная. Каждая спека полезна сама по себе, но корпус спек даёт качественно больше — он превращается в модель продукта, которой можно задавать вопросы.

Ну и мораль, ради которой всё писалось.

А мораль в том, что правильный конвейер агентской разработки держится на двух китах: контекст и цикл. Спеки и SDD — чтобы у агента было что положить на вход и чтобы договорённости пережили длину задачи. Actor-critic и его три уровня — чтобы выход был проверен, а сам инструмент со временем становился лучше. Инструмент, харнес, конкретный набор MCP — всё это безусловно важно, но вторично; спор «каким харнесом — не тот спор. А работа инженера при этом никуда не девается — она смещается. Из написания строчек кода она перетекает в проектирование контекста и проектирование циклов. Мы всё меньше пишем код руками и всё больше проектируем систему, которая пишет код за нас — и, если всё сделано правильно, чинит заодно и саму себя.