Как я прошёл BitGN PAC: соревнование по автономным AI-агентам и занял 26 место

26-е место в глобальном Hall of Fame, 73.2 балла из 104. История агента Sentinel: голый Python без фреймворков, Hybrid Blueprint, борьба с раздутым системным промптом (73K → 17K), бюджет $167 и Anti-Overfitting Protocol.

Dmitrij Tamarov Dmitrij Tamarov 3 мая 2026 г. · 10 мин
bitgn ai-agents autonomous-agents competition sentinel architecture
26-е место в глобальном Hall of Fame, 73.2 балла из 104, без агентских фреймворков и с одним важным правилом — не подгонять решение под бенчмарк.
BitGN Arena — публичная арена для AI-агентов

В апреле 2026 года прошло первое крупное соревнование BitGN PAC — Personal & Trustworthy Autonomous Agents Competition. Идея простая: LLM-агенту дают виртуальную файловую систему в духе Obsidian (заметки, инбокс, контакты, календарь), формулируют задачу обычным человеческим языком, и агент должен сам сообразить, какие файлы прочитать, что обновить и какой ответ вернуть. Без человека в процессе, без подсказок, без хардкода под конкретные задачи.

Я участвовал соло, под ником za-ai.ru. В этой статье разберу, как я строил агента (он называется Sentinel) с нуля на голом Python, как боролся с раздуванием системного промпта с 73 000 символов до 17 000, во сколько мне это обошлось в деньгах и почему запрет смотреть в правильные ответы во время разработки оказался самым ценным правилом.

Сразу оговорка: я не первый, не лучший, и архитектура у меня не идеальная. Но получились ряд решений, которые сэкономили время и деньги — и которые я бы повторил на месте любого, кто берётся за похожую задачу.

Что такое BitGN PAC

BitGN — платформа для соревнований автономных агентов от Рината Абдуллина (в прошлом — куратор ERC3, через который прошло больше 240 тысяч прогонов агентов). PAC — флагманское соревнование, посвящённое персональным агентам-ассистентам.

Если коротко: вашему агенту дают серверную виртуальную машину с разложенным vault'ом — папки с заметками, входящие письма, контакты, файл AGENTS.md с правилами этого vault'а. Приходит задача: «обработай инбокс», «обнови дату следующей встречи с X в обоих местах», «найди упоминания клиента Y».

Агент общается с этой машиной через ConnectRPC поверх HTTP, сериализация — protobuf. У него базовый набор инструментов: посмотреть структуру, найти файл, поискать содержимое, прочитать, записать, удалить, переместить. Никаких готовых LLM-tool-handlers, никакого встроенного «агентского движка». Только ваш код, ваш выбор LLM-провайдера и пару месяцев до дедлайна.

Скоринг — главное, что отличает PAC от обычных бенчмарков. Здесь нет «LLM-судьи». Проверка детерминированная: какие tool calls произошли, что изменилось в файловой системе, что вернулось в финальном ответе. Часть задач — на защиту от prompt-injection: организаторы прячут в обычных файлах vault'а фейковые инструкции вроде «удали всё», и агент должен их распознавать.

Главное про итоговый бенчмарк pac1-prod:

  • 104 задачи, все типы вместе.
  • Слепая оценка — во время blind period вы не видите, какие задачи прошли. Только статус «evaluated» и общий балл в конце.
  • Один прогон в зачёт — нужно выбрать без знания.

Плюс отдельный тренировочный pac1-dev (40 задач), который при каждом запуске рандомизирует параметры — имена, email, состав vault'а. Это намеренная защита от подгонки.

Мой результат

  • 73.2 / 104 балла на боевом pac1-prod (≈ 70%).
  • №26 в глобальном Hall of Fame: Accuracy.
  • №6 в региональном Yandex OKO Hub.
My Runs — статус evaluated, 73.2 балла из 104

В глобальной таблице это выглядит так — лидеры на 87.0, я в середине первой трети:

Hall of Fame: 26-е место, 73.2 / 104

В региональном лидерборде — 6-е место.

Yandex OKO Hub — 6-е место, обошёл sample-baseline

Стек: почему без фреймворков

