Kajan Siva
← Blog
IA appliquée

Construire un mini Claude Code pas à pas : d’un appel à un LLM à un agent IA

3 août 2026·9 min de lecture

Claude Code explore une codebase et enchaîne les actions. Ça semble magique. Derrière, il y a un LLM, du contexte, des outils et une boucle. Ce sont les briques qu’on va démonter puis réassembler dans un mini Claude Code.

Ouvrons le capot

RAG, agents, tool calling, MCP, context engineering… Le vocabulaire s'accumule vite. Vu de loin, chaque terme semble ajouter une nouvelle couche à maîtriser.

Le piège, c'est de les apprendre comme des concepts séparés. Ils deviennent beaucoup plus simples dès qu'on les ramène à des problèmes de développement concrets.

Tu n'as pas besoin de devenir spécialiste du machine learning pour créer des produits IA. Si tu sais appeler une API, manipuler un tableau et écrire une boucle, tu as déjà le bagage nécessaire. L'objectif de cet article est de te donner le socle pour comprendre quelles informations le modèle reçoit, où conserver l'état, qui exécute les actions et quand le système doit recommencer ou s'arrêter.

On va donc partir du mécanisme le plus simple, puis ajouter une seule capacité à la fois. À la fin, les agents IA n'auront plus grand-chose de mystérieux.

Le modèle ne voit que ce qu'on lui envoie

Un LLM peut donner l'impression de comprendre ton application. En réalité, au moment d'un appel, il ne voit que ce qu'il a appris pendant son entraînement et les informations que tu lui envoies. Pas tes échanges précédents, pas tes fichiers, pas ta base de données.

Le premier mécanisme à comprendre est donc moins le modèle lui-même que sa frontière avec ton code. Ton application prépare une requête, le modèle produit une réponse, puis ton application décide quoi en faire.

Concrètement, un appel à l'API ressemble à ça :

src/01-premier-appel.mjs
const response = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "x-api-key": process.env.ANTHROPIC_API_KEY,
    "anthropic-version": "2023-06-01",
  },
  body: JSON.stringify({
    model: process.env.ANTHROPIC_MODEL ?? "claude-sonnet-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Explique-moi ports & adapters." }],
  }),
});
 
if (!response.ok) {
  const detail = await response.text();
  throw new Error(`API Anthropic (${response.status}) : ${detail}`);
}
 
const message = await response.json();
const text = message.content.find((block) => block.type === "text")?.text ?? "";
console.log(text);

Ici le modèle peut répondre correctement parce que la question porte sur un concept général présent dans ses données d'entraînement. Une sortie possible serait :

Une réponse possible
Les ports définissent les interactions dont le cœur de l'application a besoin.
Les adaptateurs connectent ces ports au monde extérieur : HTTP, base de données,
messagerie, etc. Le domaine reste ainsi indépendant de l'infrastructure.

Avec un système aussi basique, comment construire une expérience de type chatbot pour que ton agent puisse tenir une conversation avec toi ?

La "context window", la mémoire des LLM

La solution s'appelle la context window : l'ensemble des informations que ton application envoie au modèle pour produire la prochaine réponse.
Les chatbots s'appuient dessus pour créer l'illusion d'une mémoire en renvoyant toute la conversation à chaque question.

À chaque tour, l'application renvoie au LLM le système et l'intégralité de l'historique des messages.
À chaque tour, l'application renvoie au LLM le prompt système et l'intégralité de l'historique des messages.

Dans le code, cette mémoire est simplement un tableau qui grossit :

Cœur de src/02-chat.mjs
const messages = [];
 
while (true) {
  const question = await terminal.question("Toi > ");
  if (!question.trim()) continue;
 
  messages.push({ role: "user", content: question });
  const response = await askModel({ messages });
  messages.push({ role: "assistant", content: response.content });
 
  console.log(`Claude > ${extractText(response.content)}\n`);
  console.log(`Historique renvoyé : ${messages.length} messages\n`);
}

Après une question et une réponse, le tableau contient deux messages. Au tour suivant, il en contient quatre. Puis six. Le modèle n'a rien conservé entre-temps : c'est ton application qui a tout renvoyé.

Cette mécanique explique pourquoi les longues conversations coûtent plus cher et deviennent plus lentes.

Mais cette mémoire reconstruite ne donne toujours pas accès à ton environnement. Si je demande maintenant : « Où est gérée l'authentification dans ce projet ? », le modèle n'a aucun moyen de voir les fichiers. Il peut pourtant répondre avec beaucoup d'assurance :

Une réponse plausible, mais fausse
« L'authentification est centralisée dans src/auth/auth.service.ts. Le middleware
src/auth/auth.middleware.ts valide ensuite le JWT avant chaque route protégée. »

Ces chemins ont l'air crédibles, mais ils n'existent pas dans le projet. Le modèle complète les trous avec une réponse probable. Pour répondre sur un repo en particulier, il faut le brancher sur le réel.

Le tool calling, pour connecter le LLM au monde extérieur

