درس 5 — Construire à la main

Agrégation et contradictions

9 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Cinq constats arrivent. L'un dit « Kestrel : p95 de 120 ms », un autre « 340 ms ». Une synthèse naïve recopie les deux, ou garde le premier venu, ou fait la moyenne. Le comité d'architecture lira un chiffre et décidera dessus. L'agrégation est l'étape où un système multi-agents gagne ou perd sa crédibilité : il faut donner au rédacteur de quoi arbitrer, une règle explicite pour le faire, puis vérifier par du code ce qu'il a rendu.

Le mécanisme

flowchart LR
  C["Constats<br/>(affirmations + sources)"] --> P["preuves()<br/>déterministe"]
  M["Métadonnées<br/>date, type de source"] --> P
  E["Échecs<br/>NON INSTRUITE"] --> P
  P --> S["Synthèse<br/>règle d'arbitrage en 3 niveaux"]
  S --> K1["citations_invalides()"]
  S --> K2["arbitrages_suspects()"]
  S --> G["noter() : grille"]
  1. Le dossier de preuves (preuves()), sans modèle : chaque affirmation est posée à côté de la date et du type de ses sources. Le rédacteur ne voit jamais les documents, mais il sait que doc-02 est une note fournisseur de février et doc-03 un banc d'essai interne de juillet.
  2. La règle d'arbitrage, écrite dans la consigne et appliquée dans l'ordre : mesure interne > donnée fournisseur ou blog ; annonce officielle ou correctif > article tiers ; à fiabilité égale, le plus récent. Chaque contradiction tranchée cite la règle utilisée.
  3. Les contrôles en code après la synthèse : citations vers des documents qu'aucun sous-agent n'a lus, arbitrages où la version retenue n'est ni plus récente ni plus fiable que l'écartée.
  4. La grille : une note déterministe, reproductible, qui sert à comparer les variantes (leçon 7).

La mise en œuvre

files/orchestre/synthese.py (dossier de preuves)
def preuves(plan, constats, docs, erreurs):
    lignes = []
    for st in plan.sous_taches:
        lignes.append(f"## {st.id} : {st.question}")
        c = constats.get(st.id)
        if c is None:
            lignes.append(f"NON INSTRUITE ({erreurs.get(st.id, 'échec')})")
            continue
        for a in c.affirmations:
            src = "; ".join(f"{i} {docs[i].date} {docs[i].source}" for i in a.sources if i in docs)
            lignes.append(f"- {a.sujet} : {a.enonce} (sources : {src or 'aucune valide'})")
        lignes += [f"- lacune : {l}" for l in c.lacunes]
    return "\n".join(lignes)

La consigne définit ce qu'est une contradiction, parce que sans définition le modèle n'en liste aucune (voir plus bas) :

