Encontrar los anclajes adecuados para RAG: palabras clave, incrustaciones y señales TOC en paralelo

ladrillo de Enterprise Document Intelligence, una serie que construye un sistema RAG empresarial a partir de cuatro ladrillos: análisis, análisis de preguntas, recuperación y generación. Es la segunda de las tres partes del ladrillo. La parte anterior, el artículo 7A (recuperación como filtrado), fija el modelo mental; éste construye la máquina: detectores de anclaje paralelos, palabra clave siempre, incrustaciones al lado y una llamada de LLM al final.

Dónde se ubica este artículo en la serie: Artículo 7 (recuperación), la parte de detección de anclajes, dentro de la Parte II (los cuatro ladrillos) – Imagen del autor

La recuperación en un sistema RAG empresarial se filtra en dos tablas estructuradas (line_df y toc_df), y cada candidato lleva un ancla (donde aterriza la coincidencia) más un contexto (lo que se expande para la generación). Ese modelo mental es el objeto del artículo 7A (recuperación como filtrado). Este artículo se centra en cómo se producen los anclajes: un proceso de tres etapas que ejecuta la detección e incrustación de palabras clave en paralelo, agrega las visitas a una unidad estructural y finaliza con una única llamada de LLM que clasifica a los candidatos con sus razones.

El usuario escribe "¿Cómo se calcula la atención?" en el papel Transformador. Seis páginas candidatas coinciden con la atención. El de la derecha menciona softmax, query, key, d_k juntos y se encuentra en la sección que el TOC llama "Atención de producto escalado". Dos recuperadores (palabra clave e incrustación) detectan el conjunto de candidatos. Ninguno de los dos puede decir por sí solo qué página responde realmente a la pregunta. Un tercer paso consiste en leer a los candidatos uno al lado del otro, con la sección en la que se sienta cada uno, y elegir el correcto con una razón que el auditor pueda leer meses después.

El proceso de tres etapas que sigue se basa en tres principios:

Las palabras clave se ejecutan siempre. La detección de palabras clave es gratuita. No hay ningún escenario en el que no quieras su señal. Se ejecuta tanto en line_df como en toc_df desde el primer milisegundo. Las incrustaciones se ejecutan en paralelo y son opcionales. Cuando se espera una discrepancia de vocabulario o la pregunta es conceptual, las incrustaciones detectan qué palabra clave falta. Con índices precalculados, el costo del tiempo de consulta es de microsegundos. Omítalas cuando la señal de la palabra clave ya esté limpia. Una llamada de LLM al final. No hay un paso intermedio de “razonamiento TOC” del LLM. El árbitro en la etapa 3 ve el TOC, las coincidencias de palabras clave, las coincidencias de incorporación y el vínculo estructural de cada candidato, en una sola llamada. Realiza el razonamiento sobre el TOC implícitamente como parte de la clasificación.

Este artículo recorre los detectores en cada tabla (Sección 2 en toc_df, Sección 3 en line_df), luego las combinaciones en ambas tablas (Sección 4). La llamada del árbitro, el árbol de decisión y el JSON de salida se encuentran en el Artículo 7C (el árbitro LLM y el JSON de salida de recuperación).

A lo largo de este artículo trabajamos en un solo documento, Attention Is All You Need (Vaswani et al. 2017, 15 páginas; licencia de distribución no exclusiva de arXiv, declarada en la página de resumen de arXiv). Incluye un TOC nativo limpio en el esquema del PDF (22 entradas, 3 niveles de profundidad) y el contenido es un territorio familiar para cualquier ingeniero que toque RAG: codificador, decodificador, atención, consultas, claves, valores. Esto mantiene el foco en los métodos de recuperación en lugar de analizar un corpus de dominio específico. Este artículo también asume que el documento lleva su propio TOC; recuperar uno a partir de texto sin formato se deja para el trabajo de seguimiento.

Todos los métodos de este artículo comienzan desde line_df y toc_df – Imagen del autor

1. El proceso de detección de anclajes

La detección de anclas se realiza en tres etapas. La etapa 1 ejecuta la detección de palabras clave y la incorporación de similitudes en paralelo en line_df y toc_df. La etapa 2 agrega las visitas a una unidad estructural (sección a través de toc_df si está disponible; de ​​lo contrario, página o fragmento). La etapa 3 entrega las unidades agregadas a una única llamada de LLM que las clasifica y escribe su razonamiento por elección.

La detección de palabras clave es la base siempre activa. Coincide con filas cuyo texto contiene las palabras clave de la pregunta, con un aumento de la coocurrencia cuando varias palabras clave aparecen en la misma línea o página. Barato, determinista, auditable. No hay razón para no ejecutarlo: no cuesta nada y cuando golpea limpiamente, le da al LLM una señal fuerte en la etapa 3.

Las incrustaciones se ejecutan en paralelo como una segunda señal opcional. Útil cuando se espera una discrepancia de vocabulario (la pregunta dice "principal", el documento dice "montant annuel"), o cuando la pregunta es conceptual en lugar de léxica. Si ha calculado previamente las incorporaciones, el costo marginal es de microsegundos en el momento de la consulta. De lo contrario, puede omitir por completo las incrustaciones en preguntas en las que la señal de palabra clave ya está limpia.

El LLM al final ve todo: aciertos de palabras clave, aciertos de incorporación, la unidad estructural a la que pertenece cada candidato. Clasifica las unidades una vez, con motivos. Dos consecuencias de diseño de colocar el LLM al final en lugar de a mitad del proceso:

El LLM razona implícitamente sobre el TOC. Cuando se le preguntó "¿qué pasa si salimos temprano?" contra un documento cuyo TOC tiene Terminación y Penalizaciones (y ninguna sección de Salida), el LLM elige ambos en el momento de la clasificación. No hay un paso LLM de “razonamiento TOC” separado al principio del proceso; el árbitro hace ese trabajo como parte de su decisión única. El LLM resuelve coincidencias de títulos sutiles. Si la pregunta es sobre “la prima” pero la sección correspondiente se titula “Resumen del contrato”, ninguna palabra clave coincidirá con el título. El LLM, dadas las coincidencias de palabras clave en las líneas de la carrocería + el accesorio estructural a esa sección, aún así la elegirá.

Tres etapas: detección (paralela) → agregación → una llamada de LLM – Imagen del autor

