Один каркас, из которого собраны очень разные вещи: шахматный тренер, инженерный расчёт, помощник по учёбе
Один каркас, из которого собраны очень разные вещи: шахматный тренер, инженерный расчёт, помощник по учёбе

За последний год я собрал 33 ИИ‑агента. Шахматный тренер для ребёнка, помощник по учёбе, ассистент для жены, тьюторы образовательных программ, боты для металлургического холдинга и энергетической компании, внутренние агенты отдела продаж и CRM.

Все они работают на одном движке. Один и тот же Docker‑образ, одна и та же кодовая база. Ни один из тридцати трёх не потребовал править ядро.

Различаются они одним: где у каждого проходит граница между тем, что считает код, и тем, что говорит модель. За год я понял, что вся работа сводится примерно к этому, а не к подбору промптов или модели.

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

TL;DR

  • Агент — это том, а не форк. 33 агента = один образ + 33 разных каталога данных. Ни одной правки ядра.

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

  • Схемы инструментов начинают обгонять промпт после третьего десятка инструментов. У CRM‑админа с 331 инструментом 302 КБ JSON‑схем ≈ 75 тысяч токенов в префиксе каждого запроса.

  • Три правки конфига дали 15 266 → 1 424 токена промпта и медиану ответа 17 → 7,7 секунды.

  • Тулсет на демо‑контуре клиента не был урезан вовсе — модель собрала себе OCR‑пайплайн через терминал и доустановку пакетов. Отдельная засада: даже когда терминал отключаешь явно, execute_code остаётся — он в другом наборе.

  • Про провалы тоже будет: правило идемпотентности я написал, а баланс всё равно разошёлся на трёх источниках. 5 863 строки Python для одного агента написаны и в бою не вызваны ни разу.

Каркас: агент — это том, а не форк

В основе — Hermes Agent от NousResearch. Питон, цикл агента, шлюз, платформы, планировщик, скиллы, плагины.

Движок молодой: первый коммит датирован июлем 2025-го. Часть агентов старше него — они начинали на другом каркасе и переехали сюда по ходу дела. Переезд, кстати, оказался лучшей проверкой тезиса про том: у нескольких агентов до сих пор лежат каталоги от прошлого движка и импортированные сессии — у детского 233 из 558 помечены как перенесённые. Личность, история и накопленные данные пережили смену каркаса, потому что никогда ему и не принадлежали.

Устроено просто:

services:
  hermes-<agent>:
    image: hermes-agent:latest
    environment:
      HERMES_HOME: /opt/data
    volumes:
      - hermes-<agent>-data:/opt/data
    command: ["gateway", "run"]

Каталог /opt/hermes внутри контейнера неизменяем. Всё, что делает агента этим агентом, лежит в /opt/data:

  • SOUL⁠.md — личность,

  • config.yaml — модель, поверхности, наборы инструментов,

  • plugins/ — доменные инструменты на Python,

  • skills/ — инструкции, подгружаемые по требованию,

  • state.db — сессии, SQLite с полнотекстовым поиском,

  • cron/jobs.json — расписания.

Новый агент = скопировать каталог home/ из репозитория в новый том. Никакого форка, никакой сборки.

Один неизменяемый образ и том агента: SOUL⁠.md, config.yaml, плагины, скиллы, база сессий и расписания
Один неизменяемый образ и том агента: SOUL⁠.md, config.yaml, плагины, скиллы, база сессий и расписания

Схема 1. Всё, что делает агента этим агентом, лежит в томе. Образ один на всех, ядро не трогается вообще.

Я прошёлся программно по конфигам всех агентов. mcp.servers пуст у каждого — MCP не используется ни одним. Когда инструмент пишешь сам, плагин дешевле — но не по токенам: JSON‑схемы у MCP и у плагина одинаковые, и платишь ты именно за них. Дешевле он по эксплуатации: нет второго процесса, есть доступ к идентификатору сессии и к серверным переменным окружения. Обратная сторона — раз секреты лежат в окружении процесса, держать плагины и терминал в одном агенте нельзя. Чем это кончается, будет в разделе про энергетику.

Системный промпт собирается один раз за сессию

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

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

«Налог движка» — служебные блоки, которые движок подставляет сам. Часть безусловна, часть зависит от конфигурации:

DEFAULT_AGENT_IDENTITY              513
HERMES_AGENT_HELP_GUIDANCE          560
TASK_COMPLETION_GUIDANCE            769
PARALLEL_TOOL_CALL_GUIDANCE         618
MEMORY_GUIDANCE                   1 426
SESSION_SEARCH_GUIDANCE             186
SKILLS_GUIDANCE                     385
STEER_CHANNEL_NOTE                  681
TOOL_USE_ENFORCEMENT_GUIDANCE       824
GOOGLE_MODEL_OPERATIONAL_GUIDANCE   860
OPENAI_MODEL_EXECUTION_GUIDANCE   2 694
--------------------------------------
все блоки списком                 9 516 символов
максимум на одного агента         8 656 символов ≈ 2,2 К токенов

Одновременно все одиннадцать не платит никто. GOOGLE_MODEL_OPERATIONAL_GUIDANCE и OPENAI_MODEL_EXECUTION_GUIDANCE взаимоисключающи: движок выбирает вставку по имени модели, агент на Gemini получает первую, агент на OpenAI‑совместимом API — вторую. MEMORY_GUIDANCE уходит вместе с выключенной памятью, SKILLS_GUIDANCE — когда скиллов нет. У агента на Gemini без памяти и скиллов налог падает до 5,0 тысячи символов. Поэтому цифры ниже, где промпт ужимается до полутора тысяч токенов, не противоречат этой таблице — там выключена как раз условная часть.

Модели у агентов разные — большинство на gemini-3.6-flash, часть на deepseek-v4-pro, запасной вариант gemini-2.5-pro. Замеры ниже от модели не зависят: считаются символы промпта и байты JSON‑схем, а не ответы. В движке для этого есть встроенная измерялка hermes prompt-size — офлайн, без обращения к API:

