Ir al contenido principal
Extracción LLM en producción: el JSON que falla sin avisar

Extracción LLM en producción: el JSON que falla sin avisar

AI Integration
7 min readPor Daily Miranda Pardo

Llevas tres semanas con el pipeline en producción. La extracción de facturas de proveedor funciona: los datos llegan a Supabase, los informes se generan solos. Esta mañana el equipo contable te manda un mensaje: los importes de 23 registros están mal. Revisas los logs. No hay ninguna excepción. Todo aparece verde.

El problema: el LLM devolvió "amount_eur": "1847.30" — un string — en lugar de "amount_eur": 1847.30. Tu código hizo JSON.parse() sin validar tipos. El dato entró en la base de datos. La suma agregada del informe salió incorrecta. Nadie lo vio hasta que alguien cuadró números a mano.

Esto no es un bug. Es una característica estadística de los LLMs.

El error que todos cometen al extraer datos con IA

Un LLM no "devuelve JSON". Genera texto que, en la mayoría de los casos, parece JSON. La diferencia importa en producción.

En local, con diez o veinte llamadas de prueba, el output es prácticamente perfecto. En producción, con miles de documentos reales — facturas mal formateadas, contratos con tipografías raras, emails con saltos de línea inesperados — el modelo empieza a cometer errores estadísticamente predecibles:

  • Tipo incorrecto: "amount": "1847.30" en lugar de "amount": 1847.30
  • Campo renombrado: "invoiceNumber" en lugar de "invoice_number" según el documento de entrada
  • Campo ausente: el modelo "olvidó" incluir due_date porque en ese documento concreto no era visible
  • JSON con texto extra: "Aquí están los datos extraídos: { ... }"JSON.parse() lanza excepción

Ninguno de estos casos lanza un error automáticamente. En los tres primeros, el dato entra en tu sistema con el valor incorrecto. En el cuarto te enteras, pero demasiado tarde.

Por qué JSON.parse() no es defensa suficiente

El patrón más común que veo en implementaciones nuevas:

// ❌ Patrón frágil — falla en producción
const response = await anthropic.messages.create({
  model: "claude-sonnet-4-6",
  messages: [{ role: "user", content: `Extrae los datos de esta factura en JSON:\n\n${text}` }],
  max_tokens: 1024,
});
const raw = response.content[0].text;
const data = JSON.parse(raw); // Lanza si hay texto extra; no valida tipos
await db.insert(data);        // Escribe datos incorrectos sin saberlo

JSON.parse() solo verifica que la sintaxis es válida. No comprueba que amount_eur sea number y no string. No detecta que invoice_number falte. No te avisa de que el LLM usó camelCase en lugar de snake_case. Tu sistema acepta el dato, lo persiste y el error se descubre tres semanas después.

Tres capas para una extracción fiable

La solución se construye en capas. Cada una atrapa un tipo distinto de error.

Capa 1 — Tool forcing: fuerza el schema desde el API

En lugar de pedir texto y parsear, defines la estructura como una herramienta y fuerzas al modelo a usarla. El API garantiza que el output es un JSON válido que sigue el schema definido — nunca texto libre.

import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();

const extractionTool = {
  name: "extract_invoice",
  description: "Extrae los datos estructurados de la factura proporcionada",
  input_schema: {
    type: "object" as const,
    properties: {
      vendor_name:     { type: "string", description: "Nombre del proveedor" },
      invoice_number:  { type: "string", description: "Número de factura" },
      amount_eur:      { type: "number", description: "Importe total en euros (número, no string)" },
      due_date:        { type: "string", description: "Fecha de vencimiento ISO YYYY-MM-DD" },
      line_items: {
        type: "array",
        items: {
          type: "object",
          properties: {
            description: { type: "string" },
            quantity:    { type: "number" },
            unit_price:  { type: "number" },
          },
          required: ["description", "quantity", "unit_price"],
        },
      },
    },
    required: ["vendor_name", "invoice_number", "amount_eur"],
  },
};

async function callExtraction(text: string) {
  const response = await anthropic.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    tools: [extractionTool],
    tool_choice: { type: "tool", name: "extract_invoice" }, // Fuerza esta herramienta concreta
    messages: [{ role: "user", content: `Extrae los datos:\n\n${text}` }],
  });

  const toolUse = response.content[0];
  if (toolUse.type !== "tool_use") throw new Error("Respuesta inesperada del modelo");
  return toolUse.input; // Objeto garantizado, nunca texto
}

