Saltar al contenido

Guías

Tutorial: un clasificador o una salida estructurada

Evalúe una función de Python que etiqueta preguntas de soporte, lea por qué se bloquea la publicación, corrija los errores y compare la corrección con el original, todo en su propia máquina, sin cuenta, sin red y sin modelo.

Lo que va a construir

Un bot de soporte que devuelve un objeto JSON con un answer y un label (refund, account u other). Lo someterá a tres requisitos: la etiqueta acierta con suficiente frecuencia, la salida siempre tiene la forma correcta, y ninguna respuesta filtra nada que parezca un número de la seguridad social de EE. UU. Dos de ellos son comprobaciones de formato que no necesitan respuesta de referencia; uno mide el éxito de la tarea frente a una etiqueta de referencia. La diferencia importa, y esta página los mantiene separados.

Los términos usados abajo (caso, ejecución, métrica, intervalo, regla, gate) se definen en Conceptos.

Requisitos previos

  • Python 3.11 o posterior.
  • Oloproof, instalado en un entorno virtual:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • El proyecto de ejemplo y el cambio candidato, que vienen con el paquete. Copie ambos en directorios nuevos y trabaje en el primero; cada archivo también aparece abajo, así que puede escribirlos a mano:
oloproof init --example support_bot support-classifier
oloproof init --example classification support-change
cd support-classifier

En esta página no se usa ninguna clave de API, cuenta de proveedor ni acceso a la red.

Los archivos

support-classifier/
  app.py              the application under test (a Python callable)
  oloproof.yaml       the suite: dataset, system, evaluators
  release.yaml        the release policy: rules the run is decided against
  data/support.jsonl  18 cases, one JSON object per line
  rubrics/helpful.md  a judge rubric, unused here

Ejecute cada comando desde el directorio support-classifier/. Oloproof guarda allí su almacén en .oloproof/; borre ese directorio para empezar de cero.

La aplicación y su adaptador

Se llega a su aplicación a través de un adaptador. Para una aplicación de Python, el adaptador es la propia función: Oloproof la importa, la llama una vez por caso con el input del caso y registra el diccionario que devuelve como salida de ese caso.

# app.py
from typing import Any

from oloproof import system


@system(name="support-bot", version="slice-a-example")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if "refund" in question:
        return {"answer": "Refunds are available within 30 days when the order is eligible.",
                "label": "refund"}
    if "password" in question or "login" in question:
        return {"answer": "Use password reset, then contact support if the login still fails.",
                "label": "account"}
    return {"answer": "A support specialist will follow up with the next step.", "label": "other"}

Para evaluar su propio clasificador, deje su código donde está y escriba una función delgada como esta que lo llame y devuelva un diccionario. La función puede ser async. Oloproof la llama; no aloja, aísla ni reinicia su aplicación, así que cualquier estado que su aplicación conserve entre llamadas lo gestiona usted.

oloproof.yaml nombra esa función y los evaluadores:

version: 1
project: support-bot-example
dataset: data/support.jsonl
system:
  name: support-bot
  version: slice-a-example
  callable: app:answer
  timeout_s: 30
evaluators:
  - type: exact_match
    criterion: exact_label
    field: label
  - type: json_schema
    criterion: format_valid
    field: null
    schema:
      type: object
      required: [answer, label]
      properties:
        answer: {type: string}
        label: {type: string}
      additionalProperties: false
  - type: regex
    criterion: pii_free
    field: answer
    pattern: '\b\d{3}-\d{2}-\d{4}\b'
    pass_if: no_match

Las salidas se almacenan en caché según el código fuente de la función, la version declarada y config. Si la función lee otros archivos (un prompt, una tabla de reglas), enumérelos en system.code_paths, para que editarlos vuelva a ejecutar el sistema.

El conjunto de datos

Un caso por línea. input es exactamente lo que su función recibe como case; expected es la referencia con la que compara el evaluador exact_match:

{"id":"refund_00","input":{"question":"Can I get a refund for yesterday's order?"},"expected":{"label":"refund"}}
{"id":"account_04","input":{"question":"I can't sign in on my new phone."},"expected":{"label":"account"}}
{"id":"other_04","input":{"question":"I don't want a refund, I just need a copy of my receipt."},"expected":{"label":"other"}}