агент

системный промпт, символов

инструментов

JSON‑схем, байт

бот конференции

10 687

3

1 995

тьютор платформы

19 765

36

55 434

корп‑агент, металлургия

21 052

36

59 121

контент‑цех

35 046

62

77 125

внутренний CRM‑админ

35 613

331

302 110

Считать надо аккуратно: слева символы русского текста, справа байты англоязычного JSON, а в UTF-8 кириллица занимает по два байта на символ. То есть реальный разрыв примерно вдвое меньше, чем кажется по таблице. Но направление всё равно видно, и зависит оно не от предмета и не от длины персоны, а от числа инструментов.

У бота с тремя инструментами схемы дают меньше десятой доли промпта — они не проблема вообще. У агентов с тремя‑четырьмя десятками уже сопоставимы с ним. А у CRM‑админа с 331 инструментом 302 КБ схем — это порядка 75 тысяч токенов, которые лежат в префиксе каждого запроса.

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

Вывод обошёлся мне дорого: тулсет — не список фич, а бюджет.

Три правки в конфиге тьютора языкового курса (узкий профиль инструментов, отключённая проба окружения, отключённый reasoning):

Метрика

Было

Стало

prompt_tokens на запрос

15 266

1 424

Медиана ответа

~17 с

7,7 с

Правки я не разделял, и честно: экономию промпта дал узкий тулсет, а ускорение почти целиком — выключенный reasoning. Это разные рычаги, и мерить надо оба. Токены режет тулсет, латентность — режим модели. При этом у соседнего агента в промпте до сих пор висит 50 стоковых навыков на 16 429 символов — «ни один не про энергетику», как я сам написал в заметке. У корпоративного агента энергетики замеренный промпт — 24 тысячи токенов на ход, и на прикладную задачу из них работает около четырёх тысяч.

Чем агенты отличаются друг от друга

Персона при одном и том же образе разлетается в пятнадцать раз:

Агент

Персона целиком

Шахматный тренер

~1,5 К токенов

Корп‑агент энергетики

~1,9 К токенов

Корп‑агент металлургии

~2,1 К токенов

Агент для дочери

~9 К токенов

Агент для жены

~12 К токенов

Наставник интенсива

~23 К токенов

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

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

Личный агент против корпоративного:

Личный

Корпоративный

Код и промпт

промпт основной, кода мало

2 100–4 600 строк движка против 80 строк персоны

Что помнит

человека (память включена, досье)

расчёт (память выключена; сегодня говорит один менеджер, завтра другой)

Что является ответом

текст модели

артефакт — лист А3, DXF, спецификация; текст лишь его пересказ

Статусы

«получилось / не получилось»

PASS / FAIL / WARN / INFO с явным «INFO ≠ PASS»

Персона — это

характер

регламент: жёсткие правила, пошаговый алгоритм, формулировки для произнесения вслух

Цена ошибки другая — и граница «модель / код» проходит гораздо левее.

Четыре агента по степени участия модели: от шахматного тренера, где код считает всё, до агента для дочери, где кода почти нет
Четыре агента по степени участия модели: от шахматного тренера, где код считает всё, до агента для дочери, где кода почти нет

Схема 2. Четыре агента из таблиц выше на одной шкале: шахматный тренер, энергетика, агент для жены, агент для дочери. Слева модель только пересказывает посчитанное, справа принимает почти все решения сама.

Шахматный тренер: модели не доверен ни один факт

Что это. Ребёнок открывает в браузере обычное веб‑приложение и играет партию против компьютера. Промптов он не пишет и вообще не знает, что внутри есть ИИ, — просто двигает фигуры мышкой. После каждого своего хода справа появляется реплика тренера: что за ход сейчас сделан, чем соперник накажет и как было бы сильнее. Каждый зевок автоматически превращается в задачу «работа над ошибками», и через день ребёнок решает ту же позицию заново. Родитель на своём экране крутит один тумблер — «мягче / как обычно / строже».

Как это выглядит. Ребёнок вывел ферзя на g4, и тот повис под конём. Вот что посчитал код — это единственное, что видит модель:

{
  "played_move": {"san": "Фg4"},
  "verdict": {
    "label": "blunder", "label_ru": "Зевок", "hybrid_basis": "material_hybrid",
    "win_before_pct": 55.0, "win_after_pct": 4.0, "delta_win_pct": 51.0,
    "eval_words_after": "позиция очень тяжёлая"
  },
  "best_move": {"san": "Фe2", "win_pct": 54.0, "line_san": ["Фe2", "d6", "d3", "Сe6"]},
  "punishment_line_san": ["Кxg4"],
  "tactics_against_student": [
    {"motif": "hanging_piece", "fact_ru": "ферзь на g4 под боем (конь с f6), никто не защищает"}
  ],
  "limits": {"max_sentences": 6, "max_motifs": 2, "eval_language": "wdl_words"}
}

А вот что из этого сделала модель — один и тот же ход в двух регистрах строгости:

Мягче: «{имя}, давай посмотрим на эту позицию, ведь до твоего хода у нас было небольшое преимущество. Казалось бы, активный выпад ферзем на g4 выглядит очень грозно и нацелен на атаку, однако этот ход уводит фигуру под удар. Наш ферзь оказался без защиты, и соперник забирает его конем Кxg4. Сильнее было просто сыграть Фe2, сохраняя отличную игровую позицию. Ты сильный игрок, просто в следующий раз перед активным выпадом обязательно проверяй, не встает ли фигура под бой соперника.»

