Tu utilises ChatGPT tous les jours. Tu colles du texte, tu récupères du texte, tu te sens productif. Mais à chaque conférence tech de 2026, un mot revient en boucle : agents. Ton PM dit ' il nous faut un agent '. Ton CTO dit ' les agents, c'est l'avenir '. LinkedIn croule sous les tribunes d'experts autoproclamés sur les agents. Et toi, tu es là, à te dire : ' Je sais même pas ce que ça veut dire. '

Voici le fossé. Un chatbot attend que tu tapes et te répond — comme un SMS avec un pote intelligent. Un agent IA, c'est autre chose. Un agent a un objectif, choisit ses propres outils, gère les erreurs, et continue de bosser jusqu'à ce que le travail soit fait ou qu'il décide que c'est impossible. Personne ne lui tient la main. C'est la différence entre poser une question à quelqu'un et embaucher quelqu'un pour faire un boulot.

Ce soir, tu combles ce fossé. Tu vas construire un vrai agent — en Python, from scratch, sans framework — qui recherche sur le web, analyse des infos, prend des décisions et sauvegarde un rapport de recherche sur ton disque. Avant d'aller dormir, tu auras compris le pattern exact qui fait tourner Claude Code, Codex, Devin et tous les autres produits agents facturés 200$/mois. Le pattern en lui-même, c'est une trentaine de lignes.

Ce qu'on construit

Un Research Agent qui :

  1. Te demande un sujet
  2. Cherche des informations pertinentes sur le web
  3. Lit et analyse ce qu'il trouve
  4. Rédige un résumé de recherche structuré
  5. Sauvegarde le résultat dans un fichier

Ce n'est pas une démo jouet. C'est la même architecture que les agents en production — tool use, boucles de raisonnement, output structuré. La seule différence entre ça et un ' agent de production ', c'est la gestion d'erreurs et l'échelle.

Étape 1 : Mise en place du projet (10 minutes)

mkdir research-agent && cd research-agent
python3 -m venv venv
source venv/bin/activate

pip install anthropic httpx

Deux dépendances :

  • anthropic — le SDK Python (Software Development Kit — une bibliothèque prête à l'emploi pour communiquer avec l'API de Claude)
  • httpx — pour faire des requêtes web depuis Python

Tu as aussi besoin d'une clé API Anthropic — en gros, un mot de passe qui autorise ton code à parler à Claude. Récupère-la sur console.anthropic.com. Les nouveaux comptes reçoivent 5$ de crédits gratuits, c'est largement suffisant pour faire tourner cet agent des centaines de fois.

export ANTHROPIC_API_KEY=sk-ant-...
touch agent.py

Étape 2 : Définir les tools (15 minutes)

Un agent sans tools, c'est juste un chatbot. Les tools sont des fonctions qui permettent à l'agent d'interagir avec le monde réel — chercher sur le web, lire des fichiers, appeler des APIs (la façon dont les programmes se crient dessus à travers Internet — imagine des SMS entre machines).

On va donner deux tools à notre agent :

# agent.py

import anthropic
import httpx
import json
import os
from datetime import datetime

client = anthropic.Anthropic()
MODEL = "claude-haiku-4.5"

# Définitions des tools — elles indiquent à Claude ce qui est disponible
tools = [
    {
        "name": "web_search",
        "description": "Search the web for information on a topic. Returns results with titles, URLs, and snippets.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "save_file",
        "description": "Save text content to a file on disk.",
        "input_schema": {
            "type": "object",
            "properties": {
                "filename": {
                    "type": "string",
                    "description": "Name of the file to save"
                },
                "content": {
                    "type": "string",
                    "description": "Content to write to the file"
                }
            },
            "required": ["filename", "content"]
        }
    }
]

Ces définitions fonctionnent comme un menu de restaurant. Claude lit les descriptions et décide quand utiliser chaque tool — tu ne codes pas l'ordre en dur. La partie input_schema utilise JSON Schema — un format standard pour décrire à quoi ressemblent les données, pour que Claude sache exactement quels paramètres chaque tool attend. Oui, tu décris ton format de données avec… un autre format de données. Bienvenue en programmation.

Étape 3 : Implémenter les tools (15 minutes)

Les définitions disent à Claude ce qui existe. Maintenant on écrit le code qui tourne vraiment quand Claude appelle un tool. C'est là que la théorie rencontre la pratique — ou plus précisément, là où tes belles abstractions se heurtent à la triste réalité de parser du HTML comme en 2003 :

def execute_tool(tool_name: str, tool_input: dict) -> str:
    """Execute a tool and return the result as a string."""
    if tool_name == "web_search":
        return do_web_search(tool_input["query"])
    elif tool_name == "save_file":
        return do_save_file(tool_input["filename"], tool_input["content"])
    else:
        return f"Error: Unknown tool '{tool_name}'"


