Lección 2 — Construire à la main

Le planificateur : décomposer sans tout lire

8 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

Si le planificateur lit les 40 documents pour décider qui lira quoi, vous avez payé l'agent unique avant même de lancer les sous-agents. S'il ne lit rien, il découpe à l'aveugle. Le compromis tient en une ligne par document : identifiant, date, type de source, titre. Le second problème arrive dès le premier essai réel : le modèle invente un doc-41, attribue 15 documents à une seule sous-tâche, ou rend un plan vide. Le plan est une proposition ; c'est le code qui décide de ce qui part chez les sous-agents.

Le mécanisme

flowchart LR
  C["Catalogue<br/>40 lignes, ~1 600 tokens"] --> M["gemini-3.8-flash<br/>response_schema=Plan"]
  M --> J["JSON brut"]
  J --> V{"valider_plan()"}
  V -->|"ids inconnus"| R1["retirés + alerte"]
  V -->|"plus de 8 documents"| R2["tronqué + alerte"]
  V -->|"sous-tâche vide"| R3["supprimée + alerte"]
  V -->|"plan vide"| E["ValueError : on s'arrête"]
  V --> P["Plan valide, ids st-1..st-n"]

Trois choix structurent cette étape :

  1. Le catalogue, pas le contenu. Le planificateur voit doc-03 | 2026-07-08 | benchmark-interne | Banc d'essai de juillet : Kestrel, Tamaris, Orvet. La date et le type de source suffisent à repérer les paires suspectes (une fiche fournisseur de février, un banc d'essai de juillet sur le même produit).
  2. Une sortie structurée (response_schema=Plan) : le SDK contraint la réponse au schéma JSON du modèle pydantic. Pas d'analyse de texte libre.
  3. Une validation qui répare ce qui se répare et refuse le reste. Un identifiant inventé ne doit jamais atteindre un sous-agent : il tenterait de le lire, échouerait, et brûlerait son budget.

La mise en œuvre

La consigne dit au modèle comment répartir, pas seulement quoi produire. Les règles viennent des échecs observés (voir plus bas).

files/orchestre/planificateur.py (consigne)
CONSIGNE_PLAN = """Tu es le planificateur d'un dossier de veille technique.
Tu reçois une demande et le catalogue d'un corpus (une ligne par document : id | date | type de source | titre).
Découpe la demande en 3 à {max_st} sous-tâches indépendantes, une par axe d'analyse.
Pour chaque sous-tâche : une question autonome (compréhensible sans voir les autres) et les documents utiles,
{max_docs} au maximum. Un document peut servir à deux sous-tâches si nécessaire.
Règles de répartition :
- deux documents qui traitent du même sujet à des dates différentes (fiche fournisseur puis mesure interne,
  bulletin puis correctif, rumeur puis annonce officielle) vont dans la MÊME sous-tâche ;
- les documents de cadrage du projet (dimensionnement, calendrier, exigences) vont dans la sous-tâche qu'ils contraignent ;
- n'écarte (liste ecartes) que les documents d'autres projets. Identifiants : st-1, st-2, ..."""

« Une question autonome » compte : le sous-agent ne verra jamais la demande globale. Une question comme « et pour Orvet ? » est inutilisable.

files/orchestre/planificateur.py (validation)
def valider_plan(plan, docs, max_st, max_docs):
    alertes, gardees = [], []
    for st in plan.sous_taches:
        inconnus = [i for i in st.doc_ids if i not in docs]
        if inconnus:
            alertes.append(f"{st.id} : identifiants inconnus retirés {inconnus}")
        ids = list(dict.fromkeys(i for i in st.doc_ids if i in docs))   # dédoublonne, garde l'ordre
        if len(ids) > max_docs:
            alertes.append(f"{st.id} : périmètre tronqué de {len(ids)} à {max_docs} documents")
            ids = ids[:max_docs]
        if ids:
            gardees.append(st.model_copy(update={"doc_ids": ids}))
        else:
            alertes.append(f"{st.id} : aucune source valide, sous-tâche supprimée")
    if len(gardees) > max_st:
        alertes.append(f"plan tronqué de {len(gardees)} à {max_st} sous-tâches")
        gardees = gardees[:max_st]
    if not gardees:
        raise ValueError("plan inutilisable : aucune sous-tâche avec des sources valides")
    for i, st in enumerate(gardees, 1):
        st.id = f"st-{i}"          # les ids viennent du code, pas du modèle
    return Plan(sous_taches=gardees, ecartes=plan.ecartes), alertes

L'appel lui-même est une sortie structurée classique, dans orchestrateur.py :

files/orchestre/orchestrateur.py (extrait)
brut = await _structure(modele, cfg.modele_plan, CONSIGNE_PLAN.format(max_st=cfg.max_sous_taches, max_docs=cfg.max_docs),
                        invite_plan(tache, docs), Plan, journal, sp, cfg.reflexion_plan)
