Головна / Статті

Проєктування контексту на практиці: дослідницький агент Claude Agent SDK

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

Видавець: REACTAPP.TOP20 хв читання4633 слів

Джерела: Context Engineering in Practice: Building a Production AI Agent with the Claude Agent SDK

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

Тут фреймворк Write, Select, Compress and Isolate перетворюється на функціональну архітектуру на Python у рамках Claude Agent SDK. Один агент вдосконалюється поступово, з кожним рішенням, і кожен етап постає з одним запитанням: що саме має бачити модель у цю мить, а що може залишитися поза нею? Метою є створення дослідницького агента, який можна запустити, а не просто каталог функцій SDK.

Створення відбувається у десяти кроках:

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

Що створюється

При отриманні запитання агент планує свої дослідження, шукає первинні джерела, доручає детальні дослідження допоміжним елементам, фіксує докази та нарешті пише з посиланнями звіт. Приклад завдання: порівняти основні підходи до довгострокової пам’яті, які використовуються в промислових AI-агентах, та пояснити витрати кожного з них. Для цього потрібні наукові статті, документація до реалізації, докази з репозиторіїв та перевірка посилань, тож це завдання достатньо складне, щоб стати справжнім тестом.

Головний агент зберігає план та складає остаточний звіт. Опрацьований стан роботи зберігається у файлах, а механізм отримання інформації обмежує кількість надходження знань; підагенти розглядають дрібні запитання в межах своїх контекстів. Жоден з цих елементів не є частиною 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 можуть потайки зробити базову версію розумнішою, ніж це передбачено.

Написання: файли scratchpad

Перша зміна додає папку для тимчасових нотаток, яка підтримується файловою системою. У 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. Він нічого не говорить про якість семантичного пошуку, оскільки тестовий фікстур є дуже малим. У реальних умовах такі оцінки має здійснювати сам механізм пошуку, ґрунтуючись на проаналізованих запитах та показниках відтворення інформації, а не на припущеннях щодо форми кінцевого тексту. Виведення даних за межі розмови звільняє простір, проте саме вибір визначає, чи зможе агент все ще знайти потрібні п’ять елементів, коли вони стануть релевантними.

Стиснення: відновлювані довгі сесії

Механізм відбору визначає, що потрапляє всередину, проте тривала сесія поступово накопичує старі обміни повідомленнями. Коли наближається ліміт обсягу контексту, Agent SDK самостійно стискає історію: старіші повідомлення перетворюються на резюме, а останні повідомлення та важливий стан зберігаються. У потоці з’являється 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 токенів як стандартний ліміт; текст над зазначеним обсягом може бути збережений на диску та використаний у контексті як посилання на файл. Цей стандартний ліміт є запасною системою безпеки. Навмисне повернення шляхів та метаданих забезпечує програмі стабільні умови роботи ще до досягнення критичного ліміту.

Сьома діаграма створена в результаті навмисного тесту на збої, який намагається вийти за межі каталогу artefaktів за допомогою ../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),
    }

Кожен варіант зберігає стан розмови: запити, виклики інструментів, результати та відповіді. Для гілки створюється новий ідентифікатор сесії, тоді як оригінальний залишається незмінним. Це підходить для запиту на кшталт „Тепер порівняйте ці результати конкретно для агентів з кодуванням“, оскільки нова гілка може будуватися на основі існуючого напрямку досліджень.

На восьмій ілюстрації показано різницю між режимами: „Продовжити“ розширює поточний напрямок, „Відновити“ — повертає обраний, а „Розділитися“ — створює нову гілку з того самого контрольного пункту.

Файли на диску потребують окремого керування. Гілки, створені шляхом форкування, ділять один і той самий робочий каталог, якщо лише додаток не визначить інший, тож обидві можуть перезаписати plan.md або final_report.md. Захист від зіткнень забезпечується шляхом створення унікального кореня виконання на основі часової мітки UTC та випадкового суфікса, а форк у продакшн-середовищі повинен скопіювати або зберегти дані у новому корені виконання перед тим, як будь-яка з гілок почне записувати. Пам’ятайте, що ідентифікатор сеансу використовується для позначення історії розмови. Це не ідентифікатор транзакції для зовнішніх систем, з якими працює агент, тому бази даних, сховища об’єктів, індекси пошуку та робочі простори кожен потребують власної політики простежуваності та конкурентності. Структура файлів робить це видимим та не приховує його за поведінкою відновлення. Форкування розділяє лише саму розмову, тоді як зовнішній стан залишається спільним, доки робочий простір не буде версіонований або розділений.

Складання агента, створеного за принципами контекстної інженерії

Готова конфігурація поєднує усі чотири операції та не включає додаткових механізмів оркестрації. Функція 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 був невеликим, а відкладене відкриття мало можливості компенсувати витрати на його вибір.

    Що говорять, а що не говорять результати

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

    Що залишається, — це функціональний артефакт із архітектурою контексту, локально виявленим збоєм та чіткими межами оцінки. Розумне наступне запитання стосується якості: чи є матеріал, який записується, вибирається, стискається та ізолюється, справді правильним матеріалом? Для відповіді потрібні оцінки на рівні завдань щодо охоплення доказів, обробки суперечностей, достовірності посилань та відновлення після стиснення. У цьому випадку ізоляція завдань скоротила обсяг даних, отриманих інструментом, проте збільшила кількість токенів та витрати для всього структурного дерева, тому інженерію контексту потрібно оцінювати як систему, а не вважати автоматичною оптимізацією.

    Пов’язана література

  • Створення захищеного компілятора контексту для AI-агентів у .NET 10 — Компілюйте контекст моделей у .NET 10 з використанням міток довіри, перевірок свіжості, функцій маскування даних, карантину для введених елементів, детерміністичних лімітів на токени та маніфесту SHA-256, який можна протестувати.