El resto de este artículo analiza los métodos de detección (Sección 2 sobre toc_df, Sección 3 sobre line_df, Sección 4 sobre cómo colaboran las dos tablas). El artículo 7C (el árbitro del LLM) es donde reside esa decisión final: la decisión única que convierte a los candidatos agregados en una respuesta clasificada.

2. Filtrado en toc_df

Se ejecutan dos detectores en el TOC: coincidencia de palabras clave (siempre, gratis) y coincidencia de incrustación (opcional, paralela). Ambos son pura puntuación, no hay LLM en esta etapa. El trabajo cognitivo (elegir las secciones correctas de una pregunta como “¿qué pasa si salimos temprano?” cuando la sección relevante se titula “Terminación”) ocurre más tarde, en la llamada del árbitro. El árbitro ve el TOC y las palabras clave/incrustaciones en una sola llamada de LLM.

A continuación mostramos una función Reason_on_toc independiente como un aparte pedagógico: aísla lo que hace el árbitro internamente cuando razona sobre el TOC. En producción, puede ejecutarlo como una llamada separada (costo adicional de LLM, útil para depurar) o incorporarlo al árbitro (una llamada de LLM en total, el valor predeterminado preferido).

2.1 Sobre qué razona el árbitro

El toc_df es lo suficientemente pequeño como para pasarlo en su totalidad a un LLM. El árbitro (desarrollado en el Artículo 7C, el árbitro LLM) aprovecha esto: lee el TOC completo y razona qué secciones responden a la pregunta. La función independiente Reason_on_toc a continuación aísla la misma lógica como una llamada separada, lo que resulta útil cuando desea inspeccionar o depurar el paso de razonamiento TOC por sí solo.

Por qué esto importa. El LLM comprende la semántica, pero lo más importante es que comprende las implicaciones. “¿Qué pasa si salimos temprano?” no comparte vocabulario con "Terminación", pero el LLM identifica que salir de un contrato es lo que significa rescisión. “¿Cómo maneja la aseguradora una inundación?” no comparte vocabulario con “procedimiento de reclamos”, pero el LLM identifica que el manejo de daños es el proceso de reclamos. “¿Hay cargos por cambiar la cobertura?” puede coincidir tanto con “Modificación de cobertura” como con “Lista de tarifas”, y el LLM elige ambos, con un razonamiento que explica por qué. Un caso sutil en producción: una pregunta sobre “la prima” llega a una sección titulada “Resumen del contrato”. No hay coincidencias de palabras clave, pero el LLM, dadas las líneas del cuerpo que mencionan los montos de las primas adjuntas a esa sección, aún así la elegirá.

Un modelo de integración captura la “salida anticipada ≈ terminación” a través de la similitud, pero no puede capturar “la salida anticipada implica sanciones”. Eso es razonamiento, no similitud.

El costo es una llamada LLM de nivel medio (unos pocos miles de tokens para un TOC típico), unos cientos de milisegundos de latencia. Cuando se incorpora al árbitro, no cuesta nada adicional: el árbitro vería el TOC de todos modos. El método no es factible en line_df: pasar 12.000 líneas de contenido a un LLM y pedirle que "elija las relevantes" es demasiado costoso, demasiado lento y demasiado poco confiable. El pequeño tamaño del TOC es lo que desbloquea este método.

clase SecciónSelección(BaseModel): sección_ids: lista[str] razonamiento: str def razón_on_toc(pregunta: cadena, toc_df: pd.DataFrame) -> SecciónSelección: """Pase el TOC completo a un LLM, pregunte qué secciones son relevantes, con razonamiento. El mensaje utiliza marcadores [id=N] para que el LLM devuelva nuestro ID de sección interno, no el número inicial del título (por ejemplo, "5.2") que no coincidiría con line_df. """ toc_text = "\n".join( f"[id={row.section_id}] {row.title} (level {row.level}, pp. {row.start_page}-{row.end_page})" for row in toc_df.itertuples() ) Prompt = ( "Dada esta pregunta y el índice del documento, " "identifique qué secciones probablemente contengan la respuesta. " "Considere las implicaciones y los conceptos relacionados, no solo la superposición de palabras clave.\n\n" "IMPORTANTE: devuelva el valor dentro de los corchetes [id=…], solo el número entero, " "por ejemplo, \"9\" no \"id=9\" ni \"5.2\".\n\n" f"Pregunta: {pregunta}\n\nTabla de contenido:\n{toc_text}" ) return client.responses.parse( model=model_chat, input=prompt, text_format=SectionSelection, ).output_parsed # Una pregunta de un lector sobre el artículo. selección = Reason_on_toc( "¿Cómo maneja el transformador las dependencias de largo alcance entre palabras?", toc_df, ) print("Secciones seleccionadas:", selección.section_ids) print("Razonamiento:", selección.razonamiento)

En el artículo de Transformer, el LLM elige secciones ['4', '11'] para la pregunta "¿Cómo maneja Transformer las dependencias de largo alcance entre palabras?". Su razonamiento textual: "La pregunta sobre cómo el Transformer maneja las dependencias de largo alcance entre palabras se aborda mejor en las secciones que analizan el mecanismo de atención (Sección 4) y el razonamiento detrás del uso de la autoatención (Sección 11). El mecanismo de atención es clave para gestionar las dependencias de largo alcance, mientras que la Sección 11 probablemente proporciona información sobre su necesidad y eficacia".

Ese párrafo es exactamente lo que hace que la elección sea auditable. Un método de palabra clave habría devuelto una lista de secciones sin decirle por qué. El LLM, al escribir su motivo en línea, le entrega la pista de auditoría de forma gratuita.

2.2 Concordancia de palabras clave de título (detector predeterminado)

Haga coincidir las palabras clave de la pregunta analizada con los títulos de las secciones. Es casi seguro que una sección cuyo título contenga la palabra clave sea la sección que desea. Este es el detector predeterminado en toc_df: barato, determinista, siempre activo. No hay razón para no ejecutarlo; sus golpes llegan directamente al árbitro.

Cuando es suficiente por sí solo: cuando el vocabulario de la pregunta no es ambiguo y coincide directamente con el vocabulario del documento. “¿Qué dice el apartado de garantía?”: coincide el título “Garantía”. El árbitro ve un golpe limpio y lo confirma.

