Skip to content
This page has been auto-translated and may contain errors.View in English

Salida estructurada

docs.scrimba.com

Primero, la forma del problema. Salida estructurada es la práctica de hacer que un modelo devuelva datos que tu código pueda leer directamente, un campo sobre el que puedas ramificarte en lugar de un párrafo que tengas que analizar manualmente. Este capítulo trata sobre hacer eso confiable.

Hasta ahora el modelo te entrega texto, que está bien cuando una persona lo lee. Pero el software no puede hacer mucho con un párrafo. Para actuar sobre la respuesta de un modelo en código, la necesitas como datos: un campo que puedas leer, un valor sobre el que puedas ramificarte, un registro que puedas guardar.

El cambio es pequeño pero cambia todo lo que viene después. Una vez que el modelo devuelve { "sentiment": "negative" } en lugar de una oración sobre cómo el cliente parece infeliz, tu código puede enrutar el ticket, actualizar un tablero o enviar una alerta. La parte interesante es cómo haces que el modelo produzca datos limpios cuando todo lo que hace es predecir texto, y la respuesta revela una palanca que no sabías que tenías.

El modelo devuelve texto. Tu código necesita datos: un campo que leer, un valor sobre el que ramificarse, un registro que guardar. Este capítulo es el puente entre esos dos, y el movimiento es dejar de esperar que el modelo formate las cosas bien y empezar a constreñir lo que puede producir.

Hay una palanca real aquí, no un truco de ingeniería de prompts. El mismo bucle de muestreo de cómo funcionan los LLM puede dirigirse para que la salida se fuerza en la forma que quieres, lo que convierte un generador de texto difuso en un componente en el que el resto de tu sistema puede confiar.

Un modelo que devuelve prosa es un componente que no puedes componer. En el momento en que su salida alimenta otro sistema, necesitas datos con una forma conocida, y necesitas que los modos de fallo de obtener esos datos sean aquellos que puedes ver y manejar.

Este capítulo trata sobre constreñir la generación a un esquema y qué costo tiene en latencia, qué no te compra (valores correctos), y cómo falla en los bordes. La maquinaria debajo es la misma que impulsa uso de herramientas: una llamada de herramienta es salida estructurada con una firma de función ocupando el lugar del esquema.

Solicitar JSON

El primer instinto es pedir en el prompt: "responde como JSON", el formato de texto plano que el software usa para pasar datos, escrito como campos y valores dentro de llaves. Pruébalo y funciona la mayoría de las veces, que es la trampa. El modelo aún no hace nada más que predecir texto probable, y a veces el texto probable es una cerca de código, o un amable "¡Claro, aquí va!" antes de los datos, o un comentario final después. Cualquiera de esos rompe el código que lee el JSON. Pedir amablemente sesga la predicción hacia JSON; no la fuerza.

Así que los proveedores te dan una palanca real. Activar modo JSON con response_format hace más que pedir: constriñe el modelo para que la sintaxis vuelva como JSON válido.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": 'Extrae la ciudad y la temperatura. Responde como JSON: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "Hace unos 18 grados en Ciudad de México ahora."},
    ],
    response_format={"type": "json_object"},  # constriñe la salida a sintaxis JSON válida
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "Ciudad de México" 18

response_format={"type": "json_object"} hace que la salida sea JSON válido, así que json.loads no se atragantará con prosa errante. Aún describes los campos que quieres en el prompt, porque el modo JSON promete JSON válido, no los campos particulares que tenías en mente. Descarta "no es JSON en absoluto", no "JSON con la forma equivocada". Una cosa que aún no puede prometer: si la respuesta se corta a mitad de camino, puedes recibir medio objeto JSON, así que es sintaxis confiable, no una garantía dura en todos los casos.

JunoSolicitar JSON Pedir JSON en el prompt solo es inestable, porque el modelo solo predice texto probable y puede envolverlo en cercas o charla que rompan el análisis. Establecer response_format en modo JSON constriñe la sintaxis para que vuelva como JSON válido. No promete que los campos coincidan con lo que pediste, y una respuesta cortada a mitad de camino aún puede llegar como medio objeto, así que describe la forma en el prompt y verifica qué llega.

El reflejo es escribir "responde como JSON" en el prompt, donde JSON es el formato de datos de campos y valores que el software pasa. Funciona la mayoría de las veces, que es exactamente el problema: la mayoría de las veces no es un contrato. El modelo predice texto probable, y el texto probable a veces incluye una cerca de código, un preámbulo "¡Claro!", o una nota final, todo lo cual rompe json.loads.

