Главная / Статьи

Практическое применение инжиниринга контекста: исследовательский агент с использованием Claude Agent SDK

Создайте исследовательского агента на Python с использованием Claude Agent SDK, применив шаблоны записи, выбора, компрессии и изоляции, затем сравните его с простым базовым вариантом в проведенных тестах.

Издатель: REACTAPP.TOP20 мин чтения4633 слов

Источники: Context Engineering in Practice: Building a Production AI Agent with the Claude Agent SDK

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

Здесь фреймворк «Написать, Выбрать, Сжать и Изолировать» превращается в рабочую архитектуру на Python в рамках Claude Agent SDK. Один агент улучшается постепенно, с каждым решением, и каждый этап ставит один и тот же вопрос: что должен видеть модель в данный момент, а что может остаться вне ее поля зрения? Цель — создать реально работающего исследовательского агента, а не просто каталог функций SDK.

Создание происходит в десяти шагах:

  1. Определить, что именно нужно создать.
  2. Установить простую базовую версию.
  3. Написание: вынос рабочего состояния вне рамок диалога.
  4. Выбор: формирование ограниченного набора данных для работы.
  5. Сжатие: обеспечение возможности восстановления длинных сессий.
  6. Изоляция: передача конкретных задач на рассмотрение специалистам.
  7. Изоляция результатов работы тяжелых инструментов в среде.
  8. Перенос контекста между сессиями.
  9. Создание агента, разработанного с учетом контекста.
  10. Сравнение двух архитектур.

Что создается

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

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

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

Начните с создания среды и установки фиксированной версии SDK перед запуском любого модуля.

# terminal
python3 -m venv .venv
source .venv/bin/activate
python -m pip install claude-agent-sdk==0.2.139
export ANTHROPIC_API_KEY="your-api-key"

Структура разделяет настройки агента от операций рабочего пространства, пользовательских инструментов, хуков и метрик. Такое разделение важно, поскольку каждая операция в определённом контексте изменяет только один аспект, не требуя создания совершенно нового приложения.

# code/
research_agent/
  config.py       # naive and engineered ClaudeAgentOptions
  hooks.py        # pre-compaction checkpoint
  metrics.py      # message-stream and context measurements
  runner.py       # repeated runs and comparison
  tools.py        # in-process MCP tools
  workspace.py    # scratchpad and bounded retrieval
knowledge/
  memory_approaches.json
tests/
CLAUDE.md

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

Простейший базовый вариант

Самая простая программа на Agent SDK передаёт цикл управления функции query(). Claude получает запрос, решает, требуется ли инструмент, анализирует результат и повторяет процесс до тех пор, пока не получит ответ или не достигнет установленного лимита. Поскольку SDK предоставляет такой цикл, приложению не нужно самостоятельно реализовывать обработку и направление вызовов инструментов.

# research_agent/minimal.py
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query

QUESTION = "Compare approaches for long-term memory in production AI agents."

async def main() -> None:
    options = ClaudeAgentOptions(
        model="sonnet",
        allowed_tools=["WebSearch", "WebFetch"],
        permission_mode="dontAsk",
        max_turns=20,
    )
    async for message in query(prompt=QUESTION, options=options):
        if isinstance(message, ResultMessage):
            print(message.result or "")

asyncio.run(main())

В этой краткой программе скрывается нечто важное: функция query() запускает цикл, но приложение всё равно определяет, что будет передано в него. Базовый вариант кода, доступный в research_agent/config.py, подробно описывает эти варианты выбора.

# research_agent/config.py
def naive_options(*, run_root, server, model, max_budget_usd):
    tools = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"]
    return ClaudeAgentOptions(
        cwd=run_root,
        model=model,
        tools=tools,
        allowed_tools=[*tools, "mcp__research__*"],
        permission_mode="dontAsk",
        mcp_servers={"research": server},
        strict_mcp_config=True,
        setting_sources=[],
        system_prompt={
            "type": "preset",
            "preset": "claude_code",
            "append": NAIVE_PROMPT,
        },
        env={"ENABLE_TOOL_SEARCH": "false"},
        max_turns=20,
        max_budget_usd=max_budget_usd,
    )

