Lección 226 · 30 min · Gratis

Guía práctica para construir sistemas autónomos con evals

Descripción general

Propósito de este manual

Este manual proporciona una guía práctica de principio a fin sobre cómo usar eficazmente los evals como proceso central para crear un sistema autónomo de grado de producción que reemplace un flujo de trabajo humano intensivo en mano de obra. Es un producto directo de la experiencia colaborativa en proyectos donde los usuarios pueden no haber comenzado con datos etiquetados impecables o una comprensión perfecta del problema, dos problemas que la mayoría de los tutoriales pasan por alto, pero que en la práctica son casi siempre desafíos serios.

Hacer de los evals el proceso central evita las conjeturas al azar y los juicios impresionistas de precisión, exigiendo en cambio rigor de ingeniería. Esto significa que podemos tomar decisiones basadas en principios sobre las compensaciones de costos y la inversión.

Público objetivo

Esta guía está diseñada para ingenieros de ML/IA y arquitectos de soluciones que buscan orientación práctica más allá de los tutoriales introductorios. Este notebook es completamente ejecutable y está organizado para ser lo más modular posible para admitir el uso de ejemplos de código directamente en tus propias aplicaciones.

Narrativa guía: De una pequeña semilla a un sistema de producción

Seguiremos una historia realista: reemplazar un servicio manual de análisis de recibos para validar gastos.

  • Comienza pequeño: Comienza con un conjunto muy pequeño de datos etiquetados (recibos de tiendas minoristas). Muchas empresas no tienen buenos conjuntos de datos de verdad fundamental.
  • Construye incrementalmente: Desarrolla un sistema viable mínimo y establece evals iniciales.
  • Alineación con el negocio: Evalúa el rendimiento de los evals en el contexto de los KPI del negocio y el impacto en dólares, y dirige los esfuerzos para evitar trabajar en mejoras de bajo impacto.
  • Iteración impulsada por evals: Mejora iterativamente usando las puntuaciones de los evals para impulsar las mejoras del modelo, luego usando mejores modelos en más datos para expandir los evals e identificar más áreas de mejora.

Cómo usar este manual

Este manual está estructurado como una guía centrada en evals a través del ciclo de vida de la construcción de una aplicación LLM.

  1. Si estás principalmente interesado en las ideas presentadas, lee el texto y revisa el código.
  2. Si estás aquí por algo más en lo que estás trabajando, puedes ir directamente a esa sección y profundizar en el código allí, copiarlo y adaptarlo a tus necesidades.
  3. Si quieres entender realmente cómo funciona todo esto, descarga este notebook y ejecuta las celdas a medida que lo lees; edita el código para hacer tus propios cambios, prueba tus hipótesis y asegúrate de que realmente entiendes cómo funciona todo junto.

Nota: Si tu organización de OpenAI tiene una política de Retención de Datos Cero (ZDR), los Evals seguirán estando disponibles, pero retendrán datos para mantener el estado de la aplicación.

Caso de uso: Análisis de recibos

Para condensar esta guía, utilizaremos un pequeño problema hipotético que sigue siendo lo suficientemente complejo como para merecer evals detallados y multifacéticos. En particular, nos centraremos en cómo resolver un problema dada una cantidad limitada de datos con los que trabajar, por lo que estamos trabajando con un conjunto de datos bastante pequeño.

Definición del problema

Para esta guía, asumimos que estamos comenzando con un flujo de trabajo para revisar y archivar recibos. Si bien, en general, este es un problema que ya tiene muchas soluciones establecidas, es análogo a otros problemas que no tienen tanto trabajo previo; además, incluso cuando existen buenas soluciones empresariales, a menudo todavía hay un problema de "última milla" que aún requiere tiempo humano.

En nuestro caso, asumiremos que tenemos una pipeline donde:

  • Las personas suben fotos de recibos
  • Un equipo de contabilidad revisa cada recibo para categorizar y aprobar o auditar el gasto

Según entrevistas con el equipo de contabilidad, toman sus decisiones basándose en:

  1. Comerciante
  2. Ubicación geográfica
  3. Monto del gasto
  4. Artículos o servicios comprados
  5. Notas o anotaciones escritas a mano

Se espera que nuestro sistema maneje la mayoría de los recibos sin ninguna intervención humana, pero que escale las decisiones de baja confianza para el control de calidad humano. Nos centraremos en reducir el costo total del proceso contable, que depende de:

  1. Cuánto costó ejecutar el sistema anterior/actual por recibo
  2. Cuántos recibos envía el nuevo sistema al control de calidad
  3. Cuánto cuesta ejecutar el sistema por recibo, más los costos fijos
  4. Cuál es el impacto comercial de los errores, ya sean recibos rechazados para revisión o errores pasados por alto
  5. El costo de ingeniería para desarrollar e integrar el sistema

Descripción general del conjunto de datos

Las imágenes de recibos provienen del conjunto de datos Receipt Handwriting Detection Computer Vision Project con licencia CC by 4.0, publicado por Roboflow. Hemos añadido nuestras propias etiquetas y un giro narrativo para contar una historia con un pequeño número de ejemplos.

Ciclo de vida del proyecto

No todos los proyectos se desarrollarán de la misma manera, pero los proyectos generalmente tienen algunos componentes importantes en común.

Project Lifecycle

Las flechas sólidas muestran las progresiones o pasos principales, mientras que la línea punteada representa la naturaleza continua de la comprensión del problema: descubrir más sobre el dominio del cliente influirá en cada paso del proceso. Examinaremos varios de estos ciclos iterativos de refinamiento en detalle a continuación. No todos los proyectos se desarrollarán de la misma manera, pero los proyectos generalmente tienen algunos componentes importantes en común.

1. Comprender el problema

Por lo general, la decisión de iniciar un proceso de ingeniería la toma la dirección, que comprende el impacto comercial pero no necesita conocer los detalles del proceso. En nuestro ejemplo, estamos construyendo un sistema diseñado para reemplazar un flujo de trabajo no basado en IA. En cierto sentido, esto es ideal: tenemos un conjunto de expertos en el dominio, las personas que actualmente realizan la tarea, a quienes podemos entrevistar para comprender los detalles de la tarea y en quienes podemos apoyarnos para ayudar a desarrollar evals apropiados.

Este paso no termina antes de que comencemos a construir nuestro sistema; invariablemente, nuestras evaluaciones iniciales son una comprensión incompleta del espacio del problema y continuaremos refinando nuestra comprensión a medida que nos acerquemos a una solución.

2. Reunir ejemplos (recopilar datos)

Es muy raro que un proyecto del mundo real comience con todos los datos necesarios para lograr una solución satisfactoria, y mucho menos para establecer confianza.

En nuestro caso, asumiremos que tenemos una muestra decente de entradas del sistema, en forma de imágenes de recibos, pero comenzamos sin ningún dato completamente anotado. Encontramos que esta es una situación no inusual al automatizar un proceso existente. Recorreremos el proceso de expandir incrementalmente nuestros conjuntos de prueba y entrenamiento en colaboración con expertos en el dominio a medida que avanzamos y hacemos que nuestros evals sean progresivamente más completos.

3. Construir un sistema V0 de extremo a extremo

Queremos construir el esqueleto de un sistema lo más rápido posible. No necesitamos un sistema que funcione bien, solo necesitamos algo que acepte las entradas correctas y proporcione salidas del tipo correcto. Por lo general, esto es casi tan simple como describir la tarea en un prompt, agregar las entradas y usar un solo modelo (generalmente con salidas estructuradas) para hacer un intento inicial de mejor esfuerzo.

4. Etiquetar datos y construir Evals iniciales

Hemos descubierto que, en ausencia de una verdad fundamental establecida, no es raro utilizar una versión temprana de un sistema para generar datos de verdad "borrador" que pueden ser anotados o corregidos por expertos en el dominio.

Una vez que tenemos un sistema de extremo a extremo construido, podemos comenzar a procesar las entradas que tenemos para generar salidas plausibles. Las enviaremos a nuestros expertos en el dominio para que las califiquen y corrijan. Utilizaremos estas correcciones y conversaciones sobre cómo los expertos están tomando sus decisiones para diseñar más evals e incrustar la experiencia en el sistema.

