écrits/tutorial/2026/05
Tutorial26 mai 2026·26 min

smolagents : Construire des agents IA orientés code en Python avec Hugging Face

Un guide pratique de smolagents, le framework minimaliste d'agents de Hugging Face. Découvrez pourquoi les agents de code surpassent l'appel d'outils en JSON, construisez un assistant de recherche en Python, ajoutez des outils personnalisés, orchestrez plusieurs agents et isolez l'exécution avec E2B.

Introduction

La plupart des frameworks d'agents suivent la même recette : le modèle émet un objet JSON décrivant l'outil à appeler, le runtime le parse, exécute l'outil, renvoie le résultat, et boucle. Cela fonctionne, mais chaque étape paie une taxe — le JSON est verbeux, la composition d'outils est laborieuse, et enchaîner trois opérations exige trois allers-retours complets.

smolagents, publié par Hugging Face en 2025 et stabilisé courant 2026, fait un pari différent : au lieu d'émettre du JSON, l'agent écrit du code Python et l'exécute. Une seule génération peut appeler trois outils, parcourir une liste, faire de l'arithmétique et stocker des variables intermédiaires — le tout en un seul coup. Les recherches de Hugging Face et DeepMind montrent que les agents de code atteignent la même précision que les agents JSON avec 30 pour cent d'étapes en moins.

La bibliothèque elle-même est remarquablement petite — moins de 1000 lignes de code central — et supporte tout modèle parlant les protocoles OpenAI, Anthropic, Hugging Face Inference ou LiteLLM. Dans ce tutoriel vous allez construire un agent de recherche prêt pour la production qui récupère des données du web, résume des articles scientifiques et coordonne avec un sous-agent — le tout en pur Python.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Python 3.10 ou plus récent
  • Un compte Hugging Face et un jeton d'accès (le niveau gratuit suffit)
  • Une clé API OpenAI ou Anthropic (optionnel, pour comparer)
  • Une familiarité avec pip et les environnements virtuels
  • Une compréhension de base des concepts LLM (tokens, prompts système, appels d'outils)
  • Environ 30 minutes de concentration

Ce que vous allez construire

Un agent de recherche multi-étapes qui :

  • Accepte une question de recherche en langage naturel
  • Cherche sur le web via DuckDuckGo
  • Récupère et nettoie des pages web
  • Résume les résultats avec un raisonnement Chain-of-Thought
  • Délègue les sous-tâches mathématiques à un sous-agent spécialisé
  • Exécute le code non fiable dans un bac à sable E2B pour la sécurité

À la fin vous disposerez d'un squelette d'agent réutilisable, prêt à être servi derrière un endpoint FastAPI.

Étape 1 : Installer smolagents

Créez un nouveau projet et installez la bibliothèque avec les intégrations nécessaires :

mkdir smol-research && cd smol-research
python -m venv .venv
source .venv/bin/activate
pip install "smolagents[toolkit,litellm,e2b]>=1.10" \
  "duckduckgo-search" "markdownify" "requests"

Les extras entre crochets amènent :

  • toolkit — la trousse d'outils officielle (recherche web, fetcher de pages, calculatrice)
  • litellm — adaptateur pour OpenAI, Anthropic, Groq, Ollama et tout fournisseur compatible LiteLLM
  • e2b — bacs à sable distants pour l'exécution de code sécurisée

Exportez vos jetons :

export HF_TOKEN="hf_xxx"
export OPENAI_API_KEY="sk-..."
export E2B_API_KEY="e2b_..."   # optionnel, pour l'étape sandbox uniquement

Étape 2 : Construire votre premier CodeAgent

Créez agent.py. Le plus petit agent utile tient en trois lignes plus un modèle :

from smolagents import CodeAgent, InferenceClientModel, DuckDuckGoSearchTool
 
model = InferenceClientModel(model_id="meta-llama/Llama-3.3-70B-Instruct")
 
agent = CodeAgent(
    tools=[DuckDuckGoSearchTool()],
    model=model,
    max_steps=6,
)
 
result = agent.run("Quel article d'IA a été le plus cité en 2025 ?")
print(result)

Lancez-le :

python agent.py

Vous verrez l'agent imprimer son raisonnement et le code Python généré à chaque étape. Une première étape typique ressemble à ceci :

# Étape 1 — code généré par l'agent
results = duckduckgo_search(query="most cited AI paper 2025")
print(results[:3])

Notez que l'agent n'a pas émis de JSON — il a écrit du vrai Python qui appelle duckduckgo_search comme une fonction. Le runtime exécute ce Python dans un interpréteur restreint, capture la sortie de print et la renvoie au modèle comme observation.

Changer de modèle

InferenceClientModel utilise l'API Hugging Face Inference. Pour passer à OpenAI ou Anthropic, échangez la classe du modèle :

from smolagents import LiteLLMModel
 
# OpenAI
model = LiteLLMModel(model_id="gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"])
 