El modo JSON es la primera palanca real. Establecer response_format en un tipo de objeto JSON constriñe la generación para que la sintaxis sea JSON válido, no prosa que parece serlo.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": 'Extrae la ciudad y la temperatura. Responde como JSON: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "Hace unos 18 grados en Ciudad de México ahora."},
    ],
    response_format={"type": "json_object"},  # constriñe la salida a sintaxis JSON válida
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "Ciudad de México" 18

La forma de response_format aquí es la de OpenAI; otros proveedores exponen la misma idea bajo su propio campo, así que porta el concepto, no la clave literal. El modo JSON constriñe la sintaxis, nunca la forma del campo. Describes los campos en el prompt, y el modelo es libre de renombrarlos, dejarlos caer o anidarlos diferentemente mientras aún emite JSON válido.

Hay una grieta más: el modo JSON solo promete JSON bien formado si la respuesta termina. Si golpeas tu techo de max_tokens a mitad de objeto, obtienes salida truncada e inanalizable, así que el análisis aún necesita una protección.

JunoSolicitar JSON El modo JSON constriñe la sintaxis para que el modelo deje de envolver datos en cercas y charla, pero nunca fija los campos, así que el modelo aún puede renombrarlos o dejarlos caer. La clave response_format es la forma de OpenAI; otros proveedores la deletrean diferente. Y la promesa solo se mantiene para una respuesta terminada, así que una respuesta truncada por max_tokens llega inanalizable. Protege el análisis de todas formas.

"Responde como JSON" en el prompt es una sugerencia, y las sugerencias fallan bajo carga: una cerca de código aquí, un preámbulo allá, un comentario final cuando el modelo se siente charlatán, cada uno una excepción de json.loads en producción. Modo JSON (aquí response_format={"type": "json_object"}) reemplaza la sugerencia con una constricción en la sintaxis generada, así que los bytes que vuelven se analizan como JSON.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": 'Extrae la ciudad y la temperatura. Responde como JSON: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "Hace unos 18 grados en Ciudad de México ahora."},
    ],
    response_format={"type": "json_object"},  # constriñe la salida a sintaxis JSON válida
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "Ciudad de México" 18

Sé preciso sobre qué constriñe esto. El modo JSON fija la sintaxis, no el esquema, y solo en una respuesta completada. El modelo aún elige nombres de campo, puede omitir un campo requerido, y puede anidar como le plazca.

La garantía es condicional a que la generación termine: si el modelo se topa con max_tokens a mitad de objeto, obtienes una cadena truncada que no es JSON válido, lo que significa que el único modo de fallo que el modo JSON no elimina es el que tu ruta de reintento más necesita manejar. La clave response_format mostrada es la de OpenAI; el equivalente en otros proveedores vive bajo un nombre diferente, así que trata el concepto como portable y el campo literal como no. Para garantías de forma necesitas un esquema, que es la siguiente sección.

JunoSolicitar JSON El modo JSON constriñe la sintaxis en una respuesta completada, nada más: el modelo aún nombra, omite, y anida campos como le plazca, y una generación matada por max_tokens te entrega salida truncada e inanalizable. Ese caso de truncamiento es el que tu ruta de error tiene que poseer, porque el modo no lo hará. La forma de response_format es la de OpenAI, así que porta la idea, no la clave. Para forma, quieres un esquema.

Bloquea la forma con un esquema

El modo JSON mantiene la sintaxis válida, pero el modelo aún podría renombrar un campo, dejar caer uno, o anidar cosas diferentemente. Para eliminar ese margen, dale un esquema: una descripción exacta de los campos y tipos que debe tener la salida, un formulario que el modelo está obligado a completar. Con strict: true, la salida está entonces garantizada que coincide con esa forma.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Extrae los detalles de contacto del mensaje."},
        {"role": "user", "content": "Hola, soy María González, contáctame en [email protected]."},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,  # impone el esquema exactamente
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)
# {"name": "María González", "email": "[email protected]"}

¿Cómo puede un esquema ser una garantía dura cuando el modelo solo predice tokens? Porque el proveedor restringe qué tokens se le permite predecir. En cada paso el modelo aún clasifica todos los posibles tokens siguientes, pero el sistema elimina los que romperían tu esquema, y el modelo elige de lo que queda.