5. Mapear Evals a métricas de negocio

Antes de lanzarnos a corregir cada error, debemos asegurarnos de que estamos invirtiendo el tiempo de manera efectiva. La tarea más crítica en esta etapa es revisar nuestros evals y comprender cómo se conectan con nuestros objetivos clave.

  • Da un paso atrás y evalúa los posibles costos y beneficios del sistema.
  • Identifica qué mediciones de eval se relacionan directamente con esos costos y beneficios.
  • Por ejemplo, ¿cuánto cuesta un "fallo" en un eval en particular? ¿Estamos midiendo algo que vale la pena?
  • Crea un modelo (no LLM) que utilice métricas de eval para proporcionar un valor en dólares.
  • Equilibra el rendimiento (precisión o velocidad) con el costo de desarrollo y ejecución.

6. Mejorar progresivamente el sistema y los Evals

Habiendo identificado qué esfuerzos vale la pena realizar, podemos comenzar a iterar sobre las mejoras del sistema. Los evals actúan como una guía objetiva para saber cuándo hemos hecho el sistema lo suficientemente bueno y para asegurar que evitamos o identificamos regresiones.

7. Integrar el proceso de control de calidad y las mejoras continuas

Los evals no son solo para el desarrollo. Instrumentar todo o una parte de un servicio de producción generará más muestras útiles de prueba y entrenamiento con el tiempo, identificando suposiciones incorrectas o encontrando áreas con cobertura insuficiente. Esta es también la única forma de asegurar que tus modelos sigan funcionando bien mucho después de que se complete tu proceso de desarrollo inicial.

Construcción del sistema V0

En la práctica, probablemente estaríamos construyendo un sistema que opera a través de una API REST, posiblemente con algún frontend web que tendría acceso a un conjunto de componentes y recursos. Para los propósitos de este manual, lo reduciremos a un par de funciones, extract_receipt_details y evaluate_receipt_for_audit que colectivamente deciden qué debemos hacer con un recibo dado.

  • extract_receipt_details tomará una imagen como entrada y producirá una salida estructurada que contendrá detalles importantes sobre el recibo.
  • evaluate_receipt_for_audit tomará esa estructura como entrada y decidirá si el recibo debe ser auditado o no.

Dividir un proceso en pasos como este tiene pros y contras; es más fácil de examinar y desarrollar si el proceso se compone de pequeños pasos aislados. Pero puedes perder información progresivamente, dejando que tus agentes jueguen al "teléfono". En este notebook dividimos los pasos y no dejamos que el auditor vea el recibo real porque es más instructivo para los evals que queremos discutir.

Comenzaremos con el primer paso, la extracción literal de datos. Estos son datos intermedios: es información que las personas examinarían implícitamente, pero que a menudo no se registra. Y por esta razón, a menudo no tenemos datos etiquetados con los que trabajar.

%pip install --upgrade openai pydantic python-dotenv rich persist-cache -qqq
%load_ext dotenv
%dotenv

# Place your API key in a file called .env
# OPENAI_API_KEY=sk-...

Modelo de salida estructurada

Captura la información significativa en una salida estructurada.

from pydantic import BaseModel


class Location(BaseModel):
    city: str | None
    state: str | None
    zipcode: str | None


class LineItem(BaseModel):
    description: str | None
    product_code: str | None
    category: str | None
    item_price: str | None
    sale_price: str | None
    quantity: str | None
    total: str | None


class ReceiptDetails(BaseModel):
    merchant: str | None
    location: Location
    time: str | None
    items: list[LineItem]
    subtotal: str | None
    tax: str | None
    total: str | None
    handwritten_notes: list[str]

Nota: Normalmente usaríamos objetos decimal.Decimal para los números anteriores y objetos datetime.datetime para el campo time, pero ninguno de ellos se deserializa bien. Para los propósitos de este manual, trabajaremos con cadenas, pero en la práctica querrías tener otro nivel de traducción para obtener la salida correcta validada.

Extracción de información básica

Construyamos nuestra función extract_receipt_details.

Por lo general, para el primer intento de algo que podría funcionar, simplemente alimentaremos a ChatGPT con los documentos disponibles que hemos reunido hasta ahora y le pediremos que genere un prompt. ¡No vale la pena dedicar demasiado tiempo a la ingeniería de prompts antes de tener un benchmark para evaluarte! Este es un prompt producido por o4-mini basado en la descripción del problema anterior.

BASIC_PROMPT = """
Given an image of a retail receipt, extract all relevant information and format it as a structured response.

# Task Description

Carefully examine the receipt image and identify the following key information:

1. Merchant name and any relevant store identification
2. Location information (city, state, ZIP code)
3. Date and time of purchase
4. All purchased items with their:
   * Item description/name
   * Item code/SKU (if present)
   * Category (infer from context if not explicit)
   * Regular price per item (if available)
   * Sale price per item (if discounted)
   * Quantity purchased
   * Total price for the line item
5. Financial summary:
   * Subtotal before tax
   * Tax amount
   * Final total
6. Any handwritten notes or annotations on the receipt (list each separately)

## Important Guidelines

* If information is unclear or missing, return null for that field
* Format dates as ISO format (YYYY-MM-DDTHH:MM:SS)
* Format all monetary values as decimal numbers
* Distinguish between printed text and handwritten notes
* Be precise with amounts and totals
* For ambiguous items, use your best judgment based on context

Your response should be structured and complete, capturing all available information
from the receipt.
"""
import base64
import mimetypes
from pathlib import Path

from openai import AsyncOpenAI

client = AsyncOpenAI()


async def extract_receipt_details(
    image_path: str, model: str = "o4-mini"
) -> ReceiptDetails:
    """Extract structured details from a receipt image."""
    # Determine image type for data URI.
    mime_type, _ = mimetypes.guess_type(image_path)

    # Read and base64 encode the image.
    b64_image = base64.b64encode(Path(image_path).read_bytes()).decode("utf-8")
    image_data_url = f"data:{mime_type};base64,{b64_image}"

    response = await client.responses.parse(
        model=model,
        input=[
            {
                "role": "user",
                "content": [
                    {"type": "input_text", "text": BASIC_PROMPT},
                    {"type": "input_image", "image_url": image_data_url},
                ],
            }
        ],
        text_format=ReceiptDetails,
    )

    return response.output_parsed

Prueba con un recibo

Evaluemos solo un recibo y revisémoslo manualmente para ver qué tan bien puede hacerlo un modelo inteligente con un prompt ingenuo.

Walmart_image
from rich import print

receipt_image_dir = Path("data/test")
ground_truth_dir = Path("data/ground_truth")

example_receipt = Path(
    "data/train/Supplies_20240322_220858_Raven_Scan_3_jpeg.rf.50852940734939c8838819d7795e1756.jpg"
)
result = await extract_receipt_details(example_receipt)

Obtendremos respuestas diferentes si lo volvemos a ejecutar, pero generalmente acierta la mayoría de las cosas con algunos errores. Aquí hay un ejemplo específico:

walmart_receipt = ReceiptDetails(
    merchant="Walmart",
    location=Location(city="Vista", state="CA", zipcode="92083"),
    time="2023-06-30T16:40:45",
    items=[
        LineItem(
            description="SPRAY 90",
            product_code="001920056201",
            category=None,
            item_price=None,
            sale_price=None,
            quantity="2",
            total="28.28",
        ),
        LineItem(
            description="LINT ROLLER 70",
            product_code="007098200355",
            category=None,
            item_price=None,
            sale_price=None,
            quantity="1",
            total="6.67",
        ),
        LineItem(
            description="SCRUBBER",
            product_code="003444193232",
            category=None,
            item_price=None,
            sale_price=None,
            quantity="2",
            total="12.70",
        ),
        LineItem(
            description="FLOUR SACK 10",
            product_code="003444194263",
            category=None,
            item_price=None,
            sale_price=None,
            quantity="1",
            total="0.77",
        ),
    ],
    subtotal="50.77",
    tax="4.19",
    total="54.96",
    handwritten_notes=[],
)

