Guías
Tutorial: generación de texto con un juez de rúbrica
Evalúe una función que escribe texto libre, aquí un resumidor de tickets, con comprobaciones de formato y un juez de rúbrica; mida ese juez frente a las etiquetas de una persona antes de que pueda decidir nada; después compare un cambio real. El juez se ejecuta en esta máquina sin modelo y sin red, y un paso opcional lo sustituye por un modelo real.
Lo que va a construir
Un resumidor que convierte un ticket de soporte en una o dos frases. "Bueno" es un juicio, no una coincidencia de cadenas, así que el éxito de la tarea lo decide un juez LLM con una rúbrica: ¿el resumen indica los hechos que necesita un agente? Dos evaluadores deterministas comprueban el formato, que no necesita referencia. Términos como caso, ejecución, métrica, juez y gate se definen en Conceptos.
La misma forma sirve para extracción o cualquier otra generación: una función devuelve texto en un diccionario, la referencia dice lo que debe contener una buena respuesta, y una rúbrica dice cómo decidir.
Requisitos previos
- Python 3.11 o posterior, y Oloproof en un entorno virtual:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof- El proyecto de ejemplo, que viene con el paquete. Cópielo en un directorio nuevo y trabaje allí:
oloproof init --example generation ticket-summaries
cd ticket-summaries- El puerto 8799 libre para el juez sustituto (si no, cámbielo en los dos sitios).
Todos los pasos hasta "Opcional: un modelo real como juez" son sin conexión y deterministas: sin clave de API, sin cuenta de proveedor, sin coste.
Los archivos
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 sheetEjecute cada comando desde ticket-summaries/.
El juez sustituto, y lo que no es
Un juez de rúbrica es un evaluador que envía un prompt (la rúbrica, la entrada del caso, su expected y la salida) a un modelo y lee de vuelta {"pass": true|false, "rationale": "..."}. Oloproof habla con cualquier servidor que use la API de chat de OpenAI, y un servidor en localhost no necesita clave.
judge_server.py es un servidor así, pero no es un modelo. Aprueba un resumen solo cuando contiene cada frase de must_mention en el expected del caso, sin distinguir mayúsculas. Es una regla fija, así que el tutorial da los mismos números en todas las máquinas. No puede detectar un hecho inventado, algo que sí se le pide a un juez con un modelo real. Inícielo en un segundo terminal y déjelo en marcha:
python judge_server.py --port 8799stand-in judge on http://127.0.0.1:8799/v1La aplicación y su 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]}El adaptador de una aplicación de Python es la función: recibe el input del caso y devuelve un diccionario. Para su propio generador, llame dentro a su modelo o cadena y devuelva el texto bajo una clave. Oloproof la llama una vez por caso y almacena la salida en caché según el código fuente de la función y la version declarada; no gestiona su cliente de modelo, sus prompts ni su estado. Enumere los archivos que lee la función, como una plantilla de prompt, en system.code_paths.
El conjunto de datos
{"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 es lo que recibe la función. expected es la referencia que lee el juez: aquí una lista de hechos que el resumen debe recoger, no un resumen de referencia completo, porque muchos resúmenes distintos son correctos. La salida para t01 es {"summary": "Hello."}.
Elegir los evaluadores
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| Criterio | Evaluador | Necesita expected | Mide |
|---|---|---|---|
| format_valid | json_schema | no | formato: un campo de texto no vacío |
| short_enough | regex | no | formato: como mucho 160 caracteres |
| covers_facts | rubric_judge | sí | éxito de la tarea, tal como lo define la rúbrica |
Hello. pasa ambas comprobaciones de formato. Solo el juez dice que es un resumen inútil. Un juez también puede ejecutarse sin referencia: una rúbrica como "PASS if the summary contains no greeting" solo lee la entrada y la salida, y un caso sin expected se sigue juzgando. Lo que entonces no puede hacer es comprobar hechos frente a una respuesta en la que usted confía.
La rúbrica:
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.La 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 es el valor por defecto del motor, escrito aquí porque es el objetivo de este tutorial: un juez que nadie ha comparado con personas no puede decidir una regla.
Ejecútelo
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 missLas reglas de formato pasan. El juez aprobó 9 de 20 resúmenes, pero la regla queda en INSUFFICIENT_EVIDENCE con el motivo evaluator_not_validated, y el gate bloquea con salida 3. La regla no decidió sobre el 45%: la tasa de error de un juez es desconocida hasta que se mide, así que un intervalo construido sobre sus veredictos llevaría un error no declarado. El motor lo informa como INSUFFICIENT_EVIDENCE, no como MANUAL_REVIEW ni FAIL: falta la evidencia para decidir, y la salida imprime los dos comandos que la aportan.
Inspeccionar los fallos
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
...La justificación del juez se muestra como "judge text, not verified": es la explicación del modelo, no evidencia. El patrón está claro de todos modos: la primera frase es a menudo un saludo.
Medir el juez frente a una persona
La validación compara los veredictos del juez con los de una persona sobre las mismas respuestas. Extraiga una muestra aleatoria de los casos de la ejecución en una hoja. Los veredictos del juez quedan fuera de ella, para que quien etiqueta no se ancle en ellos:
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 extrae la muestra en esta máquina sin pedírsela a un espacio de trabajo alojado; el motor elige la semilla. Con 20 casos, una muestra de 20 los incluye todos. En la práctica, una persona lee el ticket y el resumen de cada fila y rellena passed. Para este tutorial, labels/reviewer_verdicts.csv contiene los veredictos que un revisor dio sobre los resúmenes de la línea base, y fill_labels.py los copia en la hoja:
python fill_labels.py sample.csv
oloproof labels import sample.csvfilled 20 rows of sample.csv
Recorded 20 labels from sample.csv (20 measurement).El revisor discrepó del juez una vez: en t02 ("I was charged twice for order 2210.") juzgó irrelevante el importe que faltaba y lo aprobó. Las etiquetas nombran la respuesta exacta que juzgaron, así que estos veredictos solo se aplican a la ejecución de la línea base.
Busque el identificador de versión del juez y valídelo:
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 judgedLea los intervalos, no el 95%: 20 etiquetas muestran una concordancia de al menos el 75.1%. Una política puede exigir más con minimum_evaluator_agreement, que compara ese límite inferior, y validate rechaza un juez por debajo de él. La guía de Jueces trata el listón, el sesgo, las sondas y oloproof review para etiquetar en el terminal.
Ahora vuelva a decidir la ejecución almacenada sin llamar al resumidor ni al juez:
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)El juez ya puede decidir, y la decisión trata sobre el resumidor: la tasa que cita, 0.500, no es el 45% del juez. Como esta ejecución tiene una muestra ciega y aleatoria de etiquetas de medición, el gate lee el juez corregido por esas etiquetas ("Judge-corrected gates" en la guía de Jueces). La corrección es PPI, inferencia potenciada por predicción: usa la muestra etiquetada para medir cuánto se aleja la tasa del juez de la de las personas, y desplaza la estimación y ensancha el intervalo en esa medida. A eso se refieren también las notas sobre PPI de la exportación. En cualquier caso, la línea base no alcanza el mínimo, y más casos no lo cambiarían.
Hacer un cambio real
app_v2.py omite las cortesías breves y conserva las dos frases siguientes. Cópielo sobre app.py, ponga version: skip-pleasantries bajo system en oloproof.yaml, mantenga el juez en marcha y:
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 missEl juez es la misma versión validada, así que su regla decide directamente. Seis juicios vinieron de la caché, sobre resúmenes que ambas versiones escribieron de forma idéntica. Nadie etiquetó estos nuevos resúmenes; la validación del juez es lo que permite que sus veredictos se mantengan.
Comparar el candidato con la línea 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)Una comparación no aplica la corrección PPI: compara los propios veredictos del juez en las dos ejecuciones, y por eso la ganancia parte del 45% del juez y no del 0.500 corregido de arriba. Once resúmenes mejoraron y ninguno empeoró; el intervalo de la ganancia queda por completo por encima de cero, así que la regla de superioridad pasa y el comando termina con 0. El formato lo protegen las reglas de ejecución, que no permiten ningún fallo, y no una comparación: con 20 casos, una comparación de dos puntuaciones de formato perfectas solo podría decir que la diferencia está dentro de 23.6 puntos.
Opcional: un modelo real como juez
Este paso abandona el camino sin conexión. Necesita un servidor de modelos y, con un proveedor en la nube, una clave y dinero.
- Local, sin clave y sin coste: Ollama, LM Studio o llama.cpp en localhost. Descargue un modelo de chat (para Ollama, ollama pull llama3.1).
- En la nube: provider: anthropic u openai con api_key_env nombrando la variable que contiene su clave, u openai_compatible con base_url y api_key_env. Cada caso es una llamada al juez (dos cuando la primera respuesta no es JSON válido), facturada a las tarifas de su proveedor, y Oloproof nunca vuelve a llamar a un juez para una respuesta que ya ha juzgado.
Escriba el juez en borrador en un archivo propio, tal como aparecería bajo 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.mdy pruébelo con las respuestas que su revisor ya etiquetó, sin validarlo ni adoptarlo:
oloproof evaluators try live_judge.yamlLos servidores locales responden a una petición cada vez por defecto; añada concurrency: {system: 2, judge: 2} a oloproof.yaml para que las llamadas en cola no agoten el tiempo. Una ejecución de este paso con un modelo local pequeño (qwen2.5vl) en un portátil imprimió:
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.769Once llamadas agotaron el tiempo, y el intervalo de concordancia cuenta cada una en ambos sentidos, así que baja hasta el 19.1%: un juez que no responde no queda medido. La solución es un modelo más grande, un tiempo de espera más largo o menos llamadas simultáneas. Para adoptar el modelo, póngalo en oloproof.yaml en lugar del sustituto. Es una nueva versión del evaluador: su configuración (modelo, endpoint, rúbrica) es su identidad, así que la validación del sustituto no se traslada. Vuelva a ejecutar la línea base con él y valídelo frente a las etiquetas, como arriba.
Solución de problemas
| Síntoma | Causa y solución |
|---|---|
| covers_facts falta en todos los casos, no_observations | El servidor del juez no está en marcha o no está en base_url. Todas las llamadas al juez fallaron; oloproof inspect RUN_ID --failures muestra por qué. |
| evaluator_not_validated después de validar | Cambió el juez (modelo, endpoint, puerto, rúbrica) y creó una nueva versión. Valide esa. |
| labels import rechaza el archivo y nombra una fila | La fila nombra un caso o una ejecución que la ejecución no contiene; vuelva a exportar desde la ejecución que etiqueta. |
| labels export dice que no se pudo contactar con un espacio de trabajo | Ha iniciado sesión en uno, así que le pidió que extrajera la muestra. --local la extrae aquí. |
| Un juez en la nube falla antes de cualquier llamada | Su clave no está en la variable que nombra api_key_env. |
Limitaciones
- El juez sustituto es una coincidencia de frases. Demuestra el flujo de trabajo, no la calidad del juicio.
- No hay evaluadores BLEU, ROUGE ni de similitud de embeddings. En el SDK, escriba uno con @evaluator; oloproof.yaml aún no puede nombrar un evaluador personalizado.
- Un juez ve texto: JSON de la entrada, la referencia y la salida. No ve imágenes ni audio.
- Veinte etiquetas dan un intervalo de concordancia ancho. Etiquete más, al azar y a ciegas, para un juez del que dependa.
- Una muestra local es solo de buena fe. Para un juez del que dependen otras personas, envíe la ejecución y deje que un espacio de trabajo alojado extraiga la muestra (Jueces).