Aller au contenu

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 sheet

Lancez 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 8799
stand-in judge on http://127.0.0.1:8799/v1

L’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ÉvaluateurBesoin de expectedMesure
format_validjson_schemanonle format : un champ texte non vide
short_enoughregexnonle format : au plus 160 caractères
covers_factsrubric_judgeouila 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.60

require_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 run
Run 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 miss

Les 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 --failures
11 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.csv
Wrote 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.csv
filled 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 alice
covers_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 judged

Lisez 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.yaml
valid-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 run
Gate: 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 miss

Le 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_facts
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
format_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.md

et 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.yaml

Les 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.769

Onze 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ômeCause et correction
covers_facts entièrement manquant, no_observationsLe 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 validationVous 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 ligneLa 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 jointVous ê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 appelSa 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).