Глава 9: Agents форка и кэш запросов

Понимание на девяносто пять процентов

Когда parent agent параллельно порождает пять дочерних agents, подавляющее большинство запросов API каждого дочернего элемента идентично. Системное prompt такое же. Определения tools те же. История разговора та же. Сообщение помощника, которое вызвало появление, то же самое. Единственное, что отличается, — это последняя директива: «вы занимаетесь миграцией базы данных», «вы пишете тесты», «вы обновляете документацию».

В типичном форке с теплым разговором общий префикс может составлять 80 000 токенов. Директива для каждого ребенка может составлять 200 токенов. Это перекрытие на 99,75%. Кэш prompts Anthropic дает 90% скидку на кэшированные входные токены. Если вы сможете заставить эти 80 000 токенов попасть в кеш для дочерних элементов со 2 по 5, вы просто сократите входную стоимость этих четырех запросов на 90%. Для родителя это разница между расходами в 4 доллара и 0,50 доллара на одну и ту же параллельную отправку.

Загвоздка в том, что кэширование prompts выполняется с точностью до байта. Не «достаточно похоже». Не «семантически эквивалентен». Байты должны совпадать, символ за символом, от первого байта System Prompt до последнего байта, прежде чем содержимое каждого дочернего элемента будет расходиться. Одно лишнее место, одно переупорядоченное определение tool, один устаревший флаг функции, меняющий фрагмент System Prompt — и кеш промахивается. Вся приставка перерабатывается за полную стоимость.

Agents-форки являются ответом Claude Code на это ограничение. Они не просто удобны для «порождения дочернего элемента с контекстом» — это механизм быстрого использования кэша, замаскированный под функцию оркестрации. Каждое проектное решение в системе разветвлений связано с одним вопросом: как мы можем гарантировать байтовую идентичность префиксов в параллельных дочерних элементах?


Что наследует ребенок-вилка

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

1. Системное prompt. Не регенерировано — с резьбой. Уже обработанные байты System Prompt родительского объекта передаются через override.systemPrompt, полученный из toolUseContext.renderedSystemPrompt. Это точная строка, которая была отправлена ​​в последнем вызове родительского объекта API.

2. Определения tools. В определении agent вилки объявляется tools: ['*'], но если для флага useExactTools установлено значение true, дочерний элемент напрямую получает собранный массив tools родителя. Никакой фильтрации, никакого переупорядочения, никакой повторной сериализации.

3. История разговоров. Каждое сообщение, которым родитель обменялся с API — повороты пользователя, повороты помощника, tool calls, результаты работы tools — клонируется в контекст ребенка через forkContextMessages.

4. Конфигурация и модель мышления. В определении вилки указывается model: 'inherit', что соответствует точной модели родительского элемента. Та же модель означает тот же токенизатор, то же контекстное окно, то же пространство имен кэша.

Само определение форк-agent минимально — почти не требуется:

Определение agent форка намеренно минимально — оно наследует все от родителя. Он определяет все tools ('*'), наследует родительскую модель, использует пузырьковый режим для разрешений (поэтому prompt отображаются в родительском терминале) и предоставляет функцию бездействующей System Prompt, которая на самом деле никогда не вызывается - настоящее prompt поступает через канал переопределения, уже визуализированное и стабильное по байтам.


Трюк с байтовым префиксом

Запрос API к Клоду имеет определенную структуру: System Prompt, затем tools, затем сообщения. Чтобы Prompt Cache работал, каждый байт от начала запроса до границы префикса должен быть идентичен во всех запросах.

Agents вилки достигают этого, обеспечивая замораживание трех слоев:

Уровень 1: System Prompt посредством потоковой обработки, а не повторных вычислений.

Когда System Prompt parent agent отображалось для его последнего вызова API, результат фиксировался в toolUseContext.renderedSystemPrompt. Это строка после всей динамической интерполяции — флаги функций GrowthBook, сведения о среде, описания серверов MCP, содержимое skills, файлы CLAUDE.md. Дочерний элемент вилки получает именно эту строку.

