Mesurer si le modèle choisit le bon skill
Lancer l'eval de routage, lire son tableau, et s'en servir pour décider si un changement du catalogue ou du prompt a aidé.
mm affiche un skill dans le catalogue et espère que le modèle ira le chercher. Qu'il le fasse ou non est une propriété du modèle, pas du code : aucun test ordinaire ne peut y répondre. Celui-ci interroge un modèle vivant, vingt fois.
Le lancer
MM_EVAL=1 AGENT_CONFIG=tests/agent.dmr-qwen.yaml go test ./internal/eval -v -timeout 25m
Les deux variables sont nécessaires en pratique : sans MM_EVAL le test se saute, et sans AGENT_CONFIG il évalue les défauts intégrés plutôt que la configuration qui vous intéresse. go test ./... le saute, donc rien dans la CI ne dépend d'un serveur démarré.
Comptez quelques minutes : un tour de modèle par cas, et les petits modèles ne sont pas rapides. Les lignes s'affichent au fur et à mesure.
Lire le tableau
[ 6/20] ✓ hit want=what-time-is-it got=what-time-is-it 1.204s "quelle heure est-il ?" ← French
[14/20] ≠ wrong want=greetings got=vulcan-salute 0.981s "Salue Bob de ma part" ← two skills claim salut/saluer
[16/20] · miss want=bg-jobs got=(none) 2.017s "lance le serveur en tâche de fond"
[17/20] ! extra want=(none) got=greetings 0.774s "write a haiku about pointers"
| Marque | Sens | Ce que ça désigne |
|---|---|---|
✓ hit |
le skill attendu, ou correctement aucun | — |
≠ wrong |
un skill a été chargé, mais pas celui-là | le catalogue : deux entrées que le modèle ne distingue pas |
· miss |
un skill s'appliquait et aucun n'a été chargé | le prompt : le modèle n'a pas pensé à chercher |
! extra |
aucun skill ne s'appliquait et un a été chargé | instruction trop insistante, ou entrée qui promet trop |
✗ error |
le tour a échoué | jamais un score de routage. Soit l'instrument (un stub qui a affamé le modèle, caseTimeout trop court, un serveur parti), soit le modèle incapable de composer un appel d'outil valide (unexpected end of JSON input, ou le garde-fou anti-emballement qui coupe des arguments répétés). Le message dit lequel. Fait échouer le test, car les chiffres sont alors incomplets. |
wrong et miss sont séparés à dessein. Les confondre masque lequel des deux problèmes vous avez, et ils n'ont pas le même remède.
S'en servir
Le chiffre seul ne veut rien dire. Il n'y a pas de seuil et le test n'échoue pas sur un mauvais score — un chiffre qui casse le build devient une cible, et la première chose qu'on fait devant un build rouge, c'est modifier les cas.
L'eval est un avant/après :
- Le lancer, garder la sortie.
- Changer une chose — les
keywordsd'un skill, le blocSKILLSdu prompt système, unedescription. - Le relancer sur le même modèle, avec la même configuration.
- Comparer.
Un changement à la fois. Deux d'un coup et vous avez mesuré leur somme.
Les petits modèles à temperature: 0.0 sont assez reproductibles pour que ça ait un sens ; relancez deux fois avant de croire à un écart d'un seul cas.
Ce qu'il mesure, et ce qu'il ne mesure pas
Il mesure le catalogue — la description d'outil que le modèle lit — sous un prompt système donné. Un tour de modèle par cas, avec des outils qui enregistrent ce qui a été demandé et n'exécutent rien : un skill qui dit « lance cet echo » ne doit pas echo vingt fois, et laisser tourner la boucle de l'agent mesurerait la récupération autant que le routage.
Il ne mesure pas si le skill, une fois chargé, a été correctement suivi. C'est une autre question, et il faut un autre instrument.
Il ne mesure pas non plus si le modèle sait émettre un appel d'outil valide. Quand il ne sait pas — et les petits modèles souvent ne savent pas — vous obtenez ✗ error, pas un mauvais score de routage. Ces deux échecs n'ont rien à voir, et aucun travail sur le catalogue ne corrige le second. Mesuré sur qwen3.5-4b : trois cas sur vingt échouent ainsi, de façon reproductible, sur les demandes qui appellent un appel plus structuré.
Ajouter un cas
internal/eval/routing_test.go, la table cases :
{"lance le serveur en tâche de fond", "bg-jobs", "French"},
{"what is 17 times 23?", none, ""},
Deux règles qui gardent les chiffres honnêtes :
- Ne formulez pas la demande avec les mots du catalogue. Un cas qui dit « utilise le skill greetings » ne mesure rien.
- Gardez les cas
none. Un catalogue qui se déclenche sur tout est aussi cassé qu'un catalogue qui ne se déclenche jamais, et s'abstenir à raison est un succès.
Quand vous trouvez un vrai échec de routage, ajoutez-le comme cas avant de le corriger. C'est ainsi que l'eval empêche le même échec de revenir.
Écrivez des cas capables de séparer les explications
Un cas isolé dit que quelque chose a échoué. Un ensemble de cas dit pourquoi, et c'est ce qui rend l'eval utile.
Exemple vécu. bg-jobs échouait sur les demandes anglaises et réussissait sur les françaises, et la lecture évidente était « la couverture française du catalogue est faible » — sauf que bg-jobs n'a aucun mot-clé français. Trois cas ont tranché :
| Cas | Résultat |
|---|---|
start the dev server in the background — anglais, nomme le concept |
✓ hit |
démarre le serveur de dev et vérifie qu'il répond — français, juste une situation |
· miss |
lance le serveur en tâche de fond — français, nomme le concept |
✓ hit |
start the dev server and check it answers — anglais, juste une situation |
· miss |
Un 2×2 net : la langue n'a rien à voir. Ce qui décide, c'est que la demande nomme le concept dont parle le skill. Ça a déplacé le problème du catalogue vers le prompt — le modèle n'a aucune raison de savoir qu'un serveur bloque le tour — et aucune retouche de mots-clés ne l'aurait trouvé.
Construisez les cas par paires qui ne diffèrent que d'une chose. Un cas seul est une anecdote.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
|