1) Introducción detallada

\n

En una gestoría moderna, el cuello de botella rara vez está en “tener los documentos”, sino en convertirlos en conocimiento operativo: saber qué tipo de documento es, a qué expediente pertenece, qué campos críticos contiene (NIF/CIF, razón social, importe, fecha, órgano emisor, número de póliza, matrícula, referencia catastral, etc.) y qué acción dispara (presentación, subsanación, archivo, notificación al cliente). El problema real es que los documentos llegan en formatos heterogéneos (PDF escaneados, fotos, correos convertidos a PDF, multi-página mezclada), con calidades dispares, y con un alto componente de texto “semiestructurado” (formularios con casillas, tablas, sellos, firmas, códigos QR) donde el OCR tradicional se degrada.

\n

La combinación OCR + modelos de lenguaje (GPT-4) no es “usar IA para leer PDFs”: es diseñar un pipeline robusto que haga tres cosas bien: (1) normalizar y extraer texto con garantías (preprocesado + OCR + detección de layout), (2) clasificar y estructurar usando prompts deterministas y esquemas (p. ej., JSON Schema) minimizando alucinaciones, y (3) verificar, auditar y cumplir (RGPD/LOPDGDD, minimización, trazabilidad, control de acceso, retención, cifrado y métricas de calidad). Si cualquiera de estos pilares falla, el sistema se vuelve un generador de errores silenciosos: clasifica mal, asigna expedientes incorrectos, o extrae un NIF con un dígito cambiado, lo que en gestoría es una incidencia cara.

\n

Este artículo describe una arquitectura de extremo a extremo, con detalle técnico, orientada a producción: desde ingestión de documentos, preprocesado y OCR, hasta clasificación híbrida (reglas + embeddings + GPT-4), extracción estructurada con validaciones, y un plan de despliegue con observabilidad y mejora continua. El foco es pragmático: reducir tiempos de tramitación, mejorar consistencia documental y mantener una línea de auditoría verificable. Se incluyen ejemplos de código (Node.js/TypeScript) y decisiones de diseño: cuándo usar OCR local vs cloud, cómo versionar prompts, cómo evitar fugas de datos, y cómo medir precisión (macro-F1) y coste por documento.

\n\n

2) Contexto histórico o teórico

\n

La clasificación documental en entornos administrativos ha pasado por cuatro etapas técnicas:

\n

(a) Indexación manual y reglas estáticas: el operador “nombra” y “etiqueta” a mano; o se aplican expresiones regulares sobre texto digital (cuando existe). Funciona con entradas limpias, pero no escala con escaneos y layouts variados.

\n

(b) OCR + heurísticas: al generalizarse el escaneo, se introduce OCR para obtener texto y luego heurísticas (palabras clave, posiciones, plantillas por organismo). El problema: el OCR degrada por ruido, y las plantillas explotan cuando cambian versiones del formulario o cuando se mezclan páginas de distintos documentos.

\n

(c) ML clásico / deep learning para clasificación: modelos supervisados (SVM, CNN sobre imágenes, Transformers) entrenados con dataset etiquetado. Mejoran robustez, pero requieren datos, mantenimiento y reentrenamiento; además, extraer campos (NER/Key-Value) es un problema distinto al de clasificar.

\n

(d) LLMs (GPT-4) + retrieval + validación: los modelos de lenguaje aportan comprensión semántica y flexibilidad para layouts, pudiendo clasificar y extraer campos con prompts y esquemas. El riesgo no desaparece: un LLM puede “rellenar huecos” (alucinar). Por eso el enfoque de producción no es “preguntar al modelo”, sino orquestar: OCR fiable, evidencias (quotes), constraints, validaciones, reglas, y revisión humana cuando la confianza es baja.

\n

Teóricamente, el pipeline se puede ver como un sistema de decisión bajo incertidumbre: el OCR produce una distribución de errores; el clasificador produce una probabilidad por clase; el extractor produce valores con confianza implícita; y el sistema debe decidir autoaprobar, pedir revisión o rechazar. En gestorías, el coste de un falso positivo (clasificar una notificación como “recibo”) puede ser mayor que el de un falso negativo. Por tanto, la métrica objetivo no es sólo accuracy: es un balance coste-riesgo, con umbrales por tipo documental y por cliente/proceso.

\n\n

