Saltar al contenido principal

Structured Outputs vs. function calling en OpenAI: cuándo usar cada uno

9 min de lecturaGuía de API

Usa Structured Outputs mediante text.format cuando solo necesitas una respuesta final con forma fija; function calling cuando tu aplicación debe consultar o actuar; y ambos cuando el resultado de la herramienta también debe alimentar una salida tipada.

Flujo que compara text.format, llamada de herramienta, ejecución en la aplicación y respuesta estructurada final

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 casoRutaQué controlaQuién ejecutaSeñal de éxito
Clasificar o extraer datos que ya están en el contextotext.formatForma de la respuesta finalEl modelo genera; tu app valida el significadoSchema válido y valores correctos
Consultar una base de datos, API o servicioHerramienta de funciónNombre y argumentos de la petición de herramientaTu aplicaciónResultado real de la herramienta y asociado al call_id
Ejecutar una acción y preparar después JSON para una UIFunction tool + text.formatArgumentos de la herramienta y forma de la respuesta finalTu aplicación ejecuta; el modelo redacta la salidaEfecto verificado y objeto final válido
Responder con prosa sin consumidor tipadoTexto normalNingún contrato JSON adicionalNadie ejecuta una herramientaRespuesta ú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:

  1. a la respuesta que recibe el usuario, mediante text.format en Responses API;
  2. 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.

javascript
import 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:

  1. envías el prompt y las tools disponibles;
  2. recibes uno o varios items function_call;
  3. tu aplicación valida los argumentos, permisos y contexto;
  4. tu aplicación ejecuta la función;
  5. devuelve un item function_call_output con el mismo call_id;
  6. 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:

text
usuario → 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.

javascript
import 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.

CapaPreguntaPrueba útilFallo que puede quedar oculto
1. JSON¿Se puede parsear?JSON.parse o parser equivalenteJSON válido con campos inesperados
2. Schema¿Cumple tipos, enums y campos requeridos?Parser del SDK o validador JSON SchemaValores plausibles pero incorrectos
3. Semántica¿Los valores representan el caso real?Reglas de dominio y comparación con la fuentePedido 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 idempotenteTool 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 y additionalProperties: false cuando quieras argumentos estrictos.
  • Conserva los items necesarios de response.output al continuar el tool loop.
  • Relaciona cada function_call_output con su call_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.

#OpenAI API#Structured Outputs#Function Calling#Responses API#JSON Schema
Share: