Retour au blog
ai

Je n'ai pas su expliquer ce qu'était un harness en entretien. Alors j'en ai construit un.

Par Youcef EL KAMEL
7 min de lecture

Je n'ai pas su expliquer ce qu'était un harness en entretien. Alors j'en ai construit un.

Vendredi, j’étais en entretien avec la compagnie X.

Ça se passait bien. On parle agents, spec-first, vélocité. Mon terrain.

Et puis la question tombe :

« C’est quoi, exactement, un harness ? »

Blanc.

J’ai bafouillé un truc sur « l’environnement autour du modèle ». Vague. Mou. Pas moi.

Le pire ? J’utilise des harness tous les jours. Mes agents tournent la nuit, gèrent ma newsletter, surveillent mes commits, mergent mes PR après review. Je vis dedans.

Mais utiliser un truc et savoir l’expliquer, ce sont deux compétences différentes. Et ce jour-là, la deuxième m’a lâché.

Alors l’après-midi même, j’ai fait la seule chose qui me remet les idées en place : j’ai ouvert un fichier vide et j’en ai construit un. 200 lignes de TypeScript. Le soir, je pouvais expliquer un harness à n’importe qui.

Cet article, c’est ce que j’aurais dû répondre. Avec le code.

C’est quoi un harness, vraiment

Un LLM seul ne fait rien. Il reçoit du texte, il rend du texte. Point.

Il ne lit pas tes fichiers. Il ne lance pas tes tests. Il ne se souvient de rien entre deux appels.

Le harness, c’est tout ce qu’on met autour du modèle pour transformer ce générateur de texte en agent qui agit.

Quatre composants. Pas plus.

1. La boucle

Le cœur. Un while qui appelle le modèle, exécute ce qu’il demande, lui renvoie le résultat, et recommence. Tant que le modèle veut agir, la boucle tourne. Quand il a fini, elle s’arrête.

2. Les tools

Les mains de l’agent. Des fonctions que toi tu écris — lire un fichier, lancer une commande, chercher sur le web — et que tu déclares au modèle. Le modèle ne les exécute jamais lui-même. Il demande. Le harness exécute.

3. La mémoire

La liste des messages. Chaque tour ajoute ce que le modèle a dit et ce que les tools ont répondu. C’est ça, la « mémoire » d’un agent : un tableau qui grossit. Rien de magique.

4. Les garde-fous

Ce qui empêche la boucle de partir en vrille. Nombre max de tours. Budget. Tools interdits. Timeout. Sans ça, tu n’as pas un agent, tu as une facture API qui tourne en boucle.

Voilà. Boucle, tools, mémoire, garde-fous. Tout le reste — sous-agents, skills, sandboxes — c’est du raffinement au-dessus de ces quatre briques.

Les 200 lignes qui comptent

Voici le harness que j’ai codé vendredi après-midi. Simplifié pour la lecture, mais c’est la vraie structure.

On commence par la mémoire. Un simple tableau de messages :

type Message =
  | { role: "user"; content: string }
  | { role: "assistant"; content: string; toolCalls?: ToolCall[] }
  | { role: "tool"; toolCallId: string; content: string };

interface ToolCall {
  id: string;
  name: string;
  input: Record<string, unknown>;
}

Ensuite, les tools. Un registre de fonctions, avec leur schéma pour que le modèle sache quoi appeler :

const tools = {
  read_file: {
    description: "Lit un fichier et retourne son contenu",
    run: async (input: { path: string }) =>
      fs.readFile(input.path, "utf8"),
  },
  list_dir: {
    description: "Liste les fichiers d'un dossier",
    run: async (input: { path: string }) =>
      (await fs.readdir(input.path)).join("\n"),
  },
  run_command: {
    description: "Exécute une commande shell et retourne stdout",
    run: async (input: { cmd: string }) =>
      execSync(input.cmd, { timeout: 30_000 }).toString(),
  },
};

Trois tools suffisent pour un agent qui explore un repo et lance des tests. Tu en ajoutes selon le besoin. C’est tout l’intérêt : les tools définissent ce que ton agent peut faire, donc ce qu’il ne peut pas faire.