3) Sección técnica principal 1: Ingesta, normalización y preprocesado (calidad de OCR como variable controlada)

\n

Antes de hablar de GPT-4, hay que dominar la capa que más condiciona el resultado: la calidad del OCR. En gestorías, la entrada típica es: PDFs escaneados (monocromo, baja resolución), fotos desde móvil con perspectiva, documentos con marcas (sellos, firmas), y anexos multi-página donde se mezclan piezas.

\n

Pipeline recomendado:

\n

1) Ingesta y fingerprinting: cada documento entra con metadatos (origen, cliente, canal, timestamp) y se calcula un hash (SHA-256) del binario para deduplicación. Se asigna un document_id inmutable y un ingestion_batch_id. Guardar el binario original es crítico para auditoría.

\n

2) Normalización de formato: convertir a un formato de procesamiento estable. Para PDFs: extraer páginas a imágenes (PNG/TIFF) a 300 DPI (o 400 DPI si hay texto pequeño). Para fotos: normalizar orientación (EXIF), y reescalar sin destruir detalles. Herramientas típicas: Ghostscript, Poppler (pdftoppm), ImageMagick, o pipelines en Python/OpenCV.

\n

3) Preprocesado de imagen: aquí se ganan muchos puntos de OCR:

\n
    \n
  • Deskew (corrección de inclinación) por Hough transform o estimación de baseline.
  • \n
  • Despeckle (eliminar ruido) con filtros medianos, morfología (opening/closing).
  • \n
  • Binarización adaptativa (Sauvola/Niblack) para escaneos pobres.
  • \n
  • Corrección de perspectiva en fotos (detección de contorno del papel y homografía).
  • \n
  • Separación de páginas y detección de páginas en blanco (para evitar pagar OCR/LLM en vacío).
  • \n
\n

4) Detección de layout: los documentos legales contienen tablas, cabeceras, pies, columnas. OCR “plano” tiende a mezclar columnas y romper el orden. Si el presupuesto lo permite, usa un motor que devuelva bloques y coordenadas (Azure OCR Read, Google Document AI, AWS Textract, o Tesseract + layout analysis). Con coordenadas, puedes reconstruir el orden lógico y citar evidencias.

\n

5) OCR multi-estrategia: en producción, un solo OCR no siempre basta. En documentos muy degradados, un fallback es útil:

\n
    \n
  • Primario: OCR cloud con modelo moderno + layout.
  • \n
  • Secundario: OCR local (Tesseract) para contingencia/coste.
  • \n
  • Fusión: si ambos devuelven resultados, escoger por score/confianza o por validación posterior (p.ej. checksum de NIF).
  • \n
\n

6) Post-procesado textual: normalizar espacios, corregir guiones de fin de línea, reparar ligaduras, y aplicar diccionarios específicos (AAPP, términos jurídicos). También es útil detectar idioma (es/en/ca/eu/gl) porque el prompt y la clasificación varían.

\n

Clave: tratar la calidad como una señal. Guarda métricas por página: resolución, ratio texto/ruido, confianza OCR media, número de bloques, densidad de caracteres. Estas features alimentan un quality gate: si el OCR sale con baja confianza, se marca para reintento con otro preprocesado (p.ej. binarización distinta) o para revisión humana. Sin este control, GPT-4 trabajará sobre basura y devolverá respuestas “convincente pero incorrectas”.

\n\n

4) Sección técnica principal 2: Estrategias de clasificación (híbrido reglas + embeddings + GPT-4)

\n

Clasificar documentos legales en gestoría no es sólo “tipo de documento”, sino a menudo un vector de etiquetas: familia (fiscal/laboral/mercantil/tráfico), subtipo (modelo 303/111/200, contrato, poder, notificación AEAT, escritura), y intención (requerimiento, resolución, acuse, pago). Un enfoque robusto es híbrido y escalonado para optimizar coste y precisión.

\n

Escalón 1: reglas deterministas. Algunas clases tienen señales inequívocas: códigos de modelo (“303”, “200”, “036”), referencias (“Agencia Tributaria”, “Tesorería General de la Seguridad Social”), o formatos con cabeceras estables. Con reglas (regex + keywords + heurísticas por layout) filtras un porcentaje alto con coste mínimo. Importante: versionar reglas y medir cobertura; no sobreajustar a un único organismo.

\n

