L’ingénierie de contexte en pratique : un agent de recherche du Claude Agent SDK
Créez un agent de recherche en Python à l’aide du Claude Agent SDK en utilisant les modèles d’écriture, de sélection, de compression et d’isolation, puis comparez-le à une version de base naïve lors d’exécutions mesurées.
Sources: Context Engineering in Practice: Building a Production AI Agent with the Claude Agent SDK
Un agent de recherche ne se heurte que rarement à une seule instruction complexe. Ce qui est problématique, c’est l’accumulation. Au cours d’une seule enquête, le contexte se remplit d’instructions de l’utilisateur, d’un plan, de requêtes de recherche, de pages téléchargées, de schémas d’outils, de métadonnées sources, d’allégations inachevées, de rapports provenant d’assistants et des étapes précédentes. Tous ces éléments actifs font que les bonnes preuves finissent par concurrencer la pile d’instructions en attente pour attirer l’attention de l’agent.
Ici, le cadre « Écrire, Sélectionner, Compresser et Isoler » devient une architecture Python fonctionnelle au sein du Claude Agent SDK. Un seul agent s’améliore pas à pas, à chaque décision prise, et chaque limite soulève la même question : que le modèle doit-il voir à ce moment-là, et quoi peut rester en dehors ? L’objectif est un agent de recherche que l’on puisse exécuter, et non un catalogue des fonctionnalités du SDK.
La construction s’effectue en dix étapes :
- Définir ce qui est construit.
- Établir une base de référence simple.
- Écrire : sortir l’état de travail de la conversation.
- Sélectionner : assembler un ensemble de travail délimité.
- Compresser : rendre les sessions longues récupérables.
- Isolement : déléguer des investigations ciblées.
- Isoler les sorties des outils gourmands dans l’environnement.
- Transférer le contexte entre sessions.
- Assembler l’agent conçu avec un mécanisme de contexte.
- Comparer les deux architectures.
Ce qui est développé
Face à une question, l’agent planifie ses recherches, consulte des sources primaires, confie des investigations plus précises à des assistants, enregistre les preuves et rédige finalement un rapport cité. La tâche d’exemple : comparer les principales approches de mémoire à long terme utilisées dans les agents d’intelligence artificielle en production et expliquer le coût de chacune. Elle nécessite des articles scientifiques, des documents d’implémentation, des preuves issues de dépôts logiciels et des vérifications de citations, ce qui en fait un test suffisamment complet.
L’agent principal conserve le plan et rédige la synthèse finale. L’état de travail mis à jour est stocké dans des fichiers, les mécanismes de récupération limitent la quantité de connaissances qui arrivent, et les sous-agents traitent de petites questions au sein de leurs propres contextes. Rien de tout cela n’est intégré dans le SDK. Celui-ci fournit le modèle et le cycle d’outils, tandis que l’application choisit où se trouve l’état et quelle quantité d’informations peut traverser chaque limite.
La première figure montre le système de départ : un agent principal connecté à des outils, à des connaissances, à une zone de travail temporaire et à des sous-agents. Le reste de la structure modifie la manière dont les informations circulent entre ces connexions.
Commencez par créer un environnement et installer une version spécifique du SDK avant d’exécuter tout module.
# 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"
La structure sépare la configuration des agents des opérations sur l’espace de travail, des outils personnalisés, des hooks et des mesures. Cette séparation est importante car chaque opération dans un contexte donné ne modifie qu’une seule frontière, sans nécessiter une application entièrement nouvelle.
# 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
L’ingénierie de contexte commence par l’attribution des responsabilités. La conversation contient la décision en cours, tandis que les fichiers, les outils, les systèmes de récupération et les sous-agents stockent tout ce qui n’a pas besoin d’être actif pour le moment.
Le point de référence naïf
Le plus petit programme Agent SDK transfère la boucle de contrôle à query(). Claude reçoit l’instruction, détermine s’un outil est nécessaire, observe le résultat et répète l’opération jusqu’à ce qu’une réponse soit obtenue ou que la limite configurée soit atteinte. Comme le SDK fournit cette boucle, l’application n’a pas besoin d’écrire elle-même le traitement et l’envoi des appels aux outils.
# 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())
Ce petit programme cache quelque chose : query() exécute la boucle, mais c’est encore l’application qui décide de ce qui y est introduit. La version de base exécutable dans research_agent/config.py précise ces choix.
# 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,
)
Dans la configuration de base, il n’y a ni outil d’écriture, ni outil d’agent, ni paramètre agents, ni crochet, ni fichier d’instructions de projet. Le responsable doit mener toute l’enquête au cours d’une seule conversation. La valeur ENABLE_TOOL_SEARCH=false empêche également que les schémas MCP soient différés. L’instruction système complète cette politique en demandant à l’agent d’utiliser l’outil whole-corpus, de conserver les résultats dans la conversation, et de ne pas écrire de notes dans l’espace de travail ni déléguer la tâche.
L’outil whole-corpus est délibérément très simple, car le problème réside dans ce qu’il renvoie et non dans sa manière de fonctionner.
# 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))
Lorsque Claude en fait la demande, le SDK émet le résultat sous forme de UserMessage contenant un ToolResultBlock. Le « utilisateur » en question représente un rôle protocolaire et non un message émanant d’une personne réelle. Ce bloc est ajouté à l’historique transmis à chaque étape ultérieure du modèle, de sorte qu’une recherche sur Internet, trois pages téléchargées et l’ensemble du corpus génèrent une séquence répétitive de ce type.
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)
Rien dans cette séquence n’est en soi incorrect. Elle est naïve car l’application ne transforme jamais les résultats bruts en un état externe organisé, et ne les retire jamais de la trajectoire active.
La deuxième figure montre le contexte naïf qui rassemble des instructions, des définitions d’outils, des pages brutes, des résultats et l’historique des conversations dans une seule fenêtre. La croissance provient de la politique de l’application et non d’un défaut dans le cycle SDK.
La recherche d’outils est activée par défaut pour la plupart des configurations d’Agent SDK, avec des exceptions documentées. Une base de référence équitable doit donc l’éteindre délibérément si l’expérience vise à précharger l’ensemble des outils disponibles. En tant que contrôle, la conception naïve est utile car elle élimine toutes les décisions de gestion du contexte que les étapes suivantes vont tester.
Avant de consacrer du temps et des ressources à une comparaison, un test de base a envoyé une requête restreinte via la même configuration. Les résultats obtenus ont montré que l’authentification, le routage et la résolution des modèles fonctionnaient tous correctement.
# Captured output: python scripts/smoke_test.py
subtype=success
result=OPENROUTER_OK
models=claude-sonnet-5
La ligne de référence complète a ensuite été exécutée trois fois, les résultats étant affichés à côté des essais modifiés dans la comparaison finale. Le premier essai permet de concrétiser cette accumulation.
# 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
Les trois dernières valeurs mesurent des choses différentes. La valeur de 16 478 correspond au contexte actif final, et 6 851 de ces tokens proviennent des résultats des outils. La valeur de 138 182 représente la quantité totale d’entrées sur les quatre étapes de l’assistant, y compris la création et la lecture des caches ; cela ne signifie donc pas que l’agent ait conservé en même temps une fenêtre de 138 182 tokens. Cette distinction devient importante lorsque des sous‑agents sont pris en compte dans la comparaison.
Un agent naïf ne constitue un contrôle fiable que si des fonctionnalités telles que la recherche d’outils sont explicitement désactivées. Sinon, les paramètres par défaut du SDK peuvent faire en sorte que la ligne de référence soit plus performante que prévu.
Rédaction : fichiers de brouillon
La première modification ajoute un carnet de notes soutenu par le système de fichiers. plan.md contient la question, les dimensions de la recherche, ce qui a été fait et ce qui reste à faire. research_notes.md rassemble les affirmations, les preuves et les contradictions, sources.json stocke les métadonnées des citations, et open_questions.md met en évidence les points non résolus. Les documents sources volumineux sont placés dans un dossier d’artefacts.
# 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
Comme ces fichiers sont durables, l’agent peut les mettre à jour après chaque étape de recherche et ne lire que celui dont il a besoin ensuite. Une panne ou un événement de compactage laisse le plan et les preuves établis intacts, permettant à une personne de lire directement l’état sans devoir le reconstituer à partir d’une transcription.
La troisième figure provient d’un script hors ligne qui initialise l’espace de travail et affiche les fichiers exacts que l’agent conçu utilise comme espace de travail temporaire.
Une session a une autre fonction. Les documents de la session la décrivent comme le stockage des prompts, des appels d’outils, des résultats d’outils et des réponses nécessaires pour poursuivre une conversation. L’espace de travail temporaire, quant à lui, contient l’état de recherche établi par l’application elle-même. Reprendre une session permet de récupérer la trajectoire suivie, tandis que la lecture de plan.md restitue une vue soigneusement maintenue du travail effectué.
Cette distinction permet d’éviter deux erreurs fréquentes. Si le transcript est la seule base de données, le récupération dépend d’un historique de plus en plus long. Si le scratchpad devient un dump de transcripts, le même problème de contexte se reproduit sur disque. Ainsi, chaque fichier dispose d’un schéma restreint et contient des conclusions, des preuves ainsi que du travail en cours, et non des conversations copiées. L’historique de session enregistre comment l’agent est arrivé à ce stade, tandis que le scratchpad conserve l’état succinct et organisé nécessaire pour décider de la suite des opérations.
Sélection : un ensemble de travail limité
L’état externe peut dépasser ce que toute seule une appel de modèle devrait pouvoir gérer. La sélection résout ce problème en chargant les données selon la phase : la planification lit plan.md, la synthèse lit research_notes.md, et l’élaboration des citations lit sources.json. Les outils et les connaissances suivent le même principe, car même leurs métadonnées peuvent occuper une part importante du prompt.
Du côté des outils, la configuration conçue permet de rechercher des outils. La documentation d’Anthropic indique qu’un catalogue peut contenir jusqu’à 10 000 outils, avec un maximum de cinq résultats pertinents par recherche, sauf modification. Elle couvre les outils MCP ainsi que les outils MCP personnalisés SDK ; ainsi, un grand catalogue peut rester en dehors du contexte jusqu’à ce que Claude requière un schéma particulier. Selon la documentation, avec environ dix outils ou moins, charger tout le contenu au préalable est souvent plus rapide, ce qui fait que le chargement différé des outils vise avant tout l’échelle plutôt qu’un avantage garanti.
En ce qui concerne les connaissances, un outil personnalisé search_knowledge s’appuie sur SQLite FTS5. Le gestionnaire prend en charge une requête ainsi qu’un nombre limité de résultats top_k et renvoie des extraits cités plutôt que l’ensemble complet de la collection. Les outils personnalisés dans le SDK fonctionnent via un serveur MCP en mémoire, de sorte que l’exemple reste autonome tout en franchissant les mêmes limites qu’un service externe.
# 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)
La requête spécifie top_k=5, alors que le schéma autorise une valeur allant de un à dix. Cette séparation entre la politique et la limite stricte permet d’ajuster la qualité de la récupération ultérieurement sans modifier le contrat de l’outil, et une requête anormale visant des milliers d’enregistrements échoue avant de pouvoir submerger le contexte.
La quatrième figure montre les lectures de fichiers, la recherche dans l’outil et la récupération de connaissances comme trois moyens de sélectionner différents types de contexte, tous aboutissant au même résultat : un ensemble de travail limité pour l’étape en cours.
Un test hors ligne exécute le code de recherche réel et vérifie que cinq constitue une limite maximale, que chaque élément retourné conserve son URL source, et que les valeurs incorrectes de top_k sont rejetées. Il ne dit rien sur la qualité de la récupération sémantique car l’outil utilisé est très limité. En production, ce jugement relève du système de récupération, étayé par des requêtes évaluées et des métriques de rappel, plutôt que d’être déduit de la manière dont le texte final est rédigé. En éliminant les données hors de la conversation, on libère de l’espace, mais c’est le processus de sélection qui détermine si l’agent peut encore trouver les cinq éléments appropriés lorsqu’ils deviennent pertinents.
Compressez : sessions longues récupérables
Le mécanisme de sélection détermine ce qui est pris en compte, mais une session prolongée continue d’accumuler de anciennes échanges. Lorsque la limite de contexte est approchée, l’Agent SDK compacte automatiquement l’historique : les messages plus anciens deviennent un résumé, tandis que les derniers échanges et l’état important sont conservés. Un SystemMessage au sous-type compact_boundary apparaît dans le flux, ce qui permet au code de mesure de compter chaque fois que cela se produit.
Un résumé générique peut omettre précisément les informations sur lesquelles repose une tâche de recherche. Le projet intègre donc des règles de conservation spécifiques à la recherche dans CLAUDE.md : il faut conserver la question originale, les résultats vérifiés, les questions en suspens, les contradictions, les URLs des sources, les décisions majeures ainsi que le plan actuel. Ce fichier guide ce qui doit figurer dans le résumé, de sorte que tout l’historique n’a pas besoin d’être transmis à chaque appel.
Au préalable de toute compression, automatique ou manuelle, un crochet PreCompact enregistre un point de contrôle. Lorsque la transcription du SDK est disponible, elle est copiée, et les métadonnées indiquent le déclencheur, la session, le répertoire de travail ainsi que l’origine de la transcription.
# 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
La cinquième figure montre comment le processus de compression réduit la conversation accumulée en un résumé qui respecte les règles de conservation, tandis que le point de contrôle et le carnet temporaire restent des enregistrements indépendants dans l’environnement.
Les carnets temporaires et la compression répondent à des besoins différents. Le carnet temporaire est un état que l’agent écrit intentionnellement au fur et à mesure, organisé pour être lu ultérieurement. La compression consiste en une compression avec perte des conversations accumulées, qui a lieu lorsque l’historique actif est devenu trop volumineux. Avec ces deux éléments en place, une session compressée peut faire appel à sources.json ou research_notes.md si le résumé a perdu des informations.
Un test hors ligne a fourni au hook une transcription temporaire et a confirmé trois choses : le point de contrôle est apparu, la transcription a été copiée et les métadonnées déclencheuses ont survécu. Lors des six essais de comparaison en temps réel, aucun compact_boundary n’est apparu, car le plus grand contexte observé était de 34 309 tokens contre une fenêtre annoncée de 200 000 tokens. Ainsi, l’expérience confirme que le hook fonctionne mais ne dit rien sur la qualité de la compression en temps réel. Le résumé doit contenir les décisions prises et les tâches non résolues, tandis que le carnet de travail doit conserver les preuves sous une forme lisible.
Isolez : sous-agents ciblés
Les enquêtes indépendantes sont plus faciles à gérer lorsqu’elles ne suivent pas une trajectoire commune en constante évolution. Une fois que l’agent principal a rédigé le plan, trois sous-agents désignés s’occupent de l’analyse papier, des preuves stockées et de la vérification des références. Chaque définition limite les tâches, les outils ainsi que la manière de transmettre les informations.
# 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,
),
Dans le code, les sous-agents sont déclarés à l’aide de AgentDefinition. Selon la documentation d’Anthropic sur les sous-agents, chacun commence une nouvelle conversation avec son propre contexte, conserve ses appels d’outils intermédiaires et leurs résultats, et n’envoie qu’un message final au parent. Le transfert constitue donc une véritable frontière de contexte, et non simplement une convention au sein de la fenêtre existante du responsable.
La sixième figure montre chaque spécialiste recevant une tâche ciblée ainsi qu’une nouvelle fenêtre. Le responsable voit uniquement le résumé du transfert, sans voir les recherches, les pages téléchargées ni les étapes d’extraction qui l’ont généré.
L’isolation a un coût. Trois sous-agents peuvent redécouvrir les mêmes sources, consommer plus de tokens et aboutir à une terminologie contradictoire. Pour y remédier, les instructions exigent des URL principales, des limites clairement définies et au plus cinq résultats. Le vérificateur de faits ne voit que les affirmations contestées et non l’ensemble du brief, ce qui permet de garder son champ d’intervention auditable et réduit le risque qu’il réécrive secrètement la synthèse.
Un responsable doit pouvoir évaluer ce qui lui est retourné. Cinq lignes sans identifiants de source sont concises mais inutilisables, tandis qu’une transcription complète de la conversation efface toute distinction. Le contrat prévoit ici les résultats, les citations, les contraintes de version et les limites, éléments dont le responsable a besoin pour comparer les affirmations. Cacher la conversation secondaire ne suffit pas à lui seul, car l’isolation ne fonctionne que si le résultat présente des preuves et de l’incertitude.
Isolez les sorties des outils complexes
Certains chargements de données devraient traverser les limites de l’outil uniquement en tant que références. materialize_source écrit un document choisi dans workspace/artifacts/ et renvoie son chemin, sa taille en octets ainsi qu’une brève vue préliminaire. Par la suite, Claude peut ouvrir la plage correspondante à l’aide des outils de fichiers, de sorte que l’ensemble du document n’apparaît jamais dans le résultat d’un seul outil.
# 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],
}
Le SDK dispose d’un mécanisme de sécurité similaire pour les résultats MCP. La documentation MCP d’Anthropic indique que toute sortie dépassant 10 000 tokens est à considérer, avec un plafond par défaut fixé à 25 000 tokens ; les textes trop volumineux peuvent être enregistrés sur disque et remplacés dans le contexte par une référence à un fichier. Ce plafond par défaut sert de solution de secours. La restitution des chemins et des métadonnées permet à l’application d’avoir un cadre stable bien avant d’atteindre la limite d’urgence.
La septième figure provient d’un test de défaillance intentionnel visant à tenter de sortir du répertoire des artefacts en utilisant ../outside.txt. L’espace de travail rejette ce chemin avant même que quoi que ce soit ne soit écrit.
L’isolation de l’environnement crée également une barrière de sécurité. Le nom d’un artefact contrôlé par un modèle ne doit en aucun cas devenir un chemin de fichier arbitraire. safe_artifact_path accepte un nom de fichier prudent et vérifie que la destination finale reste à l’intérieur de workspace/artifacts/. L’échec observé avait cet aspect.
# Captured output: python scripts/demonstrate_failure.py
Blocked artifact path: artifact name must use only letters, numbers, dots, underscores, or hyphens
Ce contrôle ne mentionne ni le sandboxing du système d’exploitation ni l’isolation multi-locataire. Il confirme une règle précise : un argument d’outil ne peut pas utiliser la navigation de chemins pour écrire en dehors du répertoire de l’artefact. Les déploiements réels nécessitent toujours des permissions au niveau du processus ainsi que des espaces de travail distincts pour chaque utilisateur ou exécution. De plus, remplacer un gros chargement par un simple chemin ne permet d’économiser du contexte que lorsque ce dernier est restreint et que l’agent peut récupérer par la suite une portion limitée.
Transférer le contexte entre sessions
Les tâches longues durent plus longtemps que les processus ainsi que que les fenêtres de contexte. Le SDK propose trois options : continuer la dernière conversation, reprendre une session spécifique par son identifiant, ou créer une branche à partir d’une session. « Continuer » s’applique à un travail en cours, « reprendre » correspond à une récupération explicite, tandis que « créer une branche » convient à un suivi qui doit hériter du contexte précédent sans modifier l’avenir de la session originale.
# 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),
}
Chaque option conserve l’état de la conversation : les prompts, les appels d’outils, les résultats et les réponses. Une branche nouvelle reçoit un identifiant de session frais, tandis que l’original reste inchangé. Cela convient à une demande telle que « Comparez maintenant ces résultats spécifiquement pour les agents de codage », car la nouvelle branche peut s’appuyer sur la trajectoire de recherche existante.
La huitième figure met en contraste les modes : « Continuer » prolonge la trajectoire actuelle, « Reprendre » restaure une trajectoire choisie, et « Créer une branche » démarre une nouvelle branche à partir du même point de contrôle.
Les fichiers sur le disque nécessitent une gestion spécifique. Les branches créées par un fork partagent un répertoire de travail à moins que l’application n’en assigne un autre, ce qui permet à toutes deux d’écraser plan.md ou final_report.md. Le programme évite les conflits en utilisant comme racine unique pour chaque exécution une date et heure UTC associée à un suffixe aléatoire ; de plus, un fork destiné à l’environnement de production devrait copier ou versionner le répertoire temporaire dans une nouvelle racine avant que l’une des branches ne écrive quoi que ce soit. Il convient de se rappeler qu’un ID de session identifie l’historique des conversations. Il ne s’agit pas d’un ID de transaction pour les systèmes externes avec lesquels l’agent interagit, c’est pourquoi les bases de données, les systèmes de stockage d’objets, les index de recherche et les espaces de travail nécessitent chacun leur propre politique de traçabilité et de concurrence. La structure des fichiers rend cette organisation visible, sans la cacher derrière un comportement de reprise. Un fork ne sépare que la conversation, tandis que l’état externe reste partagé tant que l’espace de travail n’a pas été versionné ou divisé.
Montage de l’agent conçu sur mesure
La configuration finale combine les quatre opérations sans ajouter de cadre d’orchestration supplémentaire. « Write » représente l’espace de travail structuré. « Select » correspond à la lecture des fichiers par phase, aux schémas d’outils différés et à la récupération de connaissances ciblée. « Compress » associe la compression des SDK aux instructions de conservation ainsi qu’à un point de contrôle pris juste avant. « Isolate » signifie l’utilisation de sous-agents délimités et le stockage des résultats des outils dans des fichiers.
# 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,
)
En définissant setting_sources=["project"], le CLAUDE.md spécifique à l’exécution peut être intégré à la configuration, et strict_mcp_config=True restreint le SDK au serveur fourni par cette application. Le permission_mode est fixé à dontAsk uniquement parce que l’exemple vise des exécutions autonomes et isolées, sans constituer une recommandation générale en matière de permissions. Les flux d’approbation avancés ainsi que les politiques de déploiement sont délibérément exclus du champ d’application.
Dans la neuvième figure, l’architecture finalisée régule quatre aspects : les écritures durables, les lectures sélectives, la compression des conversations et l’exécution isolée. Le modèle principal ne reçoit qu’un contexte actif ciblé.
Le dernier exécutant s’appuie sur ClaudeSDKClient et omet l’aide-mémoire plus léger query(). Ils partagent le même cycle d’agent, mais le client propose une limite de réponse explicite ainsi qu’une appelation finale get_context_usage(). C’est l’instrumentation qui a guidé ce choix, et l’architecture est restée inchangée.
La première tentative de conception, basée uniquement sur des indications, a révélé une autre limite en environnement de production. Les trois exécutions des SDK se sont terminées, mais un seul a généré final_report.md ; deux agents principaux ont renvoyé des résultats alors que le travail délégué restait en fait inachevé. Une sortie propre du SDK ne signifie donc pas que la tâche de l’application est terminée. La solution a consisté à rendre les appels aux sous-agents explicitement synchrones et à fournir au contrôleur son propre contrat de finition : il cherche un rapport complet contenant au moins deux URL et autorise une seule requête de correction ciblée au sein de la même session.
# 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)
Le résultat échoué basé uniquement sur l’instruction reste dans measurements/initial-prompt-only-comparison.json, car un contrôleur doit vérifier des résultats durables et non les déduire de l’état final du modèle.
Le jeu de tests hors ligne gère tout ce qui ne nécessite pas de modèle : les ensembles d’options, la création d’un espace de travail, la récupération ciblée, le rejet des chemins invalides, les points de contrôle de compactage ainsi que la comptabilisation des messages. C’est ce qu’il a rapporté dans l’environnement indiqué.
# Captured output: python -m unittest discover -s tests -q
----------------------------------------------------------------------
Ran 12 tests in 0.048s
OK
À ce stade, le framework est opérationnel : chaque verbe est associé à une frontière qui possède son propre code, un responsable pour son état ainsi qu’une invariante que les tests peuvent vérifier.
Comparaison des deux architectures
Les affirmations concernant l’architecture doivent se traduire en chiffres. RunMetrics surveille le flux de messages du SDK, supprime les étapes dupliquées au niveau supérieur à l’aide des IDs de message, compte chaque appel d’outil et chaque frontière de compactage, évalue le nombre de caractères dans les résultats des outils et demande un aperçu final du contexte. L’utilisation cumulative du modèle dans l’ensemble de l’arbre d’agents, y compris les sous-agents, provient de ResultMessage.model_usage, tandis que total_cost_usd représente une estimation locale et non un relevé de facturation.
# 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
Chaque architecture a été exécutée trois fois, de manière indépendante, sur une même question, en utilisant le même SDK, l’alias du modèle, le fournisseur ainsi que les mêmes paramètres de connaissances locales. Les enregistrements de chaque exécution ainsi que les moyennes dérivées se trouvent dans measurements/comparison.json, et un petit script d’affichage régénère le tableau compact à partir de ce fichier.
# terminal
python scripts/show_comparison.py ../measurements/comparison.json
Le dixième graphique présente les moyennes de trois points et révèle un véritable compromis. L’isolement a réduit la charge utile finale que le leader pouvait voir, mais l’arbre de sous-agents a consommé bien plus de tokens et un coût estimé plus élevé. La sortie capturée du script d’affichage suit.
# 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
Le succès se présente sous deux formes. Un succès lié à l’artefact signifie que le contrat de sortie de l’architecture a été respecté : pour l’agent naïf, un rapport retournant au moins deux URL distinctes ; pour l’agent conçu spécifiquement, un fichier final_report.md durable de 500 caractères ou plus contenant deux URL. Les six exécutions ont toutes atteint cet objectif.
Le succès du SDK exige que le sous-type final ResultMessage soit de type success. Les exécutions naïves y parviennent dans trois cas sur trois, tandis que les exécutions optimisées y arrivent une fois sur trois. Deux exécutions optimisées ont généré des rapports cités mais se sont terminées avec l’état error_max_budget_usd, et l’une d’elles a utilisé la seule requête de réparation du contrôleur. L’exemple concernant le budget d’Anthropic explique que les dépenses sont vérifiées après une appel API, de sorte qu’une exécution peut achever la création de son artefact et respecter encore l’estimation configurée avant que le résultat final n’arrive. Considérez cette limite comme un garde-fou, et non comme un signal indiquant que l’application a terminé.
Les chiffres :
- Le fil conducteur a reçu moins d’informations. La moyenne des caractères visibles liés au résultat de l’outil était de 28 572 pour la ligne de référence et de 23 428 une fois isolés, ce qui représente une baisse de 18,0 % ; la part des résultats de l’outil dans le snapshot final est passée de 7 351 à 6 202 tokens (soit une diminution de 15,6 %). Exactement trois sous-agents ont été utilisés dans chaque essai conçu, et leurs recherches intermédiaires n’ont jamais influencé la trajectoire du fil conducteur.
- Le coût global a fortement augmenté. La quantité totale d’entrées dans l’arborescence était en moyenne de 102 210 tokens au début et de 737 036 par la suite, ce qui correspond à une hausse de 621,1 %. La quantité maximale d’entrées liant le fil conducteur au contexte était en moyenne de 17 184 puis de 32 668, soit une augmentation de 90,1 % ; en outre, l’estimation du coût fournie par le SDK est passée de 0,663 $ à 3,270 $, ce qui représente une hausse de 393,4 %.
Aucune compression n’a jamais été appliquée, car chaque exécution indiquait zéro limite. Cela ne prouve pas que la compression tenant compte de la préservation des données soit inefficace. Cela montre simplement que cette tâche n’a jamais atteint la limite de 200 000 tokens. L’expérience ne permet pas non plus d’évaluer pleinement l’utilité de la recherche d’outils, car le catalogue MCP était restreint et la découverte différée avait peu de marge pour compenser les coûts liés à sa sélection.
Ce que les résultats disent et ne disent pas
Rien dans ce texte ne constitue une condamnation générale de l’ingénierie contextuelle. Les données montrent pourquoi ces quatre opérations doivent être utilisées de manière sélective. L’écriture de fichiers et la récupération ciblée permettent d’obtenir un état durable qui peut être examiné. L’isolation s’avère utile lorsque la trajectoire d’un enfant risquerait sinon de polluer un contexte principal déjà très long, et elle n’est jamais gratuite. Pour cette taille de tâche, un seul contexte principal accompagné d’un espace de travail temporaire peut très bien être la meilleure solution en production, à moins que les améliorations en termes de qualité spécialisée ne justifient les coûts supplémentaires.
Ce qui reste est un artefact fonctionnel doté d’une architecture contextuelle, d’une défaillance détectée localement et de limites d’évaluation claires. Une question pertinente concerne la qualité : le matériel qui est écrit, sélectionné, compressé et isolé est-il réellement celui approprié ? Pour y répondre, il faut recourir à des évaluations au niveau de la tâche concernant la couverture des éléments, la gestion des contradictions, la validité des citations et la récupération après compression. Dans ce cas, l’isolation de la tâche a réduit la charge des résultats générés par l’outil, mais a multiplié le nombre de tokens et les coûts pour tout l’arbre, ce qui oblige à évaluer l’ingénierie contextuelle en tant que système et non à la considérer automatiquement comme une optimisation.
Lectures complémentaires
- Ingénierie du contexte pour les applications LLM : sorties structurées, outils et limites des agents — Comment construire délibérément les entrées des modèles : sélection du contexte, modèles de prompts, schémas JSON, décodage contraint, appels d’outils, boucles d’agents et autonomie limitée.
- Ingénierie du contexte pour les agents IA : écrire, sélectionner, compresser, isoler — Pourquoi les agents se dégradent à mesure que le contexte augmente, les quatre stratégies pour le gérer, les quatre modes de défaillance, la conception des prompts et des outils, ainsi que la manière dont Claude Code, Manus et d’autres le font.