Почему бы просто не позвонить еще раз на getSystemPrompt()? Потому что генерация системных prompts не является чистой. GrowthBook сигнализирует о переходе из холодного State в теплое, когда SDK получает удаленную конфигурацию. Флаг, который вернул false во время первого хода родительского элемента, может вернуть true к моменту раскручивания дочернего элемента. Если System Prompt включает условный блок, ограниченный этим флагом, повторно отображаемое prompt отличается хотя бы на один символ. Кэш взломан. Полная обработка 80 000 токенов, умноженная на пять детей.

Обработка обработанных байтов устраняет весь этот класс расхождений.

Уровень 2: определения tool посредством точного прохождения.

Обычные sub-agents проходят через resolveAgentTools(), который фильтрует пул tools на основе массивов tools и disallowedTools определения agent, применяет различия в режимах разрешений и, возможно, меняет порядок tools. Результирующий сериализованный массив tools будет отличаться от родительского — другое подмножество, другой порядок, другие аннотации разрешений.

Agents форка полностью пропускают это:

const resolvedTools = useExactTools
  ? availableTools  // parent's exact array
  : resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools

Флаг useExactTools имеет значение true только на пути ветвления. Дочерний элемент получает родительский набор tools «как есть». Те же tools, тот же порядок, та же сериализация. Это включает в себя сохранение самого tool «Agent» в дочернем пуле, даже если дочернему элементу запрещено его использовать — его удаление приведет к изменению массива tools и разрушению кеша.

Уровень 3: построение массива сообщений.

Здесь buildForkedMessages() делает свою тщательную работу. Функция создает два последних сообщения, которые находятся между общей историей и директивой для каждого ребенка:

Функция buildForkedMessages() создает последние два сообщения, которые находятся между общей историей и директивой для каждого ребенка. Алгоритм:

  1. Клонировать сообщение помощника родителя (сохранив все блоки tool_use с их исходными идентификаторами).
  2. Для каждого блока tool_use создайте tool_result с постоянной строкой-заполнителем (идентичной для всех дочерних элементов).
  3. Создайте одно пользовательское сообщение, содержащее все результаты-заполнители, за которыми следует директива для каждого ребенка, заключенная в шаблонный тег.
  4. Верните [clonedAssistantMessage, userMessageWithPlaceholdersAndDirective].
// Pseudocode — illustrates the message construction
function buildChildMessages(directive, parentAssistant) {
  const cloned = cloneMessage(parentAssistant)
  const placeholders = parentAssistant.toolUseBlocks.map(b =>
    toolResult(b.id, CONSTANT_PLACEHOLDER)  // Byte-identical across children
  )
  const userMsg = createUserMessage([...placeholders, wrapDirective(directive)])
  return [cloned, userMsg]
}

Результирующий массив сообщений для каждого дочернего элемента выглядит так:

[...shared_history, assistant(all_tool_uses), user(placeholder_results..., directive)]

Каждый элемент перед директивой идентичен для всех дочерних элементов. FORK_PLACEHOLDER_RESULT — постоянная строка 'Fork started -- processing in background' — обеспечивает байтовую идентичность даже блоков результатов tool. Значения tool_use_id идентичны, поскольку они ссылаются на одно и то же сообщение помощника. Изменяется только последний текстовый блок, содержащий директиву для каждого ребенка.

Граница кэша проходит прямо перед последним текстовым блоком. Все, что выше этого — потенциально десятки тысяч токенов системных prompts, определений tools, истории разговоров и результатов-заполнителей — попадает в кеш со скидкой 90% для каждого дочернего элемента после первого.


Шаблонный тег вилки

Директива каждого дочернего элемента заключена в шаблонный тег XML, который служит двум целям: он инструктирует дочерний элемент о том, как себя вести, и действует как маркер для обнаружения рекурсивного разветвления.

Шаблон содержит около 10 правил. Ключевые из них:

  • Переопределить инструкцию родительского разветвления. В системном prompt родителя указано «по умолчанию разветвление» — шаблон явно сообщает дочернему элементу: «Эта инструкция предназначена для родителя. Вы ЯВЛЯЕТЕСЬ ответвлением. НЕ порождайте sub-agents».
  • Выполнять бесшумно, сообщить один раз. Никакого диалогового текста между вызовами tools. Используйте tools напрямую, а затем подготовьте структурированное резюме.
  • Оставайтесь в рамках. Ребенок не должен выходить за рамки своих указаний.
  • Структурированный формат вывода. Ответ должен следовать шаблону «Область/Результат/Ключевые файлы/Измененные файлы/Проблемы», который позволяет родителю легко анализировать результаты, когда несколько дочерних элементов отчитываются одновременно.