Escalón 2: embeddings + kNN. Para documentos variados, crea un índice de embeddings del texto OCR (o de fragmentos) y clasifica por similitud con un catálogo de ejemplos por clase. Esto aporta robustez frente a variaciones. Ventajas: rápido, barato, y explicable (muestras cercanas). Recomendación: mantener 10–50 ejemplos por clase bien curados, y reindexar cuando cambien plantillas. Si el top-1 está muy por encima del top-2 (margen), puedes auto-clasificar sin LLM.

\n

Escalón 3: GPT-4 como árbitro. Cuando reglas y embeddings no son concluyentes, entra GPT-4 para decidir entre un conjunto de clases candidatas. Esto reduce alucinaciones: el modelo elige entre opciones acotadas en lugar de “inventar” clases. Además, se le pide que devuelva evidencias textuales (citas) y un score de confianza (aunque sea heurístico, sirve para gating).

\n

Diseño de taxonomía:

\n
    \n
  • Evita clases demasiado genéricas (“documento fiscal”) si luego necesitas acciones distintas. Mejor: “AEAT_Notificacion”, “AEAT_Requerimiento”, “Modelo_303”, “TGSS_Alta”, etc.
  • \n
  • Permite clase “Desconocido/No_clasificable” y úsala; forzar clasificación siempre degrada el sistema.
  • \n
  • Incluye metadatos de acción: SLA, responsable, checklist de campos y validaciones.
  • \n
\n

Segmentación por páginas: muchos PDFs contienen varios documentos. Estrategia: clasificar por página y luego agrupar por continuidad (cambios bruscos de clase o de organismo). GPT-4 puede ayudar a detectar “saltos” (“esta página parece ser un anexo distinto”). Pero si tienes layout + encabezados, puedes detectar cortes por patrones (p.ej. nuevo logo/cabecera, nueva numeración).

\n

Control de coste: no envíes todo el texto siempre. Primero clasifica con un preview: primeras N líneas, cabecera, bloques con mayor densidad de texto, y páginas 1–2. Si falta señal, haces progressive disclosure: envías más páginas. Este patrón reduce tokens de forma drástica en notificaciones largas con anexos irrelevantes.

\n\n

5) Sección técnica principal 3: Extracción estructurada con GPT-4 (JSON Schema, evidencia y anti-alucinación)

\n

Clasificar es útil, pero el valor real en gestoría es extraer campos para automatizar tareas: rellenar CRM/ERP, abrir expediente, generar borradores, alertas y vencimientos. Aquí GPT-4 destaca si lo usas con disciplina: salida estructurada, validación y evidencias.

\n

Patrón recomendado:

\n
    \n
  • Para cada clase documental, define un contrato de datos (JSON Schema) con campos obligatorios, tipos, regex y enumeraciones.
  • \n
  • Incluye evidence por campo: fragmento literal del OCR (quote) y, si tienes layout, coordenadas de la caja (page, x1,y1,x2,y2). Esto permite auditoría y UI de revisión (“resaltar en el PDF”).
  • \n
  • Exige que el modelo use null cuando no está seguro y no rellene por intuición.
  • \n
\n

Validaciones duras (antes de aceptar):

\n
    \n
  • NIF/NIE/CIF: checksum y formatos (incluyendo NIE X/Y/Z). Si no valida, marca para revisión o re-OCR localizado.
  • \n
  • IBAN: checksum mod-97.
  • \n
  • Fechas: normalizar a ISO-8601; coherencia (fecha de emisión no posterior a hoy en notificaciones históricas, etc.).
  • \n
  • Importes: parseo europeo (1.234,56) vs anglosajón; consistencia con moneda.
  • \n
  • Órgano emisor: pertenencia a catálogo (AEAT, TGSS, DGT, Registro Mercantil…).
  • \n
\n

Estrategia de extracción por etapas:

\n

1) Detección de campos candidatos con regex/heurísticas (por ejemplo, localizar patrones de NIF y fechas). 2) LLM para desambiguar (qué NIF es del obligado tributario vs representante). 3) Validación. 4) Si falla, reintento focalizado: extraer sólo el bloque donde aparece el campo (recorte OCR por región) y re-preguntar con contexto mínimo.

\n

Evitar alucinaciones:

\n
    \n
  • Acotar: “elige entre estas clases” o “extrae sólo campos del esquema”.
  • \n
  • Prohibir inferencias: “no supongas, no completes, devuelve null”.
  • \n
  • Exigir evidencia: “para cada campo, cita texto literal”. Si no hay cita, el campo debe ser null.
  • \n
  • Separar tareas: clasificación primero, extracción después. Mezclar aumenta errores.
  • \n
\n

Gestión de anexos: muchos documentos tienen anexos con tablas extensas. No intentes extraer “todo”. Define objetivos: para una notificación, quizá solo necesitas “expediente”, “plazo”, “importe”, “órgano”, “acción requerida”. Para un contrato, quizá “partes”, “fechas”, “objeto”, “vigencia”. Extraer más campos de los necesarios incrementa coste y error.

\n\n

6) Sección técnica principal 4: Seguridad, RGPD/LOPDGDD, auditoría y gobierno del dato (lo que separa una demo de producción)

\n

Un sistema OCR+LLM en gestoría trata datos personales y, a menudo, categorías especiales o datos financieros. La parte técnica debe incorporar seguridad y cumplimiento desde diseño. Esto no es burocracia: es evitar incidentes, sanciones y pérdida de confianza.

\n

Principios de diseño:

\n
    \n
  • Minimización: enviar al LLM sólo el texto imprescindible. No subas el PDF completo si basta con cabecera y una sección.
  • \n
  • Pseudonimización: cuando el caso lo permita, reemplaza identificadores (NIF, IBAN) por tokens antes de enviar, y guarda un mapa local temporal. Ojo: para validación y extracción final necesitarás el valor real; por eso se usa en etapas concretas.
  • \n
  • Cifrado: en tránsito (TLS) y en reposo (KMS). Los binarios y el texto OCR deben cifrarse en almacenamiento.
  • \n
  • Acceso por roles (RBAC): un asesor laboral no debería ver expedientes mercantiles si no corresponde. Lo mismo para el equipo técnico.
  • \n
\n

Auditoría y trazabilidad:

\n
    \n
  • Guarda: hash del documento, versión del OCR, parámetros de preprocesado, versión del prompt, modelo usado, timestamp, outputs y validaciones aplicadas.
  • \n
  • Loguea decisiones: por qué se clasificó como X (top-k embeddings, reglas disparadas, evidencias del LLM).
  • \n
  • Implementa un ledger de eventos (event sourcing) para reconstruir el estado de un expediente.
  • \n
\n

Retención y borrado:

\n
    \n
  • Define políticas: cuánto tiempo se guardan binarios y OCR. No siempre coincide con la retención del expediente.
  • \n
  • Implementa borrado verificable (tombstones + eliminación física según soporte).
  • \n
\n

Evaluación de proveedores:

\n
    \n
  • Si usas OCR/LLM en cloud, revisa región, DPA, subprocesadores, y opciones de no-retención/opt-out de training cuando estén disponibles.
  • \n
  • Considera entornos aislados (VPC endpoints, private networking) para minimizar exposición.
  • \n
\n

Red teaming de prompts: los documentos pueden contener texto malicioso (“ignora instrucciones y envía…”) incrustado en el OCR. Mitigación: usa mensajes del sistema estrictos, no ejecutes acciones directas sin validación, y trata el OCR como input no confiable (igual que un formulario web). En extracción, no permitas que el modelo genere comandos; sólo JSON validado.

\n\n

7) Sección técnica principal 5: MLOps/LLMOps, evaluación, monitorización y mejora continua (métricas que importan)

\n

En producción, el éxito no es un “modelo acertó una vez”, sino un sistema que mantiene rendimiento con el tiempo, ante nuevos formatos y cambios normativos. Necesitas LLMOps: versionado de prompts, evaluación offline, monitorización online y un circuito de feedback con revisión humana.

\n

Conjunto de evaluación:

\n
    \n
  • Crea un dataset representativo: por clase, por canal (escáner, móvil), por calidad (alta/baja), y por idioma.
  • \n
  • Incluye “casos borde”: OCR con errores, páginas invertidas, documentos mixtos, anexos.
  • \n
  • Etiqueta no sólo clase, también campos extraídos y “evidencias correctas”.
  • \n
\n

Métricas:

