درس 3 — Choisir et exécuter

Qui choisit le skill ?

7 دقائق قراءة

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

Qui choisit le skill ?

« Pose 3 jours de congés du 4 au 6 novembre dans l'outil RH. » Aucun skill installé ne sait poser des congés ; l'assistant doit le dire. Pourtant la requête contient « congés », « jours », « RH » : un sélecteur par mots-clés ou par similarité choisit leave-balance-calculator ou hr-policy-qa, l'assistant charge 500 tokens d'instructions inutiles, puis répond à côté. Choisir un skill, c'est aussi savoir n'en choisir aucun.

Le mécanisme

Deux familles de sélection coexistent :

  • Sélection par le modèle : le catalogue (nom et description) est dans le contexte, avec un outil activate_skill. Le modèle lit la demande, la compare aux descriptions et décide d'appeler l'outil, ou non. C'est ce que font Claude, Gemini Enterprise (« utilise automatiquement un skill quand il est pertinent ») et ADK, et ce que recommande le guide client.
  • Sélection par similarité : le harness calcule un embedding de chaque description et de la requête, prend le skill le plus proche (cosinus) et l'active si le score dépasse un seuil. Aucun appel au modèle génératif : un seul appel d'embedding, beaucoup moins cher.
flowchart TB
  Q["Requête"] --> X{"Combien de skills ?"}
  X -->|"jusqu'à une centaine"| M["Catalogue complet + activate_skill : le modèle décide"]
  X -->|"des centaines"| S["Similarité : top 10 des descriptions"]
  S --> M2["Catalogue réduit + activate_skill : le modèle décide"]
  M --> R["Skill activé, ou aucun"]
  M2 --> R
  U["Commande explicite /nom-du-skill"] --> R

La troisième voie, toujours disponible, est l'activation explicite : l'utilisateur tape /expense-report-checker ou mentionne le skill avec @ (Gemini Enterprise le propose), et le harness injecte le corps sans demander l'avis du modèle.

La mise en œuvre

Pour mesurer la sélection seule, on n'exécute que le premier tour de la boucle et on lit le premier appel de fonction (selection.py) :

files/skillkit/selection.py
def pick_by_model(client, loader, query, model=MODEL):
    r = generate(client, model=model, contents=query, config=types.GenerateContentConfig(
        system_instruction=loader.system_instruction(), tools=[loader.tool()],
        automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True)))
    for fc in r.function_calls or []:
        if fc.name == "activate_skill":
            return (fc.args or {}).get("name"), r.usage_metadata.prompt_token_count
    return None, r.usage_metadata.prompt_token_count

Le sélecteur par similarité utilise gemini-embedding-2 avec les préfixes de tâche du modèle : title: … | text: … pour les descriptions, task: search result | query: … pour la requête, et une Content par texte (une liste de chaînes dans une seule Content produirait un vecteur agrégé).

files/skillkit/selection.py
self.matrix = self._embed([f"title: {s.name} | text: {s.description}" for s in loader.skills.values()])
...
def scores(self, queries):
    return self._embed([f"task: search result | query: {q}" for q in queries]) @ self.matrix.T

def pick(self, row, threshold=None):
    i = int(np.argmax(row))
    return self.names[i] if row[i] >= (self.threshold if threshold is None else threshold) else None

Résultat obtenu

python run.py eval-trigger, 30 requêtes (24 qui doivent déclencher un skill précis, 6 qui ne doivent en déclencher aucun, dont 5 quasi-homonymes), 2026-10-15 :