Правило 1 особенно интересно. Системное prompt родителя, которое дочерний элемент наследует дословно по причинам кэширования, содержит инструкции типа «по умолчанию выполняется разветвление, когда у вас параллельная работа». Если бы дочерний элемент следовал этой инструкции, он попытался бы создать форк своих собственных дочерних элементов, создавая бесконечную рекурсию agents. Шаблон явно переопределяет: «Эта инструкция предназначена для родителя. Вы — ветвь».

Формат структурированного вывода (Объем/Результат/Ключевые файлы/Измененные файлы/Проблемы) не является декоративным. Он ограничивает вывод дочернего элемента фактическими отчетами, что упрощает анализ и агрегирование результатов для родителя, когда пять дочерних элементов отчитываются одновременно.


Предотвращение рекурсивных вилок

Дочерний элемент форка сохраняет tool «Agent» в своем пуле tools. Это необходимо - его удаление приведет к изменению сериализованного массива tools и разрушению Prompt Cache. Но если дочерний элемент на самом деле вызывает tool «Agent» без subagent_type, путь вилки сработает снова, создав дочернюю вилку. Этот внук унаследует еще более широкий контекст (разговор родитель-потомок), создаст свои собственные ответвления и так далее.

Два охранника предотвращают это:

Основная защита: проверка querySource. При создании дочернего элемента его context.options.querySource устанавливается значение 'agent:builtin:fork'. Метод call() проверяет это, прежде чем разрешить путь ветвления:

// In AgentTool.call():
if (effectiveType === undefined) {
  // Fork path -- but are we already in a fork?
  if (querySource === 'agent:builtin:fork') {
    // Reject: already a fork child
  }
}

Это быстрый путь. Он проверяет одну строку в объекте параметров.

Резервная защита: сканирование сообщений. Для предотвращения вилок используются две меры защиты: тег querySource, устанавливаемый во время создания (быстрый путь — сравнение одной строки), и резервный вариант, который сканирует историю сообщений для шаблонного тега XML. Резервный вариант существует, потому что querySource выдерживает автокомпактное исполнение, но в крайних случаях, когда он не был должным образом связан с потоками, резервный вариант сканирования сообщений улавливает рекурсию. Это подход «ремни и подтяжки», при котором стоимость проверки (сканирования сообщений) тривиальна по сравнению со стоимостью случайного рекурсивного разветвления (безудержные затраты API).

Почему резервный вариант? Потому что Claude Code имеет функцию автосжатия, которая перезаписывает массив сообщений, когда контекст становится слишком длинным. Autocompact может перезаписывать содержимое сообщения, но сохраняет querySource в параметрах. Теоретически достаточно одного querySource. На практике резервный вариант сканирования сообщений улавливает крайние случаи, когда querySource не был правильно связан с потоками - подход с поясом и подтяжками, при котором стоимость проверки (сканирования сообщений) тривиальна по сравнению с затратами на случайное рекурсивное разветвление (безудержные затраты API).


Переход от синхронизации к асинхронности

Дочерний элемент вилки начинает работать на переднем плане: его сообщения передаются на родительский терминал, а родительские блоки ожидают завершения. Но что, если ребенок слишком долго ходит? Claude Code позволяет работать в фоновом режиме в середине выполнения — пользователь (или автоматический тайм-аут) может перевести работающий agent переднего плана в фоновый режим без потери какой-либо работы.

Механизм на удивление чист:

  1. Когда agent переднего плана регистрируется через registerAgentForeground(), создается Promise фонового сигнала.

  2. Цикл синхронизации родительского элемента переключается между потоком сообщений agent и фоновым сигналом:

while (true) {
  const result = await Promise.race([
    iterator.next(),         // next message from agent
    backgroundSignal,        // "move to background" trigger
  ])
  if (result === BACKGROUND_SIGNAL) break
  // ... process message
}
  1. Когда срабатывает фоновый сигнал, итератор переднего плана корректно завершается с помощью iterator.return(). Это запускает блок генератора finally, который выполняет очистку.

  2. Новый экземпляр runAgent() создается с помощью isAsync: true, используя тот же идентификатор agent и накопленную на данный момент историю сообщений. Agent продолжает работу с того места, на котором остановился, теперь работает в фоновом режиме.

  3. Исходный синхронный call() возвращает { status: 'async_launched' }, и родительский элемент продолжает диалог.

Никакая работа не будет потеряна, поскольку история сообщений — это State agent. Транскрипт боковой цепи на диске содержит все сообщения, созданные agent. Новый экземпляр async воспроизводится из этой записи и возобновляет работу с того места, где экземпляр синхронизации остановился.


Автоматический фоновый режим

Если включена переменная среды CLAUDE_AUTO_BACKGROUND_TASKS или флаг tengu_auto_background_agents GrowthBook, agents переднего плана автоматически переходят в фоновый режим через 120 секунд:

При включении через переменную среды или флаг функции agents переднего плана автоматически переводятся в фоновый режим через 120 секунд. Если отключено, функция возвращает 0 (автоматическое фоновое воспроизведение отсутствует).

Это UX-решение, имеющее финансовые последствия. Agent переднего плана блокирует родительский терминал — пользователь не может печатать, не может давать новые инструкции, не может создавать других agents. Двух минут достаточно для того, чтобы agent мог выполнить большинство быстрых Task синхронно (когда потоковые выходные данные представляют собой полезную обратную связь), но достаточно мало, чтобы длительные Task не держали терминал в заложниках.

В рамках эксперимента с форком вопрос об автоматическом фоновом режиме является спорным: все порождения форков с самого начала принудительно асинхронны. Параметр run_in_background полностью скрыт из схемы. Каждый дочерний элемент форка работает в фоновом режиме, сообщает об окончании через <task-notification>, а родительский элемент никогда не блокируется.


Когда вилка НЕ ​​используется

Форк — один из нескольких режимов оркестровки, который намеренно исключен в трёх случаях:

Coordinator Mode. Coordinator Mode и режим разветвления являются взаимоисключающими. У координатора есть структурированная модель делегирования: он поддерживает план, назначает Task работникам с явными prompts и отслеживает прогресс. Подход Форка «наследовать все» подорвет это. Разветвленный координатор унаследует системную prompt родительского координатора (в которой говорится: «Вы координатор, делегируйте работу»), и дочерний элемент будет пытаться организовывать, а не выполнять. Функция isForkSubagentEnabled() сначала проверяет isCoordinatorMode() и возвращает false, если она активна.

Неинтерактивные сеансы. Потребители SDK и API (режим --print, Claude Agent SDK) работают без терминала. permissionMode: 'bubble' Fork отображает запросы разрешений на родительский терминал, которого нет в неинтерактивном режиме. Вместо создания отдельного потока разрешений путь ветвления просто отключается. Вместо этого потребители SDK используют явный выбор subagent_type.

Явный subagent_type. Если в модели указан subagent_type (e.g., "Explore", "Plan", "general-purpose"), путь ветвления не запускается. Форк срабатывает только тогда, когда subagent_type опущен. Это позволяет модели выбирать между «Мне нужен специализированный agent с собственной системной prompt и набором tools» (явный тип) и «Я хочу, чтобы мой клон, наследующий контекст, обрабатывал это параллельно» (опущенный тип).


Экономика

Рассмотрим конкретный сценарий. Разработчик просит Claude Code провести рефакторинг модуля. Родительский agent анализирует кодовую базу, формирует план и параллельно отправляет пять дочерних вилок: один для обновления схемы базы данных, один для переписывания уровня сервиса, один для обновления маршрутизатора, один для исправления тестов и один для обновления типов.

На этом этапе разговора общий контекст существенен:

  • Системное prompt: ~4000 токенов.
  • Определения tools (более 40 tools): ~ 12 000 жетонов.
  • История разговоров (анализ + планирование): ~30 000 токенов
  • Сообщение помощника с пятью блокамиtool_use: ~2000 токенов.
  • Tool results-заполнителя: ~ 500 токенов.