Строже: «Стоп, {имя}, а зачем туда ферзем на g4? Позиция была примерно равная, а после этого хода стала очень тяжелая. Ты подставил фигуру под бой, ведь у соперника находится простое Кxg4, и наш ферзь потерян. Сильнее было сделать спокойный ход Фe2, сохраняя отличную игру. Ты сильный игрок, просто в этот раз потерял концентрацию. Договорились больше не отдавать фигуры задарма?»

Ни одного нового шахматного факта модель не добавила. Те же Фg4, Кxg4, Фe2 и оценка словами из поля eval_words_after — меняются только тон и порядок изложения. Имя ребёнка в контракт не кладётся, на его месте плейсхолдер.

Здесь граница проведена дальше всего. Формулировка из README проекта: «Движок считает — тренер объясняет». В докстринге модуля анализа жёстче: «факты о тактике добываются кодом, никогда — вопросом к LLM».

Технически это веб‑приложение: FastAPI, 11,7 тысячи строк бэкенда, 18 таблиц SQLite, четырнадцать e2e‑сценариев. Агент подключён к нему бэкендом одной функции: получает строгий JSON с фактами, отдаёт одну реплику тренера.

Соперник — два независимых инстанса Stockfish. Один играет, второй работает «мозгом тренера» с фиксированным числом узлов ради детерминизма. В комментарии прямо: «никакого open‑ended depth».

Сила соперника задана узлами, а не временем:

OPPONENT_LEVELS = {
    1: {"skill": 0,  "nodes": 1500,   "blunder_p": 0.50},
    ...
    8: {"skill": 20, "nodes": 200000, "blunder_p": 0.0},
}

Два вывода записаны прямо в код как результат замеров. Первый: время зависит от железа — «на быстром процессоре Skill 0 за 50 мс досчитает до ~1300 Elo и раздавит новичка». Второй: даже Skill 0 слишком силён для ребёнка. Поэтому слабые уровни с вероятностью blunder_p играют случайный легальный ход — «детский зевок». Только это опускает нижний уровень к 400–800 Elo.

Сила = f(узлы, skill, вероятность зевка). Не f(железо).

Легальность хода не проходит через модель вообще. Ход приходит с фронта, приложение проверяет move in board⁠.legal_moves и отвечает 400. Состояние партии ведёт код: доска в памяти плюс полная сериализация в SQLite после каждого хода, переживает рестарт сервера реплеем из базы. У модели вообще нет диалоговой памяти о партии — каждый вызов stateless.

Классификация хода — формула, а не мнение. Win% из оценки по логистической функции, потеря в пунктах, шкала как у Lichess:

<2 best | <5 good | <10 ok | <20 inaccuracy | <30 mistake | иначе blunder

Симметрия здесь важна до нюанса: позиции «до» и «после» считаются одинаковым бюджетом узлов. Иначе Δwin% прыгает из‑за разной глубины, и метка скачет между «лучший ход» и «ошибка» на одном и том же ходу.

Поверх шкалы — три гибридных правила, потому что чистая Δwin% врёт в крайних случаях:

  • Отдал фигуру даром → форсируем «зевок», даже если позиция и так была проиграна (там Δwin% маленькая). С тремя исключениями, и каждое — вычисленный факт, а не догадка: ход совпадает с лучшим по движку (корректная жертва); после хода форсированный мат в пользу ученика; фигура висела уже до хода.

  • После хода появился форсированный мат против ученика.

  • У ученика был мат, и он его не поставил → минимум «ошибка».

Тактику ищут четыре символьных детектора на python‑chess: висящие фигуры, вилки, связки, мат в один. Каждый выдаёт готовую русскую фразу, которую модель только пересказывает. Все детекторы pin‑aware: абсолютно связанная фигура не считается ни атакующим, ни защитником — иначе факт «под боем» врёт.

Отдельная функция считает честный «выигрыш» вилки: соперник спасает самую дорогую цель, значит забираем вторую по цене. Без этого дешёвая вилка с ферзём в списке вытесняла из объяснения реально висящую ладью.

Детекторы прогоняются дважды — по позиции сразу после хода ученика и после первого хода наказания. Многие вилки видны только там.

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

Имя ребёнка в контракт не кладётся — там плейсхолдер {name}, подстановка на выдаче. Имя не уходит в модель и не оседает в кэше.

Четыре стадии: код считает факты, контракт-JSON с разрешёнными ходами, модель пересказывает, механическая проверка выхода
Четыре стадии: код считает факты, контракт‑JSON с разрешёнными ходами, модель пересказывает, механическая проверка выхода

Схема 3. Полный путь одного хода: движок считает, контракт ограничивает, модель пересказывает, проверка ловит выдуманное.

Выход модели проверяется механически. В контракт кладётся allowlist всех ходов, которые тренеру разрешено назвать. После генерации 342 строки разбирают реплику — включая русскую фигурную нотацию Кf3, Фxb2, рокировки и кириллические омоглифы координат — и проверяют каждый токен. Выдуманная координата (e0, Qz9) ловится во всех формах записи, которые я нашёл, — включая русскую нотацию и кириллические омоглифы. Реакция: одна регенерация с напоминанием, если снова грязно — детерминированный шаблон из фактов.

Рабочей, а не косметической, эту защиту делает вот что: половина кода посвящена тому, чтобы НЕ сработать зря. Голое «король на g8» не считается заявленным ходом — это может быть ссылка на поле. При отсутствии доски проверка не заваливается. Второй guard, ловящий «ты выиграл» после поражения, сделан через негативный lookahead, чтобы честное «ты выиграл пешку в дебюте» не срезалось.

В докстринге записана причина: «ложный срез ломает голос тренера — а ради честности голоса вся защита и делается».

Что осталось модели. Ровно три вещи: тон, выбор регистра разбора (по вычисленным полям) и пересказ фактов человеческим языком. Даже на свободный вопрос «есть ли тут вилка?» факты обязана дать функция.

У прямого драйвера принудительно выключен thinking, и мотивировка записана рядом: «факты уже посчитаны движком, тренеру думать нечего — он их пересказывает».

