gorban.dev
Лимассол --:--RU / ENвсе посты

Я запретил своему агенту писать код по памяти

Год обвязки вокруг ИИ-агента: лестница источников вместо памяти, два ревьюера, гейт на «готово» и опенсорсный gor-mobile.

Несколько лет назад я доверял паре «гуру» из интернета. Потом всё-таки полез в официальную документацию и обнаружил неприятное: человек, которому я верил, продавал за деньги полную чушь.

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

С тех пор правило простое: не верь на слово — проверяй первоисточник.

И вот спустя несколько лет я поймал на том же самом ИИ-агента 🤷

Он тоже пишет очень уверенно. Только обучение у него закончилось когда-то в прошлом, а Compose, Media3, Navigation и Room успели уехать дальше. В итоге агент выдаёт сигнатуру метода, которой уже нет. Или которой вообще никогда не было. И тут же объясняет, почему именно так правильно.

Знакомая схема, только теперь «гуру» живёт в терминале.

Последний год я собирал вокруг агента обвязку, которая не даёт ему рассуждать об Android API по памяти. Часть вынес в опенсорс, часть пока живёт только у меня. Ниже покажу весь процесс: от поиска документации до финального ревью на устройстве.

Главное правило

Формулировка специально грубая: писать код по памяти запрещено.

Не «посмотри документацию, если застрял». Не «желательно проверить актуальную версию». Агент должен открыть источник до того, как начнёт предлагать решение.

У источников есть порядок.

Сначала официальная документация. У Google теперь есть Android CLI: агент вызывает android docs search, затем android docs fetch и получает живую страницу с developer.android.com. Для Firebase, Maps, Play и других продуктов Google подключён их отдельный Developer Knowledge MCP.

Если документация отстаёт от версии библиотеки в проекте, начинается второй уровень. Агент идёт в Gradle cache, распаковывает артефакт и смотрит JVM-представление API через javap. Для Kotlin-специфики вроде suspend, extension-функций и default arguments этого не всегда хватает — тогда нужны metadata или исходники.

А иногда сигнатура правильная, но непонятно поведение. В одном из таких случаев документация обещала, что компонент «управляет соотношением сторон», а внутри я нашёл fillMaxSize().wrapContentSize() — и никакого заданного соотношения. Тут уже помогли только исходники конкретной версии.

Получается довольно простая лестница:

  1. официальная документация;
  2. артефакт той версии, которая подключена в проекте;
  3. исходный код.

И агент обязан написать, до какой ступени дошёл и на что опирался. Фразы «обычно это делается так» больше не принимаются.

Сначала пришлось дать агенту глаза

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

Поэтому обвязка собиралась не по красивому плану, а по боли. Что очередной раз сломалось — туда и появлялся новый инструмент.

Устройство и экран

Через Android CLI агент собирает проект, ставит его на устройство, запускает приложение и снимает скриншот. Может прочитать layout tree или попросить Android Studio отрендерить Compose Preview.

Физический телефон не обязателен. Если устройства нет, агент поднимает эмулятор сам.

Со скриншотами история ещё интереснее. Агент размечает элементы на изображении, превращает метки обратно в координаты, нажимает нужную кнопку и смотрит, что изменилось. То есть проверка «кнопка открывает правильный экран» перестаёт быть текстовым предположением.

Figma

Figma подключена напрямую.

Раньше я присылал агенту скриншот и потом ещё десять минут словами объяснял, что дизайнер имел в виду. Сейчас агент открывает макет сам. И это неожиданно сильно улучшило результат: мой пересказ оказался самым дырявым интерфейсом во всей системе 😅

Браузер

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

Код

Для Kotlin и Swift подключены языковые серверы. Они дают определения и usages, а не приблизительный поиск по похожим строкам.

Плюс стоит ast-index: утилита на Rust, которая складывает индекс проекта в SQLite и умеет искать символы, вызовы, реализации и строить call tree. В бенчмарках автора поиск получился в 17–69 раз быстрее grep. Я не воспринимаю эту цифру как универсальную, но на большом проекте разница действительно заметна: вместо простыни файлов агент получает несколько нужных символов.

Чтобы агент не ленился, есть хук. Если репозиторий проиндексирован, голый grep по идентификатору блокируется. Хочешь найти класс — иди в индекс.

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

