← Notes

Deux agents, un aiguilleur : l'orchestrateur qui décide qui répond

· ia · claude · process · 10 min · EN

Jusqu’ici, ma série sur les agents tournait autour d’un seul agent : le harness qui transforme un LLM en exécutant, le Claude Agent SDK qui te livre la boucle. Un agent, une tâche, une réponse.

La vraie bascule arrive quand tu en veux deux. Et la surprise, c’est qu’elle ne se joue pas dans les agents. Elle se joue dans la pièce qui ne répond jamais : l’aiguilleur.

Le use case que je voulais : un petit assistant sur mon propre site. Tu lui poses une question, et selon ce que tu demandes, il va soit fouiller mes notes, soit chercher l’actu sur le web. Deux compétences qui n’ont rien à voir. La question intéressante n’est pas « comment chacun répond » — c’est « qui décide lequel des deux bosse ? »

Le pattern a un nom : le routeur

Ce que je décris est un pattern d’agents documenté : le routing. Un composant classe l’intention de la requête, puis la dispatche vers le spécialiste adapté. Pas de magie — c’est exactement ce que fait un standard téléphonique : il ne résout pas ton problème, il te met en relation avec la bonne personne.

Question


Orchestrateur ──► détecte l'intention

   ├─ "info fraîche / externe"  ──►  Agent Web     (cherche sur internet)
   └─ "info qui vit chez moi"   ──►  Agent Interne (fouille mes notes)

Trois composants, deux agents. L’orchestrateur n’est pas un troisième agent qui « fait » quelque chose — c’est un trieur. Et c’est lui le sujet de l’article, parce que c’est lui qui rend l’ensemble intelligent.

Trois composants, surtout du markdown

Avant le code, l’essentiel : chacune de ces trois pièces n’est pas un bloc de TypeScript, c’est un dossier. Le pattern que je répète depuis l’article harnessagent.md + skill + tools — s’applique tel quel. Voici l’arborescence complète du système :

.claude/
├─ CLAUDE.md                          ← agent.md : le contrat commun aux 3
└─ skills/
   ├─ routeur-intention/SKILL.md      ← l'orchestrateur
   ├─ recherche-web/SKILL.md          ← l'agent web
   └─ recherche-interne/SKILL.md      ← l'agent interne

Trois SKILL.md, un CLAUDE.md, et un petit bout de code qui câble. Détaillons chaque pièce.

L’agent.md — le contrat que les trois partagent

Le CLAUDE.md, c’est l’agent.md : le ton, la langue, les règles que tous les agents respectent. On l’écrit une fois, il s’applique à l’orchestrateur comme aux spécialistes.

.claude/CLAUDE.md
# Assistant du site — contrat commun

- Réponds en français, ton direct, zéro blabla.
- L'essentiel d'abord, le détail ensuite.
- Tu ne sais pas ? Tu le dis. Tu n'inventes jamais une source.

L’orchestrateur — un skill sans aucun tool

Sa seule responsabilité : transformer une question floue en décision nette. Son SKILL.md ne lui donne aucun outil — un orchestrateur qui peut chercher ou lire, c’est un orchestrateur qui va faire le travail des spécialistes au lieu de les aiguiller.

.claude/skills/routeur-intention/SKILL.md
---
name: routeur-intention
description: Classe une question entrante et choisit l'agent qui répond.
allowed-tools: []          # aucun outil : il décide, c'est tout
---

Tu ne réponds JAMAIS à la question. Tu la classes.

- "web"     → info externe et fraîche (actualité, prix, sorties).
- "interne" → info qui vit dans mes notes / mon site.

Rends { route, reason, reformulated }. Reformule la question
pour le spécialiste — sans y répondre.

L’agent web — un skill, un seul tool : WebSearch

.claude/skills/recherche-web/SKILL.md
---
name: recherche-web
description: Répond aux questions qui demandent une info externe à jour.
allowed-tools: WebSearch    # il voit le web, pas mes fichiers
---