Cuando no es suficiente: títulos genéricos (“Artículo 1”, “Sección 2.1”), falta de coincidencia de vocabulario (“salida anticipada” versus “Terminación”) o títulos sutiles donde el título de la sección relevante no menciona los términos de la pregunta (“principal” versus “Resumen del contrato”). El árbitro se encarga de eso. La concordancia de palabras clave todavía se ejecuta y contribuye en todo lo que puede, el árbitro toma la decisión a partir de ahí.

def match_titles(toc_df: pd.DataFrame, palabras clave: lista[str]) -> pd.DataFrame: """Devuelve filas toc_df cuyo título contiene cualquiera de las palabras clave (no distingue entre mayúsculas y minúsculas).""" keywords_lower = [kw.lower() for kw in keywords] mask = toc_df["title"].str.lower().apply( lambda t: any(kw in t for kw in keywords_lower) ) return toc_df[mask] # Una búsqueda natural de "atención": coincide con cinco secciones (una principal + cuatro subsecciones). match_titles(toc_df, ["atención"])

Ejecutándolo en el TOC del artículo con la palabra clave atención:

Una palabra clave, cinco aciertos claros, sin llamada de LLM, sin incrustación – Imagen del autor

2.3 Coincidencia de incrustación de título (señal paralela opcional)

Incruste el título de cada sección una vez en el momento de la ingesta, luego, en el momento de la consulta, incruste la pregunta y busque los títulos más cercanos por similitud de coseno. Se ejecuta en paralelo con la concordancia de palabras clave y aporta una segunda señal de detección al árbitro.

Dónde ayuda: casos de discrepancia de vocabulario en los que el título no comparte palabras con la pregunta (“salir temprano” frente a “Terminación”). El coseno capta la proximidad semántica incluso cuando la palabra clave no lo hace. Con las incrustaciones de títulos precalculadas, el costo marginal del tiempo de consulta es de microsegundos.

Donde agrega poco: cuando la señal de la palabra clave ya está limpia y el árbitro habría elegido la sección correcta de todos modos. Puede omitir las incrustaciones en preguntas en las que las coincidencias de palabras clave parecen sólidas; el árbitro todavía recibe suficiente señal para decidir.

def embed_match_titles(query: str, toc_df_with_embeddings: pd.DataFrame, top_k: int = 3): """Buscar títulos más cercanos a la consulta por similitud de coseno.""" query_vec = get_embedding(query, client=client) scoring =[]para la fila en toc_df_with_embeddings.itertuples(): title_vec = np.array(row.embedding) sim = float(np.dot(query_vec, title_vec) / (np.linalg.norm(query_vec) * np.linalg.norm(title_vec))) scoring.append((row.section_id, row.title, sim)) devolver ordenado(puntuado, clave=lambda x: -x[2])[:top_k] # Incrustar cada título y clasificar por similitud a una consulta vaga del lector. toc_df_emb = toc_df.copy() toc_df_emb["embedding"] = toc_df_emb["title"].apply(lambda t: get_embedding(t, client=client)) embed_match_titles("¿dónde explican la autoatención?", toc_df_emb, top_k=3)

Clasificado según la pregunta “¿dónde explican la autoatención?” contra los títulos de las secciones del artículo:

La inserción sobre títulos clasifica la subsección derecha en la parte superior, con un espacio estrecho – Imagen del autor

2.4 Resumen

El flujo predeterminado en toc_df: ejecutar la coincidencia de palabras clave de título (siempre, gratis) y la coincidencia de incrustación de título (paralela, opcional) como detectores. Deje que el árbitro razone cuando clasifique a los candidatos agregados. La función independiente Reason_on_toc que se muestra arriba aísla ese paso de razonamiento para la depuración o para canalizaciones que prefieren dos llamadas LLM a una.

3. Filtrado en line_df

line_df es donde se encuentra el texto de respuesta real. No puede darse el lujo de ejecutar un LLM en decenas de miles de líneas, pero el trabajo de comprensión del contenido del LLM no está ausente: está deslocalizado al bloque de análisis de preguntas. Dos métodos se ejecutan directamente en line_df: coincidencia de palabras clave (que lleva el vocabulario ampliado de la pregunta analizada) e incorporación de similitud.

Concordancia de palabras clave de contenido: el método estándar en line_df, con varias mejoras que convierten la búsqueda ingenua de palabras clave en algo de nivel empresarial.

Utilice las palabras clave de la pregunta analizada, con ponderaciones. El bloque de análisis de preguntas produjo una lista de palabras clave de tres fuentes, cada una con su propio peso en el momento de la recuperación:

Extracción directa del texto del usuario (premium). Ampliación del LLM para sinónimos y variantes (prima, cotización). Entradas de diccionario experto con patrones de expresiones regulares y desambiguadores (premium cerca de €).

Aquí es donde aparece la contribución del LLM al filtrado de contenidos. Las palabras clave ampliadas y los patrones del diccionario experto provienen del análisis de preguntas basado en LLM. Por lo tanto, el filtro de palabras clave en line_df es mucho más inteligente que la búsqueda léxica ingenua; transmite la comprensión del LLM del vocabulario de preguntas como una señal precalculada, no como una llamada de LLM por consulta.

3.1 Impulso a la coexistencia

Es mucho más probable que una línea que contenga palabras clave de dos grupos semánticos (el tema y un término en forma de valor) sea la respuesta que una línea que contenga solo uno. "premium" por sí solo coincide con el encabezado de la sección, las definiciones y la prosa explicativa. “prima” cerca de un número cerca de “— coincide con la línea de resumen que indica el monto.

La demostración utiliza el artículo La atención es todo lo que necesitas (Vaswani et al. 2017, preimpresión de arXiv), con la pregunta "¿Cómo se calcula la atención?". El grupo primario es el vocabulario de atención; el grupo secundario es "cualquier línea que parezca una fórmula" (tokens matemáticos: softmax, query, key, d_k). Es casi seguro que las líneas que afectan a AMBOS grupos son definiciones de fórmulas, no la prosa que las rodea.

def co_occurrence_score(text: str, primario: lista[str], secundario: lista[str]) -> int: """Califica una línea según la cantidad de palabras clave de cada grupo semántico que contiene. Devuelve 0 si falta alguno de los grupos; el punto es la coocurrencia, no la frecuencia. """ p_hits = suma(1 para kw en primario si re.search(kw, text, re.IGNORECASE)) s_hits = suma(1 para kw en secundaria if re.search(kw, text, re.IGNORECASE)) if p_hits == 0 or s_hits == 0: return 0 return p_hits + s_hits # Pregunta: "¿Cómo se calcula realmente la atención?" primario = [r"\battention\b"] secundario = [r"\bsoftmax\b", r"\bquery(ies)?\b", r"\bkey(s)?\b", r"\bd_?k\b"] anotó = line_df.assign(score=line_df["text"].apply( lambda t: co_occurrence_score(t, primario, secundario) )) superior = puntuado[puntuado["puntuación"] > 0].sort_values("puntuación", ascending=False).head(8) superior[["page_num", "line_num", "section_id", "score", "text"]]

