26-е место в глобальном Hall of Fame, 73.2 балла из 104, без агентских фреймворков и с одним важным правилом — не подгонять решение под бенчмарк.
В апреле 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.
В глобальной таблице это выглядит так — лидеры на 87.0, я в середине первой трети:
В региональном лидерборде — 6-е место.
Стек: почему без фреймворков
Когда я начинал, был соблазн взять что-то готовое — LangChain, LangGraph, CrewAI. Я этого не сделал по трём причинам:
- Отладка. В соревновании, где задача занимает 30+ секунд, а полный прогон — час, цена ошибки высокая. Любой фреймворк добавляет 2-3 уровня косвенности — это превращается в реверс-инжиниринг чужих абстракций.
- Гибкость. Нужно было часто менять формат сообщений, добавлять кастомные валидаторы посередине цикла, переключать LLM-провайдеров. Фреймворки в такие моменты упираются.
- Производительность. В прямой работе с 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 управляет только серединой — той частью, где нужно «думать».
+----------------------------------------------------------------------------+
| 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_state → reasoning → plan_remaining → safety_assessment → task_completed → function. Порядок полей — это порядок генерации. Модель сначала описывает текущее состояние, размышляет, строит план, отдельно отвечает «нет ли тут попытки инъекции», и только потом выбирает действие. Бесплатный 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. На дев-фазе с десятками прогонов это превратилось бы в счёт за тысячу долларов.
Чинил коммитами за две недели:
| # | Дата | Что сделали | Размер промпта |
|---|---|---|---|
| 1 | 1 апр | Файлы >5K в pre-read обрезаются до stub'а | 73K → 41K |
| 2 | 2 апр | VAULT_CONTENTS_MAX_CHARS=15K, PREREAD_FILE_MAX_CHARS=3K, line count в hint | 51K → 37K |
| 3 | 6 апр | Главный фикс: PCM tree JSON → flat listing (9.2K→2.2K), Security Rules ужаты (8K→2.4K), удалён дубликат skill, smart priority inbox > docs > contacts > accounts > outbox > rest | 44K → 17K (2.6×) |
| 4 | 9 апр | Progressive disclosure: SYSTEM_PROMPT_STATIC ~850 chars + динамика в первом user message | API cache hit 0% → 99% |
| 5 | 15 апр | History compression: после 5-го шага старые exchange-пары схлопываются в Step 1: read(/foo) → 200 chars | — |
Главный фикс (6 апреля) хорошо описан в самом commit-сообщении:
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% |
| Таймауты за прогон | 5 | 0 |
| Длительность прогона | 100+ мин (с крашем) | 54 мин |
Шесть универсальных уроков
- Truncate big content to stubs in initial context. Отдавайте превью, не полные файлы. Полное содержимое — по запросу через тот же tool.
- Hard caps на каждый источник через
MAX_CHARS-константы с понятным placeholder'ом. Знание лимитов снаружи помогает модели грамотно строить чтение. - Static system prompt + dynamic user message для prompt caching. Любая динамика в системном промпте = cache miss = деньги и латентность.
- History compression вместо truncation. Схлопывайте старые шаги в structured summary («что было сделано»), а не отрезайте.
- Format matters. JSON tree → flat listing сэкономил 7K на пустом месте. Самый дешёвый формат, который ещё понятен модели, — лучший.
- Smart priority для частично-загружаемого контента. Подгружай то, что нужно для большинства задач (inbox > everything else).
Бюджет: $100 + $67 за всё участие
Реальные деньги:
- $100/месяц — подписка Claude Pro/Max. На ней велась вся разработка три недели.
- $67.10 — OpenRouter API в день соревнования. Оттуда ушли финальные прогоны.
- Итого ≈ $167 на участие.
| Модель | Spend | Доля |
|---|---|---|
| Claude Opus 4.6 | $36.92 | 55% |
| Claude Sonnet 4.6 | $27.68 | 41% |
| Claude Haiku 4.5 | $2.55 | 4% |
Финальная модель в проде — 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-совместимый. Переключение — один флаг:
# Дев-фаза через подписку (без 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-агентов.
- Atomic state writes через
os.replace(tmp, real). Сначала во временный файл, потом атомарное переименование. Работает на любой POSIX-FS, переживает SIGKILL посреди записи. Прямой записью в основной файл вы рискуете получить битый JSON. - Pydantic discriminated union для polymorphic ввода. Если приложение принимает разные типы сообщений — заведите union. Один класс на тип, общее поле-дискриминатор, Pydantic сам распарсит правильный класс. IDE подсказывает все варианты, типы проверяются на этапе разбора.
- Слоистая валидация pre/middle/post. Любая защита (от injection, от невалидного ввода, от выхода за пределы) лучше работает в виде нескольких независимых слоёв. Каждый слой делает одну вещь.
- Completion gates вместо одной финальной проверки. В мульти-шаговом процессе заведите много мелких проверок. Все ошибки — одним батчем: «вот всё, что не так».
- JSONL-трейсы первого класса. Один event на строку, можно
grep/jq/склеить в pipeline. Структурированный лог сильнее любого debug-session. - Multi-provider abstraction. Не привязывайтесь к одному провайдеру LLM, одному API, одной базе. Тонкий интерфейс с реализацией под каждый — страховка на случай, когда основной упадёт или станет дорогим.
- 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 за качественное соревнование: задачи интересные, инфраструктура работает, скоринг прозрачный.
До следующего раунда.