Cherche sur le web avant de répondre. Cite tes sources (titre + lien).
Si les sources se contredisent, dis-le plutôt que de trancher au hasard.

L’agent interne — un skill, des tools de lecture (et pas de RAG)

.claude/skills/recherche-interne/SKILL.md
---
name: recherche-interne
description: Répond aux questions sur mes notes, mon site, mes specs.
allowed-tools: Read, Glob, Grep   # il voit mes fichiers, pas le web
---

Fouille ./content TOI-MÊME — recherche agentique, pas de RAG.
Glob pour cibler, Grep pour trouver, Read pour lire en entier.
Reste fidèle à ce que j'ai écrit ; ne complète pas avec du savoir général.

Le point clé est dans le frontmatter allowed-tools : les outils ne vivent pas dans le code, ils vivent dans le skill. Charger un skill, c’est charger ses tools. L’agent web ne peut pas lire mes fichiers ; l’agent interne ne peut pas toucher au réseau. Ce n’est pas qu’une question de propreté — c’est de la sécurité par périmètre, déclarée en deux mots de markdown.

La carte complète

Trois lignes, et tu vois tout le système — agent.md, skill, tools, modèle :

Composantagent.mdskilltoolsmodèle
OrchestrateurCLAUDE.mdrouteur-intentionaucunHaiku (rapide)
Agent WebCLAUDE.mdrecherche-webWebSearchOpus
Agent InterneCLAUDE.mdrecherche-interneRead · Glob · GrepOpus

Le CLAUDE.md est commun ; ce qui distingue les trois, c’est le skill et ses tools. Et le modèle : classer une question est trivial, donc l’orchestrateur tourne sur un Haiku rapide et pas cher, pendant qu’Opus est réservé à la réponse. Payer le bon modèle au bon endroit, c’est l’un des vrais gains du routeur.

Le code ne fait que câbler

Une fois les fichiers écrits, le TypeScript est mince : il charge le bon skill et passe le relais. La sortie structurée force l’orchestrateur à rendre une décision typée, pas une phrase.

import { query } from "@anthropic-ai/claude-agent-sdk";

const ROUTE_SCHEMA = {
  type: "object",
  properties: {
    route: { type: "string", enum: ["web", "interne"] },
    reason: { type: "string" },
    reformulated: { type: "string" },
  },
  required: ["route", "reason", "reformulated"],
};

// 1. L'orchestrateur classe — son skill, aucun tool, un modèle rapide.
const decision = await query({
  prompt: question,
  options: {
    settingSources: ["project"],            // charge CLAUDE.md
    skills: ["routeur-intention"],          // → allowed-tools: [] vient du skill
    model: "claude-haiku-4-5-20251001",
    outputFormat: { type: "json_schema", schema: ROUTE_SCHEMA },
  },
});

// 2. On dispatche vers le spécialiste — il charge SON skill, donc SES tools.
const SKILL = { web: "recherche-web", interne: "recherche-interne" };
const { route, reformulated } = decision.structured_output;
const answer = await query({
  prompt: reformulated,
  options: {
    settingSources: ["project"],
    skills: [SKILL[route]],                 // le skill embarque ses allowed-tools
  },
});

Remarque ce qui n’est pas dans le code : aucune liste d’outils en dur, aucun prompt système géant. Le comportement de chaque agent est dans son SKILL.md ; le code se contente de choisir lequel charger. C’est ça, « tu écris surtout du markdown ».

Au fait : comment l’orchestrateur « appelle » l’agent ?

Question qu’on survole toujours. La réponse tient en une phrase : les agents ne partagent aucun canal magique — « appeler un agent », c’est un appel d’outil. Et il y a deux façons de l’organiser.