Ejecute la pregunta "¿Cómo se calcula la atención?" pregunta con atención como grupo principal y tokens de fórmula (softmax, query, key, d_k) como grupo secundario:

La coocurrencia colapsa más de 200 menciones a las pocas líneas que contienen la fórmula – Imagen del autor

Regex para patrones de alto valor: algunas formas de respuesta son demasiado específicas para la coincidencia de palabras clave, pero triviales para regex: montos monetarios, fechas ISO, códigos de póliza, números de cláusulas. Cuando el patrón de forma esperada de la pregunta analizada dice "la respuesta es una fecha" o "la respuesta es una cantidad monetaria", la recuperación aumenta las líneas cuyo texto coincide con la expresión regular correspondiente. El papel Atención no tiene valores monetarios, por lo que lo demostramos con algunas cuerdas sintéticas.

Cada forma dispara sólo a su objetivo; El costo es una expresión regular por patrón por línea – Imagen del autor

Coincidencia de léxico para entidades enumeradas. Otras formas de respuesta no tienen forma de expresiones regulares, sino que están enumeradas: la respuesta pertenece a una lista finita y conocible. Nombres de países, códigos de moneda, códigos de idioma ISO, partes contratantes nombradas, referencias de productos. El gemelo del patrón: en lugar de una expresión regular, llevas un diccionario que enumera cada valor válido con sus variantes. Cuando la forma esperada de la pregunta dice "la respuesta es un país", la recuperación aumenta las líneas que contienen cualquier término del léxico del país.

Al igual que el catálogo de expresiones regulares, el léxico proviene del diccionario experto creado en el momento del análisis de preguntas. El trabajo se realiza una vez: el experto enumera las entidades y sus variantes ("France": ["France", "FR", "FRA", "République française", "Hexagone"]). Luego, el motor de recuperación paga una búsqueda económica por línea.

Los dos casos juntos cubren la mayoría de situaciones de “la respuesta tiene una forma conocida”: expresión regular cuando la forma es un patrón sintáctico (número más moneda, formato de fecha ISO, prefijo de código), léxico cuando la forma es membresía en un conjunto cerrado (país, moneda, idioma, parte contratante, código de producto). Ambos se activan solo cuando el análisis de preguntas marca la forma esperada, por lo que no cuestan nada en preguntas en las que no son relevantes.

Lexicon resuelve variantes (Alemania, EE. UU., Gran Bretaña) de nombres canónicos – Imagen del autor

3.2 BM25 y TF-IDF

Una variante más avanzada utiliza TF-IDF o BM25. BM25 (Best Match 25) es la fórmula clásica de puntuación de palabras clave que pondera los términos según su contenido de información y los normaliza según la longitud del documento: términos comunes como "el" transmiten poca señal, términos raros como "L131-1" son altamente discriminativos. En la comunidad RAG, BM25 a menudo se presenta como el complemento escaso de incrustaciones densas, la receta de “recuperación híbrida”.

En RAG empresarial, BM25 tiene un rendimiento inferior al filtrado de palabras clave con ponderación empresarial. Aquí está el patrón, ilustrado en tres dominios.

Ejemplo 1, seguro: "¿Cuál es la prima anual?" La página A es una línea de resumen "Prima anual: $125,000". (una “prima”). La página B es un apartado explicativo de seis párrafos sobre el concepto de prima (diez “prima”, sin importe). La página A es la respuesta; BM25 favorece a la página B.

Ejemplo 2, documentos de software: "¿Cuál es el límite de velocidad para el punto final de búsqueda?" La página A es una fila de referencia "/búsqueda: 100 solicitudes/minuto" (una "tasa", un "límite"). La página B es un tutorial sobre conceptos de limitación de velocidad (doce “tasas”, nueve “límites”). La página A es la respuesta; BM25 favorece a la página B.

Ejemplo 3, contratos legales: “¿Cuál es el plazo de preaviso para la rescisión?” La página A es la cláusula de rescisión “previa notificación por escrito con treinta (30) días de antelación” (una de cada). La página B es una sección de Definiciones que utiliza "terminación" y "aviso" cinco o más veces al definir conceptos adyacentes. La página A es la respuesta; BM25 favorece a la página B.

La forma que comparten estos ejemplos es la forma dominante de los documentos empresariales. La respuesta correcta menciona la palabra clave una vez, junto con un valor específico (cantidad, número, duración). La respuesta incorrecta menciona la palabra clave muchas veces en prosa explicativa o definitoria, sin ningún valor específico. BM25 clasifica por frecuencia, por lo que favorece la respuesta incorrecta.

Lo que funciona en su lugar:

La coocurrencia aumenta las líneas de recompensa donde están presentes múltiples grupos semánticos (el tema más un grupo de valores). La página A en cada ejemplo empareja el tema con un valor; La página B solo tiene el tema. Los patrones de alto valor refuerzan las líneas que contienen la forma de respuesta esperada, ya sea que la forma sea un patrón sintáctico (una expresión regular monetaria captura el Ejemplo 1, una expresión regular numérica captura el Ejemplo 2, una expresión regular de duración captura el Ejemplo 3) o una búsqueda de membresía (el léxico del país marca líneas que mencionan “Francia”, “FR” o “République française”). La ponderación del diccionario de expertos permite a un experto en el dominio marcar "prima", "límite de tasa", "período de aviso" como conceptos de valor requerido que solo obtienen una puntuación alta cuando se combinan con un patrón de valor.

Estas tres señales son conocimiento empresarial explícito, no heurística estadística. La fórmula IDF de BM25 no codifica ninguno de ellos, independientemente de la cantidad de datos del corpus que vea. El corpus empresarial también es demasiado limitado para que IDF pueda discriminarlo bien: un solo contrato, o una colección homogénea, carece de la variación entre documentos que BM25 fue creado para explotar.