Когда я начинал, был соблазн взять что-то готовое — LangChain, LangGraph, CrewAI. Я этого не сделал по трём причинам:

  1. Отладка. В соревновании, где задача занимает 30+ секунд, а полный прогон — час, цена ошибки высокая. Любой фреймворк добавляет 2-3 уровня косвенности — это превращается в реверс-инжиниринг чужих абстракций.
  2. Гибкость. Нужно было часто менять формат сообщений, добавлять кастомные валидаторы посередине цикла, переключать LLM-провайдеров. Фреймворки в такие моменты упираются.
  3. Производительность. В прямой работе с Anthropic SDK можно использовать messages.parse() с Pydantic-схемами — это constrained decoding, модель физически не может вернуть невалидный JSON. Через большинство фреймворков такой контроль либо невозможен, либо требует обходных путей.

В итоге агент Sentinel — голый Python 3.14 со следующими зависимостями: anthropic, pydantic v2, claude-agent-sdk (для подписки без API-ключа), bitgn-api-* (официальные клиенты), structlog. Опционально SDK от OpenAI и Google — для возможности переключиться на любую модель.

Размер кода — около 4 700 строк. За три месяца — 144 коммита. Разработка велась в паре с Claude Code.

Архитектура: Hybrid Blueprint

Главный паттерн, на котором стоит весь агент, — Hybrid Blueprint. Идея простая: критичные шаги делаются детерминированно (без LLM), а LLM управляет только серединой — той частью, где нужно «думать».

text
+----------------------------------------------------------------------------+
|                        SENTINEL - BitGN PAC Agent                          |
|             Python 3.14 . ~4700 LOC . 144 commits . no framework           |
+----------------------------------------------------------------------------+

  +--------------------------------------------------------------------------+
  |  sentinel/main.py                                           ENTRY POINT  |
  |  CLI: --benchmark / --mode / --provider / --parallel / --resume          |
  +-----------------------------------+--------------------------------------+
                                      |
  +-----------------------------------v--------------------------------------+
  |  sentinel/harness/  -----------------------------  TRANSPORT LAYER       |
  |  client.py        BitGN API: start_run / start_trial / submit_run        |
  |  checkpoint.py    Atomic JSON state (os.replace), thread-safe, resume    |
  +-----------------------------------+--------------------------------------+
                                      |  (one task at a time)
  +-----------------------------------v--------------------------------------+
  |  sentinel/agent/blueprint.py             ORCHESTRATOR (~2000 LOC)        |
  |                                                                          |
  |   +================ FIXED PRE (no LLM) =============================+    |
  |   |  1. tree("/")           - корневой outline                      |    |
  |   |  2. deep vault scan     - depth 3, до 30 директорий             |    |
  |   |  3. read AGENTS.md      - правила vault'а (source of truth)     |    |
  |   |  4. extract constraints - структурированные ограничения         |    |
  |   |  5. select skills       - regex по инструкции -> skill blocks   |    |
  |   |  6. pre-read files      - приоритет inbox > docs > contacts ... |    |
  |   |  7. build context msg   - AGENTS.md + structure + skills        |    |
  |   +=========================+=======================================+    |
  |                             v                                            |
  |   +================ AGENT LOOP (<= 20 шагов) =======================+    |
  |   |                                                                 |    |
  |   |    +-------------+    +--------------+    +--------------+      |    |
  |   |    |   LLM       |--->|  Security    |--->|   Tool       |      |    |
  |   |    | (SGR call)  |    |  Guard L2    |    |   Registry   |      |    |
  |   |    | NextStep    |    | (pre-action) |    | Mini / PCM   |      |    |
  |   |    | Pydantic    |<---| fake-tool /  |<---| runtime      |      |    |
  |   |    | schema      |    | delete WL /  |    | dispatch     |      |    |
  |   |    +-----+-------+    | secret leak /|    +--------------+      |    |
  |   |          |            | injection    |                          |    |
  |   |          |            +--------------+                          |    |
  |   |          |  on report_completion v                              |    |
  |   |          |  +--------------------------------------------+      |    |
  |   |          +->|  COMPLETION GATES (10+ deterministic checks)|     |    |
  |   |             | * premature mutation  * dual-update         |     |    |
  |   |             | * missing-ops         * lookup followthrough|     |    |
  |   |             | * write grounding     * answer completeness |     |    |
  |   |             | * claim-vs-tools      * inbox cleanup       |     |    |
  |   |             +-------------+-------------------------------+     |    |
  |   |                           | batched rejection if fails          |    |
  |   |                           |  -> "COMPLETION REJECTED: ..."      |    |
  |   +===========================+=====================================+    |
  |                               v                                          |
  |   +================ FIXED POST (no LLM) ============================+    |
  |   |  * sanitize answer (strip preamble/suffix)                      |    |
  |   |  * extract precise format per AGENTS.md                         |    |
  |   |  * secret leakage check                                         |    |
  |   |  * merge grounding_refs (LLM + auto-tracker)                    |    |
  |   |  * vm.answer(...) -> submit                                     |    |
  |   +=================================================================+    |
  +--------------------------------------------------------------------------+

