Aller au contenu principal

Notateurs personnalisés

Mis à jour le 28 juillet 2026

Au-delà des assertions simples

Parfois, les assertions intégrées ne suffisent pas. equals, contains et regex répondent à une question binaire posée sur du texte brut, et beaucoup de sorties réelles échappent à ce cadre. Vous avez besoin d’une logique de notation sur mesure : vérifier la structure d’un JSON, calculer un score de similarité, ou appliquer des règles métier complexes. Une réponse qui doit contenir un identifiant client cohérent avec la date de commande mentionnée juste à côté ne se valide par aucune expression régulière — il faut lire les deux champs et les comparer entre eux.

Le principe des notateurs personnalisés est simple : Promptfoo vous passe la sortie du modèle dans une variable output, vous écrivez le code que vous voulez, et vous retournez le verdict. Tout ce que votre langage sait faire devient disponible dans votre évaluation.

Notateurs JavaScript

tests:
  - vars:
      question: "Quelles sont les capitales des pays nordiques ?"
    assert:
      - type: javascript
        value: |
          // Verifier que la reponse mentionne au moins 3 capitales
          const capitales = ['Stockholm', 'Oslo', 'Helsinki', 'Copenhague', 'Reykjavik'];
          const mentionnees = capitales.filter(c => output.includes(c));
          return mentionnees.length >= 3;

Ce notateur illustre un besoin que les assertions standard traitent mal : la réussite partielle acceptable. La question est ouverte, la formulation de la réponse est libre, et exiger les cinq capitales serait aussi arbitraire que n’en exiger qu’une. En comptant les occurrences et en fixant un seuil, vous exprimez une exigence qui correspond réellement à ce que vous attendez du produit. Le même schéma se transpose partout : une réponse de support qui doit couvrir au moins trois des cinq points d’une procédure se vérifie exactement de cette façon.

Notateurs Python

tests:
  - vars:
      input: "Génère un JSON avec nom, age et ville"
    assert:
      - type: python
        value: |
          import json
          try:
              data = json.loads(output)
              has_keys = all(k in data for k in ['nom', 'age', 'ville'])
              age_valid = isinstance(data.get('age'), int) and 0 < data['age'] < 150
              return has_keys and age_valid
          except:
              return False

Ce notateur vérifie deux choses de nature différente, et la distinction vaut d’être vue. has_keys contrôle la forme : les trois clés demandées sont présentes. age_valid contrôle le sens : l’âge est bien un entier, et un entier plausible. Un modèle peut parfaitement produire un JSON impeccable avec un âge rendu sous forme de chaîne "32" ou une valeur de 250 ; le premier test passerait, le second non. C’est cette validation sémantique, impossible à écrire en regex, qui justifie le passage au code.

Le try/except mérite la même attention que dans la leçon sur les évaluations par code : une sortie mal formée doit faire échouer le cas, pas interrompre la campagne. Une exception non rattrapée interrompt la boucle en cours de route, et vous perdez les résultats de tous les cas qui restaient à traiter.

Notateurs externes (fichiers)

Pour des logiques complexes, déplacez le code dans un fichier :

tests:
  - vars:
      document: "..."
    assert:
      - type: python
        value: file://graders/document_grader.py

Dès qu’un notateur dépasse une dizaine de lignes, le laisser dans le YAML devient pénible : pas de coloration syntaxique utilisable, pas d’autocomplétion, et surtout aucune possibilité de le tester séparément. Externalisé dans un fichier, il redevient du code ordinaire que vous versionnez, que vous relisez en revue de code et que vous réutilisez sur plusieurs cas de test. C’est aussi la seule manière raisonnable de partager un notateur entre deux configurations d’évaluation.

Scores continus

Au lieu d’un simple vrai/faux, vous pouvez retourner un score :

tests:
  - vars:
      question: "..."
    assert:
      - type: javascript
        value: |
          // Score de 0 a 1 base sur le nombre de criteres remplis
          let score = 0;
          if (output.includes('introduction')) score += 0.25;
          if (output.includes('conclusion')) score += 0.25;
          if (output.length > 200) score += 0.25;
          if (output.length < 1000) score += 0.25;
          return { pass: score >= 0.75, score: score };

L’objet retourné porte deux informations : pass décide si le cas compte comme réussi, score conserve le détail. Cette nuance change la manière dont vous lirez vos itérations. Avec une notation binaire, un prompt qui remplissait deux critères sur quatre et qui en remplit maintenant trois affiche toujours un échec, et vous croyez avoir régressé alors que vous avez progressé. Le score continu rend ce progrès visible et vous évite d’abandonner une piste qui fonctionnait.

Ce que les notateurs sur mesure rendent possible

Quatre familles de besoins justifient à elles seules de sortir des assertions intégrées. La validation de structures de données complexes vient en premier : un JSON imbriqué dont il faut vérifier qu’une liste d’articles contient bien un prix et une quantité pour chaque entrée, ou un XML dont un attribut doit exister. Viennent ensuite les contraintes métier, c’est-à-dire les formats spécifiques et les règles de conformité propres à votre secteur — un numéro de dossier construit selon votre convention interne, une mention légale obligatoire dans un courrier client.

Le calcul de métriques de similarité sémantique forme la troisième famille : quand deux formulations très différentes sont également correctes, comparer des chaînes ne mène nulle part et il faut mesurer une proximité de sens. La quatrième, souvent oubliée, est la cohérence entre plusieurs champs de sortie. Un modèle qui remplit correctement chaque champ pris isolément peut produire un ensemble absurde — une date de fin antérieure à la date de début, un total qui ne correspond pas à la somme des lignes. Seul un notateur qui lit les champs ensemble détecte ce genre d’erreur.

Entretenir ses notateurs

Un notateur est du code, avec les mêmes défauts que le reste de votre code. Gardez-le simple et lisible : une fonction de vingt lignes qui enchaîne cinq conditions imbriquées finira par contenir un bug que personne ne verra, et un notateur faux est pire qu’une absence d’évaluation, puisqu’il vous donne confiance à tort. Documentez vos critères, ne serait-ce qu’en commentaire, pour que la personne qui lira le score dans six mois sache ce qu’il mesure.

Testez surtout les notateurs eux-mêmes, exactement comme vous testeriez n’importe quelle autre fonction : fabriquez deux ou trois sorties fictives dont le verdict ne fait aucun doute, passez-les au notateur et vérifiez qu’il tranche comme vous. C’est l’étape que tout le monde saute, et elle explique la plupart des campagnes d’évaluation trompeuses. Enfin, préférez un score continu à un verdict binaire chaque fois que la qualité de la réponse se décline en degrés, pour les raisons vues plus haut.

Points clés à retenir

  • Les notateurs personnalisés reçoivent la sortie dans output et retournent le verdict que vous décidez.
  • Ils permettent la réussite partielle, la validation sémantique et les règles métier hors de portée d’une regex.
  • Externalisez tout notateur dépassant quelques lignes avec file:// : il devient testable et réutilisable.
  • Un retour { pass, score } rend visibles les progrès intermédiaires qu’une note binaire écrase.
  • Protégez le parsing par try/except et testez chaque notateur comme une fonction ordinaire, sur des sorties fabriquées.