Recomendación práctica: si ya dispone de un índice BM25, consérvelo como base económica. Si no lo hace, no agregue uno por el bien de la "recuperación híbrida". Invierta el mismo esfuerzo de ingeniería en el diccionario experto, los patrones de coocurrencia y el catálogo de expresiones regulares. Obtendrás mayor precisión por hora invertida.

3.3 Coincidencia de incrustación de fragmentos

El método estándar de búsqueda de vectores: fragmenta line_df en fragmentos de 200 a 500 tokens, incrusta cada fragmento una vez en el momento de la ingesta, incrusta la consulta en el momento de la recuperación y devuelve los fragmentos con la mayor similitud de coseno.

Cuando gana.

Falta de coincidencia de vocabulario donde ayudan las reescrituras. La “salida anticipada” se acerca a las “disposiciones de terminación anticipada” una vez que las reescrituras de las preguntas analizadas están en juego. Errores de coincidencia de palabras clave; incrustar capturas. Preguntas conceptuales o confusas: “¿Es razonable el límite de responsabilidad?”: sin palabra clave específica. La incrustación puede exponer pasajes con la forma conceptual correcta. Documentos sin TOC y sin vocabulario especializado. Memos, correos electrónicos, artículos. Sin estructura para navegar; ningún diccionario experto. Incrustar similitud es la señal más útil.

Tres principios cuando este es tu método.

Varias consultas, no una: utilice las reescrituras del análisis de preguntas. Similitud máxima entre reescrituras, no promedio. Un fragmento que coincide bien con una reescritura supera a un fragmento que coincide mediocremente con todas las reescrituras. Es una señal, no todo el proceso. Combine con palabras clave de contenido y métodos TOC (Sección 4).

Nota sobre el LLM en line_df: a veces verá propuestas para ejecutar un filtro basado en LLM directamente en el contenido de line_df, por ejemplo, calificar cada fragmento pasándolo a través de un LLM con la pregunta. Esto funciona para documentos muy pequeños (unos pocos cientos de líneas), pero no escala: con 10.000 líneas, una llamada LLM por línea tiene un costo y una latencia prohibitivos. La serie maneja la comprensión de LLM a nivel de contenido mediante el análisis de preguntas (reescrituras y diccionario) en lugar de llamadas de LLM por consulta sobre el contenido. El LLM participa en la configuración de las palabras clave antes de su recuperación, no en la puntuación de las líneas durante la recuperación.

3.4 Coseno a nivel de línea para explicar (y reclasificar) un acceso a nivel de página

Una queja persistente contra la recuperación de incrustaciones a nivel de página es que la puntuación es opaca. Cuando retrieve_pages_by_similarity devuelve la página 9 con coseno 0,7843 y la página 6 con coseno 0,7728, no hay forma de decir qué líneas de cada página impulsaron la puntuación y, por lo tanto, no hay forma de defender la clasificación ante un auditor. La denuncia es la que planteó el oleoducto mínimo RAG en el mismo documento y la misma pregunta.

La solución es el mismo truco de incrustación aplicado con una granularidad más fina. Elija las K páginas principales de la recuperación a nivel de página. Para cada uno, inserte cada línea en la página, ejecute el coseno en la pregunta y observe las líneas superiores. El resultado es una explicación por línea de la señal a nivel de página: qué líneas contienen vocabulario cercano a la pregunta.

Página 9 (ganador a nivel de página): la señal se dispersa en las filas de la Tabla 3, sin punto de acceso compacto – Imagen del autor
Página 6 (la respuesta real): un denso grupo de color rojo intenso en el título de la sección y las ecuaciones – Imagen del autor

Cuatro cosas se hacen visibles inmediatamente en el mapa de calor.

Primero, la vista a nivel de línea muestra lo que lleva cada página y en qué parte de la página se encuentra la señal. La página 9, la ganadora a nivel de página en coseno, muestra una dispersión de tintes naranjas en las filas de la Tabla 3: líneas que comparan la “codificación posicional sinusoidal” con las “incrustaciones posicionales aprendidas”, contenido relacionado, pero no la sección que define la codificación posicional. La página 6, clasificada en tercer lugar por coseno a nivel de página, muestra un denso grupo de color rojo intenso en su sección titulada "Codificación posicional" y las ecuaciones que siguen. La línea superior de la página tiene una puntuación de 0,8902, más alta que cualquier línea de la página 9.

En segundo lugar, la clasificación a nivel de página es un mal agregador. La página 6 contiene la sección que responde a la pregunta, pero la página tiene un promedio de 53 líneas (la sección de codificación más el contenido circundante) y el resto reduce la media. La página 9 tiene una distribución más plana, por lo que su media se mantiene más alta aunque su pico sea más bajo. Cambiar la puntuación de la página de mean(line_sims) a max(line_sims) (una línea de código) invierte la clasificación y coloca la página 6 en primer lugar en esta pregunta.

En tercer lugar, la vista a nivel de línea restaura el marco de anclaje/contexto del Artículo 7A (recuperación como filtrado). Cada línea superior es un ancla (la frase real que obtuvo una puntuación alta, auditable para el lector); la página es el contexto que se transmite de generación en generación. El mismo patrón detect_then_extract, con incrustación en lugar de palabra clave.

Cuarto, y como era de esperar cuando uno lee las líneas superiores reales: las líneas con mayor puntuación en ambas páginas son las que contienen físicamente codificación posicional o incrustación posicional. En la página 6, los tres primeros son el título de la sección en sí (Codificación posicional) y dos líneas del cuerpo que nombran el término (“como las incrustaciones, para que los dos puedan sumarse”, “donde pos es la posición e i es la dimensión”). En la página 9, las dos más fuertes son las líneas de la fila (E) de la Tabla 3 que mencionan la variación (“codificación posicional sinusoidal con incrustaciones posicionales aprendidas”, “incrustación posicional en lugar de sinusoides”). La incrustación no está sacando a la superficie ninguna propiedad semántica profunda aquí. Se trata de encontrar la palabra clave literal, y el resto de las palabras de cada línea mueven la puntuación unos pocos puntos porcentuales alrededor de la línea base de la palabra clave. Cuando la pregunta incluye un término clave obvio, la incrustación a nivel de línea se reduce a la concordancia de palabras clave con pasos adicionales. Un filtro simple str.contains("positional") en line_df habría mostrado las mismas líneas superiores, en microsegundos, sin ningún modelo incrustado en el bucle.