Si el esquema dice que el siguiente campo debe ser email, los tokens que iniciarían cualquier otro campo se sacan de la mesa antes de que el modelo elija. Los tokens fuera de forma se eliminan, así que el modelo no puede desviarse de la forma.

Hay un efecto más suave que vale la pena saber. Los nombres de campo que eliges se convierten en parte de lo que el modelo lee al predecir el valor, así que un nombre claro guía una respuesta mejor. Pidiendo llenar tempC, el modelo se inclina hacia un número en Celsius; pidiendo llenar value, tiene mucho menos para ir. Nombrar campos claramente es en parte instrucción, en parte esquema.

JunoBloquea la forma con un esquema Un esquema es el formulario que el modelo tiene que completar, y con strict: true obtiene los campos y tipos exactos, porque el sistema elimina cualquier token que rompería la forma antes de que el modelo elija. Eso es imposición, no una solicitud educada. Nombra tus campos claramente también, ya que el nombre del campo es contexto que el modelo lee cuando predice el valor. Me tomó un tiempo confiar en que un nombre como tempC hace trabajo real.

El modo JSON fija la sintaxis pero deja el modelo libre para renombrar, dejar caer o reanitar campos. Un esquema cierra esa brecha: una descripción exacta de campos y tipos, y con strict: true la salida está garantizada que coincide.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Extrae los detalles de contacto del mensaje."},
        {"role": "user", "content": "Hola, soy María González, contáctame en [email protected]."},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,  # impone el esquema exactamente
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)
# {"name": "María González", "email": "[email protected]"}

El mecanismo es decodificación constreñida: poda de tokens aplicada al paso de muestreo. En cada posición el modelo produce su clasificación usual sobre todos los tokens, entonces el sistema enmascara cualquier token que haría que la salida deje de coincidir con el esquema, y el modelo muestrea de los sobrevivientes. Si la gramática dice que el siguiente token debe abrir el campo email, los tokens que iniciarían cualquier otra cosa se cero antes del sorteo. El modelo no puede desviarse de la forma porque los movimientos fuera de forma se eliminan del tablero, no se desalientan en el prompt.

Dos controles prácticos. Los nombres de campo son instrucción, así que nómbralос para el valor que quieres. tempC dirige el modelo hacia Celsius donde value lo deja adivinando, y una descripción en un campo dirige más aún. Y la misma constricción impone un enum (una lista fija de valores permitidos) o un rango de números, que es lo que hace la clasificación confiable: en la posición de la etiqueta, solo tus etiquetas listadas sobreviven la máscara, así que el modelo no puede acuñar una categoría que no esté en la lista.

JunoBloquea la forma con un esquema Con strict: true, la decodificación constreñida enmascara cada token que rompería el esquema antes de que el modelo muestree, así que la forma se impone, no se solicita. Apóyate en ello: los nombres de campo y las descripciones son instrucciones, así que nombra tempC no value y el modelo lo completa mejor. La misma enmascaración impone un enum, que es por qué la clasificación se mantiene, el modelo no puede inventar una etiqueta que no esté en tu lista.

El modo JSON te da bytes analizables; un esquema te da una forma conocida. Con strict: true la salida está garantizada que coincide con el conjunto de campos y tipos que declares, que es la diferencia entre datos que puedes indexar ciegamente y datos que tienes que sondear defensivamente.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Extrae los detalles de contacto del mensaje."},
        {"role": "user", "content": "Hola, soy María González, contáctame en [email protected]."},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)

El mecanismo es decodificación constreñida, y quieres entenderlo desde cero porque sus costos no son libres. El modelo emite una probabilidad para cada token en su vocabulario en cada paso. La decodificación constreñida compila tu esquema en una gramática (un conjunto de reglas para qué secuencias de tokens son legales), entonces en cada paso construye una máscara que fija la probabilidad de cada token ilegal a cero, así que el muestreo solo puede aterrizar en un token que mantenga la salida válida hasta ahora. La forma se impone por construcción, no pidiendo.

Esa compilación no es libre, y se muestra como latencia. La primera llamada contra un nuevo esquema paga un costo único para construir la gramática, así que la latencia en frío en un esquema fresco es más alta que el estado estable. Mantén tus esquemas estables y reutilizados en lugar de generados por solicitud, o pagas esa compilación una y otra vez.