El modelo extrajo muchas cosas correctamente, pero renombró algunos de los elementos de línea, incorrectamente, de hecho. Más importante aún, se equivocó en algunos de los precios y decidió no categorizar ninguno de los elementos de línea.

Está bien, ¡no esperamos tener respuestas perfectas en este punto! En cambio, nuestro objetivo es construir un sistema básico que podamos evaluar. Luego, cuando comencemos a iterar, no estaremos "intuyendo" nuestro camino hacia algo que parezca mejor, estaremos diseñando una solución confiable. Pero primero, agregaremos una decisión de acción para completar nuestro sistema de borrador.

Decisión de acción

A continuación, necesitamos cerrar el ciclo y llegar a una decisión real basada en los recibos. Esto se ve bastante similar, así que presentaremos el código sin comentarios.

Normalmente, uno comenzaría con el modelo más capaz —o3, en este momento— para una primera pasada, y luego, una vez establecida la corrección, experimentaría con diferentes modelos para analizar cualquier compensación por su impacto comercial, y potencialmente consideraría si son remediables con la iteración. Un cliente puede estar dispuesto a aceptar una cierta pérdida de precisión por una menor latencia o costo, o puede ser más efectivo cambiar la arquitectura para alcanzar los objetivos de costo, latencia y precisión. Más adelante veremos cómo hacer explícitas y objetivas estas compensaciones.

Para este manual, o3 podría ser demasiado bueno. Usaremos o4-mini para nuestra primera pasada, de modo que obtengamos algunos errores de razonamiento que podamos usar para ilustrar los medios para abordarlos cuando ocurran.

A continuación, necesitamos cerrar el ciclo y llegar a una decisión real basada en los recibos. Esto se ve bastante similar, así que presentaremos el código sin comentarios.

from pydantic import BaseModel, Field

audit_prompt = """
Evaluate this receipt data to determine if it need to be audited based on the following
criteria:

1. NOT_TRAVEL_RELATED:
   - IMPORTANT: For this criterion, travel-related expenses include but are not limited
   to: gas, hotel, airfare, or car rental.
   - If the receipt IS for a travel-related expense, set this to FALSE.
   - If the receipt is NOT for a travel-related expense (like office supplies), set this
   to TRUE.
   - In other words, if the receipt shows FUEL/GAS, this would be FALSE because gas IS
   travel-related.

2. AMOUNT_OVER_LIMIT: The total amount exceeds $50

3. MATH_ERROR: The math for computing the total doesn't add up (line items don't sum to
   total)

4. HANDWRITTEN_X: There is an "X" in the handwritten notes

For each criterion, determine if it is violated (true) or not (false). Provide your
reasoning for each decision, and make a final determination on whether the receipt needs
auditing. A receipt needs auditing if ANY of the criteria are violated.

Return a structured response with your evaluation.
"""


class AuditDecision(BaseModel):
    not_travel_related: bool = Field(
        description="True if the receipt is not travel-related"
    )
    amount_over_limit: bool = Field(description="True if the total amount exceeds $50")
    math_error: bool = Field(description="True if there are math errors in the receipt")
    handwritten_x: bool = Field(
        description="True if there is an 'X' in the handwritten notes"
    )
    reasoning: str = Field(description="Explanation for the audit decision")
    needs_audit: bool = Field(
        description="Final determination if receipt needs auditing"
    )


async def evaluate_receipt_for_audit(
    receipt_details: ReceiptDetails, model: str = "o4-mini"
) -> AuditDecision:
    """Determine if a receipt needs to be audited based on defined criteria."""
    # Convert receipt details to JSON for the prompt
    receipt_json = receipt_details.model_dump_json(indent=2)

    response = await client.responses.parse(
        model=model,
        input=[
            {
                "role": "user",
                "content": [
                    {"type": "input_text", "text": audit_prompt},
                    {"type": "input_text", "text": f"Receipt details:\n{receipt_json}"},
                ],
            }
        ],
        text_format=AuditDecision,
    )

    return response.output_parsed

Un esquema del proceso general muestra dos llamadas a LLM:

Process Flowchart

Si ejecutamos nuestro ejemplo anterior a través de este modelo, esto es lo que obtenemos; nuevamente, usaremos un resultado de ejemplo aquí. Cuando ejecutes el código, podrías obtener resultados ligeramente diferentes.

audit_decision = await evaluate_receipt_for_audit(result)
print(audit_decision)
audit_decision = AuditDecision(
    not_travel_related=True,
    amount_over_limit=True,
    math_error=False,
    handwritten_x=False,
    reasoning="""
    The receipt from Walmart is for office supplies, which are not travel-related, thus NOT_TRAVEL_RELATED is TRUE.
    The total amount of the receipt is $54.96, which exceeds the limit of $50, making AMOUNT_OVER_LIMIT TRUE.
    The subtotal ($50.77) plus tax ($4.19) correctly sums to the total ($54.96), so there is no MATH_ERROR.
    There are no handwritten notes, so HANDWRITTEN_X is FALSE.
    Since two criteria (amount over limit and travel-related) are violated, the receipt
    needs auditing.
    """,
    needs_audit=True,
)

Este ejemplo ilustra por qué nos importan los evals de extremo a extremo y por qué no podemos usarlos de forma aislada. Aquí, la extracción inicial tuvo errores de OCR y envió los precios al auditor que no suman el total, pero el auditor no los detecta y afirma que no hay errores matemáticos. Sin embargo, pasar esto por alto no cambia la decisión de auditoría porque sí detectó las otras dos razones por las que el recibo debe ser auditado.

Por lo tanto, AuditDecision es objetivamente incorrecto, pero la decisión que nos importa es correcta. Esto nos da una ventaja para mejorar, pero también nos guía para tomar decisiones acertadas sobre dónde y cuándo aplicar nuestros esfuerzos de ingeniería.

Dicho esto, ¡construyamos algunos evals!

Evals iniciales

Una vez que tenemos un sistema mínimamente funcional, debemos procesar más entradas y conseguir que expertos en el dominio ayuden a desarrollar datos de verdad fundamental. Los expertos en el dominio que realizan tareas especializadas pueden no tener mucho tiempo para dedicar a nuestro proyecto, por lo que queremos ser eficientes y empezar poco a poco, buscando amplitud en lugar de profundidad al principio.

Si tus datos no requieren experiencia en el dominio, entonces querrías recurrir a una solución de etiquetado (como Label Studio) e intentar anotar tantos datos como puedas dadas las restricciones de política, presupuesto y disponibilidad de datos. En este caso, procederemos como si el etiquetado de datos fuera un recurso escaso; uno en el que podemos confiar para pequeñas cantidades cada semana, pero estas son personas con otras responsabilidades laborales cuyo tiempo y disposición para ayudar pueden ser limitados. Sentarse con estos expertos para ayudar a anotar ejemplos puede hacer que la selección de futuros ejemplos sea más eficiente.

Debido a que tenemos una cadena de dos pasos, estaremos recolectando tuplas del tipo [FilePath, ReceiptDetails, AuditDecision]. Generalmente, la forma de hacer esto es tomar muestras sin etiquetar, ejecutarlas a través de nuestro modelo y luego hacer que los expertos corrijan la salida. Para los propósitos de este notebook, ya hemos pasado por ese proceso para todas las imágenes de recibos en data/test.

Consideraciones adicionales

Sin embargo, hay un poco más que eso, porque cuando estás evaluando un proceso de varios pasos, es importante conocer tanto el rendimiento de extremo a extremo como el rendimiento de cada paso individual, condicionado a la salida del paso anterior.

En este caso, queremos evaluar:

  1. Dada una imagen de entrada, ¿qué tan bien extraemos la información que necesitamos?
  2. Dada la información del recibo, ¿qué tan bueno es nuestro juicio para nuestra decisión de auditoría?
  3. Dada una imagen de entrada, ¿qué tan exitosos somos al tomar nuestra decisión final de auditoría?