В базовой конфигурации отсутствуют инструмент Write, инструмент Agent, настройка agents, хуки и файл с инструкциями по проекту. Руководитель должен завершить всё расследование в рамках одного диалога. Установка параметра ENABLE_TOOL_SEARCH=false также предотвращает отложенную обработку схем MCP. Системное сообщение дополняет правила, указывая агенту вызвать инструмент whole-corpus, сохранять результаты в диалоге и не записывать примечания в рабочее пространство, а также не делегировать задачи.

Инструмент whole-corpus преднамеренно очень прост, поскольку проблема заключается в том, что он возвращает, а не в способе его реализации.

# research_agent/tools.py
@tool(
    "load_knowledge_corpus",
    "Return the entire local memory-research collection. Intended only for the naive baseline.",
    {},
    annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
)
async def load_knowledge(_: dict[str, Any]) -> dict[str, Any]:
    return _text_result(load_corpus(corpus_path))

Когда Claude делает запрос, SDK выводит результат в виде объекта UserMessage, содержащего блок ToolResultBlock. Под «пользователем» здесь имеется в виду протокольная роль, а не сообщение от реального человека. Этот блок добавляется в историю, передаваемую каждому последующему шагу обработки модели, так что один поиск в интернете, три загруженные страницы и весь корпус данных формируют подобную повторяющуюся последовательность.

UserMessage(question)
AssistantMessage(ToolUseBlock: load_knowledge_corpus)
UserMessage(ToolResultBlock: entire corpus)
AssistantMessage(ToolUseBlock: WebSearch + WebFetch)
UserMessage(ToolResultBlock: raw search and page content)
AssistantMessage(final report)

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

На втором рисунке показан наивный подход к сбору контекста: инструкции, определения инструментов, сырые страницы, результаты и история общения собираются в одном окне. Рост объема данных обусловлен политикой приложения, а не каким-либо дефектом в цикле SDK.

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

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

# Captured output: python scripts/smoke_test.py
subtype=success
result=OPENROUTER_OK
models=claude-sonnet-5

Затем полная базовая версия была протестирована три раза, причем результаты были представлены рядом с модифицированными вариантами в итоговом сравнении. Первый тест позволяет оценить фактическое накопление эффекта.

# Captured output: python scripts/show_naive_trial.py ../measurements/comparison.json
Naive trial 1
SDK result: success
Assistant steps: 4
Tool calls: 6
  WebFetch: 3
  WebSearch: 1
  mcp__research__load_knowledge_corpus: 1
  mcp__research__search_knowledge: 1
Final active context: 16,478 tokens
Final tool-result payload: 6,851 tokens
Cumulative tree input: 138,182 tokens
Estimated cost: $0.754

Последние три значения отражают разные показатели. Число 16,478 — это количество активных контекстов запроса, причем 6,851 из этих токенов поступило из результатов работы инструментов. Число 138,182 представляет собой суммарный объем входных данных за все четыре этапа работы ассистента, включая создание и чтение кэша; это не означает, что агент в какой-то момент обрабатывал окно из 138,182 токенов. Это различие становится важным, когда в сравнение включаются подагенты.

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

Запись: файлы рабочей площадки

Первое изменение предусматривает добавление рабочей доски, хранящейся в файловой системе. Файл plan.md содержит описание вопроса, аспектов исследования, уже выполненной работы и того, что осталось сделать. Файл research_notes.md собирает утверждения, доказательства и противоречия, sources.json хранит метаданные цитат, а open_questions.md отображает нерешенные вопросы. Объемные исходные материалы помещаются в папку artifacts.

# research_agent/workspace.py
def initialize_workspace(root: Path, question: str) -> Path:
    workspace = root / "workspace"
    workspace.mkdir(parents=True, exist_ok=True)
    (workspace / "artifacts").mkdir(exist_ok=True)
    (workspace / "checkpoints").mkdir(exist_ok=True)

    for name, template in WORKSPACE_FILES.items():
        path = workspace / name
        if not path.exists():
            path.write_text(template.format(question=question), encoding="utf-8")
    return workspace

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

