Cuando enviamos la primera versión de nuestra base de conocimientos interna, recibí un mensaje de Slack de un colega del equipo de cumplimiento. Le había preguntado al sistema sobre nuestro proceso de incorporación de contratistas. La respuesta fue segura, bien estructurada y equivocada exactamente en la forma que más importa en el trabajo de cumplimiento: describió el proceso general pero omitió la cláusula de excepción que se aplicaba a los contratistas en proyectos regulados.
La excepción estaba en el documento. Había sido ingerido. El modelo de incrustación lo había codificado. El LLM, dado el contexto adecuado, lo habría manejado sin dudarlo. Pero el sistema de recuperación nunca lo sacó a la luz porque el fragmento que contenía esa excepción se había dividido justo en el límite del párrafo donde terminaba la regla general y comenzaba la calificación.
Recuerdo haber abierto los registros de fragmentos y mirar dos registros consecutivos. El primero terminaba a mitad de discusión: 'Los contratistas siguen el proceso de incorporación estándar como se describe en la Sección 4…' El segundo comenzaba de una manera que no tenía sentido sin su predecesor: '…a menos que estuvieran involucrados en un proyecto clasificado bajo el Anexo B, en cuyo caso…'. Cada trozo, de forma aislada, era un fragmento. Juntos, contenían una información completa y crítica. Por separado, ninguno de los dos era recuperable de manera significativa.
La canalización se veía bien en nuestras consultas de prueba. El oleoducto no estaba bien.
En ese momento, el mensaje de cumplimiento de Slack y el registro de fragmentos abiertos uno al lado del otro es donde dejé de tratar el fragmento como un detalle de configuración y comencé a tratarlo como la decisión de diseño más importante en la pila. Todo lo que sigue es lo que aprendí después, en el orden en que lo aprendí.
Esto es lo que encontré y cómo lo encontré.
En este artículo
Qué es el fragmentación y por qué la mayoría de los ingenieros lo subestiman La primera grieta: fragmentación de tamaño fijo Cada vez más inteligente: ventanas de oraciones cuando sus documentos tienen estructura: fragmentación jerárquica La opción atractiva: fragmentación semántica El problema del que nadie habla: archivos PDF, tablas y diapositivas Un marco de decisión, no una clasificación Lo que RAGAS le dice sobre sus fragmentos Dónde nos deja esto
Qué es el fragmentación y por qué la mayoría de los ingenieros lo subestiman
En el artículo anterior, describí la fragmentación como "el paso en el que la mayoría de los equipos se equivocan". Lo mantengo y permítanme intentar explicarlo con muchos detalles.
Si aún no lo ha leído, consúltelo aquí: Una guía práctica de RAG para bases de conocimiento empresariales
Una canalización RAG no recupera documentos. Recupera trozos. Cada respuesta que su sistema produce se genera a partir de una o más de estas unidades, no del documento fuente completo, no de un resumen, sino del fragmento específico que su sistema de recuperación encontró lo suficientemente relevante como para pasarlo al modelo. La forma de ese fragmento determina todo lo que sucede aguas abajo.
Esto es lo que eso significa concretamente. Un fragmento que es demasiado grande contiene múltiples ideas: la incrustación que lo representa es un promedio de todas ellas, y ninguna idea tiene la puntuación suficiente para ganar un concurso de recuperación. Un fragmento demasiado pequeño es preciso pero está varado: una oración sin el párrafo que la rodea a menudo no es interpretable y el modelo no puede generar una respuesta coherente a partir de un fragmento. Un fragmento que trasciende un límite lógico supone una excepción para el contratista: la información completa se divide en dos partes incompletas, cada una de las cuales parece irrelevante de forma aislada.
La fragmentación se encuentra antes de cada modelo en su pila. Su modelo de incrustación no puede arreglar una parte defectuosa. Tu reclasificador no puede resurgir un fragmento que nunca se recuperó. Su LLM no puede responder desde un contexto que nunca se le proporcionó.
La razón por la que esto se subestima es que falla silenciosamente. Un error de recuperación no genera una excepción. Produce una respuesta que es casi correcta, plausible, fluida y sutilmente incorrecta en el sentido que importa. En una demostración, con consultas cuidadosamente seleccionadas, casi lo correcto está bien. En producción, con la distribución completa de preguntas de usuarios reales, se produce una lenta erosión de la confianza. Y el sistema que erosiona silenciosamente la confianza es más difícil de arreglar que uno que se rompe ruidosamente.
Aprendí esto de la manera más difícil. Lo que sigue es la progresión de las estrategias que trabajé, no una taxonomía de opciones, sino una secuencia de problemas y el pensamiento que llevó de uno al siguiente.
La primera grieta: fragmentación de tamaño fijo
Empezamos donde empiezan la mayoría de los equipos: fragmentación de tamaño fijo. Divida cada documento en ventanas de 512 tokens, con superposición de 50 tokens. Tomó una tarde para configurarlo. Las primeras demostraciones se veían bien. Nadie lo cuestionó.
de llama_index.core.node_parser importar analizador TokenTextSplitter = TokenTextSplitter( chunk_size=512, chunk_overlap=50 ) nodos = parser.get_nodes_from_documents(documentos)
La lógica es intuitiva. Los modelos integrados tienen límites simbólicos. Los documentos son largos. Divídalos en ventanas fijas y obtendrá un índice uniforme y predecible. La superposición garantiza que la información que cruza fronteras tenga una segunda oportunidad. Sencillo, rápido y completamente indiferente a lo que realmente dice el texto.
Esa última parte “completamente indiferente a lo que dice el texto” es el problema. El tamaño fijo no sabe nada acerca de dónde termina una oración, o que una excepción de política de tres párrafos debe permanecer junta, o que dividir una lista numerada en el paso cuatro produce dos fragmentos inútiles. Es una operación mecánica aplicada a un objeto semántico, y la discrepancia eventualmente aparecerá en su recuerdo de contexto.
Para nuestro corpus: páginas de Confluence, políticas de recursos humanos, runbooks de ingeniería, apareció en el recuerdo del contexto. Cuando realicé nuestra primera evaluación RAGAS, estábamos en 0,72 en recuperación de contexto. Eso significaba que aproximadamente a una de cada cuatro consultas le faltaba información que existía en el corpus. Para una base de conocimientos interna, eso no es un error de redondeo. Ese es el mensaje de cumplimiento de Slack, esperando a suceder.
Cuando funciona el tamaño fijo: documentos breves y uniformes donde cada sección es independiente, como preguntas frecuentes sobre productos, resúmenes de noticias y descripciones de tickets de soporte. Si su corpus parece una lista de entradas independientes, el tamaño fijo le será de gran utilidad y la simplicidad es una ventaja genuina.
La superposición ayuda. Pero hay un límite en cuanto a cuánto puede compensar una superposición de 50 tokens por un párrafo de 300 tokens que cruza el límite de un fragmento. Es un parche, no una solución. Lo mantuve en nuestra cartera para un subconjunto de documentos en los que realmente encajaba. Para todo lo demás, comencé a buscar en otra parte.
Cada vez más inteligente: ventanas de oraciones
El problema de la excepción del contratista, una vez que lo entendí claramente, apuntó directamente a lo que se necesitaba: una forma de recuperar con la precisión de una sola oración, pero generar con el contexto de un párrafo completo. Ni uno ni el otro, ambos, en diferentes etapas del proceso.
LlamaIndex SentenceWindowNodeParser está diseñado exactamente para esto. En el momento de la indexación, crea un nodo por oración. Cada nodo lleva la oración en sí como texto recuperable, pero almacena la ventana circundante de oraciones, tres a cada lado por defecto, en sus metadatos. En el momento de la consulta, el recuperador encuentra la frase más relevante. En el momento de la generación, un posprocesador lo expande nuevamente a su ventana antes de que el contexto llegue al LLM.
from llama_index.core.node_parser import SentenceWindowNodeParser from llama_index.core.postprocessor import MetadataReplacementPostProcessor # Tiempo de índice: un nodo por oración, ventana almacenada en el analizador de metadatos = SentenceWindowNodeParser.from_defaults( window_size=3, window_metadata_key="window", original_text_metadata_key="original_text" ) # Tiempo de consulta: expandir la oración nuevamente a la ventana circundante postprocesador = MetadataReplacementPostProcessor( target_metadata_key="ventana")
La excepción de cumplimiento que había sido invisible para la tubería de tamaño fijo se volvió recuperable de inmediato. La frase "a menos que participe en un proyecto clasificado en el Anexo B" obtuvo una puntuación alta en una consulta sobre excepciones a la incorporación de contratistas porque contenía exactamente esa información, sin diluirla. Luego, la expansión de la ventana le dio al LLM las tres oraciones anteriores y posteriores, lo que proporcionó el contexto para generar una respuesta completa y precisa.
En nuestro conjunto de evaluación, la recuperación del contexto pasó de 0,72 a 0,88 después de cambiar a ventanas de oraciones para nuestros tipos de documentos narrativos. Esa única mejora métrica valió varios días de depuración combinados.
Donde fallan las ventanas de oraciones: tablas y bloques de código. Un analizador de oraciones no tiene el concepto de fila de tabla. Dividirá una tabla de seis columnas en docenas de nodos de una sola oración, cada uno de los cuales no tiene sentido de forma aislada. Si su corpus contiene datos estructurados, como la mayoría de los corpus empresariales los contienen, las ventanas de oraciones resolverán un problema mientras crean otro. Más sobre esto en breve.
También descubrí que window_size necesita ajustes para su dominio específico. Una ventana de tres funciona bien para textos narrativos de políticas donde el contexto es local. Para los runbooks técnicos en los que un paso de un procedimiento hace referencia a una sección de configuración cinco párrafos antes, tres oraciones no son suficiente contexto y lo verá en las puntuaciones de relevancia de sus respuestas. Terminé realizando evaluaciones en tamaños de ventana de 2, 3 y 5, comparando las métricas de RAGAS en los tres antes de decidirme por 3 como el mejor equilibrio para nuestro corpus. No asuma que el valor predeterminado es correcto para su caso de uso. Mídelo.
Cuando sus documentos tienen estructura: fragmentación jerárquica
Nuestro corpus de ingeniería: registros de decisiones arquitectónicas, documentos de diseño de sistemas, especificaciones API no se parecían en nada a los archivos de políticas de recursos humanos. Mientras que los documentos de recursos humanos fluían en prosa, los documentos de ingeniería estaban estructurados: secciones con títulos, subsecciones con pasos numerados, tablas de parámetros, ejemplos de códigos. El enfoque de ventana de oraciones que funcionó maravillosamente en los textos de políticas estaba produciendo resultados mediocres en estos.
La razón quedó clara cuando miré los fragmentos recuperados. Una consulta sobre nuestra política de limitación de velocidad de API mostraría una oración de la sección de limitación de velocidad, expandida a su ventana, pero la ventana tenía tres oraciones en medio de un proceso de configuración de doce pasos. El modelo recibió un contexto que técnicamente trataba sobre la limitación de la tasa, pero le faltaban los números reales porque aparecían en una tabla a dos párrafos de la oración explicativa que se había recuperado.
La solución fue obvia una vez que la enmarqué correctamente: recuperar con granularidad de párrafo, pero generar con contexto a nivel de sección. El párrafo es lo suficientemente específico como para ganar un concurso de recuperación. La sección es lo suficientemente completa como para que el LLM pueda razonar. Necesitaba algo que no hiciera ni lo uno ni lo otro.
de llama_index.core.node_parser import HierarchicalNodeParser, get_leaf_nodes de llama_index.core.storage.docstore import SimpleDocumentStore de llama_index.core.indices.postprocessor import AutoMergingRetriever # Jerarquía de tres niveles: página -> sección (512t) -> párrafo (128t) analizador = HierarchicalNodeParser.from_defaults( chunk_sizes=[2048, 512, 128] ) nodes = parser.get_nodes_from_documents(documents) leaf_nodes = get_leaf_nodes(nodes) # Solo los nodos hoja van al índice vectorial # Jerarquía completa almacenada en el almacén de documentos para que AutoMergingRetriever pueda recorrerla docstore = SimpleDocumentStore() docstore.add_documents(nodes) # En el momento de la consulta: si coinciden suficientes hojas de hermanos, devuelva el padre en su lugar retriever = AutoMergingRetriever( vector_retriever, Storage_context, verbose=True )
AutoMergingRetriever es lo que hace que esto sea práctico. Si recupera suficientes nodos hoja hermanos del mismo padre, los promueve al nodo padre automáticamente. No codifica 'recuperar párrafos, devolver secciones'; el patrón de recuperación impulsa la decisión de granularidad en tiempo de ejecución. Las consultas específicas obtienen párrafos. Las consultas amplias que tocan varias partes de una sección obtienen la sección.
Para nuestros documentos de ingeniería, la precisión del contexto mejoró notablemente. Ya no le estábamos pasando al modelo una frase sobre la limitación de tasas despojada de la tabla que contenía los límites reales. Le estábamos pasando la sección, lo que significaba que el modelo tenía los números que necesitaba.
Antes de elegir la fragmentación jerárquica: verifique la estructura de su documento. Ejecute una auditoría rápida de los niveles de encabezado en todo su corpus. Si el documento mediano tiene menos de dos niveles de encabezado significativos, la jerarquía no tiene nada con qué trabajar y obtendrá un comportamiento más cercano al tamaño fijo. La fragmentación jerárquica adquiere su complejidad sólo cuando la estructura que explota está realmente ahí.
La opción atractiva: fragmentación semántica
Después de la fragmentación jerárquica, me encontré con la fragmentación semántica y durante uno o dos días estuve convencido de que había estado haciendo todo mal. La idea es clara: en lugar de imponer límites basados en el recuento de tokens o la estructura del documento, se deja que el modelo de incrustación detecte dónde cambia realmente el tema. Cuando la distancia semántica entre oraciones adyacentes cruza un umbral, ese es el punto de corte. Cada fragmento, en teoría, cubre exactamente una idea.
de llama_index.core.node_parser import SemanticSplitterNodeParser parser = SemanticSplitterNodeParser( buffer_size=1, breakpoint_percentile_threshold=95, # 5% superior de distancias = límites embed_model=embed_model )
En teoría, esta es la abstracción correcta. En la práctica, introduce dos problemas que importan en la producción.
El primero es la latencia de indexación. La fragmentación semántica requiere incrustar cada oración antes de que pueda determinar un límite único. Para un corpus de 50.000 documentos, éste no es un trabajo de una tarde. Realizamos una prueba en un subconjunto de 5000 documentos y el tiempo de indexación fue aproximadamente cuatro veces mayor que el de la fragmentación jerárquica en el mismo corpus. Para un sistema que necesita reindexarse gradualmente a medida que cambian los documentos, ese costo es real.
El segundo es el umbral de sensibilidad.
El breakpoint_percentile_threshold controla la agresividad con la que corta el analizador. A 95, rara vez corta y produce trozos grandes. A los 80 años, corta con frecuencia y produce fragmentos. El valor correcto depende de su dominio, su modelo de incrustación y la densidad de sus documentos y no puede saberlo sin realizar evaluaciones. Pasé dos días ajustándolo en nuestro corpus y me decidí por 92, que produjo resultados razonables pero nada que justificara claramente el costo de indexación sobre el enfoque jerárquico para nuestros documentos de ingeniería estructurados.
Donde la fragmentación semántica realmente superó a las alternativas fue en nuestros documentos de formato mixto: páginas exportadas desde Notion que combinaban explicaciones narrativas, tablas en línea, fragmentos de código corto y listas con viñetas sin ninguna jerarquía en particular. Para ellos, ni las ventanas de oraciones ni el análisis jerárquico tenían una señal estructural fuerte con la que trabajar, y la división semántica al menos produjo fragmentos temáticamente coherentes.
Mi opinión sincera: vale la pena probar la fragmentación semántica cuando su corpus está genuinamente desestructurado y los enfoques estratégicos homogéneos tienen un rendimiento consistentemente inferior. No es una actualización predeterminada de los enfoques jerárquicos o de ventana de oraciones. Cambia la simplicidad y la velocidad por la coherencia teórica, y ese intercambio solo vale la pena con evidencia de sus propios datos de evaluación.
El problema del que nadie habla: archivos PDF, tablas y diapositivas
Todo lo que he descrito hasta ahora supone que sus documentos son texto limpio y bien formado. En la práctica, las bases de conocimiento empresarial están llenas de cosas que no son textos limpios y bien formados. Son archivos PDF escaneados con diseños de dos columnas. Son exportaciones de hojas de cálculo donde la información más importante está en una tabla. Son presentaciones de PowerPoint donde la idea clave es un diagrama con un título, y el título sólo tiene sentido junto al diagrama.
Ninguna de las estrategias anteriores maneja estos casos. Y en muchos corpus empresariales, estos casos no son casos extremos, sino que constituyen la mayor parte del contenido.
Archivos PDF escaneados y análisis según el diseño
El cargador de PDF estándar LlamaIndex utiliza PyPDF internamente. Extrae texto en orden de lectura, lo que funciona aceptablemente para documentos simples de una sola columna, pero falla gravemente en cualquier documento con un diseño complejo. Un artículo académico de dos columnas obtiene su texto intercalado columna por columna. Un formulario escaneado obtiene texto confuso o nada en absoluto. Un informe con llamadas en la barra lateral las inserta a mitad de la oración en el cuerpo principal.
Para un procesamiento de PDF serio, cambié a PyMuPDF (también llamado fitz) para la extracción basada en el diseño y a pdfplumber para documentos en los que necesitaba un control granular sobre la detección de tablas. La diferencia en documentos complejos fue lo suficientemente significativa como para considerarlo un proceso de preprocesamiento separado en lugar de simplemente un cargador diferente.
importar fitz # PyMuPDF def extraer_con_diseño(ruta_pdf): doc = fitz.open(ruta_pdf) páginas =[]para la página en el documento: # get_text('blocks') devuelve texto agrupado por bloque visual blocks = page.get_text('blocks') # Ordena de arriba a abajo, de izquierda a derecha dentro de cada columna blocks.sort(key=lambda b: (round(b[1]/ página.rect.altura * 20), b[0])) # depósito de filas normalizado page_text = ' '.join(b[4]para b en bloques si b[6]== 0) # tipo 0 = páginas de texto.append({'text': page_text, 'page': page.number}) devolver páginas
La idea clave con PyMuPDF es que get_text('blocks') le brinda texto agrupado por bloque de diseño visual y no en orden de caracteres sin formato. La clasificación de estos bloques por posición vertical y luego por posición horizontal reconstruye el orden de lectura correcto para diseños de varias columnas de una manera que la simple extracción del orden de los caracteres no puede.
Para documentos con artefactos de escaneo pesados o elementos escritos a mano, Tesseract OCR a través de pytesseract es la alternativa. Es más lento y ruidoso, pero para formularios de recursos humanos escaneados o contratos firmados, suele ser la única opción. Envié documentos a OCR solo cuando PyMuPDF devolvió menos de 50 palabras por página, una heurística que identificó documentos escaneados de manera confiable sin agregar latencia de OCR a la ruta limpia del PDF.
LlamaParse como alternativa administrada: LlamaParse de LlamaCloud maneja diseños PDF complejos, tablas e incluso extracción de diagramas sin necesidad de crear y mantener un proceso de preprocesamiento. Para los equipos que no pueden permitirse el lujo de invertir tiempo de ingeniería en la infraestructura de análisis de PDF, vale la pena evaluarlo. La contrapartida es enviar documentos a un servicio externo. Verifique los requisitos de residencia de sus datos antes de usarlos en un entorno regulado.
Tablas: El agujero negro de recuperación
Las tablas son la causa más común de fallas de recuperación silenciosa en los sistemas RAG empresariales y casi nunca se abordan en los tutoriales estándar. La razón es sencilla: una tabla es una estructura bidimensional en una representación unidimensional. Cuando aplanas una tabla para convertirla en texto, las relaciones fila-columna desaparecen y lo que obtienes es una secuencia de valores que esencialmente no se puede interpretar sin el contexto de los encabezados.
Tome una tabla con columnas para Producto, Región, Ingresos del tercer trimestre y Crecimiento interanual. Aplánelo ingenuamente y el fragmento recuperado se verá así: 'Producto A EMEA 4,2 millones 12 % Producto B APAC 3,1 millones -3 %'. El modelo de incrustación no tiene idea de lo que significan esos números entre sí. El LLM, incluso si recibe ese fragmento, no puede reconstruir de manera confiable las relaciones fila-columna. Obtiene una respuesta que suena segura y que es aritméticamente incorrecta.
El enfoque que funcionó para nosotros fue tratar las tablas como un tipo de extracción independiente y reconstruirlas como descripciones en lenguaje natural antes de indexarlas. Para cada fila de la tabla, genere una oración que codifique la fila en forma legible, preservando la relación entre los valores y los encabezados.
importar pdfplumber def tablas_a_sentencias(ruta_pdf): oraciones =[]con pdfplumber.open(pdf_path) como pdf: para página en pdf.pages: para tabla en page.extract_tables(): si no es tabla o len(tabla) < 2: continuar encabezados = tabla[0]for fila en tabla[1:]: # Reconstruir fila como pares de oraciones legibles = [f'{h}: {v}' for h, v in zip(headers, row) if h y v] oraciones.append(', '.join(pares) + '.') devuelve oraciones
Esta es una compensación deliberada. Se pierde la estructura visual de la tabla, pero se gana algo más valioso: fragmentos semánticamente completos. Una consulta que solicita "Ingresos del producto A en EMEA para el tercer trimestre" ahora coincide con un fragmento que dice "Producto: Producto A, Región: EMEA, Ingresos del tercer trimestre: 4,2 millones, crecimiento interanual: 12%", que es a la vez recuperable e interpretable.
Para tablas que son demasiado complejas para la reconstrucción fila por fila, tablas dinámicas, encabezados de varios niveles, celdas fusionadas, encontré más confiable pasar la tabla sin procesar a un LLM capacitado en el momento de la indexación y pedirle que genere un resumen en prosa. Esto agrega costo y latencia al proceso de indexación, pero vale la pena para tablas que contienen información realmente importante.
Plataformas de diapositivas y documentos con muchas imágenes
Las presentaciones en PowerPoint y en PDF son un desafío particular porque la información más significativa a menudo no se encuentra en el texto. Una diapositiva con el título 'Decisión de arquitectura del tercer trimestre' y un diagrama que muestra las dependencias del servicio tiene la mayor parte de su significado en el diagrama, no en los seis puntos debajo de él.
Para la extracción de texto de presentaciones de diapositivas, python-pptx maneja la extracción mecánica de títulos de diapositivas, cuadros de texto y notas del orador. Las notas del orador suelen contener más información que el cuerpo de la diapositiva y siempre deben indexarse junto a él.
desde pptx import Presentación def extract_slide_content(pptx_path): prs = Presentación(pptx_path) diapositivas =[]para i, deslice en enumerar (prs.slides): título = '' cuerpo_texto =[]notas = '' para forma en diapositiva.formas: si no, forma.has_text_frame: continuar # Use placeholder_format para detectar de manera confiable el título (idx 0) # el tipo de forma por sí solo no es confiable en todos los diseños de diapositivas si shape.is_placeholder y shape.placeholder_format.idx == 0: título = forma.text_frame.text else: body_text.append(shape.text_frame.text) si slide.has_notes_slide: notas = slide.notes_slide.notes_text_frame.text slides.append({ 'diapositiva': i + 1, 'título': título, 'cuerpo': ' '.join(body_text), 'notas': notas }) regresar diapositivas
Para diagramas, capturas de pantalla y diapositivas con muchas imágenes donde el contenido visual tiene significado, la extracción de texto por sí sola es insuficiente. Las opciones prácticas son dos: usar un modelo multimodal para generar una descripción de la imagen en el momento de la indexación, o usar LlamaParse, que maneja la extracción de contenido mixto, incluidas imágenes.
Utilicé GPT-4V en el momento de la indexación para nuestras presentaciones de diapositivas más importantes, las revisiones trimestrales de arquitectura y los documentos de diseño del sistema a los que los ingenieros hacían referencia con frecuencia. El costo era manejable porque se aplicaba sólo a las diapositivas marcadas como con muchos diagramas, y la mejora en la calidad de recuperación de consultas técnicas era notable. Para una pila completamente local, LLaVA a través de Ollama es una alternativa viable, aunque la calidad de la descripción para diagramas complejos es significativamente menor que la de GPT-4V al momento de escribir este artículo.
La heurística práctica: si una diapositiva tiene menos de 30 palabras de texto pero contiene una imagen, márquela para procesamiento multimodal. Si tiene más de 30 palabras, probablemente la extracción de texto sea suficiente. Esta sencilla regla dirige correctamente aproximadamente el 85% de las diapositivas en un corpus de presentación empresarial típico sin clasificación manual.
Un marco de decisión, no una clasificación
Cuando terminé con todo esto, había dejado de pensar en estrategias de fragmentación como una lista clasificada donde una era la "mejor". La pregunta correcta no es "¿qué estrategia es la más sofisticada?" Es "¿qué estrategia coincide con este tipo de documento?" Y en la mayoría de los corpus empresariales, la respuesta es diferente para distintos tipos de documentos.
La lógica de enrutamiento con la que terminamos fue lo suficientemente simple como para implementarla en una tarde y marcó una diferencia mensurable en todos los ámbitos.
from llama_index.core.node_parser import ( SentenceWindowNodeParser, HierarchicalNodeParser, TokenTextSplitter ) def get_parser(doc): doc_type = doc.metadata.get('doc_type', 'unknown') source = doc.metadata.get('source_format', '') # Documentos estructurados con jerarquía de encabezados clara si doc_type está en ['spec', 'runbook', 'adr', 'contract']: return HierarchicalNodeParser.from_defaults( chunk_sizes=[2048, 512, 128] ) # Texto narrativo: políticas, documentos de recursos humanos, guías de incorporación elif doc_type in ['policy', 'hr', 'guide', 'faq']: return SentenceWindowNodeParser.from_defaults(window_size=3) # Las diapositivas y los archivos PDF pasan por su propio preprocesamiento de la primera fuente elif en ['pptx', 'pdf_complex']: devuelve TokenTextSplitter(chunk_size=256, chunk_overlap=30) # Valor predeterminado seguro para contenido desconocido o de formato corto; devuelve TokenTextSplitter(chunk_size=512, chunk_overlap=50)
Los metadatos doc_type provienen de sus cargadores de documentos. El conector Confluence de LlamaIndex expone el tipo de plantilla de página. SharePoint expone el tipo de contenido. Para archivos PDF y diapositivas cargados desde un directorio, puede inferir el tipo a partir de patrones de nombres de archivos o estructura de carpetas. Etiquetar documentos en el momento de la carga, actualizar los metadatos de tipo en un índice existente es complicado.
Aquí está el mapa de decisión completo:
Lo que RAGAS te dice sobre tus trozos
Todo lo que he descrito anteriormente, la excepción del contratista, la mejora de la recuperación del contexto, las fallas en la recuperación de la tabla, solo pude cuantificarlo porque tenía RAGAS ejecutándose desde el principio del proyecto. Sin él, habría estado depurando por intuición, solucionando las consultas que noté y omitiendo las que no noté.
Una cosa que vale la pena decir claramente antes de continuar es que es una herramienta de medición, no una herramienta de reparación. Le dirá con precisión qué categoría de falla está considerando. No le dirá cómo solucionarlo. Esa parte sigue siendo tuya.
En el contexto de la fragmentación específicamente, cada una de las cuatro métricas principales diagnostica un modo de falla diferente:
El patrón que primero me dijo que la fragmentación de tamaño fijo estaba fallando fue una puntuación de recuerdo de contexto de 0,72 junto con una puntuación de fidelidad de 0,86. El modelo se basaba fielmente en lo que recibió. El problema fue que lo que recibió estaba incompleto. Al perro perdiguero le faltaba aproximadamente uno de cada cuatro datos relevantes. Ese patrón, específicamente, apunta hacia arriba: el problema está en la fragmentación o recuperación, no en la generación.
Después de cambiar a ventanas de oraciones para nuestros documentos narrativos, la recuperación del contexto pasó a 0,88 y la fidelidad se mantuvo en 0,91. La precisión del contexto también mejoró de 0,71 a 0,83 porque los nodos a nivel de oración son más específicos por tema que las ventanas de 512 tokens, y el recuperador mostraba menos fragmentos irrelevantes junto con los correctos.
Sepa qué métrica está fallando antes de cambiar algo. Un recuerdo de bajo contexto es un problema de recuperación. Una baja fidelidad es un problema generacional. Una baja precisión de contexto es un problema de fragmentación. Tratarlos indistintamente es cómo pasas una semana haciendo cambios que no sirven para nada.
El punto es simple: ejecute RAGAS antes y después de cada cambio significativo. Los números le ahorrarán días de conjeturas.
Donde nos deja esto
Mi colega de cumplimiento nunca supo que su mensaje de Slack era el comienzo de semanas de trabajo fragmentado. Por su parte, el sistema empezó a dar mejores respuestas después de unas semanas. Según el mío, ese mensaje fue el comentario más útil que recibí durante todo el proyecto, no porque me dijera qué construir, sino porque me dijo en qué me había equivocado.
Esa brecha entre la experiencia del usuario y la realidad de la ingeniería es la razón por la que es tan fácil subestimar la fragmentación. Cuando falla, los usuarios no presentan tickets por "recuperación deficiente del contexto en K=5". Silenciosamente dejan de confiar en el sistema. Y cuando notas la caída en el uso, el problema ya lleva semanas ahí.
El hábito que esto me impuso fue simple: evaluar antes de optimizar. Ejecute RAGAS en un conjunto de consultas realista antes de decidir que su fragmentación es lo suficientemente buena. Lea manualmente una muestra aleatoria de fragmentos, incluidos los que produjeron respuestas incorrectas, no solo los que probó. Mire el registro de fragmentos cuando un usuario informe un problema. La evidencia está ahí. La mayoría de los equipos simplemente no miran.
La fragmentación no es una ingeniería glamorosa. No es que las conferencias sean impresionantes. Pero es la capa la que determina si todo lo que está encima de ella, el modelo de incorporación, el recuperador, el reclasificador, el LLM, realmente tiene posibilidades de funcionar. Hágalo bien, mídalo rigurosamente y el resto del proceso tendrá una base sobre la que vale la pena construir.
El LLM no es el cuello de botella. En la mayoría de los sistemas RAG de producción, el cuello de botella es la decisión sobre dónde termina un fragmento y comienza el siguiente.
Hay más en esta serie y si Production RAG me ha enseñado algo es que cada capa que crees entender tiene al menos un modo de falla que aún no has encontrado. El próximo artículo encontrará otro. Manténganse al tanto.