La diferencia de fraseo entre el #2 y el #3 se debe a que si le damos a nuestro auditor datos incorrectos, esperamos que llegue a conclusiones incorrectas. Lo que queremos es tener la confianza de que el auditor está tomando la decisión correcta basándose en la evidencia disponible, incluso si esa evidencia es engañosa. Si no prestamos atención a ese caso, podemos terminar entrenando al auditor para que ignore sus entradas y hacer que nuestro rendimiento general se degrade.

Calificadores

El componente central de un eval es el calificador. Nuestro eval final utilizará 18 de ellos, pero solo usamos tres tipos, y todos son conceptualmente bastante sencillos.

Aquí hay ejemplos de uno de nuestros calificadores de verificación de cadenas, uno de nuestros calificadores de similitud de texto y, finalmente, uno de nuestros calificadores de modelo.

example_graders = [
    {
        "name": "Total Amount Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_receipt_details.total }}",
        "reference": "{{ item.correct_receipt_details.total }}",
    },
    {
        "name": "Merchant Name Accuracy",
        "type": "text_similarity",
        "input": "{{ item.predicted_receipt_details.merchant }}",
        "reference": "{{ item.correct_receipt_details.merchant }}",
        "pass_threshold": 0.8,
        "evaluation_metric": "bleu",
    },
]

# A model grader needs a prompt to instruct it in what it should be scoring.
missed_items_grader_prompt = """
Your task is to evaluate the correctness of a receipt extraction model.

The following items are the actual (correct) line items from a specific receipt.

{{ item.correct_receipt_details.items }}

The following items are the line items extracted by the model.

{{ item.predicted_receipt_details.items }}

Score 0 if the sample evaluation missed any items from the receipt; otherwise score 1.

The line items are permitted to have small differences or extraction mistakes, but each
item from the actual receipt must be present in some form in the model's output. Only
evaluate whether there are MISSED items; ignore other mistakes or extra items.
"""

example_graders.append(
    {
        "name": "Missed Line Items",
        "type": "score_model",
        "model": "o4-mini",
        "input": [{"role": "system", "content": missed_items_grader_prompt}],
        "range": [0, 1],
        "pass_threshold": 1,
    }
)

Cada calificador evalúa una parte de una salida predicha. Esto podría ser una verificación muy específica para un campo en una salida estructurada, o una verificación más holística que juzga una salida en su totalidad. Algunos calificadores pueden funcionar sin contexto y evaluar una salida de forma aislada (por ejemplo, un juez LLM que evalúa si un párrafo es grosero o inapropiado). Otros pueden evaluar basándose en la entrada y la salida, mientras que los que estamos usando aquí se basan en una salida y una salida de verdad fundamental (correcta) para comparar.

La forma más directa de usar Evals proporciona un prompt y un modelo, y permite que el eval se ejecute en una entrada para generar la salida por sí mismo. Otro método útil utiliza respuestas o finalizaciones previamente registradas como fuente de las salidas. No es tan simple, pero lo más flexible que podemos hacer es proporcionar un elemento que contenga todo lo que queremos que use; esto nos permite que la función de "predicción" sea un sistema arbitrario en lugar de restringirla a una sola llamada de modelo. Así es como lo estamos usando en los ejemplos siguientes; el EvaluationRecord que se muestra a continuación se utilizará para completar las variables de la plantilla {{ }}.

Nota sobre la selección del modelo:
Seleccionar el modelo correcto es crucial. Si bien los modelos más rápidos y menos costosos suelen ser preferibles en producción, los flujos de trabajo de desarrollo se benefician al priorizar los modelos más capaces disponibles. Para esta guía, usamos o4-mini tanto para tareas del sistema como para la calificación basada en LLM; si bien o3 es más capaz, nuestra experiencia sugiere que la diferencia en la calidad de la salida es modesta en relación con el aumento sustancial del costo. En la práctica, gastar $10+/día/ingeniero en evals es típico, pero escalar a $100+/día/ingeniero puede no ser sostenible.

No obstante, es valioso comparar periódicamente con un modelo más avanzado como o3. Si observas mejoras significativas, considera incorporarlo para un subconjunto representativo de tus datos de evaluación. Las discrepancias entre modelos pueden revelar casos extremos importantes y guiar las mejoras del sistema.

import asyncio


class EvaluationRecord(BaseModel):
    """Holds both the correct (ground truth) and predicted audit decisions."""

    receipt_image_path: str
    correct_receipt_details: ReceiptDetails
    predicted_receipt_details: ReceiptDetails
    correct_audit_decision: AuditDecision
    predicted_audit_decision: AuditDecision


async def create_evaluation_record(image_path: Path, model: str) -> EvaluationRecord:
    """Create a ground truth record for a receipt image."""
    extraction_path = ground_truth_dir / "extraction" / f"{image_path.stem}.json"
    correct_details = ReceiptDetails.model_validate_json(extraction_path.read_text())
    predicted_details = await extract_receipt_details(image_path, model)

    audit_path = ground_truth_dir / "audit_results" / f"{image_path.stem}.json"
    correct_audit = AuditDecision.model_validate_json(audit_path.read_text())
    predicted_audit = await evaluate_receipt_for_audit(predicted_details, model)

    return EvaluationRecord(
        receipt_image_path=image_path.name,
        correct_receipt_details=correct_details,
        predicted_receipt_details=predicted_details,
        correct_audit_decision=correct_audit,
        predicted_audit_decision=predicted_audit,
    )


async def create_dataset_content(
    receipt_image_dir: Path, model: str = "o4-mini"
) -> list[dict]:
    # Assemble paired samples of ground truth data and predicted results. You could
    # instead upload this data as a file and pass a file id when you run the eval.
    tasks = [
        create_evaluation_record(image_path, model)
        for image_path in receipt_image_dir.glob("*.jpg")
    ]
    return [{"item": record.model_dump()} for record in await asyncio.gather(*tasks)]


file_content = await create_dataset_content(receipt_image_dir)

Una vez que tenemos los calificadores y los datos, crear y ejecutar nuestros evals es muy sencillo:

from persist_cache import cache


# We're caching the output so that if we re-run this cell we don't create a new eval.
@cache
async def create_eval(name: str, graders: list[dict]):
    eval_cfg = await client.evals.create(
        name=name,
        data_source_config={
            "type": "custom",
            "item_schema": EvaluationRecord.model_json_schema(),
            "include_sample_schema": False,  # Don't generate new completions.
        },
        testing_criteria=graders,
    )
    print(f"Created new eval: {eval_cfg.id}")
    return eval_cfg


initial_eval = await create_eval(
    "Initial Receipt Processing Evaluation", example_graders
)

# Run the eval.
eval_run = await client.evals.runs.create(
    name="initial-receipt-processing-run",
    eval_id=initial_eval.id,
    data_source={
        "type": "jsonl",
        "source": {"type": "file_content", "content": file_content},
    },
)
print(f"Evaluation run created: {eval_run.id}")
print(f"View results at: {eval_run.report_url}")

Después de ejecutar ese eval, podrás verlo en la interfaz de usuario y deberías ver algo como lo siguiente.

(Nota: si tienes un acuerdo de Retención de Datos Cero, OpenAI no almacena estos datos, por lo que no estarán disponibles en esta interfaz).

como:

Summary UI

Puedes profundizar en la pestaña de datos para ver ejemplos individuales:

Details UI

Conectando Evals a Métricas de Negocio

Los evals te muestran dónde puedes mejorar y te ayudan a seguir el progreso y las regresiones a lo largo del tiempo. Pero los tres evals anteriores son solo mediciones; necesitamos imbuirlos de una razón de ser.

Lo primero que necesitamos es agregar evaluaciones para la etapa final de nuestro procesamiento de recibos, para que podamos comenzar a ver los resultados de nuestras decisiones de auditoría. Lo siguiente que necesitamos, lo más importante, es un modelo de relevancia comercial.

Un modelo de negocio

Casi nunca es fácil determinar los costos y beneficios que se podrían obtener de un nuevo sistema, dependiendo de su rendimiento. A menudo, la gente evita intentar poner números a las cosas porque saben cuánta incertidumbre hay y no quieren hacer suposiciones que los hagan quedar mal. Está bien; solo tenemos que hacer nuestra mejor suposición, y si obtenemos más información más tarde, podemos refinar nuestro modelo.

