Cuando las citas parecen perfectas y el recuento de personas sigue siendo incorrecto
Un RAG salarial citó el PDF correcto y aun así contó erróneamente a doce empleados como uno solo; la búsqueda híbrida, la compresión sensible a tablas y los rastros de pipeline lo solucionaron.
Una pregunta sobre nóminas debería ser aburrida. Si preguntas cuántas personas aparecen en el registro de salarios de abril de 2025, esperarías un número, una cita y nada interesante. El sistema devolvió una frase pulida: un empleado, PDF de origen con nombre especificado, página marcada. El registro indicaba doce.
Ese tipo de fallo es lo que hace que la generación reforzada por recuperación sea peligrosa en la práctica. El proceso no generó errores; los registros permanecieron en silencio. La respuesta se parecía a todas las respuestas correctas de esa misma semana. Un fallo sutil con una nota al pie es peor que un cierre brusco, porque nadie da prioridad a un párrafo educado.
Este texto reconstruye el proceso que generó la mentira, los diagnósticos que la revelaron y los cambios concretos que devolvieron el conteo a doce. La tecnología utilizada es deliberadamente común: pdfplumber para manejar texto PDF con información de formato, un divisor de texto de LangChain únicamente para dividir el contenido en fragmentos, embeddings de MiniLM, Qdrant para trabajar con vectores, un sistema híbrido de recuperación basado en métodos densos y BM25, un reordenador mediante cross-encoder, y un compresor cuya función era reducir el contexto antes de que funcionara el generador. Ninguna de estas opciones era excepcional. Los problemas residían en la forma en que interactuaban con las tablas e identificadores.
Una sola ingestión, respuestas en cada consulta
Los documentos llegan una sola vez. Las preguntas, en cambio, son continuas. Mantener separados estos flujos es importante al depurar, ya que una respuesta incorrecta puede deberse a vectores obsoletos, filtros defectuosos o a un generador que nunca procesó las filas adecuadas.
Ingestion recorre cada PDF, extrae el texto teniendo en cuenta su formato, lo divide en fragmentos superpuestos, los incrusta y los inserta en Qdrant junto con metadatos que posteriormente pueden ser utilizados por filtros:
PDF → extract text per page → cut into chunks
→ convert each chunk to 384 numbers → store in a vector database
La respuesta se genera mediante un gráfico diferente. Se incrusta la pregunta, se recuperan las opciones posibles, opcionalmente se combinan los resultados basados en palabras clave, se vuelve a clasificar, se comprime el contenido y luego se le plantea la pregunta al modelo con las citas correspondientes:
question → search → rerank → compress → ask the LLM → cited answer
↓ ↓ ↓
5 chunks 3 chunks ~600 chars
Sobre el papel, ese flujo es el típico descrito en los libros de texto. Pero en la práctica, las suposiciones de esos libros fallan con registros de empleados, tablas de facturas y cualquier documento cuya respuesta sea una estructura en lugar de un eslogan.
Por qué cada componente tiene su lugar
pdfplumber no es la biblioteca PDF más rápida. Es una de las pocas que conserva suficientes señales de formato como para evitar que las filas con salarios se conviertan en un único párrafo desorganizado. Los extractores más rápidos que reducen las tablas a oraciones continuas hacen que las preguntas posteriores como “cuántos empleados hay” sean casi imposibles de responder, ya que el modelo nunca percibe los límites entre filas.
Para este proyecto se utilizó el divisor de caracteres recursivo de LangChain y nada más del framework. El objetivo era lograr tamaños de ventana predecibles con solapamiento, y no crear una cadena RAG basada en opiniones personales. Tras una breve comparativa, se eligió all-MiniLM-L6-v2 como modelo de embeddings: los modelos más pequeños omitían los tokens identificadores; los modelos más grandes aumentaban la latencia sin solucionar el problema real. Qdrant ganó por su capacidad de mantener la continuidad entre entornos locales y en la nube, además de sus filtros de carga que se ajustaban a la forma en que la aplicación ya clasificaba los tipos de documentos.
La búsqueda híbrida combinó la similitud semántica con una puntuación de palabras clave al estilo BM25 en una proporción aproximada de 0.65 / 0.35. La búsqueda puramente semántica fue excelente para preguntas como “¿cuál es la política de licencia parental?” pero terrible con STL/2025-26/003. La reclasificación mediante un codificador cruzado mejoró la lista de resultados. La compresión tenía como objetivo conservar solo las oraciones de alto valor antes de pasarlas al modelo LLM. Fue en esa última etapa cuando realmente comenzó la historia de cómo doce resultados se redujeron a uno.
La respuesta incorrecta pero segura
Para la pregunta sobre salarios se recuperó una página plausible, se citó y se subestimaron los resultados. Al analizar las puntuaciones brutas de recuperación se pudo observar el primer problema: los vecinos semánticos parecían “lo suficientemente cercanos”, mientras que las filas con registros exactos competían mal frente al texto narrativo relacionado con recursos humanos presente en otras partes del corpus.
0.781 Invoice_INV2025001_Arvind.pdf <- returned
0.779 Invoice_INV2025002_Trident.pdf
0.776 Invoice_INV2025003_Welspun.pdf <- actually wanted
La similitud de significado no puede realizarse con un procesamiento exacto de tokens. La adición de BM25 y la combinación de puntuaciones modificó el ranking, haciendo que los fragmentos que contenían identificadores y tablas ascendieran en posición:
0.854 Invoice_INV2025003_Welspun.pdf <- correct
0.549 Invoice_INV2025001_Arvind.pdf
0.545 Invoice_INV2025002_Trident.pdf
Eso por sí solo no restableció el recuento adecuado. Solo aseguró que se considerara el documento correcto. Las siguientes dificultades estaban dentro del propio documento.
Los IDs de empleados como experimento de supervivencia
En lugar de discutir sobre aspectos subjetivos, se rastrearon los IDs de empleados ST001 hasta ST012 en cada etapa: extracción, fragmentación, recuperación, reclasificación, compresión y montaje final del mensaje.
retrieved chunk ST001 ST002 ST003 ST004 ST005 ... ST012 (12 present)
after rerank ST001 ST002 ST003 ST004 ST005 ... ST012 (12 present)
after compression ST001 ST002 (2 present)
prompt sent ST001 ST002 (2 present)
Varios identificadores desaparecieron entre la compresión y el modelo. El compresor mantenía un presupuesto fijo de “frases principales”. Cada fila de una tabla cuenta como una frase. Una tabla densa de salarios agota el presupuesto en las filas iniciales, mientras que las filas posteriores —y el texto que las explica— nunca llegan al prompt. El modelo entonces informa con sinceridad el subconjunto que pudo ver.
# before — keep the top N
best = scored[:40]
# after — keep the best until the character budget runs out
budget = MAX_CONTEXT_CHARS // TOP_K_AFTER_RERANK
for sentence, score in ranked:
if best and len(sentence) + 2 > budget:
continue
best.append(sentence)
budget -= len(sentence) + 2
Una vez que la compresión barajó y truncó las filas, la tabla de salarios apareció como un mazo de cartas distribuido en orden aleatorio. Cada número podría existir en algún lugar de la ventana de contexto, pero la estructura necesaria para contar los empleados distintos había desaparecido. Contar personas en una tabla que alguien ha dividido en fragmentos ordenados es una tarea diferente a leer la tabla completa.
Puntuaciones de reclasificación que parecían buenas pero seguían causando problemas
Las puntuaciones del codificador cruzado en las filas críticas parecían “razonables”. Razonable no significa conservar una tabla completa.
0.446 keep ST001 Rajesh Kumar M CFO 75,000 ...
0.323 keep ST002 Priya Sundaram Finance Mgr 45,000 ...
0.420 keep ST003 Murugan K Sr Accountant 28,000 ...
0.259 DROP ST005 Selvam R Production Mgr 38,000 ... ← below 0.30
0.422 keep ST006 Karthik P Supervisor 18,000 ...
# pick the best by score…
chosen = set(id(p) for p in best)
# …then put them back in document order before joining
best = [p for p in scored if id(p) in chosen]
La solución no consistió en “elevar el umbral en todas partes”. Se trataba de detectar líneas similares a tablas y eximirlas de una compresión agresiva. Una heurística práctica: si una línea contiene cuatro o más números y se encuentra entre al menos tres líneas similares en el mismo bloque, trátela como una fila de tabla y consérvela independientemente de su puntuación en la oración.
ROW_MIN_NUMBERS = 4
TABULAR_MIN_ROWS = 3
def is_row(sentence):
return len(NUMBER_RE.findall(sentence)) >= ROW_MIN_NUMBERSif looks_tabular(sentences):
survivors = [p for p in scored if is_row(p[0]) or p[1] >= threshold]
else:
survivors = [p for p in scored if p[1] >= threshold]
Al eximir las filas de tabla, los doce IDs sobrevivieron hasta la solicitud en la siguiente ejecución. Finalmente, el generador contó con una estructura que podía analizar.
Pequeñas correcciones que eliminaron otras minas ocultas
Mientras el camino de la tabla estaba abierto, algunos problemas relacionados recibieron el mismo tratamiento: variables de entorno vacías que reemplazaron silenciosamente los valores predeterminados previstos, filtros de tipo de documento que se comportaban de manera diferente entre Qdrant local y Qdrant Cloud, y cargas de respuesta que devolvían solo texto cuando los operadores necesitaban el rastro del proceso.
# the context cap must be at least CHUNK_SIZE × TOP_K_AFTER_RERANK
MAX_CONTEXT_CHARS = 5000 # was 2000 while 1200 × 3 = 3600
Cada respuesta ahora devuelve todo el proceso, no solo la oración final: los IDs recuperados, las puntuaciones, las decisiones de compresión y los filtros; así se puede analizar el siguiente error sin tener que adivinar.
{
"answer": "12 employees are listed...",
"citations": [{"doc": "Salary_Register_April_2025.pdf", "page": 1}],
"pipeline": {
"retrieved": 5,
"after_rerank": 3,
"chars_before_compression": 3847,
"chars_after_compression": 2103
}
}
El filtrado por tipo de documento funcionaba con una instancia local de Qdrant, pero fallaba de la misma manera en Qdrant Cloud hasta que los índices de la carga de respuesta y las configuraciones de los filtros coincidían con lo que realmente se indexaba en la versión en la nube.
500 — Index required but not found for "doc_type"
Equivalente en Python: os.getenv("KEY", "default") devuelve una cadena vacía cuando la variable existe pero está en blanco. Eso no es el valor por defecto. Prefiera or cuando sea necesario pasar al valor siguiente cuando esté vacío:
QDRANT_URL = os.getenv("QDRANT_URL") or "http://localhost:6333"
Dónde terminó la pila
Tras la recuperación híbrida, la compresión adaptada a tablas y la telemetría explícita del pipeline, la pregunta sobre el registro de abril arrojó doce resultados con referencias que señalaban las filas que justificaban esa cifra.
Retrieval: hybrid, 0.65 semantic + 0.35 BM25
Reranking: cross-encoder, 5 in → 3 out
Compression: sentence-level, table-aware, ~35% reduction
Grounding: prompt + threshold + temperature 0.0
Evaluation: 13/15 on the golden set, refusals tested
Latency: ~4s end to end
Cost: ~$0.003 per fifteen-question run
Nada de esto requería un nuevo modelo base. Lo que se necesitaba era tratar RAG como un pipeline de datos con tasas de supervivencia medibles para los tokens importantes. Los tutoriales prometen “incorporar, recuperar, generar”. En la producción lo importante es “demostrar que las filas llegaron al prompt”.
Hábitos operativos que mantienen la cifra precisa
Mantenga un conjunto de preguntas clave que incluya al menos una cuenta pura sobre una tabla, una búsqueda por identificador exacto y una pregunta relacionada con políticas semánticas. Si solo pasan las preguntas semánticas, la demostración está mintiendo sobre su estado de preparación.
Registre los IDs de los fragmentos a través de la compresión. Si un ID presente después de la recuperación falta tras la compresión, se trata de un error, incluso si la respuesta final resulta ser correcta en ese momento.
Preferir los híbridos explícitos para corpus que mezclan prosa y diferentes registros lingüísticos. La recuperación intensiva por sí sola seguirá funcionando bien con ensayos pero fallará con códigos.
Trate las citas como algo necesario pero no suficiente. Un número de página junto a un recuento incorrecto sigue siendo un recuento incorrecto.
Al pasar de Qdrant local a la nube, vuelva a verificar los índices de la carga y el comportamiento del filtro con las mismas pruebas. La compatibilidad de API no implica valores predeterminados idénticos para el indexado.
El contexto presupuestario se refiere a la estructura en su totalidad, no solo a las “frases importantes”. Las tablas constituyen parte de la estructura. Comprimirlas como párrafos de blog es la forma en que doce se convierten en uno.
Por qué los fallos menores merecen controles más estrictos
Los equipos suelen aprobar los lanzamientos de RAG basándose en el criterio de que “las respuestas se ven bien en una hoja de cálculo con prompts de caso ideal”. Ese criterio no detecta los fallos reales en áreas como nóminas, inventario y cumplimiento normativo: las subcuentas erróneas. Se deben añadir verificaciones automáticas que aseguren que los conjuntos de entidades extraídas se mantengan en el prompt para elementos fijos conocidos. Fallar la compilación cuando ST001–ST012 no aparezcan todos después de la compresión en el elemento fijo de salarios.
Ese mismo control puede verificar que los pesos de mezcla híbrida estén realmente activados en la configuración en ejecución, y no solo mencionados en un README. La discrepancia entre los experimentos realizados en cuadernos de notas y la configuración del servicio es una causa común por la que BM25 “desaparece” tras una refactorización.
Finalmente, enseñe a los operadores a desconfiar de las citas aparentemente convincentes. La interfaz del producto debe mostrar los registros de recuperación y compresión junto a la respuesta para los usuarios internos, hasta que el conjunto de datos permanezca en estado verde durante todo un ciclo de lanzamiento. Los usuarios externos pueden seguir utilizando párrafos limpios; los internos, en cambio, necesitan herramientas para analizar en profundidad la información.
Cierre
El registro nunca indicó que hubiera un solo empleado. El proceso sí lo hizo, después de descartar silenciosamente las filas que podrían haberlo corregido. Corregir eso significó utilizar búsquedas híbridas para los identificadores, compresión adaptada a la estructura de las tablas y respuestas que incluyeran su propio rastro de evidencias. Una vez implementados estos elementos, doce siguió siendo doce, y la siguiente cita convincente contó con algo real en lo que basarse.
Ese es el estándar que merece mantenerse: no si el modelo puede sonar seguro, sino si el sistema puede demostrar los hechos que justifican esa certeza.
Un análisis más profundo de la puntuación híbrida en la práctica
La recuperación densa codifica la pregunta y cada fragmento en el mismo espacio vectorial y realiza una clasificación mediante el producto coseno o el producto escalar. Esto funciona cuando el idioma del usuario coincide con el idioma del documento: licencia parental, indemnización por despido, política de trabajo remoto. Falla cuando el usuario pega un token opaco que apenas aparece en los datos de entrenamiento y rara vez coocurre con palabras cercanas en el espacio de embedding. Los códigos de empleados, los números de factura y los IDs de contrato pertenecen exactamente a esa categoría de tokens.
BM25 y los evaluadores léxicos relacionados invierten el problema. Les importa que el token aparezca, qué tan raro es en el corpus y con qué frecuencia aparece en el fragmento candidato. No les importa que “STL/2025-26/003” esté semánticamente cercano a nada. Combinar las dos puntuaciones no es una cuestión filosófica; se trata de reconocer que los corpora de nóminas contienen tanto políticas similares a ensayos como tablas similares a registros.
La combinación de 0,65 / 0,35 utilizada aquí es un punto de partida, no una ley natural. Los corpora con muchos códigos pueden necesitar más peso léxico, mientras que los corporas centrados en narrativas pueden requerir menos. Lo importante es medir ambos tipos de preguntas en el conjunto de referencia y evitar entregar una combinación que solo funcione con prompts narrativos.
Al implementar la combinación, normalice las puntuaciones antes de mezclarlas. Las similitudes densas en bruto y las puntuaciones BM25 en bruto operan en escalas diferentes. Los equipos que añaden números no normalizados a menudo descubren que un canal domina por casualidad. Tanto la fusión min-máx como la de rango (RRF) son aceptables siempre que se prueben con datos que incluyan IDs exactos.
La compresión como códec con pérdida para tablas
La compresión de contexto existe porque los modelos tienen ventanas finitas y porque las oraciones irrelevantes diluyen la atención. La idea general es: calificar las oraciones según su relevancia, conservar las k mejores y descartar el resto. Esa idea asume que las oraciones son unidades intercambiables de significado. Las filas de una tabla no lo son. La fila 7 sin las filas 1–6 no es simplemente una tabla ligeramente peor; es una tabla defectuosa.
La heurística de exención —muchos números en una línea, varias líneas similares cerca— es intencionadamente rudimentaria. Permitirá conservar algunas líneas que no son tablas pero parecen numéricas, y eso es aceptable. Los falsos positivos cuestan tokens; los falsos negativos, precisión. En el caso de nóminas e inventarios, la precisión es lo más importante.
Los detectores más sofisticados pueden utilizar la estructura PDF de pdfplumber: cuadros que delimitan las celdas, columnas alineadas, coordenadas x repetidas. Esos indicadores son excelentes cuando están disponibles. La heurística basada en oraciones sigue siendo útil como solución de respaldo cuando el texto ya ha sido convertido a formato Markdown o texto plano antes de ejecutarse la compresión.
Otro modo de fallo es el mezclado aleatorio. Incluso si todas las filas se conservan, reordenarlas según su puntuación de relevancia puede alterar los totales y conteos. Es preferible mantener un orden estable de los documentos para las filas de tablas exentas. Puedes clasificar el texto en prosa libremente; mantén las tablas en orden de lectura.
Citas que defienden la respuesta incorrecta
Citation UX suele resaltar el PDF y la página que aportaron el fragmento recuperado en mayor medida. Cuando la compresión reduce posteriormente a la mitad la tabla, la citación sigue apuntando al archivo correcto. Los usuarios lo interpretan como una confirmación. El diseño del producto debe indicar las filas específicas utilizadas en la solicitud final o mostrar un rastro de recuperación. De lo contrario, la interfaz se convierte en cómplice.
En dominios regulados, guarde el hash exacto del contexto de la solicitud junto con la respuesta. Si un auditor pregunta por qué el sistema indicó “un empleado”, puede reproducir la tabla truncada y mostrar el error en lugar de debatir sobre las características del modelo.
Bases de datos vectoriales locales frente a las en la nube
El filtro de tipo de documento que funcionaba localmente pero falló en Qdrant Cloud es un recordatorio de que “la misma API” no significa “la misma configuración del índice”. Los índices de carga útil, los filtros por palabra clave y el manejo de valores nulos difieren entre los modos de despliegue. Las pruebas de fixtures deben ejecutarse con la misma clase de despliegue que se utiliza en producción. Un conjunto de pruebas local que funcione bien pero un filtro en la nube que dé error es cómo se entregan las consultas que devuelven resultados vacíos sin problemas.
Las variables de entorno vacías merecen la misma precaución. Los sistemas de configuración que exportan cadenas en blanco para secretos no definidos ignorarán los valores predeterminados en aquellos lenguajes donde lo vacío se considera verdadero. Normalice la configuración al inicio del proceso: trate lo vacío como si faltara, aplique luego los valores predeterminados y, si las claves necesarias siguen ausentes, genere un error inmediato.
Midiendo la supervivencia, no las sensaciones
El experimento de supervivencia del ID del empleado sirve como plantilla. Elija las entidades que deben aparecer para obtener una respuesta correcta. Asegúrese de que existan después de la extracción, del particionamiento, de la recuperación, del reclasificado y de la compresión. Grafique las disminuciones. El primer obstáculo suele ser un error.
Extienda esta idea a los agregados numéricos. Si la pregunta es una suma, asegúrese de que cada fila de sumandos haya llegado al mensaje de entrada. Si la pregunta es un conteo de elementos únicos, asegúrese de que el conjunto de claves únicas esté completo. Estas verificaciones son económicas en comparación con los incidentes reales.
Qué no culpar primero
Es tentador culpar al LLM cuando el conteo está incorrecto. A veces el modelo realmente no puede contar. Con más frecuencia, el modelo nunca recibió una estructura cuantificable. Cambie de modelo solo después de que el gráfico de supervivencia muestre un color verde. De lo contrario, “arreglará” un error de conteo al pasar a un modelo más grande que generará erróneamente el número que deseaba, hasta que llegue el siguiente registro.
De manera similar, resista la tentación de eliminar todo el framework solo porque un compresor no funcionó bien. Aísle esa etapa, añada la excepción correspondiente, realice las pruebas necesarias y siga adelante. Las reescrituras a gran escala parecen productivas, pero a menudo reintroducen el mismo tipo de error bajo nombres diferentes.
Lista de verificación mínima para reforzar la seguridad
- Preguntas clave: conteo de tablas, ID exacto, política semántica.
- Búsqueda híbrida con mezcla normalizada o RRF.
- Compresión consciente de las tablas con orden estable de filas.
- Rastreo del proceso en cada respuesta interna.
Ejecuta esa lista de verificación antes de considerar que un RAG de nómina está “listo”. El registro de abril no será el último documento que parezca sencillo y falle de forma discreta.
Consecuencias para el equipo
Una vez que la respuesta generada por los doce empleados se estabilizó, la misma telemetría detectó dos errores menores: un alias de colección obsoleto tras una nueva ingestión, y un tiempo de espera del reordenador que hacía que se utilizaran resultados híbridos sin clasificar, sin marcar la respuesta como de calidad inferior. Ambos habrían pasado desapercibidos si la API hubiera devuelto solo una cadena de texto. Al devolver estructura —puntuaciones, tiempos, soluciones alternativas—, el asistente se convirtió en algo en lo que los operadores podían confiar lo suficiente como para depurarlo a las 2 a.m.
Esa es la verdadera lección del producto. Los sistemas RAG no son simples capas de chat sobre PDFs; son pipelines de datos que comunican información en forma de párrafos. Trátelos como tales: mida las pérdidas, preserve la estructura y nunca permita que una cita sustituya a la prueba.
Otra revisión de la respuesta incorrecta original
Volver a reproducir la mala respuesta con los registros adjuntos hace que la historia resulte casi aburrida. El sistema de recuperación casi encontró el PDF correcto; la puntuación híbrida completó esa tarea. Luego, el proceso de compresión utilizó todo su presupuesto de recursos en las primeras filas numéricas y descartó el resto. El modelo contó lo que quedaba e hizo referencia al archivo. Cada etapa realizó algo defendible por separado; juntas generaron confianza.
Esa es la razón por la que las métricas a nivel de etapa engañan. Retrieval@k puede parecer adecuado mientras que la compresión destruye la respuesta. La supervivencia de las entidades de extremo a extremo es la métrica que refleja el daño al usuario. Úsela desde temprano, especialmente cuando los documentos son tablas disfrazadas de PDF.
Si no hay algo más que llevarse del incidente con los doce empleados, tome esto: los fallos corteses en RAG son errores en el proceso hasta que se demuestre lo contrario. Instrumente la ruta, proteja la estructura y haga que el sistema muestre su trabajo antes de confiar en su resultado. Los fallos leves merecen controles estrictos, pruebas repetidas y operadores capaces de ver cada detalle antes de que los usuarios vuelvan a confiar en un conteo citado.
La supervivencia de las entidades tras la compresión sigue siendo el control más sencillo y honesto para corpus con muchos tablas.
Cuando llegue el próximo registro, ejecute nuevamente el mismo gráfico de supervivencia de IDs antes de confiar en cualquier cita recién generada.
Lecturas relacionadas
- Cuando un bot RAG cita el deducible de seguro incorrecto — Un divisor de tamaño fijo distribuía una cantidad en dólares en varias líneas; fue 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.
- RAG en producción en Azure: división de datos, búsqueda híbrida, filtros y citas — Las necesidades empresariales de recuperación de información requieren datos divididos teniendo en cuenta su estructura, búsqueda híbrida, filtros ACL, reclasificación, prompts basados en contexto y evaluación, no solo una demostración con PDF.