Шахматные инструменты у агента при этом есть. Персона запрещает их звать в разборе партии: там истина уже в контракте.

Агент для дочери: граница проходит по доверию

Что это. У девятилетнего ребёнка в мессенджере есть собеседник, которому она пишет сама: присылает фото домашки, спрашивает про немецкие слова, просит сказку на ночь. Готовых ответов он не даёт принципиально — задаёт наводящие вопросы. Решённые задачи и домашние дела оплачиваются звёздами по таблице наград, звёзды копятся и меняются на поход за мороженым.

Дальше работает расписание. Утром бот сам пишет квест: убрать за кошкой, расчесаться — пять звёзд, до половины девятого бонус. Вечером второй квест. Ребёнок отвечает «готово», агент дописывает начисление в журнал и объявляет новый баланс. Ночью тот же агент пишет уже не ребёнку, а родителю — короткий рассказ на один экран: что учили, какое было настроение, сколько звёзд заработано, был ли тревожный сигнал. По понедельникам — недельная сводка взрослым тоном, без детских эмодзи.

Если день выдался пустым, отчёта не будет вообще — про это ниже.

Здесь пропорция обратная шахматам: персона огромная (SOUL почти 20 КБ плюс девять тысяч символов в конфиге плюс собственный скилл на 361 строку), а своего Python‑кода почти нет. И это осознанно — задача не вычислительная. Но три места всё же вынесены в код, и все три про доверие.

(1) Журнал звёзд с идемпотентностью. Правило записано в персону дословно:

{"ts":"...","delta":2,"reason":"...","key":"2026-07-07:learn-de"}
  • баланс = сумма всех delta по журналу; отдельное число не хранить и не «помнить»;

  • начислять только после того, как строка реально дописана;

  • у события есть ключ "ДАТА:тип"; перед начислением проверить, нет ли уже строки с таким ключом;

  • stars.json — только зеркало для быстрых отчётов, источник правды журнал.

Причина названа в персоне прямым текстом: «это ядро доверия, раньше баланс всё время расходился и она злилась».

(2) Протокол тишины. 94 строки Python, которые сами, без модели, решают — есть ли что докладывать родителю. Скрипт сканирует сессии за дату, отфильтровывает крон‑сессии (по платформе, по имени файла и по маркеру в первом сообщении), считает пользовательские сообщения, смотрит записи о звёздах и возвращает код возврата: 0 — активности нет, 1 — есть. При нуле промпт обязан вывести ровно [SILENT] и ничего больше — иначе у родителя в канале копится спам «сегодня опять тихо».

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

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

Правило написал, дисциплины не хватило

Три источника баланса расходятся:

Источник

Значение

stars.jsonbalance

461

Сумма по массиву внутри того же файла

466

Журнал — объявленный источник правды

457

В журнале одна строка — стартовый перенос. За месяц с лишним после введения правила в него не дописано ни одного события.

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

«Стрейки — это арифметика дат. LLM, редактирующая JSON, забывает, ошибается в днях и начисляет дважды. Здесь математика точная и идемпотентная.»

Разница между «написал в промпте» и «вынес в код» измеряется этими тремя числами.

Ограничение инструментов режется по каналу, а не по агенту

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

В исходниках движка это описано прямо: platform_toolsets — механизм мягкий, жёсткая граница только disabled_toolsets. А он у этого агента пуст.

Агент для жены: промпт растёт как журнал инцидентов

Что это. Бытовой помощник в мессенджере. Основной сценарий — надиктовать расходы одной фразой, как они были потрачены, не раскладывая по категориям: «180 кафе 240 такси 95 аптека 60 кофе 320 продукты». В ответ приходит разобранная таблица с предпросмотром:

Проверь перед записью (правь строку по №):

0. 180 → Дом/Кафе        «кафе»
1. 240 → Дом/Транспорт   «такси»
2. 95  → Здоровье/Аптека «аптека»
3. 60  → Прочее/кофе     «кофе» ❓
4. 320 → Дом/Продукты    «продукты»

Итого в батче: 895 (строк: 5).
Ок — «записывай». Правка — «строку N: <категория>».

Знак вопроса в третьей строке — это не ошибка разбора, а честное «категорию подобрал с натяжкой, посмотри сама». В бюджет ничего не попадает, пока человек не подтвердит.

Сюда же — фотография банковской выписки, дневник питания, напоминания. Бюджет, выписки, здоровье, покупки. Пропорция обратная детскому агенту: SOUL почти стоковый, а персона в конфиге — 36 382 символа в 257 строках.

Она не была такой. Стартовый seed — 1,9 КБ. За полгода вырос в тридцать блоков, и почти каждый начинается словами «раньше было так, больше так не делай»:

«Раньше главная проблема была: ты говорила „готово / загрузила / отправила“, а у пользователя результата НЕ БЫЛО. БОЛЬШЕ ТАК НЕ ДЕЛАЙ: ссылки давай ТОЛЬКО те, что вернул реальный ответ API. НИКОГДА не придумывай и не собирай ссылку „по шаблону“».

«Раньше видео ломалось: склеенный ролик выходил на 1 секунду или битым. В его JSON смотри ok и duration_sec: отправляй ТОЛЬКО если ok=true и длительность разумная.»

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

Что именно унесли из модели:

Разбор диктовки расходов. Токенизатор режет свободную фразу «370 кафе 54 такси 6 сладости 300 дети одежда» на пары «сумма — метка»: числа якоря, слова между ними метка. Разбор сумм понимает форматы обеих стран, где живёт семья: 1.234,56, 1 234,56, и даже арифметику 72+108 прямо в строке.

Классификатор — детерминированный роутер: словарь алиасов, нечёткое сравнение с порогом 0,82, и обучаемая таблица: после подтверждённой записи первое слово комментария сохраняется как маппинг, и в следующий раз категория подставляется вообще без модели.

