Przejdź do treści

Przewodniki

Samouczek: generowanie tekstu z sędzią z rubryką

Oceń funkcję, która pisze swobodny tekst, tutaj narzędzie streszczające zgłoszenia, za pomocą sprawdzeń formatu i sędziego z rubryką; zmierz tego sędziego względem etykiet nadanych przez człowieka, zanim będzie mógł cokolwiek rozstrzygać; potem porównaj prawdziwą zmianę. Sędzia działa na tej maszynie bez modelu i bez sieci, a opcjonalny krok podstawia prawdziwy model.

Co zbudujesz

Narzędzie, które zamienia zgłoszenie do wsparcia w jedno lub dwa zdania. "Dobre" to kwestia oceny, a nie dopasowania tekstu, więc o sukcesie zadania decyduje sędzia LLM z rubryką: czy streszczenie podaje fakty potrzebne konsultantowi? Dwa ewaluatory deterministyczne sprawdzają format, co nie wymaga wzorca. Terminy takie jak przypadek, przebieg, metryka, sędzia i bramka są zdefiniowane w Pojęciach.

Ten sam schemat pasuje do ekstrakcji i każdego innego generowania: funkcja zwraca tekst w słowniku, wzorzec mówi, co musi zawierać dobra odpowiedź, a rubryka mówi, jak rozstrzygać.

Wymagania wstępne

  • Python 3.11 lub nowszy oraz Oloproof w środowisku wirtualnym:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • Przykładowy projekt, dostarczany z pakietem. Skopiuj go do nowego katalogu i pracuj tam:
oloproof init --example generation ticket-summaries
cd ticket-summaries
  • Wolny port 8799 dla zastępczego sędziego (jeśli nie, zmień go w obu miejscach).

Każdy krok aż do "Opcjonalnie: prawdziwy model jako sędzia" działa offline i deterministycznie: bez klucza API, bez konta u dostawcy, bez kosztów.

Pliki

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

Uruchamiaj każde polecenie z ticket-summaries/.

Zastępczy sędzia i czym nie jest

Sędzia z rubryką to ewaluator, który wysyła prompt (rubrykę, wejście przypadku, jego expected i wynik) do modelu i odczytuje {"pass": true|false, "rationale": "..."}. Oloproof rozmawia z każdym serwerem obsługującym API czatu OpenAI, a serwer na localhost nie potrzebuje klucza.

judge_server.py jest takim serwerem, ale nie jest modelem. Przepuszcza streszczenie tylko wtedy, gdy zawiera ono każdą frazę z must_mention w expected przypadku, bez względu na wielkość liter. To stała reguła, więc samouczek daje te same liczby na każdej maszynie. Nie zauważy zmyślonego faktu, o co prosi się prawdziwego sędziego-model. Uruchom go w drugim terminalu i zostaw włączonego:

python judge_server.py --port 8799
stand-in judge on http://127.0.0.1:8799/v1

Aplikacja i jej 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]}

Adapterem dla aplikacji w Pythonie jest funkcja: otrzymuje input przypadku i zwraca słownik. Dla własnego generatora wywołaj w niej swój model lub łańcuch i zwróć tekst pod kluczem. Oloproof wywołuje ją raz na przypadek i cache'uje wynik według kodu źródłowego funkcji i zadeklarowanej version; nie zarządza Twoim klientem modelu, promptami ani stanem. Pliki, które funkcja czyta, na przykład szablon promptu, wymień w system.code_paths.

Zbiór danych