Para este manual, vamos a crear una estructura de costos simple:

  • nuestra empresa procesa 1 millón de recibos al año, con un costo base de $0.20 / recibo
  • auditar un recibo cuesta alrededor de $2
  • no auditar un recibo que deberíamos haber auditado cuesta un promedio de $30
  • el 5% de los recibos necesitan ser auditados
  • el proceso existente
    • identifica los recibos que necesitan ser auditados el 97% de las veces
    • identifica erróneamente los recibos que no necesitan ser auditados el 2% de las veces

Esto nos da dos comparaciones de referencia:

  • si identificáramos cada recibo correctamente, gastaríamos $100,000 en auditorías
  • nuestro proceso actual gasta $135,000 en auditorías y pierde $45,000 en gastos no auditados

Además de eso, el proceso impulsado por humanos cuesta $200,000 adicionales.

Esperamos que nuestro servicio ahorre dinero al costar menos de ejecutar (≈1¢/recibo si usamos los prompts anteriores con o4-mini), pero si ahorramos o perdemos dinero en auditorías y auditorías perdidas depende de qué tan bien funcione nuestro sistema. Podría valer la pena escribir esto como una función simple; a continuación se muestra una versión que incluye los factores anteriores pero que ignora los matices y los costos de desarrollo, mantenimiento y servicio.

def calculate_costs(fp_rate: float, fn_rate: float, per_receipt_cost: float):
    audit_cost = 2
    missed_audit_cost = 30
    receipt_count = 1e6
    audit_fraction = 0.05

    needs_audit_count = receipt_count * audit_fraction
    no_needs_audit_count = receipt_count - needs_audit_count

    missed_audits = needs_audit_count * fn_rate
    total_audits = needs_audit_count * (1 - fn_rate) + no_needs_audit_count * fp_rate

    audit_cost = total_audits * audit_cost
    missed_audit_cost = missed_audits * missed_audit_cost
    processing_cost = receipt_count * per_receipt_cost

    return audit_cost + missed_audit_cost + processing_cost


perfect_system_cost = calculate_costs(0, 0, 0)
current_system_cost = calculate_costs(0.02, 0.03, 0.20)

print(f"Current system cost: ${current_system_cost:,.0f}")

Conectando con las evaluaciones

El objetivo del modelo anterior es que nos permite darle significado a una evaluación que, de otro modo, sería solo un número. Por ejemplo, cuando ejecutamos el sistema anterior, nos equivocamos el 85% de las veces en los nombres de los comerciantes. Pero al investigar, parece que la mayoría de las instancias son problemas de mayúsculas o "Shell Gasoline" vs. "Shell Oil #2144" — problemas que, cuando los seguimos, no parecen afectar nuestra decisión de auditoría ni cambiar nuestros costos fundamentales.

Por otro lado, parece que no detectamos las "X" escritas a mano en los recibos aproximadamente la mitad de las veces, y aproximadamente la mitad de las veces que hay una "X" en un recibo que se pasa por alto, resulta en que un recibo no se audita cuando debería. Estos están sobrerrepresentados en nuestro conjunto de datos, pero si eso representa incluso el 1% de los recibos, ese 50% de falla nos costaría $75,000 al año.

De manera similar, parece que tenemos errores de OCR que nos hacen auditar recibos con bastante frecuencia debido a que las cuentas no cuadran, hasta el 20% de las veces. ¡Esto podría costarnos casi $400,000!

Ahora, estamos en un punto para agregar más evaluadores y comenzar a trabajar hacia atrás desde la precisión de la decisión de auditoría para determinar en qué problemas debemos enfocarnos.

A continuación, se muestran el resto de nuestros evaluadores y los resultados que obtenemos con nuestros prompts iniciales no optimizados. ¡Ten en cuenta que en este punto lo hacemos bastante mal! De nuestras 20 muestras (8 positivas, 12 negativas), tuvimos dos falsos negativos y dos falsos positivos. Si extrapoláramos a todo nuestro negocio, estaríamos perdiendo $375,000 en auditorías que pasamos por alto y $475,000 en auditorías innecesarias.

simple_extraction_graders = [
    {
        "name": "Merchant Name Accuracy",
        "type": "text_similarity",
        "input": "{{ item.predicted_receipt_details.merchant }}",
        "reference": "{{ item.correct_receipt_details.merchant }}",
        "pass_threshold": 0.8,
        "evaluation_metric": "bleu",
    },
    {
        "name": "Location City Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_receipt_details.location.city }}",
        "reference": "{{ item.correct_receipt_details.location.city }}",
    },
    {
        "name": "Location State Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_receipt_details.location.state }}",
        "reference": "{{ item.correct_receipt_details.location.state }}",
    },
    {
        "name": "Location Zipcode Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_receipt_details.location.zipcode }}",
        "reference": "{{ item.correct_receipt_details.location.zipcode }}",
    },
    {
        "name": "Time Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_receipt_details.time }}",
        "reference": "{{ item.correct_receipt_details.time }}",
    },
    {
        "name": "Subtotal Amount Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_receipt_details.subtotal }}",
        "reference": "{{ item.correct_receipt_details.subtotal }}",
    },
    {
        "name": "Tax Amount Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_receipt_details.tax }}",
        "reference": "{{ item.correct_receipt_details.tax }}",
    },
    {
        "name": "Total Amount Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_receipt_details.total }}",
        "reference": "{{ item.correct_receipt_details.total }}",
    },
    {
        "name": "Handwritten Notes Accuracy",
        "type": "text_similarity",
        "input": "{{ item.predicted_receipt_details.handwritten_notes }}",
        "reference": "{{ item.correct_receipt_details.handwritten_notes }}",
        "pass_threshold": 0.8,
        "evaluation_metric": "fuzzy_match",
    },
]

item_extraction_base = """
Your task is to evaluate the correctness of a receipt extraction model.

The following items are the actual (correct) line items from a specific receipt.

{{ item.correct_receipt_details.items }}

The following items are the line items extracted by the model.

{{ item.predicted_receipt_details.items }}
"""

missed_items_instructions = """
Score 0 if the sample evaluation missed any items from the receipt; otherwise score 1.

The line items are permitted to have small differences or extraction mistakes, but each
item from the actual receipt must be present in some form in the model's output. Only
evaluate whether there are MISSED items; ignore other mistakes or extra items.
"""

extra_items_instructions = """
Score 0 if the sample evaluation extracted any extra items from the receipt; otherwise
score 1.

The line items are permitted to have small differences or extraction mistakes, but each
item from the actual receipt must be present in some form in the model's output. Only
evaluate whether there are EXTRA items; ignore other mistakes or missed items.
"""

item_mistakes_instructions = """
Score 0 to 10 based on the number and severity of mistakes in the line items.

A score of 10 means that the two lists are perfectly identical.

Remove 1 point for each minor mistake (typos, capitalization, category name
differences), and up to 3 points for significant mistakes (incorrect quantity, price, or
total, or categories that are not at all similar).
"""

item_extraction_graders = [
    {
        "name": "Missed Line Items",
        "type": "score_model",
        "model": "o4-mini",
        "input": [
            {
                "role": "system",
                "content": item_extraction_base + missed_items_instructions,
            }
        ],
        "range": [0, 1],
        "pass_threshold": 1,
    },
    {
        "name": "Extra Line Items",
        "type": "score_model",
        "model": "o4-mini",
        "input": [
            {
                "role": "system",
                "content": item_extraction_base + extra_items_instructions,
            }
        ],
        "range": [0, 1],
        "pass_threshold": 1,
    },
    {
        "name": "Item Mistakes",
        "type": "score_model",
        "model": "o4-mini",
        "input": [
            {
                "role": "system",
                "content": item_extraction_base + item_mistakes_instructions,
            }
        ],
        "range": [0, 10],
        "pass_threshold": 8,
    },
]