Дедупликация — не «последние N строк», а sha1 от «месяц|сумма|валюта|категория|подкатегория|комментарий». Причём дубль не блокируется молча, а помечается флажком в предпросмотре: переспросить должен человек.

Запись двухфазная: stage() кладёт в черновик и показывает превью, commit() переносит в основную таблицу. В бюджет ничего не попадает мимо предпросмотра.

Банковские PDF. Код писался под конкретные боевые ошибки — и, как выяснится в конце главы, до пользователя не доехал. Разбор ниже про то, каким он должен был быть. Тип файла определяется по magic‑bytes %PDF-, а не по метаданным вложения из мессенджера. Разбор сумм отказывается считать 141,288 числом — «центы это ровно два знака», три знака после запятой дают предупреждение, а не сумму‑мутанта «минус 141288». Из выписки вытаскиваются заявленные итоги и сверяются с посчитанной суммой транзакций с точностью до двух копеек. Расхождение уходит в предупреждения.

Молчание как возвращаемое значение. Функция напоминания о воде содержит всю логику: активное окно с 8 до 22, цель распределена линейно, молчать, если пил меньше 90 минут назад, молчать, если отставание меньше 400 мл. Иначе вернуть готовый текст.

Пустая строка = «не дёргать человека». Спам‑контроль вынесен из промпта в функцию. Ровно так же устроены follow‑up по здоровью: возвращается либо текст, либо пустота, означающая «сегодня молчим».

Курс валют — fail‑soft без выдумывания. Два источника подряд, суточный кэш. При сбое отдаётся последний кэш с пометкой «курс из кэша». Если и кэша нет — честное «курс недоступен, уточни вручную». Ни одного выдуманного числа ни при каком раскладе.

Астрология через эфемериды. Пример намеренно из области, где проверять некому — тем нагляднее принцип: Swiss Ephemeris, юлианская дата, накшатра как 360/27 с падой. В Human Design момент дизайна ищется методом Ньютона‑Рафсона — Солнце должно быть ровно на 88,000° дуги раньше рождения, до 50 итераций с точностью 1e-9. В докстринге: «если расчёт невозможен — модуль ЧЕСТНО кидает ошибку; НИКОГДА не гадай знаки».

Предмет тут дело десятое. Даже там, где никто не проверит, выбран путь «посчитать или честно отказаться» вместо «модель что‑нибудь скажет».

Где я сам провалился

5 863 строки собственного Python. Ни одна из трёх баз данных не создана.

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

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

Ещё одни грабли, на которые наступил сам. Я закрыл файлы конфигурации от изменения флагом неизменяемости — чтобы агент не переписал себе расписание. Под тот же флаг попал файл заданий планировщика. А планировщик сохраняет состояние через os.replace(). Получилось PermissionError 86 раз за сутки, и расписание не отработало больше недели.

Мера защиты от того, чтобы агент менял себе расписание, молча выключила расписание целиком.

Корпоративный агент, металлургия: домен как код, а не как база знаний

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

Теперь менеджер пишет боту одной фразой: «нужно общежитие на 42 человека, по трое в комнате» — или просто кидает фотографию чужого чертежа. Через пару минут в чат приходит лист формата А3: план этажа с осями, размерными цепочками, экспликацией помещений и чертёжным штампом. Плюс два трёхмерных вида здания и проверка по строительным нормам с пометками «прошло / не прошло». Дальше диалогом меняются параметры: «в два этажа», «санузлы посередине», «сколько стоит».

Вот два листа одного и того же общежития. Сверху — эталонный лист, нарисованный архитектором вручную. Снизу — то, что сгенерировал код из одной фразы:

Сверху лист архитектора, нарисованный в Revit за один-три дня. Снизу тот же план, сгенерированный кодом из текстового брифа за сорок секунд
Сверху лист архитектора, нарисованный в Revit за один‑три дня. Снизу тот же план, сгенерированный кодом из текстового брифа за сорок секунд

Оси, шаг модулей, экспликация и площади совпадают: 267,46 м² против эталонных 267,20 на двадцати помещениях. Расхождение в 26 сантиметров набегает из‑за округлений в калибровочной таблице, о которой ниже.

Вынесу отдельно: RAG в этом агенте нет вообще.

Я проверил поиском по всему проекту — совпадений ноль. В базе только стоковые таблицы движка. Каталог для базы знаний существует, в админке есть страницы загрузки — но каталог пуст, и ни персона, ни плагин к нему не обращаются. Это задел на будущее, не работающий тракт.

Домен раскидан по четырём местам, и все четыре — код или данные:

Каталог конструктива — 97 строк:

MODULE_CATALOG = {
    "BM-STD": {"len": 6023, "wid": 2448, "h": 2920, "axis_pitch": 2455, "purpose": "жилой/санитарный"},
    "BM-COR": {"len": 2448, "wid": 2015, "h": 2920, "axis_pitch": 2455, "purpose": "коридорный"},
    "BM-L":   {"len": 7350, "wid": 2920, "h": 2920, "axis_pitch": 2925, "purpose": "удлинённый"},
}
WALL_EXT  = 100   # наружная стена (сэндвич)
WALL_PART = 80    # перегородка

Калибровка площадей по эталонному чертежу — самое неочевидное место. Толщина панелей уже «съедена» в чистых площадях готового проекта, поэтому наивная геометрия не сходится с экспликацией. Решение — таблица, снятая с подписей на эталонном листе:

AREA_CALIBRATION = {
    ("living",  "interior"): 12.52,   # рядовой пролёт
    ("living",  "gable"):    12.12,   # торцевой — уже на 0,40 м²
    ("corridor", None):      44.50,
}

с геометрическим fallback'ом, если ключа нет. Результат: сгенерированный лист даёт 267,46 м² против эталонных 267,20 м² на двадцати помещениях.

