Cuando un bot RAG cita el monto de deducible incorrecto del seguro
Un divisor de tamaño fijo distribuía una cantidad en dólares entre líneas; era necesario reconstruir el análisis y ascender por cuatro niveles de división, desde líneas base recursivas hasta la recuperación del documento padre, antes de poder confiar en las respuestas.
Todo lo que se indica a continuación es una narrativa compuesta con fines educativos. Los nombres de las empresas, las personas y las cantidades en dólares son inventados; los modos de fallo corresponden a aquellos con los que realmente se enfrentan los sistemas RAG en la producción en sectores regulados como el de seguros y otros ámbitos de alto riesgo.
Muy tarde un martes, un responsable de reclamaciones contactó al equipo de ingeniería: la asistente le había dicho a un cliente que su deducible por inundación era de 500 dólares, cuando en la póliza se indicaba 5,000 dólares. El bot mejorado con capacidad de recuperación de información —“Atlas”— ya estaba en funcionamiento desde hacía tres semanas, equipado con un almacén de vectores, un modelo de incrustación sólido, un LLM eficaz y una interfaz de usuario pulida. Lo que le faltaba era una estrategia clara para dividir la información en fragmentos manejables.
El fragmento recuperado tenía el siguiente aspecto:
...applies to all covered perils. Deductible: $5
00 for flood events in Zone A, $500 for wind events, and $1,0
La extracción de PDF había dividido un número en varias líneas. A continuación, un divisor de tamaño fijo dividió cada 500 caracteres a partir de esa división. El sistema de incrustación indexó un fragmento que indicaba “$5” y “$500”. El modelo respondió con confianza… pero erróneamente.
A continuación se explica cómo se reconstruyó la tubería de ingesta: una etapa de análisis que establece el límite máximo, seguida de cuatro niveles de fragmentación que se acercan a dicho límite con un costo cada vez mayor.
Un enfoque coherente mantiene la transparencia económica: casi todo el costo de la fragmentación corresponde al cálculo único de indexación. Este proceso se ejecuta sin conexión, antes de que surja cualquier consulta del usuario. El único costo relacionado con la fragmentación que aparece en el p95 de las consultas es el del propio modelo de embedding. Que los niveles superiores sean “caros” significa que son costosos una sola vez por documento, no por solicitud.
Etapa 0: No se puede fragmentar una estructura que se destruyó durante el análisis
El primer instinto es utilizar un fragmentador más inteligente. La mejor primera pregunta es: mostrar el texto que se va a fragmentar, no el PDF. El texto inicial de Atlas era un muro de caracteres; los títulos se habían fusionado con el cuerpo del texto, y una tabla de deducibles se había convertido en un texto continuo donde los valores de diferentes filas aparecían uno al lado del otro. El encabezado de página (“Formulario de póliza HO-3 — Página 14 de 62”) se repetía cada pocos miles de caracteres.
Ningún fragmentador puede recuperar los límites que el analizador ya ha destruido. Si los títulos han desaparecido, un divisor de encabezados en Markdown no tiene nada sobre lo que dividir. Si las tablas se han disuelto, nada mantiene las filas intactas.
Asigne a cada tipo de entrada un analizador que genere una estructura que el fragmentador pueda utilizar:
PDFs y DOCX nativos (formularios de pólizas, endosos, directrices). Preferir parsers que tengan en cuenta el diseño: LlamaParse, Unstructured.io, Docling, Azure Document Intelligence, en lugar de simples extracciones de texto. Se requiere formato Markdown con tipos de elementos (título, tabla, lista), y no solo una cadena simple.
PDFs escaneados, imágenes y faxes. Ejecutar OCR (Tesseract es el más débil; PaddleOCR, Textract, Document AI y Azure son más eficaces). Se necesita el texto además de los bloques de diseño, junto con un nivel de confianza por bloque. El nivel de confianza sirve posteriormente para indicar “esta cifra es incierta: no se deben deducir valores a partir de ella”.
HTML y wikis internos. Eliminar elementos innecesarios y generar Markdown limpio manteniendo los títulos.
Diapositivas y hojas de cálculo. Conservar los límites entre diapositivas o hojas para que ningún grupo mezcle diapositivas no relacionadas.
Audio y video (llamadas de reclamaciones, seminarios web). Whisper, Deepgram o AssemblyAI deben generar una transcripción que indique quién habló y cuándo. Si los oradores no están separados, declaraciones opuestas de un ajustador y un cliente pueden fusionarse en un único párrafo engañoso.
Después de la reanálisis teniendo en cuenta el diseño, la sección relativa a las deducciones tenía este aspecto:
## Section 4 — Deductibles
### 4.2 Peril-specific deductibles
| Peril | Zone A | Zone B |
|----------------|---------|---------|
| Flood | $5,000 | $2,500 |
| Wind / hail | $500 | $500 |
| Named storm | $1,000 | $1,000 |
Se recuperó la tabla. Se recuperó el árbol de encabezados. “$5,000” volvió a ser un único token. El código de fragmentación aún no había cambiado, y la recuperación ya era más segura.
Nivel 1: Fragmentación sintáctica — económico, rápido y donde debería comenzar
Tamaño fijo: el prototipo que se lanzó por accidente
Divide cada N caracteres o tokens (CharacterTextSplitter, tiktoken). El costo de indexación es casi cero. Aceptable para picos temporales; fatal en entornos de producción. Corta en medio de oraciones, tablas o números: exactamente como ocurren los incidentes que generan deducciones. Regla después de esa experiencia: los sistemas de tamaño fijo nunca deben usarse en un cuaderno de notas.
Los equipos siguen descubriendo el mismo patrón: un corpus de demostración con entradas cortas de blogs sobrevive a la división por caracteres, pero en cuanto llega el primer PDF con varias columnas o una aprobación procesada por OCR, la calidad de las respuestas se derrumba de inmediato. Considere los ventanales fijos como soportes para experimentos locales, con un punto específico en la lista de verificación para reemplazarlos antes de que ingrese tráfico de usuarios externos.
Superposición: un dial, no una estrategia
chunk_overlap repite el final del bloque N al principio de N+1. Un porcentaje del 10 al 15 es común (por ejemplo, 50 en un bloque de 500 tokens). La superposición funciona como una forma económica de protección para los niveles 1–2; no corrige un límite deficiente, sino que lo duplica. Se esperan aproximadamente un 10% más de vectores y coincidencias duplicadas en el top-k cuando la correspondencia se encuentra dentro de la zona de superposición.
Cuando las duplicadas dominan la lista de vecinos, los herramientas de reclasificación y el LLM desperdician la ventana de contexto en la misma oración dos veces. La deduplicación mediante el ID del padre o mediante hashes de texto casi idénticos después de la recuperación es una solución económica si la superposición debe mantenerse alta por otros motivos.
Oración/párrafo: respeta la gramática, ignora el documento
Empaqueta oraciones completas dentro de un presupuesto (NLTK, spaCy, LlamaIndex SentenceSplitter). Nunca corta a mitad de oración, lo que evitaría que se interrumpiera el número en la línea; sin embargo, no reconoce encabezados, tablas ni listas de exclusión. Es excelente para transcripciones de llamadas; mediocre en formularios estructurados de políticas.
Carácter recursivo: el valor predeterminado que se utiliza primero
RecursiveCharacterTextSplitter prueba los separadores en orden de prioridad: línea en blanco, salto de línea, oración, palabra; solo recurre a otros métodos cuando un fragmento sigue siendo demasiado grande, y luego combina los fragmentos pequeños hasta alcanzar chunk_size. Un valor de referencia común es 500 tokens con un solapamiento del 50 %:
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(policy_markdown)
En la sección de deducibles reanalizada, se mantuvieron los 4.2 completos, incluida la tabla, ya que las líneas en blanco formaban una unidad natural por debajo de 500 tokens. El error desapareció. Envíe recursive-500/50 como la base de referencia para medir, no como el diseño final.
Escriba esa puntuación de referencia en una pizarra y rechace las promociones de fragmentadores “más inteligentes” que no puedan superarla con las mismas preguntas de referencia. Muchas ideas costosas parecen brillantes hasta que se comparan con un divisor recursivo sencillo en Markdown limpio.
Nivel 2: Fragmentación consciente de la estructura — corte donde el documento ya tiene líneas
Con un conjunto de referencia de unas 80 preguntas reales, recursive-500/50 alcanzó aproximadamente el 71%. Los fallos se concentraron en secciones largas que abarcaban varios fragmentos y en respuestas donde el modelo no podía determinar cuál política o sección era la fuente de la respuesta.
División de encabezados en Markdown/HTML
MarkdownHeaderTextSplitter / HTMLHeaderTextSplitter dividen según la jerarquía de encabezados y incluyen la ruta del encabezado en los metadatos:
from langchain_text_splitters import MarkdownHeaderTextSplitter
header_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[("#", "h1"), ("##", "h2"), ("###", "h3")]
)
sections = header_splitter.split_text(policy_markdown)
# then apply the recursive splitter inside each section
Cada fragmento extraído de la subsección 4.2 debe contener una ruta de navegación como “formulario HO-3 → deducibles → tabla específica del riesgo”. Se debe concatenar esa ruta al texto antes de incrustarlo para que el vector codifique tanto la ubicación como los números. Los errores relacionados con “¿Qué sección?” desaparecen; las citas pueden indicar “según la Sección 4.2”. El costo se mantiene cercano a cero, pero solo si la Etapa 0 emitió un markdown real. El texto aplanado se convierte silenciosamente en un único bloque enorme. Es ideal para wikis, documentos técnicos y políticas estructuradas con encabezados.
Los campos de metadatos deben acompañar al vector: ID del documento de origen, ruta de la sección, fecha de vigencia y jurisdicción si las políticas varían por estado. Estos campos permiten utilizar filtros (“solo HO-3 en vigor después del 2024-01-01”) que una búsqueda puramente por similitud no puede expresar.
Consciente del diseño / por_título
chunk_by_title no estructurado (y sus equivalentes en LlamaParse/Docling) respeta los límites de los elementos del analizador: las tablas permanecen completas, los títulos dan inicio a los bloques y las listas conservan su oración introductoria. Herramienta ideal para contratos y PDFs complejos. Las APIs de análisis cuestan dinero una sola vez al indexar. Un analizador económico anula la necesidad de estas herramientas; no compre un volante sofisticado para un coche sin motor.
Consciente del código (AST)
No es relevante para corpus de políticas, pero esencial para Terraform/Python RAG. La división por líneas separa las firmas de los cuerpos del texto. tree-sitter o divisores recursivos conscientes del lenguaje realizan el corte en los nodos de función/clase. Es la opción predeterminada cuando el corpus está formado por repositorios o IaC.
Mezclar políticas de prosa y código fuente en un mismo índice sin utilizar separadores suele generar lo peor de ambos mundos: funciones truncadas a mitad de su contenido y tablas de políticas dañadas por heurísticas orientadas a los corchetes.
Nivel 3: Fragmentación basada en modelos — pagar por el juicio solo cuando la estructura está perdida
Aproximadamente el 84% en el conjunto de pruebas de referencia presentó una nueva clase de fallos: transcripciones de llamadas no estructuradas sin encabezados. Bloques recursivos de 500 tokens abarcaban cambios de tema —por ejemplo, una reclamación relacionada con un techo y luego la dirección de envío— por lo que los embeddings reflejaban en promedio dos temas y no se ajustaban bien a ninguno de ellos.
Fragmentación semántica
Inserte las representaciones una oración a la vez; corte el texto cuando la similitud coseno consecutiva supere un umbral determinado (SemanticChunker, cualquier herramienta de embedding decente). Realice una sola pasada de embedding al indexar. Este método es transformador en el caso del habla; los umbrales dependen del corpus específico. El umbral utilizado para las llamadas telefónicas dividió el texto denso de políticas en fragmentos muy pequeños; por lo tanto, utilice el chunking semántico únicamente en habla no estructurada y mantenga las políticas en el nivel 2.
Enrutar según la clase del documento —habla, formulario o wiki— es mejor que buscar un chunker universal. El costo de la orquestación consiste en un clasificador o en un simple cambio de tipo de archivo al ingresar los datos; la ventaja es obtener representaciones menos confusas.
Chunking de propuestas/factores atómicos
Un pequeño LLM reescribe los pasajes en afirmaciones independientes (“La deducible por inundación para la Zona A bajo HO-3 es de $5,000”). La precisión aumenta porque cada unidad corresponde a un hecho con el contexto incluido directamente. El costo es una llamada al LLM por cada pasaje, lo cual es razonable para corpus pequeños y de alta precisión como una FAQ de 40 páginas. Problema: las propuestas se desvían de la redacción original. Cuando los clientes cuestionan las respuestas, citar literalmente es mejor que parafrasear. Utilice las propuestas como ayuda para la recuperación de información y devuelva el pasaje original para la citación.
Las organizaciones encargadas del cumplimiento y de las reclamaciones eventualmente solicitarán la imagen de la página o los resúmenes en PDF detrás de una respuesta. Diseñe los identificadores de citación con antelación para que los índices de propuestas sigan siendo un acelerador y no el sistema principal de registro.
Fragmentación por agente
Entregue un documento completo a un modelo principal para que elija los límites. Funciona, pero su escalabilidad es deficiente: el costo aumenta de forma lineal con el tamaño del corpus, mientras que el valor no lo hace. Adecuado para unos pocos cientos de documentos clave; no para millones.
Nivel 4: Fragmentación por tiempo de recuperación — buscar unidades pequeñas, devolver unidades más grandes
Los niveles 1–3 suponen que la unidad indexada es también la unidad devuelta. Esto implica un falseo compromiso: las unidades pequeñas permiten embeddings nítidos, pero privan al LLM de contexto; las unidades grandes proporcionan contexto, pero empañan los embeddings. Ajustar chunk_size indefinidamente nunca permite encontrar un punto óptimo universal.
El nivel 4 separa las funciones: una unidad de búsqueda adaptada al módulo de embeddings y una unidad de entrega adaptada al LLM. Prueba decisiva: si necesita un segundo almacenamiento (docstore, mapa padre, árbol), se encuentra en el nivel 4.
Documento padre / de pequeño a grande
Indexar niños pequeños (150–200 tokens). Al realizar la búsqueda, devolver el padre más grande (toda la Sección 4.2 o una subsección de ~1,500 tokens):
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1500)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=InMemoryStore(), # the "second store"
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)
retriever.add_documents(policy_docs)
LlamaIndex AutoMergingRetriever ofrece una jerarquía similar. El costo adicional del índice es casi nulo, ya que se incrusta el mismo texto en partes más pequeñas. En el conjunto de pruebas, solo este cambio hizo que Atlas pasara del ~84% al ~91%; la consulta “what’s my flood deductible” encontró la fila exacta de la tabla, mientras que el LLM aún podía ver las notas circundantes (incluyendo cómo se define la Zona A). Costo operativo: un docstore indexado por el ID del padre que se mantiene sincronizado con las actualizaciones.
La eliminación y la versionado son importantes aquí: cuando se revisa la Sección 4.2, los hijos y el padre deben actualizarse juntos, de lo contrario el sistema recuperará un hijo preciso que haga referencia a un padre obsoleto. Tratar la incorporación de datos como una publicación transaccional que incluye al padre, a los hijos y a las incrustaciones, y no como tres tareas independientes.
Recuperación contextual
Antes de incrustar el texto, pida a un mini LLM una o dos oraciones de contexto y úncelas al principio; combínelo con BM25. Fragmentos como “Zona A: $5,000 / Zona B: $2,500” se vuelven recuperables. El costo es una llamada al LLM por cada bloque de texto; es muy caro sin caché de prompts; con caché, el prefijo del documento permanece activo y las llamadas por bloque siguen siendo económicas.
División en bloques tardía
Incruste todo el documento con un modelo de incrustación para contexto largo y luego combine los vectores de tokens dentro de cada bloque. Los vectores de los bloques conservan el contexto del documento sin necesidad de realizar llamadas al LLM por cada bloque. Es competitivo con los métodos de recuperación contextual, pero obliga a utilizar modelos de incrustación para contexto largo.
Jerárquico / RAPTOR
Los grupos de datos se resumen y se reorganizan en una estructura jerárquica; se indexa cada nivel. Las preguntas generales afectan a los resúmenes, mientras que las específicas llegan hasta las hojas más profundas. El costo del nivel 4 es muy alto y resulta problemático cuando los documentos cambian, ya que las reconstrucciones son costosas. Las actualizaciones trimestrales de políticas hicieron que RAPTOR fuera la opción adecuada para Atlas.
Las bases de conocimiento estáticas, como las enciclopedias internas que se actualizan anualmente, pueden soportar las reconstrucciones de la estructura jerárquica. Pero los formularios de seguros dinámicos no lo permiten. Elija métodos jerárquicos solo cuando se hayan definido claramente la tasa de cambios y el presupuesto para las reconstrucciones.
Qué indica la hoja de procedimientos reconstruida
Seis semanas después del ejercicio de simulacro en Slack, Atlas alcanzó casi el 93% de precisión con un conjunto de preguntas compuesto por unas 300. La versión simplificada para la revisión del diseño:
Comience con la división recursiva de caracteres junto con cortes basados en la estructura a aproximadamente 500 tokens y un 50% de superposición; esto cuesta casi nada y proporciona una línea de referencia medible. Primero actualice a la recuperación del documento padre para que los tamaños de coincidencia y entrega puedan ser diferentes. Solo entonces invierta en recuperación contextual si el conjunto de evaluación sigue señalando problemas de recuerdo.
Nada de lo anterior puede salvar un análisis defectuoso. Repare la extracción antes de ajustar los divisores.
La versión de una página
Etapas 0: Análisis. Asigne analizadores a las entradas: markdown estructurado para documentos nativos; confianza en texto, diseño y OCR para escaneos; markdown con encabezados limpios para HTML; límites de diapositivas o hojas para presentaciones; transcripciones diarizadas para audio. Esto establece el límite máximo.
Nivel 1: Sintáctico. Tamaño fijo solo para prototipos. La superposición se regula mediante un dial (10–15%), lo que genera resultados duplicados en las primeras k entradas. La división de oraciones respeta la gramática, ignorando la lógica del documento. El valor por defecto es 500/50 recursivo, pero no representa el objetivo final.
Nivel 2: Con conciencia de estructura. La división de encabezados incluye rutas de sección; su calidad depende del analizador. El diseño by_title mantiene las tablas completas; los analizadores económicos lo invalidan. Se utiliza división AST para repositorios de código.
Nivel 3: Basado en modelos. Los cortes semánticos se realizan según la similitud; deben ajustarse según el corpus. Las proposiciones mejoran la precisión en corpos pequeños, pero se desvían con el lenguaje utilizado. Los cortes basados en agentes rara vez son válidos a gran escala.
Nivel 4: Tiempo de recuperación. Coincidencia en unidades pequeñas; entrega de las grandes. El documento padre representa la mejora con mayor retorno de inversión. La recuperación contextual requiere caché. El particionamiento tardío es más económico pero está limitado por el sistema de incrustación. RAPTOR se vuelve obsoleto con las actualizaciones. Si necesita un segundo almacenamiento, pertenece al nivel 4.
El incidente en cuestión nunca se trató realmente de elegir entre LangChain y LlamaIndex. Se trataba de respetar la estructura del documento antes que cualquier cálculo, medir los sistemas de particionamiento con preguntas reales de clientes y negarse a entregar fragmentos del modelo que no pudieran contener jamás un dato numérico completo.
Construya el conjunto de datos definitivo a partir de los tickets que ya han causado problemas al producto: deducibles incorrectos, exclusiones faltantes, garantías confusas, y no a partir de información sintética sin fundamento. Calcule por separado la exactitud de las respuestas y el grado de fidelidad en las citaciones, para que un número incorrecto nunca parezca ser una victoria. Cuando llegue un nuevo modelo, se debe exigir que supere el umbral de referencia recursivo en ambos indicadores antes de que entre en contacto con el tráfico real del producto.
Finalmente, mantenga un mecanismo de apagado: si la confianza del OCR en una sección numérica es baja, o si las versiones padre e hijo difieren, el asistente debe rechazar la pregunta sobre los deducibles y ascender la situación en lugar de inventar una confianza falsa. Los usuarios perdonan mucho más rápido la respuesta “no se puede verificar a partir del formulario presentado” que una respuesta errónea de quinientos dólares dada de forma casual. Ese camino de rechazo debe formar parte de los requisitos del producto, y no solo de una presentación posterior compartida después de que ya se haya causado daño.
Lecturas relacionadas
- Construir un asistente de chat RAG para Wikipedia con LlamaIndex y ReAct — Indexar páginas de Wikipedia bajo demanda, incrustar fragmentos con un codificador pequeño y permitir que un agente ReAct llame a una herramienta de motor de consultas para que las respuestas de Chainlit se basen en evidencias recuperadas.
- Recuperación híbrida RAG con pgvector, BM25 y un reclasificador de codificador transversal — Entender por qué la búsqueda vectorial pura omite números de pieza y códigos de error, y cómo combinar pgvector, BM25 y reclasificación en LangChain para una recuperación RAG precisa.