La forma de la distribución de la puntuación añade una segunda y útil señal: en la página 6, las cinco líneas superiores decaen suavemente (0,890, 0,866, 0,843, 0,802, 0,798), una densa meseta que indica que “esta sección analiza el tema”; en la página 9, la distribución muestra dos valores atípicos (0,826, 0,823) y luego una caída de cinco puntos hasta 0,771, un patrón escaso que indica que “la página menciona el tema de pasada”. La densidad de líneas por encima del umbral, no sólo el máximo, separa una sección definitoria de una mención pasajera.

Lo que esto no significa: una puntuación de incrustación aún no puede indicarle si la línea coincidente es la definición que deseaba o una mención pasajera en una tabla de comparación. La vista a nivel de línea hace que la clasificación sea inspeccionable; no lo hace preciso. Para mayor precisión, cuando la pregunta conlleva un token de alta señal (codificación posicional, límite de responsabilidad, fecha de vigencia), los métodos de palabras clave y expresiones regulares de la Sección 3 siguen siendo las herramientas adecuadas. Una concordancia de palabra clave en "codificación posicional" habría marcado tanto la página 6 como la página 9 directamente, sin ningún paso de inserción. El coseno a nivel de línea es la superposición de auditoría que hace que un proceso de incrustación a nivel de página sea defendible cuando las incrustaciones son el método correcto para empezar, y el Artículo 7C (el árbitro LLM) desarrolla cuándo lo es y cuándo no.

4. Combinando las dos tablas

La canalización de la Sección 1 ejecuta detectores en ambas mesas en paralelo y entrega todo al árbitro al final. Las combinaciones siguientes son tres formas concretas en que la capa de detección puede realizar una polinización cruzada de line_df y toc_df antes de que el árbitro vea a los candidatos: cada una facilita el trabajo del árbitro al entregarle candidatos con alcance y potenciados previamente en lugar de coincidencias sin formato.

4.1 Razón y luego coincidencia (alternativa de dos llamadas LLM)

Un proceso de dos etapas que utiliza una llamada de LLM adicional por adelantado: el LLM lee el TOC, selecciona las secciones relevantes, devuelve una breve lista de ID de sección y luego la recuperación de palabras clave se ejecuta solo en las líneas dentro de esas secciones.

Cuándo vale la pena hacer una llamada adicional: un contrato de 100 páginas tiene 50 secciones; el LLM elige de 2 a 3 en una llamada; La recuperación de palabras clave opera en unos pocos cientos de líneas en lugar de las 15.000 completas. La compensación frente al patrón de árbitro único: usted paga dos llamadas de LLM en lugar de una, pero la búsqueda de palabras clave de la segunda etapa se realiza en un grupo mucho más pequeño, lo que importa cuando el grupo es enorme (piense: una presentación regulatoria de 500 páginas). Para documentos del rango empresarial típico (de 10 a 100 páginas), el patrón de árbitro único es suficiente; el árbitro ve el TOC completo y la palabra clave al mismo tiempo y realiza el razonamiento de la sección como parte de su decisión única.

Flujo de dos etapas: LLM selecciona secciones de toc_df, las palabras clave puntúan líneas en el interior – Imagen del autor

Este patrón es un truco de reducción previo al árbitro. El despachador lo utiliza en documentos muy grandes donde el patrón de árbitro único alimentaría demasiadas líneas al LLM; En documentos empresariales de tamaño normal, el despachador incorpora el razonamiento TOC al propio árbitro.

def Reason_then_match(question: str, Primary_kw: list[str], second_kw: list[str], line_df: pd.DataFrame, toc_df: pd.DataFrame, top_n: int = 5): """Etapa 1: LLM selecciona secciones de toc_df. Etapa 2: puntuación de palabras clave dentro de esas secciones. La etapa 2 filtra por rango de páginas (no por ID de sección) para que Se incluyen todas las subsecciones anidadas que comparten páginas con sus padres: line_df.section_id se colapsa cuando una página aloja varias secciones, pero la unión del rango de páginas recupera todo lo que está dentro del alcance seleccionado """ selección = Reason_on_toc(question, toc_df) relevantes_ids = set(selection.section_ids) print(f"Secciones seleccionadas de la etapa 1: {sorted(relevant_ids)}") print(f" razonamiento: {selection.reasoning[:200]}") páginas_relevantes = set() para sid en identificadores_relevantes: filas = toc_df[toc_df["section_id"] == sid] para _, segundo en filas.iterrows(): páginas_relevantes.update(range(int(sec["start_page"]), int(sec["end_page"]) + 1)) candidatos = line_df[line_df["page_num"].isin(relevant_pages)].copy() candidatos["score"] = candidatos["text"].apply( lambda t: co_occurrence_score(t, Primary_kw, second_kw) ) top = candidatos[candidatos["score"] > 0].sort_values("score", ascending=False).head(top_n) return top # Un lector pregunta: ¿dónde está la fórmula real? No la prosa sobre la atención. resultado = Reason_then_match( "¿Dónde definen la fórmula real para la atención del producto punto escalado?", primario_kw=primario, secundario_kw=secundario, line_df=line_df, toc_df=toc_df, ) resultado[["page_num", "line_num", "section_id", "score", "text"]]

Ejecute la pregunta "¿Dónde definen la fórmula real para la atención escalada del producto punto?", con las secciones seleccionadas de la etapa 1 y las líneas de mayor puntuación de la etapa 2 una al lado de la otra:

El seguimiento en dos etapas hace que el resultado sea auditable de principio a fin – Imagen del autor

4.2 Coincidencia ponderada por sección

Una línea que coincide con la palabra clave y se encuentra en una sección cuyo título también coincide tiene muchas más probabilidades de ser la respuesta que una línea que coincide con la palabra clave en una sección no relacionada. Esta es la alternativa de cero LLM cuando desea una puntuación previa económica que tenga en cuenta la sección antes de la llamada del árbitro. Ambas señales (concordancia de título y concordancia de contenido) son operaciones puras de palabras clave. La calidad está por debajo del patrón de árbitro completo en los casos difíciles, pero es adecuada en la mayoría de los casos fáciles, y puede usarlo como prefiltro cuando el grupo de candidatos es demasiado grande para enviarlo al LLM de una sola vez.