files/orchestre/synthese.py (consigne, extrait)
Une contradiction = deux valeurs différentes pour la même grandeur ou le même statut d'un même produit
(latence, licence, état d'une faille, pérennité), même si elles viennent de sous-tâches différentes
et même si les conditions de mesure expliquent l'écart. Tranche chacune selon cette règle, dans l'ordre :
1. une mesure interne au protocole commun l'emporte sur une donnée fournisseur ou un blog ;
2. une annonce officielle ou un correctif publié l'emporte sur un article tiers ;
3. à fiabilité égale, le document le plus récent l'emporte.

Le contrôle des arbitrages compare les métadonnées des sources retenues et écartées :

files/orchestre/synthese.py (contrôle)
RANG = {"benchmark-interne": 3, "audit": 3, "annonce-officielle": 2, "changelog": 2, "bulletin-securite": 2,
        "devis": 2, "compte-rendu": 2, "ticket": 2, "note-technique": 1, "note-fournisseur": 1, "blog": 0}

def arbitrages_suspects(contradictions, docs):
    suspects = []
    for c in contradictions:
        r = [docs[i] for i in c.sources_retenues if i in docs]
        e = [docs[i] for i in c.sources_ecartees if i in docs]
        if not r or not e:
            continue
        plus_recent = max(d.date for d in r) >= max(d.date for d in e)
        plus_fiable = max(RANG.get(d.source, 1) for d in r) > max(RANG.get(d.source, 1) for d in e)
        if not (plus_recent or plus_fiable):
            suspects.append(c.sujet)
    return suspects

La grille (grille.json, evaluation.py) pondère : 50 points pour les 12 faits attendus (motifs regex dans le dossier), 30 pour les 4 contradictions plantées (version retenue et version écartée dans le bon sens), 20 pour la part de citations valides, moins 5 par document hors sujet cité.

Résultat obtenu (2026-10-15)

Contradictions rendues par la synthèse de make run :

Performances de Kestrel Serve | retenu : p95 340 ms, 1 900 tok/s | écarté : 120 ms (p95), 2 400 tok/s
  règle 1 : une mesure interne au protocole commun l'emporte sur une donnée fournisseur ou un blog.
Régime de licence de Tamaris Inference | retenu : licence TSL depuis juin 2026 | écarté : Apache 2.0 sans coût
  règle 2 : une annonce officielle l'emporte sur un article de blog tiers ; règle 3 : le plus récent l'emporte
qualité 80.8/100 : faits 11/12, contradictions 2/4 (C1, C2), citations 25/25, bruit cité : aucun

Les deux contradictions manquées s'expliquent par la trajectoire, pas par la synthèse : C4 parce que le planificateur a écarté doc-30 et doc-31 (leçon 2), C3 parce que le sous-agent sécurité a fusionné les deux documents dans une seule affirmation (« Orvet présentait une vulnérabilité critique... corrigée en 3.4.2 »). Le rédacteur n'a jamais vu deux versions.

Ce qui casse en production

  • Aucune contradiction listée. Premier essai avec une consigne qui disait seulement « repère les contradictions » : liste vide, alors que le dossier citait 120 ms et 340 ms côte à côte. Le modèle jugeait l'écart « expliqué par les conditions de mesure ». Parade : la définition explicite ci-dessus, qui exige de lister même un écart explicable.
  • « Retenu » et « écarté » inversés. Avec gemini-3.5-flash-lite en rédacteur, une exécution a rendu pour ORV-SA-2026-004 : retenu « sans correctif [doc-16] », écarté « corrigée en 3.4.2 [doc-17] », avec la justification « le document le plus récent l'emporte (doc-17) ». La règle était juste, les champs inversés. Parade : arbitrages_suspects() le détecte (le test test_arbitrage_inverse_detecte rejoue ce cas), et un modèle plus solide pour ce rôle.
  • Un sous-agent qui tranche à la place de la synthèse. C'est le cas de C3 ci-dessus, et de C2 dans une autre exécution où le sous-agent coûts n'a gardé que la licence TSL. Diagnostic : le journal enregistre pour chaque sous-agent lus_non_cites, les documents lus mais cités par aucune affirmation ; doc-09 y figurait. Parade : la consigne « une version ancienne démentie reste une affirmation ». Elle a réduit ces oublis sans les supprimer : 0 ou 1 document par branche dans make run, mais encore 3 pour une branche de l'exécution ADK parallèle de la leçon 7.
  • Un calcul faux dans un constat. Un sous-agent coûts a écrit « 6 GPU en 2026 et 12 en 2027, soit 18 GPU cumulés pour le calcul énergétique » : une somme de parcs annuels qui n'a pas de sens physique. Aucune règle d'arbitrage ne l'attrape, car il n'y a pas de contradiction. Parade : les calculs se font en code (outil de calcul ou post-traitement), le modèle fournit les hypothèses.
  • Une source inventée. Le faux modèle des tests cite [doc-77] exprès ; citations_invalides() le renvoie. Sur les exécutions réelles du 2026-10-15, aucune citation invalide, mais le contrôle coûte une ligne.

La règle de décision

Donnez au rédacteur les métadonnées des sources, une définition de la contradiction et une règle d'arbitrage ordonnée ; ne lui demandez pas de « faire au mieux ». Vérifiez en code tout ce qui peut l'être (citations, sens des arbitrages, chiffres) et gardez au modèle ce qui demande du jugement (rédaction, recommandation). Quand une contradiction plantée est manquée, remontez la trajectoire avant de retoucher la consigne de synthèse : dans l'exécution ci-dessus, les deux contradictions manquées venaient de l'amont.

Pratique

La grille indique C3 manquée. Le dossier de preuves ne contient qu'une affirmation sur ORV-SA-2026-004, sourcée doc-17. Le journal montre doc-16 dans lus_non_cites du sous-agent sécurité. Où corrigez-vous ?

Vous hésitez entre faire arbitrer les contradictions par le modèle de synthèse ou par une fonction Python qui applique les rangs de source et les dates. Que recommandez-vous ?

Exercice. Ajoutez un contrôle qui signale chaque sous-tâche dont les lus_non_cites ne sont pas vides, et faites-le figurer dans la section « Limites du dossier » en l'ajoutant au dossier de preuves.

Voir la solution

Dans l'orchestrateur, gardez les documents lus non cités par sous-tâche, puis passez-les à preuves() :

non_cites: dict[str, list[str]] = {}
# dans une(), après succès :
cites = {i for a in c.affirmations for i in a.sources}
non_cites[st.id] = [i for i in perimetre.lus if i not in cites]

# dans preuves(), après les lacunes d'une sous-tâche :
for i in non_cites.get(st.id, []):
    lignes.append(f"- lu mais non exploité : {i} {docs[i].date} {docs[i].source} « {docs[i].titre} »")

La synthèse voit alors qu'un bulletin de sécurité d'avril a été lu sans être rapporté. Elle ne peut pas inventer son contenu, mais elle peut le signaler en limite, et vous pouvez décider de relancer la sous-tâche avec une consigne plus stricte quand ce signal apparaît.

  • Je sais construire un dossier de preuves qui porte les métadonnées des sources
  • Je sais écrire une règle d'arbitrage ordonnée et la faire citer
  • Je sais vérifier en code citations et sens des arbitrages
  • Je sais remonter une contradiction manquée jusqu'à l'étape fautive
التالي