Третья диаграмма взята из офлайн-скрипта, который инициализирует рабочее пространство и выводит точные названия файлов, которые использует агент в качестве рабочей памяти.

Сессия выполняет ещё одну функцию. В документации к сессии она описывается как хранилище подсказок, вызовов инструментов, результатов их работы и ответов, необходимых для продолжения диалога. Рабочая память, в свою очередь, хранит собственное состояние исследований, составленное приложением. Возобновление сессии восстанавливает ход выполнения задачи, тогда как чтение файла plan.md восстанавливает намеренно поддерживаемый вид результата работы.

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

Выбор: ограниченный набор данных для работы

Объём внешнего состояния может превысить объём данных, который должна обработать одна вызов модели. Выбор решает эту проблему путём загрузки информации в соответствии с этапами: на этапе планирования читается файл plan.md, на этапе синтеза — research_notes.md, а при формировании цитат — sources.json. Инструменты и знания также работают по тому же принципу, поскольку даже их метаданные могут занимать значительную часть входных данных.

Что касается инструментов, специально разработанная система позволяет выполнять поиск инструментов. В документации Anthropic указано, что каталог может содержать до 10 000 инструментов, причем при каждом поиске возвращается до пяти наиболее релевантных, если только это не изменено. Он охватывает инструменты MCP и пользовательские инструменты SDK MCP, поэтому большой каталог может оставаться вне поля зрения, пока Claude не потребует конкретную структуру данных. Согласно документации, при наличии примерно десяти или меньше инструментов загрузка всего сразу часто происходит быстрее, что делает отложенную загрузку инструментов средством для масштабирования, а не гарантированным преимуществом.

Что касается знаний, пользовательский инструмент search_knowledge работает на базе SQLite FTS5. Обработчик принимает запрос и ограничение top_k, после чего возвращает цитируемые фрагменты вместо всего коллектива. Пользовательские инструменты в SDK выполняются через внутренний сервер MCP, поэтому пример остается самодостаточным, но при этом работает через те же интерфейсы, что и внешний сервис.

# research_agent/tools.py
@tool(
    "search_knowledge",
    "Search the local memory-research collection and return only the most relevant cited passages.",
    {
        "type": "object",
        "properties": {
            "query": {"type": "string", "minLength": 1},
            "top_k": {"type": "integer", "minimum": 1, "maximum": 10},
        },
        "required": ["query", "top_k"],
        "additionalProperties": False,
    },
    annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
)
async def search_knowledge(args: dict[str, Any]) -> dict[str, Any]:
    try:
        return _text_result(
            search_corpus(corpus_path, args["query"], args["top_k"])
        )
    except (KeyError, TypeError, ValueError, sqlite3.Error) as error:
        return _error_result(error)

В запросе указано top_k=5, в то время как схема разрешает значение от одного до десяти. Таким образом сохраняется разделение между правилами и строгими ограничениями: качество получения данных можно настроить позже без изменения условий использования инструмента, а запрос на тысячи записей будет отклонён до того, как он сможет перегрузить контекст.

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

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

Сжатие: восстановление длинных сессий

Механизм выбора определяет, что будет принято, однако длительные сессии приводят к накоплению старых сообщений. По мере приближения к лимиту объема контекста библиотека SDK Agent сама сжимает историю обмена: более старые сообщения превращаются в краткое резюме, а недавние сообщения и важные состояния сохраняются. В потоке появляется сообщение типа SystemMessage с подтипом compact_boundary, что позволяет коду для измерений считать количество таких событий.

Общее резюме может упустить именно те факты, от которых зависит исследовательская работа. Поэтому в файле CLAUDE.md задаются правила сохранения, специфичные для исследований: необходимо сохранять первоначальный вопрос, проверенные результаты, открытые вопросы, противоречия, URL источников, ключевые решения и текущий план. Этот файл определяет, что должно быть включено в резюме, и вся информация не обязана передаваться при каждом вызове.

Перед любой компактацией, автоматической или ручной, хук PreCompact сохраняет точку контроля. Когда становится доступен текстовый отчет SDK, он копируется, а метаданные фиксируют причину запуска, сессию, рабочую директорию и источник отчета.

# research_agent/hooks.py

