Pular para o conteúdo

Guias

Tutorial: geração de texto com um juiz de rubrica

Avalie uma função que escreve texto livre, aqui um resumidor de tickets, com verificações de formato e um juiz de rubrica; meça esse juiz contra os rótulos de uma pessoa antes que ele possa decidir qualquer coisa; depois compare uma mudança real. O juiz roda nesta máquina sem modelo e sem rede, e um passo opcional o troca por um modelo real.

O que você vai construir

Um resumidor que transforma um ticket de suporte em uma ou duas frases. "Bom" é um julgamento, não uma correspondência de strings, então o sucesso da tarefa é decidido por um juiz LLM com uma rubrica: o resumo traz os fatos de que um atendente precisa? Dois avaliadores determinísticos verificam o formato, o que não precisa de referência. Termos como caso, execução, métrica, juiz e gate são definidos em Conceitos.

A mesma forma serve para extração ou qualquer outra geração: uma função retorna texto em um dicionário, a referência diz o que uma boa resposta precisa conter, e uma rubrica diz como decidir.

Pré-requisitos

  • Python 3.11 ou posterior, e o Oloproof em um ambiente virtual:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • O projeto de exemplo, que vem com o pacote. Copie-o para um novo diretório e trabalhe nele:
oloproof init --example generation ticket-summaries
cd ticket-summaries
  • A porta 8799 livre para o juiz substituto (se não estiver, mude-a nos dois lugares).

Todo passo até "Opcional: um modelo real como juiz" é offline e determinístico: nenhuma chave de API, nenhuma conta de provedor, nenhum custo.

Os arquivos

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

Execute todos os comandos a partir de ticket-summaries/.

O juiz substituto, e o que ele não é

Um juiz de rubrica é um avaliador que envia um prompt (a rubrica, a entrada do caso, o seu expected e a saída) a um modelo e lê de volta {"pass": true|false, "rationale": "..."}. O Oloproof conversa com qualquer servidor que fale a API de chat da OpenAI, e um servidor em localhost não precisa de chave.

judge_server.py é um servidor desse tipo, mas não é um modelo. Ele só aprova um resumo quando ele contém toda frase listada em must_mention no expected do caso, ignorando maiúsculas e minúsculas. É uma regra fixa, então o tutorial dá os mesmos números em qualquer máquina. Ele não consegue perceber um fato inventado, o que se pede a um juiz baseado em modelo de verdade. Inicie-o em um segundo terminal e deixe-o rodando:

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

A aplicação e o seu adaptador

# 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]}

O adaptador de uma aplicação Python é a função: ela recebe o input do caso e retorna um dicionário. Para o seu próprio gerador, chame dentro dela o seu modelo ou a sua cadeia e retorne o texto sob uma chave. O Oloproof a chama uma vez por caso e coloca a saída em cache com base no código-fonte da função e na version declarada; ele não gerencia o seu cliente de modelo, os prompts nem o estado. Liste os arquivos que a função lê, como um template de prompt, em system.code_paths.

O 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 é o que a função recebe. expected é a referência que o juiz lê: aqui uma lista de fatos que o resumo precisa trazer, não um resumo de referência completo, porque muitos resumos diferentes estão corretos. A saída para t01 é {"summary": "Hello."}.

Escolhendo os avaliadores

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érioAvaliadorPrecisa de expectedMede
format_validjson_schemanãoformato: um único campo de string não vazio
short_enoughregexnãoformato: no máximo 160 caracteres
covers_factsrubric_judgesimsucesso da tarefa, como a rubrica o define

Hello. passa nas duas verificações de formato. Só o juiz diz que é um resumo inútil. Um juiz também pode funcionar sem referência: uma rubrica como "PASS if the summary contains no greeting" lê apenas a entrada e a saída, e um caso sem expected ainda é julgado. O que ele não consegue fazer então é verificar fatos contra uma resposta em que você confia.

A rubrica:

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.

A política

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 é o padrão do engine, escrito aqui por extenso porque é o ponto deste tutorial: um juiz que ninguém comparou com pessoas não pode decidir uma regra.

Execute

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

As regras de formato passam. O juiz aprovou 9 de 20 resumos, mas a regra é INSUFFICIENT_EVIDENCE com o motivo evaluator_not_validated, e o gate bloqueia com saída 3. A regra não decidiu com base nos 45%: a taxa de erro de um juiz é desconhecida até ser medida, então um intervalo construído sobre os seus veredictos carregaria um erro não declarado. O engine informa isso como INSUFFICIENT_EVIDENCE, não como MANUAL_REVIEW ou FAIL: falta a evidência para decidir, e a saída imprime os dois comandos que a fornecem.

Inspecione as falhas

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

A justificativa do juiz aparece como "judge text, not verified": é a explicação do modelo, não evidência. O padrão fica claro de qualquer forma: a primeira frase muitas vezes é uma saudação.

Meça o juiz contra uma pessoa

A validação compara os veredictos do juiz com os de uma pessoa sobre as mesmas respostas. Sorteie uma amostra aleatória dos casos da execução em uma planilha. Os veredictos do juiz ficam de fora dela, para que quem rotula não seja ancorado neles:

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 sorteia nesta máquina sem pedir a um espaço de trabalho hospedado; o engine escolhe a seed. Com 20 casos, uma amostra de 20 é todos eles. Na prática, uma pessoa lê o ticket e o resumo de cada linha e preenche passed. Para este tutorial, labels/reviewer_verdicts.csv contém os veredictos que um revisor deu sobre os resumos da linha de base, e fill_labels.py os copia para a planilha:

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

O revisor discordou do juiz uma vez: em t02 ("I was charged twice for order 2210.") considerou o valor ausente irrelevante e aprovou. Os rótulos nomeiam a resposta exata que julgaram, então esses veredictos valem apenas para a execução da linha de base.

