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 sheetExecute 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 8799stand-in judge on http://127.0.0.1:8799/v1A 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ério | Avaliador | Precisa de expected | Mede |
|---|---|---|---|
| format_valid | json_schema | não | formato: um único campo de string não vazio |
| short_enough | regex | não | formato: no máximo 160 caracteres |
| covers_facts | rubric_judge | sim | sucesso 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.60require_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 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 missAs 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 --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
...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.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 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.csvfilled 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 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 judgedLeia 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.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)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 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 missO 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_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)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.mde teste-o contra as respostas que o seu revisor já rotulou, sem validá-lo nem adotá-lo:
oloproof evaluators try live_judge.yamlServidores 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.769Onze 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
| Sintoma | Causa e correção |
|---|---|
| covers_facts todo faltante, no_observations | O 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 validar | Você mudou o juiz (modelo, endpoint, porta, rubrica) e criou uma nova versão. Valide essa. |
| labels import recusa o arquivo e nomeia uma linha | A 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 trabalho | Você está conectado a um, então ele pediu a ele que sorteasse. --local sorteia aqui. |
| Um juiz de nuvem falha antes de qualquer chamada | A 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).