additionalProperties: false y una lista required completa no son decoración: son lo que permite al sistema rechazar campos extra o faltantes directamente, y en proveedores que soportan rechazo impulsado por esquema, también son lo que da al modelo una forma limpia de devolver un rechazo estructurado en lugar de un objeto malformado. Déjalos caer y amplías lo que cuenta como válido, que es lo opuesto a por qué alcanzaste el modo estricto.

Unas pocas realidades de producción. El soporte estricto es desigual: algunos proveedores imponen una gramática real, otros la aproximan o soportan solo un subconjunto de JSON Schema (no pattern, anidamiento limitado), así que prueba lo que tu proveedor honra realmente en lugar de asumir la especificación. La salida parcial y por streaming es el caso incómodo: mientras los tokens fluyen, sostienes un objeto sintácticamente incompleto, así que o almacena en búfer hasta completar antes de analizar o usa un analizador incremental que tolere una estructura medio construida. Y esto es la misma maquinaria que uso de herramientas: el esquema de argumentos de una función se constriñe de la forma idéntica, así que todo aquí se transfiere directamente a hacer llamadas de herramientas confiables.

JunoBloquea la forma con un esquema La decodificación constreñida compila tu esquema a una gramática y ceros cada token ilegal antes de muestrear, así que la forma se impone por construcción. Esa compilación cuesta latencia en la primera llamada contra un nuevo esquema, así que reutiliza esquemas en lugar de acuñar uno por solicitud. Mantén additionalProperties: false y una lista required completa, son lo que imponen rechazo y, en algunos proveedores, un rechazo estructurado limpio. El soporte estricto varía, el streaming te entrega medio objeto, y la misma enmascaración impulsa args de llamada de herramienta, así que lo que aprendes aquí paga dividendos dos veces.

Valida de todas formas

Un esquema constriñe la forma, pero trata la salida como datos del mundo exterior de todas formas. La forma puede ser válida mientras el contenido es incorrecto: un correo electrónico extraído que es en realidad un error tipográfico, un número que el modelo adivinó, un campo en blanco porque la entrada no lo contenía. Y la llamada aún puede fallar de maneras ordinarias, como ser cortada por max_tokens y llegar como JSON truncado. Analiza defensivamente: una forma válida no es un valor correcto.

python
import json

def parse_contact(raw):
    try:
        data = json.loads(raw)
        if not data.get("name") or not data.get("email"):
            return None  # presente pero vacío
        return data
    except json.JSONDecodeError:
        return None  # no es JSON válido (por ejemplo, una respuesta truncada)

contact = parse_contact(response.choices[0].message.content)
if not contact:
    pass  # manejar el fallo: reintentar, preguntar de nuevo, o mostrar un error amable

Esta es la lección de alucinación de cómo funcionan los LLM en un disfraz nuevo: que la forma sea correcta no hace que los valores sean correctos. Un esquema te garantiza que obtengas un campo name; no garantiza que el nombre sea correcto. Un try/except alrededor del análisis más una comprobación de los valores es un seguro barato contra la respuesta que está bien formada y aún es incorrecta.

JunoValida de todas formas Un esquema garantiza la forma, nunca la verdad de los valores, así que un registro válido aún puede contener un valor incorrecto o vacío, y una llamada aún puede llegar truncada. Envuelve el análisis en try/except y comprueba los valores antes de confiar en ellos. Decide de antemano qué sucede cuando la validación falla, un reintento o un error amable, para que no sea un ajetreo después.

El modo estricto garantiza forma, no contenido. La salida puede coincidir con el esquema y aún ser incorrecta: un correo electrónico que parece plausible pero es un error tipográfico, un número inventado para llenar un campo requerido, una cadena vacía donde la entrada no contenía nada. Y el análisis puede fallar completamente cuando una respuesta se trunca en max_tokens. Así que validas, cada vez.

python
import json

def parse_contact(raw):
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return None, "unparseable"  # truncado o malformado

    if not data.get("name") or not data.get("email"):
        return None, "empty_field"  # presente pero en blanco
    if "@" not in data["email"]:
        return None, "bad_email"  # forma ok, valor no
    return data, None

contact, error = parse_contact(response.choices[0].message.content)
if error == "unparseable":
    pass  # reintentar una vez, probable truncamiento: elevar max_tokens o acortar entrada
elif error:
    pass  # registrar el fallo a nivel de valor y recurrir