Общий общий префикс: ~48 500 токенов. Директива для каждого ребенка: ~200 токенов.

Без форка (пять независимых agents, каждый со свежим контекстом и собственной системной prompt):

  • Каждый ребенок обрабатывает свою системную prompt + tools + prompt Task.
  • Нет совместного использования кеша (разные системные prompt, разные наборы tools)
  • Стоимость: 5 x полная обработка ввода

С форком (идентичные по байтам префиксы):

  • Ребенок 1: 48 700 жетонов за полную стоимость (промах в кэше по первому запросу)
  • Дети 2–5 лет: 48 500 жетонов по цене 10% (попадание в кэш) + 200 жетонов по полной цене каждый.
  • Эффективная стоимость для детей 2–5 лет: ~4850 + 200 = ~5050 жетонов, эквивалентных каждому.

Шкала экономии с размером контекста и количеством детей. Для теплого сеанса со 100 тысячами токенов истории, порождающих 8 параллельных вилок, экономия кеша может превысить 90% от стоимости входных токенов без совместного использования.

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


Конструктивные противоречия

Система вилок предполагает явные компромиссы, которые стоит понять:

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

Безопасность и эффективность кэша. Tool «Agent» остается в пуле tools дочернего форка, даже если дочерний элемент не должен его использовать. Удаление его было бы безопаснее (дочерний элемент не сможет даже попытаться выполнить форк), но это изменило бы сериализацию массива tools. Шаблонный тег и рекурсивная защита вилок являются компенсирующими элементами управления — предотвращением выполнения во время выполнения вместо удаления статики.

Простота и эффективность кэша. Tool results-заполнителя являются ложью. Дочерний элемент видит 'Fork started -- processing in background' для каждого блокаtool_use в сообщении помощника родителя, независимо от того, что на самом деле сделали эти tool calls. Это нормально, потому что директива дочернего процесса говорит ему, что делать — ему не нужны точные tool results от диспетчерской работы родителя. Но это означает, что история разговоров ребенка технически бессвязна. Заполнитель выбран из соображений краткости и единообразия, а не точности.

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


Примените это: проектирование для быстрого повышения эффективности кэша

Шаблон форк-agent выходит за рамки Claude Code. Любая система, которая отправляет несколько параллельных вызовов LLM из одного и того же контекста, может извлечь выгоду из построения запросов с учетом кэша. Принципы:

1. prompt, отображаемые в потоке, не пересчитывайте. Если ваша System Prompt включает в себя какой-либо динамический контент — флаги функций, временные метки, пользовательские настройки, варианты A/B-тестов — запишите визуализированный результат и передайте его дочерним элементам по значению. Пересчет рисков дивергенции.

2. Заморозьте массив tools. Если вашим детям нужны разные наборы tools, вы отказываетесь от совместного использования кеша в блоке tools. Рассмотрите возможность сохранения полного набора tools и использования средств защиты во время выполнения (например, стандартного шаблона вилки «не использовать agent») вместо удаления во время компиляции.

3. Увеличьте общий префикс и минимизируйте суффикс для каждого дочернего элемента. Структурируйте массив сообщений так, чтобы все общие сообщения располагались первыми, а содержимое для каждого дочернего элемента добавлялось в конце. Чередование общего и дочернего контента фрагментирует границу кэша.

4. Используйте постоянные заполнители для содержимого переменных. Если структура сообщения требует ответов на предыдущие tool calls, используйте одинаковые строки заполнителей для всех дочерних элементов, а не фактические (расходящиеся) результаты.

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

По своей сути система форк-agents представляет собой механизм оперативного использования кэша. Это отвечает на вопрос, с которым в конечном итоге сталкивается каждый bundler multi-agent систем: когда кэш дает вам 90% скидку на повторяющиеся префиксы, насколько сильно вы реструктурируете свою архитектуру, чтобы претендовать на эту скидку? Ответ Claude Code: очень далеко.

Prompt Cache Calculator

Calculate fork agent cache sharing savings.