Ни одна модель не «додумала» бы такую таблицу. Она снимается с бумаги руками.

Реестр норм — полные названия сводов правил, редакции и конкретные пункты: не менее 6 м² на человека; один душ, один умывальник и один унитаз на шестерых; не менее двух эвакуационных выходов, коридор не уже 1400 мм. Отдаётся отдельным инструментом на вопрос «а по каким нормам вы проверяете».

Габариты считает функция, а не модель. Модель отдаёт «сколько людей и по сколько в комнате». Дальше:

def _derive_bays(rooms_needed, reserve, floors=1, stair_bays=0):
    """Пролётов в ряду под нужное число ЖИЛЫХ КОМНАТ (не людей —
    так микс ИТР/рабочие с разной плотностью считается честно)."""
    living_per_floor = math.ceil(rooms_needed / max(floors, 1))
    bays = math.ceil((living_per_floor + SANITARY_BAYS + reserve + stair_bays) / ROWS)
    return max(4, min(bays, MAX_BAYS))

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

Не влезает — значит не влезает. Если запрошенное не помещается в гребёнку из максимум 14 пролётов, функция не строит молча здание поменьше. Она возвращает fits: False, список вариантов и подсказку модели, что именно сказать человеку.

Персона домена почти не содержит — 80 строк правил поведения. Правило номер один:

НИКОГДА не выдумывай размеры, площади, число модулей и результаты проверок — все цифры бери ТОЛЬКО из результата инструмента.

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

И побочный эффект, ради которого это в итоге продавалось: на эталонном листе, который лёг в основу калибровки, площадь на человека расходится с нормой, процитированной в его же примечаниях — 4,17 м² против шести. Валидатор прототипа ловит это автоматически, и именно этот момент оказался самым убедительным на демонстрации.

Корпоративный агент, энергетика: PASS только там, где посчитано

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

Теперь он пишет в чат одной строкой: «Нужна 2КТП-400/10/0,4 кВ, два ввода с АВР, пять отходящих: жильё 120 А, жильё 100 А, котельная 160 А, ФАП 80 А, уличное освещение 25 А» — или фотографирует бумажное техзадание. Примерно за минуту приходит комплект: чертёж однолинейной схемы на листе А3 картинкой и в DXF, который открывается в AutoCAD, перечень оборудования в Excel, ведомость работ, отчёт проверки по нормам и вилка стоимости со сроком.

Однолинейная схема двухтрансформаторной подстанции: два ввода 10 кВ, два трансформатора, распределительное устройство на две секции с автоматическим вводом резерва, пять отходящих линий, контур заземления и таблица технических показателей
Однолинейная схема двухтрансформаторной подстанции: два ввода 10 кВ, два трансформатора, распределительное устройство на две секции с автоматическим вводом резерва, пять отходящих линий, контур заземления и таблица технических показателей

Тот же лист выгружается в DXF — заказчику нужен файл, который правится в AutoCAD, а не картинка. Подпись «требуется разработка РД» стоит на листе не для красоты: агент делает стадию «П», а не рабочую документацию, и говорит об этом сам.

Второй артефакт — отчёт проверки. Он интереснее чертежа, потому что показывает главное решение всего агента:

Итог проверок: 3 PASS · 3 FAIL · 1 WARN · 5 INFO

вводной автомат ≥ Iном тр-ра НН (909 А)     ✅ PASS   QF1 — 1000 А; Iном тр-ра 909 А
режим N-1: один тр-р несёт нагрузку          ❌ FAIL   суммарный ток 1200 А; Iном одного 909 А;
                                                      допустимость перегрузки не доказана
защита линий: Iрасч ≤ Iном ≤ Iдоп кабеля     ❌ FAIL   QF4: 600 ≤ 800 ≤ 290 А
ОПН на стороне ВН и НН                       ℹ️ INFO   предусмотрены; координация изоляции — стадия РД
заземляющее устройство                       ℹ️ INFO   предусмотрено; R ≤ 4 Ом — по замеру, стадия РД
отключающая способность против Iкз           ✅ PASS   расч. Iкз ≈ 16,5 кА; мин Icu 35 кА — это прикидка,
                                                      точный ТКЗ по ГОСТ 28249 на стадии РД

Обратите внимание на разницу между FAIL и INFO. Заземление предусмотрено — но статус INFO, а не PASS, потому что наличие контура не равно измеренному сопротивлению. Ограничители перенапряжения стоят — тоже INFO: наличие не равно координации изоляции. PASS ставится только там, где условие реально посчитано из модели.

В комментариях движок открыто описан как порт архитектуры предыдущего агента: «аналог каталога, но для электроэнергетики». Между двумя корпоративными проектами переиспользована не библиотека, а скелет: каталог → генератор → валидатор → рендер → спецификация → костинг → плагин из четырёх‑пяти инструментов.

Каталог как единственный источник номиналов — 275 строк: пять трансформаторов, восемь ячеек, автоматы низкой стороны, кабели, опоры, изоляторы, оболочки.

Подбор — лестница, а не «нейросеть предложила»:

def pick_lv_input_breaker(kva):
    i = transformer_lv_current(kva)          # I = S/(√3·0,4)
    for key, inom in ladder:
        if inom >= i: return key
    raise ValueError(f"ток трансформатора {i:.0f} А выше лестницы вводных автоматов")

def pick_hv_fuse(kva):
    """Возвращает копию карточки; молчаливого подбора вне таблицы нет."""
    try: return dict(HV_FUSES[int(kva)])
    except (KeyError, TypeError, ValueError) as exc:
        raise ValueError(f"нет таблицы плавкой вставки для {kva} кВА") from exc

Принцип: подмена только вверх и только явно. Запрошенные 800 кВА не превращаются молча в 630 — берётся следующий не меньший типоразмер, и в чат уходит WARN: запрошено 800 кВА, движок применил следующий не меньший каталожный типоразмер 1000 кВА.

