Handleidingen
Tutorial: tekstgeneratie met een rubric-judge
Evalueer een functie die vrije tekst schrijft, hier een ticketsamenvatter, met formaatcontroles en een rubric-judge; meet die judge tegen de labels van een mens voordat hij iets mag beslissen; en vergelijk daarna een echte wijziging. De judge draait op deze machine zonder model en zonder netwerk, en een optionele stap zet er een echt model voor in de plaats.
Wat je gaat bouwen
Een samenvatter die een supportticket omzet in één of twee zinnen. "Goed" is een oordeel, geen tekstovereenkomst, dus taaksucces wordt beslist door een LLM-judge met een rubric: noemt de samenvatting de feiten die een medewerker nodig heeft? Twee deterministische evaluators controleren het formaat, waarvoor geen referentie nodig is. Termen als case, run, metriek, judge en gate worden gedefinieerd in Kernbegrippen.
Dezelfde vorm past op extractie of elke andere generatie: een functie geeft tekst terug in een dictionary, de referentie zegt wat een goed antwoord moet bevatten, en een rubric zegt hoe je beslist.
Vereisten
- Python 3.11 of nieuwer, en Oloproof in een virtuele omgeving:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof- Het voorbeeldproject, dat met het pakket meekomt. Kopieer het naar een nieuwe map en werk daar:
oloproof init --example generation ticket-summaries
cd ticket-summaries- Poort 8799 vrij voor de vervangende judge (pas hem anders op beide plaatsen aan).
Elke stap tot "Optioneel: een echt model als judge" is offline en deterministisch: geen API-sleutel, geen provideraccount, geen kosten.
De bestanden
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 sheetVoer elk commando uit vanuit ticket-summaries/.
De vervangende judge, en wat hij niet is
Een rubric-judge is een evaluator die een prompt (de rubric, de input van de case, haar expected en de uitvoer) naar een model stuurt en {"pass": true|false, "rationale": "..."} terugleest. Oloproof praat met elke server die de OpenAI-chat-API spreekt, en een server op localhost heeft geen sleutel nodig.
judge_server.py is zo'n server, maar het is geen model. Hij laat een samenvatting alleen slagen als die elke zinsnede onder must_mention in de expected van de case bevat, zonder op hoofdletters te letten. Dat is een vaste regel, dus de tutorial geeft op elke machine dezelfde getallen. Hij kan geen verzonnen feit opmerken, wat een echte modeljudge wel gevraagd wordt. Start hem in een tweede terminal en laat hem draaien:
python judge_server.py --port 8799stand-in judge on http://127.0.0.1:8799/v1De applicatie en haar adapter
# 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]}De adapter voor een Python-applicatie is de functie: ze ontvangt de input van de case en geeft een dictionary terug. Roep voor je eigen generator je model of keten erin aan en geef de tekst terug onder een sleutel. Oloproof roept haar één keer per case aan en cachet de uitvoer op de broncode van de functie en de gedeclareerde version; het beheert je modelclient, prompts of toestand niet. Zet bestanden die de functie leest, zoals een promptsjabloon, onder system.code_paths.
De 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 is wat de functie ontvangt. expected is de referentie die de judge leest: hier een lijst feiten die de samenvatting moet bevatten, geen volledige referentiesamenvatting, omdat veel verschillende samenvattingen juist zijn. De uitvoer voor t01 is {"summary": "Hello."}.
De evaluators kiezen
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| Criterium | Evaluator | Heeft expected nodig | Meet |
|---|---|---|---|
| format_valid | json_schema | nee | formaat: één niet-leeg tekstveld |
| short_enough | regex | nee | formaat: hoogstens 160 tekens |
| covers_facts | rubric_judge | ja | taaksucces, zoals de rubric het definieert |
Hello. haalt beide formaatcontroles. Alleen de judge zegt dat het een nutteloze samenvatting is. Een judge kan ook zonder referentie draaien: een rubric als "PASS als de samenvatting geen groet bevat" leest alleen de input en de uitvoer, en een case zonder expected wordt toch beoordeeld. Wat hij dan niet kan, is feiten controleren tegen een antwoord dat je vertrouwt.
De rubric:
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.De 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 is de standaard van de engine, hier uitgeschreven omdat het de kern van deze tutorial is: een judge die niemand met mensen heeft vergeleken mag geen regel beslissen.
Draaien
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 missDe formaatregels slagen. De judge liet 9 van de 20 samenvattingen slagen, maar de regel is INSUFFICIENT_EVIDENCE met de reden evaluator_not_validated, en de gate blokkeert met exit 3. De regel besliste niet op de 45%: het foutpercentage van een judge is onbekend tot het gemeten is, dus een interval gebouwd op zijn oordelen zou een onvermelde fout dragen. De engine meldt dit als INSUFFICIENT_EVIDENCE, niet als MANUAL_REVIEW of FAIL: het bewijs om te beslissen ontbreekt, en de uitvoer print de twee commando's die het leveren.
De mislukkingen bekijken
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
...De motivering van de judge wordt getoond als "judge text, not verified": het is de uitleg van het model, geen bewijs. Het patroon is toch duidelijk: de eerste zin is vaak een groet.
De judge tegen een mens meten
Validatie vergelijkt de oordelen van de judge met die van een mens op dezelfde antwoorden. Trek een willekeurige steekproef van de cases van de run in een sheet. De oordelen van de judge blijven eruit, zodat de labelaar er niet door beïnvloed wordt:
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 trekt op deze machine zonder een gehoste workspace te vragen; de engine kiest de seed. Met 20 cases is een steekproef van 20 ze allemaal. In de praktijk leest een mens het ticket en de samenvatting van elke rij en vult passed in. Voor deze tutorial bevat labels/reviewer_verdicts.csv oordelen die een reviewer gaf op de samenvattingen van de baseline, en fill_labels.py kopieert ze in de sheet:
python fill_labels.py sample.csv
oloproof labels import sample.csvfilled 20 rows of sample.csv
Recorded 20 labels from sample.csv (20 measurement).De reviewer was het één keer oneens met de judge: bij t02 ("I was charged twice for order 2210.") vond hij het ontbrekende bedrag onbelangrijk en liet het slagen. Labels noemen het exacte antwoord dat ze beoordeelden, dus deze oordelen gelden alleen voor de baselinerun.
Zoek het versie-id van de judge en valideer hem:
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 judgedLees de intervallen, niet de 95%: 20 labels tonen een overeenstemming van minstens 75.1%. Een policy kan meer eisen met minimum_evaluator_agreement, dat die ondergrens vergelijkt, en validate weigert een judge eronder. De gids Judges behandelt de lat, bias, probes en oloproof review om in de terminal te labelen.
Beslis nu de opgeslagen run opnieuw zonder de samenvatter of de judge aan te roepen:
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)De judge mag nu beslissen, en de beslissing gaat over de samenvatter: het percentage dat hij noemt, 0.500, is niet de 45% van de judge. Omdat deze run een blinde, willekeurige steekproef van meetlabels heeft, leest de gate de judge gecorrigeerd door die labels ("Judge-corrected gates" in de gids Judges). De correctie is PPI, prediction-powered inference: ze gebruikt de gelabelde steekproef om te meten hoe ver het percentage van de judge van dat van de mensen ligt, en verschuift de schatting en verbreedt het interval met zoveel. Daar verwijzen ook de opmerkingen over PPI in de export naar. Hoe dan ook haalt de baseline de ondergrens niet, en meer cases zouden dat niet veranderen.
Een echte wijziging maken
app_v2.py slaat korte beleefdheden over en houdt de volgende twee zinnen. Kopieer het over app.py, zet version: skip-pleasantries onder system in oloproof.yaml, laat de judge draaien, en:
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 missDe judge is dezelfde gevalideerde versie, dus zijn regel beslist direct. Zes oordelen kwamen uit de cache, op samenvattingen die beide versies identiek schreven. Niemand labelde deze nieuwe samenvattingen; de validatie van de judge is wat zijn oordelen laat staan.
De kandidaat met de baseline vergelijken
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)Een vergelijking past de PPI-correctie niet toe: ze vergelijkt de eigen oordelen van de judge op de twee runs, en daarom begint de winst bij de 45% van de judge en niet bij de gecorrigeerde 0.500 hierboven. Elf samenvattingen verbeterden en geen enkele werd slechter; het interval voor de winst ligt geheel boven nul, dus de superioriteitsregel slaagt en het commando eindigt met 0. Formaat wordt bewaakt door de runregels, die geen mislukking toestaan, in plaats van door een vergelijking: over 20 cases zou een vergelijking van twee perfecte formaatscores alleen kunnen zeggen dat het verschil binnen 23.6 punten ligt.
Optioneel: een echt model als judge
Deze stap verlaat het offline pad. Hij heeft een modelserver nodig, en bij een cloudprovider een sleutel en geld.
- Lokaal, geen sleutel en geen kosten: Ollama, LM Studio of llama.cpp op localhost. Haal een chatmodel op (voor Ollama ollama pull llama3.1).
- Cloud: provider: anthropic of openai met api_key_env dat de variabele noemt waarin je sleutel staat, of openai_compatible met base_url en api_key_env. Elke case is één judge-aanroep (twee als het eerste antwoord geen geldige JSON is), gefactureerd tegen de tarieven van je provider, en Oloproof roept nooit opnieuw een judge aan voor een antwoord dat al beoordeeld is.
Schrijf de conceptjudge in een eigen bestand, zoals hij onder evaluators: zou staan:
# 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.mden probeer hem op de antwoorden die je reviewer al labelde, zonder hem te valideren of over te nemen:
oloproof evaluators try live_judge.yamlLokale servers beantwoorden standaard één verzoek tegelijk; voeg concurrency: {system: 2, judge: 2} toe aan oloproof.yaml zodat aanroepen in de wachtrij niet in een time-out lopen. Een run van deze stap met een klein lokaal model (qwen2.5vl) op een laptop printte:
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.769Elf aanroepen liepen in een time-out, en het overeenstemmingsinterval telt elk daarvan in beide richtingen, dus het reikt tot 19.1%: een judge die niet antwoordt wordt niet gemeten. Een groter model, een langere time-out of minder gelijktijdige aanroepen is de oplossing. Om het model over te nemen, zet je het in oloproof.yaml in plaats van de vervanger. Dat is een nieuwe evaluatorversie: de configuratie (model, endpoint, rubric) is de identiteit, dus de validatie van de vervanger gaat niet mee. Draai de baseline er opnieuw mee en valideer hem tegen de labels, zoals hierboven.
Problemen oplossen
| Symptoom | Oorzaak en oplossing |
|---|---|
| covers_facts helemaal ontbrekend, no_observations | De judgeserver draait niet of niet op base_url. Elke judge-aanroep gaf een fout; oloproof inspect RUN_ID --failures toont waarom. |
| evaluator_not_validated nadat je valideerde | Je wijzigde de judge (model, endpoint, poort, rubric) en maakte een nieuwe versie. Valideer die. |
| labels import weigert het bestand en noemt een rij | De rij noemt een case of uitvoering die de run niet bevat; exporteer opnieuw vanuit de run die je labelt. |
| labels export zegt dat een workspace niet bereikbaar was | Je bent bij een ingelogd, dus vroeg het die om te trekken. --local trekt hier. |
| Een cloudjudge faalt vóór elke aanroep | Zijn sleutel staat niet in de variabele die api_key_env noemt. |
Beperkingen
- De vervangende judge is een zinsnede-overeenkomst. Hij demonstreert de werkwijze, niet de kwaliteit van beoordelen.
- Er zijn geen BLEU-, ROUGE- of embedding-similarity-evaluators. Schrijf er in de SDK een met @evaluator; oloproof.yaml kan nog geen eigen evaluator noemen.
- Een judge ziet tekst: JSON van de input, referentie en uitvoer. Hij ziet geen afbeeldingen of audio.
- Twintig labels geven een breed overeenstemmingsinterval. Label er meer, willekeurig en blind, voor een judge waarop je vertrouwt.
- Een lokale steekproef is alleen te goeder trouw. Push voor een judge waarop anderen vertrouwen de run en laat een gehoste workspace de steekproef trekken (Judges).