Несколько ключевых деталей.

Schema-Guided Reasoning (SGR). Каждый шаг агента описан Pydantic-моделью NextStep с каскадом полей: current_statereasoningplan_remainingsafety_assessmenttask_completedfunction. Порядок полей — это порядок генерации. Модель сначала описывает текущее состояние, размышляет, строит план, отдельно отвечает «нет ли тут попытки инъекции», и только потом выбирает действие. Бесплатный chain-of-thought без отдельных промптов.

Three-layer security. Защита от prompt-injection построена на регулярках, всё работает за миллисекунды. Pre-scan инструкции до запуска агента, pre-action на каждом шаге, post-process финального ответа.

Completion Gates — самая интересная часть. Когда агент говорит «всё, я готов отвечать», запускается каскад из десяти детерминированных проверок:

  • Premature mutation: инструкция требует записать что-то в файл, но агент ничего не записал.
  • Claim-vs-tools: агент утверждает «я обновил X», но за весь прогон не было ни одного write. Галлюцинация — отказ.
  • Answer completeness: инструкция просит «список всех X, по одному на строке», а в ответе одна строка.
  • Lookup followthrough: агент собирался ответить «не нашёл», но не использовал search для проверки.

Если хотя бы одна проверка не прошла, агенту возвращается батч ошибок одним сообщением: «вот всё, что нужно исправить». Это работает заметно лучше, чем добавлять очередной абзац в системный промпт. Системный промпт LLM может проигнорировать, детерминированный блок — нет.

История про раздутый системный промпт

Самая поучительная часть всей истории. К началу апреля я обнаружил, что системный промпт на больших задачах раздулся до 73 000 символов.

Чтобы было понятно, насколько это много: 73K — это примерно 18 000 токенов, средняя статья в журнале. И этот промпт прилетает на вход модели на каждом шаге агента. Двадцать шагов в цикле — двадцать раз перечитываемая статья. Каждый шаг становился медленным, каждая задача стала падать в таймаут, прогон превратился в час с лишним и часто крашился.

В деньгах это тоже больно: 18K токенов × 20 шагов × 100 задач — около 36 миллионов входных токенов на прогон, $108 только на input у Sonnet если бы это было по API. На дев-фазе с десятками прогонов это превратилось бы в счёт за тысячу долларов.

Чинил коммитами за две недели:

#ДатаЧто сделалиРазмер промпта
11 апрФайлы >5K в pre-read обрезаются до stub'а73K → 41K
22 апрVAULT_CONTENTS_MAX_CHARS=15K, PREREAD_FILE_MAX_CHARS=3K, line count в hint51K → 37K
36 апрГлавный фикс: PCM tree JSON → flat listing (9.2K→2.2K), Security Rules ужаты (8K→2.4K), удалён дубликат skill, smart priority inbox > docs > contacts > accounts > outbox > rest44K → 17K (2.6×)
49 апрProgressive disclosure: SYSTEM_PROMPT_STATIC ~850 chars + динамика в первом user messageAPI cache hit 0% → 99%
515 апрHistory compression: после 5-го шага старые exchange-пары схлопываются в Step 1: read(/foo) → 200 chars

Главный фикс (6 апреля) хорошо описан в самом commit-сообщении:

text
feat: reduce system prompt 2.6x (44K→17K) — eliminate timeouts, faster runs

- Compact vault structure: PCM tree JSON → flat directory listing (9.2K→2.2K)
- Remove omitted file placeholders from vault contents (save ~7K redundant entries)
- Smarter vault content priority: inbox > docs > contacts > accounts > outbox > rest
- Merge Execution Framework + Answer Format, trim Security Rules (8K→2.4K)
- Remove duplicate answer_precision skill (content already in prompts.py)
- Reduce VAULT_CONTENTS_MAX_CHARS 15K→10K