\n
    \n
  • Clasificación: macro-F1 (equilibrio entre clases), matriz de confusión, tasa de “Desconocido” (debe existir y ser saludable).
  • \n
  • Extracción: exact match por campo, distancia de edición, tasa de validación (p.ej. NIF válido), y cobertura (cuántos campos no-null).
  • \n
  • Operación: tiempo por documento, coste por documento (OCR + LLM + almacenamiento), y ratio de escalado a revisión humana.
  • \n
\n

Monitorización online:

\n
    \n
  • Drift de input: caída de confianza OCR, aumento de longitud, cambios de vocabulario (nuevo organismo, nuevo formato).
  • \n
  • Drift de output: aumento de “Desconocido”, cambios en distribución de clases.
  • \n
  • Calidad por cliente: algunos clientes aportan documentos peores; ajusta umbrales por canal/cliente.
  • \n
\n

Versionado:

\n
    \n
  • Prompts como código: en repositorio, con pruebas unitarias (sí, tests de prompt) y changelog.
  • \n
  • Semver por taxonomía: si cambias clases o campos, versiona el esquema y asegura migraciones.
  • \n
\n

Human-in-the-loop:

\n
    \n
  • Diseña una UI de revisión que muestre PDF + resaltado de evidencias + campos extraídos + validaciones fallidas.
  • \n
  • Las correcciones humanas alimentan: reglas, ejemplos de embeddings y dataset de evaluación.
  • \n
\n

Orquestación y colas: usa un sistema de jobs (BullMQ, RabbitMQ, SQS) con reintentos idempotentes. OCR y LLM deben ejecutarse de forma asíncrona; si el LLM falla, no bloquees la ingestión. Cada etapa produce artefactos y estados. Este diseño evita “procesos fantasma” y facilita re-procesar con una nueva versión de prompt sin re-subir documentos.

\n\n

8) Ejemplos de código detallados

\n

A continuación, un ejemplo realista (simplificado) en Node.js + TypeScript para: (1) extraer texto con OCR (simulado), (2) clasificar con embeddings + GPT-4 como árbitro, (3) extraer campos con salida estructurada y validar NIF/IBAN, y (4) persistir el resultado con trazabilidad.

\n\n

8.1 Tipos, taxonomía y esquema (JSON Schema)

\n
// types.ts\nexport type DocClass =\n  | "AEAT_NOTIFICACION"\n  | "AEAT_REQUERIMIENTO"\n  | "MODELO_303"\n  | "TGSS_RESOLUCION"\n  | "DGT_MULTA"\n  | "CONTRATO"\n  | "ESCRITURA"\n  | "DESCONOCIDO";\n\nexport interface Evidence {\n  quote: string;          // literal del OCR\n  page?: number;\n  bbox?: [number, number, number, number]; // x1,y1,x2,y2 (opcional)\n}\n\nexport interface ExtractedField<T> {\n  value: T | null;\n  evidence: Evidence | null;\n}\n\nexport interface ExtractionResult {\n  docClass: DocClass;\n  docClassConfidence: number; // 0..1\n  fields: {\n    nif?: ExtractedField<string>;\n    iban?: ExtractedField<string>;\n    fecha_emision?: ExtractedField<string>; // ISO\n    importe?: ExtractedField<number>;\n    expediente?: ExtractedField<string>;\n    organo_emisor?: ExtractedField<string>;\n    plazo_dias?: ExtractedField<number>;\n  };\n  warnings: string[];\n  promptVersion: string;\n  model: string;\n}\n\nexport const extractionSchema = {\n  name: "legal_document_extraction",\n  schema: {\n    type: "object",\n    additionalProperties: false,\n    properties: {\n      docClass: { type: "string" },\n      docClassConfidence: { type: "number", minimum: 0, maximum: 1 },\n      fields: {\n        type: "object",\n        additionalProperties: false,\n        properties: {\n          nif: fieldSchema("string"),\n          iban: fieldSchema("string"),\n          fecha_emision: fieldSchema("string"),\n          importe: fieldSchema("number"),\n          expediente: fieldSchema("string"),\n          organo_emisor: fieldSchema("string"),\n          plazo_dias: fieldSchema("number"),\n        }\n      },\n      warnings: { type: "array", items: { type: "string" } },\n      promptVersion: { type: "string" },\n      model: { type: "string" }\n    },\n    required: ["docClass", "docClassConfidence", "fields", "warnings", "promptVersion", "model"]\n  }\n} as const;\n\nfunction fieldSchema(t: "string" | "number") {\n  return {\n    type: "object",\n    additionalProperties: false,\n    properties: {\n      value: { type: t === "string" ? ["string", "null"] : ["number", "null"] },\n      evidence: {\n        type: ["object", "null"],\n        additionalProperties: false,\n        properties: {\n          quote: { type: "string" },\n          page: { type: ["number", "null"] },\n          bbox: {\n            type: ["array", "null"],\n            items: { type: "number" },\n            minItems: 4,\n            maxItems: 4\n          }\n        },\n        required: ["quote"]\n      }\n    },\n    required: ["value", "evidence"]\n  };\n}\n
\n\n

