Si solo necesitas que la respuesta final siga un JSON Schema, usa Structured Outputs mediante text.format. Si el modelo necesita pedir a tu aplicación que consulte datos o ejecute una acción, usa function calling. Si debe consultar o actuar y después entregar un objeto estable para la interfaz, usa ambos.
La frontera más importante es operativa: el modelo no ejecuta tu función. Emite un function_call; tu código valida, autoriza y ejecuta; después devuelve un function_call_output. Un JSON correcto tampoco demuestra que el dato sea verdadero ni que el efecto externo haya terminado.
| Tu caso | Ruta | Qué controla | Quién ejecuta | Señal de éxito |
|---|---|---|---|---|
| Clasificar o extraer datos que ya están en el contexto | text.format | Forma de la respuesta final | El modelo genera; tu app valida el significado | Schema válido y valores correctos |
| Consultar una base de datos, API o servicio | Herramienta de función | Nombre y argumentos de la petición de herramienta | Tu aplicación | Resultado real de la herramienta y asociado al call_id |
| Ejecutar una acción y preparar después JSON para una UI | Function tool + text.format | Argumentos de la herramienta y forma de la respuesta final | Tu aplicación ejecuta; el modelo redacta la salida | Efecto verificado y objeto final válido |
| Responder con prosa sin consumidor tipado | Texto normal | Ningún contrato JSON adicional | Nadie ejecuta una herramienta | Respuesta útil; sin complejidad innecesaria |
Detente antes de añadir tools si no hay datos externos, acción ni decisión dinámica. Y no uses text.format como sustituto de autorización, validación de negocio o comprobación del efecto.
No son funciones rivales
La comparación “Structured Outputs vs. function calling” parece enfrentar dos funciones independientes, pero la documentación de Structured Outputs describe dos formas de aplicar la misma garantía de esquema:
- a la respuesta que recibe el usuario, mediante
text.formaten Responses API; - a los argumentos de una función, mediante una tool con
strict: true.
Por eso strict no convierte una salida estructurada en una acción. Solo restringe la forma permitida. Function calling añade otro contrato: el modelo puede pedir una herramienta y la aplicación debe resolver esa petición.
Piensa en tres propietarios:
- El JSON Schema define qué forma se admite.
- El modelo decide qué contenido proponer y, si corresponde, qué herramienta pedir.
- Tu aplicación valida permisos y datos, ejecuta la función, controla duplicados y registra el resultado.
Si esos propietarios quedan mezclados, aparecen fallos engañosos: un argumento puede cumplir el schema pero apuntar al pedido equivocado; una tool call puede existir sin haberse ejecutado; una respuesta final puede parsear aunque el sistema externo haya rechazado la operación.
Ruta A: text.format para una respuesta final
Usa esta ruta cuando toda la información necesaria ya está en input y el siguiente consumidor necesita un objeto estable. Ejemplos razonables son clasificar un ticket, extraer campos de un documento o preparar datos para componentes de una interfaz.
Este script es un esqueleto ejecutable con el SDK de JavaScript. Requiere OPENAI_API_KEY, un modelo compatible indicado en OPENAI_MODEL y npm install openai. Si todavía no tienes el proyecto y la credencial preparados, empieza por la guía de clave de OpenAI y primera llamada a Responses API.
javascriptimport OpenAI from "openai"; const client = new OpenAI(); const model = process.env.OPENAI_MODEL; if (!model) { throw new Error("Define OPENAI_MODEL con un modelo compatible del proyecto"); } const response = await client.responses.create({ model, input: [ { role: "user", content: "Clasifica este mensaje: PED-1042 llegó con la caja dañada. " + "No consultes sistemas externos.", }, ], text: { format: { type: "json_schema", name: "clasificacion_ticket", strict: true, schema: { type: "object", properties: { pedido_id: { type: "string" }, categoria: { type: "string", enum: ["entrega", "facturacion", "otro"], }, requiere_revision: { type: "boolean" }, }, required: ["pedido_id", "categoria", "requiere_revision"], additionalProperties: false, }, }, }, }); if (response.status !== "completed") { throw new Error(`Respuesta incompleta: ${response.status}`); } const refusal = response.output .filter((item) => item.type === "message") .flatMap((item) => item.content) .find((item) => item.type === "refusal"); if (refusal) { console.error("La solicitud fue rechazada:", refusal.refusal); } else { const ticket = JSON.parse(response.output_text); console.log(ticket); }
El schema evita claves extra y restringe categoria, pero no puede comprobar por sí solo que PED-1042 exista o que el daño haya ocurrido. Esa es la separación entre estructura y significado.
Tampoco conviene omitir la rama de rechazo. OpenAI documenta que un refusal puede aparecer fuera del objeto esperado, y que una salida ajustada al schema todavía puede contener errores semánticos. Si el input es incompatible con la tarea, diseña una rama explícita en vez de forzar valores inventados.
Ruta B: function calling para obtener datos o actuar
La guía oficial de function calling define un loop de aplicación, no una ejecución remota automática:
- envías el prompt y las tools disponibles;
- recibes uno o varios items
function_call; - tu aplicación valida los argumentos, permisos y contexto;
- tu aplicación ejecuta la función;
- devuelve un item
function_call_outputcon el mismocall_id; - pide al modelo que continúe.
Hay dos consecuencias importantes. Primero, guarda response.output para el siguiente turno: puede contener otros items necesarios para continuar, incluidos items de razonamiento en modelos que los devuelven junto con la tool call. Segundo, no marques la operación como completada cuando solo ves function_call; espera el resultado del sistema propietario.
Con strict: true, la sección de strict mode exige que cada objeto use additionalProperties: false y que todas sus propiedades estén en required. Un campo opcional se representa, cuando el schema lo permite, con un tipo anulable. Declara strict: true de forma explícita para que el contrato no dependa de normalizaciones o fallbacks.
tool_choice cambia la libertad de selección, no la seguridad. Puedes permitir selección automática, exigir una tool o forzar una función concreta, pero tu aplicación sigue siendo responsable de autorizar la acción. Forzar emitir_reembolso nunca debe saltarse la comprobación de importe, identidad o estado del pedido.
Ruta C: function calling y Structured Outputs juntos
El patrón híbrido sirve cuando necesitas información externa y, después, una respuesta final tipada. El flujo es:
textusuario → modelo emite function_call → aplicación valida y ejecuta → aplicación devuelve function_call_output → modelo genera respuesta final con text.format
El siguiente ejemplo usa una consulta local simulada para que el loop sea legible. Es un skeleton reproducible, no un benchmark ni un resultado de laboratorio ya ejecutado. Sustituye consultarEstadoPedido por el cliente real de tu sistema y añade autenticación, autorización, timeout, registro e idempotencia antes de producción.
javascriptimport OpenAI from "openai"; const client = new OpenAI(); const model = process.env.OPENAI_MODEL; if (!model) { throw new Error("Define OPENAI_MODEL con un modelo compatible del proyecto"); } const tools = [ { type: "function", name: "consultar_estado_pedido", description: "Obtiene el estado actual de un pedido después de validar su identificador.", strict: true, parameters: { type: "object", properties: { pedido_id: { type: "string", description: "Identificador con formato PED-1234", }, }, required: ["pedido_id"], additionalProperties: false, }, }, ]; function consultarEstadoPedido(pedidoId) { // Sustituir por una llamada autorizada al sistema propietario. const datosSimulados = { "PED-1042": { estado: "en_transito", incidencia: "embalaje_danado" }, }; return datosSimulados[pedidoId] ?? { estado: "no_encontrado", incidencia: null, }; } const input = [ { role: "user", content: "Comprueba PED-1042 y prepara una respuesta estructurada para soporte.", }, ]; const firstResponse = await client.responses.create({ model, input, tools, tool_choice: "auto", }); // Conserva todos los items, no solo la llamada de función. input.push(...firstResponse.output); const calls = firstResponse.output.filter( (item) => item.type === "function_call", ); if (calls.length === 0) { throw new Error("El modelo no pidió la consulta esperada"); } for (const call of calls) { if (call.name !== "consultar_estado_pedido") { throw new Error(`Tool no autorizada: ${call.name}`); } const args = JSON.parse(call.arguments); if (!/^PED-\d{4}$/.test(args.pedido_id)) { throw new Error("pedido_id no supera la validación de negocio"); } const result = consultarEstadoPedido(args.pedido_id); input.push({ type: "function_call_output", call_id: call.call_id, output: JSON.stringify(result), }); } const finalResponse = await client.responses.create({ model, input, tools, tool_choice: "none", text: { format: { type: "json_schema", name: "respuesta_soporte", strict: true, schema: { type: "object", properties: { pedido_id: { type: "string" }, estado: { type: "string", enum: ["en_transito", "entregado", "no_encontrado"], }, mensaje_cliente: { type: "string" }, requiere_revision_humana: { type: "boolean" }, }, required: [ "pedido_id", "estado", "mensaje_cliente", "requiere_revision_humana", ], additionalProperties: false, }, }, }, }); if (finalResponse.status !== "completed") { throw new Error(`Respuesta final incompleta: ${finalResponse.status}`); } const refusal = finalResponse.output .filter((item) => item.type === "message") .flatMap((item) => item.content) .find((item) => item.type === "refusal"); if (refusal) { console.error("La respuesta final fue rechazada:", refusal.refusal); } else { console.log(JSON.parse(finalResponse.output_text)); }
El ejemplo hace visibles dos schemas distintos. El schema de la tool describe qué argumentos puede pedir el modelo. El schema de text.format describe qué objeto final debe recibir la interfaz. Entre ambos vive la función real, que OpenAI no ejecuta por ti.
Para una operación con efectos secundarios —un reembolso, un correo o una reserva— añade una clave de idempotencia propia, registra el identificador devuelto por el sistema y consulta su estado antes de reintentar. Un timeout no significa necesariamente que la acción no ocurrió. Si además recibes un 429, separa cuota de facturación de rate limiting con la guía de errores de cuota de OpenAI API antes de repetir a ciegas.
Valida cuatro capas, no solo el JSON
Una integración está lista solo cuando las cuatro capas tienen una prueba observable.
| Capa | Pregunta | Prueba útil | Fallo que puede quedar oculto |
|---|---|---|---|
| 1. JSON | ¿Se puede parsear? | JSON.parse o parser equivalente | JSON válido con campos inesperados |
| 2. Schema | ¿Cumple tipos, enums y campos requeridos? | Parser del SDK o validador JSON Schema | Valores plausibles pero incorrectos |
| 3. Semántica | ¿Los valores representan el caso real? | Reglas de dominio y comparación con la fuente | Pedido equivocado, importe falso, categoría errónea |
| 4. Efecto | ¿La consulta o acción terminó en el sistema propietario? | ID de operación, estado confirmado y log idempotente | Tool call generada pero no ejecutada, timeout ambiguo o duplicado |
1. JSON válido
Esta es la barra mínima. JSON mode puede cubrirla, pero no impone tu schema. Si el consumidor necesita campos exactos, JSON válido no basta.
2. Schema válido
Structured Outputs controla esta capa. Aun así, trata refusal, respuesta incomplete y cortes como ramas distintas. No conviertas una ausencia de dato en una cadena inventada solo para satisfacer required.
3. Significado correcto
Valida formatos, rangos, relaciones y pertenencia. Un pedido_id con sintaxis correcta puede pertenecer a otra cuenta. Un enum aceptado puede no ser válido en la transición actual del flujo de trabajo.
4. Efecto confirmado
Solo el sistema que ejecuta la operación puede probar esta capa. Conserva un identificador, consulta el estado final y deduplica reintentos. La frase del modelo “reembolso completado” no es evidencia contable.
Cuándo usar solo uno, ambos o ninguno
Usa solo text.format cuando:
- el contexto ya contiene los datos necesarios;
- el siguiente paso consume un objeto tipado;
- no hace falta consultar ni modificar un sistema externo.
Usa function calling cuando:
- el modelo debe decidir si consultar o actuar;
- la respuesta depende de datos que no están en el contexto;
- tu aplicación debe ejecutar código y devolver el resultado.
Combina ambos cuando:
- una tool obtiene o modifica estado;
- después necesitas un objeto final estable para UI, almacenamiento u otra API;
- quieres validar por separado argumentos, tool output y respuesta final.
Usa texto normal cuando:
- el lector necesita prosa y no existe consumidor tipado;
- el schema no reduce ningún fallo observable;
- una tool solo añadiría una vuelta sin datos ni acción reales.
La decisión no debe basarse en cuál feature parece más “agentic”. Debe basarse en dónde vive la información, quién ejecuta y qué prueba el éxito.
JSON mode es una nota de migración, no una tercera arquitectura
La comparación oficial separa claramente los contratos: JSON mode busca JSON válido; Structured Outputs busca adherencia al schema. Si mantienes una integración antigua, JSON mode puede seguir siendo un fallback, pero necesitarás validación de schema y tratamiento de output incompleto en la aplicación.
En Responses API, no copies de forma mecánica ejemplos antiguos de Chat Completions con response_format. La superficie actual para una respuesta final estructurada es text.format; function tools tienen su propio schema y ciclo.
Checklist antes de producción
- Clasifica la necesidad como respuesta final, acción/consulta, híbrido o texto libre.
- Declara quién posee el schema, quién ejecuta y qué sistema confirma el efecto.
- Usa
strict: true, todos los campos requeridos yadditionalProperties: falsecuando quieras argumentos estrictos. - Conserva los items necesarios de
response.outputal continuar el tool loop. - Relaciona cada
function_call_outputcon sucall_id. - Rechaza tools, argumentos y recursos que la identidad actual no puede usar.
- Trata refusal, incomplete, timeout y tool error como ramas explícitas.
- Valida JSON, schema, semántica y efecto por separado.
- Añade idempotencia antes de cualquier side effect repetible.
- Registra modelo, versión del SDK y fecha; vuelve a comprobar la documentación antes de desplegar.
La elección correcta queda demostrada cuando puedes señalar dos cosas sin ambigüedad: qué contrato controla la forma y qué sistema prueba la acción. Si ambas respuestas apuntan al modelo, todavía falta una frontera de aplicación.