def build_precompact_hook(workspace: Path):
    async def archive_before_compaction(
        input_data: dict[str, Any],
        tool_use_id: str | None,
        context: Any,
    ) -> dict[str, Any]:
        del tool_use_id, context

        checkpoint_dir = workspace / "checkpoints"
        checkpoint_dir.mkdir(parents=True, exist_ok=True)
        timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
        session_id = input_data["session_id"]
        trigger = input_data["trigger"]
        stem = f"{timestamp}-{session_id}-{trigger}"
        transcript = Path(input_data["transcript_path"])

        metadata = {
            "session_id": session_id,
            "trigger": trigger,
            "created_at": datetime.now(timezone.utc).isoformat(),
            "source_transcript": str(transcript),
            "custom_instructions": input_data.get("custom_instructions"),
            "archived": transcript.is_file(),
        }
        if transcript.is_file():
            shutil.copy2(transcript, checkpoint_dir / f"{stem}.jsonl")
        (checkpoint_dir / f"{stem}.json").write_text(
            json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
        )
        return {}

    return archive_before_compaction

На пятом рисунке показано, как процесс сжатия преобразует накопленный текст чата в краткое резюме, соответствующее правилам сохранения, в то время как точки контроля и временные записи остаются независимыми записями в среде.

Временные записи и процесс сжатия решают разные задачи. Временные записи — это данные, которые агент намеренно сохраняет по ходу работы для последующего просмотра. Сжатие представляет собой процесс с потерями при сокращении объема накопленного чата, возникающий из-за чрезмерного увеличения объема активной истории. При наличии обоих механизмов сжатая сессия может обратиться к файлам sources.json или research_notes.md, если в резюме что-то было утеряно.

Во время теста в оффлайн-режиме в хук был передан временный текст транскрипции, что позволило подтвердить три факта: появилась точка контроля, транскрипция была скопирована, а метаданные триггера сохранились. Во время шести тестов сравнения в реальном времени элемент compact_boundary так и не появился, поскольку максимальный объем контекста, наблюдавшийся, составил 34 309 токенов против заявленного диапазона в 200 000 токенов. Таким образом, эксперимент подтверждает, что хук функционирует, но ничего не говорит о качестве процесса сжатия в реальном времени. Резюме должно содержать принятые решения и нерешенные задачи, а временная память — доказательства в формате, пригодном для повторного чтения.

Изоляция: специализированные подагенты

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

# research_agent/config.py
"paper-researcher": AgentDefinition(
    description="Analyzes primary research papers for memory mechanisms and trade-offs.",
    prompt=(
        "Investigate only the assigned paper question. Use primary sources. "
        "Return at most five findings, each with a URL and an explicit limitation."
    ),
    tools=["WebSearch", "WebFetch", "mcp__research__search_knowledge"],
    model=model,
),

В коде сабагенты объявляются с помощью AgentDefinition. Согласно документации Anthropic о сабагентах, каждый из них начинает новый диалог со своим собственным контекстом, сохраняет там результаты промежуточных вызовов инструментов и отправляет обратно на родительский узел только окончательное сообщение. Таким образом, передача данных представляет собой настоящую границу контекста, а не просто конвенцию в рамках существующего окна руководителя.

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

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

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

Изоляция результатов работы сложных инструментов

Некоторые данные должны передаваться между инструментами исключительно в качестве ссылок. Функция materialize_source записывает выбранный документ в папку workspace/artifacts/ и возвращает путь к нему, его размер в байтах, а также краткий превью. Позже Claude может использовать инструменты работы с файлами, чтобы открыть соответствующий участок документа, так что весь документ никогда не остается в результате работы одного инструмента.

# research_agent/workspace.py (full function; use the Gist when publishing)
def materialize_source(corpus_path: Path, workspace: Path, source_id: str) -> dict:
    document = next(
        (item for item in load_corpus(corpus_path) if item["source_id"] == source_id),
        None,
    )
    if document is None:
        raise ValueError(f"unknown source_id: {source_id}")

    path = safe_artifact_path(workspace, f"{source_id}.txt")
    content = (
        f"Title: {document['title']}\n"
        f"URL: {document['url']}\n"
        f"Published: {document['published']}\n\n"
        f"{document['text']}\n"
    )
    path.write_text(content, encoding="utf-8")
    return {
        "path": str(path),
        "characters": len(content),
        "preview": content[:240],
    }

В SDK также предусмотрена аналогичная система защиты для результатов MCP. В документации Anthropic к MCP указано, что объем вывода превышающий 10 000 токенов считается чрезмерным, причем по умолчанию установлен лимит в 25 000 токенов; текст большего объема может быть сохранен на диск и использован в качестве ссылки на файл в контексте. Этот лимит служит резервным механизмом. Предоставление путей и метаданных заранее обеспечивает приложению стабильные условия работы задолго до достижения критического предела.

Седьмая диаграмма получена в результате специального теста на сбой, при котором пытаются выйти за пределы каталога artefактов с помощью пути ../outside.txt. Рабочая среда отклоняет такой путь еще до того, как что-либо будет записано.

Изоляция среды также создаёт границу безопасности. Имя артефакта, контролируемое моделью, никогда не должно превращаться в произвольный путь в файловой системе. Функция safe_artifact_path принимает одно консервативное имя файла и проверяет, что результирующее местоположение остаётся внутри папки workspace/artifacts/. Наблюдаемая ошибка выглядела именно так.

# Captured output: python scripts/demonstrate_failure.py
Blocked artifact path: artifact name must use only letters, numbers, dots, underscores, or hyphens

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

Перенос контекста между сессиями

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

# examples/session_modes.py
def session_options(session_id: str) -> dict[str, ClaudeAgentOptions]:
    return {
        "continue": ClaudeAgentOptions(continue_conversation=True),
        "resume": ClaudeAgentOptions(resume=session_id),
        "fork": ClaudeAgentOptions(resume=session_id, fork_session=True),
    }

Каждый вариант сохраняет состояние диалога: запросы, вызовы инструментов, результаты и ответы. При создании форка присваивается новый идентификатор сессии, в то время как оригинальный остается прежним. Это подходит для запросов вроде «Теперь сравните эти результаты конкретно для агентов-кодеров», поскольку новая ветвь может развиваться на основе существующего направления исследований.

На восьмом рисунке показано сравнение режимов: функция «Продолжить» расширяет текущее направление, функция «Восстановить» возвращает выбранное направление, а функция «Форк» создаёт новую ветвь от того же точки контроля.

Файлы на диске требуют особого обращения. Ветки, созданные с помощью функции fork, делят один рабочий каталог, если только приложение не укажет им другой, поэтому обе могут перезаписать файлы plan.md или final_report.md. Инструмент для запуска избегает конфликтов, формируя уникальную структуру каталога на основе временной метки UTC и случайного суффикса; кроме того, форк в производственной среде должен скопировать или сохранить в версию временные файлы в новую структуру каталога перед тем, как любая из веток начнёт запись. Следует помнить, что идентификатор сессии используется для обозначения истории диалога. Это не идентификатор транзакции для внешних систем, с которыми взаимодействует агент, поэтому базам данных, хранилищам объектов, индексам поиска и рабочим пространствам необходимы собственные правила отслеживания изменений и управления конкурентностью. Структура файлов делает это видимым и не скрывает его за механизмом возобновления работы. Функция fork разделяет лишь сам диалог, в то время как внешнее состояние остаётся общим до тех пор, пока рабочее пространство не будет заархивировано в версию или разделено.

Сборка агента, разработанного с учётом контекста

Готовая конфигурация объединяет все четыре операции и не включает дополнительных механизмов оркестрации. Функция Write представляет собой структурированное рабочее пространство. Функция Select обеспечивает чтение файлов по этапам, задержку применения схем инструментов и извлечение знаний в ограниченных пределах. Функция Compress сочетает сжатие данных с помощью SDK с инструкциями по хранению информации и созданием контрольной точки непосредственно перед сжатием. Функция Isolate позволяет создавать локальные подагенты и сохранять результаты работы инструментов в файлах.