simple_audit_graders = [
    {
        "name": "Not Travel Related Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_audit_decision.not_travel_related }}",
        "reference": "{{ item.correct_audit_decision.not_travel_related }}",
    },
    {
        "name": "Amount Over Limit Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_audit_decision.amount_over_limit }}",
        "reference": "{{ item.correct_audit_decision.amount_over_limit }}",
    },
    {
        "name": "Math Error Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_audit_decision.math_error }}",
        "reference": "{{ item.correct_audit_decision.math_error }}",
    },
    {
        "name": "Handwritten X Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_audit_decision.handwritten_x }}",
        "reference": "{{ item.correct_audit_decision.handwritten_x }}",
    },
    {
        "name": "Needs Audit Accuracy",
        "type": "string_check",
        "operation": "eq",
        "input": "{{ item.predicted_audit_decision.needs_audit }}",
        "reference": "{{ item.correct_audit_decision.needs_audit }}",
    },
]


reasoning_eval_prompt = """
Your task is to evaluate the quality of *reasoning* for audit decisions on receipts.
Here are the rules for audit decisions:

Expenses should be audited if they violate any of the following criteria:
1. Expenses must be travel-related
2. Expenses must not exceed $50
3. All math should be correct; the line items plus tax should equal the total
4. There must not be an "X" in the handwritten notes

If ANY of those criteria are violated, the expense should be audited.

Here is the input to the grader:
{{ item.predicted_receipt_details }}

Below is the output of an authoritative grader making a decision about whether or not to
audit an expense. This is a correct reference decision.

GROUND TRUTH:
{{ item.correct_audit_decision }}


Here is the output of the model we are evaluating:

MODEL GENERATED:
{{ item.predicted_audit_decision }}


Evaluate:
1. For each of the 4 criteria, did the model correctly score it as TRUE or FALSE?
2. Based on the model's *scoring* of the criteria (regardless if it scored it
   correctly), did the model reason appropriately about the criteria (i.e. did it
   understand and apply the prompt correctly)?
3. Is the model's reasoning logically sound, sufficient, and comprehensible?
4. Is the model's reasoning concise, without extraneous details?
5. Is the final decision to audit or not audit correct?

Grade the model with the following rubric:
- (1) point for each of the 4 criteria that the model scored correctly
- (3) points for each aspect of the model's reasoning that is meets the criteria
- (3) points for the model's final decision to audit or not audit

The total score is the sum of the points, and should be between 0 and 10 inclusive.
"""


model_judgement_graders = [
    {
        "name": "Audit Reasoning Quality",
        "type": "score_model",
        "model": "o4-mini",
        "input": [{"role": "system", "content": reasoning_eval_prompt}],
        "range": [0, 10],
        "pass_threshold": 8,
    },
]

full_eval = await create_eval(
    "Full Receipt Processing Evaluation",
    simple_extraction_graders
    + item_extraction_graders
    + simple_audit_graders
    + model_judgement_graders,
)

eval_run = await client.evals.runs.create(
    name="complete-receipt-processing-run",
    eval_id=full_eval.id,
    data_source={
        "type": "jsonl",
        "source": {"type": "file_content", "content": file_content},
    },
)

eval_run.report_url

Large Summary UI

Pon en marcha el ciclo de mejora

Tener nuestro modelo de negocio significa que tenemos un mapa de lo que vale la pena hacer y lo que no. Nuestras evaluaciones iniciales son una señal de tráfico que nos permite saber que nos estamos moviendo en la dirección correcta; pero eventualmente necesitaremos más señalización. En este punto del proceso, generalmente tenemos muchas cosas diferentes en las que podemos trabajar, con algunos ciclos vinculados donde la mejora en uno abrirá más espacio para la mejora en un ciclo diferente.

Development Flywheel

  1. Nuestras evaluaciones nos muestran dónde podemos mejorar, y podemos usarlas de inmediato para guiarnos en la selección del modelo, la ingeniería de prompts, el uso de herramientas y las estrategias de fine-tuning.
  2. No hemos terminado una vez que el sistema funciona bien según nuestras evaluaciones. Es entonces cuando es el momento de mejorar nuestras evaluaciones. Procesaremos más datos, se los daremos a nuestros expertos en el dominio para que los revisen, y alimentaremos las correcciones para construir evaluaciones mejores y más completas.

Este ciclo puede durar un tiempo. Podemos acelerarlo identificando la frontera eficiente de datos "interesantes" para examinar. Hay algunas técnicas para esto, pero una fácil es volver a ejecutar modelos en entradas para priorizar el etiquetado de entradas que no obtienen respuestas consistentes. Esto funciona especialmente bien cuando se usan diferentes modelos subyacentes, y a menudo incluso se beneficia del uso de modelos menos inteligentes (si un modelo tonto está de acuerdo con un modelo inteligente, entonces probablemente no sea un problema difícil).

Una vez que parece que hemos llegado a un punto de rendimientos decrecientes en el rendimiento, podemos seguir usando las mismas técnicas para optimizar el costo del modelo; si tenemos un sistema que funciona bastante bien, entonces el fine-tuning o alguna forma de destilación de modelos probablemente nos permitirá obtener un rendimiento similar de modelos más pequeños, más baratos y más rápidos.

Mejoras del sistema

Con nuestras evaluaciones implementadas y una comprensión de cómo se conectan con nuestras métricas de negocio, finalmente estamos listos para centrar nuestra atención en mejorar la salida de nuestro sistema.

Arriba, notamos que nos equivocamos en los nombres de los comerciantes el 85% de las veces, más que en cualquier otra salida que estamos evaluando. Esto parece bastante malo, y probablemente sea algo que podamos mejorar drásticamente con poco trabajo, pero en su lugar, comencemos desde el punto final de nuestras métricas de negocio y trabajemos hacia atrás para ver qué problemas causaron decisiones incorrectas.

Cuando hacemos eso, vemos que los errores que cometimos en los nombres de los comerciantes no tienen ninguna correlación con nuestra decisión final de auditoría, y no hay evidencia de que tengan algún impacto en esa decisión. Según nuestro modelo de negocio, en realidad no vemos la necesidad de mejorarlo; en otras palabras, no todas las evaluaciones importan. En cambio, podemos examinar específicamente los ejemplos en los que tomamos una mala decisión de auditoría. Solo hay dos de ellos (de 20). Al examinarlos de cerca, observamos que en ambos casos el problema provino de la segunda etapa del pipeline, que tomó una decisión incorrecta basada en una extracción no problemática. Y, de hecho, ambos provienen de una falla al razonar correctamente sobre los gastos relacionados con viajes.

En el primer caso, la compra es un cepillo para nieve de una tienda de autopartes. Este es un caso un poco atípico, pero nuestros expertos en el dominio lo identificaron como un gasto de viaje válido (porque los conductores podrían necesitar uno para limpiar su parabrisas). Esto parece que explicar el proceso de decisión con más detalle y proporcionar un ejemplo análogo corregiría el error.

En el segundo caso, la compra son algunas herramientas de una tienda de mejoras para el hogar. Las herramientas no tienen nada que ver con la conducción normal, por lo que este recibo debe auditarse como un "gasto no relacionado con viajes". En este caso, nuestro modelo correctamente lo identifica como un gasto no relacionado con viajes, pero luego razona incorrectamente sobre ese hecho, aparentemente malinterpretando que true para not_travel_related debería implicar true para needs_audit. De nuevo, este parece un ejemplo en el que una mayor claridad en nuestras instrucciones y algunos ejemplos deberían solucionar el problema.

Conectando esto de nuevo a nuestro modelo de costos, notamos que tenemos 1 falso negativo y 1 falso positivo, junto con 7 verdaderos positivos y 11 verdaderos negativos. Extrapolando esto a las frecuencias que vemos en producción, esto aumentaría nuestros costos generales en $63,000 por año.

Modifiquemos el prompt y volvamos a ejecutar nuestras evaluaciones para ver cómo nos va. Proporcionaremos más orientación en forma de un ejemplo específico en las instrucciones sobre el aceite de motor (diferente de un cepillo para nieve, pero requiere el mismo razonamiento), e incluiremos tres ejemplos extraídos de nuestro conjunto de entrenamiento (data/train) como guía few-shot.

first_ai_system_cost = calculate_costs(
    fp_rate=1 / 12, fn_rate=1 / 8, per_receipt_cost=0.01
)