Состав линии выводится формулами:

опоры      = ceil(L/пролёт) + 1
анкерные   = 2 концевые + (ceil(L/500 м) - 1) внутренних + по одной на отпайку
провод     = L × 3 фазы × 1,03 (монтажный запас), округление вверх до 5 м
изоляторы  = штыревые 3/промежуточную; натяжные 6/анкерную, 3/концевую

Отпайка за пределами трассы — ошибка, а не догадка. В реальном логе сессии: {"ok": false, "error": "vl_taps[2].km=0.53 вне трассы 0…0.53 км"}.

Валидатор — 473 строки, десять проверок для подстанции и восемь для линии. Каждая возвращает правило, требование, статус и способ исправления.

Каждая проверка возвращает не только статус, но и способ исправления. Отключающая способность против прикидочного тока короткого замыкания даёт WARN, а не FAIL: полный расчёт по ГОСТ здесь не делается, и выдавать прикидку за проверку нельзя.

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

При FAIL код сам дописывает в промпт жёсткую подсказку, не полагаясь на то, что модель заметит статус в JSON:

if fail_count:
    validation_hint = (f"⛔ ВАЛИДАЦИЯ СОДЕРЖИТ {fail_count} FAIL. Явно перечисли FAIL и не называй "
                       "решение прошедшим проверку или готовым к применению. ")

Честно говоря, это по‑прежнему просьба к модели — просто гарантированно доставленная. Жёсткий вариант, когда артефакт не отдаётся, пока FAIL не снят, я не сделал.

Стоимость — арифметика снизу вверх: сумма по каталогу, плюс монтаж 30–50%, пусконаладка, проектирование, региональный коэффициент, округление до сотни тысяч. Причём коэффициент берётся только из явного поля региона, и рядом комментарий: «название населённого пункта здесь намеренно не анализируется: совпадение подстроки не определяет регион».

Чертёж рисуется одной функцией в двух форматах. Экспортёр DXF мимикрирует под холст SVG — те же примитивы — поэтому одна и та же функция компоновки даёт идентичную геометрию и в вебе, и в AutoCAD. Растр рендерится headless‑браузером, который уже умеет кириллицу и системные шрифты.

Успех рендера проверяется не через «файл существует»:

valid_signature = head[:8] == b"\x89PNG\r\n\x1a\n"
if size < 1024 or not valid_signature or not valid_ihdr or actual_wh != (w_px, h_px):
    png_path.unlink(missing_ok=True); raise RuntimeError(...)

Код возврата, сигнатура файла и размеры из заголовка. Битый файл до пользователя не доезжает.

Два случая, которыми хвастаться нечем

Модель подгоняла вход, пока FAIL не исчез. На реальном техзадании три последовательных вызова генератора:

Вызов

Отходящие линии

Итог

1

2 × 600 А

FAIL

2

2 × 385 А

FAIL

3

200 + 200 + 185 + 185 А

OK

Сложите суммы: 1200 → 770 → 770 А. Между первым и вторым вызовом модель срезала треть заявленной нагрузки — вот это и есть подмена исходных данных. Между вторым и третьим сумма не менялась вообще, поменялось только дробление: два фидера превратились в четыре. Само по себе это законное проектное действие под ограничение по номиналу, но выбрала его модель, а не инженер.

Валидатор проверяет схему, а не её соответствие техзаданию, поэтому «OK» здесь не значит ничего без человека, который сверит нагрузку с исходником. И правильный фикс — не «пусть инженер посмотрит», а признак происхождения у каждого параметра: пришёл из ТЗ или придуман моделью. Любой переход FAIL → OK, полученный сменой параметров второго типа, должен уходить в отчёт отдельной строкой. Механизм для этого у меня уже есть — те самые «потерянные требования клиента». Расширить его на подменённые входы я не догадался.

Модель построила себе OCR‑пайплайн, потому что ей это разрешили. Прислали техзадание сканом без текстового слоя. Плагин PDF не принимает. Но набор инструментов даёт терминал, а в конфиге разрешена ленивая доустановка пакетов. Реальная цепочка вызовов из истории сессии:

skill_view("ocr-and-documents")   → инструкция
python -c "import fitz"           → ModuleNotFoundError
pdftotext                         → command not found
python -c "import pypdf"          → ModuleNotFoundError
tesseract                         → command not found
[предупреждение: тот же инструмент падает третий раз подряд]
uv pip install pymupdf → «externally managed» → создаёт venv → успех
render 13 страниц в PNG → установка pillow → OCR
"=== PAGE 1 === puJoKeHne KIpHKa3yIIAO ..."   ← кириллица распалась

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

Выводов тут два. Агентность каркаса вытащила задачу, на которую прикладной код не был рассчитан. Она же оказалась дырой: агент ставит пакеты и рендерит файлы в контейнере на лету, без песочницы, потому что тулсет никто не урезал. Контур демонстрационный, боевых данных в нём нет — но в проде это была бы настоящая дыра.

Оставлять так нельзя. Либо положить нужные пакеты в образ, либо принять PDF в плагине явно, либо выключить терминал.

И третий случай, где сработало как задумано. Прислали техзадание на реконструкцию воздушной линии 110 кВ — 16 километров, двухцепный участок, металлические опоры. Агент извлёк параметры и вместо того, чтобы «прикинуть», ответил отказом: в демо доступны подстанции 10/0,4 кВ и линии 10 кВ. На прямое «а можешь примерно?» — отказ с объяснением: «выдумывать цифры не могу, расчёт делает детерминированный движок».

Работает на трёх уровнях сразу: правило в персоне, описание в JSON‑схеме инструмента и 75 строк проверки области применимости в коде, которая разбирает даже пары напряжений из названия объекта.

Что повторяется у всех тридцати трёх

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