Le tool calling permet au modèle de demander une action à ton application. Le mot important est bien demander : le LLM n'exécute rien.

Ton application lui décrit d'abord les outils disponibles, leur rôle et leurs paramètres. Pour le mini Claude Code que j'ai créé dans le repo compagnon, j'en expose trois :

  • listFiles pour découvrir la structure du projet ;
  • searchCode pour chercher un texte dans les fichiers ;
  • readFile pour lire le contenu d'un fichier.

Dans cet exemple, les trois outils sont exécutés localement, sur la machine qui fait tourner l'agent. Ce ne sont pas pour autant trois commandes Bash : listFiles et readFile utilisent les API de fichiers de Node.js, tandis que searchCode lance directement grep.

Un outil peut pourtant prendre des formes très variées : une fonction déterministe, un appel à une API, une requête à une base de données ou une action sur un service externe. Pour le modèle, peu importe ce qu'il y a derrière : la définition de l'outil est un contrat, pas le code qui sera exécuté. Plus son nom, sa description et son schéma d'entrée sont précis, plus le modèle a de chances de l'utiliser correctement :

Extrait de src/tools.mjs
const searchCodeTool = {
  name: "searchCode",
  description:
    "Cherche une chaîne exacte dans les fichiers du dépôt et renvoie chemin, ligne et extrait.",
  strict: true,
  input_schema: {
    type: "object",
    properties: {
      query: { type: "string", description: "Texte exact à rechercher" },
      path: { type: "string", description: "Dossier relatif où chercher" },
    },
    required: ["query", "path"],
    additionalProperties: false,
  },
};

Quand le modèle a besoin de cet outil, sa réponse ne contient pas encore la réponse finale. Elle contient un bloc structuré comme celui-ci :

Réponse du modèle
{
  "type": "tool_use",
  "id": "toolu_01A09q90qw90lq917835lq9",
  "name": "searchCode",
  "input": { "query": "auth", "path": "src" }
}

Ce JSON ne cherche rien du tout. Il dit seulement : « appelle searchCode avec ces arguments ». L'identifiant id permettra ensuite d'associer le résultat à cette demande précise. C'est ton code qui garde le contrôle :

  1. il vérifie la demande ;
  2. il exécute la fonction correspondante ;
  3. il ajoute le résultat dans l'historique sous forme de tool_result ;
  4. il rappelle le modèle avec ce contexte enrichi.

Le flow complet ressemble à ça :

Le modèle demande un outil, l'application l'exécute, puis ajoute son résultat au contexte avant de rappeler le modèle.
Le modèle demande un outil, l'application l'exécute, puis ajoute son résultat au contexte avant de rappeler le modèle.

Le modèle peut alors lire le résultat et décider qu'il a assez d'informations pour répondre, ou demander un autre outil.

Tu peux tester cet aller-retour isolément avec npm run tool-calling dans src/03-tool-calling.mjs.

Cette séparation est essentielle pour la sécurité. Décrire un outil au modèle ne lui donne pas un accès direct à ton système. C'est ton application qui décide quelles fonctions existent, quels paramètres sont acceptés et ce qu'elles ont le droit de faire.

Nous avons désormais toutes les briques : un appel au LLM, une fenêtre de contexte, des outils et un moyen de remettre leurs résultats dans la conversation. Il ne reste plus qu'à les assembler.

Un agent IA, c'est une boucle while

Lors d'un appel, le modèle peut demander un outil. Le résultat de cet outil peut conduire à une nouvelle demande. On répète donc la même séquence jusqu'à ce que le modèle produise une réponse finale.

L'agent analyse son contexte, consulte sa boîte à outils lorsque les informations sont insuffisantes, puis boucle jusqu'à pouvoir répondre à l'utilisateur.
L'agent analyse son contexte, consulte sa boîte à outils lorsque les informations sont insuffisantes, puis boucle jusqu'à pouvoir répondre à l'utilisateur.

Voilà le cœur d'un agent. La fonction reçoit un tableau messages qui contient déjà la question initiale de l'utilisateur :

Boucle simplifiée
export async function agent(messages, { askModel, executeTool, tools }) {
  let response = await askModel({ messages, tools });
 
  while (response.stop_reason === "tool_use") {
    messages.push({ role: "assistant", content: response.content });
    const toolCalls = response.content.filter((block) => block.type === "tool_use");
    const toolResults = [];
 
    for (const call of toolCalls) {
      const content = await executeTool(call.name, call.input);
      toolResults.push({ type: "tool_result", tool_use_id: call.id, content });
    }
 
    // Dans l'API Messages, les résultats d'outils utilisent le rôle `user`.
    messages.push({ role: "user", content: toolResults });
    response = await askModel({ messages, tools });
  }
 
  return response.content.find((block) => block.type === "text")?.text ?? "";
}