# Anthropic
model = LiteLLMModel(
    model_id="anthropic/claude-haiku-4-5",
    api_key=os.environ["ANTHROPIC_API_KEY"],
)
 
# Ollama en local
model = LiteLLMModel(model_id="ollama/llama3.1:8b", api_base="http://localhost:11434")

Tout le reste demeure identique. C'est la raison principale de l'adoption croissante de smolagents — indépendant du fournisseur par conception, sans schéma d'appel d'outils spécifique à chaque API.

Étape 3 : Ajouter un outil personnalisé

La trousse officielle couvre les bases, mais les vrais agents ont besoin d'outils métier. Un outil dans smolagents est une fonction Python avec une docstring et des annotations de type — le framework génère le schéma automatiquement.

Ajoutez un outil qui récupère une page web et la convertit en Markdown propre :

from smolagents import tool
import requests
from markdownify import markdownify
import re
 
@tool
def fetch_page(url: str) -> str:
    """Récupère une page web et retourne son contenu en Markdown propre.
 
    Args:
        url: L'URL complète de la page à récupérer.
    """
    response = requests.get(url, timeout=10, headers={"User-Agent": "Mozilla/5.0"})
    response.raise_for_status()
    markdown = markdownify(response.text, heading_style="ATX")
    markdown = re.sub(r"\n{3,}", "\n\n", markdown)
    return markdown[:8000]

Le décorateur @tool fait trois choses :

  1. Parse la signature de la fonction en une description équivalente à JSON Schema
  2. Extrait la docstring comme description de l'outil
  3. Enregistre les imports sûrs pour que l'agent puisse l'appeler

Enregistrez le nouvel outil au moment de construire l'agent :

agent = CodeAgent(
    tools=[DuckDuckGoSearchTool(), fetch_page],
    model=model,
    max_steps=8,
    additional_authorized_imports=["re", "json"],
)

Le paramètre additional_authorized_imports est critique. Par défaut l'interpréteur bloque tous les imports sauf une petite liste blanche (math, statistics, datetime). Toute bibliothèque dont vos outils dépendent doit être déclarée explicitement — c'est ce qui maintient le code généré sûr.

Étape 4 : Raisonnement, planification et mémoire

Pour les questions non triviales, un seul tour de recherche ne suffit pas. smolagents intègre une étape de planification activable :

agent = CodeAgent(
    tools=[DuckDuckGoSearchTool(), fetch_page],
    model=model,
    max_steps=10,
    planning_interval=3,
    additional_authorized_imports=["re", "json"],
)

Avec planning_interval=3, toutes les trois étapes l'agent s'arrête et écrit un plan en langage naturel résumant ce qu'il a appris et ce qu'il lui reste à faire. Dans nos tests, cela réduit la dérive sur les questions multi-sauts d'environ 40 pour cent.

Vous pouvez aussi inspecter ou persister la mémoire de l'agent :

result = agent.run("Compare l'empreinte carbone de l'inférence de GPT-4o et Llama 3.1.")
 