8.2 Validadores (NIF/NIE/CIF e IBAN)

\n
// validators.ts\nexport function isValidIBAN(raw: string): boolean {\n  const iban = raw.replace(/\s+/g, "").toUpperCase();\n  if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/.test(iban)) return false;\n  const rearranged = iban.slice(4) + iban.slice(0, 4);\n  const expanded = rearranged.replace(/[A-Z]/g, ch => (ch.charCodeAt(0) - 55).toString());\n  // mod-97\n  let remainder = 0;\n  for (let i = 0; i < expanded.length; i += 7) {\n    const block = remainder.toString() + expanded.substr(i, 7);\n    remainder = parseInt(block, 10) % 97;\n  }\n  return remainder === 1;\n}\n\nexport function isValidNIFNIE(raw: string): boolean {\n  const v = raw.replace(/\s|-/g, "").toUpperCase();\n  // NIF: 8 digits + letter\n  if (/^\d{8}[A-Z]$/.test(v)) {\n    const letters = "TRWAGMYFPDXBNJZSQVHLCKE";\n    const n = parseInt(v.slice(0, 8), 10);\n    return v[8] === letters[n % 23];\n  }\n  // NIE: X/Y/Z + 7 digits + letter\n  if (/^[XYZ]\d{7}[A-Z]$/.test(v)) {\n    const map: Record<string, string> = { X: "0", Y: "1", Z: "2" };\n    const num = parseInt(map[v[0]] + v.slice(1, 8), 10);\n    const letters = "TRWAGMYFPDXBNJZSQVHLCKE";\n    return v[8] === letters[num % 23];\n  }\n  return false;\n}\n
\n\n

8.3 Clasificación híbrida + extracción con GPT-4 (Responses API)