print(f"First version of our system, estimated cost: ${first_ai_system_cost:,.0f}")
nursery_receipt_details = ReceiptDetails(
    merchant="WESTERN SIERRA NURSERY",
    location=Location(city="Oakhurst", state="CA", zipcode="93644"),
    time="2024-09-27T12:33:38",
    items=[
        LineItem(
            description="Plantskydd Repellent RTU 1 Liter",
            product_code=None,
            category="Garden/Pest Control",
            item_price="24.99",
            sale_price=None,
            quantity="1",
            total="24.99",
        )
    ],
    subtotal="24.99",
    tax="1.94",
    total="26.93",
    handwritten_notes=[],
)

nursery_audit_decision = AuditDecision(
    not_travel_related=True,
    amount_over_limit=False,
    math_error=False,
    handwritten_x=False,
    reasoning="""
    1. The merchant is a plant nursery and the item purchased an insecticide, so this
       purchase is not travel-related (criterion 1 violated).
    2. The total is $26.93, under $50, so criterion 2 is not violated.
    3. The line items (1 * $24.99 + $1.94 tax) sum to $26.93, so criterion 3 is not
       violated.
    4. There are no handwritten notes or 'X's, so criterion 4 is not violated.
    Since NOT_TRAVEL_RELATED is true, the receipt must be audited.
    """,
    needs_audit=True,
)

flying_j_details = ReceiptDetails(
    merchant="Flying J #616",
    location=Location(city="Frazier Park", state="CA", zipcode=None),
    time="2024-10-01T13:23:00",
    items=[
        LineItem(
            description="Unleaded",
            product_code=None,
            category="Fuel",
            item_price="4.459",
            sale_price=None,
            quantity="11.076",
            total="49.39",
        )
    ],
    subtotal="49.39",
    tax=None,
    total="49.39",
    handwritten_notes=["yos -> home sequoia", "236660"],
)
flying_j_audit_decision = AuditDecision(
    not_travel_related=False,
    amount_over_limit=False,
    math_error=False,
    handwritten_x=False,
    reasoning="""
    1. The only item purchased is Unleaded gasoline, which is travel-related so
       NOT_TRAVEL_RELATED is false.
    2. The total is $49.39, which is under $50, so AMOUNT_OVER_LIMIT is false.
    3. The line items ($4.459 * 11.076 = $49.387884) sum to the total of $49.39, so
       MATH_ERROR is false.
    4. There is no "X" in the handwritten notes, so HANDWRITTEN_X is false.
    Since none of the criteria are violated, the receipt does not need auditing.
    """,
    needs_audit=False,
)

engine_oil_details = ReceiptDetails(
    merchant="O'Reilly Auto Parts",
    location=Location(city="Sylmar", state="CA", zipcode="91342"),
    time="2024-04-26T8:43:11",
    items=[
        LineItem(
            description="VAL 5W-20",
            product_code=None,
            category="Auto",
            item_price="12.28",
            sale_price=None,
            quantity="1",
            total="12.28",
        )
    ],
    subtotal="12.28",
    tax="1.07",
    total="13.35",
    handwritten_notes=["vista -> yos"],
)
engine_oil_audit_decision = AuditDecision(
    not_travel_related=False,
    amount_over_limit=False,
    math_error=False,
    handwritten_x=False,
    reasoning="""
    1. The only item purchased is engine oil, which might be required for a vehicle
       while traveling, so NOT_TRAVEL_RELATED is false.
    2. The total is $13.35, which is under $50, so AMOUNT_OVER_LIMIT is false.
    3. The line items ($12.28 + $1.07 tax) sum to the total of $13.35, so
       MATH_ERROR is false.
    4. There is no "X" in the handwritten notes, so HANDWRITTEN_X is false.
    None of the criteria are violated so the receipt does not need to be audited.
    """,
    needs_audit=False,
)

examples = [
    {"input": nursery_receipt_details, "output": nursery_audit_decision},
    {"input": flying_j_details, "output": flying_j_audit_decision},
    {"input": engine_oil_details, "output": engine_oil_audit_decision},
]

# Format the examples as JSON, with each example wrapped in XML tags.
example_format = """
<example>
    <input>
        {input}
    </input>
    <output>
        {output}
    </output>
</example>
"""

examples_string = ""
for example in examples:
    example_input = example["input"].model_dump_json()
    correct_output = example["output"].model_dump_json()
    examples_string += example_format.format(input=example_input, output=correct_output)

audit_prompt = f"""
Evaluate this receipt data to determine if it need to be audited based on the following
criteria:

1. NOT_TRAVEL_RELATED:
   - IMPORTANT: For this criterion, travel-related expenses include but are not limited
   to: gas, hotel, airfare, or car rental.
   - If the receipt IS for a travel-related expense, set this to FALSE.
   - If the receipt is NOT for a travel-related expense (like office supplies), set this
   to TRUE.
   - In other words, if the receipt shows FUEL/GAS, this would be FALSE because gas IS
   travel-related.
   - Travel-related expenses include anything that could be reasonably required for
   business-related travel activities. For instance, an employee using a personal
   vehicle might need to change their oil; if the receipt is for an oil change or the
   purchase of oil from an auto parts store, this would be acceptable and counts as a
   travel-related expense.

2. AMOUNT_OVER_LIMIT: The total amount exceeds $50

3. MATH_ERROR: The math for computing the total doesn't add up (line items don't sum to
   total)
   - Add up the price and quantity of each line item to get the subtotal
   - Add tax to the subtotal to get the total
   - If the total doesn't match the amount on the receipt, this is a math error
   - If the total is off by no more than $0.01, this is NOT a math error

4. HANDWRITTEN_X: There is an "X" in the handwritten notes

For each criterion, determine if it is violated (true) or not (false). Provide your
reasoning for each decision, and make a final determination on whether the receipt needs
auditing. A receipt needs auditing if ANY of the criteria are violated.

Note that violation of a criterion means that it is `true`. If any of the above four
values are `true`, then the receipt needs auditing (`needs_audit` should be `true`: it
functions as a boolean OR over all four criteria).

If the receipt contains non-travel expenses, then NOT_TRAVEL_RELATED should be `true`
and therefore NEEDS_AUDIT must also be set to `true`. IF THE RECEIPT LISTS ITEMS THAT
ARE NOT TRAVEL-RELATED, THEN IT MUST BE AUDITED. Here are some example inputs to
demonstrate how you should act:

<examples>
{examples_string}
</examples>

Return a structured response with your evaluation.
"""

Las modificaciones que hicimos al prompt anterior son:

  1. En el punto 1, sobre gastos relacionados con viajes, agregamos un punto
- Travel-related expenses include anything that could be reasonably required for
  business-related travel activities. For instance, an employee using a personal
  vehicle might need to change their oil; if the receipt is for an oil change or the
  purchase of oil from an auto parts store, this would be acceptable and counts as a
  travel-related expense.
  1. Agregamos una guía más prescriptiva sobre cómo evaluar un error matemático. Específicamente, agregamos los puntos:
   - Add up the price and quantity of each line item to get the subtotal
   - Add tax to the subtotal to get the total
   - If the total doesn't match the amount on the receipt, this is a math error
   - If the total is off by no more than $0.01, this is NOT a math error

Esto en realidad no tiene que ver con los problemas que mencionamos, pero es otro problema que notamos como un defecto en el razonamiento proporcionado por el modelo de auditoría.

  1. Agregamos una guía muy fuerte (de hecho, tuvimos que declararla y reiterarla enfáticamente) para decir que los gastos no relacionados con viajes deben ser auditados.
Note that violation of a criterion means that it is `true`. If any of the above four
values are `true`, then the receipt needs auditing (`needs_audit` should be `true`: it
functions as a boolean OR over all four criteria).

If the receipt contains non-travel expenses, then NOT_TRAVEL_RELATED should be `true`
and therefore NEEDS_AUDIT must also be set to `true`. IF THE RECEIPT LISTS ITEMS THAT
ARE NOT TRAVEL-RELATED, THEN IT MUST BE AUDITED.
  1. Agregamos tres ejemplos, pares de entrada/salida JSON envueltos en etiquetas XML.