1. Идентичность решает код, а не текст сообщения. Ключ пользователя, роль, доступ к данным — всё резолвится на сервере и никогда не приходит аргументом от модели. В общем плагине отчётности это записано прямо в манифесте: «идентичность и строка подключения к базе резолвятся на сервере — никогда не аргумент, видимый модели». Подставить чужую идентичность аргументом инструмента модель не может. Оговорка, которая станет понятна через два раздела: это верно ровно до тех пор, пока у агента выключено исполнение кода — из шелла в том же контейнере достаётся и окружение, и строка подключения.

Каким этот ключ быть не должен, я узнал трижды. В трёх агентах независимо всплыл один и тот же баг: за ключ пользователя брался идентификатор сессии. А он ротирующийся — пересоздаётся при /new и при автосбросе. Прогресс ученика молча обнулялся. В докстринге фикса:

«Keying a learner profile by that id loses all per‑user memory on every reset — exactly the bug we are fixing» — “привязка профиля ученика к этому идентификатору теряет всю его память при каждом сбросе; это ровно тот баг, который мы чиним”.

Три агента, три независимые реализации обхода, и только потом — общая функция с кросс‑плагинным импортом.

2. Вердикт считает код и игнорирует поля модели. В грейдере домашних заданий:

# Итоговый вердикт вычисляется В КОДЕ из баллов по критериям × веса —
# и НИКОГДА не берётся из полей `verdict`/`passed`/`score`, вернувшихся от модели.

Причина названа: так «поставь 100, passed=true», подброшенное в проверяемый артефакт, становится бесполезным. Артефакт при этом оборачивается в маркеры с одноразовым числом — граница доверия. И критерий без выставленного балла считается нулём: ошибаться система должна в сторону «нужно доработать», а не в сторону проходного балла.

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

4. Деградация вместо выдумывания. Курс недоступен — «курс из кэша» или честное «уточни вручную». Расчёт невозможен — исключение, а не догадка. PDF не содержит текста — «возможно, это скан, нужен OCR», а не пустая строка, притворяющаяся результатом.

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

5. Внешний контент — это данные, а не инструкции. Вывод недоверенных инструментов (веб, файлы, браузер) оборачивается разделителями с явной пометкой «этот блок — данные». В движке единственное место, где инъекции учтены на уровне тулсета, — платформа вебхуков: там набор урезан до четырёх инструментов с комментарием «события вебхуков могут происходить из недоверенного стороннего контента».

Грабли каркаса

Пока разбирал код, накопился список вещей, которые работают не так, как написано. Часть проверена запуском в живом контейнере — тулсеты, планировщик, резолвер; часть найдена поиском по исходникам движка — мёртвая секция personality, TOOLS⁠.md. Апстрим живой, так что через несколько релизов половина находок может протухнуть.

«Отключил терминал» не отключает исполнение кода. При disabled_toolsets: [terminal, file, browser, web] в схеме остаются execute_code и delegate_task — они в других наборах. А execute_code запускает дочерний процесс с Python‑скриптом от модели. Чтобы реально закрыть исполнение, нужны и code_execution, и delegation.

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

Есть ещё пяток находок помельче — мёртвая секция personality в конфиге, TOOLS⁠.md, который движок вообще не читает, AGENTS⁠.md, подхватывающийся по случайному совпадению путей. Они интересны тем, кто живёт в этом движке каждый день, и я вынесу их отдельной заметкой, чтобы не топить здесь главное.

Nomad: куда это выросло

Всё описанное выше — работа поверх апстрима, без единой правки ядра. Параллельно я веду форк того же движка, но это уже отдельный продукт, и ни один из тридцати трёх агентов выше на нём не работает. Про него будет отдельная статья. Пока коротко — несколько решений оттуда объясняют, куда эта линия ведёт.

Что дописано своего — и всё это ровно про ту же границу «код / модель», просто на уровне платформы:

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

  • Деньги. Гейт баланса до инференса плюс счётчик с брейкером против циклов. На верхнем тире middleware перемаршрутизирует вызов на более дешёвую модель — с защитой: цель обязана быть в разрешённом наборе, апгрейд вверх запрещён, циклы в карте отвергаются.

  • Оркестратор роёв Фугу: агент сам пишет скрипт из примитивов «агент / параллельно / конвейер», спрашивает подтверждение на фан‑аут с оценкой стоимости — и выводы субагентов остаются внутри движка, в контекст возвращается только результат. В документации это сформулировано как «тридцать субагентов стоят одного саммари».

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

Выводы

Агент — это том, а не форк. Тридцать три агента на одном образе без единой правки ядра. Если руки тянутся форкнуть движок ради одного агента — почти наверняка задача решается плагином и конфигом. Форк оправдан тогда, когда продуктом становится сам движок: у меня это случилось один раз на тридцать три.

Вся настройка сводится к границе «код / модель». Остальное — детали. Если граница проведена правильно, агент ошибается редко и предсказуемо; если нет — врёт красиво и убедительно, и заметить это некому.

Промпт — это черновик кода. Каждое правило вида «НИКОГДА не выдумывай X» — заявка на функцию. У меня из шести таких правил выросли шесть модулей, и это нормальный ход вещей. Хуже, когда правило так и остаётся в промпте: баланс звёзд разошёлся на трёх источниках именно поэтому.

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

У отказа должно быть имя. Курс из кэша, «не могу посчитать», FAIL с перечислением, INFO вместо PASS там, где не считалось. Молчаливая деградация обходится дороже громкой.

Проверять надо запуском. Половина находок в этом разборе — из прогонов внутри живых контейнеров, а не из чтения конфигов. Что тулсет режется по каналу, а по расписанию открыт; что «отключил терминал» не отключает исполнение кода; что закрытый от записи файл расписания убивает планировщик — ничего из этого по документации не видно.

Про свои эксперименты с агентами пишу в канале «Готовим ИИшницу» — там короткие заметки по ходу дела, а сюда выношу разборы целиком.