El patrón es un análisis defensivo que distingue modos de fallo, porque quieren manejo diferente. Un JSONDecodeError usualmente significa truncamiento, así que la solución es elevar max_tokens o encoger la entrada y reintentar. Un valor vacío u fuera de rango es un fallo de contenido, no uno de formato, así que reintentar la misma llamada raramente ayuda; registra y recurre.

Separa "no pude analizar" de "analicé pero es incorrecto", porque el arreglo difiere. Esto se conecta de nuevo a alucinación: la decodificación constreñida removió el modo de fallo de salida malformada y dejó el de valor incorrecto completamente intacto.

JunoValida de todas formas El modo estricto fija forma, no contenido, así que valida cada vez: un registro puede estar bien formado y contener un error tipográfico, una adivinanza, o un blanco. Separa los fallos, porque un `JSONDecodeError` usualmente significa truncamiento (elevar `max_tokens` o encoger la entrada e reintentar) mientras que un valor incorrecto es un fallo de contenido que reintentar la misma llamada no arreglará. La decodificación constreñida eliminó el problema de salida malformada e izquierda valor incorrecto intacto.

La decodificación constreñida cierra el modo de fallo de salida malformada y no cierra nada más. La forma está garantizada; el contenido no. Un campo requerido que la entrada nunca soportó se llena con una adivinanza confiada, un número aterriza fuera de cualquier rango sensato, un correo se analiza como cadena pero es un error tipográfico.

La garantía del formato en sí se vuelve nula en truncamiento. Así que la validación no es una higiene opcional, es la capa que atrapa lo que el esquema estructuralmente no puede.

python
import json

def parse_contact(raw, retry):
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return None, "truncated"  # generación incompleta, no un problema de contenido

    if not data.get("name") or "@" not in data.get("email", ""):
        return None, "invalid_value"  # forma se mantuvo, valor no
    return data, None

contact, failure = parse_contact(raw, retry=False)
if failure == "truncated":
    pass  # elevar max_tokens o recortar entrada, entonces reintentar la llamada
elif failure == "invalid_value":
    pass  # ruta de reparación: re-preguntar con la salida incorrecta realimentada, o enrutar a alternativa

Tres cosas separan un validador que se mantiene de un try/except que engulle todo:

  • Clasifica el fallo: truncamiento es un problema de presupuesto arreglado por un reintento con más max_tokens, mientras que un valor inválido es un problema de contenido que un reintento ciego reproducirá.
  • Construye una ruta de reparación para el caso a nivel de valor en lugar de solo una alternativa. Realimentar la salida inválida con una instrucción para arreglarla a menudo la recupera en una llamada extra, que es más barato que fallar la solicitud, pero limita los reintentos así que una entrada persistentemente incorrecta no puede enbuclarse.
  • Limita el costo: cada reintento es otra llamada facturada y otra ronda de latencia, así que un presupuesto de reintento es parte del diseño, no una idea tardía.

Valida los valores, no solo el análisis. Esta es la historia de contención de alucinación aplicada a datos estructurados: la decodificación constreñida te da un sobre limpio y no te dice nada sobre lo que hay adentro, así que el sobre es exactamente donde los equipos dejan de validar y se queman. Validación de esquema, chequeos de rango y semántica, y una ruta de reparación limitada son las tres capas, y atrapen cosas diferentes: el esquema atrapa forma, los chequeos atrapen valores sinsentido, la ruta de reparación recupera los salvables.

JunoValida de todas formas El modo estricto borra salida malformada y deja valores incorrectos completamente intactos, así que valida contenido, no el análisis solo. Clasifica el fallo: truncamiento es un arreglo de presupuesto (más max_tokens, reintento), un valor inválido es un fallo de contenido que un reintento ciego solo reproduce, así que construye una ruta de reparación que realimente la salida incorrecta, y limita los reintentos así que una entrada incorrecta no puede enbuclarte la factura. El sobre limpio es exactamente donde la gente deja de chequear y se quema.

Dos patrones: clasificar y extraer

La mayoría del trabajo de salida estructurada es una de dos formas.

Clasificación ordena una entrada en uno de un conjunto fijo de etiquetas. Un enum en el esquema constriñe la salida exactamente a esas etiquetas, usando el mismo truco de poda de tokens: en la posición de la etiqueta, solo los valores permitidos pueden ser predichos, así que el modelo no puede inventar una categoría que no esté en tu lista.

