Guide
Tutorial: generazione di testo con un giudice a rubrica
Valuta una funzione che scrive testo libero, qui un riassuntore di ticket, con controlli di formato e un giudice a rubrica; misura quel giudice rispetto alle etichette di una persona prima che possa decidere qualcosa; poi confronta una modifica reale. Il giudice gira su questa macchina senza modello e senza rete, e un passo facoltativo lo sostituisce con un modello reale.
Che cosa costruirai
Un riassuntore che trasforma un ticket di assistenza in una o due frasi. "Buono" è un giudizio, non una corrispondenza di stringhe, quindi il successo del compito è deciso da un giudice LLM con una rubrica: il riassunto riporta i fatti di cui un operatore ha bisogno? Due valutatori deterministici controllano il formato, che non richiede un riferimento. Termini come caso, esecuzione, metrica, giudice e gate sono definiti in Concetti.
La stessa forma si adatta all'estrazione o a qualsiasi altra generazione: una funzione restituisce testo in un dizionario, il riferimento dice che cosa deve contenere una buona risposta, e una rubrica dice come decidere.
Prerequisiti
- Python 3.11 o successivo, e Oloproof in un ambiente virtuale:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof- Il progetto di esempio, incluso nel pacchetto. Copialo in una nuova directory e lavora lì:
oloproof init --example generation ticket-summaries
cd ticket-summaries- La porta 8799 libera per il giudice sostitutivo (altrimenti cambiala in entrambi i punti).
Ogni passo fino a "Facoltativo: un modello reale come giudice" è offline e deterministico: nessuna chiave API, nessun account di provider, nessun costo.
I file
ticket-summaries/
app.py the summariser under test (baseline)
app_v2.py the candidate change
judge_server.py a stand-in judge speaking the OpenAI API on 127.0.0.1
rubrics/covers_facts.md the judge's rubric
oloproof.yaml the suite
release.yaml rules for a run
compare.yaml a rule for a comparison
data/tickets.jsonl 20 cases
labels/reviewer_verdicts.csv one person's verdicts on the baseline's summaries
fill_labels.py copies those verdicts into a labelling sheetEsegui ogni comando da ticket-summaries/.
Il giudice sostitutivo, e ciò che non è
Un giudice a rubrica è un valutatore che invia un prompt (la rubrica, l'input del caso, il suo expected e l'output) a un modello e ne rilegge {"pass": true|false, "rationale": "..."}. Oloproof parla con qualsiasi server che implementi la chat API di OpenAI, e un server su localhost non richiede chiavi.
judge_server.py è un server di questo tipo, ma non è un modello. Fa passare un riassunto solo quando contiene ogni frase elencata sotto must_mention nell'expected del caso, senza badare a maiuscole e minuscole. È una regola fissa, quindi il tutorial dà gli stessi numeri su ogni macchina. Non può accorgersi di un fatto inventato, cosa che a un vero giudice basato su modello viene chiesta. Avvialo in un secondo terminale e lascialo in esecuzione:
python judge_server.py --port 8799stand-in judge on http://127.0.0.1:8799/v1L'applicazione e il suo adattatore
# app.py
@system(name="ticket-summariser", version="first-sentence")
def summarise(case: dict[str, Any]) -> dict[str, str]:
return {"summary": sentences(str(case["ticket"]))[0]}L'adattatore per un'applicazione Python è la funzione: riceve l'input del caso e restituisce un dizionario. Per il tuo generatore, chiama al suo interno il tuo modello o la tua catena e restituisci il testo sotto una chiave. Oloproof la chiama una volta per caso e mette in cache l'output in base al sorgente della funzione e alla version dichiarata; non gestisce il tuo client del modello, i prompt o lo stato. Elenca i file che la funzione legge, come un template di prompt, sotto system.code_paths.
Il dataset
{"id":"t01","input":{"ticket":"Hello. Order 1042 arrived with a cracked screen. I would like a replacement, not a refund."},"expected":{"must_mention":["1042","cracked","replacement"]}}
{"id":"t06","input":{"ticket":"Please cancel my subscription at the end of this month. I am moving abroad."},"expected":{"must_mention":["cancel","end of this month"]}}input è ciò che la funzione riceve. expected è il riferimento che il giudice legge: qui una lista di fatti che il riassunto deve riportare, non un riassunto di riferimento completo, perché molti riassunti diversi sono corretti. L'output per t01 è {"summary": "Hello."}.
Scegliere i valutatori
version: 1
project: ticket-summaries
dataset: data/tickets.jsonl
system:
name: ticket-summariser
version: first-sentence
callable: app:summarise
timeout_s: 30
evaluators:
- type: json_schema
criterion: format_valid
field: null
schema:
type: object
required: [summary]
properties:
summary: {type: string, minLength: 1}
additionalProperties: false
- type: regex
criterion: short_enough
field: summary
pattern: '^.{1,160}$'
pass_if: match
- type: rubric_judge
criterion: covers_facts
provider: openai_compatible
model: stand-in-judge
base_url: http://127.0.0.1:8799/v1
rubric_file: rubrics/covers_facts.md| Criterio | Valutatore | Richiede expected | Misura |
|---|---|---|---|
| format_valid | json_schema | no | formato: un unico campo stringa non vuoto |
| short_enough | regex | no | formato: al massimo 160 caratteri |
| covers_facts | rubric_judge | sì | successo del compito, come lo definisce la rubrica |
Hello. supera entrambi i controlli di formato. Solo il giudice dice che è un riassunto inutile. Un giudice può anche funzionare senza riferimento: una rubrica come "PASS if the summary contains no greeting" legge solo l'input e l'output, e un caso senza expected viene comunque giudicato. Ciò che allora non può fare è verificare i fatti rispetto a una risposta di cui ti fidi.
La rubrica:
PASS when the summary states every fact listed under must_mention in the expected answer, in
words a support agent would recognise, and adds nothing the ticket does not say.
FAIL when any listed fact is missing, changed or contradicted.La policy
version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
require_validated_evaluators: true
rules:
- id: valid-format
metric: format_valid
kind: observed_count
max_failures: 0
- id: short-enough
metric: short_enough
kind: observed_count
max_failures: 0
- id: covers-facts-floor
metric: covers_facts
min: 0.60require_validated_evaluators: true è l'impostazione predefinita del motore, scritta qui per esteso perché è il punto di questo tutorial: un giudice che nessuno ha confrontato con delle persone non può decidere una regola.
Eseguilo
oloproof runRun run_01M4... [DECIDED/COMPLETE]
Gate: BLOCK (exit 3)
│ valid-format │ format_valid │ PASS │ observed_failures_within_limit │
│ short-enough │ short_enough │ PASS │ observed_failures_within_limit │
│ covers-facts-floor │ covers_facts │ INSUFFICIENT_EVIDENCE │ evaluator_not_validated │
covers-facts-floor: the judge (or model or custom evaluator) behind this rule has not been measured against
people yet, so it may not decide.
Label a sample: oloproof review run_01M4... --criterion covers_facts --by YOU --sample 20
Then measure it: oloproof evaluators validate EVALUATOR_ID --by YOU (ids: oloproof evaluators list)
│ format_valid │ 100.0% │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ short_enough │ 100.0% │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ covers_facts │ 45.0% │ [23.0%, 68.5%] │ 9 / 20 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/20 miss; judgment 0 hit/60 missLe regole di formato passano. Il giudice ha fatto passare 9 riassunti su 20, ma la regola è INSUFFICIENT_EVIDENCE con il motivo evaluator_not_validated, e il gate blocca con uscita 3. La regola non ha deciso sul 45%: il tasso di errore di un giudice è ignoto finché non viene misurato, quindi un intervallo costruito sui suoi verdetti porterebbe con sé un errore non dichiarato. Il motore lo riporta come INSUFFICIENT_EVIDENCE, non come MANUAL_REVIEW o FAIL: mancano le evidenze per decidere, e l'output stampa i due comandi che le forniscono.
Esamina i fallimenti
oloproof inspect RUN_ID --failures11 of 20 cases failed, errored or did not finish
t01
output: {"summary": "Hello."}
covers_facts: failed
judge text, not verified: missing: 1042, cracked, replacement
t02
output: {"summary": "I was charged twice for order 2210."}
covers_facts: failed
judge text, not verified: missing: 49
...La motivazione del giudice viene mostrata come "judge text, not verified": è la spiegazione del modello, non un'evidenza. Lo schema è comunque chiaro: la prima frase è spesso un saluto.
Misura il giudice rispetto a una persona
La validazione confronta i verdetti del giudice con quelli di una persona sulle stesse risposte. Estrai un campione casuale dei casi dell'esecuzione in un foglio. I verdetti del giudice ne sono esclusi, così chi etichetta non ne viene condizionato:
oloproof labels export RUN_ID --criterion covers_facts --sample 20 --local --out sample.csvWrote 20 cases to sample.csv, drawn at random with seed 2701013296, without the judge's verdict.
This is a local sample, good-faith only, because it was drawn on this machine.
Fill in `passed` (pass or fail) and `labelled_by` on each row you judge, then run `oloproof labels import sample.csv`.--local estrae su questa macchina senza chiedere a uno spazio di lavoro ospitato; il seed lo sceglie il motore. Con 20 casi, un campione di 20 li comprende tutti. In pratica, una persona legge il ticket e il riassunto di ogni riga e compila passed. Per questo tutorial, labels/reviewer_verdicts.csv contiene i verdetti che un revisore ha dato sui riassunti della baseline, e fill_labels.py li copia nel foglio:
python fill_labels.py sample.csv
oloproof labels import sample.csvfilled 20 rows of sample.csv
Recorded 20 labels from sample.csv (20 measurement).Il revisore è stato in disaccordo con il giudice una volta: su t02 ("I was charged twice for order 2210.") ha ritenuto irrilevante l'importo mancante e lo ha fatto passare. Le etichette nominano la risposta esatta che hanno giudicato, quindi questi verdetti valgono solo per l'esecuzione della baseline.
Trova l'id della versione del giudice e validalo:
oloproof evaluators list
oloproof evaluators validate EVALUATOR_ID --by alicecovers_facts LLM_JUDGE UNVALIDATED (declared) sha256:a662...
covers_facts: sha256:a662... is now VALIDATED
agreement 95.0% [75.1%, 99.9%] · 19 of 20 labelled cases agreed · 0 labelled but not judged · kappa 0.900
bias -5.0 points [-32.4, +20.7] · the judge's pass rate minus the people's · 20 cases · 0 labelled but not judged
passes what people pass 90.0% [55.4%, 99.8%] · the judge passed 9 of 10 cases people passed · 0 labelled but not judged
fails what people fail 100.0% [69.1%, 100.0%] · the judge failed 10 of 10 cases people failed · 0 labelled but not judgedLeggi gli intervalli, non il 95%: 20 etichette mostrano un accordo di almeno il 75,1%. Una policy può chiedere di più con minimum_evaluator_agreement, che confronta quel limite inferiore, e validate rifiuta un giudice al di sotto. La guida Giudici tratta la soglia, la distorsione, le sonde e oloproof review per etichettare nel terminale.
Ora decidi di nuovo l'esecuzione memorizzata senza chiamare il riassuntore né il giudice:
oloproof gate RUN_ID --policy release.yamlvalid-format: PASS (observed_failures_within_limit)
short-enough: PASS (observed_failures_within_limit)
covers-facts-floor: INSUFFICIENT_EVIDENCE (interval_overlaps_threshold)
no sample size would make this PASS: the observed rate (0.500) is itself below the threshold (0.600), so more cases would move it toward FAIL
Gate: BLOCK (exit 3)Ora il giudice può decidere, e la decisione riguarda il riassuntore: il tasso che riporta, 0.500, non è il 45% del giudice. Poiché questa esecuzione ha un campione cieco e casuale di etichette di misura, il gate legge il giudice corretto da quelle etichette ("Judge-corrected gates" nella guida Giudici). La correzione è PPI, prediction-powered inference: usa il campione etichettato per misurare quanto il tasso del giudice si discosta da quello delle persone, e sposta la stima e allarga l'intervallo di altrettanto. È anche ciò a cui si riferiscono le note dell'esportazione su PPI. In ogni caso, la baseline non raggiunge la soglia, e più casi non lo cambierebbero.
Fai una modifica reale
app_v2.py salta i convenevoli brevi e tiene le due frasi successive. Copialo sopra app.py, imposta version: skip-pleasantries sotto system in oloproof.yaml, lascia il giudice in esecuzione, e:
oloproof runGate: ALLOW (exit 0)
│ covers-facts-floor │ covers_facts │ PASS │ lower_bound_meets_minimum │
│ covers_facts │ 100.0% │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/20 miss; judgment 6 hit/54 missIl giudice è la stessa versione validata, quindi la sua regola decide direttamente. Sei giudizi sono arrivati dalla cache, su riassunti che entrambe le versioni hanno scritto in modo identico. Nessuno ha etichettato questi nuovi riassunti; è la validazione del giudice a far valere i suoi verdetti.
Confronta il candidato con la baseline
version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
require_validated_evaluators: true
rules:
- id: covers-more-facts
kind: superiority
metric: covers_factsoloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yamlformat_valid: +0.0 points [-23.6, +23.6] · 20 paired · 0 missing · 0 excluded
short_enough: +0.0 points [-23.6, +23.6] · 20 paired · 0 missing · 0 excluded
covers_facts: +55.0 points [+13.0, +84.4] · 20 paired · 0 missing · 0 excluded
Decisions
covers-more-facts covers_facts superiority PASS difference_above_zero
Gate: ALLOW (exit 0)Un confronto non applica la correzione PPI: confronta i verdetti del giudice stesso sulle due esecuzioni, ed è per questo che il guadagno parte dal 45% del giudice e non dallo 0.500 corretto visto sopra. Undici riassunti sono migliorati e nessuno è peggiorato; l'intervallo del guadagno è interamente sopra lo zero, quindi la regola di superiorità passa e il comando esce con 0. Il formato è protetto dalle regole di esecuzione, che non ammettono fallimenti, anziché da un confronto: su 20 casi un confronto tra due punteggi di formato perfetti potrebbe solo dire che la differenza è entro 23,6 punti.
Facoltativo: un modello reale come giudice
Questo passo lascia il percorso offline. Richiede un server di modelli e, con un provider cloud, una chiave e del denaro.
- In locale, senza chiave e senza costi: Ollama, LM Studio o llama.cpp su localhost. Scarica un modello di chat (per Ollama, ollama pull llama3.1).
- Cloud: provider: anthropic o openai con api_key_env che nomina la variabile contenente la tua chiave, oppure openai_compatible con base_url e api_key_env. Ogni caso è una chiamata al giudice (due quando la prima risposta non è JSON valido), fatturata alle tariffe del tuo provider, e Oloproof non chiama mai di nuovo un giudice per una risposta che ha già giudicato.
Scrivi la bozza del giudice in un file a sé, come apparirebbe sotto evaluators::
# live_judge.yaml
type: rubric_judge
criterion: covers_facts
provider: openai_compatible
model: llama3.1
base_url: http://localhost:11434/v1
rubric_file: rubrics/covers_facts.mde provala sulle risposte che il tuo revisore ha già etichettato, senza validarla né adottarla:
oloproof evaluators try live_judge.yamlI server locali rispondono a una richiesta alla volta per impostazione predefinita; aggiungi concurrency: {system: 2, judge: 2} a oloproof.yaml così le chiamate in coda non vanno in timeout. Un'esecuzione di questo passo con un piccolo modello locale (qwen2.5vl) su un portatile ha stampato:
covers_facts: draft sha256:b88a... on 20 labelled cases · 20 judged now, 0 from cache, 11 errored
agreement 88.9% [19.1%, 99.9%] · 8 of 9 labelled cases agreed · 11 labelled but not judged · kappa 0.769Undici chiamate sono andate in timeout, e l'intervallo di accordo conta ciascuna in entrambi i sensi, quindi scende fino al 19,1%: un giudice che non risponde non viene misurato. Il rimedio è un modello più grande, un timeout più lungo o meno chiamate concorrenti. Per adottare il modello, mettilo in oloproof.yaml al posto del sostituto. È una nuova versione del valutatore: la sua configurazione (modello, endpoint, rubrica) è la sua identità, quindi la validazione del sostituto non si trasferisce. Esegui di nuovo la baseline con esso e validalo rispetto alle etichette, come sopra.
Risoluzione dei problemi
| Sintomo | Causa e rimedio |
|---|---|
| covers_facts tutto mancante, no_observations | Il server del giudice non è in esecuzione o non è su base_url. Ogni chiamata al giudice è andata in errore; oloproof inspect RUN_ID --failures mostra perché. |
| evaluator_not_validated dopo la validazione | Hai cambiato il giudice (modello, endpoint, porta, rubrica) e creato una nuova versione. Valida quella. |
| labels import rifiuta il file e nomina una riga | La riga nomina un caso o un'esecuzione che l'esecuzione non contiene; esporta di nuovo dall'esecuzione che etichetti. |
| labels export dice che non è stato possibile raggiungere uno spazio di lavoro | Hai effettuato l'accesso a uno, quindi gli ha chiesto di estrarre. --local estrae invece qui. |
| Un giudice cloud fallisce prima di qualsiasi chiamata | La sua chiave non è nella variabile indicata da api_key_env. |
Limitazioni
- Il giudice sostitutivo è una corrispondenza di frasi. Dimostra il flusso di lavoro, non la qualità del giudizio.
- Non ci sono valutatori BLEU, ROUGE o di similarità tra embedding. Nell'SDK, scrivine uno con @evaluator; oloproof.yaml non può ancora nominare un valutatore personalizzato.
- Un giudice vede testo: il JSON dell'input, del riferimento e dell'output. Non vede immagini né audio.
- Venti etichette danno un intervallo di accordo ampio. Etichettane di più, a caso e alla cieca, per un giudice su cui fai affidamento.
- Un campione locale vale solo in buona fede. Per un giudice su cui altre persone fanno affidamento, fai il push dell'esecuzione e lascia che sia uno spazio di lavoro ospitato a estrarre il campione (Giudici).