Encontre o id da versão do juiz e valide-o:

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

Leia os intervalos, não os 95%: 20 rótulos mostram concordância de pelo menos 75,1%. Uma política pode exigir mais com minimum_evaluator_agreement, que compara esse limite inferior, e validate recusa um juiz abaixo dele. O guia Juízes trata do padrão, do viés, das sondas e de oloproof review para rotular no terminal.

Agora decida de novo a execução armazenada sem chamar o resumidor nem o juiz:

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)

O juiz agora pode decidir, e a decisão é sobre o resumidor: a taxa que ele cita, 0.500, não são os 45% do juiz. Como esta execução tem uma amostra cega e aleatória de rótulos de medição, o gate lê o juiz corrigido por esses rótulos ("Judge-corrected gates" no guia Juízes). A correção é PPI, prediction-powered inference: ela usa a amostra rotulada para medir o quanto a taxa do juiz se afasta da das pessoas, e desloca a estimativa e alarga o intervalo nessa mesma medida. É também a isso que se referem as notas da exportação sobre PPI. De qualquer forma, a linha de base não atinge o piso, e mais casos não mudariam isso.

Faça uma mudança real

app_v2.py pula as amenidades curtas e mantém as duas frases seguintes. Copie-o por cima de app.py, defina version: skip-pleasantries em system no oloproof.yaml, mantenha o juiz rodando, e:

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

O juiz é a mesma versão validada, então a sua regra decide diretamente. Seis julgamentos vieram do cache, sobre resumos que as duas versões escreveram de forma idêntica. Ninguém rotulou esses novos resumos; é a validação do juiz que permite que os seus veredictos valham.

Compare o candidato com a linha de base

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)

Uma comparação não aplica a correção PPI: ela compara os próprios veredictos do juiz nas duas execuções, e é por isso que o ganho parte dos 45% do juiz e não do 0.500 corrigido acima. Onze resumos melhoraram e nenhum piorou; o intervalo do ganho fica inteiramente acima de zero, então a regra de superioridade passa e o comando sai com 0. O formato é protegido pelas regras de execução, que não permitem nenhuma falha, e não por uma comparação: sobre 20 casos, uma comparação de duas pontuações de formato perfeitas só poderia dizer que a diferença está dentro de 23,6 pontos.

Opcional: um modelo real como juiz

Este passo sai do caminho offline. Ele precisa de um servidor de modelos e, com um provedor de nuvem, de uma chave e de dinheiro.

  • Local, sem chave e sem custo: Ollama, LM Studio ou llama.cpp em localhost. Baixe um modelo de chat (no Ollama, ollama pull llama3.1).
  • Nuvem: provider: anthropic ou openai com api_key_env nomeando a variável que contém a sua chave, ou openai_compatible com base_url e api_key_env. Cada caso é uma chamada ao juiz (duas quando a primeira resposta não é JSON válido), cobrada pelas tarifas do seu provedor, e o Oloproof nunca chama um juiz de novo para uma resposta que ele já julgou.

Escreva o rascunho do juiz em um arquivo próprio, como ele apareceria em 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

e teste-o contra as respostas que o seu revisor já rotulou, sem validá-lo nem adotá-lo:

oloproof evaluators try live_judge.yaml

Servidores locais respondem a uma requisição por vez por padrão; acrescente concurrency: {system: 2, judge: 2} ao oloproof.yaml para que as chamadas na fila não deem timeout. Uma execução deste passo com um modelo local pequeno (qwen2.5vl) em um notebook imprimiu:

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 chamadas deram timeout, e o intervalo de concordância conta cada uma nos dois sentidos, então ele desce até 19,1%: um juiz que não responde não é medido. A correção é um modelo maior, um timeout mais longo ou menos chamadas simultâneas. Para adotar o modelo, coloque-o em oloproof.yaml no lugar do substituto. Essa é uma nova versão do avaliador: a sua configuração (modelo, endpoint, rubrica) é a sua identidade, então a validação do substituto não é herdada. Execute a linha de base de novo com ele e valide-o contra os rótulos, como acima.

Solução de problemas

SintomaCausa e correção
covers_facts todo faltante, no_observationsO servidor do juiz não está rodando ou não está em base_url. Toda chamada ao juiz deu erro; oloproof inspect RUN_ID --failures mostra por quê.
evaluator_not_validated depois de validarVocê mudou o juiz (modelo, endpoint, porta, rubrica) e criou uma nova versão. Valide essa.
labels import recusa o arquivo e nomeia uma linhaA linha nomeia um caso ou uma execução que a execução não contém; exporte de novo a partir da execução que você rotula.
labels export diz que não foi possível acessar um espaço de trabalhoVocê está conectado a um, então ele pediu a ele que sorteasse. --local sorteia aqui.
Um juiz de nuvem falha antes de qualquer chamadaA chave dele não está na variável que api_key_env nomeia.

Limitações

  • O juiz substituto é uma correspondência de frases. Ele demonstra o fluxo de trabalho, não a qualidade do julgamento.
  • Não há avaliadores BLEU, ROUGE ou de similaridade de embeddings. No SDK, escreva um com @evaluator; oloproof.yaml ainda não pode nomear um avaliador personalizado.
  • Um juiz vê texto: o JSON da entrada, da referência e da saída. Ele não vê imagens nem áudio.
  • Vinte rótulos dão um intervalo de concordância largo. Rotule mais, ao acaso e às cegas, para um juiz em que você confia.
  • Uma amostra local vale apenas como boa-fé. Para um juiz em que outras pessoas confiam, faça push da execução e deixe um espaço de trabalho hospedado sortear a amostra (Juízes).