Este artículo es el componente de análisis de Enterprise Document Intelligence, una serie que construye un sistema RAG empresarial a partir de cuatro componentes: análisis, análisis de preguntas, recuperación y generación. El análisis es lo primero, y esta es la primera de sus dos partes. Esta parte cubre la primera de las dos capas del análisis: conocer la naturaleza del documento (nacido digital versus escaneado, software fuente, metadatos declarados, TOC nativo) más un breve resumen escrito por un LLM de lo que es. La siguiente parte convierte el contenido en un conjunto relacional de tablas.
En un proceso RAG, el analizador tiene un trabajo. Lea el documento como lo haría un humano antes de responder una pregunta al respecto.
¿Qué es esto? ¿Un CV, un contrato de seguro, un texto normativo, un trabajo académico? ¿Cuántas páginas? ¿Nacido digital o escaneado, o cosido a partir de ambos? ¿Qué contiene: párrafos, tablas, diseño de varias columnas, imágenes incrustadas? ¿En qué idioma?
Cada una de esas comprobaciones es un caso de falla del que el resto del proceso no puede recuperarse:
Un CV exportado desde una plantilla de diseñador. El nombre del candidato aparece en una imagen de logotipo en la parte superior de la página 1, el resto de la página en texto limpio. Cuando se le pregunta "¿cuál es el nombre?", la recuperación no encuentra nada que coincida y vuelve al campo de autor de los metadatos del PDF, que es quien editó el archivo por última vez. La respuesta es incorrecta antes de que se ejecute la generación. Un contrato de seguro con has_text_layer=True. La capa de texto tiene salida OCR con calidad 0,3. La “tarifa de renovación 250 EUR” aparece como “tarifa de renovación1 250 EUR”. La recuperación de palabras clave nunca coincide; La generación lee un número diferente cercano y se compromete con él. Un texto normativo de 200 páginas sin TOC ni encabezados que el analizador pueda detectar. El oleoducto lo trata como una masa homogénea. El analizador de preguntas no tiene idea. La página 4 contiene las definiciones y la página 187 contiene las exclusiones. Un artículo académico con diseño de dos columnas. La extracción ingenua de texto intercala las columnas izquierda y derecha línea por línea. El fragmento recuperado parece un galimatías.
La misma forma cada vez. A un experto le hicieron una pregunta sobre un documento que nunca había abierto. Ellos adivinaron. El oleoducto hizo lo mismo.
El análisis tiene dos capas. Este artículo (5_A) cubre el primero: conocer la naturaleza del documento (nacido digital versus escaneado, software fuente, metadatos declarados, TOC nativo si corresponde) y un breve resumen escrito por un LLM (recuento de páginas, más tres o cuatro oraciones que nombran el tipo de documento, el tema principal, los campos que contiene). El siguiente artículo (5_B) cubre el segundo: conocer el contenido con precisión a través de una base relacional donde cada línea, intervalo, imagen y entrada TOC se convierte en una fila codificada por página y posición.
El artículo utiliza PyMuPDF (también importado como fitz), una biblioteca Python gratuita que lee bytes de PDF directamente. Sin herramientas externas, sin clave API. Lo suficientemente rápido como para ejecutarse en el momento de la ingesta y preciso en archivos PDF digitales. El mismo contrato parse_pdf se puede volver a implementar mediante motores más pesados (Azure Layout, Docling, Camelot, vision-LLM alternativa). Cuando una página exige más profundidad de la que Fitz puede ofrecer, se envía una cascada adaptativa a través de ella. Esa escalada es un tema de seguimiento, más allá del alcance de este artículo.
1. Señales a nivel de documento
Un PDF le brinda dos tipos de información. Señales a nivel de documento: metadatos, marcadores nativos, propiedades declaradas. Contenido a nivel de página: lo que contiene cada página. El analizador los lee en ese orden y confía en el contenido cuando no están de acuerdo.
Los metadatos son un puñado de campos que el PDF entrega en milisegundos. Productor, Creador, marcadores nativos, indicador de cifrado. A nivel de documento, sin páginas móviles. Los lee al comienzo de cada análisis para realizar una llamada de enrutamiento. Exportación de palabras → extracción directa. Escaneo de Kofax → Canalización de OCR. Cualquier cosa ambigua → el contenido pasa más lento.
A veces los metadatos mienten. Ghostscript y qpdf sobrescriben el campo Productor ascendente cuando se recomprimen, por lo que un PDF de Word redestilado dos veces afirmará ser Ghostscript y no le dirá nada sobre el verdadero origen. El asistente expone tanto la etiqueta inferida como las cadenas sin procesar de creador_raw/productor_raw para que las reglas posteriores puedan argumentar.
1.1. software fuente
Un PDF casi siempre anuncia su origen a través de los campos Creador y Productor. Esa única señal nos dice qué tan difícil será el resto del análisis y nos permite dirigirnos a la estrategia correcta antes de abrir cualquier página.
Los productores se dividen en aproximadamente cinco grupos, ordenados del más fácil al más difícil de analizar. "Tablas vectoriales" a continuación significa tablas dibujadas como líneas nativas + texto (las celdas sobreviven como datos); lo opuesto es una tabla aplanada en una sola imagen (solo OCR puede recuperar las celdas).
Herramientas de creación de Office (las más sencillas). Exportaciones de Microsoft Word, PowerPoint, LibreOffice (Writer/Impress), OpenOffice, Google Docs y Slides, Apple Pages y Keynote. Conservan la estructura lógica (encabezados, listas, párrafos) con fuentes vectoriales nativas. La extracción directa de texto funciona bien, el orden de lectura es confiable y las tablas suelen ser tablas vectoriales. La mayor parte de los "documentos de oficina" que verá en un corpus empresarial. Procesadores de documentos. Motores LaTeX (pdfTeX, XeTeX, LuaTeX), Pandoc, Quarto, R Markdown, ReportLab, WeasyPrint. Excelente fidelidad del texto, pero con sus propias peculiaridades: la separación de palabras divide las palabras en líneas, las matemáticas se representan como rutas vectoriales o imágenes (texto no extraíble), las referencias y citas tienen espacios inusuales. Las tablas son tablas vectoriales la mayor parte del tiempo. Herramientas de diseño y publicación. Adobe InDesign, Illustrator, QuarkXPress, Affinity Publisher. Flujo de varias columnas con orden de lectura desordenado. PyMuPDF a menudo obtiene un orden de lectura incorrecto en diseños densos. Las tablas se pueden dibujar como gráficos vectoriales en lugar de tablas vectoriales. Los títulos, las barras laterales y los elementos decorativos complican el análisis. Espere escalar a un analizador que tenga en cuenta el diseño en diseños densos. Tuberías de impresión y recompresores. Impresión desde navegador (Chrome, Safari, Firefox), cuadros de diálogo de impresión a PDF del sistema operativo, Ghostscript, qpdf, herramientas de clase distiller. Calidad mixta. Los archivos PDF impresos desde el navegador conservan el texto pero pierden hipervínculos y marcadores. Ghostscript y qpdf a menudo pasan el contenido pero sobrescriben el campo Productor ascendente, por lo que la señal original desaparece. Es por eso que el asistente expone tanto creador_raw como productor_raw. Software de escáner y aplicaciones de captura (las más difíciles). Kofax, ABBYY, Adobe Scan, ScanSnap, CamScanner, canales de fax. Imagen pura, sin texto nativo. OCR obligatorio. Las aplicaciones de tipo CamScanner añaden problemas de calidad de imagen (desviación, baja resolución, artefactos JPEG) además. def detect_source_software(doc: fitz.Document) -> str: """Clasificar el software de producción utilizando los metadatos del Creador/Productor.""" meta = doc.metadata o {} combinado = f"{(meta.get('creator') o '').lower()} {(meta.get('producer') o '').lower()}" # Grupo 1: herramientas de creación de Office si "microsoft" está combinado y "word" combinado: devuelve "word_export" si "pdfmaker" está combinado y "word" combinado: devuelve "word_export" si "powerpoint" está combinado: devuelve "powerpoint_export" si lo hay(s en combinado para s in ("libreoffice", "openoffice")): devuelve "libreoffice_export" # Grupo 2: procesadores de documentos, si los hay(s en combinado para s in ("pdftex", "xetex", "luatex")): devuelve "latex_export" si "pandoc" en combinación: devolver "pandoc_export" # Grupo 3: herramientas de diseño y publicación si "indesign" está combinado: devolver "indesign_export" # Grupo 4: canalizaciones de impresión y recompresores si "ghostscript" en combinación: devolver "ghostscript" si hay alguno combinado para s en ("chrome", "safari", "firefox"): devolver "browser_print" # Grupo 5: software de escáner (OCR obligatorio) si hay alguno en combinado para s en ("kofax", "abbyy", "adobe scan", "scansnap", "camscanner")): devuelve "scanner_software" devuelve "unknown_source"
La detección es imperfecta: un PDF de Word redestilado a través de Ghostscript tendrá su Productor sobrescrito y los productores raros caerán en desconocido_fuente. En un corpus mixto de documentos, contratos escaneados, informes impresos en el navegador y exportaciones de Office, aproximadamente nueve de cada diez archivos PDF llegan al grupo correcto en la primera lectura. Suficiente para impulsar las rutas. Exponemos tanto la etiqueta source_software inferida como las cadenas sin procesar Creator_raw / Producer_raw para que las reglas posteriores puedan compensar cuando sea necesario.
El resto de este artículo se incluye en dos archivos PDF de demostración: el documento Attention Is All You Need (Vaswani et al. 2017; licencia de distribución no exclusiva de arXiv, declarada en la página de resumen de arXiv) y el NIST Cybersecurity Framework 2.0 (CSWP-29; trabajo del gobierno de EE. UU., dominio público en EE. UU., consulte la declaración de derechos de autor del NIST). El detector los coloca en dos cubos diferentes:
1.2. Tabla de contenido nativa
PyMuPDF expone el esquema del documento a través de doc.get_toc(), que devuelve una lista de tripletas [nivel, título, página]. build_toc_df envuelve eso y agrega parent_idx y ruta de navegación para que la jerarquía sea consultable.
Ejecútelo en el documento Attention Is All You Need (Vaswani et al. 2017; licencia de distribución no exclusiva de arXiv, declarada en la página de resumen de arXiv) y obtendrá una estructura real de tres niveles:
Cuando el documento tiene un TOC, lo tratamos como una estructura declarada: el documento nos dice cómo se organiza.
Cuando el documento no tiene TOC (la mayoría de los escaneos y exportaciones rápidas), get_toc() devuelve una lista vacía. Reconstruir un TOC a partir de señales tipográficas (líneas grandes en negrita, patrones de numeración) es un problema aparte, fuera del alcance de este artículo.
1.3. Otras propiedades declaradas
Cifrado (doc.is_encrypted, doc.needs_pass), campos de formulario (doc.is_form_pdf), firmas digitales, fechas de creación y modificación. Todo barato de leer. Algunos son importantes para analizar el enrutamiento (los archivos PDF cifrados deben manejarse); la mayoría son importantes a nivel del corpus (versiones, auditoría, control de acceso) y están cubiertos en los artículos del corpus (artículos 15 a 20).
2. Qué contiene cada página
Una vez leídos los metadatos, recorremos las páginas. El contenido es la verdad básica: cuando un PDF dice ser una exportación de Word pero cada página es un escaneo que alguien pegó en Word y reexportó, solo el contenido lo capta. Los metadatos dicen una cosa, los cuadros delimitadores dicen otra. Creemos en los cuadros delimitadores.
Para cada página extraemos elementos de contenido en orden de prioridad.
2.1. Texto y modo de renderizado
El texto es el entregable más importante. La unidad natural es la línea: una línea lleva una cadena junto con su cuadro delimitador (el rectángulo que la encierra en la página), la tipografía dominante (fuente, tamaño, negrita, cursiva, color) y una bandera crítica, el modo de renderizado: un código a nivel de PDF que nos dice si el texto fue escrito de forma nativa o colocado de manera invisible mediante una capa de OCR encima de una imagen escaneada.
raw = page.get_text("rawdict") nativo_chars = 0 ocr_chars = 0 para bloque en raw["blocks"]: si bloque["type"] != 0: continuar para línea en bloque["lines"]: para intervalo en línea["spans"]: if span.get("render_mode", 0) == 3: ocr_chars += len(span["text"]) else: nativo_chars += len(lapso["texto"])
El modo de renderizado 3 significa que el texto se dibuja de forma invisible: una capa que el software OCR coloca debajo de la imagen de la página para que se pueda realizar búsquedas en el escaneo. El texto está ahí, pero sólo como caracteres ocultos. Distinguir el modo de renderizado 3 del texto nativo es importante: es la única forma confiable de saber si una página escaneada ya tiene una capa utilizable para realizar búsquedas o si necesita volver a realizar un OCR.
Yendo más allá: cuando la tipografía varía dentro de una sola línea (una palabra en negrita en medio de una oración, un título en color, una etiqueta rotada), capturarla requiere bajar al nivel de extensión. Presentamos la extracción a nivel de intervalo en la sección 3 de este artículo, porque algunas etapas posteriores (detección de encabezados, agregación de listas en respuestas largas) la necesitan.
2.2. Imágenes y cobertura a página completa
Las imágenes ocupan el segundo lugar porque a menudo contienen texto o información visual crítica que, de otro modo, el oleoducto RAG perdería. Los logotipos identifican al emisor. Los esquemas describen sistemas. Las fotografías documentan la evidencia. Las tablas exportadas como imágenes contienen datos.
Para cada imagen incrustada registramos su cuadro delimitador mostrado (en puntos PDF), sus dimensiones intrínsecas (en píxeles) y un hash de contenido para la deduplicación. La imagen también se extrae y persiste (S3 o almacenamiento local) para que las etapas posteriores puedan procesarla.
Un error común: page.get_images() devuelve las dimensiones intrínsecas de cada imagen, no el área mostrada en la página. Para calcular la cobertura real, utilice page.get_image_info(), que devuelve el cuadro delimitador en puntos PDF tal como se representa.
page_area = page.rect.width * page.rect.height max_coverage = 0.0 para información en page.get_image_info(): bbox = info["bbox"] img_area = (bbox[2]- caja b[0]) * (bbox[3]- caja b[1]) cobertura_máxima = máx(cobertura_máxima, área_img / área_página) has_imagen_página_completa = cobertura_máxima >= 0,95
Una página en la que una imagen cubre ≥ 95% de la superficie es, con una probabilidad muy alta, una página escaneada. El umbral del 95% es empírico: tolera los pequeños márgenes que un escáner agrega alrededor del borde de la página sin detectar páginas que legítimamente usan una imagen principal grande dentro de un diseño. Los valores del 90% al 99% funcionan en la práctica; ajuste el umbral si su corpus tiene muchas portadas a sangrado completo, aflójelo si los escáneres recortan demasiado.
2.3. Tablas vectoriales
Las tablas no se fragmentan como si se ejecutara texto. La linealización ingenua destruye la semántica celular. La etapa de análisis señala su presencia y ubicación; la extracción estructurada real ocurre en un paso de análisis adaptativo de seguimiento que escala a un motor consciente del diseño cuando la suposición de fila de Fitz parece poco confiable.
PyMuPDF (desde 1.23) detecta tablas vectoriales, aquellas creadas a partir de líneas dibujadas combinadas con texto nativo, a través de page.find_tables(). La llamada devuelve un objeto TableFinder cuya lista .tables tiene una entrada por tabla detectada: tablas = page.find_tables(); n_tables = len(tablas.tablas).
Para tablas escaneadas representadas como imágenes, find_tables() no se activa. En ese caso, la detección requiere herramientas visuales (Camelot, Docling, PaddleStructure), que están fuera del alcance de este artículo.
2.4. Columnas: izquierda, derecha, única, múltiple
La detección de columnas es difícil. Los diseños de dos columnas rompen el orden de lectura ingenuo: un trabajo de investigación analizado sin conocimiento de las columnas devuelve la línea 1 de la columna 1, luego la línea 1 de la columna 2, luego la línea 2 de la columna 1, y así sucesivamente, uniendo oraciones de diferentes columnas en ruido. Tres o más columnas hacen que cualquier heurística razonable sea inestable.
El movimiento pragmático es anotar cada línea donde se ubica horizontalmente en lugar de intentar recuperar un orden de lectura perfecto. Agregamos un campo column_position a line_df con cuatro valores:
single: la página tiene una columna. izquierda/derecha: la página tiene dos columnas; la línea cae en uno u otro. multi: la página tiene tres o más columnas; lo marcamos en lugar de adivinar.
La detección agrupa el borde izquierdo de cada línea a lo largo del eje x: en line_df ese borde es bbox_x0, la coordenada x donde comienza la línea. Una página donde cada línea comienza aproximadamente en la misma banda horizontal es de una sola columna. Una página con dos bandas claras tiene dos columnas y dividimos las líneas según la banda en la que caen. La página 4 del artículo de Atención nos dará los números reales en la sección 4.2: x0 ≈ 148 para la columna de la izquierda, x0 ≈ 364 para la columna de la derecha.
def asignar_column_positions( line_df: pd.DataFrame, gap_threshold: float = 80.0, min_cluster_fraction: float = 0.10, ) -> pd.DataFrame: """Agregue un campo `column_position`: single / left / right / multi.""" out = line_df.copy() out["column_position"] = "single" para _, sub en line_df.groupby("page_num"): x0_values = sub["x0"].tolist() si no x0_values: continuar clusters = _cluster_x0(x0_values, gap_threshold) sig = _significant_clusters(clusters, len(x0_values), min_cluster_fraction) n_cols = max(1, len(sig)) if n_cols == 1: continuar si n_cols == 2: c1_center = suma(sig[0]) / len(sig[0]) c2_centro = suma(sig[1]) / len(sig[1]) split = (c1_center + c2_center) / 2 left_idx = sub.index[sub["x0"] < split] right_idx = sub.index[sub["x0"] >= split] out.loc[left_idx, "column_position"] = "left" out.loc[right_idx, "column_position"] = "right" else: out.loc[sub.index, "column_position"] = "multi" regresar
El valor predeterminado gap_threshold es 80 puntos PDF (1 punto PDF = 1/72 de pulgada, es decir, 80 ≈ 2,8 cm). Ese es el ancho típico del margen entre columnas en un documento estilo NeurIPS o en un documento de política de dos columnas. Cualquier cosa más estrecha es más probable que sea una sangría de párrafo que un salto de columna.
¿Por qué molestarse con izquierda/derecha? El caso de uso que le otorga a este campo su lugar son los datos estructurados en la página donde la posición es el esquema. En las facturas, la dirección del emisor se encuentra en la parte superior izquierda, la dirección del cliente se encuentra en la parte superior derecha (o al revés, según la plantilla). Pedirle al recuperador que extraiga "el bloque de clientes" de la mitad derecha de la página 1 es mucho más natural que pedir un bbox exacto. El mismo patrón aparece en formularios, extractos y contratos con un bloque de encabezado. Una vez que el campo está en line_df, las etapas posteriores pueden filtrar por column_position == "right" como cualquier otra consulta de tabla.
El usuario también puede señalarlo directamente. Los operadores familiarizados con sus documentos dirán "la respuesta está en la columna de la izquierda" o "el número de póliza está a la derecha". Esa oración es una consulta sobre column_position, no una tarea de visión.
Dos columnas es donde esta etiqueta se mantiene. Con tres o más columnas, "izquierda versus derecha" pierde significado y marcamos la página como múltiple en lugar de adivinar. Periódicos, manuales de referencia densos y páginas con márgenes laterales son los casos a tener en cuenta. Cuando column_position == "multi" aparece en una página que importa, es una señal para escalar a un analizador que tenga en cuenta el diseño.
Aquí reside un modo de falla frecuente de las tuberías RAG “mínimas”. El autor realiza pruebas en un documento de Word (column_position == "single" en todas partes), la recuperación funciona, luego un cliente envía un informe anual de dos columnas y el sistema comienza a devolver oraciones cortadas a la mitad. El error parece un problema de generación (“el modelo no puede leer”); la causa es un problema de análisis (para empezar, las líneas nunca estuvieron en el orden correcto).
2.5. Clasificación de páginas
Una vez recopiladas las señales por página, cada página recibe un tipo primario (mutuamente excluyente) y indicadores aditivos (booleanos independientes).
Los tipos primarios:
Las banderas aditivas describen lo que contiene la página, independientemente del tipo:
has_text / has_native_text / has_ocr_layer: cualquier texto presente; cualquier texto nativo (no OCR); cualquier capa invisible de OCR. has_image/has_full_page_image: cualquier imagen incrustada; una imagen que cubra ≥ 95% de la página. has_vector_table: al menos una tabla detectada mediante page.find_tables() (líneas + texto nativo, no acoplado a una imagen). has_vector_graphics: la página contiene trazados dibujados que NO son una tabla de vectores (gráficos, esquemas, formas decorativas, figuras matemáticas). Vale la pena señalarlo porque se trata de contenido PDF que el extractor de texto no ve como nada.
Separar el tipo de las banderas nos permite cruzar criterios: "todas las páginas con una tabla de vectores" independientemente del tipo, "páginas mixtas que también contienen una tabla", etc.
El clasificador consume un objeto PageFeatures: el subconjunto de señales por página que necesita para decidir. El text_quality_score en ese objeto tiene una proporción de 0 a 1: 0 significa que el texto de la página está confuso (alta proporción de caracteres no reconocidos), 1 significa texto nativo limpio. Una cascada adaptativa lo construye completamente a partir de señales sin procesar; aquí es sólo una entrada al clasificador:
@dataclass clase PageFeatures: char_count: int n_fonts: int n_images: int has_full_page_image: bool nativo_chars: int ocr_chars: int text_quality_score: float
En este artículo se encuentran tres vistas de "campos a nivel de página". El esquema (el diagrama page_df a continuación en la sección 3.3) enumera todos los campos a los que se dirige el modelo de datos. PageFeatures es el subconjunto de lecturas de classify_page. El ejemplo actual de page_df es el triplete central que el paquete construye hoy: los indicadores aditivos del esquema aparecen progresivamente a medida que las etapas posteriores los solicitan.
La lógica de clasificación en sí es breve:
def classify_page(características: PageFeatures) -> str: si características.char_count < 10 y características.n_images == 0: devuelve "vacío" si características.n_fonts == 0 y características.has_full_page_image: devuelve "escaneado" si (características.has_full_page_image y características.ocr_chars > características.native_chars y características.ocr_chars > 50): devuelve "scanned_ocr_good" si características.text_quality_score >= 0.7 más "scanned_ocr_bad" si características.has_full_page_image y características.native_chars > 50: devuelve "mixto" si características.n_fonts > 0 y características.native_chars > 0: devuelve "native_with_image" si características.n_images > 0 más "nativo" devolver "desconocido"
Las señales decisivas son estructurales, no estadísticas: fuentes declaradas, modo de renderizado, cobertura de la imagen mostrada. Nunca nos basamos únicamente en los umbrales de recuento de caracteres para decidir si son nativos o escaneados. Una página nativa con tres líneas de texto sigue siendo nativa.
Yendo más allá: la puntuación de calidad del OCR (la text_quality_score utilizada anteriormente) merece su propio tratamiento. Las dos señales confiables son la proporción de caracteres de reemplazo Unicode y la proporción de palabras que se encuentran en un diccionario. Deben evitarse listas de “personajes sospechosos” como ●◦•; esas son viñetas perfectamente legítimas en documentos formateados. El proceso de puntuación completo es un tema de seguimiento.
3. La zona semántica de parsing_summary: una llamada de LLM, calificación de aviso del sistema
Las secciones 1 y 2 repasaron el NIST CSF y el documento Atención, ambos ricos en señales estructurales. La sección 3 pasa a un tipo de documento donde la estructura por sí sola no resuelve nada: el CV de una página. El ejemplo actual es un CV ficticio, Sarah Mitchell, analista de datos.
Las señales de las secciones 1 y 2 son todo lo que un analizador determinista puede producir en unos pocos segundos sin una llamada al modelo. Nos dicen qué es el documento y cómo está presentado. No nos dicen de qué se trata. Se harán dos páginas de una sola página con el mismo recuento de páginas, el mismo diseño de una sola columna y el mismo productor de word_export que aún difieren en cada recuperación de preguntas.
Un breve resumen en prosa cierra esa brecha. Una llamada de LLM en el momento del análisis, alimentó las primeras una o dos páginas, pidió que devolviera tres o cuatro oraciones nombrando el tipo de documento, el tema principal y los campos que contiene. Alrededor de doscientas fichas. Se almacena en caché para siempre, ya que el análisis se ejecuta una vez por documento. El resultado llega a tres campos del mismo dictado de nivel de documento (parsing_summary): tipo_doc, campos_típicos y resumen.
Ejecute ese CV limpio mediante el análisis y la zona semántica de parsing_summary se leerá así:
{ "doc_type": "currículum", "típico_fields": ["nombre", "correo electrónico", "teléfono", "experiencia", "educación", "habilidades", "idiomas"], "summary": "Currículum de una página de Sarah Mitchell, analista de datos con sede en Londres con aproximadamente cuatro años de experiencia. Enumera puestos en Northwind Retail y Brightwave Insurance, una licenciatura en estadística de Leeds y habilidades en Python, SQL, BigQuery y Power BI. Secciones de CV estándar: Resumen, experiencia, educación, habilidades." }
Colocado en el mensaje del sistema del analizador de preguntas, esto soluciona el problema "¿cuál es el nombre?" caso del abridor. El analizador ahora ve que este documento trata sobre Sarah Mitchell antes de ver la pregunta del usuario. El nombre ya no es una palabra de rol ambigua que busca una ocurrencia literal. El analizador sabe que el nombre del candidato es Sarah Mitchell y dirige la pregunta en esa dirección.
Los mismos tres campos funcionan para cada pregunta del mismo documento. “¿Dónde trabajó?” Ahora tiene un referente. "¿Cuál es su pila tecnológica?" se asigna a la sección Habilidades que figura en los campos_típicos. El recuento de páginas avanza de forma gratuita en el mismo dictado: "resumir la página 1" en un CV de una página se convierte en "resumir todo el documento", se omite la recuperación, la generación lee el contenido completo.
La forma del campo de resumen importa más que su longitud. Un puñado de reglas de trabajo:
Tres o cuatro frases, prosa sencilla, registro fáctico. Sin tono de marketing (“un CV brillante con muchos logros” envenena cada respuesta posterior con afirmaciones que el analizador luego propagará). Abra con el tipo de documento y el asunto principal: “Currículum vitae de una página de Sarah Mitchell, analista de datos…”. El analizador utiliza el primer sintagma nominal para eliminar la ambigüedad de palabras de rol como nombre, rol, empleador. Enumere las secciones estándar cuando existan: “Secciones estándar de CV: Resumen, Experiencia, Educación, Habilidades”. El analizador utiliza esto para asignar temas de preguntas a ámbitos de recuperación. Cíñete a los hechos que un lector pueda verificar en la primera o segunda página. No hay afirmaciones sobre contenido que el LLM no haya visto.
Un conjunto de CV ficticios con la misma forma (una o dos páginas, el candidato en la parte superior, secciones debajo) pero con diferentes diseños y calidad de contenido enfatiza esta disciplina. Un resumen que dice "currículum de,, con" se generaliza en todos ellos. Un resumen que deriva en opciones de renderizado (“diseño de dos columnas con una barra lateral coloreada”) se adapta a un archivo y se divide en el siguiente.
Esta es la pieza que convierte la mirada en la metáfora del documento en algo que un chatbot puede usar. Las señales deterministas de las secciones 1 y 2 dicen cómo analizar. La zona semántica de parsing_summary dice lo que se analizó. Juntos forman el dictado a nivel de documento que lee cada bloque posterior, comenzando con el mensaje del sistema del analizador de preguntas.
Todo esto aparece en Enterprise Document Intelligence, la aplicación de escritorio que estoy creando. La captura de pantalla a continuación tiene el mismo CV ficticio abierto, con los campos de contexto del documento mostrados y resaltados en la página: nombre del candidato, función objetivo, años de experiencia. El breve resumen escrito una vez en el momento del análisis es lo que impulsa ese panel.
Conclusión
Un PDF consta de dos documentos apilados uno encima del otro: las señales declaradas (metadatos, TOC nativo, software fuente) y el contenido a nivel de página (texto versus escaneo, imágenes, tablas, columnas, perfil de página). El analizador los lee en ese orden y confía en el cuerpo cuando los dos no están de acuerdo. Un breve campo de resumen escrito por LLM, pagado una vez por documento y almacenado en caché, se encuentra junto a ellos en el mismo diccionario parsing_summary, y el analizador de preguntas lo lee como parte del mensaje del sistema en cada llamada.
Cada señal guardada en el momento del análisis se convierte en una columna que lee el resto de la canalización. Cada decisión a nivel de página dirige la página al controlador descendente correcto: las páginas de texto puro pasan por el OCR-skip, las páginas con muchas tablas pasan por una ruta de extracción estructurada, las páginas de varias columnas obtienen un orden de lectura con reconocimiento de columnas. La diferencia entre un analizador que envía una cadena plana y un analizador que envía algo que el código posterior puede consultar está aquí, en las señales que se molestó en registrar.
El siguiente artículo (“Deje de devolver texto plano desde un PDF: la forma relacional que RAG necesita”) le mostrará los ocho DataFrames que el analizador produce a partir de estas señales, demostrados en dos documentos reales. Los mismos DataFrames son la entrada que el proceso mínimo de RAG consume de extremo a extremo y se encuentran dentro de la serie más amplia Enterprise Document Intelligence.
Fuentes y lecturas adicionales
Al principio de la serie:
El analizador que describe este artículo sigue la misma arquitectura que Docling (Auer et al., Docling Technical Report, IBM Research 2024): detección de diseño, TableFormer, orden de lectura. La extracción de tablas sin bordes utiliza el modelo de Smock et al. (PubTables-1M / Transformador de mesa, CVPR 2022). La taxonomía de clases de páginas se basa en la misma base que Pfitzmann et al. (DocLayNet, KDD 2022). El artículo agrega un pase de detección del modo de renderizado (nativo/escaneado/mixto) con puntuación de calidad OCR en la parte superior. El analizador produce un conjunto relacional de tablas (line_df, page_df, image_df, toc_df, object_registry, cross_ref_df, span_df, más un dict parsing_summary); la recuperación, generación y anotación posteriores no vuelven a leer el PDF, consultan DataFrames.
Misma dirección que el artículo:
Auer et al., Informe técnico Docling, IBM Research 2024 (arXiv:2408.09869). Arquitectura de referencia para la canalización que describe este artículo: detección de diseño, TableFormer, orden de lectura, representación de documentos unificada. Smock, Pesala, Abraham, PubTables-1M / Table Transformer (TATR), CVPR 2022 (arXiv:2110.00061). Detección de tablas y reconocimiento de estructuras basado en visión; el modelo detrás de la mayoría de los analizadores de tablas modernos. Pfitzmann et al., DocLayNet, KDD 2022 (arXiv:2206.01062). Línea de base empírica para la taxonomía de clases de páginas y los puntos de referencia de detección de diseño. Lo et al., PaperMage, demostraciones de EMNLP 2023. Se asigna a la división de indexación versus lectura (el análisis para la recuperación no es un análisis para la generación de respuestas).
Ángulo diferente, contexto diferente:
Faysse et al., ColPali: Recuperación eficiente de documentos con modelos de lenguaje visual, 2024 (arXiv:2407.01449). Recuperación visión-lenguaje en la imagen de la página. El contexto es de recuperación donde la imagen de la página es el artefacto, sin paso de análisis en tablas. En su lugar, este artículo utiliza DataFrames anclados en cuadros delimitadores como base. Wang et al., DocLLM: un modelo de lenguaje generativo compatible con el diseño para la comprensión de documentos multimodales, JPMorgan 2024 (arXiv:2401.00908). LLM compatible con el diseño que lee el PDF directamente sin un bloque de análisis relacional explícito. Misma familia de enfoque que ColPali; diferente del artefacto relacional consultable de este artículo. Kim et al., Transformador de comprensión de documentos sin OCR (Donut), ECCV 2022 (arXiv:2111.15664). Comprensión de documentos sin OCR de extremo a extremo; contraste útil con el pase de puntuación de calidad de OCR que este artículo agrega además de la detección del modo de renderizado.