Saltar al contenido

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 sheet

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

La 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
CriterioEvaluadorNecesita expectedMide
format_validjson_schemanoformato: un campo de texto no vacío
short_enoughregexnoformato: como mucho 160 caracteres
covers_factsrubric_judgesíé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.60

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

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

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

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

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

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

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

y pruébelo con las respuestas que su revisor ya etiquetó, sin validarlo ni adoptarlo:

oloproof evaluators try live_judge.yaml

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

Once 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íntomaCausa y solución
covers_facts falta en todos los casos, no_observationsEl 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 validarCambió el juez (modelo, endpoint, puerto, rúbrica) y creó una nueva versión. Valide esa.
labels import rechaza el archivo y nombra una filaLa 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 trabajoHa 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 llamadaSu 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).