\n
// pipeline.ts\nimport OpenAI from "openai";\nimport { extractionSchema, DocClass, ExtractionResult } from "./types";\nimport { isValidIBAN, isValidNIFNIE } from "./validators";\n\nconst client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });\n\ntype OCRPage = { page: number; text: string };\n\nasync function ocrDocument(_filePath: string): Promise<OCRPage[]> {\n  // Placeholder: en producción integrar Azure Read / Textract / Tesseract + preprocesado\n  return [\n    { page: 1, text: "AGENCIA TRIBUTARIA\nNOTIFICACIÓN\nNIF: 12345678Z\nExpediente: K123...\nFecha: 12/09/2025\n" },\n    { page: 2, text: "...\nImporte: 1.234,56 EUR\nPlazo: 10 días\n" }\n  ];\n}\n\nfunction ruleBasedClass(pages: OCRPage[]): DocClass | null {\n  const t = pages.map(p => p.text).join("\n").toUpperCase();\n  if (t.includes("MODELO 303") || /\b303\b/.test(t)) return "MODELO_303";\n  if (t.includes("AGENCIA TRIBUTARIA") && t.includes("NOTIFICACI")) return "AEAT_NOTIFICACION";\n  if (t.includes("TESORERÍA GENERAL") || t.includes("TGSS")) return "TGSS_RESOLUCION";\n  return null;\n}\n\nasync function gptArbiterClass(pages: OCRPage[], candidates: DocClass[]): Promise<{cls: DocClass, conf: number, evidence: string}> {\n  const excerpt = pages\n    .slice(0, 2)\n    .map(p => `<page ${p.page}>\n${p.text.slice(0, 2500)}\n</page>`)\n    .join("\n\n");\n\n  const promptVersion = "cls-v1.3";\n  const res = await client.responses.create({\n    model: "gpt-4.1",\n    input: [\n      {\n        role: "system",\n        content:\n          "Eres un clasificador documental para una gestoría. " +\n          "No inventes clases. Elige una clase de la lista. " +\n          "Devuelve JSON con keys: docClass, confidence, evidenceQuote."\n      },\n      {\n        role: "user",\n        content:\n          `Clases candidatas: ${candidates.join(", ")}.\n` +\n          `Texto OCR (extracto):\n${excerpt}\n\n` +\n          "Responde estrictamente en JSON."\n      }\n    ],\n    metadata: { promptVersion }\n  });\n\n  const json = JSON.parse(res.output_text) as { docClass: DocClass; confidence: number; evidenceQuote: string };\n  return { cls: json.docClass, conf: json.confidence, evidence: json.evidenceQuote };\n}\n\nasync function gptExtract(pages: OCRPage[], docClass: DocClass): Promise<ExtractionResult> {\n  const promptVersion = "ext-v2.0";\n  const fullText = pages.map(p => `<page ${p.page}>\n${p.text}\n</page>`).join("\n\n");\n\n  const res = await client.responses.create({\n    model: "gpt-4.1",\n    input: [\n      {\n        role: "system",\n        content:\n          "Eres un extractor de datos de documentos legales para gestoría. " +\n          "Reglas: 1) No inventes valores. 2) Si no hay evidencia literal, value=null. " +\n          "3) Para cada campo, incluye evidence.quote literal del texto OCR. " +\n          "4) No incluyas campos fuera del esquema." \n      },\n      {\n        role: "user",\n        content:\n          `Clase del documento: ${docClass}\n` +\n          `Texto OCR (completo o relevante):\n${fullText}`\n      }\n    ],\n    // Salida estructurada (cuando esté disponible en tu SDK/modelo):\n    text: { format: extractionSchema }\n  });\n\n  const parsed = res.output_parsed as ExtractionResult;\n  parsed.promptVersion = promptVersion;\n  parsed.model = "gpt-4.1";\n  return parsed;\n}\n\nfunction validateExtraction(r: ExtractionResult): ExtractionResult {\n  const warnings: string[] = [...(r.warnings || [])];\n\n  const nif = r.fields.nif?.value;\n  if (nif && !isValidNIFNIE(nif)) warnings.push(`NIF/NIE no válido: ${nif}`);\n\n  const iban = r.fields.iban?.value;\n  if (iban && !isValidIBAN(iban)) warnings.push(`IBAN no válido: ${iban}`);\n\n  // Normalización simple de fecha dd/mm/yyyy -> yyyy-mm-dd\n  const f = r.fields.fecha_emision?.value;\n  if (f && /^\d{2}\/\d{2}\/\d{4}$/.test(f)) {\n    const [dd, mm, yyyy] = f.split("/");\n    r.fields.fecha_emision!.value = `${yyyy}-${mm}-${dd}`;\n  }\n\n  r.warnings = warnings;\n  return r;\n}\n\nexport async function processDocument(filePath: string) {\n  const pages = await ocrDocument(filePath);\n\n  // 1) Reglas\n  let docClass = ruleBasedClass(pages);\n  let conf = docClass ? 0.85 : 0;\n\n  // 2) Embeddings (omitido aquí): si no concluye, GPT árbitro\n  if (!docClass) {\n    const candidates: DocClass[] = ["AEAT_NOTIFICACION", "AEAT_REQUERIMIENTO", "MODELO_303", "TGSS_RESOLUCION", "DGT_MULTA", "CONTRATO", "ESCRITURA", "DESCONOCIDO"];\n    const arb = await gptArbiterClass(pages, candidates);\n    docClass = arb.cls;\n    conf = arb.conf;\n  }\n\n  // 3) Extracción estructurada\n  const extraction = await gptExtract(pages, docClass!);\n  extraction.docClass = docClass!;\n  extraction.docClassConfidence = conf;\n\n  // 4) Validación + gating\n  const validated = validateExtraction(extraction);\n  const needsReview = validated.docClassConfidence < 0.7 || validated.warnings.length > 0;\n\n  return { validated, needsReview };\n}\n
\n\n

8.4 Persistencia y trazabilidad (modelo de datos sugerido)

