Validación de salida estructurada de LLM en Python que es sólida
Deja de interpretar sensaciones. Valida contratos.
La mayoría de los tutoriales sobre “salida estructurada” de los LLM son poco serios. Te enseñan a pedir JSON amablemente y luego a esperar que el modelo se comporte. Eso no es validación. Eso es optimismo con llaves.
La propia documentación de OpenAI hace explícita la distinción. El modo JSON te da JSON válido, mientras que las Salidas Estructuradas (Structured Outputs) exigen el cumplimiento del esquema, y OpenAI recomienda usar las Salidas Estructuradas en lugar del modo JSON siempre que sea posible.

Eso no hace que la carga de datos sea digna de confianza. El esquema JSON define la estructura y los valores permitidos, Pydantic te ofrece validación tipificada en Python, y OpenAI señala explícitamente que una respuesta válida según el esquema aún puede contener valores incorrectos. Además de eso, los rechazos y las salidas incompletas pueden eludir la forma que esperabas. En producción, la validación de salidas estructuradas es un flujo de trabajo, no un interruptor. Esta misma frontera también debe vivir dentro del contexto más amplio de rendimiento, reintentos y límites del programador en el hub de ingeniería de rendimiento de LLM.
La validación de salida estructurada es un contrato
La validación de salida estructurada para los LLM significa que defines la forma de la respuesta de antemano, restringes al modelo para que produzca esa forma cuando sea posible y luego validas el resultado nuevamente antes de que tu aplicación confíe en él. En términos prácticos, eso significa verificar campos obligatorios, tipos, enumeraciones, formas de objetos cerradas y reglas de dominio antes de que la carga de datos toque tu base de datos, interfaz de usuario, cola o servicio aguas abajo. El esquema JSON existe exactamente para este tipo de validación estructural, Pydantic está diseñado para validar datos no confiables contra las pistas de tipo de Python, y la biblioteca jsonschema de Python te da una forma directa de validar una instancia contra un esquema.
También existe una clara división entre dos casos de uso comunes. Si el modelo debe responder al usuario en un formato estructurado, usa un formato de respuesta estructurado. Si el modelo debe llamar a las herramientas o funciones de tu aplicación, usa la llamada de funciones. La documentación de OpenAI detalla esa distinción y, para la llamada de funciones, recomiendan habilitar strict: true para que los argumentos cumplan de manera fiable con el esquema de la función.
Mi opinión firme es simple. Trata cada respuesta estructurada de un LLM como un límite de API. Una vez que empieces a pensar en términos de contratos en lugar de indicaciones (prompts), la arquitectura se vuelve más limpia, los errores son más baratos y el problema de “por qué el modelo inventó un nuevo campo en producción” desaparece en gran medida. Esa es la verdadera respuesta a “¿qué es la validación de salida estructurada para LLMs” y es una respuesta mucho mejor que “pedirle amablemente al modelo que te dé JSON”.
El modo JSON no es validación
Si recuerdas solo una cosa de este artículo, que sea esta: el modo JSON no es validación de esquema. El Centro de Ayuda de OpenAI dice que el modo JSON no garantizará que la salida coincida con ningún esquema específico, solo que es JSON válido y se analiza sin errores. La guía de Salidas Estructuradas dice lo mismo de una manera más clara. Tanto el modo JSON como las Salidas Estructuradas pueden producir JSON válido, pero solo las Salidas Estructuradas exigen el cumplimiento del esquema.
Esa diferencia importa más de lo que la gente admite. En su publicación de lanzamiento de Salidas Estructuradas, OpenAI informó que gpt-4o-2024-08-06 con Salidas Estructuradas obtuvo el 100 por ciento en sus evaluaciones de esquemas JSON complejos, mientras que gpt-4-0613 obtuvo menos del 40 por ciento. No necesitas tratar esos números como una verdad universal para ver el punto más amplio. El cumplimiento del esquema cambia la superficie de fallo de “puede pasar cualquier cosa” a “el contrato es mucho más estricto”.
Aún existen casos extremos y pretender lo contrario es cómo las demostraciones de juguete se convierten en deberes de guardia de pagers. OpenAI documenta que el modelo puede rechazar una solicitud no segura y esos rechazos se muestran fuera de tu ruta de esquema normal. También documenta respuestas incompletas, incluyendo casos como alcanzar max_output_tokens o una interrupción del filtro de contenido. Por lo tanto, la pregunta frecuente “¿es el modo JSON suficiente para una salida de LLM fiable?” tiene una respuesta corta y una larga. La respuesta corta es no. La respuesta larga es que incluso una salida estructurada estricta necesita manejo de errores explícito.
Dónde sigue rompiéndose la salida estructurada
El cumplimiento del esquema reduce el problema. No lo elimina. En el tráfico real, aún ves cargas de datos rotas o sorprendentes por razones que tienen poco que ver con la redacción de tu indicación.
Formas de fallo para las que vale la pena diseñar
Los modelos y los clientes discrepan sobre detalles. Puedes obtener prosa extra antes o después del JSON, bloques delimitados de Markdown alrededor de la carga de datos o una llamada de herramienta cuyo nombre es válido pero cuyos argumentos son JSON que no coincide con tu modelo de Pydantic. La transmisión (streaming) lo empeora porque podrías validar un búfer a medio terminar. El código defensivo debería asumir “entrada de texto, quizás JSON dentro” en lugar de “los bytes en la red ya coinciden con mi modelo”.
Diferencias entre proveedores y APIs
No todos los hosts exponen la misma superficie de salida estructurada. Un stack podría darte una completación vinculada al esquema de primera clase, otro podría solo garantizar la sintaxis JSON y los tiempos de ejecución locales podrían ir rezagados respecto a las APIs alojadas. Esa es una razón por la que la pregunta frecuente “¿cómo validas el JSON de LLM en Python?” comienza con la imposición del proveedor cuando existe y aún termina con la validación en el lado de Python. Para una visión más amplia de cómo se comparan los proveedores, consulta la comparación de salida estructurada entre proveedores populares de LLM. Si ejecutas modelos localmente, el mismo flujo de validación aplica después de normalizar el formato de red, por ejemplo después de la extracción con Ollama como en salida estructurada de LLM con Ollama en Python y Go. Cuando un tiempo de ejecución aún envuelve JSON con prefijos extraños o trazas de razonamiento, espera la misma clase de fallos del analizador descritos en problemas de salida estructurada de Ollama GPT-OSS.
El stack de Python que realmente funciona
Mi recomendación es aburrida a propósito. Primero, deja que el proveedor del modelo imponga el contrato estructural cuando pueda. Segundo, valida la carga de datos devuelta en Python con Pydantic. Tercero, usa validación explícita de reglas de negocio para hechos que un esquema por sí solo no puede probar. Cuarto, prueba el contrato con fixtures y ejemplos adversarios en lugar de señalar una captura de pantalla de un playground y decir que está listo. La documentación de Salidas Estructuradas de OpenAI, el modelo de validador de Pydantic, las herramientas de jsonschema de Python y los propios ejemplos de evaluación de salida estructurada de OpenAI apuntan todos en esa dirección.
Pydantic es el centro de gravedad adecuado para Python. Te permite modelar la salida como tipos normales de Python, generar un esquema JSON con model_json_schema() y validar JSON crudo con model_validate_json(). La documentación de Pydantic también señala que model_validate_json() es generalmente el mejor camino que hacer json.loads(...) primero y luego validar, porque esa ruta de dos pasos agrega trabajo de análisis extra en Python.
Si mantienes archivos de esquema independientes en tu repositorio, o quieres que CI valide las cargas de datos de los fixtures independientemente del código del modelo, el paquete jsonschema de Python te da la verificación de contrato más simple posible con jsonschema.validate(...). Si quieres eso en pre-commit, check-jsonschema existe específicamente como una CLI y gancho de pre-commit construido sobre jsonschema. Eso es un ajuste muy bueno para equipos que quieren que los cambios de esquema sean revisados como cambios de código.
Los frameworks pueden reducir el cableado, pero no eliminan la necesidad de validación real. LangChain ahora selecciona automáticamente la salida estructurada nativa del proveedor cuando el proveedor lo soporta y recurre a una estrategia de herramienta de lo contrario. Instructor añade modelos de respuesta de Pydantic, validación, reintentos y soporte multi-proveedor sobre las llamadas al modelo. Guardrails se enfoca en validadores y capas de seguridad de entrada-salida. Herramientas útiles, todas ellas. Pero el esquema y las reglas de negocio aún te pertenecen. Si estás eligiendo entre bibliotecas de nivel superior, la comparación de BAML vs Instructor para Python es un compañero útil para este artículo.
Un ejemplo minimalista de OpenAI y Pydantic
El ejemplo más pequeño digno de producción tiene algunos elementos innegociables. Usa un conjunto cerrado de valores tipo enum cuando sea posible. Prohibe claves extra. Añade descripciones de campo para que el esquema sea comprensible para humanos y más legible para el modelo. Mantén el objeto raíz explícito y aburrido. OpenAI recomienda nombres claros más títulos y descripciones para claves importantes, el esquema JSON usa enum para restringir valores y Pydantic puede cerrar la forma del objeto con extra="forbid".
from typing import Literal
from openai import OpenAI
from pydantic import BaseModel, ConfigDict, Field
class TicketClassification(BaseModel):
model_config = ConfigDict(extra="forbid")
category: Literal["billing", "bug", "how_to", "abuse"] = Field(
description="Categoría del ticket de soporte."
)
priority: Literal["low", "medium", "high"] = Field(
description="Urgencia operativa."
)
needs_human: bool = Field(
description="Si un humano debe revisar el caso."
)
summary: str = Field(
description="Un resumen de una oración del problema."
)
client = OpenAI()
response = client.responses.parse(
model="gpt-4o-2024-08-06",
input=[
{
"role": "system",
"content": "Clasifica los tickets de soporte. Devuelve solo el resultado estructurado.",
},
{
"role": "user",
"content": "El cliente informa de cargos duplicados después de actualizar el checkout.",
},
],
text_format=TicketClassification,
)
result = response.output_parsed
print(result.model_dump())
Dos detalles en ese ejemplo son fáciles de pasar por alto y absolutamente vale la pena cuidarlos. extra="forbid" en el lado de Pydantic refleja la idea de JSON Schema de additionalProperties: false, lo cual también es un requisito para esquemas de herramientas estrictos en la documentación de llamadas de función de OpenAI. Y los enums no son cosméticos. Son una de las formas más simples de evitar que el modelo invente un valor que tu código no entiende.
El SDK de Python de OpenAI soporta client.responses.parse(...) con un modelo de Pydantic suministrado como text_format, y el objeto analizado se devuelve en response.output_parsed. El mismo SDK también soporta client.chat.completions.parse(...), donde el objeto analizado vive en message.parsed. Si quieres extracción de datos estructurados directa con un pegamento mínimo, esas ayudas son el punto de partida más limpio.
Analizar, normalizar y luego validar
Las Salidas Estructuradas y model_validate_json eliminan mucho del dolor de análisis cuando el stack está alineado de extremo a extremo. El momento en que soportas un proveedor que devuelve texto de chat plano, un modelo que envuelve JSON en vallas (fences) o una ruta de registro que almacena la cadena de completación cruda, quieres un punto de estrangulamiento que convierta texto en un diccionario antes de que Pydantic se ejecute.
import json
def parse_json_from_llm_text(text: str) -> dict:
cleaned = text.strip()
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[1]
cleaned = cleaned.rsplit("```", 1)[0].strip()
# Prefijo común "Claro, aquí está el JSON:" antes del objeto.
if not cleaned.startswith("{") and "{" in cleaned and "}" in cleaned:
start = cleaned.find("{")
end = cleaned.rfind("}")
if end > start:
cleaned = cleaned[start : end + 1]
return json.loads(cleaned)
ticket_dict = parse_json_from_llm_text(raw_completion_text)
ticket = TicketClassification.model_validate(ticket_dict)
Esa ayuda es intencionalmente aburrida. Maneja bloques delimitados “json ... ” y un preludio de lenguaje natural inicial cuando la carga de datos es aún un objeto de nivel superior único. No es un extractor JSON completo. Si el modelo anida llaves dentro de valores de cadena, la segmentación ingenua puede romperlo, y la corrección correcta suele ser indicaciones más estrictas, completaciones vinculadas al esquema o una biblioteca de análisis dedicada.
Completaciones en streaming
Si transmites tokens de chat, no ejecutes json.loads ni model_validate_json en cada delta. Almacena en búfer hasta que la API informe de un mensaje terminado (verifica tu cliente por la terminación del stream o finish_reason), concatena el texto y luego analiza una vez. La misma regla aplica cuando los argumentos de llamada de herramienta llegan en fragmentos. Solo validas después de que la cadena de argumentos esté completa.
chunks: list[str] = []
for chunk in completion_stream:
delta = chunk.choices[0].delta.content or ""
chunks.append(delta)
raw_completion_text = "".join(chunks)
ticket = TicketClassification.model_validate_json(raw_completion_text)
Aún puedes pasar raw_completion_text a través de parse_json_from_llm_text primero cuando esperas vallas o charla alrededor del JSON.
Una vez que tienes el análisis de cadenas planas, la siguiente restricción a menudo no es Python, sino el dialecto de esquema JSON del proveedor y lo que la API remota acepta realmente.
Límites del esquema del proveedor (antes de ser ingenioso en Python)
No descargues ciegamente cualquier salida de generador de esquemas en una API y asumas que cada característica de JSON Schema es soportada. OpenAI soporta un subconjunto de JSON Schema, requiere que todos los campos sean obligatorios para las Salidas Estructuradas, requiere que la raíz sea un objeto en lugar de un anyOf de nivel superior y documenta límites en la profundidad de anidamiento y el recuento total de propiedades. Mantén el esquema orientado al proveedor simple. Eso no es una concesión. Eso es buena ingeniería.
Si necesitas una ruta de validación agnóstica al proveedor, o quieres validar fixtures y mocks almacenados, Pydantic más jsonschema sigue siendo una gran combinación.
from jsonschema import validate as validate_json
schema = TicketClassification.model_json_schema()
payload = {
"category": "bug",
"priority": "high",
"needs_human": True,
"summary": "El checkout duplica los cargos después de la actualización.",
}
validate_json(instance=payload, schema=schema)
ticket = TicketClassification.model_validate(payload)
print(ticket)
Ese patrón es especialmente útil en pruebas, fixtures de contrato e integraciones donde el proveedor del modelo no ofrece cumplimiento nativo de salida estructurada. Solo recuerda que un esquema generado localmente puede ser más amplio que el subconjunto soportado por un proveedor dado, por lo que “válido localmente” no significa automáticamente “aceptado por cada API de LLM”. También ten en cuenta que algunos proveedores preprocesan y almacenan en caché artefactos de esquema, por lo que la primera solicitud para un nuevo esquema puede ser más lenta que las solicitudes cálidas.
Las llamadas de herramienta son un segundo contrato
La llamada de función o herramienta es la otra forma principal de salida estructurada. El modelo elige un nombre y pasa argumentos que deberían coincidir con un esquema JSON que controlas. OpenAI recomienda strict: true en las definiciones de herramienta para que los argumentos permanezcan alineados con ese esquema. En stacks pesados en agentes, el muestreo malo se convierte en JSON de herramienta inválido rápido; mantén los ajustes del muestreador alineados con el trabajo de múltiples pasos usando la referencia de parámetros de inferencia agéntica para Qwen y Gemma.
Los fragmentos a continuación asumen que ya mapeaste el objeto de llamada de herramienta del proveedor en una cadena name y un diccionario arguments, por ejemplo analizando tool_calls[].function en completaciones de chat (los argumentos de cadena JSON se convierten en json.loads primero). dispatch_tool es el paso después de esa normalización.
Dos reglas prácticas ayudan en Python. Primero, valida el nombre de la herramienta contra una lista blanca explícita antes de enrutar la ejecución. Segundo, valida el diccionario de argumentos con el mismo modelo de Pydantic que usas en pruebas, no con acceso de clave ad hoc. El modo de fallo que estás evitando es “argumentos JSON válidos, forma incorrecta para la herramienta que se activó”, lo cual se escapa de los controles de cadena.
from typing import Any, Callable
from pydantic import BaseModel
ToolHandler = Callable[[dict[str, Any]], str]
def dispatch_tool(
*,
name: str,
arguments: dict[str, Any],
handlers: dict[str, tuple[type[BaseModel], ToolHandler]],
) -> str:
if name not in handlers:
raise ValueError(f"herramienta no soportada {name}")
model_cls, handler = handlers[name]
validated = model_cls.model_validate(arguments)
return handler(validated.model_dump())
handlers: dict[str, tuple[type[BaseModel], ToolHandler]] = {
"classify_ticket": (
TicketClassification,
lambda data: f"encolado como {data['category']}",
),
}
Ese patrón mantiene el enrutamiento y la validación en un solo lugar. Tus manejadores reales serán más ricos, pero la división debería permanecer igual: nombres permitidos, argumentos tipificados y luego efectos secundarios.
La validación de esquema aún necesita reglas de negocio
Un objeto válido no es lo mismo que un objeto correcto. OpenAI lo dice directamente. Las Salidas Estructuradas no previenen errores dentro de los valores del objeto JSON. Por eso la pregunta frecuente “¿por qué la validación de esquema y la validación de reglas de negocio importan ambas?” tiene una respuesta directa. Porque una respuesta puede coincidir perfectamente con el esquema y aún estar equivocada de una manera que dañe al negocio.
Aquí hay un ejemplo realista. La estructura puede ser válida, pero la lógica de precios aún puede ser un sinsentido.
from decimal import Decimal
from typing import Literal
from typing_extensions import Self
from pydantic import BaseModel, ConfigDict, Field, model_validator
class Offer(BaseModel):
model_config = ConfigDict(extra="forbid")
currency: Literal["USD", "EUR", "GBP"]
amount: Decimal = Field(gt=0)
original_amount: Decimal | None
discounted: bool
@model_validator(mode="after")
def check_discount_logic(self) -> Self:
if self.discounted:
if self.original_amount is None:
raise ValueError(
"original_amount es requerido cuando discounted es verdadero"
)
if self.original_amount <= self.amount:
raise ValueError(
"original_amount debe ser mayor que amount"
)
return self
Ese validador hace algo que los esquemas a menudo hacen mal en sistemas reales. Verifica semánticas entre campos después de que todo el modelo ha sido analizado. model_validator de Pydantic existe exactamente para este tipo de validación de objeto completo. Nota el campo Decimal | None sin un valor por defecto. Eso mantiene el campo presente mientras aún permite null, lo cual coincide con el patrón documentado de OpenAI para valores tipo opcional bajo Salidas Estructuradas estrictas.
Si quieres que los fallos de validación se retroalimenten automáticamente al modelo, Instructor es una capa práctica sobre Pydantic. Su documentación describe un bucle de reintento donde los errores de validación se capturan, formatean como retroalimentación y se usan para pedirle al modelo que intente de nuevo.
import instructor
retrying_client = instructor.from_provider("openai/gpt-4o", max_retries=2)
offer = retrying_client.create(
response_model=Offer,
messages=[
{
"role": "user",
"content": (
"Extrae la oferta de este texto. "
"Era 49.00 USD, ahora 19.00 USD."
),
}
],
)
Esta es una de las pocas comodidades que recomendaré felizmente. Los reintentos automáticos vinculados a errores de validación reales son útiles. La coerción silenciosa no lo es. La capa de modelo de Instructor, la documentación de reintentos y la documentación de validación se inclinan todas hacia esa misma idea, y tienen razón al hacerlo.
Puedes implementar la misma idea sin un framework. El bucle es pequeño. Pide al modelo, valida con Pydantic y, si la validación falla, envía los detalles del error de vuelta en un mensaje de usuario de seguimiento y pide solo JSON corregido. Limita los intentos, registra el fallo final y muestra un error controlado a los llamadores. Cuando ya confías en responses.parse u otras ayudas vinculadas al esquema, es posible que rara vez ejercites esta ruta. Aún importa para el modo JSON, puntos finales de chat más antiguos o cualquier pasarela que te entregue una cadena cruda.
from openai import OpenAI
from pydantic import ValidationError
client = OpenAI()
messages = [
{"role": "system", "content": "Devuelve solo JSON que coincida con el esquema del ticket."},
{"role": "user", "content": "El cliente informa de cargos duplicados después de actualizar el checkout."},
]
ticket: TicketClassification | None = None
for attempt in range(2):
completion = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=messages,
response_format={"type": "json_object"},
)
raw_text = completion.choices[0].message.content or ""
try:
ticket = TicketClassification.model_validate_json(raw_text)
break
except ValidationError as exc:
messages.append(
{
"role": "user",
"content": f"La validación falló con {exc.errors()}. Devuelve solo JSON corregido.",
}
)
else:
raise RuntimeError("agotados los reintentos de salida estructurada")
assert ticket is not None
En servicios reales, adjuntarías IDs de trazado, redactarías el texto del cliente en los registros y distinguirías errores de validación recuperables de rechazos o respuestas incompletas. La parte importante es que el reintento es impulsado por la salida real del validador, no por un mensaje genérico de “intenta de nuevo”.
Prueba, reintenta y falla cerrado
¿Qué debería pasar cuando la validación de LLM falla? No un encogimiento de hombros. Rechaza la carga de datos, registra el fallo, reintenta con intentos acotados si la tarea vale la pena reintentar y falla cerrado en lugar de normalizar basura en algo que solo parece aceptable. Este también es el lugar donde muchos equipos olvidan manejar rechazos y salidas incompletas explícitamente, aunque la documentación del proveedor les dice que esas rutas existen.
Para la API de Respuestas de OpenAI, el manejo de errores debería ser código de primera clase, no una reflexión tardía. La variable es response de client.responses.create o parse, no completion de streaming de chat en otra parte de este artículo.
if response.status == "incomplete":
raise RuntimeError(response.incomplete_details.reason)
content = response.output[0].content[0]
if content.type == "refusal":
raise RuntimeError(content.refusal)
Eso no es sobreingeniería defensiva. Está directamente alineado con los modos de fallo documentados. Si el modelo rechaza, no tienes una carga de datos válida según el esquema. Si la respuesta es incompleta, no tienes una carga de datos válida según el esquema. Trata ambas como ramas explícitas en tu flujo de control.
También deberías probar el contrato fuera de la llamada al modelo en sí.
import pytest
from jsonschema import validate as validate_json
from pydantic import ValidationError
def test_ticket_fixture_matches_schema():
payload = {
"category": "bug",
"priority": "high",
"needs_human": True,
"summary": "El checkout duplica los cargos después de la actualización.",
}
validate_json(instance=payload, schema=TicketClassification.model_json_schema())
def test_discount_logic_rejects_broken_offer():
with pytest.raises(ValidationError):
Offer.model_validate(
{
"currency": "USD",
"amount": "19.00",
"original_amount": "10.00",
"discounted": True,
}
)
def test_ticket_rejects_unknown_category_string():
with pytest.raises(ValidationError):
TicketClassification.model_validate(
{
"category": "refund",
"priority": "high",
"needs_human": True,
"summary": "El cliente quiere un reembolso.",
}
)
def test_ticket_rejects_extra_keys():
with pytest.raises(ValidationError):
TicketClassification.model_validate(
{
"category": "bug",
"priority": "high",
"needs_human": True,
"summary": "Flujo roto.",
"severity": "critical",
}
)
Esta es la forma correcta de estrategia de prueba para la validación de salida de LLM en Python. Valida fixtures dorados con jsonschema para que cada campo en el contrato sea ejercitado. Valida semánticas con Pydantic, luego añade casos adversarios como cadenas de enum ilegales, claves extra prohibidas y contradicciones entre campos que te importan. Si tomas instantáneas de salidas reales del modelo, limpia los datos personales identificables (PII) y trátalos como fixtures de regresión.
Si tu equipo vive en el stack de OpenAI, la API de Evals también incluye recetas de evaluación de salida estructurada específicamente para probar e iterar en tareas que dependen de formatos legibles por máquina. Y si mantienes archivos de esquema crudos en el repositorio, conecta check-jsonschema en CI o pre-commit. Envía contratos, no vibras.
Comprobaciones de producción que te salvan más tarde
Cuando la validación falla, la respuesta de la pregunta frecuente es directa. Rechaza la carga de datos, registra por qué, reintenta con retroalimentación dirigida cuando la tarea vale otro intento y falla cerrado en lugar de coaccionar datos malos en una cola.
Una lista de verificación operativa corta ayuda a los equipos a evitar incidentes repetidos.
- Registra la versión del esquema o un hash del esquema JSON que enviaste al proveedor para que puedas reproducir los fallos con precisión.
- Redacta las entradas y salidas del modelo en los registros. Los registros estructurados son inútiles si filtran texto del cliente.
- Emite contadores o métricas para la tasa de rechazo, tasa de respuesta incompleta, tasa de fallo de validación y tasa de éxito de reparación. Los picos allí superan adivinar cuándo se desplegó un cambio de modelo o indicación.
La guía más amplia de observabilidad para sistemas de LLM ayuda a conectar esas señales en paneles, trazas y revisiones de SLO una vez que existen los contadores.
La mejor práctica no es complicada. Usa Salidas Estructuradas del lado del proveedor o esquemas de herramienta estrictos cuando puedas. Normaliza texto crudo cuando debas. Refleja el contrato en Python con Pydantic. Añade validación de reglas de negocio para lo que el esquema no puede probar. Maneja rechazos y respuestas incompletas como ramas normales. Prueba el contrato hasta que deje de ser una demostración y empiece a ser software. Cualquier cosa menos es solo cosplay de ingeniería de indicaciones.