Aller au contenu

Guides

Tutoriel : un classifieur ou une sortie structurée

Évaluez une fonction Python qui étiquette des questions d’assistance, lisez pourquoi la publication est bloquée, corrigez les erreurs et comparez la correction à l’original, le tout sur votre propre machine, sans compte, sans réseau et sans modèle.

Ce que vous allez construire

Un bot d’assistance qui renvoie un objet JSON avec une answer et un label (refund, account ou other). Vous lui imposerez trois exigences : l’étiquette est juste assez souvent, la sortie a toujours la bonne forme, et aucune réponse ne divulgue quoi que ce soit qui ressemble à un numéro de sécurité sociale américain. Deux d’entre elles sont des vérifications de format qui n’ont besoin d’aucune réponse de référence ; l’autre mesure la réussite de la tâche par rapport à une étiquette de référence. La différence compte, et cette page les garde séparées.

Les termes utilisés ci-dessous (cas, exécution, métrique, intervalle, règle, porte) sont définis dans Concepts.

Prérequis

  • Python 3.11 ou ultérieur.
  • Oloproof, installé dans un environnement virtuel :
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • Le projet d’exemple et la modification candidate, livrés avec le paquet. Copiez-les tous deux dans de nouveaux répertoires et travaillez dans le premier ; chaque fichier est aussi listé ci-dessous, vous pouvez donc les saisir vous-même :
oloproof init --example support_bot support-classifier
oloproof init --example classification support-change
cd support-classifier

Aucune clé d’API, aucun compte de fournisseur ni aucun accès réseau n’est utilisé sur cette page.

Les fichiers

support-classifier/
  app.py              the application under test (a Python callable)
  oloproof.yaml       the suite: dataset, system, evaluators
  release.yaml        the release policy: rules the run is decided against
  data/support.jsonl  18 cases, one JSON object per line
  rubrics/helpful.md  a judge rubric, unused here

Lancez chaque commande depuis le répertoire support-classifier/. Oloproof y garde son stockage dans .oloproof/ ; supprimez ce répertoire pour repartir de zéro.

L’application et son adaptateur

Votre application est atteinte par un adaptateur. Pour une application Python, l’adaptateur est la fonction elle-même : Oloproof l’importe, l’appelle une fois par cas avec l’input du cas, et enregistre le dictionnaire qu’elle renvoie comme sortie de ce cas.

# app.py
from typing import Any

from oloproof import system


@system(name="support-bot", version="slice-a-example")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if "refund" in question:
        return {"answer": "Refunds are available within 30 days when the order is eligible.",
                "label": "refund"}
    if "password" in question or "login" in question:
        return {"answer": "Use password reset, then contact support if the login still fails.",
                "label": "account"}
    return {"answer": "A support specialist will follow up with the next step.", "label": "other"}

Pour évaluer votre propre classifieur, laissez son code où il est et écrivez une fine fonction comme celle-ci qui l’appelle et renvoie un dictionnaire. La fonction peut être async. Oloproof l’appelle ; il n’héberge, n’isole ni ne réinitialise votre application, donc tout état qu’elle garde entre deux appels est à votre charge.

oloproof.yaml nomme cette fonction et les évaluateurs :

version: 1
project: support-bot-example
dataset: data/support.jsonl
system:
  name: support-bot
  version: slice-a-example
  callable: app:answer
  timeout_s: 30
evaluators:
  - type: exact_match
    criterion: exact_label
    field: label
  - type: json_schema
    criterion: format_valid
    field: null
    schema:
      type: object
      required: [answer, label]
      properties:
        answer: {type: string}
        label: {type: string}
      additionalProperties: false
  - type: regex
    criterion: pii_free
    field: answer
    pattern: '\b\d{3}-\d{2}-\d{4}\b'
    pass_if: no_match

Les sorties sont mises en cache selon la source de la fonction, la version déclarée et la config. Si la fonction lit d’autres fichiers (un prompt, une table de règles), listez-les sous system.code_paths, pour que les modifier relance le système.

Le jeu de données

Un cas par ligne. input est exactement ce que votre fonction reçoit comme case ; expected est la référence avec laquelle l’évaluateur exact_match compare :

{"id":"refund_00","input":{"question":"Can I get a refund for yesterday's order?"},"expected":{"label":"refund"}}
{"id":"account_04","input":{"question":"I can't sign in on my new phone."},"expected":{"label":"account"}}
{"id":"other_04","input":{"question":"I don't want a refund, I just need a copy of my receipt."},"expected":{"label":"other"}}

