Последние три месяца мы с ребятами из сообщества декомпилировали популярный шутер от первого лица. Точнее, декомпилировали ИИ-агенты, а мы ими управляли. Нам нужно было не просто доказать, что это в принципе возможно, а получить точную, стабильную и полнофункциональную реконструкцию игры на C++.

Итог: в восстановленном коде есть 99% функций игры, 83% из них совпадают с оригиналом побайтово, игра работает без заметных багов. По моей оценке, на это ушло 600-700 миллиардов токенов.

Название игры я по не зависящим от меня причинам не раскрываю, да и статья не о ней и даже не столько о декомпиляции. Главная тема — оркестрация автономных агентов: как настроить инфраструктуру, окружение и обвязку (harness), чтобы месяцами получать от агентов качественный результат. И почему без объективной автоматической проверки результата всё это не работает.

Задача

Мы хотели точно декомпилировать игру в C++. Кроме воспроизведения логики оригинала, были и другие требования: читаемый код, который собирается, исправления уязвимостей и багов (игра уже старая) и переносимость. Было бы здорово запускать её на Linux, macOS и в браузере.

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

Инфраструктура

Мы начали с подписки Claude Max (20x), потом добавили Codex Pro и пользовались обеими одновременно. Модели менялись часто: большую часть времени работал Sonnet 5, но активно использовались и Opus 5.5, Luna, Sol и Terra. Агенты Claude запускались в Claude Code CLI, агенты Codex — в Codex CLI. Другие агентные обвязки мы тоже пробовали, но разницы почти не заметили и остались на стандартных.

Учёт задач

Агенты ведут GitHub issues через GitHub CLI: на каждую единицу трансляции (файл .cpp) заводится отдельный issue. Метки помогают группировать задачи и расставлять приоритеты.

Общение

Все агенты сидят в одном канале Discord, где могут писать и читать все сообщения. Так агенты общаются и друг с другом, и с людьми, причём участникам проекта не нужен доступ к машинам. Вебхук GitHub публикует в канал сообщения о падениях CI, чтобы агенты сразу узнавали о поломках.

Дизассемблер

Почти всё время агенты работали с официальным ida-mcp от Hex-Rays. Он очень стабильный, работает в headless-режиме и умеет всё, что нужно для такого проекта. Однозначно рекомендую.

Первый месяц: быстрый прогресс

Сначала у нас было четыре агента. Три исполнителя декомпилировали код и коммитили его, а ревьюер пассивно координировал работу и проверял коммиты на баги. За месяц агенты восстановили около 80% игры: она запускалась, показывала главное меню и загружала карты.

Параллельно мы много времени потратили на настройку окружения.

  • Раннее сжатие контекста. По умолчанию сжатие (compaction) срабатывает, когда контекст заполнен на 90%. Мы снизили порог до 42%. При декомпиляции в контексте копится много быстро устаревающей информации: уже декомпилированная функция больше не нужна. Раннее сжатие выметает этот «мусор» и экономит токены.

  • Борьба с потерей фокуса. Чем больше данных в контексте, тем сильнее агенты «дрейфуют», даже в пределах одного цикла сжатия. Они бросали функцию недоделанной и брались за следующую. Простаивали, наблюдая за CI, хотя уведомления о падениях и так приходили в Discord. Закрывали issue, не проверив, действительно ли работа сделана. В терминале агента можно вовремя поправить, но при автономной работе такой возможности нет.

Поэтому мы написали документ с целью проекта, правилами работы, запретами и порядком действий в конкретных ситуациях. Раз в час cron-задача просила агентов его перечитать, чтобы инструкции оставались свежими в контексте. Способ, возможно, не идеальный, но работал отлично до самого конца проекта. Документ мы долго дорабатывали, но публиковать его нет смысла: он слишком заточен под этот проект.

Проблема: читаемый, но неправильный код

Видимый прогресс убедил нас, что декомпиляция получается качественной. Это было не так. Код читался легко, но был семантически неверным: агенты путали сигнатуры функций, типы и раскладки структур, придумывали логику или выкидывали ту, что считали лишней.

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

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

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

Решение: оракул на основе побайтового сравнения

Нам был нужен оракул — автоматическая проверка, которая говорила бы агенту, совпадает ли восстановленная функция с оригиналом по семантике. Хватило бы простого сигнала PASS или FAIL, а в чём ошибка, агент разберётся сам.