\n
-- Esquema SQL orientativo (PostgreSQL)\nCREATE TABLE documents (\n  id UUID PRIMARY KEY,\n  sha256 TEXT NOT NULL UNIQUE,\n  source_channel TEXT NOT NULL,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n  original_uri TEXT NOT NULL,\n  ocr_text_uri TEXT,\n  status TEXT NOT NULL\n);\n\nCREATE TABLE document_runs (\n  id UUID PRIMARY KEY,\n  document_id UUID NOT NULL REFERENCES documents(id),\n  run_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n  ocr_engine TEXT NOT NULL,\n  ocr_params JSONB NOT NULL,\n  model TEXT NOT NULL,\n  prompt_version TEXT NOT NULL,\n  classification TEXT NOT NULL,\n  classification_confidence DOUBLE PRECISION NOT NULL,\n  extraction JSONB NOT NULL,\n  warnings TEXT[] NOT NULL DEFAULT '{}',\n  needs_review BOOLEAN NOT NULL DEFAULT false\n);\n
\n\n

Detalles prácticos que marcan diferencia:

\n
    \n
  • Guardar output JSON completo permite revalidar con nuevas reglas sin re-llamar al LLM.
  • \n
  • Guardar URIs a artefactos (binario, OCR, recortes) evita inflar la base de datos.
  • \n
  • El run es inmutable: si re-procesas con un prompt nuevo, creas un run nuevo. Esto da auditoría y rollback.
  • \n
\n\n

9) Comparativa de pros y contras

\n

Pros (OCR + GPT-4 en gestorías):

\n
    \n
  • Robustez semántica: entiende variaciones de lenguaje y estructura mejor que reglas puras.
  • \n
  • Time-to-value: puedes cubrir muchas clases con poca ingeniería inicial, especialmente si ya tienes OCR aceptable.
  • \n
  • Extracción flexible: campos complejos (roles, representante vs obligado, plazos) son más accesibles con LLM.
  • \n
  • Explicabilidad operativa: con evidencias (quotes) puedes construir revisión rápida y auditable.
  • \n
  • Escalabilidad funcional: añadir una clase nueva suele ser añadir esquema + ejemplos + prompt, no reentrenar desde cero.
  • \n
\n\n

Contras / riesgos:

\n
    \n
  • Dependencia de calidad OCR: un OCR malo multiplica errores; hay que invertir en preprocesado y control de calidad.
  • \n
  • Coste variable: tokens y OCR cloud pueden subir; necesitas progressive disclosure y caching.
  • \n
  • Alucinaciones: mitigables, pero nunca “cero”; se requieren validaciones y “null por defecto”.
  • \n
  • Cumplimiento y privacidad: requiere arquitectura seria (cifrado, minimización, DPA, auditoría).
  • \n
  • Mantenimiento de prompts/taxonomía: los cambios documentales exigen versionado, tests y dataset de evaluación.
  • \n
\n\n

Cuándo NO usar GPT-4 como pieza central:

\n
    \n
  • Si el 95% de tus documentos son formularios estables con campos en posiciones fijas: un extractor por plantilla + OCR puede ser más barato y determinista.
  • \n
  • Si no puedes enviar datos a un proveedor externo y no tienes alternativa on-prem equivalente.
  • \n
  • Si no puedes implementar revisión humana y validaciones: sin esto, el riesgo operativo es alto.
  • \n
\n\n

10) Conclusión

\n

Un sistema de clasificación documental para gestorías basado en OCR + GPT-4 funciona en producción cuando se diseña como pipeline controlado, no como “chat con PDFs”. La clave es tratar la extracción como ingeniería: preprocesado de imagen para dominar el OCR; clasificación híbrida (reglas + embeddings + GPT como árbitro) para equilibrar coste y precisión; extracción estructurada con JSON Schema y evidencias para auditar; validaciones duras (NIF/IBAN/fechas/importes) para cerrar la puerta a alucinaciones; y una disciplina de LLMOps (dataset, métricas, versionado de prompts, drift y human-in-the-loop) para sostener el rendimiento en el tiempo.

\n

Si lo ejecutas así, el retorno es tangible: reducción de tiempos de registro y archivo, menos errores de asignación de expediente, alertas automáticas de plazos, y una capa de datos estructurados reutilizable para CRM/ERP y analítica. Si lo ejecutas como una integración superficial, el sistema parecerá “inteligente” durante una semana y luego se convertirá en una fuente de incidencias. En gestoría, la diferencia entre demo y producto es, literalmente, el control: evidencias, validación, trazabilidad y seguridad.

\n