La forma más rápida de entender qué es RAG es crear la versión más pequeña que realmente funcione, ejecutarla en un documento real y observar de cerca lo que acaba de suceder.
Ese es este artículo. Aproximadamente cien líneas de Python (sin base de datos vectorial, sin marco, sin agentes) ejecutándose en el documento Attention Is All You Need (Vaswani et al. 2017; licencia de distribución no exclusiva de arXiv, declarada en la página de resumen de arXiv), devolviendo una respuesta con las líneas fuente exactas resaltadas en la página.
Luego volvemos a recorrer cada bloque y hacemos la pregunta que plantea naturalmente. Cada pregunta es lo que desarrolla un artículo posterior.
La canalización mínima es la cantidad más pequeña de código que respeta los cuatro ladrillos y produce una respuesta verificable. Cada artículo posterior agrega capacidades que el equipo necesita después de una falla específica en documentos reales, no porque la arquitectura necesite más capas.
1. Qué estamos construyendo
La tubería tiene cuatro ladrillos (la Parte II analiza cada uno en detalle) más un paso final de renderizado opcional. Cada ladrillo dice lo que recibe y lo que devuelve; lo que pasamos de un ladrillo a otro es lo que ahorramos.
El análisis del documento toma una ruta PDF y devuelve line_df (una fila por línea de texto, con page_num, line_num, texto y el cuadro delimitador) más page_df. La versión mínima guarda ambos en la memoria; los sistemas más grandes los conservan (el artículo 23 cubre cuándo pasar a una base de datos). El análisis de preguntas convierte la pregunta del usuario en una ParsedQuestion que contiene la pregunta normalizada más una breve lista de palabras clave marcadas. Se mantiene estrecho a propósito: aquí no hay lógica de recuperación, no hay incrustación de preguntas. La recuperación consume ParsedQuestion y emite los primeros números de página (y, cuando sea necesario, los números de línea coincidentes dentro de esas páginas). Mantener el traspaso a los números de página sólo lo mantiene pequeño; el siguiente paso reconstruye las líneas filtradas de line_df en el acto. La cuestión de la incrustación vive en este ladrillo porque depende del índice del corpus. La generación reúne la pregunta, line_df y los números de página recuperados, y produce una AnswerWithEvidence: un JSON escrito que contiene la respuesta, el intervalo de evidencia (página_inicio, línea_inicio, página_final, línea_final), una confianza, una justificación, las citas exactas de la fuente y cualquier advertencia. Vale la pena guardar el JSON completo para su evaluación, auditoría y reproducción. La anotación en PDF es opcional. Dado el PDF de origen y la extensión de la evidencia, escribe un PDF anotado con rectángulos dibujados alrededor de las líneas citadas. Una herramienta CLI, un trabajo por lotes o un consumidor de API pueden omitirlo; la respuesta con citas ya está completa después de generación.
Los primeros cuatro son los cuatro ladrillos (el artículo 5 desarrolla el análisis de documentos, el artículo 6 el análisis de preguntas, el artículo 7 la recuperación, el artículo 8 la generación). La anotación en PDF es el paso de renderizado, no un ladrillo en sí mismo.
Se incluye un PDF y una pregunta. Cada bloque convierte su entrada en algo más estructurado: el análisis del documento convierte el PDF en filas, el análisis de la pregunta convierte la pregunta en palabras clave listas para la búsqueda, la recuperación reduce las filas a unos pocos números de página, la generación produce una respuesta escrita y la anotación del PDF devuelve las líneas citadas a la fuente. Lo que surge no es una burbuja de chatbot. Es una respuesta JSON obtenida más un PDF anotado que puede abrir y verificar.
Las dependencias son mínimas:
pymupdf analiza archivos PDF en texto más información de posición; los cuadros delimitadores que devuelve son los que usamos para resaltar la respuesta en la página de origen. openai es el cliente LLM; A través de base_url, la misma biblioteca sirve a Azure, OpenRouter, Ollama o cualquier punto final compatible. pandas mantiene el documento como un DataFrame, el formato que utiliza cada paso de análisis y recuperación. pydantic define el esquema de respuesta que fuerza JSON estructurado con citas.
Sin base de datos vectorial, sin marco de orquestación, sin biblioteca RAG especializada. Artículos posteriores analizan cuándo los ayudantes de esas bibliotecas se vuelven útiles y cuándo interfieren con la visión de lo que está sucediendo.
"Para un artículo de 15 páginas, el LLM puede leerlo completo. ¿Por qué molestarse en recuperarlo?" Punto justo en este documento. Usamos el papel para enseñar el método, no para guardar fichas en estas 15 páginas. La objeción a menudo apunta al punto de referencia Needle in a Haystack (Kamradt, 2023), donde los modelos de frontera obtienen una puntuación casi perfecta al recuperar una sola oración palabra por palabra de un contexto de 1 millón de tokens.
Ese punto de referencia es la investigación, no la práctica. Una aguja es un hecho aislado y textual, mientras que las preguntas empresariales se agregan (“cada contrato cuyo deducible exceda los 5.000 €”), se comparan (“cláusula 12 de estas tres pólizas”) o se resumen en muchos pasajes. Ninguno de esos es una sola frase para encontrar.
Dos razones prácticas más mantienen la recuperación al día. Los documentos empresariales suelen ser largos:
un contrato de seguro de 300 páginas, un expediente reglamentario de 500 páginas, una especificación técnica de varios volúmenes.
Enviar todo al LLM cuesta dinero real en cada pregunta, cada repetición, cada usuario, y diluye su atención en páginas irrelevantes.
Y la misma pregunta se repite en cientos o miles de documentos a la vez:
"encontrar todos los contratos que excluyen los daños por terremoto", "resumen los cambios regulatorios de este año en todas las presentaciones".
A esa escala, “tirarlo todo” deja de ser una estrategia. La recuperación es lo que hace que el oleoducto sobreviva a ambos movimientos: de un documento corto a un contrato largo, y de un documento a un corpus completo.
2. Los cuatro ladrillos y un PDF resaltado
Cada paso declara sus entradas y salidas, y los pasos son independientes. La salida del paso N es la entrada del paso N+1, guardada como un DataFrame con nombre para que cualquier paso pueda volver a ejecutarse por sí solo con la salida guardada del anterior. En la era de la codificación de IA, un asistente al que se le pide que "arregle la recuperación" puede modificar silenciosamente el analizador de preguntas cuando debería haber permanecido intacto. Los módulos independientes le permiten trabajar con confianza en una pieza sin romper el resto.
Los fragmentos de configuración a continuación los cargan junto con el cliente OpenAI.
Cada bloque que habla con un modelo necesita un cliente configurado. La serie utiliza el SDK de Python de OpenAI; cualquier proveedor que exponga un punto final compatible con OpenAI (Azure OpenAI, vLLM, –api-server de llama.cpp,…) ingresa cambiando base_url y el nombre del modelo.
importar sistema operativo desde openai importar OpenAI desde dotenv importar load_dotenv load_dotenv() cliente = OpenAI( api_key=os.getenv("API_KEY"), base_url=os.getenv("BASE_URL"), ) model_chat = os.getenv("MODEL_CHAT", "gpt-4.1") model_embed = os.getenv("MODEL_EMBED", "incrustación de texto-3-pequeño")
2.1 Análisis de documentos
Extraemos cada línea de texto del PDF junto con su posición en la página. La salida es un DataFrame donde cada fila es una línea, con page_num, line_num, el texto en sí y las cuatro coordenadas del cuadro delimitador x0, y0, x1, y1.
En: una ruta PDF. Salida: line_df (una fila por línea de texto, con page_num, line_num, texto y el cuadro delimitador) más un page_df que crearemos en la sección 2.3.
Los cuadros delimitadores son importantes: son los que usamos para resaltar el PDF de origen al final.
def fitz_pdf_to_line_df(ruta_archivo): doc = fitz.open(ruta_archivo) datos =[]para page_num en rango(len(doc)): página = doc[page_num] bloques = page.get_text("dict").get("bloques",[]) line_num = 0 para el bloque en bloques: si block.get("type") != 0: continúa para la línea en block.get("lines",[]): abarca = line.get("se extiende",[]) si no se extiende: continúe text = "".join(s["text"] para s en intervalos) rect = fitz.Rect(spans[0]["bbox"]) para intervalo en intervalos[1:]: rect |= fitz.Rect(span["bbox"]) data.append({ "page_num": page_num + 1, "line_num": line_num + 1, "text": texto, "x0": float(rect.x0), "y0": float(rect.y0), "x1": float(rect.x1), "y1": float(rect.y1), }) line_num += 1 retorno pd.DataFrame(datos)
Al ejecutar line_df = fitz_pdf_to_line_df(pdf_path) en el documento de Atención se obtienen 1048 líneas en 15 páginas.
El papel, convertido en filas. Cada línea es una fila, con su texto y los cuatro números que la ubican en la página. Las columnas x0, y0, x1, y1 aún no significan mucho; en la sección 2.5 son los que usamos para dibujar rectángulos en el PDF de origen, exactamente sobre las líneas que citó el modelo.
Este DataFrame, line_df, es la estructura de datos central del resto de la serie. El artículo 5 presenta un modelo relacional más rico (line_df, chunk_df, toc_df, page_df, image_df).
Lo que este analizador no hace: detectar tablas (Tabla 1 página 4, Tabla 3 página 9 aplanadas en líneas simples), reconstruir encabezados, notas al pie, referencias cruzadas o manejar diseños de varias columnas. Nada de esto importa para la pregunta que hacemos aquí. Para otras preguntas sobre el mismo documento, lo hará. El artículo 5 cubre el análisis en su totalidad.
2.2 Análisis de preguntas
Antes de que la pregunta sea recuperada, la ejecutamos a través de una pequeña llamada de LLM. El objetivo es extraer las palabras clave más útiles para buscar en el documento: frases cortas que probablemente utilice el documento, no necesariamente las palabras literales de la pregunta.
En: una pregunta de texto. Salida: una ParsedQuestion que contiene la pregunta normalizada y una breve lista de palabras clave marcadas.
Este paso no sabe acerca de la recuperación. Tampoco calcula la incrustación de preguntas. Ese está vinculado al índice de corpus y se encuentra en la sección 2.3. Mantenga esa línea limpia y podrá cambiar el modelo de incrustación o agregar un recuperador híbrido mañana sin tocar el análisis de preguntas.
¿Por qué preocuparse por una tubería mínima? Dos razones:
Puedes explicar por qué la recuperación eligió lo que eligió. Cuando el sistema responde mal, podemos ver si las palabras clave estaban equivocadas (problema de análisis de preguntas) o si las palabras clave correctas llegaron a la página incorrecta (problema de recuperación). Sin lugar a dudas, la recuperación es una caja negra. La pregunta es un insumo real, al igual que el documento. La sección 2.1 analizó el documento en line_df. Esta subsección analiza la pregunta en ParsedQuestionMinimal. Ambas entradas merecen ser analizadas antes de llegar al paso de búsqueda. El artículo 6 construye el bloque más rico (parse_question, con forma de respuesta, filtros de alcance, descomposición,…).
En la pregunta "¿Cuáles son las opciones mencionadas para la codificación posicional?", la llamada parsed_question = get_keywords_from_question(question, client=client) devuelve parsed_question.keywords = ['codificación posicional', 'opciones', 'mencionada'].
question = "¿Cuáles son las opciones mencionadas para la codificación posicional?" parsed_question = get_keywords_from_question(pregunta, cliente=cliente) print(parsed_question.keywords) ['codificación posicional']
El LLM produce una frase única y literal como ['codificación posicional']. Eso es deliberado. Un borrador anterior de este mensaje pedía "de 3 a 5 palabras clave cortas útiles para realizar búsquedas", y el LLM felizmente llenó la cuota con paráfrasis (opciones de codificación posicional, tipos de codificación posicional, codificación posicional transformadora). Ninguno de ellos está escrito en el documento. Sólo lo es la codificación posicional. La coincidencia de subcadenas es estricta: una sola palabra faltante anula la coincidencia. La versión mínima le pide al LLM que haga menos (extraer la frase nominal literal, eliminar el marco de la pregunta) y confía en que el siguiente bloque haga el resto.
Lo que esta versión mínima no hace:
detectar una forma_respuesta (Preguntas y respuestas frente a resumen) descomponer preguntas compuestas extraídas de un glosario de dominio adjuntar sugerencias de recuperación
Todo cubierto en el Artículo 6, bajo el bloque parse_question más rico. Aquí mantenemos dos campos, pregunta_corregida y palabras clave, la versión más pequeña que hace visible el ladrillo.
Nota: anulando el indicador del sistema. get_keywords_from_question expone el indicador del sistema como un kwarg con KEYWORDS_PROMPT como predeterminado. Para probar una variante (dominio diferente, reglas más estrictas, ejemplos adicionales), pase system_prompt=… en el sitio de la llamada. No hay edición de la función. El mismo patrón para cada asistente de LLM en docintel (llm_answer_with_evidence expone tanto system_prompt como user_template). Abajo: la misma llamada, realizada dos veces sobre una pregunta de tipo contrato. Primero, con el valor predeterminado del artículo de investigación, que sigue siendo genérico. Luego, con un mensaje de dominio de contrato, que recoge vocabulario de seguros como exclusiones y deducibles.
demo_question = "¿Los terremotos están excluidos de la cobertura?" # Predeterminado: mensaje de trabajo de investigación. parsed_question_default = get_keywords_from_question(demo_question, client=client) print("Predeterminado (artículo de investigación):", parsed_question_default.keywords) # Anulación: mensaje de seguro/contrato legal. contract_prompt = ( "Extraiga de 1 a 3 palabras clave breves de la pregunta del usuario para buscar un " "contrato de seguro o póliza legal. Prefiera los términos literales que " "es probable que utilice el contrato: cláusulas, exclusiones, riesgos nombrados, deducibles, topes. Suelte " "palabras que enmarcan la pregunta. Genere de 1 a 3 palabras clave." ) parsed_question_contract = get_keywords_from_question( demo_question, system_prompt=contrato_prompt, cliente=cliente, ) print("Solicitud de contrato: ", parsed_question_contract.keywords) Predeterminado (artículo de investigación): ['terremotos', 'cobertura'] Solicitud de contrato: ['terremotos', 'exclusiones', 'cobertura']
2.3 Recuperación
Enviar las 1048 líneas al LLM funciona en un papel de este tamaño, pero no escala y diluye la atención del modelo. Recortamos el documento a las pocas páginas que probablemente contengan la respuesta.
En: las palabras clave marcadas (y/o la pregunta normalizada, según el método) del apartado 2.2. Salida: los números de las páginas k superiores y, opcionalmente, los números de línea coincidentes dentro de esas páginas.
La incrustación de preguntas se calcula aquí, no en la sección 2.2, porque una incrustación sólo tiene sentido en relación con el índice sobre el que se creó. La misma lógica para cualquier puntuación híbrida o estadística BM25.
La respuesta estándar en los tutoriales de RAG 2024 son las incrustaciones: convertir cada página en un vector, puntuar por similitud de coseno. A ellos está dedicado el artículo 2. Para la versión mínima, no lo hacemos deliberadamente, por una razón.
Las incrustaciones son opacas. La similitud del coseno devuelve un número como 0,7798 y le pide al usuario que confíe en que "la página 6 es relevante para la pregunta". Muestre esa puntuación a un experto en el dominio, al propietario de un producto o a un gerente: nadie entiende lo que significa 0,78 ni por qué es superior a 0,65. Los desarrolladores pueden argumentar que lo entienden (“producto escalar de vectores normalizados”). Entienden las matemáticas, no la relevancia. Cuando se les pregunta por qué esta página específica obtuvo una puntuación de 0,7798 en esta pregunta específica, se encogen de hombros y señalan el modelo.
En un contexto empresarial, la recuperación es el paso que más cuestionan los usuarios. ¿Por qué el sistema miró esta página y no aquella? Tienes que explicarlo. Entonces, la versión mínima usa algo que podemos leer con nuestros propios ojos: la concordancia de palabras clave. La sección 2.2 extrajo las palabras clave; Calificamos cada página según la cantidad de esas palabras clave que aparecen en ella y mantenemos las tres primeras.
Dónde buscamos versus qué devolvemos: ambas páginas aquí. La recuperación real tiene dos niveles. El ancla es donde realmente llega la palabra clave o la incrustación (una línea, una oración). El contexto es lo que entregamos a la generación (las líneas que lo rodean, la página). Buscamos en pequeño, volvemos en grande. Aquí usamos la página para ambos. Eso funciona en un artículo académico donde cada página es aproximadamente una idea. El artículo 7 separa los dos niveles para contratos largos, informes de varias columnas y documentos con muchas tablas.
page_df = build_page_df(line_df) colapsa las 1048 líneas en 15 páginas, una fila por página.
2.3.a Incrustaciones + similitud de coseno
Incruste cada página (una llamada por página), incruste la pregunta, calcule la similitud del coseno, mantenga la k superior. El resultado: un número como 0,7798 por página. Mire las puntuaciones a continuación: ¿puede decir por qué una página quedó entre las tres primeras? ¿Podrías explicarle la clasificación a un experto en el dominio? Ése es el problema de la puntuación opaca con el que comienza el artículo.
Tres números, todos muy próximos entre sí (0,7843, 0,7798, 0,7728). ¿Puedes decir por qué la página 9 supera a la página 6? La vista previa del texto lo hace obvio: la página 9 es la tabla de variaciones en la arquitectura del transformador, la página 5 trata sobre los valores de salida y la concatenación, la página 6 es la tabla de longitudes máximas de ruta. La página que realmente responde a la pregunta, la sección 3.5 Codificación posicional, se encuentra en la página 6 y ocupa el último lugar entre los tres primeros. La página 5 no relacionada ocupa el segundo lugar. Las puntuaciones parecen precisas, pero la clasificación no tiene una historia detrás: no hay ningún token que señalar, ni frase que defender, sólo un producto escalar en dos vectores de cajas negras. Las incrustaciones funcionan en muchos casos, y el Artículo 2 explica de dónde proviene esta puntuación. Pero la partitura en sí nunca llega a ser interpretable, y durante el resto de este artículo utilizamos un recuperador que puedes leer con tus propios ojos.
2.3.b Concordancia de palabras clave
Para cada página, cuente cuántas palabras clave analizadas aparecen en ella (coincidencia de subcadena que no distingue entre mayúsculas y minúsculas). Descartar páginas con cero coincidencias; mantén el top-k por conteo de partidos. La siguiente tabla de resultados incluye las palabras clave coincidentes reales por página, por lo que cualquiera puede leerla y ver por qué se seleccionó una página.
retrieve_pages(page_df, line_df, parsed_question.keywords, top_k=3) devuelve las tres páginas principales por recuento de palabras clave más las líneas filtradas: 314 líneas guardadas de las páginas 6, 9, 7.
Tres páginas, clasificadas por recuento de partidos, con los partidos reales dispuestos. Las páginas 6, 8 y 9 contienen cada una la codificación posicional de frase literal; La página 6 contiene la Sección 3.5 Codificación posicional con la respuesta real. Cualquiera que lea la tabla puede verificar el resultado manualmente: busque en la fuente la codificación posicional y encontrará estas tres páginas.
Dos opciones de diseño:
Descarta páginas con cero coincidencias. Una recuperación que dice "nada coincide" es más útil que una que completa con tres páginas aleatorias. La ruta nula del esquema (siguiente subsección) maneja el caso vacío limpiamente. No rompemos lazos. Cuando las páginas empatan en el mismo recuento de coincidencias, el orden es el mayor retorno de los pandas. El LLM posterior ve las líneas de todas las páginas vinculadas en el orden del documento y decide.
De 1048 líneas a 300, y sabemos que ahí se encuentra el material correcto.
def cosine_sim_matrix(query_vec, doc_matrix): q = query_vec / (np.linalg.norm(query_vec) + 1e-12) d = doc_matrix / np.linalg.norm(doc_matrix, axis=1, keepdims=True) return d @ q def retrieve_pages(page_df, line_df, question, top_k=3): q_vec = np.asarray(get_embedding(question), dtype=np.float32) doc_matrix = np.vstack(page_df["embedding"].values) sims = cosine_sim_matrix(q_vec, doc_matrix) puntuado = page_df.copy() puntuado["similitud"] = sims retrieved_pages_df = anotó.nlargest(top_k, "similitud") keep_pages = retrieved_pages_df["page_num"].tolist() filtered_line_df = line_df[line_df["page_num"].isin(kept_pages)] return retrieved_pages_df, filtered_line_df
Nota: la trampa de "dividir en palabras individuales". Un reflejo natural cuando las frases de varias palabras no coinciden: dividirlas y buscar las fichas individuales. A continuación, ampliamos cada palabra clave en sus palabras, deduplicamos y luego volvemos a ejecutar la recuperación. Obtenemos coincidencias y también falsos positivos, porque palabras como codificación, transformador, red aparecen en todo el documento en contextos no relacionados.
Ahora cada página entre las tres primeras coincide con varios tokens, pero observe qué tokens. Palabras como codificación y transformador cubren la mayor parte del artículo. Las páginas sobre codificación de capas o pilas de codificadores parecen tan relevantes como la página que realmente responde a la pregunta. La división intercambia un error (cero coincidencias) por otro (falsos positivos). El artículo 7 cubre las soluciones reales (ampliación de sinónimos a través de un diccionario, puntuación híbrida); Por ahora, mantén la frase completa.
2.3.c Una pregunta más difícil: dónde se rompe cada perro perdiguero
Mismo canal, una pregunta diferente. Preguntamos sobre el valor de épsilon utilizado en el suavizado de etiquetas. La respuesta está en la página 8 del artículo, escrita como ε_ls = 0,1 (letra griega ε, nunca la palabra inglesa épsilon). Mira lo que hace cada perro perdiguero.
question_2 = "¿Cuál es el valor de épsilon utilizado en el suavizado de etiquetas?" parsed_question_2 = get_keywords_from_question(question_2, cliente=cliente) print("Palabras clave:", parsed_question_2.keywords) Palabras clave: ['épsilon', 'suavizado de etiquetas']
Dos fallos de diferentes formas:
Las incrustaciones clasifican las páginas por proximidad temática. La página de la derecha (página 8, donde vive ε_ls = 0,1) puede estar o no entre las tres primeras. Las páginas con mucha notación matemática aparecen incluso cuando no están relacionadas. Las palabras clave son ciegas a los símbolos. El LLM emite épsilon, suavizado de etiquetas, etc. El documento escribe la letra griega ε. La coincidencia de subcadena devuelve cero en cualquier cosa que mencione épsilon solo por símbolo. La página que contiene la respuesta es invisible para el recuperador de palabras clave.
La sección 4.4 recoge esto como puente hacia el Artículo 2 (las incrustaciones manejan sinónimos y variaciones de superficie) y el Artículo 6 (el análisis de preguntas más rico incluye alternativas como la letra griega).
2.4 Generación
Enviamos las líneas recuperadas al LLM con la pregunta, formateadas como un bloque separado por tabulaciones donde num_página y num_línea se encuentran al lado de cada línea. Ese formato le da al LLM las coordenadas exactas que necesita citar.
En: la pregunta original, line_df y los números de página recuperados de la sección 2.3. Salida: un AnswerWithEvidence, un JSON estructurado con la respuesta, el intervalo de evidencia (start_page_num, start_line_num, end_page_num, end_line_num), una confianza, una justificación, las citas exactas y cualquier advertencia.
clase AnswerWithEvidence(BaseModel): respuesta: str = Campo(…) start_page_num: int | Ninguno start_line_num: int | Ninguno end_page_num: int | Ninguno end_line_num: int | Ninguna confianza: float = Field(…, ge=0.0, le=1.0) justificación: str = Field(…) comillas: list[str] = Field(default_factory=list) advertencias: list[str] = Field(default_factory=list)
Vale la pena guardar el JSON sin formato en producción: justificación, citas, advertencias y confianza en toda la evaluación, auditoría y reproducción del feed, mucho más allá del campo de respuesta que muestra una interfaz de usuario de chat.
Serializamos las líneas filtradas en un TSV con encabezado page_numtline_numttext, una fila por línea. El LLM ve las coordenadas exactas al lado de cada fragmento de texto para poder citarlo por (núm_página, núm_línea) en su respuesta.
Esto es lo que hace que la respuesta sea fundamentada: el esquema obliga al modelo a completar (página_inicio, línea_inicio, página_final, línea_final), una cita textual y advertencias si hay algo incierto. Sin prosa, sólo un objeto mecanografiado con citas.
Llamamos a respuesta = llm_answer_with_evidence(question, filtered_line_df, client=client) y obtenemos una instancia de AnswerWithEvidence, representada a continuación como una imagen JSON con estilo para que las etiquetas de los campos permanezcan legibles.
def llm_answer_with_evidence(question, filtered_text_prompt): resp = client.responses.parse( model=model_chat, input=[ { "role": "system", "content": ( "Responda usando SÓLO las líneas proporcionadas. " "Devolver solo JSON." ), }, { "role": "user", "content": ( f"Líneas:n{filtered_text_prompt}nn" f"Pregunta:n{question}nn" "Elija un intervalo de evidencia contiguo." ), }, ], text_format=AnswerWithEvidence, store=False, ) return resp.output_text
Llamamos a respuesta = llm_answer_with_evidence(question, filtered_line_df, client=client) y obtenemos una instancia de AnswerWithEvidence.
{ "answer": "Las opciones para la codificación posicional mencionadas son incrustaciones posicionales aprendidas y codificaciones posicionales fijas (específicamente, usando funciones seno y coseno de diferentes frecuencias).", "start_page_num": 6, "start_line_num": 31, "end_page_num": 6, "end_line_num": 32, "confidence": 0.98, "justification": "Las líneas 31–32 indican explícitamente: 'Hay muchas opciones de codificaciones posicionales, aprendidas y fijas.[9].' Además, líneas adicionales detallan la codificación sinusoidal como la opción fija, y la fila (E) de la Tabla 3 analiza el uso de incrustaciones aprendidas en su lugar.", "quotes": [ "Hay muchas opciones de codificaciones posicionales, aprendidas y fijas[9]." ], "caveats": [ "Más detalles sobre la implementación específica de las incrustaciones aprendidas solo se mencionan en otra parte, pero ambas opciones se mencionan aquí." ], "complete_answer_found": true, "context_structured": true, "llm_discovered_keywords": [ "incrustaciones posicionales aprendidas", "codificaciones posicionales fijas", "codificación posicional sinusoidal" ] }
Sucedieron tres cosas que importan:
La respuesta es correcta. Ambas opciones identificadas, parafraseadas correctamente. La evidencia (página 6, líneas 26-44) apunta a una región específica. No "en algún lugar de la página 6". Líneas exactas. El modelo no podría haber alucinado una cita: solo vio líneas de las páginas recuperadas, y el esquema forzó un rango real (página, línea) que podemos verificar.
Si el modelo no puede completar el esquema, se permiten campos nulos y registros de advertencias por qué. El artículo 8 desarrolla el esquema en una forma mucho más rica con campos de retroalimentación por bloque; El artículo 23 construye la arquitectura de almacenamiento a su alrededor.
Control de cordura. En un artículo tan corto, también podemos enviar el line_df completo al LLM sin recuperarlo y verificar las coincidencias de las respuestas. Tranquilo aquí, no se ampliará a documentos grandes.
{ "answer": "Las opciones mencionadas para la codificación posicional son codificaciones posicionales sinusoidales (usando funciones seno y coseno de diferentes frecuencias) e incrustaciones posicionales aprendidas.", "start_page_num": 6, "start_line_num": 27, "end_page_num": 6, "end_line_num": 41, "confidence": 0.99, "justification": "Líneas 6:27-6:41 describe la adición de 'codificaciones posicionales' a las incrustaciones de entrada, especifica el método sinusoidal y menciona la experimentación con incrustaciones posicionales aprendidas, afirmando que ambas opciones se probaron y produjeron resultados casi idénticos.", "quotes": [ "Dado que nuestro modelo no contiene recurrencia ni convolución, para que el modelo haga uso del orden de la secuencia, debemos inyectar alguna información sobre la posición relativa o absoluta de los tokens en la secuencia. Para este fin, agregamos 'codificaciones posicionales' a. las incrustaciones de entrada en la parte inferior de las pilas de codificadores y decodificadores tienen la misma dimensión dmodel que las incrustaciones, por lo que las dos se pueden sumar. Hay muchas opciones de codificaciones posicionales, aprendidas y fijas.[9]. En este trabajo, utilizamos funciones seno y coseno de diferentes frecuencias: … También experimentamos con el uso de incrustaciones posicionales aprendidas.[9]en su lugar, y descubrió que las dos versiones produjeron resultados casi idénticos (consulte la fila (E) de la Tabla 3). Elegimos la versión sinusoidal porque puede permitir que el modelo extrapola longitudes de secuencia más largas que las encontradas durante el entrenamiento." ], "caveats": [ "Las fórmulas matemáticas exactas para la codificación sinusoidal están presentes aquí, pero no todos los detalles de las incrustaciones aprendidas. La fila (E) de la Tabla 3 y más detalles pueden ampliar los resultados, pero no son necesarios para la pregunta de opciones." ], "complete_answer_found": true, "context_structured": true, "llm_discovered_keywords": [ "codificación posicional sinusoidal", "incrustaciones posicionales aprendidas", "funciones seno y coseno", "posición relativa o absoluta" ] }
2.5 Anotación de PDF en el PDF de origen
Ahora la parte satisfactoria. Usamos el intervalo de evidencia para dibujar rectángulos directamente en el PDF de origen.
En: el PDF fuente y la evidencia de AnswerWithEvidence. Salida: un PDF anotado con rectángulos dibujados alrededor de las líneas citadas. Opcional. Una herramienta CLI, un trabajo por lotes o una API pueden omitirlo; la respuesta con citas ya está completa después de la sección 2.4.
Tres llamadas hacen el trabajo:
pasaje_lines_df_from_answer(line_df, respuesta) reconstruye el DataFrame de la línea citada a partir del intervalo de evidencia. pasaje_bbox_by_page(passage_df) agrupa cuadros delimitadores por página. draw_passage_rectangles(pdf_path, bboxes_df, out_pdf_path) escribe el PDF anotado.
def pasaje_lines_df_from_answer(line_df, respuesta_json): a = json.loads(answer_json) sp, sl = a["start_page_num"], a["start_line_num"] ep, el = a["end_page_num"], a["end_line_num"] si sp es Ninguno: devuelve line_df.iloc[0:0] máscara = ( line_df["page_num"].between(sp, ep) & ((line_df["page_num"] != sp) | (line_df["line_num"] >= sl)) & ((line_df["page_num"] != ep) | (line_df["line_num"] <= el)) ) return line_df.loc[mask].copy() def pasaje_bbox_by_page(passage_df): devuelve pasaje_df.groupby("page_num", as_index=False).agg( x0=("x0", "min"), y0=("y0", "min"), x1=("x1", "max"), y1=("y1", "max")) def draw_passage_rectangles(pdf_path, bboxes_df, out_path): doc = fitz.open(pdf_path) para _, r en bboxes_df.iterrows(): página = doc[int(r["page_num"]) – 1] page.add_rect_annot(fitz.Rect(r["x0"], r["y0"], r["x1"], r["y1"])) doc.save(out_path)
Realmente es del pasaje de donde viene la respuesta. El cuadro rojo envuelve el párrafo de Codificación posicional: la oración que introduce la elección (“usamos funciones seno y coseno de diferentes frecuencias”) y la fórmula de dos líneas directamente debajo. El lector puede pasar de la respuesta del chat a la cita y al párrafo fuente sin salir de la misma pantalla. Ese es el punto.
¿Por qué un cuadro alrededor de todo el párrafo y no de las palabras exactas? Debido a que trabajamos en la granularidad de la línea: line_df lleva un cuadro delimitador por línea de texto, el LLM cita un tramo (línea_inicial, línea_final) y pasaje_bbox_by_page colapsa cada línea en ese tramo en un rectángulo envolvente. Si desea dibujar el cuadro alrededor de las palabras exactas sin(pos / 10000^(2i/d_model)) en lugar de todo el párrafo, el enfoque es el mismo. Simplemente cambie la granularidad. Reemplace line_df con un word_df a nivel de palabra (page.get_text("words") de PyMuPDF le proporciona un cuadro delimitador por palabra), haga que el esquema cite (start_word, end_word) y pasaje_bbox_by_page ya hace lo correcto. El mismo oleoducto de cuatro ladrillos, un alcance más fino.
3. Encadenar los ladrillos y probar la tubería
3.1 Todo el proceso como una sola función
Los ladrillos se encadenan en una única llamada. Introduzca un PDF y una pregunta; obtenga una respuesta escrita con citas de líneas y, opcionalmente, un PDF anotado.
En: una ruta PDF y una pregunta de texto (más un top_k opcional y una ruta PDF de salida opcional). Salida: un AnswerWithEvidence y (si se proporciona annotate_pdf) un PDF anotado en el disco.
En el interior, pdf_qa_baseline encadena el análisis de documentos → análisis de preguntas → recuperación → generación → anotación de PDF. Lo que cruza el límite de recuperación → generación son solo los números de página; el line_df filtrado se reconstruye dentro de la generación.
def pdf_qa_baseline( pdf_path: str, question: str, top_k: int = 3, annotate_pdf: str | Ninguno = Ninguno,): # 1. Análisis de line_df = fitz_pdf_to_line_df(pdf_path) # 2. Recuperación de page_df = embed_page_df(build_page_df(line_df)) _, filtrado = retrieve_pages(page_df, line_df, question, top_k) # 3. Generación de respuesta = llm_answer_with_evidence(question, filtered) # 4. Resaltado opcional en el PDF de origen si annotate_pdf no es Ninguno: pasaje = pasaje_lines_df_from_answer(line_df, respuesta) bboxes = pasaje_bbox_by_page(paso) draw_passage_rectangles(pdf_path, bboxes, annotate_pdf) return respuesta { "answer": "Las opciones mencionadas para la codificación posicional son codificaciones posicionales fijas y aprendidas, específicamente codificaciones posicionales sinusoidales (que utilizan funciones seno y coseno de diferentes frecuencias) e incrustaciones posicionales aprendidas.", "start_page_num": 6, "start_line_num": 31, "end_page_num": 6, "end_line_num": 41, "confidence": 0.99, "justification": "Las líneas 31 a 41 analizan las opciones para codificaciones posicionales, afirmando que hay muchas opciones, incluidas codificaciones aprendidas y fijas. Luego explica el uso de funciones seno y coseno (codificación sinusoidal) y señala que también se experimentaron con incorporaciones posicionales aprendidas.", "quotes": [ "Hay muchas opciones de codificaciones posicionales, aprendidas y fijas[9].", "En este trabajo, utilizamos funciones seno y coseno de diferentes frecuencias: …", "También experimentamos con el uso de incorporaciones posicionales aprendidas[9]en su lugar, y descubrió que las dos versiones produjeron resultados casi idénticos (consulte la fila (E) de la Tabla 3)". ], "advertencias":[], "complete_answer_found": true, "context_structured": true, "llm_discovered_keywords": [ "codificaciones posicionales", "aprendidas", "fijas", "sinusoidales", "funciones seno y coseno", "incrustaciones posicionales aprendidas" ] }
Esta es la API del artículo. Artículos posteriores crean una función hermana preguntar_corpus (pregunta, corpus, …) para el trabajo a escala de archivo: mismo contrato (respuesta escrita con citas), alcance diferente (primero filtrar el corpus, luego ejecutar el trabajo a nivel de documento en los documentos coincidentes).
3.2 Pruébelo en un documento diferente
Introduzca cualquier PDF que tenga a mano: un trabajo de su propio campo, un contrato, un informe de trabajo. Aquí elegimos la Perspectiva de los mercados de productos básicos de abril de 2026 del Banco Mundial (publicación del Banco Mundial, número de abril de 2026; CC BY 3.0 IGO, como se declara en la página de publicación del Repositorio Abierto de Conocimientos del Banco Mundial para este número): un informe de 69 páginas sobre los mercados de energía, agricultura y fertilizantes, lejos de ser un trabajo de investigación en tono y estructura.
Los mismos cuatro ladrillos, las mismas indicaciones predeterminadas, las mismas páginas de recuperación, el mismo esquema. Nada sobre los cambios en la canalización para un nuevo documento.
Comenzamos con una pregunta cuya respuesta se encuentra en lo más profundo del informe, en el capítulo de metales y no en el Resumen ejecutivo: las perspectivas de los precios del aluminio en 2026.
Llamamos a pdf_qa_baseline de extremo a extremo: pasamos el PDF de CMO, la pregunta de aluminio, top_k=3 y una ruta annotate_pdf para que la canalización también escriba la fuente resaltada. El respuesta_cmo_al devuelto es la misma forma de AnswerWithEvidence que vimos en el documento de Atención.
{ "answer": "Se proyecta que los precios del aluminio aumentarán alrededor de un 22 por ciento en 2026 (a/a) para alcanzar un máximo histórico, alrededor de un 21 por ciento más que sus proyecciones de enero de 2026, respaldados por condiciones de oferta ajustadas y un sólido crecimiento de la demanda. Se espera que los precios disminuyan alrededor de un 6 por ciento en 2027 a medida que las condiciones de oferta mejoren gradualmente.", "start_page_num": 45, "start_line_num": 32, "end_page_num": 45, "end_line_num": 43, "confidence": 0.98, "justification": "El intervalo seleccionado proporciona explícitamente el aumento porcentual proyectado para los precios del aluminio en 2026, el contexto de estos movimientos y las perspectivas para 2027. También menciona el pronóstico de nivel récord y los factores que impulsan el precio.", "quotes": [ "Se proyecta que los precios del aluminio aumentarán aproximadamente un 22 por ciento en 2026 (a/a) alcanzará un máximo histórico, alrededor de un 21 por ciento más que sus proyecciones de enero de 2026, respaldado por condiciones de oferta ajustadas y un crecimiento sólido de la demanda (tabla 1).", "Se espera que los precios disminuyan alrededor de un 6 por ciento en 2027 a medida que las condiciones de oferta se relajen gradualmente". ], "advertencias":[], "complete_answer_found": true, "context_structured": true, "llm_discovered_keywords": [ "máximo histórico", "condiciones de oferta ajustadas", "crecimiento sólido de la demanda" ] }
La vista compuesta coloca la página fuente resaltada junto a la pregunta y la respuesta, para que la cita se pueda verificar de un vistazo:
Una pregunta más difícil en el mismo informe. ¿Qué pasa si preguntamos sobre algo que el informe menciona sólo de pasada? Probamos la pregunta sobre la demanda de electricidad relacionada con la IA, cuya respuesta el Banco Mundial desarrolló sólo en una barra lateral sobre “Riesgo al alza” en la página 31.
Misma forma de llamada, pregunta más difícil: pdf_qa_baseline(pdf_path=pdf_path_cmo, question=question_cmo_ai, top_k=3, …). El proceso debe decidir si las páginas recuperadas realmente contienen la cifra de electricidad de IA o si marcan la respuesta como no encontrada.
{ "answer": "Las líneas proporcionadas mencionan que una expansión más rápida de lo previsto de los centros de datos relacionados con la IA podría impulsar la demanda de ciertos metales como el aluminio y el cobre, pero no cuantifican la contribución de los centros de datos relacionados con la IA al crecimiento de la demanda mundial de electricidad.", "start_page_num": 47, "start_line_num": 39, "end_page_num": 47, "end_line_num": 40, "confidence": 0.8, "justification": "La única mención de los centros de datos relacionados con la IA es en relación con la demanda de metales, no con la demanda de electricidad. No hay ninguna estimación cuantitativa ni porcentaje de su impacto en el crecimiento de la demanda mundial de electricidad.", "quotes": [ "Además, una expansión más rápida de lo previsto de los centros de datos relacionados con la IA podría naumentar la demanda de aluminio y cobre, elevando los precios n." ], "advertencias": [ "En las líneas proporcionadas no se encontraron cifras específicas ni declaraciones directas sobre el crecimiento de la demanda mundial de electricidad causado por los centros de datos relacionados con la IA". ], "complete_answer_found": false, "context_structured": true, "llm_discovered_keywords": [ "Centros de datos relacionados con la IA", "crecimiento de la demanda de electricidad", "impulsar la demanda de aluminio y cobre" ] }
Pero ¿cómo podemos estar seguros de que la respuesta realmente no existe en el documento? Estrictamente, no podemos, al menos no sólo desde este camino nulo. Lo que dice el esquema es "el LLM no encontró la respuesta en las líneas que se mostraron", que es una afirmación diferente de "la respuesta no está en el documento". La barra lateral de riesgo al alza en la página 31 del mismo informe de la CMO cuantifica la cifra (el Banco Mundial cita la proyección del 8% de la AIE sobre el crecimiento de la demanda mundial de electricidad de 2024 a 2030). La canalización de palabras clave predeterminada se sacó de la página 47 y de las páginas cercanas, donde la prosa del informe analiza el efecto de la IA en la demanda de metales. Demostrar la ausencia requeriría ejecutar el LLM en cada página o un método de recuperación que muestre el texto de la barra lateral y las breves menciones de referencia. Eso es exactamente lo que desarrolla el Artículo 7 (Recuperación); para la versión mínima, lo que informamos es "No lo encontré en las tres páginas principales".
3.3 Más preguntas en una tabla
Un pequeño lote de cuatro preguntas sobre los mismos dos documentos, todos los resultados en una tabla. Lea la tabla para ver los patrones, no para cada celda.
Valor numérico: tasa de aprendizaje del Transformador base. Número específico, esperado en la página 7 (sección 5.3 sobre el optimizador Adam). Sin respuesta en documento: composición química del agua de mar. La ruta nula del esquema debería activarse; Ambos recuperadores extraerán páginas de aspecto aleatorio. Tema diferente en CMO: perspectivas para los precios de la urea. El mismo proceso en la sección de fertilizantes del informe del Banco Mundial, lejos de la barra lateral de IA. Pregunta compuesta: d_k y d_v en el Transformer. Se piden dos valores a la vez. También prueba el límite de análisis de la tabla (los valores se encuentran en la Tabla 1, página 4, analizados como líneas planas). def run_pipeline_test( pregunta: str, line_df_in: pd.DataFrame, page_df_in: pd.DataFrame, page_df_emb_in: pd.DataFrame, top_k: int = 3, client=client, ) -> dict: """Ejecute ambos recuperadores + generación en una pregunta; devuelva un dictado resumido.""" parsed_q = get_keywords_from_question(pregunta, cliente=cliente) retrieved_emb_df, _ = retrieve_pages_by_similarity( page_df_emb_in, line_df_in, pregunta, top_k=top_k, cliente=cliente, ) retrieved_kw_df, filtered_lines_kw = retrieve_pages( page_df_in, line_df_in, parsed_q.palabras clave, top_k=top_k, ) # Si la recuperación de palabras clave no encuentra nada, recurra al documento completo para que la generación # aún se ejecute (solo archivos PDF pequeños: no escalaría a un corpus real). líneas_para_generación = (líneas_filtradas_kw si len(líneas_filtradas_kw) > 0 más línea_df_in ) respuesta = llm_answer_with_evidence( pregunta, líneas_para_generación, cliente=cliente, ) return { "pregunta": pregunta, "palabras clave": parsed_q.keywords, "emb_top3": recuperado_emb_df["page_num"].tolist(), "kw_top3": ( retrieved_kw_df["page_num"].tolist() if len(retrieved_kw_df) > 0 else "(no hay coincidencia de kw)" ), "answer_excerpt": (answer.answer[:80] + ("…" if len(answer.answer) > 80 else "")), "cite_page": respuesta.start_page_num, }
Lea la tabla de izquierda a derecha por fila. Cuatro patrones para llevar:
Las palabras clave superan a las incrustaciones en la fila de tasa de aprendizaje. El programa de entrenamiento básico del Transformer se encuentra en la página 7 (sección 5.3, Optimizador). Las incrustaciones clasifican las páginas 8/9/10; La página 7 no está entre las tres primeras. El recuperador de palabras clave encuentra la página 7 inmediatamente mediante la tasa de aprendizaje de frases literales. La misma lección que la fila épsilon de la sección 2.3.c: cuando la pregunta depende de un término preciso que el documento imprime palabra por palabra, las palabras clave son la mejor herramienta. Ambos recuperadores fallan en la fila de agua de mar y el fallo es visible. El PDF no tiene nada que decir sobre el agua de mar. La columna de palabras clave muestra (sin coincidencia de kw) directamente, sin '3 páginas principales' falsas que parezcan plausibles. Luego, el esquema devuelve una respuesta nula con una advertencia. Un "No sé" claro es el comportamiento más valioso del sistema en preguntas fuera de alcance. Ambos perros perdigueros trabajan en la fila de urea. La OCM tiene una sección de fertilizantes; Tanto las incrustaciones como las palabras clave traen de vuelta la página 42, la generación la cita correctamente. Las canalizaciones entre dominios funcionan siempre que el vocabulario de la pregunta llegue al documento. La fila compuesta d_k y d_v expone el límite de análisis de la tabla. Los dos valores se encuentran en la Tabla 1, página 4 del artículo de Transformer, donde cada fila enumera d_model, h, d_k, d_v, etc. Nuestro analizador aplanó la tabla en líneas simples, por lo que un modelo que solicita dos celdas una al lado de la otra tiene que volver a ensamblar la fila solo a partir del texto. Las palabras clave recuperan la página 4 (la frase literal d_k aparece allí), pero la cita a menudo apunta a un valor mientras que el otro está parafraseado. La solución es estructural: analizar las tablas como tablas, no como líneas. Ése es el Artículo 5 (análisis) y el Artículo 6 (descomposición de preguntas compuestas) haciendo su trabajo.
4. Las preguntas que plantea cada bloque
Qué hace bien este sistema mínimo:
Una respuesta real y verificable. Un objeto estructurado con la respuesta, la página, las líneas, la cita. El usuario puede comprobar la cita en segundos. "No encontrado" manejado limpiamente. Cuando la respuesta no está en las líneas recuperadas, el esquema permite campos nulos y el campo de advertencias indica el motivo. Sin fabricación. La respuesta vinculada a la fuente. El PDF resaltado cierra el círculo entre el reclamo del LLM y el documento. Esto es lo que separa un sistema RAG útil de un chatbot que lee documentos. Fácil de seguir. Cada función hace una cosa. Sin estado oculto, sin marco mágico. Cuando algo sale mal, la depuración es leer el código.
Ahora mire el mismo sistema nuevamente. Cada bloque esconde suposiciones que vale la pena cuestionar.
4.1 Análisis de documentos: solo leemos líneas
Extrajimos el texto línea por línea. Eso es razonable para un artículo académico, pero mire lo que descartamos: estructura de secciones, títulos, diseños de tablas, figuras, notas a pie de página, referencias cruzadas. La página 4 de este documento contiene la Tabla 1 con las complejidades por capa. Analizamos cada una de sus filas como líneas simples, perdiendo por completo la estructura de la tabla. La página 9 contiene la Tabla 3, el estudio de ablación. El mismo problema.
Para una pregunta como "¿Cuáles son las opciones para la codificación posicional?" esto no importa. La respuesta está en prosa continua. Para una pregunta como "¿Cuál es la complejidad de la autoatención por capa?" de repente lo hace, porque la respuesta se encuentra en una celda de la tabla que nuestro analizador redujo a ruido.
Ese es el tema del Artículo 5: Análisis. Los documentos tienen estructura. Ignorarlo es la mayor fuente de fallas posteriores.
4.2 Análisis de preguntas: solicitamos palabras clave, pero solo palabras clave
Nuestro paso de análisis de preguntas extrae una lista plana de palabras clave. Eso funciona en una pregunta limpia frente a un artículo académico. Empieza a desmoronarse tan pronto como las preguntas se vuelven más difíciles.
Tres cosas que esta versión mínima no hace.
No detecta intención. “Resumir el capítulo 3”, “Traducir esta cláusula al francés”, “Comparar X e Y” requieren cada uno de ellos un gasoducto descendente diferente. Un solo campo de palabras clave no puede transmitir esa señal.
No descompone preguntas compuestas. “¿Cuáles son las exclusiones y el deducible?” analizado como una lista plana de palabras clave contamina la recuperación (las palabras clave para "exclusiones" y "deducible" se encuentran en dos ámbitos diferentes que interfieren). El artículo 6 explica cómo detectar preguntas compuestas, decidir si descomponerlas y enrutar las subpreguntas de forma independiente.
No detecta una forma de respuesta esperada. "¿Cuál es el monto de la prima?" quiere un número con una moneda. “¿Cuáles son las obligaciones?” quiere una lista. “Comparar las dos políticas” quiere una tabla. La versión mínima trata cada respuesta como texto libre. El artículo 6 introduce el campo expect_answer_shape que impulsa la plantilla de generación en sentido descendente.
Ese es el tema del Artículo 6: Análisis de preguntas. El mismo ladrillo, JSON mucho más rico.
4.3 Fragmentación: agregamos por página
Elegimos páginas como unidad de recuperación. ¿Por qué páginas? ¿Por qué no párrafos, secciones o fragmentos de tamaño fijo de 512 tokens como recomienda cada tutorial estándar de RAG?
La respuesta es que la agregación a nivel de página funciona para este artículo porque las páginas se alinean aproximadamente con las unidades semánticas. En un contrato, en un texto legal, en un manual técnico con cláusulas numeradas, las páginas son cortes arbitrarios y, en su lugar, preferirías fragmentos a nivel de cláusula o de sección. La fragmentación "correcta" depende del documento y la pregunta, no de un valor predeterminado.
La tentación, cuando un enfoque de tamaño fijo comienza a fallar, es buscar en cuadrículas los tamaños de fragmentos y las superposiciones. Ese es el reflejo del aprendizaje automático. Es el marco equivocado para lo que en realidad es una decisión estructural. Artículo 3: RAG no es aprendizaje automático, y el error de seis meses de tratarlo como tal lo demuestra en su totalidad.
4.4 Recuperación: la coincidencia de palabras clave es transparente, pero ciega al vocabulario
Nuestra recuperación simplemente funcionó. La página 6 regresó con la palabra clave coincidente, por delante del resto, y la sección Codificación posicional está en la página 6. Cualquiera puede mirar la tabla de coincidencias y ver por qué. Ese es el trato que hicimos: la recuperación más sencilla posible, completamente auditable.
El comercio tiene un costo. La concordancia de palabras clave es ciega cuando el vocabulario de la pregunta no coincide con el del documento. Tres modos de falla aparecen inmediatamente en el mismo papel.
Símbolo versus palabra. Pregunte "¿Cuál es el valor de épsilon utilizado en el suavizado de etiquetas?" Las palabras clave del análisis de preguntas probablemente sean algo así como ["épsilon", "suavizado de etiquetas"]. La respuesta real (ε_ls = 0,1) se encuentra en la página 8, pero el documento la escribe como la letra griega ε, nunca como la palabra inglesa “épsilon”. La verificación de subcadena devuelve cero en la página de sólo símbolos; sólo la etiqueta de frase literal suavizado aparece en la página 8.
Falta de coincidencia de sinónimos. Pregunte “¿Cómo sabe el modelo el orden de las palabras en una oración?” Las palabras clave pueden ser ["orden de palabras", "orden de oraciones"]. El documento llama a esto codificación posicional. Ninguna de las palabras clave de la pregunta aparece en la página 6. El recuperador elige páginas que mencionan "orden" u "oración" de pasada, y ninguna de ellas contiene la respuesta.
Paráfrasis. Pregunte "¿Qué mecanismo de atención utiliza el codificador?" El documento dice autoatención y atención de múltiples cabezales, nunca la frase “mecanismo de atención que utiliza el codificador”. Las palabras clave extraídas de la pregunta, incluso después de la expansión, pueden incluir o no la redacción exacta del documento. Cuando lo hacen, la recuperación funciona. Cuando no lo hacen, se degrada silenciosamente.
Los dos primeros fracasos son tan comunes que el resto de la serie dedica dos artículos a ellos.
Artículo 6: El análisis de preguntas convierte la extracción de palabras clave en un paso mucho más rico que se basa en un glosario de dominio, amplía los sinónimos e incluye probables frases del documento en lugar de las palabras literales de la pregunta. Artículo 2: Incrustaciones presenta representaciones vectoriales que coinciden en todo el vocabulario superficial: dónde brillan las incrustaciones (sinónimos, paráfrasis, errores ortográficos, concordancia entre idiomas), dónde fallan silenciosamente (negación, valores exactos, acrónimos internos, términos polisémicos) y cómo combinarlos con la concordancia de palabras clave para obtener lo mejor de ambos mundos. Los artículos 7 y 9 sitúan la recuperación híbrida resultante en un índice de documentos real.
La respuesta correcta es combinar, no elegir un ganador. Los dos métodos fallan en casos casi opuestos: las incrustaciones fallan cuando la pregunta depende de un símbolo preciso, un término con nombre o un valor exacto; Las palabras clave tropiezan cuando el vocabulario del autor de la pregunta no aparece literalmente en el documento. Ejecutar ambos recuperadores, tomar la unión de sus candidatos y (opcionalmente) reclasificar con un codificador cruzado es la receta híbrida estándar. El artículo 2 lo desarrolla; Los artículos 7 y 9 lo integran en un corpus.
La versión mínima sigue siendo de un solo recuperador porque primero enseña el reflejo correcto: el recuperador debe ser auditable. La concordancia de palabras clave hace que ese reflejo sea concreto (puedes ver exactamente qué palabras llegaron a cada página). Una vez que ese reflejo está en su lugar, las incrustaciones se convierten en una adición controlada en lugar de un valor predeterminado opaco, y la combinación de los dos se convierte en una elección de ingeniería deliberada en lugar de una tendencia.
Generación 4.5: pedimos fuentes y las conseguimos
Éste es el bloque que funcionó mejor, casi con demasiada facilidad. Definimos un esquema Pydantic con start_page_num, start_line_num, end_page_num, end_line_num, confianza, justificación, comillas y advertencias, y el modelo lo completó correctamente.
¿Cuánto más podemos pedir? Una comparación estructurada para preguntas comparativas, una lista de conflictos si el documento se contradice, múltiples citas de múltiples partes del documento, un desglose de confianza por afirmación. Sí a todo lo anterior. El paso generacional es mucho más controlable de lo que la mayoría de los equipos creen. El artículo 8: Generación como ejecución controlada explora esto en profundidad.
5. La forma de lo que viene después
Esta tubería mínima es la columna vertebral de todo lo que sigue. Cada parte de la serie profundiza en una de las preguntas planteadas anteriormente.
Los errores que acaban con la mayoría de los proyectos provienen de tener una imagen equivocada de uno de estos bloques: RAG no es ML (Artículo 3), las incrustaciones no son mágicas (Artículo 2), no todos los problemas de RAG tienen el mismo aspecto (Artículo 4). Esa es la Parte I.
Luego, cada bloque obtiene su propia inmersión profunda: análisis de documentos, análisis de preguntas, recuperación, generación. Esa es la Parte II, los cuatro ladrillos.
Una vez que los bloques están sólidos, los recombinamos para casos que parecen producción: documentos largos, justificación y manejo de ausencias, recuperación basada en tablas de contenido, listas de preguntas, extracción estructurada, canal compuesto. Esa es la Parte III.
Luego cambiamos de escala. De un documento a muchos. Desde un único documento hasta un archivo de cientos o miles de documentos. La arquitectura cambia sustancialmente. Esa es la Parte IV.
Finalmente, lo que se necesita para operar el sistema en producción: evaluación, costo y monitoreo, seguridad y cumplimiento, la arquitectura del código base en sí. Esa es la Parte V.
Los bloques no cambian. Sus partes internas sí.
Algunas notas de encuadre:
Los cuatro ladrillos (Parte II) son el núcleo conceptual. La mayor parte del resto de la serie trata sobre hacer cada uno mejor. La Parte III y la Parte IV son recombinaciones: las mismas cuatro ideas en diferentes escalas y para diferentes tipos de preguntas. El alcance de la serie son los documentos empresariales. Contratos, especificaciones técnicas, presentaciones regulatorias, procedimientos internos: todos tienen estructura (TOC, secciones, tablas) y vocabulario limitado (jerga de la industria, términos de expertos). RAG trabaja en esos corpus debido a esa estructura, no a trucos heroicos de incrustación. Los documentos sin estructura (novelas, transcripciones largas y no estructuradas) y las preguntas que requieren intención en lugar de localizar un pasaje están fuera de alcance; El artículo 4 vuelve a donde cae la línea. El código es ilustrativo, no está listo para producción. Lo que ha leído funciona en un PDF real, pero carece del manejo de errores, validación, almacenamiento en caché, controles de costos, monitoreo y seguridad que necesita un sistema de producción. Cada uno recibe su propio artículo.
Aquí está el mapa explícito de este sistema mínimo al resto de la serie:
El análisis de PDF descarta la estructura → Artículo 5, Artículo 10 El análisis de preguntas necesita más que palabras clave (intención, descomposición, forma de respuesta esperada) → Artículo 6 La estrategia de fragmentación no es un hiperparámetro → Artículo 3 El vocabulario de las preguntas no coincide con los términos del documento → Artículo 2, Artículo 6 La recuperación elige la página equivocada → Artículo 7, Artículo 9 El modelo parafrasea su cita → Artículo 8, Artículo 21 “No encontrado” necesita matices → Artículo 4 Preguntas compuestas, de listado, de comparación y de resumen → Artículo 6, Artículos 11-13 Corpus multidocumental → Parte IV (Artículos 15-20) Producción, evaluación, seguridad, arquitectura → Parte V (Artículos 21-25)
Puedes leer esto
6. Conclusión
Cien líneas de Python y un esquema Pydantic son suficientes para enviar un sistema RAG funcional en un PDF real. Lo que hace que el sistema sea confiable no es el recuento de líneas: es la respuesta estructurada con citas a nivel de línea, la ruta nula del esquema que se niega a fabricar y el resaltado del PDF que vincula cada afirmación con su fuente. Los cuatro ladrillos (análisis, análisis de preguntas, recuperación, generación) son el núcleo conceptual; Todo lo que sigue en la serie trata sobre hacer cada uno mejor.
La versión mínima es una base, no un destino. El siguiente artículo aborda la idea errónea que arruina la mayoría de los proyectos RAG: que RAG es un problema de aprendizaje automático. No lo es.
7. Fuentes y lecturas adicionales
El marco de resultados estructurados con citas que utiliza este artículo para AnswerWithEvidence es la misma dirección que Bohnet et al. (Respuesta a preguntas atribuidas, 2022). El equivalente completo a nivel de producción de este tipo de tubería aparece en Contextual Retrieval de Anthropic (septiembre de 2024), que el Artículo 9 presentará en una vista previa. El término RAG en sí proviene de Lewis et al. (2020). El Volumen 3 (Ladrillos Agenticos) regresa a la ruta de actualización agente sobre los cuatro ladrillos definidos aquí.
Misma dirección que el artículo:
Bohnet et al., Respuesta a preguntas atribuidas, 2022 (arXiv:2212.08037). Salida estructurada con citas como mecanismo de confianza; la idea publicada más cercana detrás del esquema AnswerWithEvidence. Recuperación antrópica y contextual (puesto de ingeniería de septiembre de 2024). Tubería de grado de producción “mínima pero lista para enviar”; aterriza en recuperación híbrida + reclasificación. El artículo 9 continúa donde termina éste. Asai et al., Self-RAG: Aprender a recuperar, generar y criticar a través de la autorreflexión, ICLR 2024 (arXiv:2310.11511). La misma dirección de confianza a través de la estructura. Los tokens de autorreflexión señalan cuándo fue útil la recuperación y cuándo un reclamo está fundamentado. Lewis et al., Generación aumentada de recuperación para tareas de PNL con uso intensivo de conocimiento, NeurIPS 2020 (arXiv:2005.11401). El periódico que nombró a RAG.
Ángulo diferente, contexto diferente:
Karpukhin et al., Recuperación de pasajes densos para control de calidad de dominio abierto, EMNLP 2020 (arXiv:2004.04906). Recuperación densa como primitiva de producción; la mayoría de los tutoriales de "RAG mínimo" descienden de esto. En su lugar, este artículo utiliza la concordancia de palabras clave (defendida en el artículo 2). Yao et al., ReAct: Sinergia del razonamiento y la actuación en modelos lingüísticos, ICLR 2023 (arXiv:2210.03629). Papel fundacional de Agentic RAG. El contexto es la selección de herramientas de uso general en tiempo de ejecución. El Volumen 3 (Agentic Bricks) desarrolla esta línea sobre los cuatro ladrillos aquí definidos. Lee et al., ¿Pueden los modelos de lenguaje de contexto largo incluir la recuperación, RAG, SQL y más?, 2024 (arXiv:2406.13121). El enfoque de recuperación de reemplazos de contexto prolongado. Datos empíricos sobre dónde funciona esto y dónde falla; Este artículo asume que el contexto largo no reemplaza la recuperación estructurada en archivos PDF empresariales.