Inżynieria kontekstu w praktyce: Agent badawczy z Claude Agent SDK
Stwórz agent badawczy w Pythonie przy użyciu Claude Agent SDK, wykorzystując wzorce write, select, compress i isolate, a następnie porównaj go z prostym modelem bazowym w przeprowadzonych testach.
Źródła: Context Engineering in Practice: Building a Production AI Agent with the Claude Agent SDK
Agenat badawczy rzadko napotyka jeden trudny prompt. Problemem jest ich nagromadzenie. Podczas jednego dochodzenia kontekst wypełnia się instrukcjami od użytkownika, planem, zapytaniami wyszukiwawczymi, pobraanymi stronami, schematami narzędzi, metadanymi źródłowymi, nieukończonymi twierdzeniami, raportami od asystentów oraz wcześniejszymi interakcjami. Gdy to wszystko pozostaje aktywne, dobre dowody muszą konkurować o uwagę agenta z jego własnymi nierozwiązanymi zadaniami.
Tutaj ramy Write, Select, Compress i Isolate stają się funkcjonalną architekturą w Pythonie w ramach Claude Agent SDK. Jeden agent ulepsza się krok po kroku, a każda granica rodzi to samo pytanie: co model musi zobaczyć w danym momencie, a co może pozostać poza tym? Celem jest agent badawczy, który można uruchomić, a nie katalog funkcji SDK.
Budowa przebiega w dziesięciu krokach:
- Zdefiniuj, co ma zostać stworzone.
- Ustal prosty punkt odniesienia.
- Pisanie: przeniesienie stanu pracy poza rozmowę.
- Wybór: skompletowanie ograniczonego zestawu zadań do wykonania.
- Kompresja: umożliwienie przywrócenia długich sesji.
- Izolacja: delegowanie skoncentrowanych badań.
- Izolacja wyników pracy intensywnych narzędzi w środowisku.
- Prowadzenie kontekstu między sesjami.
- Skompilowanie agenta zaprojektowanego z myślą o kontekście.
- Porównanie obu architektur.
Co zostaje stworzone
Na podstawie zadania agent planuje swoje badania, przeszukuje źródła pierwotne, przekazuje szczegółowe dochodzenia pomocnikom, rejestruje dowody i ostatecznie pisze raport z odniesieniami. Przykładowe zadanie to porównanie głównych podejść do długoterminowej pamięci stosowanych w agentach sztucznej inteligencji produkcyjnych oraz wyjaśnienie kosztów każdego z nich. Wymaga artykułów naukowych, dokumentacji implementacyjnej, dowodów z repozytoriów oraz weryfikacji odniesień, więc jest na tyle złożone, by stanowić prawdziwy test.
Główny agent przechowuje plan i sporządza ostateczną syntezę. Opracowany stan roboczy znajduje się w plikach, a mechanizm pobierania ogranicza ilość wpływającej wiedzy, natomiast podagenty zajmują się małymi zagadnieniami w ramach swoich własnych kontekstów. Nic z tego nie jest wbudowane w SDK. SDK dostarcza model oraz pętlę narzędziową, a aplikacja decyduje, gdzie ma znajdować się stan oraz ile informacji może przekroczyć każdą granicę.
Pierwszy rysunek przedstawia początkowy system: jeden główny agent połączony z narzędziami, wiedzą, notatnikiem oraz podagentami. Pozostałe elementy konstrukcji zmieniają sposób przepływu informacji pomiędzy tymi połączeniami.
Najpierw utwórz środowisko i zainstaluj określoną wersję SDK, zanim uruchomisz jakikolwiek moduł.
# 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"
Rozkład utrzymuje konfigurację agenta oddzielnie od operacji w przestrzeni roboczej, dostosowanych narzędzi, hooków oraz pomiarów. To rozdzielenie jest ważne, ponieważ każda operacja w danym kontekście zmieni tylko jedną granicę, bez konieczności tworzenia zupełnie nowej aplikacji.
# 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
Inżynieria kontekstu zaczyna się od przydzielenia odpowiedzialności. Rozmowa przechowuje aktualną decyzję, natomiast pliki, narzędzia, systemy wyszukiwania oraz podagenty przechowują wszystko to, co w danym momencie nie musi być aktywne.
Prosty punkt wyjścia
Najprostszy program Agent SDK przekazuje pętlę sterującą funkcji query(). Claude otrzymuje zapytanie, decyduje, czy potrzebne jest jakieś narzędzie, obserwuje wynik i powtarza proces, aż uzyska odpowiedź lub osiągnie ustalony limit. Ponieważ SDK zapewnia tę pętlę, aplikacja nie musi sama implementować mechanizmów analizy i wysyłania żądań do narzędzi.
# 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())
To krótkie programy kryją coś: query() uruchamia pętlę, ale aplikacja nadal decyduje, co do niej trafi. Podstawowy kod wykonywalny w research_agent/config.py opisuje te możliwości dokładnie.
# 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,
)
W wersji bazowej nie ma narzędzia Write, narzędzia Agent, ustawienia agents, żadnego hooka ani pliku z instrukcjami projektu. Lider musi przeprowadzić całe śledztwo w ramach jednej rozmowy. Ustawienie ENABLE_TOOL_SEARCH=false zapobiega również odroczeniu wykonywania schematów MCP. Komenda systemowa uzupełnia zasady, polecając agencie użyć narzędzia whole-corpus, przechowywać wyniki w ramach rozmowy oraz ani nie zapisywać notatek w przestrzeni roboczej, ani niczego nie delegować.
Narzędzie whole-corpus jest celowo proste, ponieważ problem leży w tym, co zwraca, a nie w sposobie jego implementacji.
# 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))
Gdy Claude o to prosi, SDK wysyła wynik w postaci UserMessage zawierającego ToolResultBlock. „Użytkownik” w tym przypadku to rola protokołu, a nie kolejna wiadomość od osoby. Ten blok zostaje dołączony do historii przekazywanej na każdym kolejnym kroku modelu, dzięki czemu jedno wyszukiwanie w sieci, trzy pobraane strony oraz całe korpus tworzą taką sekwencję powtarzającą się w kółko.
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)
Sama sekwencja ta nie ma w sobie nic złego. Jest naiwna, ponieważ aplikacja nigdy nie przekształca surowych wyników w uporządkowany stan zewnętrzny, ani ich nie usuwa z aktywnej ścieżki.
Druga ilustracja pokazuje naiwny kontekst gromadzący instrukcje, definicje narzędzi, surowe strony, wyniki badań oraz historię rozmów w jednym oknie. Ten wzrost pochodzi z polityki aplikacji, a nie z jakiegokolwiek błędu w pętli SDK.
Dla większości konfiguracji Agent SDK wyszukiwanie narzędzi jest włączone domyślnie, z udokumentowanymi wyjątkami. Dlatego uczciwy punkt odniesienia musi je celowo wyłączyć, jeśli eksperyment ma na celu wcześniejsze załadowanie całej listy dostępnych narzędzi. Jako element kontroli, naiwny projekt jest przydatny, ponieważ eliminuje wszystkie decyzje związane z zarządzaniem kontekstem, które będą sprawdzane w późniejszych krokach.
Zanim poświęcono czas i zasoby na porównanie, przeprowadzono test wstępny, wysyłając jedną ograniczoną prośbę przez tę samą konfigurację. Uzyskany wynik pokazał, że autoryzacja, routowanie oraz rozwiązywanie modeli funkcjonowały prawidłowo.
# Captured output: python scripts/smoke_test.py
subtype=success
result=OPENROUTER_OK
models=claude-sonnet-5
Pełna linia bazowa została następnie przeprowadzona trzy razy, a wyniki umieszczono obok eksperymentów modyfikowanych w końcowym porównaniu. Pierwszy eksperyment sprawia, że akumulacja staje się konkretna.
# 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
Ostatnie trzy wartości mierzą różne rzeczy. Liczba 16 478 to ostateczny aktywny kontekst leadów, a 6 851 z tych tokenów pochodziło z wyników narzędzi. Liczba 138 182 to łączna ilość danych wprowadzonych podczas wszystkich czterech kroków asystenta, uwzględniająca tworzenie i odczyty z pamięci cache, więc nie oznacza to, że agent miał w danym momencie okno zawierające 138 182 tokenów. Różnica ta staje się istotna, gdy do porównania dołączają podagenty.
Agent naiwny stanowi sprawiedliwą grupę kontrolną tylko wtedy, gdy takie udogodnienia jak wyszukiwanie narzędzi są wyraźnie wyłączone. W przeciwnym razie domyślne ustawienia SDK mogą potajemnie sprawić, że grupa bazowa będzie bardziej inteligentna, niż zakładano.
Pisanie: pliki scratchpad
Pierwsza zmiana polega na dodaniu notatnika wspieranego systemem plików. Plik plan.md zawiera pytanie, wymiary badania, to, co zostało zrobione, oraz to, co pozostaje do zrobienia. Plik research_notes.md gromadzi twierdzenia, dowody i sprzeczności, sources.json przechowuje metadane cytowań, a open_questions.md pokazuje nierozwiązane kwestie. Obfite materiały źródłowe trafiają do folderu artefaktów.
# 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
Ponieważ te pliki są trwałe, agent może je aktualizować po każdym kroku badawczym i odczytywać tylko ten, który jest mu potrzebny dalej. Awaria lub zdarzenie kompresji pozostawiają przygotowany plan oraz dowody nietknięte, dzięki czemu osoba może bezpośrednio przeczytać aktualny stan, nie musząc go składać z transkrypcji.
Trzeci rysunek pochodzi ze skryptu offline, który inicjuje przestrzeń roboczą i wyświetla dokładne pliki, których używa zaprojektowany agent jako swoją tablicę roboczą.
Sesja pełni jeszcze jedną funkcję. Dokumentacja sesji opisuje ją jako miejsce przechowywania promptów, wywołań narzędzi, wyników ich działania oraz odpowiedzi niezbędnych do kontynuowania rozmowy. Tablica robocza natomiast przechowuje własny, przygotowany przez aplikację stan badań. Przywrócenie sesji przywraca ścieżkę postępu, natomiast odczyt pliku plan.md przywraca celowo utrzymywany widok zadania.
To rozróżnienie zapobiega dwóm powszechnym błędom. Jeśli transkrypcja jest jedyną bazą danych, odzyskanie informacji zależy od coraz dłuższej historii działań. Jeśli notatnik tymczasowy stanie się zbiorem transkrypcji, to samo problem z kontekstem powstaje na dysku. Dlatego każdy plik ma wąskie schemat struktury i zawiera wnioski, dowody oraz niedokończone prace, a nie skopiowane rozmowy. Historia sesji odnotowuje, jak agent doszedł do danego stanu, natomiast notatnik tymczasowy przechowuje uporządkowany zbiór informacji niezbędny do podjęcia decyzji o dalszych krokach.
Wybór: ograniczony zbiór danych do pracy
Zewnętrzny stan może przewyższyć objętość, jaką powinna pochłonąć pojedyncza wywołanie modelu. Wybór rozwiązuje ten problem poprzez ładowanie danych według fazy: planowanie odbywa się na podstawie pliku plan.md, synteza – z pliku research_notes.md, a tworzenie cytatów – z pliku sources.json. Narzędzia i wiedza działają według tej samej zasady, ponieważ nawet ich metadane mogą zajmować znaczną część treści zapytania.
Z punktu widzenia narzędzi, zaprojektowana konfiguracja umożliwia ich wyszukiwanie. Dokumentacja Anthropic wskazuje, że katalog może zawierać nawet 10 000 narzędzi, przy czym przy każdej wyszukiwce zwracane są maksymalnie pięć istotnych wyników, chyba że zostanie to zmienione. Obejmuje ona narzędzia MCP oraz niestandardowe narzędzia SDK typu MCP, dzięki czemu duży katalog może pozostać poza kontekstem, dopóki Claude nie zażąda określonego schematu. Według dokumentacji, przy mniej niż dziesięciu narzędziach ładowanie ich wszystkich z góry jest często szybsze, co sprawia, że opóźnione ładowanie narzędzi służy raczej skalowalności niż gwarantowanym korzyściom.
Jeśli chodzi o wiedzę, niestandardowe narzędzie search_knowledge opiera się na SQLite FTS5. Ten mechanizm przyjmuje zapytanie oraz ograniczoną liczbę elementów określoną przez top_k i zwraca cytowane fragmenty zamiast całej kolekcji. Niestandardowe narzędzia w SDK działają poprzez serwer MCP wewnątrz procesu, dzięki czemu przykład pozostaje samodzielny, ale przekracza te same ograniczenia co usługa zewnętrzna.
# 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)
Zapytanie wymaga wartości top_k=5, podczas gdy schemat dopuszcza wartości od jednej do dziesięciu. W ten sposób polityka i sztywne ograniczenia są oddzielone: jakość pozyskiwanych danych można dostosować później bez zmiany umowy narzędzia, a błędne zapytanie dotyczące tysięcy rekordów zostanie odrzucone, zanim zdąży zasypać kontekst.
Czwarty rysunek przedstawia odczyty plików, wyszukiwanie w narzędziu oraz pozyskiwanie wiedzy jako trzy sposoby wyboru różnych rodzajów kontekstu, które wszystkie prowadzą do tego samego rezultatu: ograniczonego zbioru danych do użycia w danym kroku.
Test offline uruchamia rzeczywisty kod wyszukiwania i sprawdza, czy liczba pięć stanowi górny limit, czy każdy zwrócony element zachowuje swoją oryginalną URL, oraz czy nieprawidłowe wartości top_k są odrzucane. Nie mówi nic o jakości semantycznego odzyskiwania informacji, ponieważ ten przykładowy scenariusz jest bardzo skromny. W warunkach produkcyjnych taką ocenę powinien dokonać mechanizm wyszukiwania, opierając się na sprawdzonych zapytaniach i metrykach odzyskiwania danych, zamiast zgadywać ją na podstawie wyglądu końcowego tekstu. Wykluczanie danych z rozmowy zwalnia miejsce, ale to selekcja decyduje o tym, czy agent nadal będzie w stanie znaleźć właściwe pięć elementów, gdy staną się one istotne.
Kompresja: możliwość przywrócenia długich sesji
Selekcja decyduje o tym, co zostanie przyjęte, ale długa sesja powoduje gromadzenie się starych wymian. Gdy zbliża się limit kontekstu, Agent SDK sam kompresuje historię: starsze wiadomości zamieniają się w streszczenie, a najnowsze odpowiedzi oraz ważny stan są zachowywane. W strumieniu pojawia się SystemMessage o podtypie compact_boundary, co umożliwia kodowi pomiarowemu liczenie takich zdarzeń.
Ogólne streszczenie może pomijać dokładnie te fakty, od których zależy badanie. Dlatego projekt umieszcza specyficzne dla badań zasady przechowywania w pliku CLAUDE.md: należy zachować oryginalne pytanie, zweryfikowane wyniki, otwarte pytania, sprzeczności, adresy URL źródeł, kluczowe decyzje oraz obecny plan. Ten plik określa, co powinno znaleźć się w streszczeniu, dzięki czemu cała notatnik nie musi być przekazywana przy każdym wezwaniu.
Zanim nastąpi jakakolwiek kompresja, automatyczna czy ręczna, haker PreCompact zapisuje punkt kontrolny. Gdy dostępna staje się transkrypcja SDK, jest ona skopiowana, a metadane odnotowują sygnał wyzwalający, sesję, katalog roboczy oraz źródło transkrypcji.
# 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
Piąty rysunek pokazuje, jak proces kompresji skraca zgromadzoną rozmowę do streszczenia, które przestrzega zasad zachowania danych, podczas gdy punkty kontrolne i notatniki pozostają niezależnymi zapisami w środowisku.
Notatniki i kompresja służą różnym celom. Notatnik to informacje, które agent celowo zapisuje w trakcie pracy, uporządkowane dla późniejszego odczytu. Kompresja to proces redukcji rozmiaru zgromadzonej rozmowy, który ma miejsce, gdy aktywna historia stała się zbyt duża. Dzięki obu tym elementom sesja skompresowana może odwołać się do pliku sources.json lub research_notes.md, jeśli streszczenie utraciło jakieś informacje.
Test offline dostarczył hookowi tymczasowy transkrypt i potwierdził trzy rzeczy: pojawił się punkt kontrolny, transkrypt został skopiowany, a metadane wyzwalacza przetrwały. W sześciu próbach porównania w trybie rzeczywistym compact_boundary nigdy się nie pojawił, ponieważ największy kontekst widziany wynosił 34 309 tokenów, podczas gdy zakładana była okno 200 000 tokenów. Dlatego eksperyment potwierdza, że hook działa, ale nic nie mówi o tym, na jakim poziomie jest efektywna kompresja w trybie rzeczywistym. Streszczenie powinno zawierać decyzje oraz niezakończone zadania, a tablica robocza powinna przechowywać dowody w formie łatwej do odczytania.
Izolacja: skoncentrowane podagenty
Niezależne linie dochodzeń są łatwiejsze w zarządzaniu, gdy nie podążają tą samą, coraz bardziej rozwijającą się ścieżką. Po tym, jak główny agent opracuje plan, trzej określeni podagenti zajmują się analizą dokumentów, sprawdzaniem dowodów przechowywanych w repozytorium oraz weryfikacją cytatów. Każda z tych definicji ogranicza zakres zadań, używane narzędzia oraz sposób przekazywania informacji pomiędzy agentami.
# 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,
),
W kodzie podagenty są deklarowane za pomocą AgentDefinition. Zgodnie z dokumentacją Anthropic dotyczącą podagentów, każdy z nich rozpoczyna nową rozmowę ze swoim własnym kontekstem, przechowuje tam swoje pośrednie wywołania narzędzi oraz wyniki, a do agenta nadrzędnego wysyła jedynie wiadomość końcową. Dlatego przeniesienie kontroli stanowi rzeczywistą granicę kontekstu, a nie tylko konwencję w ramach istniejącego okna agenta nadrzędnego.
Szósty rysunek pokazuje, jak każdy specjalista otrzymuje określone zadanie oraz nowe okno. Agent nadrzędny widzi zwięzłe przeniesienie kontroli, a nie żadne wyszukiwania, pobraane strony ani kroki ekstrakcji, które je spowodowały.
Izolacja ma swoje koszty. Trzy podagenty mogą ponownie odkryć te same źródła, zużyć więcej tokenów i zastosować sprzeczne terminologie. Aby temu zapobiec, instrukcje wymagają podania głównych adresów URL, określenia ograniczeń oraz nie więcej niż pięciu wyników. Sprawdzacz faktów widzi jedynie sporne twierdzenia, a nie całe zlecenie, co zapewnia przejrzystość jego działań i zmniejsza ryzyko, że potajemnie przepisze syntezę.
Rodzic musi być w stanie ocenić to, co otrzymuje. Pięć linijek bez identyfikatorów źródeł jest zwięzłe, ale niewydajne, natomiast pełny transkrypt rozmowy eliminuje wszelkie granice. Umowa przewiduje dostarczenie wyników, cytatów, ograniczeń wersji oraz ograniczeń ogólnych – to właśnie te dane są potrzebne kierownikowi przy porównywaniu różnych twierdzeń. Samo ukrywanie rozmowy nie wystarcza, ponieważ izolacja działa tylko wtedy, gdy wynik zawiera dowody oraz informacje o niepewnościach.
Izolacja wyników generowanych przez zaawansowane narzędzia
Pewne treści powinny przechodzić przez granice narzędzia wyłącznie jako odniesienia. Funkcja materialize_source zapisuje wybrany dokument do katalogu workspace/artifacts/ i zwraca jego ścieżkę, rozmiar w bajtach oraz krótkie wideo przedpremierowe. Później Claude może otworzyć odpowiedni zakres za pomocą narzędzi do plików, dzięki czemu cały dokument nigdy nie trafia do wyniku jednego narzędzia.
# 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 posiada podobne zabezpieczenie dla wyników MCP. Dokumentacja MCP firmy Anthropic wskazuje, że długość wyjścia powyżej 10 000 tokenów jest problematyczna, a domyślnym limitem ustawiono 25 000 tokenów; tekst o zbyt dużej długości może zostać zapisany na dysku i użyty jako odniesienie do pliku w kontekście. Ten domyślny limit pełni rolę zapory bezpieczeństwa. Zamierzone zwracanie ścieżek i metadanych zapewnia aplikacji spójne warunki działania na długo przed osiągnięciem limitu awaryjnego.
Siódmy przykład pochodzi z celowego testu awaryjnego, który ma na celu wydostanie się z katalogu artefaktów za pomocą ../outside.txt. Przestrzeń robocza odrzuca tę ścieżkę, zanim cokolwiek zostanie zapisane.
Izolacja środowiska tworzy również granicę bezpieczeństwa. Nazwa artefaktu kontrolowanego przez model nie może w żadnym wypadku stać się dowolną ścieżką w systemie plików. Funkcja safe_artifact_path przyjmuje jedną konserwatywną nazwę pliku i sprawdza, czy ostateczna ścieżka pozostaje w obrębie katalogu workspace/artifacts/. Zaobserwowany błąd wyglądał w ten sposób.
# Captured output: python scripts/demonstrate_failure.py
Blocked artifact path: artifact name must use only letters, numbers, dots, underscores, or hyphens
Sprawdzenie nie zawiera żadnych informacji na temat izolacji w środowisku sandbox systemu operacyjnego ani izolacji wielu użytkowników. Potwierdza jedną wąską zasadę: argument narzędzia nie może wykorzystywać metody przechodzenia po ścieżkach, aby zapisać dane poza katalogiem artefaktów. Rzeczywiste implementacje nadal wymagają uprawnień na poziomie procesu oraz oddzielnych przestrzeni roboczych dla każdego użytkownika lub zlecenia. Zastąpienie dużego ładunku danych jedynie ścieżką pozwala zaoszczędzić kontekst tylko wtedy, gdy ścieżka jest ograniczona, a agent może później pobrać tylko określoną jej część.
Przenoszenie kontekstu między sesjami
Długotrwałe zadania trwają dłużej niż procesy oraz okna kontekstu. SDK oferuje trzy opcje: kontynuowanie najnowszej rozmowy, wznowienie konkretnej sesji za pomocą ID lub utworzenie gałęzi z danej sesji. Opcja kontynuacji służy do dalszej pracy nad aktualnym zadaniem, wznowienie to wyraźna próba przywrócenia stanu, natomiast utworzenie gałęzi nadaje się do dalszych działań, które powinny odziedziczyć wcześniejszy kontekst bez zmiany przyszłości oryginalnej sesji.
# 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),
}
Każda opcja zachowuje stan rozmowy: pytania, wywołania narzędzi, wyniki oraz odpowiedzi. Dla gałęzi tworzonej za pomocą „fork” przydzielany jest nowy identyfikator sesji, podczas gdy oryginalna pozostaje bez zmian. To odpowiada prośbom takim jak „Teraz porównaj te wyniki specjalnie dla agentów kodujących”, ponieważ nowa gałąź może budować na istniejącej ścieżce badań.
Osiąg ósmy przedstawia porównanie różnych trybów: „continue” rozszerza obecną ścieżkę, „resume” przywraca wybraną, a „fork” uruchamia nową gałąź od tego samego punktu kontrolnego.
Pliki na dysku wymagają osobnego sposobu obsługi. Gałęzie utworzone przez fork dzielą się katalogiem roboczym, chyba że aplikacja wyznaczy im inny, więc obie mogą nadpisać pliki plan.md lub final_report.md. Narzędzie unika kolizji, tworząc unikalną bazę dla danej sesji na podstawie daty w formacie UTC oraz losowego sufiksu, a fork produkcyjny powinien skopiować lub zarchiwizować pliki tymczasowe do nowej bazy przed zapisem jakiejkolwiek gałęzi. Należy pamiętać, że identyfikator sesji służy do oznaczania historii rozmowy. Nie jest to identyfikator transakcji dla zewnętrznych systemów, z którymi pracuje agent, dlatego bazy danych, magazyny obiektów, indeksy wyszukiwania oraz przestrzenie robocze wymagają osobnych zasad śledzenia zmian i zarządzania równoczesnością. Struktura plików sprawia, że jest to widoczne i nie jest ukrywane za mechanizmami kontynuacji pracy. Fork oddziela jedynie treść rozmowy, natomiast stan zewnętrzny pozostaje wspólny, dopóki przestrzeń robocza nie zostanie zarchiwizowana lub podzielona.
Konstruowanie agenta zaprojektowanego z myślą o kontekście
Gotowa konfiguracja łączy wszystkie cztery operacje i nie dodaje żadnego dodatkowego frameworku do orkiestracji. Funkcja Write to ustrukturyzowana przestrzeń robocza. Funkcja Select polega na czytaniu plików w fazach, wykorzystywaniu schematów narzędzi z odroczoną realizacją oraz pobieraniu wiedzy w określonych granicach. Funkcja Compress łączy skompresowanie SDK z instrukcjami przechowywania danych oraz punktem kontrolnym utworzonym tuż przed kompresją. Funkcja Isolate oznacza tworzenie subagentów w określonym zakresie oraz przechowywanie wyników działania narzędzi w plikach.
# 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,
)
Ustawienie setting_sources=[„project“] umożliwia temu specyficznemu dla wykonywania plikowi CLAUDE.md dołączenie do konfiguracji, a strict_mcp_config=True ogranicza SDK do serwera dostarczanego przez tę aplikację. permission_mode ma wartość dontAsk wyłącznie dlatego, że przykład dotyczy niezawodnionych, izolowanych wykonań, i nie stanowi ogólnej zalecenia dotyczącego uprawnień. Zaawansowane procesy zatwierdzania oraz polityki wdrażania są celowo pomijane.
Na dziewiątym rysunku przedstawiona architektura reguluje cztery obszary: trwałe zapisy, selektywne odczyty, kompresję rozmowy oraz izolowane wykonywanie. Główny model otrzymuje jedynie skoncentrowany, aktywny kontekst.
Ostatni procesor opiera się na ClaudeSDKClient i pomija lżejszy pomocnik query(). Dzielą one ten sam cykl agenta, ale klient zapewnia wyraźną granicę odpowiedzi oraz zamknięcie poprzez wywołanie get_context_usage(). Decyzja ta była podyktowana potrzebą instrumentacji, a architektura pozostała bez zmian.
Pierwsza próba opracowania rozwiązania, oparta wyłącznie na instrukcjach, ujawniła kolejną granicę w środowisku produkcyjnym. Wszystkie trzy wykonywania SDK zakończyły się, ale tylko jedno stworzyło plik final_report.md; dwa główne agenty zwróciły się, podczas gdy powierzone im zadania w praktyce nie zostały ukończone. Dlatego czyste zakończenie SDK nie oznacza, że zadanie aplikacji jest ukończone. Rozwiązaniem było uczynienie wywołań podagentów wyraźnie synchronicznymi oraz zapewnienie kontrolerowi własnego kontraktu zakończenia: ten szuka obszernego raportu zawierającego co najmniej dwa adresy URL i zezwala na jedno skupione zapytanie naprawcze w ramach tej samej sesji.
# 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)
Nieudany wynik oparty wyłącznie na prośbie pozostaje w pliku measurements/initial-prompt-only-comparison.json, ponieważ kontroler powinien weryfikować trwałe wyniki, a nie wnioskować je na podstawie końcowego stanu modelu.
Zestaw testów offline obsługuje wszystko, co nie wymaga modelu: oba zestawy opcji, tworzenie przestrzeni roboczej, ograniczone wyszukiwanie, odrzucanie nieprawidłowych ścieżek, punkty kontrolne kompresji oraz rozliczanie wiadomości. To właśnie zostało zgłoszone w podanym środowisku.
# Captured output: python -m unittest discover -s tests -q
----------------------------------------------------------------------
Ran 12 tests in 0.048s
OK
W tym momencie framework jest gotowy do użycia: każde czasownikowe działanie jest powiązane z określoną granicą, która ma swój własny kod, osobę odpowiedzialną za jej stan oraz niezmiennik, który można sprawdzić w ramach testu.
Porównanie obu architektur
Stwierdzenia dotyczące architektury powinny przekształcić się w liczby. RunMetrics obserwuje strumień wiadomości SDK, usuwa duplikaty kroków asystenta na najwyższym poziomie za pomocą identyfikatorów wiadomości, liczy każde wezwanie narzędzia oraz każdą granicę kompresji, liczy znaki w wynikach narzędzi i żąda końcowego zrzutu stanu kontekstu. Łączne zużycie modelu w całym drzewie agentów, łącznie z podagentami, pochodzi z ResultMessage.model_usage, natomiast total_cost_usd to lokalna szacunkowa wartość, a nie faktura.
# 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
Każda architektura była wykonywana trzy razy, niezależnie od siebie, na jednym wspólnym zadaniu, przy użyciu identycznego SDK, aliasu modelu, dostawcy oraz konfiguracji lokalnej wiedzy. Zapisy z każdego wykonywania oraz uzyskane średnie znajdują się w pliku measurements/comparison.json, a niewielki skrypt wyświetlania regeneruje z tego pliku kompaktową tabelę.
# terminal
python scripts/show_comparison.py ../measurements/comparison.json
Dziesiąty wykres przedstawia średnie wartości trzech rund i ukazuje rzeczywisty kompromis. Izolacja zmniejszyła końcową ilość danych o wyniku narzędzia, którą widział lider, ale drzewo podagentów zużyło znacznie więcej tokenów oraz wygenerowało wyższy szacunkowy koszt. Poniżej znajduje się zapis wyjścia skryptu wyświetlania.
# 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
Sukces ma dwa oblicza. Sukces typu „artifact” oznacza, że spełniono umowę wyjściową architektury: w przypadku prostego agenta jest to raport zwrócony zawierający co najmniej dwa różne adresy URL, natomiast w przypadku agenta zaprojektowanego specjalnie – trwały plik final_report.md o długości co najmniej 500 znaków z dwoma adresami URL. Wszystkie sześć prób osiągnęło ten cel.
Aby SDK działało pomyślnie, ostateczny podtyp ResultMessage musi mieć wartość success. Próby typu „naive” kończyły się pomyślnie we wszystkich trzech przypadkach, natomiast próby typu „engineered” – tylko w jednym z trzech. Dwie próby typu „engineered” wygenerowały raporty, ale zakończyły się błędem error_max_budget_usd, przy czym jedna z nich wykorzystała pojedyncze zapytanie naprawcze kontrolera. Przykład budżetu firmy Anthropic wyjaśnia, że wydatki są sprawdzane po wezwaniu API, więc próba może zakończyć generowanie swojego pliku i nadal spełnić ustaloną wycenę, zanim pojawi się ostateczny wynik. Traktuj limit jako zabezpieczenie, a nie sygnał, że aplikacja została ukończona.
Liczby:
- Prowadzący otrzymał mniej danych. Średnia liczba widocznych znaków wynikających z narzędzia dla punktu wyjścia wyniosła 28 572, a po ich izolacji – 23 428, co stanowi spadek o 18,0%. Udział wyników narzędzia w końcowym zrzutku kontekstu zmniejszył się z 7 351 do 6 202 tokenów (o 15,6% mniej). W każdym przeprowadzonym teście brało udział dokładnie trzy podagenty, a ich tymczasowe wyszukiwania nigdy nie wpłynęły na tor rozwoju prowadzącego.
- Ogólne koszty gwałtownie wzrosły. Łączna liczba tokenów wejściowych w drzewie wynosiła początkowo średnio 102 210, a później 737 036, co stanowi wzrost o 621,1%. Średnia liczba tokenów wejściowych dotyczących prowadzącego i kontekstu w momencie szczytu wynosiła odpowiednio 17 184 i 32 668, co oznacza wzrost o 90,1%. Szacunkowy koszt SDK zmienił się z 0,663 USD na 3,270 USD, czyli o 393,4% więcej.
Kompresja nigdy nie była stosowana, ponieważ każde uruchomienie wykazywało zerowe granice. To nie oznacza, że kompresja uwzględniająca konieczność zachowania struktury jest nieskuteczna – pokazuje to jedynie, że to zadanie nigdy nie zbliżyło się do progu 200 000 tokenów. Eksperyment nie izoluje również wartości wyszukiwania narzędzi, ponieważ katalog MCP był mały, a opóźnione odkrywanie narzędzi miało niewiele możliwości na pokrycie kosztów związanych z ich wyborem.
Co mówią, a czego nie mówią wyniki
Nic tutaj nie stanowi jednoznacznego zarzutu wobec inżynierii kontekstu. Dane pokazują, dlaczego te cztery operacje powinny być stosowane selektywnie. Zapisywanie plików oraz ograniczona wydobywanie danych tworzą trwały stan, który można sprawdzić. Izolacja okazuje się korzystna wtedy, gdy ścieżka rozwoju danego elementu mogłaby inaczej zanieczyścić już i tak długi kontekst główny, a jej stosowanie nigdy nie jest bezkosztowe. Przy takiej wielkości zadań pojedynczy kontekst główny z pamięcią tymczasową może być lepszym rozwiązaniem w praktyce, chyba że korzyści z wyższej jakości specjalistycznej usługi uzasadniają dodatkowe wydatki.
Pozostaje artefakt funkcjonalny z architekturą kontekstową, lokalnie zaobserwowanym błędem oraz wyraźną granicą oceny. Rozsądne pytanie następne dotyczy jakości: czy materiał, który jest pisany, wybierany, kompresowany i izolowany, to rzeczywiście odpowiedni materiał? Odpowiedź wymaga ocen na poziomie zadań pod kątem pokrycia dowodów, radzenia sobie z sprzecznościami, ważności cytatów oraz odzyskiwania po kompresji. W tym przypadku izolacja zadań zmniejszyła objętość danych wynikowych narzędzia, ale zwiększyła liczbę tokenów i koszt całego drzewa, dlatego inżynieria kontekstu musi być oceniana jako całość systemu, a nie zakładana automatycznie jako optymalizacja.
Literatura pokrewna
- Inżynieria kontekstu dla aplikacji LLM: ustrukturyzowane wyjścia, narzędzia i ograniczenia agentów — Jak celowo przygotowywać dane wejściowe dla modeli: selekcja kontekstu, szablony promptów, schematy JSON, ograniczona dekodacja, wykonywanie narzędzi, pętle agentów oraz ograniczona autonomia.
- Inżynieria kontekstu dla agentów AI: pisanie, selekcja, kompresja, izolacja — Dlaczego wydajność agentów spada wraz ze wzrostem objętości kontekstu, cztery strategie zarządzania tym problemem, cztery rodzaje awarii, projektowanie promptów i narzędzi oraz sposób, w jaki to robią Claude Code, Manus i inne rozwiązania.