Всё остальное

Сборка iOS у меня подключена отдельным MCP. Трекер тоже: задачи, комментарии, ворклоги, спринты, доски. Для коммитов, релизов и release notes есть отдельные команды и скилы.

Google ещё публикует доменные скилы под конкретные Android-области. Например, скил по Navigation 3 за последнюю неделю у меня сработал 11 раз.

Я специально не зашиваю их каталог в gor-mobile: Google обновляет его независимо от меня. Есть команда, которая показывает актуальный список и позволяет поставить то, что нужно сейчас.

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

Инструментов стало много. Процесса всё ещё не было

В какой-то момент агент уже видел документацию, устройство, Figma и код. Но работал всё равно как придётся: сегодня сначала написал половину фичи, завтра решил подумать, послезавтра объявил всё готовым, не запуская сборку.

Так появился gor-mobile.

Это уже опенсорсный инструмент под MIT. Он устанавливает поверх Claude Code и Codex CLI мой процесс мобильной разработки: brainstorm → plan → implement → review → verify.

За основу я взял superpowers Jesse Vincent. Важная поправка: это не проект Anthropic, хотя плагин доступен через официальный marketplace Claude Code. Я беру исходные скилы и накладываю сверху свои Android-оверлеи, правила и гейты. Оригинальный плагин в этом репозитории отключается, чтобы одинаковые скилы не жили в двух экземплярах.

Получается не форк и не пересказ. Базовый процесс остаётся от superpowers, Android-специфика и ограничения — мои.

Да, TDD я убрал

Первое, что я выкинул из базового процесса, — обязательный TDD.

Там была классическая схема RED → GREEN → REFACTOR: сначала падающий тест, потом минимальная реализация. На бумаге всё правильно. В руках агента это неожиданно превратилось в фабрику бессмысленных тестов.

Нужно проверить одну кнопку? Агент выносит её поведение в отдельный helper, потому что helper удобно покрывать тестом. Архитектура начинает обслуживать тест, а не задачу.

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

Подозреваю, за этот абзац меня отдельно будут бить в комментариях. Ничего, переживу 🙂

Brainstorm — это документ, а не разговор

Цикл начинается с brainstorm. Название немного обманчивое: мы не просто поговорили, выбрали вариант и пошли писать код.

На выходе должен появиться файл-спека с датой и темой. Пока файла нет, brainstorm не закончен.

Перед сохранением спека проходит два гейта.

Первый смотрит наружу. Каждый API внешней библиотеки, на который опирается решение, должен быть подтверждён официальной документацией со ссылкой. Причём до сравнения вариантов.

Это важно. Если сначала придумать «правильную» архитектуру по памяти, документация потом превращается в попытку её оправдать. А иногда в нужной версии уже есть готовый composable, о котором агент просто не знал.

Второй гейт смотрит внутрь проекта. Агент обязан открыть эталонные примеры кода того слоя, который собирается менять. Не перечислить имена файлов, а прочитать содержимое.

Допустим, местный data source — одна строка запроса. Тогда новая спека не должна внезапно притащить туда собственный протокол ретраев, три интерфейса и фабрику фабрик только потому, что агент видел такой код в интернете.

Brainstorm остаётся на основной модели. Здесь нужно сравнивать, сомневаться и принимать решения. Экономить на этом месте я не хочу.

План написали — стоп

Это, наверное, моя любимая часть процесса.

После спеки агент пишет план в файл. И останавливается.

Не начинает «заодно» создавать классы. Не делает первый маленький шаг, пока контекст ещё тёплый. Просто кладёт план на диск, фиксирует состояние в progress.md и чистит контекст.

При следующем запуске хук находит прогресс и понимает, где мы остановились.

Зачем так сложно? Потому что агент не должен помнить длинный разговор. Он должен помнить документ.

Разговор после compaction легко превращается в «мы вроде решили использовать другой подход». В файле всё проще: вот решение, вот ограничения, вот следующий шаг.

Думает одна модель, пишет другая

Роли разделены довольно прагматично.

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

Но подзадача не получает промпт «сделай хорошо».