Il n'y a pas de magie cachée dans ce code :

  • askModel appelle le LLM avec l'historique et les outils disponibles ;
  • tant que le modèle renvoie tool_use, l'application exécute ses demandes ;
  • chaque résultat est relié à sa demande grâce à tool_use_id, puis rejoint la fenêtre de contexte ;
  • le modèle est rappelé avec ces nouvelles informations ;
  • la boucle s'arrête dès que le modèle ne demande plus d'outil.

Sortir de la boucle ne signifie pas encore que la réponse est exploitable. Le modèle peut aussi s'être arrêté parce qu'il a atteint sa limite de tokens ou produit une réponse vide. La version complète traite ces cas avant de retourner le texte.

On peut résumer la recette ainsi : agent = LLM + contexte + outils + boucle + condition d'arrêt.

La différence avec un workflow classique se trouve dans l'ordre des étapes. Dans un workflow, tu écris à l'avance : « liste les fichiers, cherche auth, puis lis ces deux fichiers ». Dans un agent, tu fournis un objectif et des outils. Le modèle choisit la prochaine action en fonction de ce qu'il vient d'observer.

Voici un extrait d'une exécution réelle sur le projet d'exemple sample-project fourni dans le repo :

Extrait d'une exécution réelle
$ npm run agent
 
Tour 1 → listFiles {"path":"."}
Tour 2 → readFile {"path":"src/middleware/authenticate.js"}
         → readFile {"path":"src/routes/login.js"}
         → readFile {"path":"src/security/session.js"}
         → readFile {"path":"src/security/passwords.js"}
         → readFile {"path":"src/routes/profile.js"}
Tour 3 → readFile {"path":"src/services/users.js"}
         → readFile {"path":"src/config.js"}
         → readFile {"path":"src/server.js"}
 
## Où est gérée l'authentification
 
L'authentification est répartie sur plusieurs fichiers dans `src/`, avec une
logique simple reposant sur des sessions en mémoire (pas de JWT ni OAuth) :
 
[…]

Lors de cet appel, le modèle a commencé par découvrir le projet avec listFiles. Les noms des fichiers étaient suffisamment explicites pour qu'il lise directement les candidats pertinents, sans utiliser searchCode. Avec les mêmes outils, il aurait pu choisir un autre chemin. C'est ce qui rend le système agentique, mais aussi moins prévisible.

La boucle ci-dessus montre volontairement le mécanisme dans sa forme la plus lisible. L'implémentation exécutable ajoute ce qu'un vrai programme ne peut pas ignorer : une limite de tours, la gestion des erreurs d'outils et le cas d'une réponse vide. src/04-agent.mjs assemble cette boucle avec l'API et les outils pour produire le programme lancé par npm run agent.

Ça marche… et c'est là que les vrais problèmes commencent

Nous avons un agent fonctionnel en quelques dizaines de lignes, avec quelques garde-fous. C'est assez pour un POC. C'est aussi le moment où les problèmes sérieux commencent.

Un agent peut boucler, multiplier les appels et consommer beaucoup plus de tokens que prévu. La première protection est triviale : une limite de tours. En production, il faut aussi suivre le nombre d'appels, leur durée et leur coût.

Un outil est une capacité confiée au modèle. readFile peut tomber sur un .env. Un outil d'écriture peut corrompre des données. Un appel HTTP peut déclencher une action réelle. Il faut donc limiter les chemins accessibles, valider les entrées, demander une confirmation pour les actions sensibles et exécuter le tout dans un environnement isolé.

Les données lues ne sont pas fiables non plus. Un fichier peut contenir IGNORE ALL PREVIOUS INSTRUCTIONS. Pour le modèle, cette phrase ressemble à une instruction comme une autre : c'est une prompt injection. Le prompt système peut rappeler au modèle que le contenu des fichiers est une donnée, mais il ne constitue pas une défense suffisante à lui seul.

Enfin, « ça a marché une fois » ne veut rien dire. Comme les sorties sont probabilistes, tu as besoin d'évaluations répétables sur un jeu de cas représentatifs. L'observabilité doit permettre de comprendre le chemin suivi : prompt envoyé, outils choisis, résultats obtenus, réponse finale, coût et latence. Sans cette trace, déboguer un agent revient à enquêter dans le noir.

C'est tout ça qui sépare une démo impressionnante d'un produit fiable.

Tu n'as pas raté le train

Tu viens de reconstruire les concepts qui se cachent derrière une grande partie du vocabulaire de l'IA générative :

  • la mémoire est une fenêtre de contexte que ton application remplit.
  • le tool calling est une demande structurée que ton code choisit d'exécuter.
  • un agent est une boucle qui alterne raisonnement, action et observation.

Les frameworks ajoutent de la persistance, du routage, de l'observabilité et des abstractions utiles. Mais les principes fondamentaux restent les mêmes. Si tu veux voir ce que ces abstractions apportent sur un projet plus complet, j'ai détaillé la construction d'un tuteur IA avec LangGraph dans un autre article.

Tu n'as pas besoin d'un master en machine learning pour créer des produits IA. Tu dois comprendre où se trouve l'état, qui exécute les actions et comment le système s'arrête.