Ga naar de inhoud

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 sheet

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

De 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
CriteriumEvaluatorHeeft expected nodigMeet
format_validjson_schemaneeformaat: één niet-leeg tekstveld
short_enoughregexneeformaat: hoogstens 160 tekens
covers_factsrubric_judgejataaksucces, 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.60

require_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 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

De 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 --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
...

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.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 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.csv
filled 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 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

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

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 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

De 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_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)

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.md

en probeer hem op de antwoorden die je reviewer al labelde, zonder hem te valideren of over te nemen:

oloproof evaluators try live_judge.yaml

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

Elf 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

SymptoomOorzaak en oplossing
covers_facts helemaal ontbrekend, no_observationsDe 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 valideerdeJe wijzigde de judge (model, endpoint, poort, rubric) en maakte een nieuwe versie. Valideer die.
labels import weigert het bestand en noemt een rijDe 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 wasJe bent bij een ingelogd, dus vroeg het die om te trekken. --local trekt hier.
Een cloudjudge faalt vóór elke aanroepZijn 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).