SélecteurExactitudePrécision des activationsFaux déclenchements (sur 6)
Modèle, gemini-3.5-flash, 2 passages (60 décisions)1,001,000
Modèle, gemini-3.5-flash-lite, 1 passage0,9670,961
Similarité, meilleur score sans seuil0,800,806
Similarité, seuil 0,600,8330,8524 (et 1 positif perdu)
scores : min_positifs 0.593 | max_negatifs 0.676
lite : attendu None -> obtenu leave-balance-calculator : "Pose 3 jours de congés du 4 au 6 novembre dans l'outil RH pour moi."
similarité : attendu None -> obtenu csv-data-profiler : 'Écris un script Python qui lit un CSV et insère chaque ligne dans Post'
similarité : attendu None -> obtenu csv-data-profiler : 'Quelle est la différence entre la médiane et la moyenne ?'

La similarité classe parfaitement les 24 requêtes positives : le skill le plus proche est toujours le bon. Elle échoue sur la décision « aucun » : la requête hors sujet la plus proche obtient 0,676, plus que la requête positive la moins proche (0,593). Aucun seuil ne sépare les deux groupes sur ce jeu. Le modèle, lui, lit la ligne « Not for » des descriptions et sait refuser. Le coût : 1 632 tokens d'entrée en moyenne pour cette décision, contre quelques dizaines de tokens d'embedding.

Ce qui casse en production

SymptômeCauseParade
Un skill se déclenche sur des requêtes voisines (« crée un modèle de note de frais »)description trop large, pas de « Not for »ajouter les exclusions avec le nom du skill voisin, retester les quasi-homonymes
Un skill ne se déclenche jamaisle modèle traite seul les demandes simples ; la description ne dit pas quand le skill apporte plusdescription impérative et insistante (« Use whenever…, even if… »)
Le seuil de similarité réglé en recette ne tient pas en productionseuil ajusté sur le jeu de test, distribution différente en réelne pas laisser la similarité décider seule ; l'utiliser pour présélectionner
Plusieurs skills activés pour une demande simpleconsigne système trop incitative« N'activez jamais un skill au cas où », et mesurer les activations multiples
Le choix change d'un jour à l'autresélection non déterministetrois passages par requête et taux de déclenchement, comme le recommande agentskills.io

La règle de décision

Laissez le modèle décider tant que le catalogue reste de l'ordre de 10 000 à 20 000 tokens, soit une centaine de skills à 100-200 tokens de description. Au-delà, présélectionnez par similarité (top 10) et laissez le modèle trancher sur ce catalogue réduit : c'est le rôle de SkillRegistry.search_skills dans ADK. N'utilisez jamais la similarité seule pour décider qu'un skill doit être activé. Gardez toujours l'activation explicite par commande pour les utilisateurs qui savent ce qu'ils veulent.

Pratique

Vous avez 400 skills. Mettre tout le catalogue dans le prompt coûte 70 000 tokens par requête. Que faites-vous ?

Après une mise à jour, gemini-3.5-flash-lite active leave-balance-calculator pour « Pose 3 jours de congés dans l'outil RH ». Que modifiez-vous d'abord ?

Exercice. Implémentez le sélecteur hybride : top 3 par similarité, puis pick_by_model sur un SkillLoader réduit à ces 3 skills. Mesurez son exactitude sur les 30 requêtes et son coût moyen en tokens.

Voir la solution
def pick_hybrid(client, loader, sel, query, k=3):
    row = sel.scores([query])[0]
    top = [sel.names[i] for i in np.argsort(row)[::-1][:k]]
    small = SkillLoader({n: loader.skills[n] for n in top})
    return pick_by_model(client, small, query)

Le catalogue passe de 6 à 3 descriptions, d'où environ 500 tokens de moins par décision. Avec 6 skills, le gain est faible ; l'intérêt apparaît à partir de quelques centaines. Vérifiez que les 6 requêtes « aucun » restent à None : le modèle garde la possibilité de refuser même quand on lui présente 3 candidats.

  • Je sais pourquoi la similarité trouve le bon skill mais pas « aucun »
  • Je sais mesurer la sélection sur le premier tour seulement
  • Je sais quand passer à une présélection par embeddings
  • Je sais corriger un faux déclenchement par la description
التالي