La función devuelve, para cada caso, un objeto como {"answer": "Use password reset, ...", "label": "account"}.

Elegir los evaluadores

CriterioEvaluadorNecesita expectedQué mide
exact_labelexact_match sobre labelsíéxito de la tarea: la etiqueta es la correcta
format_validjson_schema sobre toda la salidanoformato: el objeto tiene exactamente los dos campos de texto
pii_freeregex sobre answer, pass_if: no_matchnouna propiedad de seguridad del texto

Una comprobación de formato aprueba una respuesta incorrecta bien formada, así que nunca puede sustituir al éxito de la tarea. Una comprobación de tarea necesita una referencia para cada caso; donde un caso no la tiene, exact_match no puede puntuarlo. Los evaluadores deterministas no necesitan validación frente a personas: ejecutar uno dos veces da el mismo veredicto.

La política

release.yaml es aquello contra lo que se decide la ejecución:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
warn_on: []
rules:
  - id: exact-label-floor
    metric: exact_label
    min: 0.70
  - id: valid-format
    metric: format_valid
    kind: observed_count
    max_failures: 0
  - id: pii-free
    metric: pii_free
    kind: observed_count
    max_failures: 0

exact-label-floor dice que la etiqueta debe acertar al menos el 70% de las veces, y solo pasa cuando todo el intervalo al 95% está en 0.70 o por encima. Las dos reglas observed_count no permiten ningún fallo en los casos que ejecutó; describen estos casos, no todas las preguntas que harán los usuarios.

Ejecútelo

oloproof run

Salida real, recortada:

Run run_01M4... [DECIDED/COMPLETE]
Gate: BLOCK (exit 3)
│ exact-label-floor │ exact_label  │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ valid-format      │ format_valid │ PASS                  │ observed_failures_within_limit │
│ pii-free          │ pii_free     │ PASS                  │ observed_failures_within_limit │
│ exact_label  │ 72.2%    │ [46.5%, 90.4%]  │ 13 / 18 observed · 0 missing · 0 excluded │
│ format_valid │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
│ pii_free     │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/18 miss; judgment 0 hit/54 miss

Cómo leerla:

  • 13 de 18 etiquetas son correctas, un 72.2%. Eso está por encima de 0.70, pero el intervalo baja hasta el 46.5%: 18 casos no pueden demostrar que la tasa real sea al menos 0.70. Así que la regla queda en INSUFFICIENT_EVIDENCE, ni PASS ni FAIL.
  • Todas las salidas tienen la forma correcta y ninguna contiene un número parecido a un SSN, así que ambas reglas de formato pasan.
  • block_on incluye INSUFFICIENT_EVIDENCE, así que el gate bloquea y el comando termina con 3. Terminar con 0 significaría que no ocurrió nada sobre lo que la política bloquea; Gates en CI enumera todos los códigos.

Vuelva a ejecutarlo y la línea de caché dice execution 18 hit/0 miss: nada cambió, así que no se llama a la función.

Inspeccionar los fallos

oloproof inspect RUN_ID --failures
oloproof inspect RUN_ID --case refund_04

RUN_ID es el identificador de la primera línea de la salida de la ejecución.

5 of 18 cases failed, errored or did not finish

refund_04
  output: {"answer": "A support specialist will follow up with the next step.", "label": "other"}
  exact_label: failed
...
other_04
  output: {"answer": "Refunds are available within 30 days when the order is eligible.", "label": "refund"}
  exact_label: failed
case refund_04
input: {
  "question": "I was charged twice this month and want my money back."
}
expected: {
  "label": "refund"
}
execution: OK, 1 ms
output: {
  "answer": "A support specialist will follow up with the next step.",
  "label": "other"
}
judgments:
  exact_label: failed
  format_valid: passed
  pii_free: passed

El patrón es evidente en cuanto se leen las entradas: "money back", "reverse the payment", "sign in" y "two-factor" no están en las listas de palabras clave, y other_04 dice "I don't want a refund", que la palabra "refund" captura de todos modos. Observe que refund_04 pasa ambas comprobaciones de formato aun siendo incorrecto: esa es la distancia entre comprobar el formato y medir el éxito.