Style A — l’orchestration vit dans ton code. C’est ce que fait le code ci-dessus : l’orchestrateur n’appelle pas vraiment le spécialiste, c’est mon TypeScript qui enchaîne deux query() et passe le résultat de l’un à l’autre. La « communication » entre les deux agents, c’est une variable JS — reformulated. Le canal, c’est le programme hôte. Explicite, débuggable, tu forces tout.

Style B — l’orchestrateur délègue lui-même. Ici l’orchestrateur est un agent qui connaît ses sous-agents et possède l’outil Task. Pendant sa propre boucle, il décide d’émettre un appel Task(...) ; le SDK lance le sous-agent et lui réinjecte le résultat. Tu déclares les sous-agents inline :

const answer = await query({
  prompt: question,
  options: {
    allowedTools: ["Task"],              // l'orchestrateur peut déléguer
    agents: {                            // les sous-agents, définis inline
      "recherche-web": {
        description: "Info externe et fraîche : actu, prix, sorties",
        prompt: "Tu cherches sur le web et cites tes sources.",
        tools: ["WebSearch"],
      },
      "recherche-interne": {
        description: "Questions sur mes notes / mon site",
        prompt: "Recherche agentique sur ./content, pas de RAG.",
        tools: ["Read", "Glob", "Grep"],
      },
    },
  },
});

(Mêmes sous-agents définissables en fichiers .claude/agents/*.md, frontmatter name / description / tools / model — le pendant fichier des skills.)

Quel que soit le style, le protocole est identique et tient en trois points :

  1. Contexte isolé — le sous-agent tourne dans sa propre fenêtre vierge. Il ne voit pas l’historique du parent.
  2. Aller — le parent lui passe juste un prompt : la tâche reformulée. Rien d’autre.
  3. Retour — seul le résultat final remonte, comme résultat d’outil dans la boucle du parent. Les étapes intermédiaires du sous-agent ne le polluent pas.

Lequel choisir ? Pour un routeur déterministe comme le mien — l’orchestrateur classe avec Haiku et une sortie structurée — le style A garde la main : c’est toi qui forces la décision. Le style B est plus autonome, mais tu délègues aussi le choix de déléguer au modèle ; réserve-le aux cas de délégation ouverte (« débrouille-toi, sous-traite ce qu’il faut »), pas au routage que tu veux contrôler.

Pourquoi pas un seul agent avec tous les outils ?

La question honnête, parce qu’elle a une vraie réponse — et parfois la réponse est « tu as raison, un seul suffit ».

Le routeur se justifie quand les spécialistes divergent vraiment :

  • Tools différentsWebSearch d’un côté, lecture de fichiers de l’autre. C’est mon cas, et c’est la meilleure raison.
  • Skills affûtés — un agent « cite tes sources web », l’autre « reste fidèle à ce que j’ai écrit ». Deux postures incompatibles dans un seul prompt.
  • Modèles différents — Haiku pour classer, Opus pour analyser. Tu ne peux pas régler ça finement dans un agent monolithique.

Si rien de tout ça n’est vrai, fais simple : un agent, les deux tools, un bon skill. L’architecture, c’est une réponse à une complexité réelle — pas un trophée.

Ce que ça m’apprend

J’ai écrit il y a peu que le métier de dev glissait vers le pilotage : la valeur n’est plus dans la production, mais dans le cadrage et le jugement. Ce petit assistant en est la version miniature et littérale.

Les trois SKILL.md sont presque banals — quelques lignes, un tool, une consigne. Toute l’intelligence du système vit dans l’aiguillage. Le composant qui ne répond jamais est celui qui décide de tout. Passer d’un agent à deux, ce n’est donc pas doubler le travail de réponse : c’est ajouter la seule question qui compte vraiment — « qui est le mieux placé pour ça ? »

Et une fois que tu tiens un routeur à deux voies, en ajouter une troisième ne coûte presque rien : un dossier SKILL.md de plus, une entrée de plus dans l’enum. L’aiguilleur, lui, ne change pas. C’est exactement ce qui rend ce pattern aussi solide qu’il en a l’air simple.