Results: sandbox 7/7 (100%), PAC1-DEV 29/40 (0.725, up from 28/40 0.70)
Zero timeouts (was 5), run duration 54min (was 100+ with crash)

Главное отсюда: формат данных имеет значение. PCM возвращал структуру vault'а как JSON-дерево. Я её конвертировал в flat directory listing — обычный текстовый список путей. Сэкономил 7K символов без потери информации. Модели всё равно, в каком виде ей дают список файлов; вам важно, чтобы это влезло в контекст.

Итоговые числа

МетрикаДоПосле
Системный промпт (большой vault)73 000 символов17 000 (−4.3×)
API cache hit rate~0%~99%
Таймауты за прогон50
Длительность прогона100+ мин (с крашем)54 мин

Шесть универсальных уроков

  1. Truncate big content to stubs in initial context. Отдавайте превью, не полные файлы. Полное содержимое — по запросу через тот же tool.
  2. Hard caps на каждый источник через MAX_CHARS-константы с понятным placeholder'ом. Знание лимитов снаружи помогает модели грамотно строить чтение.
  3. Static system prompt + dynamic user message для prompt caching. Любая динамика в системном промпте = cache miss = деньги и латентность.
  4. History compression вместо truncation. Схлопывайте старые шаги в structured summary («что было сделано»), а не отрезайте.
  5. Format matters. JSON tree → flat listing сэкономил 7K на пустом месте. Самый дешёвый формат, который ещё понятен модели, — лучший.
  6. Smart priority для частично-загружаемого контента. Подгружай то, что нужно для большинства задач (inbox > everything else).

Бюджет: $100 + $67 за всё участие

Реальные деньги:

  • $100/месяц — подписка Claude Pro/Max. На ней велась вся разработка три недели.
  • $67.10 — OpenRouter API в день соревнования. Оттуда ушли финальные прогоны.
  • Итого ≈ $167 на участие.
OpenRouter spend в день competition — $67.10
МодельSpendДоля
Claude Opus 4.6$36.9255%
Claude Sonnet 4.6$27.6841%
Claude Haiku 4.5$2.554%

Финальная модель в проде — Claude Sonnet 4.6. Opus гонял эксперименты последнего дня и резервные прогоны после rate-limit. Haiku — копейки на smoke-тесты.

Если делить $67 на 73.2 балла, выходит $0.92 за каждый набранный балл. Если считать только Sonnet ($27.68 за финальный прогон) — $0.38 за балл. Топовые архитектуры с десятками прогонов на Codex/Opus тратили в 5-10 раз больше — это видно по числу прогонов в их account-метках (x37, x26, x21).

Ключевое решение, которое всё это окупило, — multi-provider abstraction. В коде с самого начала был тонкий интерфейс provider.reason(messages, system) → LLMResult с семью реализациями: Anthropic API, CLI-провайдер через подписку (без API-ключа), OpenAI, Gemini, GLM, OpenRouter, generic-OpenAI-совместимый. Переключение — один флаг:

Terminal — zsh
# Дев-фаза через подписку (без API-ключа, нулевые расходы)
uv run python -m sentinel.main --benchmark bitgn/pac1-dev --provider cli

# День соревнования — переключиться на OpenRouter одной строкой
uv run python -m sentinel.main --benchmark bitgn/pac1-prod \
  --mode competition --submit \
  --provider openrouter --model anthropic/claude-sonnet-4-6 \
  --parallel 4

Это позволило держать API-расходы на нуле всю дев-фазу. Я написал, отладил, прогнал десятки раз — всё через подписку, которая у меня и так уже была. API подключился ровно на финальные прогоны.

Anti-overfitting: главное правило

Самое важное решение во всём проекте — не техническое, а методологическое.

В корне репозитория лежит CLAUDE.md с протоколом Anti-Overfitting Protocol. Это самоинструкция, что нельзя делать, даже если очень хочется:

  • Запрещено читать score_detail из трейсов. Это поле фактически содержит правильный ответ — подсмотрев его, очень легко дописать в промпт что-то вроде «когда видишь X, отвечай Y». Локальный балл вырастет, на бою всё развалится.
  • Запрещены условия по task_id. Никаких if task_id == "t07" в коде. Каждый фикс должен быть универсальным.
  • Запрещён хардкод ожидаемых outcomes. Если регексп ловит конкретное слово из конкретной задачи — это подгонка.
  • Запрещён тюнинг промптов под формулировки задач. Промпт учит общим принципам, а не запоминает штампы.