python
# fragmento de esquema para clasificación
{"type": "string", "enum": ["billing", "technical", "general"]}

Extracción extrae campos específicos de texto libre, como el ejemplo de contacto anterior: un nombre, una fecha, un monto, una lista de nombres de producto. Juntas, clasificación y extracción cubren una gran parte del trabajo real de IA: enrutamiento de tickets de soporte, etiquetado de contenido, convertir correos en registros, leer recibos. Ambas convierten un generador de texto difuso en un componente confiable en el que tu código puede construir.

JunoDos patrones: clasificar y extraer La clasificación ordena una entrada en uno de un conjunto fijo de etiquetas, bloqueado con un enum así que el modelo no puede inventar una categoría fuera de tu lista. La extracción extrae campos nombrados de texto libre en un registro. Ambas convierten un generador de texto difuso en un componente confiable, y entre ellas cubren una gran parte de características prácticas de IA.

Casi todo lo que construyes con salida estructurada es una de dos formas, y nombrarlas te ayuda a alcanzar el esquema correcto.

Clasificación mapea una entrada a una etiqueta de un conjunto fijo. El enum lleva todo el trabajo: la decodificación constreñida enmascara cada token excepto tus etiquetas listadas en la posición de la etiqueta, así que el modelo devuelve uno de ellos o nada más.

python
# clasificación: una etiqueta de un conjunto cerrado
{"type": "string", "enum": ["billing", "technical", "general"]}

Extracción extrae campos nombrados de texto libre en un registro, los ejemplos de contacto y ticket. Los dos se componen: un esquema único puede clasificar y extraer a la vez, que es la mayoría de características reales. Los conjuntos cerrados obtienen un enum; los valores abiertos obtienen un campo tipado.

La decisión a hacer conscientemente: cuando un valor realmente es un conjunto fijo (estado, categoría, prioridad), usa un enum así que el modelo no puede derivar, y cuando es abierto (un nombre, un resumen), usa un campo tipado simple y acepta que estás confiando en el valor lo suficiente para validarlo. Mezclarlos, un enum donde el mundo real tiene más casos, o un campo libre donde un conjunto cerrado habría atrapado errores, es donde estas características silenciosamente se portan mal.

JunoDos patrones: clasificar y extraer Dos formas cubren la mayoría de esto: clasificación mapea a una etiqueta de un conjunto cerrado (el enum hace la imposición) y extracción extrae campos nombrados en un registro, y un esquema puede hacer ambos a la vez. Elige deliberadamente: enum para conjuntos fijos así que el modelo no puede derivar, campo tipado simple para valores abiertos que validarás. Un enum que le falta un caso del mundo real es una fuente silenciosa de etiquetas incorrectas.

La salida estructurada colapsa a dos patrones, y la distinción es una palanca de diseño, no solo vocabulario. Clasificación mapea entrada a una etiqueta de un conjunto cerrado, impuesto por un enum que la decodificación constreñida reduce a una elección dura entre tus valores. Extracción extrae campos tipados de texto libre. La mayoría de esquemas de producción los combinan.

python
# clasificación: la decodificación constreñida permite solo estos tokens en la posición de la etiqueta
{"type": "string", "enum": ["billing", "technical", "general"]}

La palanca es dónde dibujas el límite de conjunto cerrado, porque el enum es una garantía de exactitud con un borde afilado. Un valor que es actualmente acotado (un estado, una prioridad) pertenece en un enum así que el modelo no puede emitir nada fuera de lista, y eso convierte una generación abierta en un chequeo en el que tu código posterior puede confiar.

Pero el enum fuerza una elección incluso cuando ninguna encaja: si la entrada real no coincide con ninguna etiqueta, el modelo está constreñido a escoger la más cercana incorrecta en lugar de decirte que no encaja. Un enum sin una ruta de escape convierte "no encaja" en una etiqueta incorrecta confiada. Así que para cualquier clasificador enfrentando entrada real desordenada, añade un miembro explícito de ruta de escape, un other u unknown, y léelo como la señal para enrutar a otro lado.

La misma precaución se aplica a confianza: si una decisión depende de la etiqueta, ten el modelo también emitir un campo de confianza o rationale corto, así que una llamada limítrofe es visible en lugar de lavada en un valor enum limpio. Estos son los mismos instintos que uso de herramientas, donde el modelo escoge una herramienta de un conjunto cerrado y enfrentas el problema idéntico de "qué si ninguna encaja".

