Заметки практика · агентские системы
Как агентская система учится на своих ошибках
Четыре месяца мы строили механизм, который переносит уроки из одной сессии ИИ-агентов в другую. Здесь его устройство, восемь решений, которые не сработали, и то, что держит сейчас. Числа взяты из замеров по логам сессий и из истории репозитория.
Задача: знание умирает вместе с сессией
Система, о которой речь, — набор ИИ-агентов вокруг одного человека-владельца. Главный агент (дальше исполнитель) делает работу в сессии: пишет код, тексты, гоняет проверки. Агент-менеджер принимает работу перед сдачей по критериям владельца, а профильные ревьюеры смотрят код, интерфейс и прозу. Проектов полтора десятка, сессий много, и почти все идут без человека у экрана.
Аудит 145 сессий: менеджер находил ошибку и записывал замечание «на будущее», владелец поправлял агента — а через сессию та же ошибка повторялась. Замечание и поправка жили ровно до конца сессии. У агента нет памяти между запусками, кроме того, что он прочитает с диска.
Учить агентов «думать лучше» мы не пытались — только замкнуть перенос урока: провал, замеченный сегодня, должен менять поведение завтра в другой сессии, а иногда и у другого агента. Владелец поставил к механизму шесть условий:
- учится вся система, включая менеджера и узких агентов, а не только исполнитель;
- обучение только улучшает, деградация заблокирована;
- ничего, кроме того, что уже есть: файлы, промпты, вызовы модели;
- контекст не раздувается, в работу идёт только релевантное;
- механизм переносится на другие агентские рантаймы;
- система сама убирает устаревшее и неверное.
Условия совпали с тем, что уже описано в литературе по памяти агентов. ExpeL склеивает все уроки в каждый промпт и плохо масштабируется, A-MEM правит память равномерно и теряет контроль над расходом токенов, у «отравления памяти» есть свои бенчмарки и фреймворки управления. Мы взяли из этого три вещи: уроки хранятся как эвристики, а не как логи; запись проходит через гейт; уборка идёт отдельным проходом вне горячего пути.
Что такое урок
Урок — это markdown-файл в папке LESSONS/ репозитория проекта, под git. Короткий фронтматтер и тело с правилом. Упрощённый пример:
---
id: L-0123
addressee: all-agents # кому адресован
trigger: [evals, re-test, model-swap] # по каким ключам всплывает
scope: "правка промпта агента, смена модели" # класс задач, где провал случается
target: "правка промпта принята без повторного прогона тестов на той версии, что уйдёт в работу"
status: active # candidate | active | retired
evidence:
hits: 10 # сколько раз провал повторился
helped: true # подтверждено ли, что правило помогает
---
Правило: после правки тела агента или смены модели прогоняй сценарии
на той версии, что уйдёт в работу, со старым телом как контролем.
Почему: ...
Журнал повторов: одна строка на случай — дата, где, что, кто поймал.
Пример иллюстративный, поля настоящие. Каждое поле закрывает одно из условий владельца. Поле target (мишень) называет воспроизводимый провал, который урок чинит: без мишени урока нет. Благодаря полям trigger и addressee урок всплывает только там, где он нужен. По status и evidence видно, насколько урок заслужил доверие. Каждое повторение провала, уже записанного в уроке, мы называем хитом.
Рядом лежит INDEX.md: таблица «id · адресат · триггеры · мишень» без тел правил. Его читают на каждом старте, поэтому он обязан оставаться лёгким. Ещё два служебных файла: QUARANTINE.md держит вход (наблюдения, которые ждут второго случая), RETIRED.md — выход (снятые уроки с причиной и адресом, куда ушло лекарство).
Кто что делает
Самое важное архитектурное решение: тот, кто ошибся, урок не пишет. Исполнитель своей ошибки, как правило, не видит — иначе бы её не совершил, — а как заинтересованная сторона ещё и смягчает формулировки. Поэтому сигнал, суждение и хранение разведены по разным ролям.
Схему можно прокрутить вбок.
- Человек — исток и арбитр. Его поправки («нет, не так», «я же говорил») — самый ценный сигнал. Переносить ли урок в ядро системы (общие правила проекта, промпт агента), тоже решает он.
- Исполнитель — единственный, кто видит всю сессию. Он ведёт буфер сигналов: поправки человека и системные находки менеджера, дословно. Буфер он сливает судье в двух точках, которые система и так видит: на финальном ритуале перед отчётом и когда наберётся около пяти сигналов. Привязывать слив к «концу сессии» нельзя: сессия часто просто затихает, и этот момент никто не наблюдает.
- Судья — отдельный агент, который вызывают точечно, пачкой. Он решает: это обобщаемый урок проекта, разовый случай или общий принцип для всех проектов. Пишет в
LESSONS/только он, так что за целостность базы отвечает одна точка. Он же периодически убирает базу (у нас это называется «сон») и переводит кандидатов в действующие. - Скаут — читатель без права записи. На большой базе он отбирает уроки, применимые к текущей задаче, и отдаёт правило цитатой, а не пересказом.
Как судить, целиком описано в промпте судьи. Это один файл, и имён других агентов в нём нет. Операцию ему не назначают: вызывающий передаёт сырьё (сигнал, контекст, замысел), а что с ним делать, судья выбирает сам. За счёт этого судья переносится в другой рантайм без правок — меняется только адаптер, три крючка: прочитать индекс на старте, положить поправку в буфер, слить буфер судье. Нажим «это точно урок, запиши» на решение не влияет.
Жизненный цикл урока
Схему можно прокрутить вбок.
Три правила этого цикла держат требование «только улучшает».
- Кандидат не меняет поведение. В горячий путь он не попадает. Агент видит его только по прямому запросу, с пометкой «гипотеза». Применил — исход идёт судье как улика «за» или «против».
- Лекарство должно быть механизмом. Правило проходит гейт, только если оно формулирует шаг «вместо X делай Y» или указывает на существующий артефакт: инструмент, чек-лист, тест. Призыв «будь внимательнее» гейт не пропускает. Если мишень реальна, а лекарства нет, урок получает статус «отложен» и требование построить артефакт.
- Всё лежит текстом под git. Каждая операция судьи — коммит. Деградация видна в диффе и откатывается одной командой. Это дешевле любой системы управления памятью.
Как урок доходит до работы
Урок мало записать: он должен всплыть в нужный момент и при этом не съесть контекст. Recall у нас трёхуровневый, плюс отдельный агент для больших баз.
Схему можно прокрутить вбок.
Индекс грузится безусловно: промах recall обходится дороже нескольких килобайт таблицы. Тела уроков тянутся только по совпавшему триггеру, кандидаты — только по запросу.
Скаут появился, когда базы выросли. Он читает все действующие уроки всех баз на пути от рабочей папки вверх (у подпроекта своя база, у репозитория своя). Полноту он доказывает строкой «ПРОЧИТАНО: карточек N из N», которую печатает команда постраничного чтения. Отчёт субагента без такой строки ничего не доказывает: харнесс режет длинный вывод, а обрезанный вывод выглядит полным. Ответ скаута заканчивается строкой «В ПЛАН». Исполнитель вписывает её пунктом плана, и вызов перед сдачей случается потому, что до него дошла очередь пунктов, а не потому, что кто-то вспомнил.
Как выстраивали
Коротко, по датам 2026 года.
- 4 июля. Дизайн и первая база. Её собрали не с нуля: ретроспективный проход вытащил из истории работы 49 сигналов провала, их свели в 21 урок. Благодаря ретро-сбору система стартовала с настоящей базой и не копила её месяцами.
- 6–9 июля. Судья превратился из регистратора в экзаменатора: появился гейт на лекарство и статус «отложен». Попытка дать судье чужую роль провалилась, роль откатили.
- 17–31 июля. Борьба с раздуванием: карантин вынесен из индекса, пороги уборки переведены в числа, судья сам назначает себе «сон». По живому A/B выбрана модель для судьи.
- 15 августа. Журнал повторов в телах уроков ужат до одной строки на случай.
- 24–27 сентября. Признаки уборки считает команда, раскладку индекса проверяет команда. Появился скаут. В журнал повторов добавлено поле, которое показывает, всплывал ли урок до промаха.
- 16–22 октября. Цена лекарства: у урока появились класс задач и срок жизни, отдельная команда меряет, сколько времени съедают предписанные ритуалы. Повтор теперь засчитывается только при реальном дефекте.
Что не сработало
Восемь решений, которые пришлось переделывать.
1. Судья-регистратор записывал увещевания
Урок вида «сверяйся с эталоном» в одном из проектов набрал 8 повторов. Поле «помогает ли» всё это время стояло пустым. Петля исправно фиксировала, что ломается, но не проверяла, лечит ли записанное правило. Правило-увещевание не лечило.
Гейт на лекарство: правило — конкретный шаг или указатель на артефакт, иначе «отложен». Гейт оттачивали три итерации, по провалам тестов. Сначала судья переформулировал увещевание в псевдодействие. Потом принял ссылку на чек-лист, которого не существовало. Сработала третья формулировка: артефакт засчитывается, только если он существует. В A/B старый промпт записал увещевание действующим уроком, новый вернул «отложен» с требованием построить артефакт.
2. Судье поручили чужую работу
Хотели, чтобы судья замечал хронический долг продукта: урок верный, применяется, а провал всё равно держится. Тогда нужен не «напомнить ещё раз», а большой ход по продукту. За 7 раундов тестов судья ни разу не сделал это как надо. Всё, что звучало как «нужен структурный артефакт», он честно превращал в рекомендацию исполнителю построить инструмент. Понятия «продуктовый ход» в его модели мира просто нет.
Откатили целиком. Хронический долг теперь видит агент-продюсер, который ведёт продукт: у него есть и доступ к урокам, и модель продукта. Если агент раз за разом «ошибается» одинаково, проверьте, его ли это вообще задача.
3. Раздувание: индекс, тела и журналы
Индекс, который читают на каждом старте, дорос до 67 КБ, и 55 % в нём занимал карантин. А карантин нужен только судье. Тела отдельных уроков доросли до 104–119 КБ, и от 59 до 75 % файла занимал журнал повторов — третья копия событий, которые и так лежат в git и в транскрипте сессии. Хуже всего с ценой: по транскриптам субагентов тела уроков прочитаны 1741 раз в одной базе (около 10,1 млн токенов) и 1680 раз в другой (около 18,0 млн). 92–93 % этого расхода пришлось на тела крупнее 20 КБ. Вес тела умножается на число чтений, а не платится один раз.
Карантин вынесен в отдельный файл. Журнал — одна строка на случай с провенансом (коммит, сессия, файл:строка), остальное в git. Размер тела стал счётным признаком уборки.
4. Агенты читали индекс наполовину
Замер логов за 30 дней: индекс уроков агенты чаще всего читают через head -20…60 (953 из 1786 обращений). Шапка индекса разрослась до 93 строк заметок судьи, раздел действующих уроков уехал вниз, и 37 сессий из 130 не увидели ни одного действующего урока. Правило «шапка без журнала» при этом было записано. Судья даже перечитывал индекс после каждой записи, и это не помогло.
Раздел действующих — не ниже 15-й строки, а проверяет это команда с кодом возврата после каждой записи судьи. На больших базах чтение поручили скауту, который доказывает полноту счётом.
5. Пороги-суждения не срабатывают
В промпте судьи стояли правила «прогоняй дозревших кандидатов попутно», «сливай дубли», «склонность сжимать, а не растить». На двух живых базах 32 кандидата висели без движения 10 дней и дольше, 4 урока стояли действующими без подтверждения пользы, а три урока одной базы держали 36 % всех повторов, хотя мишени у них были разные. Ни у одного правила не было числа, при котором оно обязано сработать. Вдобавок кап «не больше трёх файлов сверх операции», введённый против долгих проходов, физически не давал прополоть 32 кандидата.
Пороги заданы числом: кандидат обязан получить вердикт, если у него 3 и больше независимых повтора или 2 повтора и возраст больше 14 дней. Бюджет прохода считается в решениях вместо файлов, дубли ищутся по классу механизма, а не по тексту мишени.
6. Уборка: то слишком часто, то никогда
Сначала судья сам решал, что база «перезрела». На живой базе это почти всегда правда, и каждый вызов разрастался в полный проход на 10–15 минут. Запуск уборки переложили на вызывающего, но тот базу не видит и уборку не заказывает. Пометка «пора убирать» висела мёртвой, пока владелец не спросил, почему уборку должен заказывать он.
Отложенное самоназначение. Судья ставит маркер в шапку индекса, когда срабатывает счётный признак (кандидатов без вердикта больше десяти, один класс набрал 25 % повторов базы и т. п.), а убирает базу при следующем вызове. Проходов столько, сколько раз база реально перезрела, а не сколько раз вызвали судью. Сами признаки считает команда, не судья.
7. Сэкономили на модели там, где думать тяжело
Ставка была такая: суждение у судьи узкое, хватит средней модели. Живой A/B на базе в 65 файлов (около 1 МБ) показал другое. Узкие части (есть ли мишень, механизм ли лекарство) держались на любой конфигурации. Тяжёлым оказался дедуп: новая это мишень или грань существующего урока. Средняя модель натянула сигнал на смежный урок, крупная с малым бюджетом рассуждения — на другой. Только крупная со средним бюджетом нашла пункт карантина, порог которого был заложен ровно под этот случай.
Судья на крупной модели со средним бюджетом. Попутно выяснилось, что он ещё и быстрее: 215 с против 250 с у средней модели и 429 с у прежней конфигурации, потому что пишет в индекс меньше прозы. Скауту, наоборот, хватает средней модели с малым бюджетом: он только находит.
8. Лекарство стало дороже болезни
Через три месяца владелец сказал, что система обросла ритуалами ради ритуалов. Замеры подтвердили. Ритуал коммита, предписанный одним уроком, в одном проекте стоил 4,7–5,3 часа сессий в неделю. Проверки занимали около 35 % времени сессий. Из 1267 повторов по 36 действующим урокам той же базы гейт поймал 387, человек — 83, а заметная часть «повторов» оказалась нарушением формы самого правила при верной работе. Такой счёт подкармливает урок и не даёт ему уйти по сроку. Ещё один замер: 108 кругов возврата работы на доработку заняли около 17 % времени сессий.
У урока появилось поле scope — класс задач, где провал реально случается; по умолчанию узкий, всеобщий только для красных линий (необратимое, секреты, деньги, то, что уходит к людям). Действующий урок без нового повтора дольше 30 дней требует вердикта. Цену ритуала меряет команда по трассам сессий: часы, медиана и p90 вызова. Повтор засчитывается только при реальном дефекте: баг, неверное число, проверка, которая не способна покраснеть. Появился законный вердикт «дешевле терпеть».
Мелочей было больше. Судья однажды удалил неотслеживаемый файл вне своей папки; исправили явным списком запрещённых команд в промпте, с причиной. Блок «рекомендации исполнителю» исполнители читали как совет и переадресовывали владельцу вопросом «делать?»; помогло переименовать заголовок в приказ «сделать в этой сессии». Исполнитель получает только вывод судьи, не его промпт, поэтому контракт должен нести сам заголовок.
Что работает сейчас
Разделение ролей и единственный писатель
Сигнал порождают человек и гейт, копит исполнитель, решает судья, читает скаут. Ни одна роль не судит собственную работу, и это разделение держится с первой недели.
Карантин и «второй случай»
Одиночное наблюдение ждёт в карантине, пока тот же механизм не повторится, и до тех пор поведения не трогает. Это главная защита от отравления памяти.
Признаки считает команда, а не модель
Небольшой CLI над базой: check (раскладка индекса), stats (признаки уборки), lint (форма урока), recalled (всплывал ли урок в сессии до промаха), cost (сколько времени стоит предписанная команда). Модель судит, что делать, а всё, что можно посчитать, считает код. Там, где счёт оставляли модели, он расходился с реальностью: самописный подсчёт судьи однажды выдал 16 просроченных кандидатов при реальном нуле.
Скаут с доказательством полноты
Окно 24–27 сентября: доля сессий, где уроки реально подняли, выросла с 35 % до 89 % в главном репозитории и с 56 % до 88 % в соседнем проекте. Урок, поднятый скаутом, ловился до гейта в 6 случаях из 9, не поднятый — в 2 из 7. Это направление, не величина. Окно — четыре сессии, а в тот же час переложили индекс, так что эффект — заслуга всего пакета изменений. Перезамер запланирован на 15 и больше сессий.
Клапан «структура → снят»
Самая сильная форма лекарства — та, что вообще не зависит от памяти: инструмент, запрет, безопасный дефолт. Когда такая структура построена и держит, урок снимается с учёта, а в архиве остаётся строка со ссылкой на структуру. Клапан включили после недельного аудита: снятых уроков не было ни одного, хотя действующих прибавлялось около десяти в неделю, а 19 из 21 повтора поймал гейт, не проза урока. Сейчас снято 15 уроков. Пример: агенты по-разному решали, сидит ли человек у экрана, и ошибались, потому что флаг выставлял сам агент и забывал его снимать. Урок сняли, когда дефолт перевернули на «человек отошёл»: держать это состояние стало незачем.
Главный вывод: лестница прочности
Самое неприятное открытие проекта даёт поле, которое показывает, всплывал ли урок до промаха. Из 165 размеченных повторов в главной базе 67 случились, когда урок уже был поднят под задачу. Ещё 78 — когда урок не был отобран, у 20 сессию не установить. Агент знал правило, читал его в начале работы и всё равно ошибся. Знание правила не создаёт момента, в который его применяют.
Схему можно прокрутить вбок.
Мы много раз проходили эту лестницу на одном и том же классе ошибок. Типичный пример: агент сообщает, что задача на доске закрыта или тестов столько-то, а в реальности это не так. Правило «сверяй перед отчётом» стояло в уроке и в общих правилах проекта, и ошибка повторялась. Не помогло и то, что команда-сверка уже существовала и в той же сессии вызывалась, только позже и по другим задачам. Сработала третья ступень: строка Доска: <вывод команды> стала обязательной частью формы отчёта. Отчёт без неё виден проверяющему сразу.
Ступень выбирают по цене: команда или ритуал стоят минут в каждой сессии, где применяются, так что подниматься по лестнице стоит только там, где промах дорог. Для дешёвого промаха нормальный ответ — «дешевле терпеть».
Как повторить у себя
Минимальный набор, с которого мы бы начали заново:
- Папка уроков под git и формат с мишенью, триггерами, статусом и счётчиком повторов. Индекс отдельно от тел.
- Отдельный промпт судьи с тремя гейтами: есть ли мишень (воспроизводимый провал), механизм ли лекарство (шаг или существующий артефакт), не грань ли это уже существующего урока. Плюс карантин до второго случая.
- Три крючка в вашем рантайме: читать индекс на старте; класть поправки человека и находки ревью в буфер дословно; сливать буфер судье на финальном шаге и по порогу размера.
- Ретро-сбор на старте: пройдите историю (changelog, ревью, переписку с поправками), вытащите повторяющиеся провалы и прогоните их через судью. Собирайте и судите разными проходами.
- С первого дня пишите в журнал повторов, кто поймал ошибку и всплывал ли урок до неё. Без этого не отличить «урок не нашёлся» от «урок нашёлся и не удержал», а лечатся они по-разному.
Чего не нужно в начале: векторной базы (плоского индекса с ключевыми словами хватает на базу в несколько десятков уроков и портфель в три с лишним сотни), автоматического повышения по числовым порогам, уборки в горячем пути, постоянного агента-надзирателя на каждом шаге. Ни одна из этих вещей нам так и не понадобилась.
Пределы и открытые вопросы
- Выборки малы. Эффект скаута измерен на четырёх сессиях, и мы называем его направлением. Многие цифры статьи — разовые замеры по одной-двум базам на конкретную дату.
- Проекты переоткрывают одно и то же. Сквозной проход по 18 базам (334 урока, 28.09) дал пять семей механизмов, которые каждый проект открывает заново. Самая массовая: «проверка не различает миры, где ты прав и где неправ», она нашлась в 9 базах. Общую базу поверх проектов мы сознательно отложили. Запись, меняющая поведение сразу во всех проектах, опаснее любой проектной ошибки. Вместо неё самые частые семьи вручную переносятся в промпты агентов.
- Обмен с другими системами идёт через общий репозиторий карточек фиксированного формата. Чужой опыт там — данные, а не инструкция: агент его пересказывает и проверяет на попытки управлять читателем, а применять ли его, решает человек.
- Цена суждения. Судья работает на крупной модели и стоит заметных денег за вызов. Мы платим это сознательно, потому что дешёвая конфигурация ошибалась ровно в самом трудном месте.
- Тестировать петлю трудно. Часть провалов (например, «исполнитель спешит сдать ход и откладывает задание судьи») на синтетическом стенде не воспроизводится: свежему агенту задан чистый выбор. Такие правки обосновываем трассами реальных сессий и проверяем повторным замером.
Словарь
- Урок
- Файл с правилом против конкретного воспроизводимого провала в проекте.
- Мишень
- Провал, который урок чинит. Нет мишени — нет урока.
- Хит
- Повторение провала, уже записанного в уроке. Засчитывается только при реальном дефекте.
- Кандидат / действующий / снят
- Статусы урока: гипотеза, которая на поведение не влияет; заслуженное правило; отозванное правило со следом в архиве.
- Карантин
- Наблюдения, которые ждут второго случая, чтобы стать кандидатом.
- Исполнитель
- Главный агент сессии, который делает работу.
- Менеджер, гейт
- Агент, принимающий работу перед сдачей; точка проверки, через которую она проходит.
- Судья
- Агент, который решает, что становится уроком, и единственный пишет в базу.
- Скаут
- Агент-читатель: отбирает уроки под задачу и доказывает, что прочитал все.
- «Сон»
- Отдельный проход уборки базы: дубли, сжатие, снятие устаревшего.