for step in agent.memory.steps:
    print(step.step_number, step.tool_calls or step.code_action)

agent.memory est une liste structurée d'objets ActionStep, PlanningStep et FinalAnswerStep — parfaite pour être loggée dans Langfuse, OpenTelemetry ou votre propre stack d'observabilité.

Étape 5 : Orchestration multi-agent

Les vraies charges de travail se découpent naturellement entre spécialistes. smolagents permet d'imbriquer des agents en tant qu'outils via l'enveloppe ManagedAgent.

Créez un sous-agent dédié au calcul :

from smolagents import ManagedAgent, ToolCallingAgent
 
math_agent = ToolCallingAgent(
    tools=[],
    model=model,
    name="math_solver",
    description=(
        "Effectue un raisonnement numérique précis. Envoyez-lui une question "
        "mathématique autonome et il renvoie la réponse."
    ),
    max_steps=4,
)
 
managed_math = ManagedAgent(
    agent=math_agent,
    name="math_solver",
    description="À utiliser pour toute arithmétique ou comparaison numérique plus lourde qu'une addition.",
)

ToolCallingAgent utilise les appels JSON classiques — utile lorsque le modèle est petit et la génération de code peu fiable. Le CodeAgent externe continuera d'émettre du Python, mais il peut désormais appeler l'agent interne comme une fonction :

research_agent = CodeAgent(
    tools=[DuckDuckGoSearchTool(), fetch_page],
    managed_agents=[managed_math],
    model=model,
    max_steps=12,
    planning_interval=4,
)
 
answer = research_agent.run(
    "Trouve l'empreinte carbone par 1000 tokens de GPT-4o et Llama 3.1 70B, "
    "puis calcule le ratio."
)

Dans une exécution typique, l'agent externe cherche les deux valeurs, puis écrit du Python comme :

ratio = math_solver(task=f"Divise {gpt_co2} par {llama_co2} et arrondis à 2 décimales")
final_answer(f"GPT-4o émet {ratio} fois plus de CO2 par 1000 tokens.")

L'avantage est double : l'agent mathématique est moins cher car il utilise un plus petit modèle, et l'orchestrateur reste concentré sur la recherche.

Étape 6 : Exécution en bac à sable avec E2B

Laisser un LLM exécuter du Python localement convient aux prototypes de confiance, mais les systèmes de production exigent l'isolation. smolagents s'intègre à E2B pour des bacs à sable distants — chaque exécution d'agent obtient un conteneur cloud éphémère.

from smolagents import CodeAgent, E2BExecutor
 
research_agent = CodeAgent(
    tools=[DuckDuckGoSearchTool(), fetch_page],
    managed_agents=[managed_math],
    model=model,
    max_steps=12,
    executor_type="e2b",
    executor_kwargs={"api_key": os.environ["E2B_API_KEY"]},
)

Ce simple changement de paramètre déplace chaque ligne de code généré de votre machine vers un conteneur jetable. Accès réseau, système de fichiers et limites CPU sont configurables via executor_kwargs. Le code de l'agent reste identique — seul l'exécuteur change.

Pour de l'isolation locale sans E2B, smolagents propose aussi un exécuteur Docker :

research_agent = CodeAgent(
    tools=[fetch_page],
    model=model,
    executor_type="docker",
    executor_kwargs={"image": "python:3.12-slim"},
)

Étape 7 : Streaming et observabilité

Les exécutions longues nécessitent un retour. Passez stream_outputs=True pour voir les tokens arriver au fil de l'eau :

for chunk in research_agent.run(
    "Résume les 5 articles d'IA les plus importants de 2025",
    stream_outputs=True,
):
    print(chunk, end="", flush=True)

Pour le traçage structuré, smolagents émet des spans OpenTelemetry nativement. Configurez un exporteur OTLP pointant vers Langfuse, Honeycomb ou tout backend compatible :

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
 
