La transición de «chatbot basado en un modelo de lenguaje de gran tamaño (LLM)» a «sistema basado en LLM que ejecuta tareas» ocurre en la capa de salidas estructuradas y llamadas a funciones. Aquí, el LLM deja de producir únicamente prosa y empieza a generar datos, tomar decisiones e integrarse con el resto de la infraestructura.
En producción, «salida estructurada» no significa «el modo JSON funcionó una vez en mi prueba». Significa disponer de un pipeline robusto que gestione la variabilidad del modelo, las respuestas malformadas, los fallos parciales, la evolución de los esquemas y la realidad de unos LLM que no siempre siguen las instrucciones.
Este artículo explica los patrones que funcionan en producción. Se presupone que conoces los fundamentos —has utilizado tool_choice de OpenAI, el uso de herramientas de Anthropic y las restricciones de JSON Schema—. Aquí vamos más allá para lograr sistemas fiables.
Los dos modos
Dos capacidades relacionadas pero distintas:
Salida estructurada: el LLM genera una respuesta que se ajusta a un esquema, normalmente JSON. Se utiliza cuando necesitas procesarla mediante código.
Llamada a una función o herramienta: el LLM recibe un conjunto de funciones, decide cuál invocar —si corresponde— y genera sus argumentos. El sistema anfitrión ejecuta la función y devuelve el resultado. A continuación, el LLM puede llamar a otras funciones o generar una respuesta final.
Normalmente, las API de los modelos exponen estas capacidades mediante:
- Un parámetro
response_format—o equivalente— que recibe un esquema JSON para salidas estructuradas sencillas, como Structured Outputs de OpenAI oresponseSchemade Google Gemini. - Un array
toolsque describe las funciones disponibles y una respuesta de llamada a herramienta cuando se invoca. La API de uso de herramientas de Anthropic también permite restringir la respuesta a un esquema: defines una sola herramienta con el esquema deseado y obligas al modelo a utilizarla.
Ambos mecanismos funcionan y están relacionados. Una «llamada a función» es, en esencia, una salida estructurada cuyo esquema corresponde a la firma de la función.
Patrón 1: Esquemas estrictos y explícitos
La mayor mejora de fiabilidad procede de los esquemas.
Un esquema poco restrictivo:
{
"type": "object",
"properties": {
"category": { "type": "string" },
"priority": { "type": "string" }
}
}
Un esquema estricto:
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "feature_request", "complaint"],
"description": "The ticket category. Use 'technical' for product bugs and 'account' for login/password issues."
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "urgent"],
"description": "Use 'urgent' only for outages or business-critical impact. 'high' for blocking issues on important customers. 'medium' for standard impact. 'low' for nice-to-haves."
}
},
"required": ["category", "priority"],
"additionalProperties": false
}
La versión estricta:
- Restringe los valores a enumeraciones conocidas (sin deriva libre).
- Incluye descripciones que actúan como prompts en línea (el modelo las usa).
- Requiere campos (así no obtienes respuestas parciales).
- Prohíbe propiedades adicionales, por lo que no aparecen claves alucinadas.
En producción, cada campo del esquema debe tener una descripción. Cada enumeración debe ser explícita. Cada campo requerido debe marcarse. Esto es “esquema como prompt” — tu esquema está haciendo ingeniería de prompts.
Patrón 2: Generación restringida
Los principales proveedores ya admiten generación restringida: el proceso de decodificación obliga al modelo a producir únicamente respuestas válidas.
- OpenAI:
response_format: { type: "json_schema", json_schema: { ..., strict: true } } - Anthropic: Herramientas con esquemas estrictos.
- Open-source:
outlines,lm-format-enforcer,jsonformer, decodificación basada en gramática de vLLM.
Utiliza estas opciones siempre. Eliminan toda una clase de fallos —JSON malformado, campos alucinados y propiedades obligatorias ausentes— con una sobrecarga de rendimiento insignificante.
Cuando la generación restringida no esté disponible —en algunos modelos abiertos o configuraciones—, utiliza validación y reintentos (consulta el Patrón 4).
Patrón 3: Versionado de esquemas
Los esquemas evolucionan. Añades campos. Dejas de usar campos. Cambias enumeraciones.
Un cambio de esquema es un cambio de código. Debe ser:
- Versionarse. Etiqueta cada esquema con un número de versión.
- Probado. El conjunto de pruebas se ejecuta contra el nuevo esquema antes del despliegue.
- Comunicarse. Los consumidores posteriores conocen el cambio.
- Compatible hacia atrás cuando sea posible. Añade nuevos campos opcionales; no elimines campos requeridos.
Una pauta eficaz consiste en almacenar los esquemas como tipos de TypeScript o modelos de Pydantic, versionarlos en el control de código fuente y generar JSON Schema a partir de ellos. Los tipos sirven tanto para la API del modelo como para el código de la aplicación.
class TicketClassificationV2(BaseModel):
category: Literal["billing", "technical", "account", "feature_request", "complaint"]
priority: Literal["low", "medium", "high", "urgent"]
confidence: float = Field(ge=0, le=1, description="Confidence in this classification, 0-1")
needs_human_review: bool = Field(description="True if any field has low confidence or unusual signal")
reasoning: str = Field(description="Brief reasoning for the classification, especially for non-obvious cases")
Un modelo de Pydantic define el esquema, valida la respuesta y actúa como tipo en el código Python: una única fuente de verdad.
Patrón 4: Validación y reintento
Incluso con generación restringida, valida la respuesta antes de utilizarla:
from pydantic import ValidationError
def call_with_validation(prompt, schema, max_retries=2):
for attempt in range(max_retries + 1):
response = llm_call(prompt, response_format=schema)
try:
parsed = schema.model_validate_json(response.content)
return parsed
except ValidationError as e:
if attempt < max_retries:
prompt = build_retry_prompt(prompt, response.content, e)
continue
raise
El prompt de reintento debe incluir las instrucciones originales, la respuesta anterior del modelo y una descripción concreta del error:
Your previous response had a validation error:
{error message}
Your previous output:
{previous output}
Please correct the issue and produce a valid response.
Los reintentos funcionan sorprendentemente bien: normalmente basta uno para recuperarse de un error del modelo.
Límites: no reintentes indefinidamente —máximo 2-3 veces—. No reintentes ante errores que no sean de validación, como límites de uso o filtros de contenido. Registra los reintentos y supervisa su tasa; un aumento indica deriva del modelo o problemas en el prompt.
Patrón 5: Revisión de los resultados
En llamadas a funciones de alto riesgo, pide al modelo que revise el resultado de la herramienta antes de utilizarlo.
Un bucle simple:
1. Call LLM with tools available.
2. Model decides to call tool X.
3. Execute X.
4. Pass result back to model.
5. Model produces final response.
Un bucle de revisión:
1. Call LLM with tools available.
2. Model decides to call tool X.
3. Execute X.
4. Pass result back to model.
5. Model evaluates: does this result match what I expected? Should I act on it?
6. If yes, model produces final response. If no, model calls another tool or asks for clarification.
Esto detecta casos como:
- La herramienta devolvió 0 resultados cuando debería haber encontrado datos → el modelo reconoce el caso vacío.
- La herramienta devolvió un error → el modelo lo gestiona explícitamente en lugar de ignorarlo.
- La herramienta devolvió datos inesperados → el modelo lo nota y se adapta.
Implementación: pide al modelo que evalúe explícitamente los resultados, por ejemplo mediante un patrón estructurado de «evalúa y después actúa».
Esto añade latencia y tokens. Para acciones de alto riesgo (envío de correo, procesamiento de pago, modificación de registros) vale la pena. Para recuperación de información de bajo riesgo, salta este paso.
Patrón 6: Idempotencia
Los LLM a veces invocan dos veces la misma herramienta o repiten llamadas que ya tuvieron éxito. Sin idempotencia aparecen duplicados: dos reembolsos, dos correos enviados o dos registros creados.
Patrones para idempotencia:
Claves de idempotencia. Cada llamada a una herramienta recibe una clave única, generada por el cliente e incluida en la solicitud. La API posterior o la capa que envuelve la herramienta utiliza esa clave para detectar duplicados y devolver el resultado existente.
Semántica get-or-create. Las herramientas que crean registros realizan antes una búsqueda. «Crear cliente con el correo X» comprueba primero si ya existe y, si es así, devuelve el registro existente en lugar de duplicarlo.
Registros de operaciones. Las herramientas registran cada operación. La capa envolvente consulta ese registro antes de ejecutar y, si la operación ya se realizó, devuelve el resultado almacenado.
Diseño conservador de herramientas. Las herramientas que realizan acciones con consecuencias se diseñan para requerir confirmación explícita o aprobación humana. El LLM no puede activarlas accidentalmente en un bucle cerrado.
Para cualquier herramienta que tenga efectos secundarios, diseña para idempotencia. Saltar esto es una fuente principal de errores en producción.
Patrón 7: Observabilidad de llamadas a herramientas
Necesitas saber qué está pasando con las llamadas a herramientas. Para cada llamada, registra:
- Marca de tiempo.
- Nombre de la herramienta y argumentos.
- Resultado (o error).
- Duración.
- El usuario/sesión a la que pertenece.
- La cadena de llamadas en esta ronda (¿esta llamada a herramienta fue parte de una cadena más larga?).
Crea paneles con estos datos. Entre las vistas habituales están:
- Volumen de llamadas a herramientas por herramienta.
- Tasa de error por herramienta.
- Duración promedio por herramienta.
- Patrones de secuencias de herramientas (“¿qué herramientas tienden a llamarse juntas?”).
- Llamadas a herramientas alucinadas (el LLM intentó llamar a una herramienta que no existe).
Estas vistas revelan dónde falla el sistema y dónde concentra sus costes.
Patrón 8: Argumentos alucinados
Los LLM a veces inventan valores para los parámetros de las herramientas. Pueden invocar search_customers(email="...") con un correo que no corresponde a la solicitud real del usuario o book_meeting(date="...") con una fecha que nunca se mencionó.
Mitigación:
Esquema estricto con descripciones. “El user_id debe ser uno mencionado anteriormente en la conversación. No inventes IDs.”
Validación en la capa de la herramienta. Si un valor no es plausible —por ejemplo, user_id no existe o la fecha está en el pasado—, la herramienta devuelve un error estructurado para que el modelo reconsidere la acción.
Reflexión. “Antes de llamar a esta herramienta, confirma que los valores que estás usando están fundamentados en la conversación.”
Descripciones de herramientas restringidas. Las herramientas que operan sobre entidades específicas solo exponen IDs de entidades recuperados anteriormente en la conversación. No expongas búsqueda cruda.
Registros de auditoría. Detecta patrones de argumentos alucinados y ajusta prompts/esquemas.
Patrón 9: Degradación controlada
Las herramientas fallan, las API sufren interrupciones y se alcanzan límites de uso. La respuesta adecuada rara vez consiste en decir al usuario que nada funciona.
Patrones:
Datos en caché o desactualizados. Si la fuente en tiempo real no está disponible, devuelve los datos almacenados e indica que pueden estar desactualizados.
Finalización parcial. Si 3 de 5 subtareas se completan, informa con claridad de lo que se hizo y lo que quedó pendiente.
Rutas de respaldo. Si la herramienta principal falla, el modelo conoce una ruta de respaldo. Por ejemplo, si “search_documents” falla, recurre a “search_web” con advertencias adecuadas.
Estados de error visibles para el usuario. Si una herramienta no puede completar la tarea, el modelo debe mostrar un mensaje claro, no fingir que la operación tuvo éxito.
El modelo debe conocer estos patrones. Documenta en el prompt del sistema:
If a tool returns an error:
- Try the alternate tool if one exists.
- Report partial results clearly if the user has already provided information.
- Never claim success when a tool returned an error.
Patrón 10: Transmisión de salidas estructuradas
Para la UX, transmitir respuestas estructuradas parciales es muy útil: el usuario ve cómo se forman los resultados en tiempo real.
Implementación:
- La mayoría de las API modernas transmiten la salida JSON token a token.
- Analiza el JSON parcial de forma incremental mediante bibliotecas como
partial-json-parsero un pequeño parser de streaming. - Actualiza la UI a medida que llegan los campos.
Funciona especialmente bien en respuestas con varias secciones. Una descripción larga de producto, un análisis con varias conclusiones o una revisión de código con múltiples hallazgos resultan mucho más ágiles cuando se transmiten.
Advertencia: no tomes decisiones a partir de respuestas parciales. Transmítelas para mostrarlas, pero espera a que finalicen antes de actuar sobre el resultado estructurado.
Patrón 11: Llamadas a funciones frente a llamadas explícitas de decisión
La llamada nativa a funciones es cómoda porque el modelo «decide» cuándo utilizar una herramienta. Sin embargo, en algunos flujos, una llamada explícita de decisión resulta más fiable.
Ejemplo: un flujo de trabajo de soporte al cliente donde el modelo debe decidir entre varias acciones.
Enfoque con llamadas nativas a funciones: entrega al modelo 5 herramientas —reembolsar, enviar un artículo, transferir a una persona, solicitar una aclaración y cerrar la solicitud— y deja que decida.
Enfoque con llamada explícita de decisión: llama primero al modelo con una sola herramienta, decide_action, que recibe un único parámetro: la acción elegida. Después, según esa decisión, vuelve a llamar al modelo exponiendo solo la herramienta pertinente.
El enfoque explícito es más lento y detallado, pero también más fiable. El modelo se concentra en cada paso y el sistema anfitrión conserva un mayor control del flujo.
Para flujos de trabajo de alto riesgo, el enfoque explícito suele ganar. Para flujos de trabajo exploratorios o simples, la llamada nativa a funciones es adecuada.
Patrón 12: Formato de resultados de herramientas
Cómo devuelves los resultados de las herramientas importa. El modelo está leyendo el resultado; el formato importa.
Malo:
{"id": "cus_123", "n": "John", "p": "12345"}
Mejor:
{
"customer_id": "cus_123",
"name": "John Doe",
"phone": "+1-555-0123",
"tier": "premium",
"open_tickets": 0
}
Mejor aún (en algunos casos):
Customer found:
- ID: cus_123
- Name: John Doe
- Tier: Premium
- Phone: +1-555-0123
- Open tickets: 0
This customer is in the premium tier and has no open tickets.
La última forma es más legible para una persona, incluye contexto y facilita la generación posterior. La segunda está más estructurada y es más fácil de procesar mediante código. Prueba qué formato maneja mejor el modelo en tus tareas posteriores.
En algunas herramientas funciona bien devolver una estructura y una explicación: «Resultado: [explicación]. Datos sin procesar: [JSON]».
Patrón 13: Reintentos que tienen en cuenta el esquema
Algunos errores de validación son irreparables (el modelo entendió fundamentalmente mal la tarea). Otros son fáciles de arreglar.
Un patrón útil: clasifica el error y responde en consecuencia.
def handle_validation_error(error):
if "missing required field" in str(error):
return retry_with_message("You omitted required field X. Please include it.")
elif "value not in enum" in str(error):
return retry_with_message("Value X is not in the allowed set. Choose from: ...")
elif "type mismatch" in str(error):
return retry_with_message("Field X must be a number, not a string.")
else:
# Unknown error — single generic retry
return retry_with_message("There was an error in your response. Please try again.")
Los reintentos específicos tienen más probabilidades de éxito que los genéricos.
Patrón 14: Composición de herramientas
Las herramientas deben componerse. Pequeñas herramientas enfocadas que hacen una sola cosa pueden combinarse por el modelo en flujos de trabajo complejos.
Una herramienta monolítica como process_customer_request(query), que lo hace todo, es una caja negra. El modelo no puede observar ni orientar su lógica interna.
Un conjunto de herramientas enfocadas — search_customer(email), get_recent_orders(customer_id), check_subscription_status(customer_id), escalate_to_human(reason) — pueden componerse por el modelo en el flujo adecuado para cada situación.
Diseña herramientas con la granularidad adecuada. Cada una debe hacer una sola cosa y combinarse con otras dentro del flujo.
Patrón 15: Un esquema para «no lo sé»
Un patrón sutil: modela explícitamente la incertidumbre en el esquema.
class CustomerInfo(BaseModel):
name: str
name_confidence: Literal["high", "medium", "low"]
needs_clarification: bool
clarification_question: Optional[str] = None
El modelo puede devolver «confianza baja» junto con una pregunta aclaratoria, en lugar de inventar datos.
Esto es mucho mejor que un modelo que siempre llena los campos con confianza, a veces con datos alucinados.
Ejemplo práctico: procesamiento de facturas
Para reunir todos los patrones, veamos un sistema de procesamiento de facturas apto para producción.
Entradas: factura PDF adjunta a un correo electrónico. Objetivo: extraer datos estructurados y enviarlos al sistema contable.
Esquema:
class LineItem(BaseModel):
description: str
quantity: float
unit_price: float
total: float
confidence: Literal["high", "medium", "low"]
class Invoice(BaseModel):
vendor_name: str
vendor_id: Optional[str] = None # null if not found in our records
invoice_number: str
invoice_date: date
due_date: Optional[date] = None
line_items: List[LineItem]
subtotal: float
tax: float
total: float
currency: str # ISO 4217
confidence: Literal["high", "medium", "low"]
needs_review: bool
review_reasons: List[str] # Specific reasons review is needed
Flujo de trabajo:
- Paso de OCR: modelo de visión extrae texto del PDF.
- Paso de extracción: llamada al LLM con el esquema anterior, generación restringida habilitada.
- Paso de validación: Pydantic valida los datos. Los errores activan un reintento con información específica sobre el fallo.
- Paso de verificación cruzada: llamada a herramienta para buscar proveedor en nuestros registros. Si
vendor_namecoincide con un proveedor conocido, adjuntavendor_id. Si no, estableceneeds_review=true. - Paso de verificación matemática: verifica
sum(line_items.total) ≈ subtotalysubtotal + tax ≈ total. Si no, estableceneeds_review=true. - Paso de verificación de confianza: si
confidencees baja, o cualquier elemento de línea tiene baja confianza, estableceneeds_review=true. - Enrutamiento: si
needs_review=true, envía el caso a la cola de revisión humana. De lo contrario, dirígelo al sistema contable. - Registro: cada paso se registra con la entrada, la respuesta, la duración y los errores.
Modos de fallo gestionados:
- JSON malformado: la generación restringida lo evita y el reintento gestiona los casos límite.
- Campos alucinados: el esquema estricto los impide.
- Errores matemáticos: validados.
- Proveedores desconocidos: marcados.
- Baja confianza: marcados.
- Errores de herramientas: gestión explícita.
Rendimiento en producción: ~95% de facturas procesadas directamente; 5% marcadas para revisión. De las procesadas automáticamente, la tasa de error es <0.5% (bien dentro de lo aceptable). De la cola de revisión, ~80% confirmado correcto, 20% necesitan correcciones.
Así son las salidas estructuradas aptas para producción. No basta con que «el modo JSON haya funcionado una vez»: hace falta un pipeline que gestione modos de fallo reales.
Errores comunes
Algunos patrones que vemos repetidamente:
Error 1: Sin validación. Pydantic o zod o lo que sea — valida. No confíes en el modelo.
Error 2: Descripciones imprecisas. «category: string» no ayuda al modelo. «category: uno de billing, technical y account, donde billing comprende…» sí.
Error 3: Demasiadas herramientas. Con 30 disponibles, el modelo elige las incorrectas. Limita cada llamada a <10 herramientas pertinentes.
Error 4: Sin reintento ante fallos de validación. Una respuesta malformada detiene todo el flujo. Reintenta una vez con información sobre el error.
Error 5: Sin observabilidad. Cuando las llamadas a herramientas fallan en producción, no puedes diagnosticar sin trazas.
Error 6: Sin idempotencia en herramientas con efectos secundarios. Reembolsos duplicados, correos duplicados. Error predecible.
Error 7: Confiar sin validar en argumentos elegidos por el LLM. Puede alucinar identificadores de usuario o fechas. Valida los argumentos antes de ejecutar la herramienta.
Error 8: Omitir el versionado del esquema. Los cambios rompen a los consumidores posteriores. Versiona los esquemas.
De la demostración al sistema de producción
Las salidas estructuradas y las llamadas a funciones convierten un «LLM que habla» en un «LLM que ejecuta tareas». Bien implementadas, permiten operar IA en producción; mal implementadas, fallan de formas complejas y costosas.
Los patrones importantes son: esquemas estrictos, generación restringida, validación con reintentos, revisión de los resultados de las herramientas, idempotencia, degradación controlada, gestión de errores según el esquema y observabilidad integral.
Cada patrón separa una demostración de un sistema de producción. Incorpóralos desde el principio.