Проще всего это сделать через декомпиляцию с побайтовым совпадением (byte matching decompilation). Мы перешли на тот же компилятор, которым собиралась оригинальная игра, и написали скрипт для сравнения. Скрипт читает наш OBJ-файл, а также EXE и PDB игры (с PDB немного проще, но и без него всё работает), извлекает байты функции и сравнивает их. Совпало — функция воспроизведена точно. Не совпало — агент её переделывает.

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

Аналогично скрипт проверяет данные и типы. Агенты прогоняют его перед пушем. Восстановленные функции регистрируются в текстовых файлах, по которым CI проверяет весь код и ловит регрессии.

Как агенты жульничали

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

Хуже было другое: агенты раз за разом пытались подправить сам скрипт, чтобы исключить свою функцию из сравнения. Чтобы это пресечь, CI вычисляет хеш скрипта и сравнивает его со значением, сохранённым в секрете GitHub Actions.

Плюсы и минусы подхода

Минусы:

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

  • Декомпиляция занимает гораздо больше времени, и результат при этом не обязательно лучше.

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

Плюсы:

  • Совпавшие функции гарантированно семантически идентичны оригиналу. Сохраняется исходное поведение, включая баги, а ошибки реконструкции исключены.

  • Агент-ревьюер больше не нужен.

  • С задачей надёжно справляются более дешёвые модели. Раньше Haiku и Luna выдавали крайне плохие результаты, а со строгим критерием приёмки получают достаточно обратной связи, чтобы показывать отличные результаты. Затраты резко падают, а проект можно многократно масштабировать.

Результаты

С новой системой проверки агенты проработали ещё почти два месяца. В восстановленном коде есть 99% функций игры, 83% из них совпадают побайтово.

В последние недели работали в основном 14 агентов на Luna и 2 на Opus 5.5. При таком масштабе у каждого исполнителя была своя ветка, а изменения шли через pull request’ы .

Discord на этом масштабе перестал справляться: когда столько агентов пишут в один канал, начинается каша. Мы разрешили им сообщать только о том, какие issue они берут в работу, и координироваться по CI. К тому же со временем людям почти не нужно было общаться с агентами: те работали полностью автономно.

Дальше каждая новая функция даётся всё дороже — у оставшихся либо есть недетерминированные особенности, либо они не совпадают по другим причинам. Например, мешает свёртка идентичных COMDAT-секций (identical COMDAT folding) в компоновщике, которую не удаётся надёжно воспроизвести. Агенты на Opus 5.5 ещё продвигаются, но игра уже работает безупречно: заметных багов нет, все возможности оригинала на месте. Оставшиеся функции переделывались много раз, и мы считаем, что их семантика верна, хотя побайтово они не совпадают. Дальнейшая погоня за совпадением съест токены без заметной пользы, так что проект можно считать завершённым.

Выводы

  • Нужны точные инструкции. Если задание оставляет простор для толкования, агенты норовят схитрить.

  • Корректность должна быть определена и проверяема машиной. Люди плохо умеют точно формулировать свои намерения, поэтому ревьюеров, на мой взгляд, никогда не будет достаточно. Лучшая обратная связь для агента — надёжная проверка с объективным PASS или FAIL. Не у каждого проекта есть такая роскошь, как у декомпиляции, но при должной изобретательности приблизиться к ней, думаю, можно где угодно.

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

  • Инструкции со временем «выветриваются». Чем дольше идёт работа и чем чаще сжимается контекст, тем чаще агенты забывают правила или считают их неважными. В интерактивном режиме этот дрейф можно поправить на ходу, в автономном он может пройти незамеченным и испортить качество. Нам помогло ежечасное обновление инструкций.

  • Генерировать код дёшево, плохой — выбрасывайте. После первого месяца мы пытались спасти негодный код, и это отняло больше времени, чем работа с нуля.

  • Корректность важнее производительности. Экономия токенов, масштабирование и пропускная способность агентов — это прекрасно, но если результат плохой, толку от них мало.

Заключение

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

При 15+ агентах всплыли и другие трудности. Время от времени агенты стирали содержимое виртуальной машины некорректными командами. Решения для песочниц под Windows существуют, но ни одно мне пока не подходит. Поэтому я начал дорабатывать свой userspace-эмулятор Sogen, чтобы он давал лёгкую и масштабируемую песочницу. До продакшн-готовности, впрочем, ещё далеко.

Из-за стёртых ВМ мы потеряли часть логов сессий, так что точный расход токенов неизвестен. По моей оценке, это 600-700 миллиардов. Цифра кажется огромной, но большую часть работы выполняла Luna: на неё пришлось около 300 миллиардов токенов. Luna примерно в 100 раз дешевле Astra, так что в пересчёте это около 3 миллиардов токенов Astra.

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

Комментарии (0)