La fonction renvoie, pour chaque cas, un objet tel que {"answer": "Use password reset, ...", "label": "account"}.

Choisir les évaluateurs

CritèreÉvaluateurBesoin de expectedCe qu’il mesure
exact_labelexact_match sur labelouila réussite de la tâche : l’étiquette est la bonne
format_validjson_schema sur toute la sortienonle format : l’objet a exactement les deux champs texte
pii_freeregex sur answer, pass_if: no_matchnonune propriété de sûreté du texte

Une vérification de format laisse passer une réponse fausse mais bien formée ; elle ne peut donc jamais remplacer la réussite de la tâche. Une vérification de tâche a besoin d’une référence pour chaque cas ; là où un cas n’en a pas, exact_match ne peut pas le noter. Les évaluateurs déterministes n’ont besoin d’aucune validation face à des personnes : en lancer un deux fois donne le même verdict.

La politique

release.yaml est ce par rapport à quoi l’exécution est décidée :

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
warn_on: []
rules:
  - id: exact-label-floor
    metric: exact_label
    min: 0.70
  - id: valid-format
    metric: format_valid
    kind: observed_count
    max_failures: 0
  - id: pii-free
    metric: pii_free
    kind: observed_count
    max_failures: 0

exact-label-floor dit que l’étiquette doit être juste au moins 70% du temps, et ne passe que lorsque l’intervalle à 95% entier est au niveau de 0.70 ou au-dessus. Les deux règles observed_count n’autorisent aucun échec sur les cas que vous avez exécutés ; elles décrivent ces cas, pas toutes les questions que poseront les utilisateurs.

L’exécuter

oloproof run

Sortie réelle, abrégée :

Run run_01M4... [DECIDED/COMPLETE]
Gate: BLOCK (exit 3)
│ exact-label-floor │ exact_label  │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ valid-format      │ format_valid │ PASS                  │ observed_failures_within_limit │
│ pii-free          │ pii_free     │ PASS                  │ observed_failures_within_limit │
│ exact_label  │ 72.2%    │ [46.5%, 90.4%]  │ 13 / 18 observed · 0 missing · 0 excluded │
│ format_valid │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
│ pii_free     │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/18 miss; judgment 0 hit/54 miss

Comment la lire :

  • 13 étiquettes sur 18 sont justes, 72.2%. C’est au-dessus de 0.70, mais l’intervalle descend jusqu’à 46.5% : 18 cas ne peuvent pas montrer que le vrai taux est d’au moins 0.70. La règle est donc INSUFFICIENT_EVIDENCE, ni PASS ni FAIL.
  • Chaque sortie a la bonne forme et aucune ne contient de numéro ressemblant à un SSN, donc les deux règles de format passent.
  • block_on liste INSUFFICIENT_EVIDENCE, donc la porte bloque et la commande se termine avec 3. Le code 0 signifierait que rien de ce sur quoi la politique bloque n’est survenu ; Portes en CI liste tous les codes.

Relancez-la et la ligne du cache indique execution 18 hit/0 miss : rien n’a changé, donc la fonction n’est pas appelée.

Inspecter les échecs

oloproof inspect RUN_ID --failures
oloproof inspect RUN_ID --case refund_04

RUN_ID est l’identifiant sur la première ligne de la sortie de l’exécution.

5 of 18 cases failed, errored or did not finish

refund_04
  output: {"answer": "A support specialist will follow up with the next step.", "label": "other"}
  exact_label: failed
...
other_04
  output: {"answer": "Refunds are available within 30 days when the order is eligible.", "label": "refund"}
  exact_label: failed
case refund_04
input: {
  "question": "I was charged twice this month and want my money back."
}
expected: {
  "label": "refund"
}
execution: OK, 1 ms
output: {
  "answer": "A support specialist will follow up with the next step.",
  "label": "other"
}
judgments:
  exact_label: failed
  format_valid: passed
  pii_free: passed

Le motif est évident dès qu’on lit les entrées : « money back », « reverse the payment », « sign in » et « two-factor » ne sont pas dans les listes de mots-clés, et other_04 dit « I don’t want a refund », que le mot « refund » attrape quand même. Notez que refund_04 passe les deux vérifications de format tout en étant faux : c’est l’écart entre vérifier le format et mesurer la réussite.