El parámetro clave es tool_choice: { type: "tool", name: "extract_invoice" }. Fuerza al modelo a llamar exactamente esa herramienta — el resultado es siempre un objeto estructurado, nunca texto libre.

Capa 2 — Validación con Zod: comprueba tipos y campos obligatorios

Tool forcing garantiza que el JSON tiene la forma correcta, pero no que los tipos sean los esperados. Zod añade esa capa:

import { z } from "zod";

const InvoiceSchema = z.object({
  vendor_name:    z.string().min(1),
  invoice_number: z.string().min(1),
  amount_eur:     z.number().positive(),
  due_date:       z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
  line_items: z.array(z.object({
    description: z.string().min(1),
    quantity:    z.number().positive(),
    unit_price:  z.number().positive(),
  })).optional(),
});

type Invoice = z.infer<typeof InvoiceSchema>;

function validate(raw: unknown): Invoice {
  const result = InvoiceSchema.safeParse(raw);
  if (!result.success) {
    throw new ValidationError(result.error.format());
  }
  return result.data;
}

Si amount_eur llega como "1847.30" (string), Zod lo detecta y lanza ValidationError antes de que el dato entre en la base de datos.

Capa 3 — Retry con feedback: el modelo corrige su propio error

Cuando la validación Zod falla, en lugar de propagar el error, se lo devuelves al modelo con contexto:

async function extractWithRetry(text: string, maxRetries = 2): Promise<Invoice> {
  let lastError: string | null = null;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const userContent = lastError
      ? `Extrae los datos de la factura. Intento anterior fallido:\n${lastError}\nCorrige los problemas.\n\nFactura:\n${text}`
      : `Extrae los datos de la factura:\n\n${text}`;

    const raw = await callExtraction(userContent);

    try {
      return validate(raw);
    } catch (err) {
      lastError = err instanceof ValidationError
        ? JSON.stringify(err.issues)
        : "Error de validación desconocido";
    }
  }

  throw new Error(`Extracción fallida tras ${maxRetries + 1} intentos`);
}

En la práctica, el retry con feedback resuelve más del 95% de los fallos de validación — el modelo entiende exactamente qué campo tenía el tipo incorrecto y lo corrige.

Cuándo usar extracción estructurada vs tool calling libre

Estos dos patrones no son lo mismo, aunque usen la misma API de herramientas:

Caso de usoPatrón recomendado
Extraer datos de un documento (factura, contrato, email)Tool forcing + Zod
Clasificar intención o categoría desde texto libreTool forcing con enum
Agente que decide llamar a APIs externas de forma autónomaTool calling libre + validación de parámetros
Respuesta conversacional o explicaciónTexto libre
Resumen estructurado al final de un análisisTool forcing para el bloque final

La diferencia clave: en tool calling clásico (como cubrimos en la validación de herramientas en producción), el modelo decide cuándo y cómo llamar a la herramienta. En extracción estructurada, tú fuerzas la herramienta desde fuera — el modelo solo tiene que rellenar los campos.

Lo que cambia cuando el pipeline es fiable

Cuando implementas estas tres capas en un agente de integración de IA, el comportamiento cambia radicalmente: los errores de tipo dejan de llegar a la base de datos, los fallos de validación se autocorrigen en el retry, y cuando algo falla de verdad (un documento completamente ilegible, por ejemplo), el sistema lanza un error claro y trazable en lugar de escribir basura silenciosa.

En proyectos de extracción de datos en producción — facturas de proveedores, contratos, emails de clientes — la combinación de tool forcing, Zod y retries con feedback reduce los errores de parsing a prácticamente cero. No porque el LLM sea más preciso, sino porque la arquitectura no permite que un output incorrecto llegue al sistema.

Si tienes un pipeline de extracción de datos con IA que falla de forma impredecible, o estás planificando construir uno, en 30 minutos te digo exactamente qué capa te falta y cómo añadirla.

Hablemos de tu pipeline de extracción de datos →

Compartir artículo

LinkedInXWhatsApp

¿Procesos repetitivos en tu empresa?

Descarga gratis el Mapa de Automatización IA — los 5 procesos que más tiempo roban y cómo resolverlos.

Sin spam. Solo el PDF. Puedes darte de baja cuando quieras.

Escrito por Daily Miranda Pardo

Ayudo a empresas a automatizar procesos, crear agentes IA y conectar sistemas inteligentes.