Vai al contenuto

Guide

Tutorial: un classificatore o un output strutturato

Valuta una funzione Python che etichetta domande di assistenza, leggi perché il rilascio è bloccato, correggi gli errori e confronta la correzione con l'originale, tutto sulla tua macchina, senza account, senza rete e senza modelli.

Che cosa costruirai

Un bot di assistenza che restituisce un oggetto JSON con un answer e una label (refund, account o other). Lo terrai a tre requisiti: l'etichetta è corretta abbastanza spesso, l'output ha sempre la forma giusta, e nessuna risposta lascia trapelare qualcosa che somigli a un numero di previdenza sociale statunitense. Due di questi sono controlli di formato che non richiedono una risposta di riferimento; uno misura il successo del compito rispetto a un'etichetta di riferimento. La differenza conta, e questa pagina li tiene separati.

I termini usati qui sotto (caso, esecuzione, metrica, intervallo, regola, gate) sono definiti in Concetti.

Prerequisiti

  • Python 3.11 o successivo.
  • Oloproof, installato in un ambiente virtuale:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • Il progetto di esempio e la modifica candidata, che sono inclusi nel pacchetto. Copia entrambi in nuove directory e lavora nella prima; ogni file è anche elencato qui sotto, quindi puoi digitarli tu:
oloproof init --example support_bot support-classifier
oloproof init --example classification support-change
cd support-classifier

In questa pagina non vengono usati chiavi API, account di provider né accesso alla rete.

I file

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

Esegui ogni comando dalla directory support-classifier/. Oloproof tiene lì il proprio store, in .oloproof/; elimina quella directory per ricominciare da zero.

L'applicazione e il suo adattatore

La tua applicazione viene raggiunta tramite un adattatore. Per un'applicazione Python l'adattatore è la funzione stessa: Oloproof la importa, la chiama una volta per caso con l'input del caso, e registra il dizionario che restituisce come output di quel caso.

# 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"}

Per valutare il tuo classificatore, lascia il suo codice dov'è e scrivi una funzione sottile come questa che lo chiama e restituisce un dizionario. La funzione può essere async. Oloproof la chiama; non ospita, non isola e non reimposta la tua applicazione, quindi qualsiasi stato che la tua applicazione mantiene tra una chiamata e l'altra spetta a te gestirlo.

oloproof.yaml nomina quella funzione e i valutatori:

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

Gli output vengono messi in cache in base al sorgente della funzione, alla version dichiarata e a config. Se la funzione legge altri file (un prompt, una tabella di regole), elencali sotto system.code_paths, così modificarli esegue di nuovo il sistema.

Il dataset

Un caso per riga. input è esattamente ciò che la tua funzione riceve come case; expected è il riferimento con cui il valutatore exact_match confronta:

{"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 funzione restituisce, per ogni caso, un oggetto come {"answer": "Use password reset, ...", "label": "account"}.

Scegliere i valutatori

CriterioValutatoreRichiede expectedChe cosa misura
exact_labelexact_match su labelsìsuccesso del compito: l'etichetta è quella giusta
format_validjson_schema sull'intero outputnoformato: l'oggetto ha esattamente i due campi stringa
pii_freeregex su answer, pass_if: no_matchnouna proprietà di sicurezza del testo

Un controllo di formato fa passare una risposta sbagliata ma ben formata, quindi non può mai sostituire il successo del compito. Un controllo del compito richiede un riferimento per ogni caso; dove un caso non ne ha, exact_match non può valutarlo. I valutatori deterministici non richiedono validazione rispetto a persone: eseguirne uno due volte dà lo stesso verdetto.

La policy

release.yaml è ciò rispetto a cui l'esecuzione viene decisa:

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 dice che l'etichetta deve essere corretta almeno il 70% delle volte, e passa solo quando l'intero intervallo al 95% è pari o superiore a 0.70. Le due regole observed_count non ammettono alcun fallimento sui casi che hai eseguito; descrivono questi casi, non ogni domanda che gli utenti porranno.

Eseguilo

oloproof run

Output reale, ridotto:

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

Come leggerlo:

  • 13 etichette su 18 sono corrette, il 72,2%. È sopra 0.70, ma l'intervallo scende fino al 46,5%: 18 casi non possono mostrare che il tasso vero sia almeno 0.70. Quindi la regola è INSUFFICIENT_EVIDENCE, né PASS né FAIL.
  • Ogni output ha la forma giusta e nessuno contiene un numero simile a un SSN, quindi entrambe le regole di formato passano.
  • block_on elenca INSUFFICIENT_EVIDENCE, quindi il gate blocca e il comando esce con 3. L'uscita 0 significherebbe che non c'è nulla su cui la policy blocca; Gating in CI elenca tutti i codici.

Eseguilo di nuovo e la riga della cache riporta execution 18 hit/0 miss: non è cambiato nulla, quindi la funzione non viene chiamata.

Esamina i fallimenti

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

RUN_ID è l'id sulla prima riga dell'output dell'esecuzione.

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

Lo schema è evidente una volta letti gli input: "money back", "reverse the payment", "sign in" e "two-factor" non sono nelle liste di parole chiave, e other_04 dice "I don't want a refund", che la parola "refund" intercetta comunque. Nota che refund_04 supera entrambi i controlli di formato pur essendo sbagliato: è il divario tra controllare il formato e misurare il successo.

Qui hanno senso due azioni successive. Correggere gli errori (sotto), oppure aggiungere casi: con più casi alla stessa accuratezza l'intervallo si restringe, e oloproof plan RUN_ID --run stima quanti ne servono.

Fai una modifica reale

Copia ../support-change/app.py sopra app.py. Aggiunge le formulazioni mancate:

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):
        ...

