Два агента, оба подходят, выбирается случайный. Так выглядит проблема маршрутизации на практике, и лечится она не в теле агента, а в одной строке описания.
Причём лечится не тем, что описание становится точнее. Тем, что в нём появляется вторая половина: чего этот агент не делает.
Описание — условие выбора
Тело агента читается после того, как он выбран. До выбора видно только описание, и именно оно решает.
Из этого следует вещь, которую я долго не понимал. Описание пишется не для человека, который просматривает список, а для механизма, который сопоставляет задачу с набором. Визитка вида «специалист по бэкенду проекта» этому механизму бесполезна: под неё подходит любая задача, где есть слово «модель».
Положительная часть описания по природе своей широкая. Она перечисляет, что агент умеет, а умеет он много. Сужать её уточнениями можно бесконечно, и это не помогает: чем длиннее перечисление умений, тем большему числу задач оно соответствует.
Три описания с отрицанием
Вот что стоит у меня сейчас, дословно.
Агент бэкенда:
Бэкенд блога на Laravel + Filament. Используй для моделей Eloquent, миграций,
политик, сервисов, артизан-команд и админ-ресурсов Filament. Все php/artisan/
composer — внутри контейнера. НЕ для публичных Blade-шаблонов (это blade-ui)
и не для прогона тестов (это qa-runner).Агент публичного интерфейса:
Публичный фронтенд блога — Blade-шаблоны, Tailwind CSS 4, Alpine.js, AOS.
Используй для вёрстки и стилей публичных страниц, светлой/тёмной темы,
адаптива и Blade-компонентов. НЕ для Filament-админки и моделей
(это backend-filament).Агент ревью:
Ревью изменений блога на Laravel/Filament на регрессии — пропуск validation/
authorization, mass-assignment, N+1 и тяжёлые запросы, побочки jobs/events/
notifications, Blade-escaping, нарушения CSP, пробелы в тестах. Используй для
проверки диффа перед коммитом/PR. Фичи не пишет — только ревью.Во всех трёх последнее предложение — отрицание. В первых двух оно ещё и называет адресата.
Почему отрицание сильнее уточнения
Уточнение положительной части добавляет признаки, по которым агент подходит. Отрицание добавляет признаки, по которым не подходит, и это принципиально другая операция.
Задача «поправь вёрстку карточки поста в списке рубрики» соответствует агенту бэкенда по словам «пост» и «рубрика». Сколько ни уточняй, что он про модели и миграции, слово «пост» никуда не денется. А строка «НЕ для публичных Blade-шаблонов» отсекает задачу целиком, потому что в ней есть слово «вёрстка».
Второй эффект — на границе двух агентов. Пара «пишет тесты» и «гоняет тесты» неразличима по положительной части: обе про тесты. У меня они разведены явно, у стратега тестов в описании стоит «прогон и починку красных делегируй qa-runner».
Третий эффект неожиданный: отрицание дисциплинирует меня самого. Чтобы написать «НЕ для этого», надо решить, для кого это. Половина моих пересечений между агентами всплыла именно в момент написания отрицательной части.
Адресат в отрицании
Отрицание без адресата работает наполовину.
«НЕ для публичных шаблонов» отсекает задачу и оставляет её без исполнителя. Дальше выбор делается из оставшихся одиннадцати описаний, и результат снова непредсказуем.
«НЕ для публичных Blade-шаблонов (это blade-ui)» отсекает и указывает. Задача уходит по адресу за один шаг.
Есть и побочная польза, про которую я узнал случайно. Ссылки между описаниями образуют карту набора: пройдя по всем «это агент X», видно, кто с кем граничит. У меня таких ссылок сейчас семь на двенадцать агентов, и все семь стоят на реальных стыках.
Проверка набора десятью задачами
Способ, которым я проверяю набор после каждой правки. Занимает минут пятнадцать.
Беру десять формулировок задач, какие реально пишу. Не выдуманных ровных, а настоящих, кривых:
1. Карточка поста в списке рубрики съезжает на узком экране
2. Добавить поле «серия» в модель поста и в форму админки
3. Проверить, не ломает ли последний дифф CSP
4. Тесты стали красными после правки сидера
5. Написать тест на превью по подписанной ссылке
6. Обновить общий модуль редактора в соседних проектах
7. Пост не появляется в RSS после публикации
8. Проверить доступность формы обратной связи с клавиатуры
9. Перед правкой понять, что вообще затрагивает смена slug рубрики
10. Перекрасить акцент рубрики «инфраструктура» в обеих темахДальше для каждой отвечаю: какой агент должен взяться. Если ответ приходит за пять секунд — строка проходит. Если я задумываюсь или называю двоих — это дефект описаний, а не задачи.
На последнем прогоне споткнулся на седьмой: пост не в ленте. Это может быть агент контента (публикация, лента, карта сайта) или агент разведки (сначала понять, где рвётся). Я оставил как есть, потому что честный ответ — «сначала разведка», и его же дала бы таблица маршрутизации.
Когда две зоны спорят
Спор за задачу — не всегда дефект. Иногда это правдивое отражение того, что задача на стыке.
Различаю так. Если два агента спорят из-за размытых описаний — правлю описания. Если спорят потому, что задача действительно затрагивает две зоны, — делю задачу, а описания не трогаю.
Пример первого случая у меня был с парой про изображения: два описания перекрывались на слове «обложка», и я свёл их в одно. Пример второго — задача про поле «серия»: она честно про модель и про админку, и режется на два шага.
Есть третий вариант, самый неприятный: спорят агент, чья зона задана каталогом, и агент, чья зона задана предметной областью. У меня это content-seo против blade-ui на задачах про вывод постов. Отрицанием тут не лечится, потому что оба правы. Развожу руками по ситуации: если задача про то, что показывается, — контент, если про то, как выглядит, — вёрстка.
Что осталось
Отрицательная часть описания — самая дешёвая правка во всём наборе. Одно предложение на агента, пишется за минуту, эффект виден сразу.
Что я так и не автоматизировал — проверку десятью задачами. Список у меня записан, прогоняю его руками, и делаю это раз в несколько месяцев, обычно после того, как задача уехала не туда. Правильнее было бы гонять его после каждой правки набора, но пока держится на памяти и раздражении.