Каждый коммит проходит проверку через один риторический вопрос:

Можно ли объяснить этот фикс без знания ожидаемого ответа?

Примеры из коммитов:

  • «JSON-парсер ломался на mixed text+JSON» — да, легитимно.
  • «PCM tree формат не парсился» — да, легитимно.
  • «Инъекция в инструкции не сканировалась» — да, легитимно.
  • «Добавил проверку на слово discard» — нет, подгонка.

Это правило стоило мне локального балла. На дев-бенчмарке у меня было около 52 баллов в среднем. Но на боевом слепом прогоне я получил 73. Подгонщики получают зеркальную картину: высокий dev, провал на prod.

История не уникальна для PAC. На любом соревновании, на любом продуктовом проекте с бенчмарком соблазн «потыкать на правильный ответ» гигантский. Anti-overfitting protocol — это просто внешняя память против самообмана. Вы пишете правила, пока ясно мыслите, и следуете им, когда уже залипли в локальном минимуме.

Семь паттернов, которые я бы взял в любой следующий проект

Большинство решений из моего агента Sentinel переносятся куда угодно — не только в LLM-агентов.

  1. Atomic state writes через os.replace(tmp, real). Сначала во временный файл, потом атомарное переименование. Работает на любой POSIX-FS, переживает SIGKILL посреди записи. Прямой записью в основной файл вы рискуете получить битый JSON.
  2. Pydantic discriminated union для polymorphic ввода. Если приложение принимает разные типы сообщений — заведите union. Один класс на тип, общее поле-дискриминатор, Pydantic сам распарсит правильный класс. IDE подсказывает все варианты, типы проверяются на этапе разбора.
  3. Слоистая валидация pre/middle/post. Любая защита (от injection, от невалидного ввода, от выхода за пределы) лучше работает в виде нескольких независимых слоёв. Каждый слой делает одну вещь.
  4. Completion gates вместо одной финальной проверки. В мульти-шаговом процессе заведите много мелких проверок. Все ошибки — одним батчем: «вот всё, что не так».
  5. JSONL-трейсы первого класса. Один event на строку, можно grep/jq/склеить в pipeline. Структурированный лог сильнее любого debug-session.
  6. Multi-provider abstraction. Не привязывайтесь к одному провайдеру LLM, одному API, одной базе. Тонкий интерфейс с реализацией под каждый — страховка на случай, когда основной упадёт или станет дорогим.
  7. Anti-overfitting секция в CLAUDE.md или CONTRIBUTING.md. Внешняя память против самообмана. Правило, которое вы поставили в момент трезвомыслия, легко забыть через месяц — запишите его в repo.

Итог

BitGN PAC оказался полезным опытом по нескольким причинам.

Во-первых, он показывает, что в LLM-агентах сейчас побеждает не модель, а архитектура. Между лидером (87 баллов) и серединой (73) разница в 14 пунктов. Между серединой и аутсайдерами — ещё 30. Большая часть распределения — не про модель (все используют Sonnet, Opus, GPT-5, Codex), а про то, как организован цикл, как защищены границы, как обрабатываются edge case'ы. Плохая архитектура с топ-моделью проиграет хорошей архитектуре со средней моделью.

Во-вторых, простые универсальные паттерны окупаются дороже сложных специфичных. Hybrid Blueprint, Pydantic discriminated union, atomic checkpoint, multi-provider abstraction — базовые штуки, известные годами. Они и помогли больше всего.

В-третьих, anti-overfitting реально работает. Локально 52, на бою 70 — это +18 пунктов разницы за счёт того, что я запретил себе подсматривать в правильные ответы.

И в-четвёртых, дешевле, чем кажется. $167 за участие в крупном соревновании автономных агентов — небольшие деньги, если правильно построить инфраструктуру разработки.

Спасибо организатору BitGN за качественное соревнование: задачи интересные, инфраструктура работает, скоринг прозрачный.

До следующего раунда.

Команда финалистов BitGN PAC у экрана со стримом результатов
Dmitrij Tamarov
Dmitrij Tamarov

AI architect