Les LLM sont excellents pour écrire du code. Ils sont catastrophiques quand il s'agit de l'exécuter en toute sécurité. Si vous laissez un modèle faire eval() sur du Python arbitraire dans votre serveur, vous lui avez en pratique tendu un shell. La solution sur laquelle l'industrie s'est alignée en 2025 est le bac à sable cloud sécurisé, et le plus populaire est E2B Code Interpreter — une microVM Firecracker qui démarre en moins de 200 ms, est livrée avec Python, Node.js, Jupyter, pandas, matplotlib et plus de mille autres paquets pré-installés, et se détruit elle-même à la fin.
Dans ce tutoriel nous allons connecter E2B à une application Next.js 15 avec le Vercel AI SDK et Claude (ou n'importe quel autre modèle compatible tool-calling) et livrer une véritable expérience "code interpreter" : un chat où le modèle peut tracer des CSV, exécuter des régressions et renvoyer images et fichiers en temps réel.
Ce que vous allez construire
Une application Next.js 15 où l'utilisateur peut :
- Uploader un fichier CSV
- Poser des questions en langage naturel comme "Quelle est la corrélation entre le chiffre d'affaires et les effectifs ?"
- Voir l'agent écrire du Python, l'exécuter dans un bac à sable isolé, et streamer du texte, des graphiques et des artefacts téléchargeables
À la fin vous comprendrez la boucle complète : prompt → appel d'outil → exécution sandbox → rendu du résultat → mise à jour de l'UI.
Prérequis
- Node.js 20 ou supérieur (exigence Next.js 15)
- Un gestionnaire de paquets (pnpm recommandé)
- Une clé API E2B depuis e2b.dev (le palier gratuit suffit pour ce tutoriel)
- Une clé API Anthropic (ou OpenAI, Google, Groq — tout fournisseur compatible tool-calling fonctionne)
- Une familiarité de base avec les React Server Components et l'App Router
Étape 1 : Mise en place du projet
Initialisez un nouveau projet Next.js 15 avec TypeScript et Tailwind.
pnpm create next-app@latest e2b-agent --typescript --tailwind --app --eslint
cd e2b-agentInstallez le AI SDK, le provider Anthropic et le SDK E2B Code Interpreter.
pnpm add ai @ai-sdk/anthropic @ai-sdk/react @e2b/code-interpreter zodTrois paquets comptent ici :
@e2b/code-interpreter— le SDK officiel qui lance les bacs à sable et exécute le codeaiet@ai-sdk/anthropic— le Vercel AI SDK et le provider Claude@ai-sdk/react— le hookuseChatqui gère le streaming côté client
Étape 2 : Configurer les variables d'environnement
Créez .env.local à la racine du projet.
E2B_API_KEY=e2b_xxxxxxxxxxxxxxxx
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxNe commitez jamais ce fichier. Ajoutez-le à .gitignore si le scaffold ne l'a pas déjà fait.
Étape 3 : Construire le helper de sandbox
Créez lib/sandbox.ts. Ce module gère le cycle de vie du bac à sable pour que le reste de l'application reste propre.
import { Sandbox } from "@e2b/code-interpreter";
const TEMPLATE = "code-interpreter-v1";
const TIMEOUT_MS = 5 * 60 * 1000;
export async function createSandbox() {
const sandbox = await Sandbox.create(TEMPLATE, {
timeoutMs: TIMEOUT_MS,
});
return sandbox;
}
export async function runPython(sandboxId: string, code: string) {
const sandbox = await Sandbox.connect(sandboxId);
const execution = await sandbox.runCode(code, { language: "python" });
return {
stdout: execution.logs.stdout.join(""),
stderr: execution.logs.stderr.join(""),
results: execution.results.map((r) => ({
text: r.text,
png: r.png,
html: r.html,
json: r.json,
})),
error: execution.error?.value,
};
}Quelques détails à noter. Sandbox.create renvoie une microVM tiède en environ 150 ms. runCode bloque jusqu'à la fin de l'exécution et vous donne des résultats typés — stdout, stderr, sorties riches (PNG matplotlib, tables HTML pandas) et erreurs structurées. Nous conservons le sandboxId pour que les appels d'outils ultérieurs dans la même conversation réutilisent la même VM et gardent leurs variables.
Étape 4 : Définir le schéma d'outils
Le Vercel AI SDK utilise Zod pour décrire les outils. Créez lib/tools.ts.
import { tool } from "ai";
import { z } from "zod";
import { runPython, createSandbox } from "./sandbox";
export function buildTools(sandboxIdRef: { current: string | null }) {
return {
execute_python: tool({
description:
"Execute Python code in a secure sandbox. Use this for data analysis, calculations, plotting charts, and file manipulation. Variables persist across calls within the same conversation.",
parameters: z.object({
code: z
.string()
.describe("Valid Python code. Use matplotlib for plots."),
}),
execute: async ({ code }) => {
if (!sandboxIdRef.current) {
const sandbox = await createSandbox();
sandboxIdRef.current = sandbox.sandboxId;
}
return runPython(sandboxIdRef.current, code);
},
}),
};
}sandboxIdRef est une petite boîte partagée passée par référence, ainsi le premier appel d'outil crée le bac à sable et chaque appel suivant le réutilise. C'est l'astuce qui rend possible le raisonnement multi-étapes — l'agent peut définir une variable à l'étape un et la lire à l'étape trois.
Étape 5 : Créer la route de chat
Dans l'App Router Next.js 15, le streaming côté serveur vit dans un route handler. Créez app/api/chat/route.ts.
import { anthropic } from "@ai-sdk/anthropic";
import { streamText, convertToCoreMessages } from "ai";
import { buildTools } from "@/lib/tools";
export const maxDuration = 60;
export async function POST(req: Request) {
const { messages, sandboxId } = await req.json();
const sandboxIdRef = { current: sandboxId ?? null };
const result = streamText({
model: anthropic("claude-sonnet-4-6"),
system:
"You are a senior data analyst. When the user asks anything that requires computation, plotting, or file inspection, write and run Python in the sandbox rather than guessing. Always show your work briefly before calling the tool.",
messages: convertToCoreMessages(messages),
tools: buildTools(sandboxIdRef),
maxSteps: 5,
onFinish: ({ response }) => {
response.headers = {
...(response.headers ?? {}),
"x-sandbox-id": sandboxIdRef.current ?? "",
};
},
});
return result.toDataStreamResponse({
headers: {
"x-sandbox-id": sandboxIdRef.current ?? "",
},
});
}Trois points importants. maxSteps: 5 permet au modèle de réfléchir, exécuter du code, lire le résultat, puis exécuter encore — sans cela, l'agent appellerait l'outil une fois et s'arrêterait. Le prompt system indique explicitement au modèle de préférer l'exécution à la devinette, ce qui est tout l'intérêt d'un code interpreter. L'en-tête de réponse x-sandbox-id est la manière dont nous renvoyons l'identifiant du bac à sable au client pour qu'il soit rattaché au tour suivant.
Étape 6 : Construire l'UI de chat
Créez app/page.tsx.
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function Home() {
const [sandboxId, setSandboxId] = useState<string | null>(null);
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({
api: "/api/chat",
body: { sandboxId },
onResponse: (response) => {
const id = response.headers.get("x-sandbox-id");
if (id) setSandboxId(id);
},
});
return (
<div className="mx-auto flex h-screen max-w-3xl flex-col p-6">
<h1 className="mb-4 text-2xl font-semibold">E2B Code Interpreter</h1>
<div className="flex-1 space-y-4 overflow-y-auto">
{messages.map((m) => (
<Message key={m.id} message={m} />
))}
</div>
<form onSubmit={handleSubmit} className="mt-4 flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Ask the agent to analyse data..."
className="flex-1 rounded border border-zinc-300 px-3 py-2"
disabled={isLoading}
/>
<button
type="submit"
className="rounded bg-black px-4 py-2 text-white disabled:opacity-50"
disabled={isLoading}
>
Send
</button>
</form>
</div>
);
}Le hook useChat fait le gros du travail — streaming de tokens, état des messages, et fusion du body qui envoie notre sandboxId à chaque requête. Le callback onResponse récupère le nouvel identifiant de sandbox depuis les en-têtes de la réponse.
Étape 7 : Afficher les appels d'outils et les sorties
Ajoutez un composant Message sous l'export de Home. C'est ici que vous donnez vie aux résultats des outils.
function Message({ message }: { message: any }) {
return (
<div className="rounded-lg border border-zinc-200 p-4">
<div className="mb-2 text-xs font-medium uppercase text-zinc-500">
{message.role}
</div>
{message.parts?.map((part: any, i: number) => {
if (part.type === "text") {
return (
<p key={i} className="whitespace-pre-wrap text-sm">
{part.text}
</p>
);
}
if (part.type === "tool-invocation") {
const { toolName, state, args, result } = part.toolInvocation;
return (
<div key={i} className="my-2 rounded bg-zinc-50 p-3 text-xs">
<div className="mb-1 font-mono text-zinc-600">
tool: {toolName} ({state})
</div>
<pre className="overflow-x-auto whitespace-pre-wrap text-zinc-800">
{args?.code}
</pre>
{result?.stdout && (
<pre className="mt-2 border-t border-zinc-200 pt-2 text-zinc-700">
{result.stdout}
</pre>
)}
{result?.results?.map((r: any, j: number) =>
r.png ? (
<img
key={j}
src={`data:image/png;base64,${r.png}`}
alt="plot"
className="mt-2 rounded border border-zinc-200"
/>
) : null,
)}
</div>
);
}
return null;
})}
</div>
);
}Le tableau parts est la représentation structurée du tour de l'assistant proposée par le AI SDK. Les parts de type texte se rendent comme paragraphes, les parts tool-invocation affichent le code exécuté, le stdout et les PNG matplotlib intégrés. Pas d'étape de téléchargement, pas de serveur de fichiers séparé — les octets voyagent dans la réponse elle-même.
Étape 8 : Ajouter l'upload de fichiers
Pour une vraie analyse, l'utilisateur doit pouvoir uploader des données. Créez app/api/upload/route.ts.
import { Sandbox } from "@e2b/code-interpreter";
import { createSandbox } from "@/lib/sandbox";
export async function POST(req: Request) {
const formData = await req.formData();
const file = formData.get("file") as File;
let sandboxId = formData.get("sandboxId") as string | null;
if (!sandboxId) {
const sandbox = await createSandbox();
sandboxId = sandbox.sandboxId;
}
const sandbox = await Sandbox.connect(sandboxId);
const buffer = Buffer.from(await file.arrayBuffer());
const path = `/home/user/${file.name}`;
await sandbox.files.write(path, buffer);
return Response.json({ sandboxId, path });
}Ensuite ajoutez un bouton d'upload dans page.tsx. Envoyez le chemin retourné comme indice dans le message suivant — "J'ai uploadé sales.csv dans /home/user/sales.csv, peux-tu le charger avec pandas ?" — et le modèle le prendra en compte.
Étape 9 : Tester
Lancez le serveur de dev.
pnpm devOuvrez http://localhost:3000 et essayez ces prompts :
- "Génère 1000 points aléatoires d'une distribution normale et trace un histogramme."
- "Calcule les 20 premiers nombres de Fibonacci et affiche-les sur un graphique linéaire."
- "Définis une fonction qui retourne le n-ième nombre premier. Utilise-la pour trouver le 100e nombre premier."
Vous verrez l'assistant écrire du Python, la boîte d'outil afficher le code dans un panneau gris, le stdout streamer en dessous, et tout graphique matplotlib apparaître comme image intégrée. Les variables persistent — le troisième tour peut référencer ce que le premier tour a défini.
Étape 10 : Durcissement pour la production
Quelques éléments à mettre en place avant de pousser ceci au-delà de la démo.
Isolation par utilisateur. Aujourd'hui le sandbox id vit côté client. Un utilisateur motivé pourrait envoyer l'id de quelqu'un d'autre. Stockez-le côté serveur indexé par session et récupérez-le dans le route handler.
Timeouts et nettoyage. Les sandboxes E2B s'auto-terminent après le timeoutMs configuré à la création, mais il faut aussi appeler sandbox.kill() depuis un webhook de nettoyage quand l'utilisateur ferme son onglet — les plans payants facturent à la seconde de runtime sandbox.
Limites de ressources. Utilisez la configuration de ressources d'E2B pour plafonner CPU et mémoire par sandbox afin qu'un modèle emballé ne vide pas votre crédit.
Streaming de l'usage de tokens. Le AI SDK expose usage dans onFinish. Loggez-le dans votre analytics pour attribuer le coût par utilisateur.
Multi-langage. Le même sandbox supporte JavaScript, TypeScript, R et bash. Ajoutez des outils supplémentaires (execute_javascript, execute_bash) avec des schémas Zod correspondants pour laisser l'agent choisir le bon runtime.
Dépannage
Aucun appel d'outil ne se produit. Vérifiez que maxSteps vaut au moins 2. Avec maxSteps: 1 le modèle planifie mais n'exécute pas.
Le démarrage à froid du sandbox paraît lent. Le premier sandbox par région prend entre 600 et 800 ms. Les suivants dans la même région descendent sous 200 ms. Pour les apps sensibles à la latence, préchauffez un sandbox par session active.
Les graphiques matplotlib reviennent vides. Le backend par défaut dans E2B est non interactif. Ajoutez plt.show() à la fin du code de tracé — le SDK détecte la figure et la sérialise en PNG automatiquement.
Les variables disparaissent entre les tours. Vous avez oublié de propager sandboxId. Confirmez que l'en-tête de réponse est lu côté client et renvoyé dans le body de la requête suivante.
Étapes suivantes
- Combinez ceci avec les agents Mastra pour des workflows multi-agents où un agent planifie et un autre exécute
- Couplez-le avec Langfuse pour tracer chaque exécution de code de bout en bout
- Construisez un pipeline RAG agentique où les documents récupérés sont analysés par le code interpreter au lieu d'être simplement injectés dans le prompt
- Remplacez Claude par DeepSeek, GPT-4.1 ou Gemini — la seule ligne qui change est l'import du modèle
Conclusion
E2B Code Interpreter transforme la capacité la plus dangereuse d'un modèle de langage — l'exécution de code arbitraire — en l'une des plus sûres. Avec moins de 200 lignes de code de colle, vous avez un agent qui analyse des feuilles de calcul, exécute des tests statistiques, trace des graphiques et renvoie des artefacts, le tout à l'intérieur d'une microVM Firecracker jetable qui disparaît à la fin de la conversation. Le même pattern s'étend à l'ingénierie de données, au calcul scientifique, à la modélisation financière et à tout domaine où "le modèle écrit le code, le sandbox l'exécute" l'emporte sur "le modèle devine la réponse."
Livrez ceci, instrumentez-le, et vos agents arrêtent d'halluciner des chiffres.