provider = TracerProvider()
provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="https://cloud.langfuse.com/api/public/otel/v1/traces"))
)
trace.set_tracer_provider(provider)

Chaque appel d'outil, étape de planification et invocation de modèle devient un span dans lequel vous pouvez plonger.

Étape 8 : Servir l'agent derrière FastAPI

Enveloppez l'agent dans une fine couche HTTP afin qu'il puisse être embarqué dans n'importe quel produit :

from fastapi import FastAPI
from pydantic import BaseModel
 
app = FastAPI()
 
class Question(BaseModel):
    question: str
 
@app.post("/research")
def research(payload: Question):
    answer = research_agent.run(payload.question)
    return {"answer": answer, "steps": len(research_agent.memory.steps)}

Lancez avec uvicorn main:app --reload --port 8000 et l'agent est accessible sur /research. Associez-le à une file comme Inngest ou Trigger.dev si vos tâches dépassent régulièrement 30 secondes.

Tester votre implémentation

Faites un test de fumée avec une question qui exige à la fois recherche et calcul :

curl -X POST http://localhost:8000/research \
  -H "Content-Type: application/json" \
  -d '{"question": "Quelle est la différence de débit entre vLLM et TGI sur Llama 3 70B ?"}'

Vous obtiendrez une réponse en 15 à 40 secondes selon le modèle. La charge utile JSON inclut la réponse finale et le nombre d'étapes de raisonnement consommées.

Pour des tests de régression déterministes, simulez le modèle :

from smolagents import FakeModel
 
fake = FakeModel(responses=[
    "Thought: I should search.\nCode:\n```py\nresults = duckduckgo_search(query='vLLM vs TGI')\nprint(results)\n```",
    "Thought: I have enough.\nCode:\n```py\nfinal_answer('vLLM is roughly 2x faster.')\n```",
])
agent = CodeAgent(tools=[DuckDuckGoSearchTool()], model=fake)
assert "2x" in agent.run("vLLM vs TGI throughput?")

FakeModel rejoue des réponses préenregistrées, ce qui permet d'asserter sur des séquences d'outils sans dépenser de tokens.

Dépannage

L'agent importe des modules interdits. Ajoutez le module à additional_authorized_imports. Si vous avez vraiment besoin de la bibliothèque standard complète, mettez additional_authorized_imports=["*"] — mais uniquement dans un bac à sable.

Les descriptions d'outils sont fausses dans les traces. Assurez-vous que chaque paramètre a une annotation de type et que la docstring suit le style Google avec un bloc Args:. Le parseur est strict.

Les sous-agents ne sont jamais appelés. L'orchestrateur décide quand déléguer en se basant sur le champ description du ManagedAgent. Rédigez les descriptions à l'impératif et mentionnez des cas d'usage concrets : "À utiliser pour les conversions de devises ou les calculs d'unités."

Le bac à sable E2B time out. La durée de vie par défaut du conteneur est de 5 minutes. Pour des exécutions plus longues, passez executor_kwargs={"timeout": 1800} (en secondes).

Les tokens explosent à la relance. La mémoire de smolagents grossit linéairement avec les étapes. Pour un usage conversationnel, appelez agent.memory.reset() entre les tours.

Étapes suivantes

Conclusion

smolagents parie que la bonne sortie d'un agent LLM est du code, pas du JSON — et le pari est en train de gagner en benchmarks comme en production. Tout au long de ce tutoriel vous avez monté un CodeAgent, ajouté un outil personnalisé, intercalé un sous-agent pour le travail spécialisé et déplacé l'exécution dans un conteneur isolé, le tout en quelques centaines de lignes de Python.

La bibliothèque est volontairement petite. Une fois les quatre primitives comprises — modèle, outil, agent, exécuteur — vous pouvez lire l'intégralité des sources en un après-midi et tout personnaliser. C'est rare dans l'écosystème des agents, et c'est exactement ce qui fait de smolagents un excellent choix par défaut quand vous voulez moins d'abstractions et plus de contrôle.