{"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 to to, co otrzymuje funkcja. expected to wzorzec, który czyta sędzia: tutaj lista faktów, które streszczenie musi zawierać, a nie pełne streszczenie wzorcowe, ponieważ poprawnych streszczeń jest wiele. Wynik dla t01 to {"summary": "Hello."}.

Wybór ewaluatorów

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
KryteriumEwaluatorWymaga expectedMierzy
format_validjson_schemanieformat: jedno niepuste pole tekstowe
short_enoughregexnieformat: co najwyżej 160 znaków
covers_factsrubric_judgetaksukces zadania, tak jak definiuje go rubryka

Hello. przechodzi oba sprawdzenia formatu. Tylko sędzia mówi, że to bezużyteczne streszczenie. Sędzia może też działać bez wzorca: rubryka taka jak "PASS if the summary contains no greeting" czyta tylko wejście i wynik, a przypadek bez expected nadal jest oceniany. Nie może wtedy sprawdzać faktów względem odpowiedzi, której ufasz.

Rubryka:

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.

Polityka

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 to domyślne ustawienie silnika, zapisane tu jawnie, bo to sedno tego samouczka: sędzia, którego nikt nie porównał z ludźmi, nie może rozstrzygać reguły.

Uruchom

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

Reguły formatu przechodzą. Sędzia przepuścił 9 z 20 streszczeń, ale reguła ma stan INSUFFICIENT_EVIDENCE z przyczyną evaluator_not_validated, a bramka blokuje z kodem 3. Reguła nie rozstrzygnęła na podstawie 45%: odsetek błędów sędziego jest nieznany, dopóki się go nie zmierzy, więc przedział zbudowany na jego werdyktach niósłby niezadeklarowany błąd. Silnik raportuje to jako INSUFFICIENT_EVIDENCE, a nie MANUAL_REVIEW czy FAIL: brakuje dowodów do rozstrzygnięcia, a wynik wypisuje dwa polecenia, które je dostarczają.

Przejrzyj niepowodzenia

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

Uzasadnienie sędziego jest pokazane jako "judge text, not verified": to wyjaśnienie modelu, a nie dowód. Wzorzec i tak jest jasny: pierwsze zdanie to często powitanie.

Zmierz sędziego względem człowieka

Walidacja porównuje werdykty sędziego z werdyktami człowieka na tych samych odpowiedziach. Wylosuj próbę przypadków przebiegu do arkusza. Werdykty sędziego są z niego pominięte, więc osoba etykietująca nie jest nimi zakotwiczona:

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 losuje na tej maszynie, bez pytania hostowanego obszaru roboczego; ziarno wybiera silnik. Przy 20 przypadkach próba 20 to wszystkie. W praktyce osoba czyta zgłoszenie i streszczenie w każdym wierszu i wypełnia passed. Na potrzeby tego samouczka labels/reviewer_verdicts.csv zawiera werdykty, które recenzent wydał o streszczeniach punktu odniesienia, a fill_labels.py kopiuje je do arkusza:

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

Recenzent nie zgodził się z sędzią raz: przy t02 ("I was charged twice for order 2210.") uznał brakującą kwotę za nieistotną i przepuścił streszczenie. Etykiety wskazują dokładną odpowiedź, którą oceniono, więc te werdykty dotyczą tylko przebiegu punktu odniesienia.

Znajdź identyfikator wersji sędziego i zwaliduj go:

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

Czytaj przedziały, a nie 95%: 20 etykiet wykazuje zgodność co najmniej 75,1%. Polityka może żądać więcej przez minimum_evaluator_agreement, które porównuje tę dolną granicę, a validate odrzuca sędziego poniżej niej. Przewodnik Sędziowie omawia poprzeczkę, obciążenie, sondy i oloproof review do etykietowania w terminalu.

Teraz podejmij ponownie decyzję dla zapisanego przebiegu bez wywoływania narzędzia streszczającego ani sędziego:

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)

Sędzia może teraz rozstrzygać, a decyzja dotyczy narzędzia streszczającego: podany odsetek, 0.500, to nie 45% sędziego. Ponieważ ten przebieg ma ślepą, losową próbę etykiet pomiarowych, bramka czyta sędziego skorygowanego tymi etykietami ("Judge-corrected gates" w przewodniku Sędziowie). Korekta to PPI, prediction-powered inference: wykorzystuje próbę z etykietami, by zmierzyć, jak daleko odsetek sędziego leży od odsetka ludzi, i o tyle przesuwa estymatę i poszerza przedział. Do tego też odnoszą się uwagi eksportu o PPI. Tak czy inaczej punkt odniesienia nie spełnia progu, a więcej przypadków by tego nie zmieniło.

Wprowadź prawdziwą zmianę

app_v2.py pomija krótkie uprzejmości i zachowuje dwa kolejne zdania. Skopiuj go na app.py, ustaw version: skip-pleasantries pod system w oloproof.yaml, nie wyłączaj sędziego i:

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

Sędzia to ta sama zwalidowana wersja, więc jego reguła rozstrzyga bezpośrednio. Sześć ocen pochodziło z cache, dla streszczeń, które obie wersje napisały identycznie. Nikt nie etykietował tych nowych streszczeń; to walidacja sędziego pozwala jego werdyktom obowiązywać.

Porównaj kandydata z punktem odniesienia

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)