plan, alertes = valider_plan(brut, docs, cfg.max_sous_taches, cfg.max_docs)
journal.fermer(sp, sous_taches=len(plan.sous_taches), alertes=alertes, ecartes=plan.ecartes)

Le test vérifie la réparation sans modèle :

.venv/bin/python -m pytest -q -k plan

Résultat obtenu (2026-10-15)

Plan rendu par gemini-3.8-flash (réflexion LOW) lors de make run : 1 656 tokens en entrée, 617 en sortie, 4,3 s, 0,0036 $.

st-1 ['doc-01','doc-02','doc-03','doc-04','doc-06','doc-07','doc-08','doc-13']  performances et stabilité
st-2 ['doc-09','doc-10','doc-11','doc-12','doc-13','doc-14','doc-15']           coût global sur 3 ans
st-3 ['doc-16','doc-17','doc-18','doc-19','doc-20','doc-21']                    sécurité et RGPD
st-4 ['doc-05','doc-22','doc-23','doc-24','doc-25','doc-26','doc-27','doc-28']  exploitation et pérennité
écartés : doc-29, doc-30, doc-31, doc-32 ... doc-40

Les paires C1 (02/03), C2 (09/10) et C3 (16/17) sont bien regroupées. Mais doc-29, doc-30 et doc-31 (note Tamaris, rumeur sur Kestrel, annonce de la fondation) ont été écartés : la sous-tâche st-4 avait atteint 8 documents. Conséquence mesurée à la leçon 5 : le dossier n'a pas mentionné la LTS 2029 et la contradiction C4 n'a pas été tranchée.

Ce qui casse en production

  • Le plafond de documents fait écarter des documents utiles. C'est le cas ci-dessus : le modèle respecte « 8 au maximum » en rangeant le surplus dans ecartes. Diagnostic : la liste des écartés contient des documents qui parlent des produits demandés. Parades : une sous-tâche de plus plutôt qu'un plafond plus haut ; un contrôle qui alerte quand un écarté mentionne un terme de la demande (exercice ci-dessous).
  • Paires contradictoires séparées. Lors d'un essai avec gemini-3.5-flash-lite comme planificateur et une consigne sans règles de répartition, doc-16 (faille sans correctif) est parti en « sécurité » et doc-17 (correctif 3.4.2) en « exploitation » : le dossier a affirmé que la faille n'était pas corrigée. Parade : la règle « même sujet, dates différentes, même sous-tâche » et un modèle plus solide pour ce rôle.
  • Documents de cadrage jugés hors sujet. Le même essai a écarté le compte rendu de dimensionnement (doc-13, 12 GPU en 2027) : sans lui, le coût de licence de Tamaris est incalculable. Parade : la règle sur les documents de cadrage, et ils peuvent servir deux sous-tâches (ici doc-13 en st-1 et st-2).
  • Identifiants du modèle réutilisés tels quels. Un plan qui renvoie deux fois st-2 écrase un résultat dans le dictionnaire des constats. Parade : renuméroter dans le code, toujours.

La règle de décision

Donnez au planificateur le modèle le plus solide de la chaîne : il est appelé une fois, et une erreur de découpage se paie dans toutes les branches. Ne lui montrez que des métadonnées tant qu'elles suffisent à répartir ; si les titres sont pauvres, ajoutez un résumé d'une phrase par document plutôt que le contenu. Surveillez deux signaux par exécution : le nombre d'alertes de validation et la liste des écartés.

Pratique

Le planificateur renvoie une sous-tâche dont les trois documents sont doc-41, doc-42 et doc-43, qui n'existent pas. Que doit faire l'orchestrateur ?

Vous constatez sur dix exécutions que le planificateur sépare souvent le bulletin de sécurité d'avril et le correctif d'août. Quelle correction a le meilleur rapport efficacité / coût ?

Exercice. Ajoutez à valider_plan une alerte quand un document écarté contient dans son titre un terme de la demande (Kestrel, Tamaris, Orvet). Sur le plan ci-dessus, combien d'alertes obtenez-vous ?

Voir la solution
def ecartes_suspects(plan, docs, termes=("kestrel", "tamaris", "orvet")):
    return [i for i in plan.ecartes
            if i in docs and any(t in docs[i].titre.lower() for t in termes)]

Appelez-la dans valider_plan et ajoutez une alerte par document. Sur le plan du 2026-10-15, elle signale doc-29 (« Tamaris Inference : pourquoi nous choisir »), doc-30 (« Kestrel Serve : la fin annoncée ? ») et doc-31 (« Kestrel Serve rejoint une fondation ») : trois alertes, exactement les documents dont l'absence a coûté C4 et F4. Étape suivante possible : rattacher automatiquement chaque écarté suspect à la sous-tâche dont la question cite le même produit, si elle est sous le plafond.

  • Je sais pourquoi le planificateur voit un catalogue et pas le contenu
  • Je sais écrire une validation de plan qui répare, alerte et refuse
  • Je sais lire la liste des écartés comme un signal de qualité
  • pytest -k plan passe
Siguiente