def section_weighted_match(question: str, line_df: pd.DataFrame, toc_df: pd.DataFrame, Primary_kw: list[str], second_kw: list[str], boost: float = 1.5): """Puntuación de líneas por co-ocurrencia de palabras clave, mejorada cuando el título de la sección de la línea también coincide.""" title_keywords = [w for w in question.lower().split() if len(w) > 3] relevantes_section_ids = set(match_titles(toc_df, title_keywords)["section_id"]) puntuado =[]para fila en line_df.itertuples(): line_score = co_occurrence_score(row.text, Primary_kw, second_kw) si line_score == 0: continuar sección_boost = aumentar si fila.section_id en relevantes_section_ids else 1.0 scoring.append((row.line_num, fila.page_num, fila.section_id, line_score * sección_boost, fila.text)) return pd.DataFrame(scored, columns=["line_num", "page_num", "section_id", "score", "text"]).sort_values("score", ascending=False).head(8) sección_weighted_match( "¿Cómo se calcula realmente la atención en este documento?", line_df, toc_df, primaria_kw=primaria, secundaria_kw=secundaria,)

Ejecute la misma pregunta de atención, con un aumento de 1,5 veces cuando el título de la sección de la línea también coincide:

Las líneas en las secciones sobre temas obtienen un aumento de 1,5 veces; barato, determinista – Imagen del autor

4.3 Incrustación híbrida

Para documentos en los que las incrustaciones son el método base adecuado (desajuste de vocabulario, preguntas conceptuales), el principio de ponderación de secciones aún se aplica: incrustar páginas de búsqueda y luego impulsar páginas en secciones cuyos títulos coincidan con la pregunta. Esto recupera la conciencia estructural de la que carece la búsqueda pura de incrustación. Incluso cuando las incrustaciones son el método base correcto, saber a qué sección pertenece un fragmento y si esa sección está relacionada con el tema mejora sustancialmente la precisión.

def hybrid_embedding(question: str, page_df: pd.DataFrame, line_df: pd.DataFrame, toc_df: pd.DataFrame, top_k: int = 5, boost: float = 1.3): """Integre páginas de búsqueda, impulse aquellas en secciones cuyo título coincida con el vocabulario de las preguntas.""" title_keywords = [w for w in question.lower().split() if len(w) > 3] relevantes_section_ids = set(match_titles(toc_df, title_keywords)["section_id"]) page_to_section = ( line_df.dropna(subset=["section_id"]) .groupby("page_num")["section_id"] .agg(lambda s: s.value_counts().index[0]) .to_dict() ) recuperado, _ = recuperar_páginas_por_similaridad(página_df, línea_df, pregunta, top_k=top_k * 2, cliente=cliente) puntuado = recuperado.copia() puntuado["section_id"] = puntuado["page_num"].map(page_to_section) puntuado["boosted_sim"] = puntuado.apply( lambda r: r["similitud"] * (impulsar si r["section_id"] en relevantes_section_ids else 1.0), eje=1, ) return scoring.sort_values("boosted_sim", ascending=False).head(top_k)[["page_num", "section_id", "similitud", "boosted_sim", "text"]] hybrid_embedding("¿Cómo se calcula realmente la atención en este documento?", page_df, line_df, toc_df, top_k=5)

Ejecute la misma pregunta de atención, con el impulso del título de la sección aplicado a la puntuación del coseno incrustado:

Top-k ingenuo más un aumento de 1,3 veces cuando la página se encuentra en una sección relacionada con el tema – Imagen del autor

4.4 Por qué ganan las combinaciones

Los detectores individuales fallan demasiado por sí solos:

La coincidencia de palabras clave del título falla cuando los títulos no comparten vocabulario con la pregunta. La concordancia de palabras clave de contenido falla cuando la misma palabra aparece en muchas secciones, de las cuales solo una es relevante. La coincidencia de incrustación de fragmentos falla cuando la respuesta está en una sección específica que las incrustaciones no pueden aislar.

Las combinaciones anteriores agregan una señal entre tablas para cada candidato (su sección, la superposición de palabras clave del título, un impulso de inserción). El árbitro que desarrolla el Artículo 7C (el árbitro LLM) es lo que los unifica: ve las coincidencias de palabras clave, las coincidencias de incrustación, el vínculo estructural y las clasifica una vez con los motivos. Las combinaciones facilitan el trabajo del árbitro; no lo reemplazan.

El LLM puede ver que una línea de puntuación de alta co-ocurrencia "la prima para el complemento opcional es 200" es menos relevante que una línea de puntuación más baja "l'assurance comprend une prime annuelle de 4 500" porque la pregunta era sobre la prima principal.

Despojado de la etiqueta de marketing (“RAG agente”), es: utilizar una pequeña llamada de LLM para emitir un juicio, después de que los métodos deterministas hayan hecho su trabajo. Desarrollamos la lógica de combinación en un pipeline integrado de seguimiento.

Una nota sobre los reclasificadores de codificadores cruzados (Cohere Rerank, bge-reranker, monoT5 y la amplia familia que vuelve a calificar a los candidatos top-k mediante un modelo de relevancia aprendido). La posición de la serie es que los reordenadores son una solución para la recuperación ascendente débil, no una etapa predeterminada de recuperación fuerte. Cuando la etapa ascendente ya incorpora palabras clave expertas, razonamiento TOC, filtros de metadatos y selección de alcance estructural (el trabajo del que trata este artículo), el pasaje correcto ya se encuentra cerca de la parte superior y un codificador cruzado brinda un pequeño impulso con un costo real de latencia e inferencia. Cuando el ascendente es una similitud de coseno genérico sobre un almacén de vectores indiferenciados, un reordenador recupera una gran brecha. Pero la respuesta arquitectónica correcta es fortalecer la etapa aguas arriba para que la brecha no se abra en primer lugar. La serie trata la reclasificación como el parche sobre un canal ascendente delgado, útil en escenarios estrechos tolerantes a la latencia y cuando el flujo ascendente es barato de calcular y difícil de mejorar, no como un valor predeterminado. Varios puntos de referencia públicos (BEIR, mTEB) muestran grandes ganancias al reclasificar además de la recuperación de sólo vectores; en la recuperación híbrida que ya incorpora BM25 y filtros de metadatos, la ganancia marginal es mucho menor. Esa brecha es la posición editorial.

Los cuatro métodos, uno al lado del otro: La misma pregunta, cuatro perros perdigueros. Observe cómo cada método se queda corto y la combinación gana.

Cuatro métodos uno al lado del otro; razón-luego-coincidencia gana en la pregunta de fórmula – Imagen del autor