def do_web_search(query: str) -> str:
    """Search using DuckDuckGo's HTML endpoint. No API key needed."""
    try:
        response = httpx.get(
            "https://html.duckduckgo.com/html/",
            params={"q": query},
            headers={"User-Agent": "ResearchAgent/1.0"},
            timeout=10.0,
        )
        response.raise_for_status()

        text = response.text
        results = []
        parts = text.split('class="result__snippet"')
        for part in parts[1:6]:  # Grab up to 5 results
            snippet_end = part.find("</a>")
            if snippet_end > 0:
                snippet = part[:snippet_end]
                clean = snippet.replace("<b>", "").replace("</b>", "")
                clean = clean.split(">")[-1] if ">" in clean else clean
                if clean.strip():
                    results.append(clean.strip())

        if results:
            return "Search results:\n" + "\n".join(
                f"- {r}" for r in results
            )
        return f"Search completed but no clear results for: {query}"

    except Exception as e:
        return f"Search error: {str(e)}"


def do_save_file(filename: str, content: str) -> str:
    """Save content to the output directory."""
    os.makedirs("output", exist_ok=True)
    filepath = os.path.join("output", filename)
    try:
        with open(filepath, "w") as f:
            f.write(content)
        return f"File saved successfully: {filepath}"
    except Exception as e:
        return f"Error saving file: {str(e)}"

La recherche web utilise le endpoint HTML de DuckDuckGo — pas de clé API, pas d'inscription, pas de frais. Le parsing HTML tient avec du scotch et de l'espoir (on gratte le markup brut de la page au lieu d'utiliser un vrai flux de données structuré), mais ça marche. En production, tu échangerais ça contre Brave Search API (2 000 requêtes gratuites/mois) ou un SearXNG auto-hébergé.

Étape 4 : Construire la boucle agent (20 minutes)

C'est le cœur de tout le truc. Chaque produit agent avec une landing page flashy et une valorisation à 50M$ fait tourner une version de ces 30 lignes :

def run_agent(topic: str, max_turns: int = 10) -> str:
    """Run the research agent on a topic."""
    print(f"\n{'='*60}")
    print(f"Research Agent — Topic: {topic}")
    print(f"{'='*60}\n")

    system_prompt = """You are a research agent. Your job is to research a topic
thoroughly and produce a well-structured summary.

Your process:
1. Search for information on the topic (multiple searches with different angles)
2. Analyze what you find
3. Write a comprehensive research summary
4. Save the summary to a file

Be thorough — do at least 3 different searches to cover the topic well.
Be critical — evaluate sources and note conflicting information.
When done, save the final summary as a markdown file.

Current date: """ + datetime.now().strftime("%Y-%m-%d")

    messages = [
        {
            "role": "user",
            "content": f"Research this topic and produce a detailed summary: {topic}"
        }
    ]

    # The agent loop
    for turn in range(max_turns):
        print(f"--- Turn {turn + 1} ---")

        response = client.messages.create(
            model=MODEL,
            max_tokens=4096,
            system=system_prompt,
            tools=tools,
            messages=messages,
        )

        print(f"Stop reason: {response.stop_reason}")

        if response.stop_reason == "tool_use":
            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    tool_name = block.name
                    tool_input = block.input
                    tool_id = block.id

                    print(f"  Tool: {tool_name}")
                    print(f"  Input: {json.dumps(tool_input, indent=2)[:200]}")

                    result = execute_tool(tool_name, tool_input)
                    print(f"  Result: {result[:200]}...")

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": tool_id,
                        "content": result,
                    })

            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

        elif response.stop_reason == "end_turn":
            final_text = ""
            for block in response.content:
                if hasattr(block, "text"):
                    final_text += block.text

            print(f"\nAgent completed in {turn + 1} turns.")
            return final_text

    return "Agent did not complete within turn limit."

Décortiquons cette boucle, parce que c'est tout le tour de magie :

  1. Envoie la tâche à Claude avec les définitions des tools
  2. Claude réfléchit et décide soit d'utiliser un tool, soit de répondre avec le texte final
  3. Si tool_use — on exécute le tool et on renvoie le résultat comme un nouveau message
  4. Claude voit le résultat et décide du prochain mouvement
  5. Recommence jusqu'à ce que Claude renvoie end_turn — ce qui signifie qu'il a terminé

L'insight crucial : tu n'as pas codé en dur ' cherche d'abord, puis analyse, puis écris '. Claude détermine le workflow en fonction de la tâche. C'est ce qui sépare un agent d'un script. Un script suit tes instructions. Un agent suit les siennes.

Le champ stop_reason est la clé. L'API de Claude renvoie soit "tool_use" (je veux appeler un tool) soit "end_turn" (j'ai fini). Ta boucle vérifie lequel et agit en conséquence.

