Guides
Tutoriel : génération de texte avec un juge à grille
Évaluez une fonction qui écrit du texte libre, ici un résumeur de tickets, avec des vérifications de format et un juge à grille ; mesurez ce juge par rapport aux étiquettes d’une personne avant qu’il puisse décider quoi que ce soit ; puis comparez une vraie modification. Le juge tourne sur cette machine sans modèle ni réseau, et une étape facultative le remplace par un vrai modèle.
Ce que vous allez construire
Un résumeur qui transforme un ticket d’assistance en une ou deux phrases. « Bon » est un jugement, pas une correspondance de chaînes : la réussite de la tâche est donc décidée par un juge LLM muni d’une grille : le résumé énonce-t-il les faits dont un agent a besoin ? Deux évaluateurs déterministes vérifient le format, ce qui ne demande aucune référence. Les termes tels que cas, exécution, métrique, juge et porte sont définis dans Concepts.
La même forme convient à l’extraction ou à toute autre génération : une fonction renvoie du texte dans un dictionnaire, la référence dit ce qu’une bonne réponse doit contenir, et une grille dit comment décider.
Prérequis
- Python 3.11 ou ultérieur, et Oloproof dans un environnement virtuel :
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof- Le projet d’exemple, livré avec le paquet. Copiez-le dans un nouveau répertoire et travaillez-y :
oloproof init --example generation ticket-summaries
cd ticket-summaries- Le port 8799 libre pour le juge de substitution (sinon, changez-le aux deux endroits).
Chaque étape jusqu’à « Facultatif : un vrai modèle comme juge » est hors ligne et déterministe : aucune clé d’API, aucun compte de fournisseur, aucun coût.
Les fichiers
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 sheetLancez chaque commande depuis ticket-summaries/.
Le juge de substitution, et ce qu’il n’est pas
Un juge à grille est un évaluateur qui envoie un prompt (la grille, l’entrée du cas, son expected et la sortie) à un modèle et lit en retour {"pass": true|false, "rationale": "..."}. Oloproof parle à tout serveur qui parle l’API de chat d’OpenAI, et un serveur sur localhost n’a besoin d’aucune clé.
judge_server.py est un tel serveur, mais ce n’est pas un modèle. Il ne fait passer un résumé que s’il contient chaque expression listée sous must_mention dans l’expected du cas, sans tenir compte de la casse. C’est une règle fixe, donc le tutoriel donne les mêmes chiffres sur toutes les machines. Il ne peut pas remarquer un fait inventé, ce qu’on demande à un vrai juge modèle. Lancez-le dans un second terminal et laissez-le tourner :
python judge_server.py --port 8799stand-in judge on http://127.0.0.1:8799/v1L’application et son adaptateur
# 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’adaptateur d’une application Python est la fonction : elle reçoit l’input du cas et renvoie un dictionnaire. Pour votre propre générateur, appelez votre modèle ou votre chaîne à l’intérieur et renvoyez le texte sous une clé. Oloproof l’appelle une fois par cas et met la sortie en cache selon la source de la fonction et la version déclarée ; il ne gère ni votre client de modèle, ni vos prompts, ni votre état. Listez les fichiers que la fonction lit, comme un gabarit de prompt, sous system.code_paths.
Le jeu de données
{"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 est ce que reçoit la fonction. expected est la référence que lit le juge : ici une liste de faits que le résumé doit porter, et non un résumé de référence complet, parce que beaucoup de résumés différents sont corrects. La sortie pour t01 est {"summary": "Hello."}.
Choisir les évaluateurs
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| Critère | Évaluateur | Besoin de expected | Mesure |
|---|---|---|---|
| format_valid | json_schema | non | le format : un champ texte non vide |
| short_enough | regex | non | le format : au plus 160 caractères |
| covers_facts | rubric_judge | oui | la réussite de la tâche, telle que la grille la définit |
Hello. passe les deux vérifications de format. Seul le juge dit que c’est un résumé inutile. Un juge peut aussi tourner sans référence : une grille telle que « PASS si le résumé ne contient aucune salutation » ne lit que l’entrée et la sortie, et un cas sans expected est quand même jugé. Ce qu’il ne peut alors pas faire, c’est vérifier des faits par rapport à une réponse en laquelle vous avez confiance.
La grille :
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 politique
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 est la valeur par défaut du moteur, écrite ici parce que c’est le cœur de ce tutoriel : un juge que personne n’a comparé à des personnes ne peut pas décider d’une règle.
L’exécuter
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 missLes règles de format passent. Le juge a fait passer 9 résumés sur 20, mais la règle est INSUFFICIENT_EVIDENCE avec la raison evaluator_not_validated, et la porte bloque avec le code 3. La règle n’a pas décidé sur les 45% : le taux d’erreur d’un juge est inconnu tant qu’il n’est pas mesuré, donc un intervalle construit sur ses verdicts porterait une erreur non déclarée. Le moteur le signale comme INSUFFICIENT_EVIDENCE, pas comme MANUAL_REVIEW ni FAIL : les preuves pour décider manquent, et la sortie affiche les deux commandes qui les fournissent.
Inspecter les échecs
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 justification du juge est affichée comme « judge text, not verified » : c’est l’explication du modèle, pas une preuve. Le motif est clair malgré tout : la première phrase est souvent une salutation.
Mesurer le juge par rapport à une personne
La validation compare les verdicts du juge à ceux d’une personne sur les mêmes réponses. Tirez un échantillon aléatoire des cas de l’exécution dans une feuille. Les verdicts du juge en sont exclus, pour que la personne qui étiquette ne soit pas influencée par eux :
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 tire l’échantillon sur cette machine sans le demander à un espace de travail hébergé ; le moteur choisit la graine. Avec 20 cas, un échantillon de 20 les contient tous. En pratique, une personne lit le ticket et le résumé de chaque ligne et remplit passed. Pour ce tutoriel, labels/reviewer_verdicts.csv contient les verdicts qu’un relecteur a donnés sur les résumés de la référence, et fill_labels.py les copie dans la feuille :
python fill_labels.py sample.csv
oloproof labels import sample.csvfilled 20 rows of sample.csv
Recorded 20 labels from sample.csv (20 measurement).Le relecteur a été en désaccord avec le juge une fois : sur t02 (« I was charged twice for order 2210. »), il a jugé le montant manquant sans importance et l’a fait passer. Les étiquettes nomment la réponse exacte qu’elles ont jugée, donc ces verdicts ne s’appliquent qu’à l’exécution de référence.
Trouvez l’identifiant de version du juge et validez-le :
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 judgedLisez les intervalles, pas les 95% : 20 étiquettes montrent un accord d’au moins 75.1%. Une politique peut exiger davantage avec minimum_evaluator_agreement, qui compare cette borne inférieure, et validate refuse un juge en dessous. Le guide Juges couvre le seuil, le biais, les sondes et oloproof review pour étiqueter dans le terminal.
Redécidez maintenant l’exécution stockée sans appeler le résumeur ni le juge :
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)Le juge peut maintenant décider, et la décision porte sur le résumeur : le taux cité, 0.500, n’est pas les 45% du juge. Comme cette exécution a un échantillon aveugle et aléatoire d’étiquettes de mesure, la porte lit le juge corrigé par ces étiquettes (« Judge-corrected gates » dans le guide Juges). La correction est la PPI, l’inférence assistée par prédiction : elle utilise l’échantillon étiqueté pour mesurer l’écart entre le taux du juge et celui des personnes, et déplace l’estimation et élargit l’intervalle d’autant. C’est aussi ce à quoi renvoient les notes de l’export sur la PPI. Dans tous les cas, la référence n’atteint pas le plancher, et plus de cas n’y changeraient rien.
Faire une vraie modification
app_v2.py saute les courtes formules de politesse et garde les deux phrases suivantes. Copiez-le par-dessus app.py, réglez version: skip-pleasantries sous system dans oloproof.yaml, laissez le juge tourner, et :
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 missLe juge est la même version validée, donc sa règle décide directement. Six jugements venaient du cache, sur des résumés que les deux versions ont écrits à l’identique. Personne n’a étiqueté ces nouveaux résumés ; c’est la validation du juge qui permet à ses verdicts de tenir.
Comparer le candidat à la référence
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)Une comparaison n’applique pas la correction PPI : elle compare les propres verdicts du juge sur les deux exécutions, c’est pourquoi le gain part des 45% du juge et non du 0.500 corrigé ci-dessus. Onze résumés se sont améliorés et aucun ne s’est dégradé ; l’intervalle du gain est entièrement au-dessus de zéro, donc la règle de supériorité passe et la commande se termine avec 0. Le format est gardé par les règles d’exécution, qui n’autorisent aucun échec, plutôt que par une comparaison : sur 20 cas, une comparaison de deux scores de format parfaits pourrait seulement dire que la différence est à moins de 23.6 points.
Facultatif : un vrai modèle comme juge
Cette étape quitte le chemin hors ligne. Elle demande un serveur de modèle, et avec un fournisseur cloud, une clé et de l’argent.
- En local, sans clé et sans coût : Ollama, LM Studio ou llama.cpp sur localhost. Téléchargez un modèle de chat (pour Ollama, ollama pull llama3.1).
- Cloud : provider: anthropic ou openai avec api_key_env nommant la variable qui contient votre clé, ou openai_compatible avec base_url et api_key_env. Chaque cas est un appel au juge (deux quand la première réponse n’est pas du JSON valide), facturé aux tarifs de votre fournisseur, et Oloproof n’appelle jamais à nouveau un juge pour une réponse qu’il a déjà jugée.
Écrivez le brouillon de juge dans un fichier à part, tel qu’il apparaîtrait sous 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.mdet essayez-le sur les réponses que votre relecteur a déjà étiquetées, sans le valider ni l’adopter :
oloproof evaluators try live_judge.yamlLes serveurs locaux répondent à une requête à la fois par défaut ; ajoutez concurrency: {system: 2, judge: 2} à oloproof.yaml pour que les appels en file n’expirent pas. Une exécution de cette étape avec un petit modèle local (qwen2.5vl) sur un ordinateur portable a affiché :
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.769Onze appels ont expiré, et l’intervalle d’accord compte chacun dans les deux sens, donc il descend jusqu’à 19.1% : un juge qui ne répond pas n’est pas mesuré. Un modèle plus grand, un délai plus long ou moins d’appels simultanés est la correction. Pour adopter le modèle, mettez-le dans oloproof.yaml à la place du juge de substitution. C’est une nouvelle version d’évaluateur : sa configuration (modèle, point d’accès, grille) est son identité, donc la validation du juge de substitution ne se transmet pas. Relancez la référence avec lui et validez-le par rapport aux étiquettes, comme ci-dessus.
Dépannage
| Symptôme | Cause et correction |
|---|---|
| covers_facts entièrement manquant, no_observations | Le serveur du juge ne tourne pas ou n’est pas sur base_url. Chaque appel au juge a échoué ; oloproof inspect RUN_ID --failures montre pourquoi. |
| evaluator_not_validated après validation | Vous avez modifié le juge (modèle, point d’accès, port, grille) et créé une nouvelle version. Validez celle-ci. |
| labels import refuse le fichier et nomme une ligne | La ligne nomme un cas ou une exécution que l’exécution ne contient pas ; exportez à nouveau depuis l’exécution que vous étiquetez. |
| labels export dit qu’un espace de travail n’a pas pu être joint | Vous êtes connecté à l’un d’eux, donc il lui a demandé de tirer l’échantillon. --local le tire ici à la place. |
| Un juge cloud échoue avant tout appel | Sa clé n’est pas dans la variable que nomme api_key_env. |
Limites
- Le juge de substitution est une correspondance d’expressions. Il montre le déroulement, pas la qualité du jugement.
- Il n’y a pas d’évaluateurs BLEU, ROUGE ou de similarité d’embeddings. Dans le SDK, écrivez-en un avec @evaluator ; oloproof.yaml ne peut pas encore nommer un évaluateur personnalisé.
- Un juge voit du texte : le JSON de l’entrée, de la référence et de la sortie. Il ne voit ni images ni audio.
- Vingt étiquettes donnent un intervalle d’accord large. Étiquetez-en davantage, au hasard et à l’aveugle, pour un juge sur lequel vous comptez.
- Un échantillon local n’est que de bonne foi. Pour un juge sur lequel d’autres comptent, poussez l’exécution et laissez un espace de travail hébergé tirer l’échantillon (Juges).