Deux actions suivantes ont un sens ici. Corriger les erreurs (ci-dessous), ou ajouter des cas : avec plus de cas à la même exactitude, l’intervalle se resserre, et oloproof plan RUN_ID --run estime combien.

Faire une vraie modification

Copiez ../support-change/app.py par-dessus app.py. Il ajoute les formulations manquées :

REFUND_WORDS = ("refund", "money back", "reverse the payment")
ACCOUNT_WORDS = ("password", "login", "sign in", "two-factor")


@system(name="support-bot", version="keywords-v2")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if any(word in question for word in REFUND_WORDS):
        ...

et réglez version: keywords-v2 sous system dans oloproof.yaml, pour que l’exécution soit enregistrée comme la nouvelle version. Puis :

oloproof run
Gate: ALLOW (exit 0)
│ exact-label-floor │ exact_label  │ PASS  │ lower_bound_meets_minimum      │
│ exact_label  │ 94.4%    │ [72.7%, 99.9%]  │ 17 / 18 observed · 0 missing · 0 excluded │

17 sur 18 sont justes et la borne inférieure de l’intervalle, 72.7%, dépasse 0.70, donc la règle passe et la commande se termine avec 0. other_04 échoue toujours : la correction n’a pas touché à la négation.

Comparer le candidat à la référence

La règle d’exécution demande si le candidat atteint votre plancher. Une comparaison demande en quoi il diffère de la référence, cas par cas. Copiez ../support-change/compare.yaml dans le projet ; il contient une règle de comparaison :

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - id: label-no-regression
    kind: non_inferiority
    metric: exact_label
    margin: 0.10
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
Comparison sha256:de76... of run_01M4...TJAD against run_01M4...ECVEF · 18 paired cases
exact_label: +22.2 points [-12.9, +57.0] · 18 paired · 0 missing · 0 excluded
format_valid: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
pii_free: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
Decisions
  label-no-regression  exact_label  non-inferiority, margin 10.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
    about 3 more paired cases would decide it, if the difference holds (21 in total at 22% discordance)
Gate: BLOCK (exit 3)

Le candidat a corrigé quatre cas et n’en a cassé aucun, un gain estimé de 22 points. Mais seuls quatre cas ont changé, et 18 cas appariés laissent un intervalle allant de 12.9 points de moins à 57 points de plus, qui traverse la marge de 10 points. La comparaison ne peut pas encore exclure que le candidat soit pire de plus que ce que vous acceptez ; elle est donc INSUFFICIENT_EVIDENCE et se termine avec 3. La ligne en dessous est l’estimation de taille. Sans --policy, compare affiche les différences, indique que le release.yaml du projet ne déclare aucune règle de comparaison, et se termine avec 0 parce que rien n’a été décidé.

Comparer un candidat à une référence explique la marge et les autres types de règles.

Dépannage

SymptômeCause et correction
ModuleNotFoundError pour appLancez depuis le répertoire contenant app.py, ou donnez à callable un chemin de module importable depuis là.
Une règle nomme une métrique qu’aucun évaluateur ne produitLa metric de la règle doit être égale au criterion d’un évaluateur ; l’erreur liste les métriques existantes.
Vous avez modifié le classifieur et l’exécution a réutilisé chaque sortieLe cache suit la source de l’appelable ; un fichier auxiliaire qu’il lit doit être listé sous system.code_paths.
exact_label signale des cas manquantsCes exécutions ont levé une exception ou expiré ; oloproof inspect RUN_ID --failures montre chaque erreur.
L’exécution se termine avec 3 malgré une estimation élevéeC’est l’intervalle, pas l’estimation, qui décide. Ajoutez des cas ou acceptez un plancher plus bas, décidé avant l’exécution.

Limites

  • Le SDK et le YAML rapportent des taux de réussite par évaluateur. Il n’y a ni matrice de confusion ni précision et rappel par classe pour un classifieur comme celui-ci ; un bloc predictive: les fournit pour un modèle à scores (Modèles prédictifs).
  • Les règles observed_count décrivent les cas que vous avez exécutés ; elles n’affirment rien sur des entrées non vues.
  • Une comparaison sur 18 cas ne distingue que de grandes différences. Cinquante cas réels ou plus sont un plancher plus utile.
  • oloproof.yaml ne peut pas nommer un @evaluator personnalisé ; il faut pour cela le SDK (Le SDK).