Étape 5 : Ajouter le point d'entrée (5 minutes)

La partie ennuyeuse. Mais même les parties ennuyeuses doivent exister, sinon rien ne tourne — une leçon que la moitié des repos de démos IA sur GitHub n'a toujours pas retenue :

if __name__ == "__main__":
    import sys

    if len(sys.argv) > 1:
        topic = " ".join(sys.argv[1:])
    else:
        topic = input("Enter research topic: ")

    result = run_agent(topic)

    print(f"\n{'='*60}")
    print("Research complete. Check the output/ directory.")
    print(f"{'='*60}")

Étape 6 : Lancer le truc (5 minutes)

python agent.py "current state of MCP protocol adoption in 2026"

Regarde le terminal. Tu vas voir l'agent réfléchir au problème tout seul :

============================================================
Research Agent — Topic: current state of MCP protocol adoption in 2026
============================================================

--- Turn 1 ---
Stop reason: tool_use
  Tool: web_search
  Input: {"query": "MCP model context protocol adoption 2026"}
  Result: Search results: - The MCP ecosystem has grown...

--- Turn 2 ---
Stop reason: tool_use
  Tool: web_search
  Input: {"query": "MCP servers enterprise production 2026"}
  Result: Search results: - Amazon Bedrock AgentCore...

--- Turn 3 ---
Stop reason: tool_use
  Tool: web_search
  Input: {"query": "MCP protocol limitations challenges 2026"}
  Result: Search results: - Stateful sessions fight with...

--- Turn 4 ---
Stop reason: tool_use
  Tool: save_file
  Input: {"filename": "mcp-research-2026.md", "content": "# MCP Protocol..."}
  Result: File saved successfully: output/mcp-research-2026.md

--- Turn 5 ---
Stop reason: end_turn

Agent completed in 5 turns.

Cinq tours. Trois recherches, une sauvegarde de fichier, un résumé final. Personne ne lui a dit de chercher sous différents angles — il a décidé ça tout seul. Ça, c'est de l'agentivité, pas du scripting.

Étape 7 : Le rendre plus malin (30 minutes)

L'agent de base fonctionne. Maintenant on ajoute trois améliorations qui le transforment d'une démo en quelque chose que tu garderais vraiment.

Mémoire entre les sessions

Pour l'instant, chaque exécution repart de zéro. Donnons à l'agent une mémoire simple — un fichier JSON (un format texte structuré que les programmes peuvent facilement lire et écrire) qui stocke ses recherches précédentes :

from pathlib import Path

MEMORY_FILE = "memory.json"

def load_memory() -> list:
    """Load previous research topics and findings."""
    if Path(MEMORY_FILE).exists():
        with open(MEMORY_FILE) as f:
            return json.load(f)
    return []

def save_memory(topic: str, summary: str):
    """Save this research session to memory."""
    memory = load_memory()
    memory.append({
        "date": datetime.now().isoformat(),
        "topic": topic,
        "summary": summary[:500],
    })
    memory = memory[-20:]  # Keep last 20 entries
    with open(MEMORY_FILE, "w") as f:
        json.dump(memory, f, indent=2)

Injecte la mémoire dans le system prompt — le texte d'instruction qui façonne le comportement de Claude :

memory = load_memory()
if memory:
    memory_context = "\n\nPrevious research sessions:\n"
    for m in memory[-5:]:
        memory_context += f"- [{m['date'][:10]}] {m['topic']}: {m['summary'][:100]}...\n"
    system_prompt += memory_context

Maintenant l'agent sait ce qu'il a recherché avant. Il peut référencer ses découvertes passées, éviter les recherches en double, et construire sur son travail précédent.

Un tool de réflexion

Celui-là, c'est une astuce. Ajoute un tool qui ne fait littéralement rien :

tools.append({
    "name": "think",
    "description": "Use this tool to think through your approach before acting. Write out your reasoning and what you need to find out next.",
    "input_schema": {
        "type": "object",
        "properties": {
            "thought": {
                "type": "string",
                "description": "Your reasoning and plan"
            }
        },
        "required": ["thought"]
    }
})

Dans l'exécuteur de tools, ça renvoie juste une confirmation :

elif tool_name == "think":
    print(f"  Thinking: {tool_input['thought'][:300]}")
    return "Thought recorded. Continue with your plan."

Pourquoi ajouter un tool qui ne fait rien ? Parce que ça donne à l'agent un espace structuré pour raisonner avant d'agir. Sans ça, Claude fonce droit vers les appels de tools. Avec, Claude fait une pause, planifie, puis exécute — et produit des résultats sensiblement meilleurs. Anthropic documente cette technique dans son guide d'utilisation des tools, et les agents en production s'en servent.