# research_agent/config.py
tools = ["Read", "Write", "Edit", "Glob", "Grep", "WebSearch", "WebFetch", "Agent"]
return ClaudeAgentOptions(
    cwd=run_root,
    model=model,
    tools=tools,
    allowed_tools=[*tools, "mcp__research__*"],
    permission_mode="dontAsk",
    mcp_servers={"research": server},
    strict_mcp_config=True,
    setting_sources=["project"],
    system_prompt={"type": "preset", "preset": "claude_code", "append": ENGINEERED_PROMPT},
    env={"ENABLE_TOOL_SEARCH": "true"},
    hooks={"PreCompact": [HookMatcher(hooks=[build_precompact_hook(workspace)])]},
    agents=research_subagents(model),
    max_turns=30,
    max_budget_usd=max_budget_usd,
)

Установка setting_sources=[“project”] позволяет использовать специфичный для запуска файл CLAUDE.md в качестве части конфигурации, а параметр strict_mcp_config=True ограничивает работу SDK сервером, предоставляемым данным приложением. Значение permission_mode установлено в dontAsk лишь потому, что пример предназначен для автономных, изолированных запусков, и это не является общей рекомендацией по разрешениям. Расширенные процессы утверждения и политики развертывания намеренно не рассматриваются.

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

Финальный запуск основан на ClaudeSDKClient и не использует более простой вспомогательный метод query(). У них один и тот же цикл агента, но клиент предоставляет четко определенные границы ответа и метод get_context_usage() для завершения работы. Именно требования инструментализации побудили к такому выбору, и архитектура осталась прежней.

Первая попытка реализации, основанная исключительно на промптах, выявила еще одну проблему в производственной среде. Все три запуска SDK завершились, но только один сгенерировал файл final_report.md; два ведущих агента вернулись, хотя делегированная работа фактически осталась незавершенной. Следовательно, чистое завершение работы SDK не означает завершения задачи приложения. Решением стало сделание вызовов подагентов явно синхронными и предоставление контроллеру собственного контракта завершения: он ищет подробный отчет, содержащий как минимум два URL-адреса, и разрешает выполнение одного конкретного запроса на устранение ошибки в рамках одной сессии.

# research_agent/runner.py
while True:
    async for message in client.receive_response():
        metrics.observe(message)

    report_path = workspace / "final_report.md"
    if mode == "naive" or _report_meets_contract(report_path):
        break
    if metrics.completion_retries >= MAX_COMPLETION_RETRIES:
        break

    metrics.completion_retries += 1
    await client.query(COMPLETION_REPAIR_PROMPT)

Результат с ошибкой, полученный только при использовании промпта, остается в файле measurements/initial-prompt-only-comparison.json, поскольку контроллер должен проверять стабильные результаты, а не выводить их на основе окончательного состояния модели.

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

# Captured output: python -m unittest discover -s tests -q
----------------------------------------------------------------------
Ran 12 tests in 0.048s

OK

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

Сравнение двух архитектур

Утверждения об архитектуре должны превращаться в числа. RunMetrics отслеживает поток сообщений SDK, удаляет дублирующиеся шаги ассистента на верхнем уровне с использованием идентификаторов сообщений, подсчитывает количество вызовов инструментов и каждой границы сжатия, определяет количество символов в результатах работы инструментов и запрашивает окончательный снимок контекста. Общее количество использования модели во всей иерархии агентов, включая субагентов, указывается в полях ResultMessage.model_usage, в то время как total_cost_usd представляет собой локальную оценку и не является счетом-фактурой.

# research_agent/metrics.py
if isinstance(message, ResultMessage):
    self.query_results += 1
    self.session_id = message.session_id
    self.result_subtype = message.subtype
    result = message.result or ""
    self.sdk_success = (
        message.subtype == "success"
        and bool(result.strip())
        and "not logged in" not in result.lower()
    )
    self.estimated_cost_usd = (
        (self.estimated_cost_usd or 0.0) + (message.total_cost_usd or 0.0)
    )
    self.result_usage = message.usage
    self.model_usage = self._merge_model_usage(self.model_usage, message.model_usage)
    self.total_tree_input_tokens = self._tree_input_tokens(self.model_usage)

if isinstance(message, SystemMessage) and message.subtype == "compact_boundary":
    self.compactions += 1