Porównanie nie stosuje korekty PPI: porównuje własne werdykty sędziego w obu przebiegach, dlatego zysk liczy się od 45% sędziego, a nie od skorygowanego 0.500 powyżej. Jedenaście streszczeń się poprawiło, a żadne nie pogorszyło; przedział zysku leży w całości powyżej zera, więc reguła wyższości przechodzi, a polecenie kończy się kodem 0. Format pilnują reguły przebiegu, które nie dopuszczają żadnego niepowodzenia, a nie porównanie: na 20 przypadkach porównanie dwóch idealnych wyników formatu mogłoby jedynie stwierdzić, że różnica mieści się w 23,6 punktu.

Opcjonalnie: prawdziwy model jako sędzia

Ten krok opuszcza ścieżkę offline. Wymaga serwera modelu, a u dostawcy chmurowego klucza i pieniędzy.

  • Lokalnie, bez klucza i bez kosztów: Ollama, LM Studio lub llama.cpp na localhost. Pobierz model czatu (dla Ollama ollama pull llama3.1).
  • Chmura: provider: anthropic lub openai z api_key_env wskazującym zmienną z Twoim kluczem albo openai_compatible z base_url i api_key_env. Każdy przypadek to jedno wywołanie sędziego (dwa, gdy pierwsza odpowiedź nie jest poprawnym JSON), rozliczane według stawek Twojego dostawcy, a Oloproof nigdy nie wywołuje sędziego ponownie dla odpowiedzi, którą już ocenił.

Zapisz roboczego sędziego w osobnym pliku, tak jak wyglądałby pod 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

i wypróbuj go na odpowiedziach, które recenzent już oetykietował, bez walidowania go i bez przyjmowania:

oloproof evaluators try live_judge.yaml

Serwery lokalne domyślnie odpowiadają na jedno żądanie naraz; dodaj concurrency: {system: 2, judge: 2} do oloproof.yaml, aby zakolejkowane wywołania nie przekraczały limitu czasu. Przebieg tego kroku z małym lokalnym modelem (qwen2.5vl) na laptopie wypisał:

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

Jedenaście wywołań przekroczyło limit czasu, a przedział zgodności liczy każde z nich na obie strony, więc sięga w dół do 19,1%: sędzia, który nie odpowiada, nie jest zmierzony. Rozwiązaniem jest większy model, dłuższy limit czasu lub mniej równoległych wywołań. Aby przyjąć model, wstaw go do oloproof.yaml w miejsce zastępczego. To nowa wersja ewaluatora: jej konfiguracja (model, punkt końcowy, rubryka) jest jej tożsamością, więc walidacja zastępczego sędziego nie przechodzi na nią. Uruchom z nim ponownie punkt odniesienia i zwaliduj go względem etykiet, jak wyżej.

Rozwiązywanie problemów

ObjawPrzyczyna i rozwiązanie
covers_facts w całości brakujące, no_observationsSerwer sędziego nie działa albo nie jest pod base_url. Każde wywołanie sędziego zakończyło się błędem; oloproof inspect RUN_ID --failures pokazuje dlaczego.
evaluator_not_validated po walidacjiZmieniłeś sędziego (model, punkt końcowy, port, rubrykę) i powstała nowa wersja. Zwaliduj tę.
labels import odrzuca plik i wskazuje wierszWiersz wskazuje przypadek lub wykonanie, którego przebieg nie zawiera; wyeksportuj ponownie z przebiegu, który etykietujesz.
labels export mówi, że nie udało się połączyć z obszarem roboczymJesteś zalogowany do jakiegoś, więc poprosił go o losowanie. --local losuje zamiast tego tutaj.
Sędzia chmurowy zawodzi przed jakimkolwiek wywołaniemJego klucza nie ma w zmiennej wskazanej przez api_key_env.

Ograniczenia

  • Zastępczy sędzia to dopasowanie fraz. Pokazuje przebieg pracy, a nie jakość oceniania.
  • Nie ma ewaluatorów BLEU, ROUGE ani podobieństwa embeddingów. W SDK napisz taki z @evaluator; oloproof.yaml nie może jeszcze wskazać własnego ewaluatora.
  • Sędzia widzi tekst: JSON wejścia, wzorca i wyniku. Nie widzi obrazów ani dźwięku.
  • Dwadzieścia etykiet daje szeroki przedział zgodności. Dla sędziego, na którym polegasz, etykietuj więcej, losowo i na ślepo.
  • Lokalna próba ma charakter wyłącznie good-faith. Dla sędziego, na którym polegają inni, wypchnij przebieg i pozwól hostowanemu obszarowi roboczemu wylosować próbę (Sędziowie).