1) Introducción detallada
\nEn 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.
\nLa 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.
\nEste 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\n2) Contexto histórico o teórico
\nLa 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.
\nTeó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\n3) Sección técnica principal 1: Ingesta, normalización y preprocesado (calidad de OCR como variable controlada)
\nAntes 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.
\nPipeline recomendado:
\n1) 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.
\n2) 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.
\n3) 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
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.
\n5) 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
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.
\nClave: 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\n4) Sección técnica principal 2: Estrategias de clasificación (híbrido reglas + embeddings + GPT-4)
\nClasificar 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.
\nEscaló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.
\nEscaló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.
\nEscaló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).
\nDiseñ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
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).
\nControl 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\n5) Sección técnica principal 3: Extracción estructurada con GPT-4 (JSON Schema, evidencia y anti-alucinación)
\nClasificar 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.
\nPatró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
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
Estrategia de extracción por etapas:
\n1) 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.
\nEvitar 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
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\n6) Sección técnica principal 4: Seguridad, RGPD/LOPDGDD, auditoría y gobierno del dato (lo que separa una demo de producción)
\nUn 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.
\nPrincipios 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
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
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
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
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\n7) Sección técnica principal 5: MLOps/LLMOps, evaluación, monitorización y mejora continua (métricas que importan)
\nEn 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.
\nConjunto 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
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
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
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
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
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\n8) Ejemplos de código detallados
\nA 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\n8.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\n8.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\n8.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\n8.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\nDetalles 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
9) Comparativa de pros y contras
\nPros (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
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
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
10) Conclusión
\nUn 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.
\nSi 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