Le dispatch, maintenant. Le modèle demande un tool, le harness l’exécute — et surtout, il renvoie les erreurs au modèle au lieu de crasher :

async function dispatch(call: ToolCall): Promise<string> {
  const tool = tools[call.name as keyof typeof tools];
  if (!tool) return `Erreur : tool inconnu "${call.name}"`;

  try {
    return await tool.run(call.input as never);
  } catch (err) {
    // L'erreur repart dans le contexte : le modèle
    // la lit et corrige son prochain essai lui-même.
    return `Erreur : ${String(err)}`;
  }
}

Ce catch est le détail qui m’a le plus appris. Un agent robuste, ce n’est pas un agent qui ne se trompe jamais. C’est un agent qui voit ses erreurs et réessaie autrement.

Et enfin, la boucle. Le vrai cœur du harness :

const MAX_TURNS = 20; // garde-fou : jamais de boucle infinie

async function runAgent(task: string): Promise<string> {
  const messages: Message[] = [{ role: "user", content: task }];

  for (let turn = 0; turn < MAX_TURNS; turn++) {
    // 1. On donne TOUT l'historique au modèle
    const response = await llm.chat({
      system: SYSTEM_PROMPT,
      messages,
      tools: toolSchemas(tools),
    });

    // 2. La mémoire s'accumule
    messages.push({
      role: "assistant",
      content: response.text,
      toolCalls: response.toolCalls,
    });

    // 3. Condition d'arrêt : plus de tool call = travail fini
    if (response.toolCalls.length === 0) {
      return response.text;
    }

    // 4. Sinon : on exécute, on renvoie, on reboucle
    for (const call of response.toolCalls) {
      const result = await dispatch(call);
      messages.push({
        role: "tool",
        toolCallId: call.id,
        content: result,
      });
    }
  }

  return "Arrêt : limite de tours atteinte.";
}

Relis cette boucle. Elle tient en une phrase :

appelle le modèle → s’il demande des tools, exécute-les et renvoie les résultats → sinon, c’est fini.

Quand j’ai vu ce for tourner pour la première fois — l’agent qui liste mon repo, lit trois fichiers, lance les tests, corrige, relance — j’ai compris pourquoi j’avais bafouillé en entretien.

Je pensais que « harness » cachait un concept compliqué. En vrai, tout ce que j’utilise au quotidien — Claude Code, mes crons, mon agent Hermes — c’est cette boucle-là, avec de meilleurs tools, une meilleure gestion du contexte et plus de garde-fous.

Le modèle fait l’intelligence. Le harness fait la structure. Et c’est toi qui écris la structure.

C’est exactement ma vision de l’IA : un accélérateur, pas un remplaçant. Le modèle ne décide ni des tools disponibles, ni des limites, ni de ce qu’est un travail « fini ». Ça, c’est ton job. Et c’est là que tes années d’expérience comptent.

Ce que je répondrais aujourd’hui

Si l’entretien de vendredi se rejouait, voici ma réponse en deux phrases :

« Un harness, c’est la boucle qui entoure le LLM : elle lui présente des tools, exécute ses tool calls, réinjecte les résultats dans le contexte, et recommence jusqu’à ce que le modèle rende un travail fini. Le modèle fournit le raisonnement ; le harness fournit les mains, la mémoire et les limites. »

Deux phrases. Quatre composants. Une après-midi pour les mériter.

La vraie leçon n’est pas technique. Rater une question, ça arrive. Ce qui compte, c’est le délai entre « je ne sais pas » et « maintenant je sais » — et le fait de le prouver avec du code plutôt qu’avec une excuse.

Vendredi matin, je ne savais pas expliquer un harness. Vendredi soir, j’en avais un qui tournait.


Ce genre de sujet — agents, harness, IA branchée sur du vrai code — c’est littéralement ce que je fais en mission freelance. Si ton équipe est en train de monter ça, on peut en parler 30 minutes : calendly.com/yelkamel/30min.

#harness #agents IA #boucle agent #tool calls #LLM #TypeScript #entretien #AI engineering