В апреле мой агент смог перешагнуть золотой порог на MLE-bench в агентских соревнованиях Berkeley RDI, а когда я решил показать «тот самый код, который взял золото» — понял, что не уверен, существует ли он вообще.
Хабр, привет! Меня зовут Георгий, и в своей первой статье на площадке я решил разобраться, что же происходило на самом деле. Цифровой детектив: с чем я преодолел планку, где этот результат теперь (спойлер: нигде) и сколько смысла в этом «золоте». Это история о том, как я расследовал собственную «победу»
Про сами агентские соревнования уже хорошо написали коллеги из AI Talent Hub — пост «Агент против агента». В них агентов оценивали не по тексту ответа, а по реальным действиям — где сам бенчмарк становится агентом: зелёный агент-судья общается с твоим — фиолетовым агентом напрямую.

Соревнование, где судья тоже агент
Соревнование AgentX–AgentBeats от Berkeley RDI проходило с осени 2025 по лето 2026: осенью команды собирали агентов-оценщиков, а весной — соревновались агенты-решатели. Больше 3400 участников второй фазы и больше десятка треков: финансы, игры, исследования, computer-use, безопасность, мультиагентность и другие. Для себя я выбрал MLE-bench — бенчмарк ML-инженерии.
MLE-bench — это набор из 75 реальных Kaggle-соревнований (изначально от OpenAI), на котором проверяют, способен ли агент пройти весь путь ML-инженера сам: прочитать данные, собрать признаки, обучить модели и отдать валидный submission.csv. Пороги золота, серебра и бронзы берутся из перцентилей оригинальной Kaggle-доски.
Главный твист формата: бенчмарк — сам агент. Зелёный (green) агент-оценщик выдаёт задачу фиолетовому (purple) агенту-решателю по протоколу A2A, тот её решает, Зелёный считает результат. Человека в цикле нет.
Green-агент (MLE-bench) │ A2A: tar.gz — данные и условие задачи ▼Purple-агент: LLM-петля, до 30 итераций модель пишет код → run_python → вывод → назад в контекст → (повтор) … пока в рабочей папке не появится submission.csv │ A2A: submission.csv (base64) ▼Green-агент: метрика по скрытому тесту → сверка с порогами
На MLE-bench Зелёный судит механически: прогоняет метрику соревнования по скрытому тесту и сверяет с порогами. Не как в разговорных треках вроде τ²-bench — там зелёный агент сам ведёт диалог, спорит и решает, дотащил ли ты задачу. «Агенты судят агентов» здесь означает «агент ставит задачу и сводит счёт», а не «кто ему больше понравился».
Задача мне выпала классическая: Spaceship Titanic — бинарная классификация, ~8700 строк train и ~4300 test, метрика — accuracy. Золотой стандарт всех курсов по Data Science и соревнованиях Kaggle.
Улика: как Зелёный передаёт задачу Фиолетовому
Мой purple-агент — сервис на FastAPI, который говорит на A2A. Зелёный присылает соревнование архивом tar.gz в частях A2A-сообщения (base64). Агент архив распаковывает, обрабатывает и возвращает submission.csv как артефакт. Распаковка идёт во временную директорию:
# executor.py: достаём соревнование из частей A2A-сообщенияdef _extract_input(self, message: Message) -> tuple[str, str]: workdir = tempfile.mkdtemp(prefix="mle_agent_") # эфемерная папка instructions = "" for part in message.parts: p = part.root if hasattr(part, "root") else part if isinstance(p, TextPart): instructions += p.text + "\n" elif isinstance(p, FilePart) and p.file.bytes: raw = base64.b64decode(p.file.bytes) archive_path = os.path.join(workdir, "competition.tar.gz") with open(archive_path, "wb") as f: f.write(raw) with tarfile.open(archive_path, "r:gz") as tar: tar.extractall(workdir) return workdir, instructions # сюда же ляжет submission.csv
Фрагмент сокращён (опущены ветка с URI и логи); целиком — в репозитории.
Для сервиса это нормально: пришла задача, развернули в /tmp, отработали, отдали ответ. Executor принимает задачу асинхронно, гоняет решатель в пуле потоков и стримит Зелёному события прогресса (TaskStatusUpdateEvent) — полноценный сервис, а не скрипт «запустил и ушёл».
Подозреваемый: LLM-петля, которая каждый раз пишет код заново
Что у purple-агента внутри? Цикл tool-use вокруг LLM. До 30 итераций, пять инструментов (list_files, read_file, inspect_csv, run_python, validate_submission) поверх живого интерпретатора, где переменные и импорты сохраняются между вызовами.
# ml_agent.py: модель сама пишет код и исполняет его через инструментыfor iteration in range(MAX_ITERATIONS): # MAX_ITERATIONS = 30 response = client.chat.completions.create( model=model, messages=messages, tools=tool_schemas, tool_choice="auto", max_tokens=4096, ) msg = response.choices[0].message messages.append(msg.model_dump(exclude_none=True)) if not msg.tool_calls: # модель не зовёт инструменты if os.path.exists(submission_path): # submission.csv готов — выходим break messages.append({"role": "user", "content": "Continue. If you have not yet created submission.csv, do so now."}) continue for tc in msg.tool_calls: # run_python, inspect_csv, ... result = dispatch(tc.function.name, json.loads(tc.function.arguments)) messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
Здесь убраны retry и обрезка длинного вывода — суть в том, что модель сама пишет код и гоняет его через инструменты.
Системный промпт задаёт жёсткий план по фазам: разведка данных, фичи, обучение нескольких моделей с OOF, стекинг, сабмит, тюнинг, если CV проседает. Но как именно писать код на каждой фазе — решает модель. На каждом прогоне заново.
Раз код пишется с нуля каждый раз, агент по своей природе стохастичен. За восемь прогонов скоры гуляли от 0.802 до 0.821 — с золотом, серебром и бронзой среди них.
Стабильный ли это решатель, или мне один раз повезло?
Здесь ещё наблюдение про модель, которое стоило мне нескольких бессонных прогонов. Лучше всех end-to-end вела себя средняя по размеру reasoning-модель, Gemini 2.5 Pro. Модели крупнее, которые я пробовал, работали хуже: переусложняли, уходили от плана, опускали очевидные указания, застревали в петле. Тонкий харнесс вокруг подходящей модели обыграл большую модель в тяжёлой обвязке (Opus 4.6 нещадно переизобретал Titanic). Контринтуитивно, но так вышло.
Расследование: за чем скрывалась победа?
Вознамерившись написать статью, я задал себе наивный вопрос: «А где, собственно, код, который взял золото?»
В репозитории лежит чистый детерминированный решатель solve_spaceship.py, который был создан для демонстрации всей связки агента. Но апрельское золото взял не он, а LLM-петля. А где взять сам золотой submission.csv? В записи прогона стоит submission_path: /tmp/tmpf205ekw1.csv. Тот самый временный файл из раздела про A2A: он жил в эфемерной директории, никогда не сохранялся и давно стёрт. Восстановить нельзя.
Но кое-что уцелело. Перебираю восемь прогонов по дайджесту Docker-образа:
|
# |
Скор |
Медаль |
Билд |
|---|---|---|---|
|
1 |
0.81609 |
серебро |
|
|
2 |
0.82069 |
золото |
|
|
3 |
0.80460 |
— |
|
|
4 |
0.80230 |
— |
|
|
5 |
0.81149 |
бронза |
|
|
6 |
0.80345 |
— |
|
|
7 |
0.80460 |
— |
|
|
8 |
0.80230 |
— |
|
Пороги лидерборда: золото 0.82066, серебро 0.81388, бронза 0.80967. Золото выпало рано, вторым прогоном, и за всё время правок его так и не удалось превзойти. А прогоны 7 и 8 — один и тот же билд dad0d9…, запущенный с разницей в 14 минут: 0.80460 и 0.80230. Тот же код, другой результат. Всего на восемь прогонов — семь разных билдов: я крутил модель и обвязку между запусками.
Ответ на вопрос — неуютный: золото поймано, а не сконструировано. Я восемь раз бросил кости, и один раз выпало золото.
На руках остался точный золотой Docker-образ (sha256:97d33c…), который я закрепил git-тегом gold-2026-04-13. Есть независимая запись прогона у организаторов. Иными словами, рецепт и кухня сохранились, конкретное блюдо — нет. У стохастического агента тот самый прогон не повторить: даже подняв тот же образ. Получу какой-то сабмит около 0.80–0.82, но не тот самый — «золотой».
Так что же по-настоящему значит 0.82069?
0.82069 — это оценка MLE-bench, а не место на публичной доске Kaggle. Два разных счётчика.
Для понимания масштаба: такой скор соответствует примерно топ-6% — планка золота тут не формальная. Но топ-6% — это ориентир силы скора, а не конкретное место.
Spaceship Titanic — соревнование учебное, и медалей за него не дают вообще.
Что я из этого вынес
Четыре вывода из расследования.
Модель оказалась важнее обвязки. Средняя reasoning-модель на тонком харнессе обошла крупные модели в тяжёлой обвязке. Если выбираете между «модель побольше» и «петля почище» — у меня выиграла вторая.
Робастность бьёт пиковую гениальность. В конкурсе, где оценивает другой агент и нет человека, который «дожмёт руками», агент, стабильно доводящий дело до валидного submission.csv, обыгрывает более умного, но хрупкого. Петля заточена доводить сабмит до конца: если модель замолчала, а файла нет — подталкиваю её «создай submission.csv сейчас»; если к концу итераций файла так и нет — агент падает с ошибкой, а не отдаёт мусор. Разброс восьми прогонов как раз об этом: каждый возвращал что-то валидное.
Для известных задач нужен детерминизм. Поэтому после соревнования я и написал solve_spaceship.py плюс быстрый путь по сигнатуре колонок (Transported + Cabin + RoomService): для знакомой задачи результат не должен зависеть от настроения LLM. Импровизация хороша на незнакомом, на знакомом это лишний риск.
Провенанс — это гигиена, а не паранойя. Сохраняйте submission.csv. Пиньте образ по sha256. Тегайте билд. Один tempfile.mkdtemp стоил мне невосстановимого золотого сабмита.
А в награду дочитавшим — одна фича из решателя, чтобы было видно: за стохастикой стоит доменная логика, а не слепой автоген. Пассажир в криосне заперт в каюте, тратить не может физически — траты и CryoSleep жёстко связаны, и это импутируется в обе стороны:
# solve_spaceship.py: в крио трат быть не может — заполняем пропуски нулямиcryo_mask = combined["CryoSleep"] == Truefor col in spend_cols: combined.loc[cryo_mask, col] = combined.loc[cryo_mask, col].fillna(0)# и обратно: все траты по нулям, а CryoSleep пуст — значит пассажир спалno_spend_mask = (combined[spend_cols].fillna(0).sum(axis=1) == 0) & combined["CryoSleep"].isna()combined.loc[no_spend_mask, "CryoSleep"] = True
Такие фичи и тащат accuracy, а не перебор гиперпараметров.
Что осталось людям
Хотите потрогать руками — я выложил обучающий Kaggle-ноутбук: пошаговый разбор решателя, от EDA до стекинга трёх GBDT. Весь код агента — в репозитории dmagog/mle-purple-agent: SOLUTION.md, README, тег gold-2026-04-13.
Спасибо коллегам за их пост: они описали BitGN PAC1 и AgentBeats со стороны поведения агентов, я — про MLE-bench и провенанс. Вместе складывается более полная картина агентских соревнований сезона.
Автономный агент прошёл золотой порог end-to-end — это интересно само по себе. Но цена этого факта — признать стохастику, не путать площадки и сохранить хотя бы образ.
Расскажите в комментариях, как вы храните провенанс прогонов своих агентов.
ссылка на оригинал статьи https://habr.com/ru/articles/1050562/