Aquí tienen sentido dos acciones siguientes. Corregir los errores (abajo), o añadir casos: con más casos y la misma precisión el intervalo se estrecha, y oloproof plan RUN_ID --run estima cuántos.

Hacer un cambio real

Copie ../support-change/app.py sobre app.py. Añade las formulaciones que faltaban:

REFUND_WORDS = ("refund", "money back", "reverse the payment")
ACCOUNT_WORDS = ("password", "login", "sign in", "two-factor")


@system(name="support-bot", version="keywords-v2")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if any(word in question for word in REFUND_WORDS):
        ...

y ponga version: keywords-v2 bajo system en oloproof.yaml, para que la ejecución se registre como la nueva versión. Después:

oloproof run
Gate: ALLOW (exit 0)
│ exact-label-floor │ exact_label  │ PASS  │ lower_bound_meets_minimum      │
│ exact_label  │ 94.4%    │ [72.7%, 99.9%]  │ 17 / 18 observed · 0 missing · 0 excluded │

17 de 18 son correctos y el límite inferior del intervalo, 72.7%, supera 0.70, así que la regla pasa y el comando termina con 0. other_04 sigue fallando: la corrección no tocó la negación.

Comparar el candidato con la línea base

La regla de ejecución pregunta si el candidato alcanza su mínimo. Una comparación pregunta en qué se diferencia de la línea base, caso por caso. Copie ../support-change/compare.yaml en el proyecto; contiene una regla de comparación:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - id: label-no-regression
    kind: non_inferiority
    metric: exact_label
    margin: 0.10
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
Comparison sha256:de76... of run_01M4...TJAD against run_01M4...ECVEF · 18 paired cases
exact_label: +22.2 points [-12.9, +57.0] · 18 paired · 0 missing · 0 excluded
format_valid: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
pii_free: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
Decisions
  label-no-regression  exact_label  non-inferiority, margin 10.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
    about 3 more paired cases would decide it, if the difference holds (21 in total at 22% discordance)
Gate: BLOCK (exit 3)

El candidato corrigió cuatro casos y no rompió ninguno, una ganancia estimada de 22 puntos. Pero solo cambiaron cuatro casos, y 18 casos emparejados dejan un intervalo que va de 12.9 puntos peor a 57 puntos mejor, que cruza el margen de 10 puntos. La comparación aún no puede descartar que el candidato sea peor en más de lo que usted acepta, así que queda en INSUFFICIENT_EVIDENCE y termina con 3. La línea de debajo es la estimación del tamaño necesario. Sin --policy, compare imprime las diferencias, indica que el release.yaml del proyecto no declara ninguna regla de comparación y termina con 0 porque no se decidió nada.

Comparar un candidato con una línea base explica el margen y los demás tipos de regla.

Solución de problemas

SíntomaCausa y solución
ModuleNotFoundError para appEjecute desde el directorio que contiene app.py, o dé a callable una ruta de módulo importable desde allí.
Una regla nombra una métrica que ningún evaluador produceEl metric de la regla debe ser igual al criterion de un evaluador; el error enumera las métricas que existen.
Editó el clasificador y la ejecución reutilizó todas las salidasLa caché sigue el código fuente del callable; un archivo auxiliar que lea debe figurar en system.code_paths.
exact_label informa de casos que faltanEsas ejecuciones lanzaron una excepción o agotaron el tiempo; oloproof inspect RUN_ID --failures muestra cada error.
La ejecución termina con 3 con una estimación altaDecide el intervalo, no la estimación. Añada casos o acepte un mínimo más bajo, decidido antes de la ejecución.

Limitaciones

  • El SDK y el YAML informan de tasas de aprobados por evaluador. No hay matriz de confusión ni precisión y exhaustividad por clase para un clasificador como este; un bloque predictive: las calcula para un modelo con puntuaciones (Modelos predictivos).
  • Las reglas observed_count describen los casos que ejecutó; no afirman nada sobre entradas no vistas.
  • Una comparación sobre 18 casos solo resuelve diferencias grandes. Cincuenta o más casos reales son un mínimo más útil.
  • oloproof.yaml no puede nombrar un @evaluator personalizado; para eso hace falta el SDK (El SDK).