В неё уходят:

  • точный шаг плана;
  • список файлов, которые разрешено менять;
  • один–три эталонных примера;
  • команда, которой результат будет проверяться.

Свободы остаётся ровно столько, сколько нужно для реализации.

В отладке Sonnet вообще работает только на чтение. Никаких «я заметил возможную проблему и сразу поправил четыре файла».

Сначала корень проблемы. Потом исправление.

Перед первой гипотезой агент ещё раз проверяет, как компонент должен работать по документации. Нормальная гипотеза выглядит так: «документация обещает X, в логах и коде вижу Z». Формулировка «мне кажется, здесь что-то не так» доказательством не считается.

Ревью идёт по ходу работы

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

После каждой задачи дифф смотрит Sonnet. За один проход он проверяет и соответствие спеке, и качество кода. Для механических изменений вроде DI, ресурсов, флагов и обычной проводки можно опуститься до Haiku: там редко нужен большой философский спор.

Ревьюер получает те же эталонные примеры слоёв, которые затронуты в диффе. Если новый код по форме расходится с принятым примером, это замечание уровня Important, а не вкусовщина.

Если примера для слоя нет, это тоже пишется прямо. Иначе агент обязательно потратит время на поиски файла, которого никогда не существовало.

Когда нужен тяжёлый ревьюер

Не все диффы одинаковые.

Изменение больше примерно 400 строк, а также безопасность, авторизация, платежи, криптография и IPC автоматически уходят к ревьюеру на основной модели. Он дороже, поэтому запускается там, где цена ошибки действительно выше.

После завершения всех задач этот же ревьюер смотрит реализацию целиком. Уже не отдельный шаг №4, а то, как все шаги состыковались между собой.

Зачем там ещё Codex

На финальном гейте можно подключить Codex как отдельный проход другой моделью.

Мне не нужен ещё один такой же ревьюер. Нужна другая модель, которая ошибается иначе. Claude может не заметить то, за что Codex зацепится сразу. В обратную сторону это тоже работает.

Поэтому Codex не заменяет основное ревью. Он запускает второй проход, а потом находки объединяются.

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

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

Да, такое правило пришлось писать явно. Агенты тоже умеют красиво отчитаться о работе, которую ещё не сделали.

«Готово» надо доказать

Самый раздражающий ответ агента выглядит примерно так:

Готово! ✅

А ниже лежит код, который никто даже не собирал.

В gor-mobile так закончить задачу нельзя. Перед финальным ответом агент должен пройти проверку: проект собран, приложение задеплоено, нужный экран открыт и просмотрен.

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

Мелочь, но именно она отделяет «код написан» от «задача работает».

Правила команды нельзя угадать

У каждой Android-команды свой набор договорённостей. Где-то ViewModel можно передавать глубже, где-то за это оторвут руки. Где-то data source — тонкая обёртка над API, где-то там живёт половина логики приложения.

Поэтому правила проекта не зашиты внутрь gor-mobile.

Они лежат в отдельном git-репозитории: manifest.json, markdown-правила и эталонные примеры по слоям. Дефолтный набор можно форкнуть, переписать под свою команду и подключить:

gor-mobile rules use <ссылка-на-репозиторий>

После этого агент сверяется не со «средним Android-проектом из интернета», а с вашим кодом.

По-моему, это единственный честный вариант. Универсальные best practices заканчиваются ровно там, где начинаются реальные договорённости команды.

Отдельные правила для Compose

С Compose я пошёл ещё дальше.

Есть книга «Jetpack Compose Internals», которая хорошо объясняет, что происходит внутри Compose. Я собрал по ней свой дайджест: девять свойств composable-функций, стабильность параметров, state hoisting, side effects, модификаторы и примеры.

Саму книгу я, разумеется, не распространяю. В репозитории лежат только мои правила и конспект.

Перед изменением @Composable агент читает этот скил. После этого заметно реже появляется творчество вроде передачи ViewModel через половину дерева компонентов.

Потом я посмотрел статистику

Claude Code показывает, сколько раз вызывался каждый скил и сколько токенов он занял. Я полез туда из любопытства и получил довольно честный отчёт о собственной работе.