Con nuestras revisiones de prompt, regeneraremos los datos para evaluar y volveremos a ejecutar la misma evaluación para comparar nuestros resultados:

file_content = await create_dataset_content(receipt_image_dir)

eval_run = await client.evals.runs.create(
    name="updated-receipt-processing-run",
    eval_id=full_eval.id,
    data_source={
        "type": "jsonl",
        "source": {"type": "file_content", "content": file_content},
    },
)

eval_run.report_url

Cuando ejecutamos la evaluación de nuevo, en realidad todavía obtuvimos dos decisiones de auditoría incorrectas. Al investigar los ejemplos en los que cometimos un error, resultó que solucionamos completamente los problemas que identificamos, pero nuestros ejemplos mejoraron el paso de razonamiento y causaron que surgieran otros dos problemas. Específicamente:

  1. Un recibo necesitaba ser auditado solo porque hubo un error en la extracción y no se identificó una "X" escrita a mano. El modelo de auditoría razonó correctamente, pero basándose en datos incorrectos.
  2. Un recibo se extrajo de tal manera que una tarifa de débito de $0.35 no era visible, por lo que el modelo de auditoría identificó un error matemático. Esto casi con certeza sucedió porque le proporcionamos instrucciones más detalladas y ejemplos claros que demostraban que necesitaba sumar todos los elementos de la línea para decidir si había un error matemático. De nuevo, esto demuestra un comportamiento correcto por parte del modelo de auditoría y sugiere que necesitamos corregir el modelo de extracción.

¡Esto es genial, y continuaremos iterando sobre los problemas a medida que los descubramos. Este es el ciclo de mejora!

Elección del modelo

Al iniciar un proyecto, generalmente comenzamos con uno de los modelos más capaces disponibles, como o4-mini, para establecer una línea de base de rendimiento. Una vez que estamos seguros de la capacidad del modelo para resolver la tarea, el siguiente paso es explorar alternativas más pequeñas, rápidas o rentables.

Optimizar el costo de inferencia y la latencia es esencial, especialmente para sistemas de producción o de cara al cliente, donde estos factores pueden afectar significativamente los gastos generales y la experiencia del usuario. Por ejemplo, cambiar de o4-mini a gpt-4.1-mini podría reducir los costos de inferencia en casi dos tercios, un ejemplo donde una selección cuidadosa del modelo conduce a ahorros significativos.

En la siguiente sección, volveremos a ejecutar nuestras evaluaciones usando gpt-4.1-mini para los pasos de extracción y auditoría para ver qué tan bien funciona un modelo más eficiente.

file_content = await create_dataset_content(receipt_image_dir, model="gpt-4.1-mini")

eval_run = await client.evals.runs.create(
    name="receipt-processing-run-gpt-4-1-mini",
    eval_id=full_eval.id,
    data_source={
        "type": "jsonl",
        "source": {"type": "file_content", "content": file_content},
    },
)

eval_run.report_url

Los resultados son bastante prometedores. No parece que la precisión de la extracción haya sufrido en absoluto. Vemos una regresión (el cepillo para nieve de nuevo), pero nuestra decisión de auditoría es correcta el doble de veces que antes de nuestros cambios de prompt.

Eval Variations

Esta es una gran evidencia de que podremos cambiar a un modelo más barato, pero podría requerir más ingeniería de prompts, fine-tuning o alguna forma de destilación de modelos. Ten en cuenta, sin embargo, que según nuestro modelo actual, esto ya nos estaría ahorrando dinero. No creemos del todo eso todavía porque no tenemos una muestra lo suficientemente grande; nuestra tasa real de falsos negativos será mayor que el 0 que vemos aquí.

system_cost_4_1_mini = calculate_costs(
    fp_rate=1 / 12, fn_rate=0, per_receipt_cost=0.003
)

print(f"Cost using gpt-4.1-mini: ${system_cost_4_1_mini:,.0f}")

Mejoras adicionales

Este manual se centra en la filosofía y las prácticas de las evaluaciones, no en la gama completa de técnicas de mejora de modelos. Para aumentar o mantener el rendimiento del modelo (especialmente al pasar a modelos más pequeños, rápidos o económicos), considera estos pasos en orden: comienza desde arriba y solo avanza si es necesario. Por ejemplo, siempre optimiza tu prompt antes de recurrir al fine-tuning; el fine-tuning con un prompt débil puede fijar un rendimiento deficiente incluso si mejoras el prompt más tarde.

Model Improvement Waterfall

  1. Selección del modelo: prueba modelos más inteligentes o aumenta su presupuesto de razonamiento.
  2. Ajuste del prompt: aclara las instrucciones y proporciona reglas muy explícitas.
  3. Ejemplos y contexto: agrega ejemplos few-shot o many-shot, o más contexto para el problema. RAG encaja aquí y puede usarse para seleccionar dinámicamente ejemplos similares.
  4. Uso de herramientas: proporciona herramientas para resolver problemas específicos, incluido el acceso a API externas, la capacidad de consultar bases de datos o, de otro modo, permitir que el modelo obtenga respuestas a sus propias preguntas.
  5. Modelos accesorios: agrega modelos para realizar subtareas limitadas, para supervisar y proporcionar barreras de seguridad, o usa una mezcla de expertos y agrega soluciones de múltiples submodelos.
  6. Fine-tuning: usa datos de entrenamiento etiquetados para el fine-tuning supervisado, evaluadores de evaluación para el fine-tuning por refuerzo, o diferentes salidas para la optimización de preferencia directa.

Las opciones anteriores son todas herramientas para maximizar el rendimiento. Una vez que intentas optimizar la relación precio:rendimiento, generalmente ya habrás hecho todo lo anterior y probablemente no necesites repetir la mayoría de los pasos, pero aún puedes ajustar modelos más pequeños o usar tu mejor modelo para entrenar un modelo más pequeño (destilación de modelos).

Una cosa realmente excelente de OpenAI Evals es que puedes usar los mismos evaluadores para Reinforcement Fine-Tuning para producir un mejor rendimiento del modelo de una manera extremadamente eficiente en cuanto a muestras. Una nota de precaución es asegurarte de usar datos de entrenamiento separados y no filtrar tus conjuntos de datos de evaluación durante el RFT.

Implementación y posdesarrollo

Construir e implementar una aplicación LLM es solo el comienzo; el valor real proviene de la mejora continua. Una vez que tu sistema esté en vivo, prioriza el monitoreo continuo: registra rastros, rastrea salidas y muestrea proactivamente interacciones de usuarios reales para revisión humana utilizando técnicas de muestreo inteligentes.

Los datos de producción son tu fuente más auténtica para evolucionar tus conjuntos de datos de evaluación y entrenamiento. Recopila y organiza regularmente nuevas muestras de casos de uso reales para identificar brechas, casos extremos y nuevas oportunidades de mejora.

En la práctica, aprovecha estos datos para una iteración rápida. Automatiza pipelines de fine-tuning periódicos que reentrenen tus modelos con muestras recientes y de alta calidad, y despliega automáticamente nuevas versiones cuando superen a las existentes en tus evaluaciones. Captura las correcciones y comentarios de los usuarios, luego alimenta sistemáticamente estas ideas a tus prompts o al proceso de reentrenamiento, especialmente cuando resaltan problemas persistentes.

Al integrar estos bucles de retroalimentación en tu flujo de trabajo de posdesarrollo, aseguras que tus aplicaciones LLM se adapten continuamente, se mantengan robustas y permanezcan estrechamente alineadas con las necesidades de los usuarios a medida que evolucionan.

Colaboradores

Este manual es un esfuerzo de colaboración conjunta entre OpenAI y Fractional.

  • Hugh Wimberly
  • Joshua Marker
  • Eddie Siegel
  • Shikhar Kwatra
Lección del curso «OpenAI Cookbook» de OpenAI, publicado con licencia MIT. Traducción y adaptación al español de IA con Clase. IA con Clase no está afiliado a OpenAI. Ver el original · Licencia
Esta lección es gratuita. El resto del curso se abre con la Membresía de IA con Clase, que incluye todos los cursos del catálogo. Ver precios