Каждая архитектура тестировалась три раза независимо по одному и тому же вопросу с использованием идентичного SDK, псевдонима модели, поставщика и настроек локальных знаний. Записи за каждый запуск и полученные средние значения хранятся в файле measurements/comparison.json, а небольшой скрипт отображения генерирует компактную таблицу на основе этого файла.

# terminal
python scripts/show_comparison.py ../measurements/comparison.json

На десятой диаграмме показаны средние значения для трех очков и выявлен реальный компромисс. Метод изоляции сократил объем итоговых данных, которые видел лидер, однако дерево подагентов потребовало гораздо больше токенов и привело к более высокой оценке затрат. Ниже приведен результат выполнения скрипта отображения.

# Captured output: python scripts/show_comparison.py ../measurements/comparison.json
Measured comparison - 3 runs per architecture
Metric                               Naive      Engineered
Artifact success                       3/3             3/3
SDK success                            3/3             1/3
Mean tree input                    102,210         737,036
Mean peak context                   17,184          32,668
Final tool-result tokens             7,351           6,202
Mean subagents                           0               3
Mean estimated cost                 $0.663          $3.270
Compactions                              0               0

Успех бывает двух видов. Успех в формате «артефакт» означает соблюдение условий выходного контракта архитектуры: для простого агента это отчет с указанием по меньшей мере двух разных URL, а для специально разработанного агента — стабильный файл final_report.md длиной 500 и более символов с двумя URL. Все шесть запусков достигли этого результата.

Для успешной работы SDK необходимо, чтобы окончательный подтип ResultMessage был равен success. В простых тестах это происходило в трех из трех случаев, в сложных — в одном из трех. Два сложных теста сгенерировали отчеты, но завершились с кодом error_max_budget_usd; при этом в одном из них использовался единственный запрос на ремонт от контроллера. В примере с бюджетом от Anthropic объясняется, что проверка расходов происходит после вызова API, поэтому тест может завершить создание своего результата и все еще соответствовать установленной оценке до того, как поступит окончательный результат. Рассматривайте этот лимит как защитный механизм, а не как сигнал о завершении работы приложения.

Цифры:

  • Количество получаемой информации сократилось. Среднее количество видимых символов результата работы инструмента составляло 28 572 в исходных условиях и 23 428 после их изоляции, что соответствует снижению на 18,0%; доля результата работы инструмента в итоговом отчете о контексте уменьшилась с 7 351 до 6 202 токенов (на 15,6%). В каждом проведенном эксперименте участвовали ровно три подагента, и их промежуточные поиски никогда не влияли на траекторию развития основного агента.
  • Общая стоимость резко возросла. Общее количество входных данных в структуре дерева изначально составляло в среднем 102 210 токенов, а затем — 737 036 токенов, что соответствует увеличению на 621,1%. Максимальное количество входных данных, связанных с основным агентом и контекстом, составляло в среднем 17 184 токена в начале и 32 668 токенов в конце, что на 90,1% больше; оценка затрат SDK выросла с 0,663 доллара до 3,270 долларов, то есть на 393,4%.
  • Активность инструментов также возросла: около 30,3 вызовов во всей структуре по сравнению с шестью в базовом сценарии. Для такой ограниченной по объему задачи три специалиста, их схемы и процессы передачи задач между ними стоили дороже, чем ресурсы, сэкономленные для руководителя проекта.
  • Сжатие так и не применялось, поскольку при каждой обработке сообщалось о отсутствии границ. Это не означает, что сжатие с учетом сохранения данных неэффективно; это указывает на то, что объем задачи никогда не приближался к порогу в 200 000 токенов. Эксперимент также не позволяет оценить роль поиска инструментов, поскольку каталог MCP был небольшим, а задержка обнаружения инструментов практически не давала возможности компенсировать свои дополнительные затраты.

    Что говорят и чего не говорят результаты

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

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

    Связанные материалы

  • Создание защищённого компилятора контекста для ИИ-агентов в .NET 10 — Компилирование контекста модели в .NET 10 с метками доверия, проверками свежести, маскировкой данных, карантином для внедрения угроз, детерминированными лимитами токенов и манифестом SHA-256, который можно протестировать.