Detectar y luego extraer: la combinación canónica “anclar en un título de TOC, extraer del cuerpo de la sección”. El ancla es una fila toc_df encontrada por palabra clave en el título; el contexto es el cuerpo completo de esa sección extraído de line_df. Esta es la función a la que hace referencia el Artículo 7A al introducir el marco de doble alcance.

def detect_then_extract(toc_df: pd.DataFrame, line_df: pd.DataFrame, palabras clave: list[str]): """Ancle los títulos de toc_df, luego extraiga el cuerpo de la sección por rango de páginas de line_df.""" matched = match_titles(toc_df, palabras clave) si coincide.empty: devuelve Ninguno, Ninguna sección = matched.iloc[0]body = line_df[ (line_df["page_num"] >= sección["start_page"]) & (line_df["page_num"] <= sección["end_page"]) ] volver sección, cuerpo # Un usuario pregunta "¿dónde describen la capacitación?" — el ancla es un título breve, el contexto es el cuerpo completo de la sección de capacitación. sección, cuerpo = detect_then_extract(toc_df, line_df, ["Capacitación"]) print("=== ANCHOR (toc_df, alcance: título, algunas palabras) ===") print(f" coincidente: sección_id={sección['section_id']} título='{sección['título']}'") print(f" rango de páginas: {sección['start_page']} a {sección['end_page']}") print() print(f"=== EXTRACCIÓN DE CONTEXTO (line_df, alcance: cuerpo de sección completo, {len(body)} líneas) ===") para _, ln en body.head(8).iterrows(): print(f" p{ln['page_num']:>2} l{ln['line_num']:>3}: {ln['text'][:90]}")

Ejecute con la palabra clave Capacitación en comparación con el TOC del documento de atención: la fila del TOC coincidente es una línea, el cuerpo de la sección extraída tiene docenas de líneas:

Dos ámbitos en una sola ejecución: el ancla es una fila de TOC, el contexto es el cuerpo de la sección – Imagen del autor

5. Conclusión

La detección de anclas se ejecuta en tres etapas, con una llamada de LLM al final.

Etapa 1. Detección (paralela). La detección de palabras clave en line_df y toc_df siempre se ejecuta (es gratuita y auditable). Las incrustaciones se ejecutan en paralelo como una segunda señal opcional, útil para discrepancias de vocabulario y preguntas conceptuales. Etapa 2. Agregado. Las visitas se agrupan en una unidad estructural (sección mediante toc_df si está disponible; de ​​lo contrario, página o fragmento). Etapa 3. Una convocatoria LLM. El árbitro ve el TOC, las coincidencias de palabras clave, las coincidencias de incorporación y el adjunto estructural, todo en una sola llamada. Realiza el razonamiento TOC y la clasificación final juntos.

Tres patrones de composición (razón y coincidencia, coincidencia ponderada por sección, incrustación híbrida) polinizan de forma cruzada line_df y toc_df antes de que el árbitro vea a los candidatos, lo que facilita su trabajo en documentos grandes o duros.

Lo que el LLM al final realmente hace con esos candidatos, lo que devuelve y cómo se convierte en un JSON defendible para la generación es el tema del Artículo 7C (el árbitro del LLM y el JSON de salida de recuperación): el informe estructurado entregado al árbitro, los roles por candidato (principal / de apoyo / tangencial / descartado) con razones, la pista de auditoría, el despachador que elige qué detectores ejecutar por pregunta, la ruta "no encontrada" y la ruta unificada Contrato de recuperación de resultados.

Este artículo forma parte de la serie Enterprise Document Intelligence. La canalización RAG mínima muestra la detección de anclajes en uso de un extremo a otro en un PDF real.

6. Fuentes y lecturas adicionales

La detección de anclaje combina la puntuación de palabras clave (siempre activa, auditable) con similitud de incrustación opcional, entregando ambas a un árbitro de LLM al final. Las referencias siguientes cubren los detectores.

Misma dirección que el artículo:

Robertson & Zaragoza, El marco de relevancia probabilística: BM25 y más allá, FnT IR 2009. Referencia canónica de BM25; La afirmación del artículo de que BM25 mide la frecuencia en la que la recuperación de negocios necesita coexistencia se basa en esto. Thakur et al., BEIR: Un punto de referencia heterogéneo para la evaluación de disparo cero de modelos de recuperación de información, NeurIPS 2021 (arXiv:2104.08663). Hay apoyo empírico para la afirmación del artículo de que BM25 es una base sólida que los perros perdigueros densos no siempre superan. Karpukhin et al., Recuperación de pasajes densos para respuesta a preguntas de dominio abierto (DPR), EMNLP 2020 (arXiv:2004.04906). Recuperación densa como valor predeterminado de producción; contraste útil con la postura de este artículo de dar prioridad a las palabras clave sobre los corpus empresariales.

Al principio de la serie:

Document Intelligence: introducción a la serie. Qué construye la serie, ladrillo a ladrillo y en qué orden. Baseline Enterprise RAG, desde PDF hasta respuesta resaltada. El canal de cuatro ladrillos de un extremo a otro: PDF de entrada, respuesta resaltada de salida. Las incrustaciones no son mágicas: los modos de falla predecibles de la recuperación de RAG. Dónde gana la incorporación de similitud (sinónimos, errores tipográficos, paráfrasis), dónde predeciblemente se rompe (términos desconocidos, negación, relevancia de término versus respuesta) y cómo usarlo de todos modos. Los rerankers tampoco son mágicos: cuando la capa de codificador cruzado vale la pena. Lo que agrega un codificador cruzado sobre las incorporaciones de codificador doble, medido y cuándo vale la pena la latencia. RAG no es aprendizaje automático y el conjunto de herramientas de ML resuelve el problema equivocado. Por qué los barridos y ajustes de tamaño de fragmentos optimizan lo incorrecto; en su lugar, ruta por tipo de pregunta. De expresiones regulares a modelos de visión: qué técnica RAG se adapta a qué problema. Dos ejes, complejidad documental y control de preguntas, que seleccionan la técnica para cada caso. 10 errores comunes de RAG que seguimos viendo en producción. Diez errores de producción, organizados ladrillo por ladrillo, con la solución para cada uno. Más allá de extract_text: las dos capas de un PDF que impulsan la calidad RAG. La primera mitad del bloque de análisis: la naturaleza del documento, las señales y el resumen. Deje de devolver texto plano desde un PDF: la forma relacional que RAG necesita. La segunda mitad del bloque de análisis: las tablas relacionales que lee cada bloque posterior.