На момент этого замера наверху оказалось ровно то, ради чего я строил процесс:

  • brainstorming — 205 вызовов;
  • writing-plans — 178;
  • systematic-debugging — 162;
  • subagent-driven-development — 118;
  • requesting-code-review — 91;
  • скил Android CLI — 72.

То есть процесс действительно работает, а не лежит красивой схемой в README.

Но внизу тоже было интересно.

Скил для параллельных агентов — один вызов за два месяца. Скил про написание скилов — два.

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

Не каждый инструмент должен вызываться ежедневно. Figma нужна, когда появился новый макет. Языковой сервер особенно полезен в незнакомом модуле. Они стоят наготове, а не создают видимость бурной деятельности.

Ещё одна цифра меня порадовала: в этой версии весь набор из четырнадцати скилов занимает в постоянном контексте около 1300 токенов. Отдельный скил — от 22 до 250.

Так получается благодаря progressive disclosure. В контексте всегда находится только короткое описание «когда меня вызывать». Полное тело подгружается в момент использования.

Без этого вся конструкция съела бы окно ещё до первой строки кода.

Что можно попробовать прямо сейчас

В опенсорсе сейчас две части.

gor-mobile

Это сам процесс: четырнадцать скилов, Android-оверлеи, два ревьюера, гейты, Android CLI, ast-index, правила проекта и скил по Compose.

Работает с Claude Code и Codex CLI. На момент написания актуальная версия — 0.3.6. Статус честный: pre-release. Я активно использую инструмент на своих проектах и регулярно что-то переделываю.

Установка на macOS:

brew install gorban-dev/gor-mobile/gor-mobile
gor-mobile setup

cd ~/code/my-android-app
gor-mobile init

Можно поставить через npm:

npm install -g gor-mobile
gor-mobile setup
gor-mobile init

setup один раз готовит машину: Android CLI, правила, хуки и интеграцию с Codex. init устанавливает workflow в конкретный репозиторий Claude Code.

Автоматических коммитов нет принципиально. Все изменения остаются в рабочей копии. Ты сам смотришь git diff и решаешь, что коммитить.

gor-dev-plugins

Это отдельный marketplace с вещами, которые не относятся к процессу напрямую, но экономят время каждый день.

swagger-android превращает OpenAPI-спеку в Kotlin-модели: data classes на kotlinx.serialization, мапперы между data и domain, enum-мапперы. Всё по заданным конвенциям именования, а не как агенту сегодня приснилось.

yandex-tracker — локальный MCP-сервер с 30+ инструментами поверх API Трекера. Задачи, комментарии, ворклоги, чеклисты, спринты, доски, переходы, вложения. Рядом есть агент для стендапов и планирования спринта.

Figma, браузер, языковые серверы, iOS-сборка и Firebase в эти репозитории не входят. Это готовые чужие плагины и MCP, которые я просто подключил к своей среде.

Следующая проблема — память проекта

Сейчас агент хорошо помнит текущую задачу, потому что у него есть файлы: спека, план, progress.md, правила.

Но проект он всё ещё не помнит.

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

Хочется памяти другого уровня:

  • этот подход уже пробовали и отказались;
  • такой крэш уже был, вот причина;
  • в этой команде принято делать иначе;
  • вот решение, которое сработало в похожем модуле.

Подход подсмотрел у Hermes Agent. У них память вынесена в подключаемые провайдеры: от локального SQLite с полнотекстовым поиском до графа знаний. Перед ходом провайдер подбирает подходящий контекст, после сессии сохраняет новые выводы.

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

Хочу сделать похожую систему для gor-mobile. Пока честно: это только план, кода нет.

Ещё один пункт в списке — Firebase. MCP уже подключён, но процесс про него ничего не знает. Хочу, чтобы агент сам открыл крэш, проверил поведение в документации, а потом нашёл issue с таким же стектрейсом.

Когда-нибудь доберусь. Главное теперь не попросить агента реализовать это по памяти 😂


Если у вас есть Android-проект и вы тоже устали от несуществующих API, попробуйте gor-mobile и напишите, что сломалось.

Мне сейчас правда полезнее хороший баг-репорт, чем ещё одна звёздочка.

🔗 https://github.com/gorban-dev/gor-mobile

🔗 https://github.com/gorban-dev/gor-dev-plugins

Обсудить в канале