JunoDos patrones: clasificar y extraer La clasificación es un conjunto cerrado impuesto por un enum, la extracción es campos tipados de texto libre, y el enum es una garantía con un borde afilado: fuerza una escogencia incluso cuando nada encaja, lavando "no coincide" en una etiqueta incorrecta confiada. Añade un miembro other u unknown y enruta en él, y emite un campo de confianza cuando una decisión depende de la etiqueta. Misma forma que escoger una herramienta de un conjunto cerrado en uso de herramientas.

En la práctica

Convirtiendo un correo de soporte desordenado en un ticket estructurado, combinando clasificación y extracción en un esquema:

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Convierte el correo de soporte en un ticket."},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "Cobrado doble el mes pasado."}

Una llamada clasifica el correo dos formas y extrae un resumen, devolviendo un registro que tu código puede enrutar y guardar, con la forma garantizada y los valores aún vale la pena chequear. Hasta ahora todo ha fluido texto dentro y texto afuera. Después das al modelo un sentido completamente diferente: en Incrustaciones, el texto se convierte en números que puedes comparar por significado, la fundación para búsqueda y para trabajar con tus propios documentos.

JunoEn la práctica Una llamada con un esquema estricto puede clasificar un correo dos formas y extraer un resumen a la vez, devolviendo un registro que tu código puede enrutar y guardar. La forma está garantizada; los valores aún vale la pena chequear. El uso de herramientas se apoya en este mismo truco después: los argumentos que un modelo envía a una herramienta también son salida estructurada.

Una característica real usualmente combina ambos patrones en un esquema. Aquí un correo de soporte se convierte en un ticket: dos clasificaciones y una extracción en una sola llamada.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Convierte el correo de soporte en un ticket."},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "Cobrado doble el mes pasado."}

Los dos enums se constriñen duro, así que category y urgency son siempre ruteable. El summary es un campo abierto, así que es el que validar antes de confiar en él. Ese reparto, etiquetas impuestas más un campo de texto libre chequeado, es la forma de la mayoría de características de extracción. Después, Incrustaciones da al modelo un sentido diferente: texto convertido en números que puedes comparar por significado, la fundación para búsqueda y para trabajar con tus propios documentos.

JunoEn la práctica Un esquema estricto puede clasificar dos formas y extraer un resumen en una sola llamada, devolviendo un registro que puedes enrutar y guardar. Los enums se imponen así que las etiquetas son seguras para actuar en ellas; el summary abierto es el campo a validar. Esa mezcla de etiquetas bloqueadas y texto libre chequeado es la forma cotidiana de una característica de extracción.

La forma de producción cotidiana es una composición: clasificar en un par de ejes y extraer un campo de texto libre, todo en un esquema y una llamada.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Convierte el correo de soporte en un ticket."},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "Cobrado doble el mes pasado."}

La imposición es desigual entre campos, y ese es el punto de diseño. Los enums son garantías duras, así que category y urgency son seguros para enrutar en ellas sin chequeos posteriores. El summary es una cadena sin constreñir, así que es exactamente donde un valor alucinado u off-base puede esconderse detrás de un esquema limpio, y el campo que gana un pase de validación. Dos etiquetas impuestas con un campo summary abierto también es el esquema más expuesto a truncamiento: un resumen generado largo es lo que te mete en max_tokens y vuelve todo el objeto inanalizable, así que dimensiona el presupuesto para el campo de texto libre, no las etiquetas.

Esta es la misma maquinaria que uso de herramientas: una llamada de herramienta es un modelo emitiendo argumentos estructurados contra un esquema de función, constreñido de la forma idéntica, así que la validación, reparación, e instintos de ruta de escape enum se transfieren wholesale a hacer agentes confiables. De aquí, Incrustaciones cambia el modelo a un modo diferente: texto como vectores que comparas por significado, la base para búsqueda y para basar respuestas en tus propios documentos.

JunoEn la práctica Un esquema estricto, dos enums impuestos y un `summary` abierto: las etiquetas son seguras para enrutar en ellas, el campo de texto libre es donde un valor alucinado se esconde y donde una generación larga tropieza en `max_tokens` en truncamiento, así que presupuesta y valida específicamente. Esta es la misma maquinaria que uso de herramientas, args constreñidos contra un esquema de función, así que cada hábito aquí, validación, reparación, rutas de escape enum, se lleva directo en agentes.