e imposta version: keywords-v2 sotto system in oloproof.yaml, così l'esecuzione viene registrata come la nuova versione. Poi:

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 su 18 sono corretti e il limite inferiore dell'intervallo, 72,7%, supera 0.70, quindi la regola passa e il comando esce con 0. other_04 fallisce ancora: la correzione non ha toccato la negazione.

Confronta il candidato con la baseline

La regola di esecuzione chiede se il candidato raggiunge la tua soglia. Un confronto chiede in che cosa differisce dalla baseline, caso per caso. Copia ../support-change/compare.yaml nel progetto; contiene una regola di confronto:

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)

Il candidato ha corretto quattro casi e non ne ha rotto nessuno, un guadagno stimato di 22 punti. Ma sono cambiati solo quattro casi, e 18 casi appaiati lasciano un intervallo da 12,9 punti peggio a 57 punti meglio, che attraversa il margine di 10 punti. Il confronto non può ancora escludere che il candidato sia peggiore di più di quanto accetti, quindi è INSUFFICIENT_EVIDENCE ed esce con 3. La riga sotto è la stima di dimensionamento. Senza --policy, compare stampa le differenze, dice che il release.yaml del progetto non dichiara alcuna regola di confronto, ed esce con 0 perché non è stato deciso nulla.

Confrontare un candidato con una baseline spiega il margine e gli altri tipi di regola.

Risoluzione dei problemi

SintomoCausa e rimedio
ModuleNotFoundError per appEsegui dalla directory che contiene app.py, o dai a callable un percorso di modulo importabile da lì.
Una regola nomina una metrica che nessun valutatore produceLa metric della regola deve essere uguale al criterion di un valutatore; l'errore elenca le metriche esistenti.
Hai modificato il classificatore e l'esecuzione ha riutilizzato ogni outputLa cache segue il sorgente del callable; un file di supporto che legge deve essere elencato sotto system.code_paths.
exact_label riporta casi come mancantiQuelle esecuzioni hanno sollevato un'eccezione o sono andate in timeout; oloproof inspect RUN_ID --failures mostra ogni errore.
L'esecuzione esce con 3 con una stima altaDecide l'intervallo, non la stima. Aggiungi casi o accetta una soglia più bassa, decisa prima dell'esecuzione.

Limitazioni

  • L'SDK e lo YAML riportano tassi di successo per valutatore. Non c'è una matrice di confusione né precisione e richiamo per classe per un classificatore come questo; un blocco predictive: li fornisce per un modello con punteggio (Modelli predittivi).
  • Le regole observed_count descrivono i casi che hai eseguito; non affermano nulla sugli input non visti.
  • Un confronto su 18 casi risolve solo differenze grandi. Cinquanta o più casi reali sono una soglia più utile.
  • oloproof.yaml non può nominare un @evaluator personalizzato; per quello serve l'SDK (L'SDK).