Gestion des erreurs

def execute_tool_safe(tool_name: str, tool_input: dict) -> str:
    """Execute a tool with automatic retries."""
    for attempt in range(3):
        try:
            result = execute_tool(tool_name, tool_input)
            if "error" in result.lower() and attempt < 2:
                print(f"  Retry {attempt + 1}...")
                continue
            return result
        except Exception as e:
            if attempt < 2:
                print(f"  Error, retrying: {e}")
                continue
            return f"Tool failed after 3 attempts: {str(e)}"

Les requêtes web plantent. Les APIs tombent. Les timeouts arrivent. Un agent de production fait des retries avant d'abandonner. Trois tentatives avec fallback, c'est le minimum syndical.

La structure finale

research-agent/
├── agent.py          # ~150 lignes de Python
├── memory.json       # Créé automatiquement, stocke l'historique
├── output/           # Créé automatiquement, stocke les rapports
│   └── *.md
└── requirements.txt  # anthropic, httpx

Moins de 200 lignes. Deux dépendances. Zéro framework.

Pourquoi pas de framework ?

Tu te demandes peut-être : ' Pourquoi ne pas utiliser LangChain ou LlamaIndex ? ' (Deux frameworks Python populaires qui ajoutent des abstractions prêtes à l'emploi autour des appels LLM.)

Parce que la boucle agent ci-dessus fait 30 lignes. LangChain ajouterait 15 dépendances et trois couches d'abstraction pour le même résultat.

Utilise un framework quand :

  • Tu as besoin de 10+ tools avec une logique de routage complexe
  • Tu as besoin d'une mémoire conversationnelle qui passe à l'échelle sur des milliers d'utilisateurs
  • Tu as besoin de plusieurs agents coordonnés sur la même tâche
  • Tu as dépassé le Python simple et tu as besoin de l'architecture de quelqu'un d'autre

Passe ton chemin quand :

  • Tu construis ton premier agent
  • Ton agent a 2 à 5 tools
  • Tu veux comprendre chaque ligne de ce qui tourne
  • ' Ça marche et je comprends tout ' bat ' ça marche et je fais confiance à l'abstraction '

En mars 2026, la documentation du SDK Anthropic montre exactement le même pattern de boucle brute qu'on vient de construire. La recommandation officielle est de commencer sans framework.

Ce que tu as construit ce soir

Faisons l'inventaire :

  1. Un agent IA fonctionnel — prend un objectif, le poursuit de façon autonome
  2. Tool use — l'agent appelle des systèmes externes (recherche web, I/O fichier)
  3. Une boucle de raisonnement — Claude décide de la prochaine action en fonction des résultats, pas d'un script codé en dur
  4. Mémoire — l'agent se souvient des sessions passées et construit dessus
  5. Gestion d'erreurs — retries avant d'échouer
  6. Persistance des résultats — les outputs atterrissent dans de vrais fichiers sur ton disque

C'est exactement la même architecture que Claude Code, Devin, OpenAI Codex et tous les autres produits agents. Ils ont de meilleurs tools, plus de gestion d'erreurs et des context windows plus larges — la quantité de texte que le modèle peut ' voir ' en même temps, comme sa mémoire de travail. Mais la boucle est identique à ce que tu viens d'écrire.

La suite

Tu comprends maintenant le pattern fondamental. Tout le reste, c'est de l'ingénierie par-dessus :

  • Plus de tools — une calculatrice, un web scraper, un connecteur base de données, un exécuteur de code
  • Meilleure mémoire — des bases de données vectorielles (des systèmes qui stockent du texte par le sens, pas juste par mots-clés) pour la recherche sémantique entre sessions
  • Appels de tools parallèles — lancer plusieurs recherches en même temps au lieu de séquentiellement
  • Systèmes multi-agents — un second agent qui review le travail du premier, comme une code review
  • Intégration MCP — Model Context Protocol, un standard pour connecter les agents IA à des outils externes, comme l'USB mais pour les sources de données

Tu n'es pas en train d'apprendre le framework de quelqu'un qui sera peut-être mort dans six mois. Tu apprends le pattern. Le même pattern qui marchait en 2024, marche en 2026, et marchera en 2028 — parce que le mécanisme sous-jacent (le modèle décide → le tool exécute → le résultat revient) est le fonctionnement de tous les systèmes agents, quel que soit le nom marketing qu'on leur colle.

L'industrie des ' agents IA ' veut te faire croire que construire des agents exige un doctorat et une Series B à 100M$. Il faut comprendre une boucle : appeler le modèle, vérifier s'il veut un tool, exécuter le tool, renvoyer le résultat, recommencer. C'est tout. Tout le reste, c'est de l'ingénierie autour de cette boucle — et maintenant tu en sais assez pour faire cette ingénierie toi-même.