La divulgation progressive
Six skills, c'est déjà 9 261 tokens si on colle tout dans le prompt système : les corps, les barèmes, les scripts. À soixante skills, l'ordre de grandeur passe à 90 000 tokens par question, avant même que l'utilisateur ait écrit un mot. La plupart des questions n'ont besoin d'aucun skill, les autres d'un seul. Le harness doit donc envoyer peu au départ et charger le reste au moment précis où le modèle le demande.
Le mécanisme
La spécification décrit trois niveaux. Le harness en est responsable ; le modèle n'en voit que les effets.
| Niveau | Contenu | Quand | Coût indicatif (spécification) | Mesuré ici |
|---|---|---|---|---|
| 1. Catalogue | name + description de chaque skill | à chaque requête | 50 à 100 tokens par skill | 1 208 tokens pour 6 skills, instructions comprises |
| 2. Instructions | corps de SKILL.md | quand le modèle active le skill | moins de 5 000 recommandés | 411 à 1 423 tokens |
| 3. Ressources | scripts/, references/, assets/ | quand les instructions les citent | variable | 69 à 886 tokens par fichier |
Nos descriptions font environ 700 caractères, avec phrases déclencheuses bilingues : 170 à 200 tokens chacune, au-dessus de la fourchette de la spécification. C'est un choix : la description porte tout le déclenchement (leçon suivante).
sequenceDiagram participant U as Utilisateur participant H as Harness participant M as Gemini participant B as Bac à sable U->>H: « Contrôle ma note de frais : … » H->>M: instructions + catalogue (niveau 1) + 3 outils M-->>H: activate_skill(expense-report-checker) H->>M: corps SKILL.md + liste des ressources (niveau 2) M-->>H: run_skill_script(scripts/check_expenses.py, --csv …) H->>B: copie jetable, sans réseau B-->>H: JSON : total_remboursable 140.0 M-->>H: read_skill_file(assets/rapport.md) (niveau 3) M-->>H: réponse finale
La mise en œuvre
Le chargeur (loader.py) expose trois outils au modèle. Le nom du skill est une énumération dans le schéma JSON : le modèle reçoit la liste exacte des noms valides au lieu d'en deviner un, et le harness rejette tout nom absent du catalogue.
def tool(self) -> types.Tool:
names = {"type": "string", "enum": sorted(self.skills)}
return types.Tool(function_declarations=[
types.FunctionDeclaration(name="activate_skill", ...,
parameters_json_schema={"type": "object", "properties": {"name": names}, "required": ["name"]}),
types.FunctionDeclaration(name="read_skill_file", ...),
types.FunctionDeclaration(name="run_skill_script", ...),
])L'activation renvoie le corps entouré de balises et liste les ressources sans les lire. Une seconde activation du même skill ne réinjecte rien.
def activate(self, name: str) -> str:
s = self._get(name)
if name in self.active:
return f'<skill_content name="{name}">déjà chargé plus haut dans la conversation</skill_content>'
self.active.append(name)
res = "\n".join(f" <file>{r}</file>" for r in s.resources)
return (f'<skill_content name="{name}" version="{s.version}">\n{s.body}\n\n'
f"<skill_resources>\n{res}\n</skill_resources>\n</skill_content>")La lecture d'un fichier passe par une liste blanche : seul un chemin présent dans skill.resources est servi. ../hr-policy-qa/SKILL.md, /etc/passwd ou SKILL.md lui-même sont refusés, et la réponse d'erreur part au modèle au lieu de lever une exception. La boucle (harness.py) désactive l'exécution automatique des fonctions du SDK et cumule usage_metadata.prompt_token_count à chaque appel : c'est la seule mesure qui compte, car chaque tour renvoie tout l'historique.
Résultat obtenu
python run.py tokens, API count_tokens, gemini-3.5-flash, 2026-10-15 (extrait) :
{
"niveau1_catalogue": 1208,
"niveau2_corps": {"csv-data-profiler": 411, "expense-report-checker": 543, "hr-policy-qa": 1177,
"leave-balance-calculator": 538, "meeting-notes-to-actions": 1309, "ticket-triage": 1423},
"tout_charger": 9261,
"economie_par_requete_sans_skill": 8053,
"ratio": 7.7
}Tokens d'entrée cumulés sur des requêtes réelles du même jour (run.py eval-quality et run.py e2e) :
| Requête | Progressif | Tout charger | Appels (progressif / tout) |
|---|---|---|---|
| Hors sujet (médiane contre moyenne) | 1 611 | non mesuré, au moins 9 261 | 1 / - |
| Compte rendu de réunion | 4 994 | 19 827 | 2 / 2 |
| Tri de 6 tickets | 5 128 | 19 844 | 2 / 2 |
| Note de frais avec script | 14 333 | 30 489 | 5 / 3 |
Le gain est d'un facteur 2 à 4 par requête, pas 7,7 : le mode progressif fait plus d'allers-retours, et chacun renvoie l'historique entier. Le cas de la note de frais le montre : cinq appels (activer, exécuter, lire le gabarit, lire la politique, répondre) contre trois.
Ce qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
| Le modèle « oublie » la procédure au 15e tour | la compaction du contexte a résumé le corps du skill | marquer les blocs <skill_content> comme protégés lors de la compaction |
| Le même corps apparaît trois fois dans l'historique | pas de déduplication des activations | liste active, réponse courte si déjà chargé |
Le modèle lit tous les fichiers de references/ « par sécurité » | instructions vagues ; chaque lecture coûte un tour et des tokens | écrire dans le corps quand lire chaque fichier (« si l'utilisateur conteste une règle ») |
| Coût en hausse malgré la divulgation progressive | un tour supplémentaire renvoie tout l'historique ; avec 8 outils lus, le progressif dépasse le tout-charger | suivre les tokens cumulés par requête, pas la taille du prompt système |
| Le modèle appelle un skill qui n'existe plus | catalogue mis en cache côté client après suppression | énumération régénérée à chaque session, skills filtrés retirés du catalogue |
La règle de décision
Utilisez la divulgation progressive dès que le total « tout charger » dépasse quelques milliers de tokens ou que la majorité des requêtes n'utilise aucun skill. Restez en chargement complet pour deux ou trois skills courts appelés presque à chaque requête : vous économisez un tour, et un préfixe stable profite du cache de prompt. Mesurez toujours les tokens cumulés d'une requête réelle, jamais seulement la taille du catalogue.
Pratique
Votre catalogue compte 4 skills de 300 tokens de corps chacun, et 90 % des requêtes en utilisent un. Le passage en divulgation progressive a augmenté la facture. Quelle explication retenez-vous ?
Avec peu de skills courts et presque toujours utilisés, le niveau 2 n'économise presque rien, alors que chaque activation ajoute un appel complet. L'énumération coûte quelques dizaines de tokens ; le format XML ou la mesure n'expliquent pas une hausse de facture.
Un skill de 40 lignes cite un fichier references/bareme-2026.md de 12 000 tokens. Le modèle le lit à chaque activation. Que changez-vous en premier ?
Un gros fichier lu systématiquement annule la divulgation progressive. Un script renvoie quelques lignes au lieu de 12 000 tokens ; à défaut, le corps doit conditionner la lecture. Le mettre dans le corps le charge à chaque activation ; augmenter la limite aggrave le coût.
Exercice. Ajoutez au résultat de SkillAgent.ask le nombre de tokens apportés par chaque niveau (catalogue, corps, ressources), en vous servant des réponses d'outils. Comparez-le aux tokens cumulés : quelle part vient de la répétition de l'historique ?
Voir la solution
Dans SkillLoader.dispatch, mesurez la taille de ce qui est renvoyé (ou appelez count_tokens en mode diagnostic) et cumulez par outil : activate_skill pour le niveau 2, read_skill_file et run_skill_script pour le niveau 3. Sur la note de frais, l'apport direct des niveaux 2 et 3 est d'environ 1 000 tokens (corps 543, gabarit 115, politique 244, sortie du script), contre 14 333 tokens cumulés : la majorité vient du renvoi de l'historique et du catalogue à chacun des cinq appels. Leçon : réduire le nombre de tours (un script qui renvoie directement le rapport) rapporte plus que réduire la taille du corps.
- Je sais expliquer les trois niveaux et ce qui déclenche chacun
- Je sais contraindre les noms de skills par une énumération
- Je sais servir les ressources par liste blanche
- Je sais mesurer les tokens cumulés d'une requête et pas seulement le prompt système