IATESISBIBLIOTECAS + INTELIGENCIA ARTIFICIAL Comprobando índice

PROYECTO FINAL · INTELIGENCIA ARTIFICIAL APLICADA

Inteligencia artificial
para explorar
bibliotecas y tesis.

Encuentra fichas bibliográficas y fragmentos de tesis. Compara búsquedas por palabras y por vectores, revisa las fuentes y pide una redacción con Ollama si la necesitas.

registros en el índice
3formas de explorar
PDFconsulta texto y páginas
Comenzar una búsqueda
IATESIS · EL PROCESO IMPLEMENTADOVer metodología ↗
  1. Preparar dos colecciones

    CatálogoTXT → 593.067 fichas JSONL → índice BM25.

    PDFTexto por página → 6.541 fragmentos → índice BM25.

  2. Entrenar el encoder con títulos

    Selección de 12.000 fichas; 8.750 títulos actualizan los pesos durante 4 épocas. Se conserva la época 3 por validación.

    TF-IDF: 4.096 → capa lineal: 1.024 → normalización L2

  3. Calcular vectores con pesos fijos

    El mismo encoder representa los títulos del catálogo y los fragmentos PDF. Los PDF reutilizan los pesos aprendidos con títulos.

  4. Recuperar en la colección elegida

    Pregunta → BM25, búsqueda densa o combinación RRF → referencias o pasajes con página y fuente.

  5. Mostrar fuentes y redactar si se solicita

    Ollama ejecuta Llama con la pregunta y evidencia recuperada. El proyecto no entrenó Llama con las tesis.

Resumen de la entrega documentada: 50 PDF revisados, 49 con fragmentos y 32 con extracción parcial. Las pruebas funcionales no equivalen a evaluación humana de relevancia.

ALUMNO

Víctor Manuel Osornio Luna

Proyecto final · IATESIS

Presentación técnica del proyecto ↗

FORMACIÓN ACADÉMICA

Diplomado en Inteligencia Artificial Aplicada

DGTIC · Universidad Nacional Autónoma de México

6.ª EMISIÓN · 2026

Dirección de Docencia en Tecnologías de Información y Comunicación

TU PUNTO DE PARTIDA

¿Qué quieres descubrir?

01
Prueba sin filtros

Combina coincidencias de palabras y similitud de vectores mediante RRF.

Acotar la búsqueda Opcional

Sólo el nombre de la facultad. Para buscar una persona, usa el campo de consulta.

⌘ / Ctrl + Enter para buscar

En «Catálogo» se consultan fichas bibliográficas. Cambia a «PDF» para buscar pasajes extraídos y abrir la página de la tesis.

Tu espacio de descubrimiento

CONSULTA PÚBLICA

EXPLORA EL CATÁLOGO CON IA

Comienza con una consulta.

Escribe un tema o el nombre de un autor.
Aquí encontrarás referencias y sus fuentes.

01Encuentra

Busca por palabras o por similitud.

02Explora

Revisa títulos, autores y enlaces.

03Conecta

Pide una redacción con Ollama.

IATESIS · ENTENDER EL PROYECTO, PASO A PASO

Manual técnico y científico del proyecto.

Una guía desde los datos hasta la respuesta: metodología aplicada, parámetros, fragmentos de código y evidencia. Entrega documentada del 14 de septiembre de 2026.

593.067registros del catálogo de esta entrega
12.000seleccionados para preparar el entrenamiento
49 PDFcon texto indexado, de 50 archivos revisados
6.541fragmentos PDF disponibles

El catálogo completo y la muestra de entrenamiento tienen funciones distintas. Las cifras de esta ficha corresponden a la entrega indicada; el contador superior consulta el catálogo que esté activo.

CAPÍTULO 01Cómo leer el manual y comprobar sus afirmaciones

La pregunta del laboratorio es cómo transformar fichas bibliográficas y texto de tesis en fuentes consultables, y cómo usar esas fuentes para apoyar una respuesta. El resultado disponible es un sistema ejecutable y trazable. Determinar cuánto mejora la relevancia o el trabajo de una biblioteca exige otra etapa: una evaluación con personas y criterios definidos.

Para comenzar sin conocimientos de IA, conviene distinguir cinco acciones. Preparar convierte datos a una estructura consistente. Indexar organiza esa estructura para encontrar resultados. Entrenar modifica parámetros a partir de ejemplos y una pérdida. Vectorizar aplica una transformación ya fijada y guarda números por texto. Generar produce texto nuevo a partir de una pregunta y un contexto. En este proyecto sólo el entrenamiento del encoder de títulos ajustó una red; las otras acciones tienen objetivos diferentes.

Cada apartado conecta entrada → operación → parámetros → archivo de salida → comprobación. Los bloques de código identificados como literales proceden de las funciones citadas. Los ejemplos abreviados, fórmulas pedagógicas y comandos para otra instalación se indican como tales: no son registros de nuevas ejecuciones.

Estado de una afirmación Cómo interpretarlo en este manual
Aplicado y comprobado Existe evidencia en el código y los artefactos o registros de esta entrega
Implementado, no activado Hay una ruta de código disponible; no se atribuyen resultados a ella
Configurable El valor puede cambiar mediante el argumento o variable indicado; otros valores requieren una nueva comprobación
Propuesta de investigación Se plantea una hipótesis o una ampliación; no está ejecutada ni se promete una mejora

Las cifras de datos se contrastaron con artifacts/entrega-servidor-entrenada-v1/payload/BUILD_REPORT.json, los manifiestos, las bases SQLite y sectesis/neural/encoder.json. La Release privada de artefactos conserva el código de preparación 9193ce88f586783d3d8ab5dd93ec6880f49116a9. Las citas de funciones se fijan al commit auditado de código 01bbc0c, para que futuras ediciones de main no desplacen sus líneas. La revisión del manual no ejecuta un nuevo entrenamiento.

GitHub solicita acceso al repositorio privado para abrir esas fuentes. Los extractos y explicaciones de esta página permiten seguir el proceso sin ese acceso. Los registros operativos locales no están publicados como si fueran datos del repositorio. Una prueba de integridad demuestra que los bytes coinciden; una prueba funcional demuestra que una operación responde; ninguna equivale por sí sola a calidad científica o validación humana.

Ruta de lectura: comenzar con los conteos y el diagrama; recorrer TXT, PDF, BM25, entrenamiento y vectores; después estudiar la consulta, la evaluación y Llama. Las secciones de API y servidor explican la operación. Al final se presentan aportes, investigación pendiente y un mapa de archivos para continuar leyendo el código. El glosario permite consultar los términos durante ese recorrido.

Volver al índice ↑

CAPÍTULO 02IATESIS: qué resuelve y de dónde proceden los datos

El proyecto aplica recuperación de información, aprendizaje automático e IA generativa a datos bibliográficos y a fragmentos de tesis. Un catálogo es una parte de los datos de una biblioteca: describe documentos mediante título, autores, año, materias y enlaces. Un PDF aporta texto del documento. Por eso la presentación principal habla de IA aplicada a datos de bibliotecas y tesis, y el buscador distingue ambas colecciones.

IATESIS es el nombre del proyecto, elegido por su autor, Víctor Manuel Osornio Luna, alumno del Diplomado en Inteligencia Artificial Aplicada, 6.ª emisión, DGTIC UNAM. La presentación y el pie de página identifican al estudiante y su formación. El proyecto utiliza la fuente bibliográfica sectesis.txt; de ella proceden el identificador técnico SECTESIS, los IDs sectesis:…, las rutas /api/v1/sectesis y las variables SECTESIS_*. Estos nombres técnicos se conservan para mantener la compatibilidad de los datos, la API y las instalaciones existentes. SECTESIS identifica esa fuente y su implementación previa; no es una técnica de IA ni el nombre visible actual del proyecto. El proyecto no implementa todos los procesos de una biblioteca: circulación, adquisiciones y administración de usuarios quedan fuera de este buscador.

El resultado esperado para una persona es encontrar referencias o pasajes, abrir sus fuentes y, opcionalmente, pedir una redacción apoyada en ellos. Para una biblioteca, aporta un índice reutilizable, acceso a texto extraído y trazabilidad por documento y página. Que encuentre resultados no demuestra por sí solo que sean los más relevantes.

Volver al índice ↑

CAPÍTULO 03593.067 y 12.000 son cifras de etapas diferentes
Cifra comprobada Qué cuenta Qué no significa
593.067 registros Catálogo completo preparado desde este TXT, indexado con BM25 y vectorizado No son 593.067 PDF ni todo el acervo de la UNAM
12.000 registros Selección reproducible para preparar el entrenamiento del encoder No es el tamaño del catálogo consultable
11.010 títulos 12.000 menos 988 títulos con menos de cuatro tokens útiles y 2 duplicados normalizados No todos actualizan los pesos
8.750 títulos Partición de entrenamiento: sí calcula gradientes No se entrenó con los títulos de validación y prueba
1.144 títulos Validación para seleccionar la época No son juicios humanos de relevancia
1.116 títulos Partición de prueba reservada Su existencia no implica que haya una evaluación humana ejecutada
50 PDF / 49 indexados Archivos revisados / archivos que produjeron fragmentos FAQS1.pdf no produjo texto extraíble
5.034 / 4.727 páginas Páginas físicas vistas / páginas que produjeron fragmentos No se ejecutó OCR adicional
6.541 fragmentos Unidades de texto PDF indexadas y vectorizadas No son tesis diferentes

De los PDF, 17 quedaron con estado text_extracted, 32 con partial_text y uno con no_extractable_text. Hubo 202 páginas sin texto y 105 con menos de 25 letras: esas 307 páginas no generaron fragmentos. Que un PDF esté indexado no significa que todo su contenido haya sido interpretado.

El archivo conserva el nombre histórico sample.jsonl, pero en esta entrega contiene 593.067 registros porque se preparó con --sample 0. El entrenamiento del encoder se ejecutó en este servidor: cuatro épocas, con selección de la tercera por pérdida de validación. No corresponde describirlo como un laboratorio de 12.000 registros simplemente importado.

Volver al índice ↑

CAPÍTULO 04Dos rutas de preparación y una forma de consultar
  1. 01

    Leer ALEPH

    Identificador, etiquetas y subcampos de la ficha.

  2. 02

    Preparar JSONL

    593.067 registros; campos normalizados y faltantes visibles.

  3. 03

    Indexar palabras

    SQLite FTS5 organiza términos y registros para BM25.

  4. 04

    Entrenar encoder

    12.000 seleccionados; 8.750 títulos actualizan los pesos.

  5. 05

    Calcular vectores

    Pesos fijos → 593.067 vectores; IDs en el mismo orden.

La consulta selecciona catálogo o PDF; la búsqueda híbrida combina dos métodos dentro de la colección elegida. No une automáticamente una ficha y un archivo PDF ni busca simultáneamente en ambas colecciones.

Volver al índice ↑

CAPÍTULO 05Ruta TXT: de ALEPH a JSONL y SQLite

Entrada y objetivo: entender una ficha antes de calcular números

La entrada es sectesis.txt, una exportación bibliográfica ALEPH secuencial de 866.475.615 bytes. Contiene fichas que describen tesis, no la transcripción de todas ellas. El objetivo de esta etapa es convertir esas líneas en registros con campos consistentes y conservar su procedencia. No interviene Llama ni se aprenden pesos.

records() lee UTF-8 estricto y agrupa líneas consecutivas por el identificador de nueve dígitos. El formato exige ese ID, una etiqueta de cinco posiciones, la letra L y el contenido; $$ separa subcampos. Una línea no interpretada se registra como anomalía cuando ya hay un registro en curso. La implementación supone que las líneas de una ficha son contiguas. Parser real: ingest.py, líneas 8–35.

LINE = re.compile(r"^(\d{9}) (.{5}) L ?(.*)$")
SUB = re.compile(r"\$\$([^$])([^$]*)")

Fragmento real del TXT:

000000001 1001  L $$aAceves Ramirez, Noe de Jesus$$esustentante
000000001 24510 L $$aOrigen, causas, evolución, perspectivas y soluciones propuestas al problema habitacional en México /$$ctesis que para obtener el título de Arquitecto, presenta Noe de Jesus Aceves Ramirez
000000001 502   L $$bLicenciatura en Arquitectura$$cUniversidad Nacional Autónoma de México,$$d1979$$gFacultad de Arquitectura,

canonical() transforma las etiquetas en un objeto con campos conocidos. Las prioridades siguientes son reglas del software, no correcciones hechas por una persona:

Campo de salida Regla aplicada Ejemplo o limitación
id Prefijo sectesis: + ID ALEPH sectesis:000000001; no es el hash de una tesis
title Primer 245$a, seguido de 245$b si existe Conserva el título; no incorpora automáticamente toda la responsabilidad 245$c
authors / contributors 100$a / 700$a No se inventan nombres ausentes
degree 502$b Licenciatura en Arquitectura
institution 502$c; si falta, 710$a Fallback explícito
faculty 502$g; si falta, 710$b Fallback explícito
year Primero 264$c, después 260$c, después 502$d; extrae el primer año de cuatro dígitos Conserva además year_raw; una fecha extraída puede requerir revisión
subjects Subcampos de 600, 610, 611, 630, 650, 651 y 653 No genera materias nuevas
abstract / urls 520$a / 856$u Son campos del catálogo; un enlace no equivale a haber leído su documento

Reglas literales: canonical(), líneas 37–68.

Selección de campos del registro canónico real —el objeto completo contiene más—:

{
  "id": "sectesis:000000001",
  "source_id": "000000001",
  "title": "Origen, causas, evolución, perspectivas y soluciones propuestas al problema habitacional en México",
  "authors": ["Aceves Ramirez, Noe de Jesus"],
  "year": 1979,
  "faculty": "Facultad de Arquitectura",
  "degree": "Licenciatura en Arquitectura"
}

Tres operaciones distintas de normalización

Operación Qué hace realmente Para qué se utiliza
clean() Unicode NFC, espacios consecutivos a uno, recorte de extremos y signos finales /,;: Limpiar la presentación de campos sin borrar acentos
fold() casefold, Unicode NFKD y eliminación de marcas combinantes Comparar sin distinguir mayúsculas/acentos; conserva puntuación y palabras
tokens() Aplica fold, extrae secuencias \w+, elimina unidades de dos caracteres o menos y una lista fija de palabras; conserva las primeras 64 Entrada de TF-IDF y normalización de la consulta léxica

La clave de deduplicación del entrenamiento es fold(título), no una lista de tokens. Dos títulos que sólo se diferencian por puntuación pueden seguir siendo distintos para esa regla. No hay lematización —reducción a una forma de diccionario— ni identificación de duplicados semánticos. Limpieza y clave: ingest.py, 10–13; tokens del proyecto: search.py, 6–7.

Preparación completa, control de calidad y salida

prepare() escribe un objeto por línea en sample.jsonl y el perfil en profile.json. La CLI básica tiene --sample 12000 por defecto; esta entrega conserva todo el TXT, como acredita sample_method=full_corpus y sample_size=593067. --sample 0 significa corpus completo. Una muestra positiva usa reservoir sampling: recorre todo el origen y conserva una selección uniforme, no las primeras filas. El perfil también se calcula sobre todo el origen leído. Preparación: ingest.py, 85–117.

Parámetro o control Aplicado Predeterminado o límite Función
Alcance de JSONL 593.067 registros CLI básica --sample 12000; 0 conserva todos Separa tamaño del catálogo de muestra de aprendizaje
Semilla de preparación 20260909 Mismo defecto en CLI Reproduce una selección cuando se usa muestreo y se mantiene el orden del origen
Año para revisión 2026 CLI básica 2026; rango admitido 1500…9999 Marca años fuera de 1500…año de validación; no los corrige
Formato alephseq_v2, UTF-8 estricto Se rechaza formato inicial desconocido Identifica las reglas usadas para interpretar los datos
Salida existente No se sobrescribe Exige nueva salida si ya existen JSONL/perfil/temporal Evita mezclar versiones
Integridad SHA-256 de origen y JSONL; comprueba tamaño/fecha del origen durante lectura Controles del código Detecta cambios o mezclas; no demuestra calidad bibliográfica

El perfil real reporta 13 registros sin título, 17 sin autor, 1.828 sin año, 31.687 sin facultad y 296 sin grado. Marca 1.827 años para revisión y 87 líneas no interpretadas. Sólo 2 registros tienen resumen según el parser; 193.503 tienen materias. Estas son medidas de presencia y banderas automáticas, no una certificación catalográfica. Los registros incompletos permanecen en el catálogo; otro filtro decide después cuáles sirven para entrenar. Evidencia: payload/sectesis/profile.json de la entrega.

Cada registro contiene dos textos preparados para usos diferentes. Fragmento literal de canonical():

rec['semantic_text']='\n'.join(v for v in [title, '; '.join(subjects),abstract] if v)
rec['lexical_text']='\n'.join(v for v in [title,'; '.join(rec['authors']),'; '.join(subjects),degree,faculty,year_raw] if v)

lexical_text se usa para BM25. Aunque exista semantic_text, el encoder didáctico activo recibe el título, no todo ese campo. record_sha256 identifica los campos parseados de la ficha; es distinto del hash de bytes del TXT y del ID bibliográfico.

De JSONL a SQLite: guardar e indexar no es entrenar

search.build() inserta cada registro en records, con ID, ámbito, año, facultad y JSON. Crea docs, una tabla virtual FTS5 para el índice invertido, y manifest, con versión, conteo y huella del JSONL. SQLite organiza texto y términos para recuperarlos; no aprende la matriz neuronal. Construcción exacta: search.py, 13–30.

con.execute('CREATE TABLE records(id TEXT PRIMARY KEY,tenant TEXT NOT NULL,year INTEGER,faculty TEXT,payload TEXT NOT NULL)')
con.execute("CREATE VIRTUAL TABLE docs USING fts5(id UNINDEXED,title,body,tokenize='unicode61 remove_diacritics 2')")

FTS5 utiliza su propio tokenizer unicode61. Al construir docs, se guarda el texto documental completo: no se recorta cada documento a los 64 tokens del encoder. El ID no se indexa como texto; sirve para enlazar el resultado con su registro. La salida real es sectesis/catalog.sqlite. El procedimiento rechaza una ruta ya existente y publica el archivo temporal al terminar, para conservar una versión coherente.

Volver al índice ↑

CAPÍTULO 06Ruta PDF: del archivo a fragmentos, IDs e índice

Qué es un fragmento y por qué se utiliza

Un fragmento, o chunk, es un tramo de texto extraído de una página que se guarda como unidad de búsqueda. Es texto del documento, no un resumen escrito por Llama ni un archivo PDF nuevo. Una página puede producir varios fragmentos y una tesis muchos; por eso 6.541 fragmentos no equivalen a 6.541 tesis.

Se divide el texto para recuperar pasajes más precisos y enviar a la generación sólo material pertinente, en lugar de una tesis completa. Cada fragmento conserva el archivo, la página y sus posiciones para volver a la fuente. Dividir también tiene un coste: puede separar ideas o dejar una explicación fuera del pasaje; por eso se conserva algo de texto compartido entre fragmentos consecutivos.

Cómo se crearon en este proyecto

PyMuPDF abre cada PDF y extrae el texto disponible por página con page.get_text("text", sort=True). Se normalizan espacios y caracteres de control. No se añaden resúmenes generados ni se ejecuta OCR. Si el PDF ya contiene una capa de texto defectuosa, esa extracción puede conservar errores; un PDF escaneado sin texto puede no aportar fragmentos.

Las páginas se dividen en fragmentos de hasta 1.600 caracteres, con un solapamiento objetivo de 200 caracteres, ajustado en límites de palabras. No se mezclan páginas. Se exige un mínimo de 25 letras en la página. Los desplazamientos char_start y char_end se refieren al texto normalizado de esa página; el extremo final es exclusivo.

Ejemplo real de 0609550.pdf, página física 33:

Realizar la planeación, programación y evaluación periódica de las actividades que se efectúan mediante el análisis de la situación de las diferentes áreas.

Es una vista abreviada: corresponde a las posiciones 562–718 dentro de un fragmento de 1.596 caracteres. No es el texto completo del fragmento. Metadatos reales:

{
  "id": "pdf-61d5c568d55a2a6b6620dcc56b7eaae1925b3241837a5180719e3561681a611d:p33:c0",
  "doc_id": "pdf-61d5c568d55a2a6b6620dcc56b7eaae1925b3241837a5180719e3561681a611d",
  "file": "0609550.pdf",
  "page": 33,
  "char_start": 0,
  "char_end": 1596
}

doc_id concatena pdf- y el SHA-256 de los bytes originales. p33 identifica la página física; c0 el primer fragmento de esa página, numerado desde cero. Un cambio de bytes cambia la identidad del documento. Con iguales bytes, extractor y opciones, la segmentación es reproducible. El ejemplo TXT y este PDF son documentos distintos; no se está afirmando una relación entre ellos.

Se producen dos representaciones del mismo conjunto: pdf/chunks.jsonl facilita inspección e intercambio y pdf/catalog.sqlite permite búsqueda. En SQLite, files guarda documentos, chunks guarda IDs y páginas, y fts indexa el texto de los fragmentos. Un manifest.json registra archivos omitidos, conteos, opciones y hashes. Los originales permanecen en data/PDF; no se convierten en pesos ni se sustituyen por SQLite.

Dos fragmentos consecutivos reales

En 0609550.pdf, la página física 33 produjo estas dos unidades, comprobadas directamente en SQLite. Comparten el mismo doc_id mostrado arriba:

Fragmento Inicio incluido Final excluido Caracteres guardados
…:p33:c0 0 1.596 1.596
…:p33:c1 1.396 1.759 363

La franja 1.396–1.596, de 200 caracteres, aparece en ambos. El segundo es más corto porque llega al final del texto de esa página. Esas posiciones cuentan caracteres del texto normalizado, no bytes del PDF ni tokens de Llama. En otras páginas, el ajuste al límite de palabra puede cambiar la longitud y el solapamiento efectivos.

El código busca un espacio para no cortar una palabra cuando alcanza el tamaño objetivo, guarda el tramo y retrocede hacia la zona de solapamiento para empezar el siguiente. No detecta automáticamente capítulos ni garantiza cortes por oraciones completas. Cada página se procesa por separado; no se repite texto de la página anterior dentro de la siguiente.

Cada unidad se escribe en chunks.jsonl y en SQLite con el mismo ID. BM25 indexa su texto; la vectorización aplica el encoder fijo y guarda una fila de 1.024 valores asociada a ese ID. La búsqueda devuelve el fragmento con su página y enlace al original. El encoder activo sólo usa hasta 64 tokens útiles, de modo que un fragmento puede estar completamente disponible para BM25 y la lectura humana, pero representado parcialmente en la búsqueda densa.

Código: extract, chunks e index_pdfs.

Parámetros aplicados, justificación y cobertura observada

El tamaño 1.600 y el solapamiento 200 son decisiones de esta implementación: acotan el pasaje y conservan algo de continuidad. No se realizó una comparación que demuestre que sean óptimos. El archivo pdf/manifest.json de la entrega confirma estas opciones y PyMuPDF 1.26.7.

Parámetro de extracción Valor aplicado Para qué sirve Cómo se puede cambiar
chunk_chars 1.600 caracteres Limita el tamaño de cada unidad de búsqueda CLI --chunk-chars, entre 200 y 8.000
overlap_chars 200 caracteres objetivo Comparte contexto entre fragmentos consecutivos de una página CLI --overlap-chars, desde 0 hasta menos de la mitad de chunk_chars
min_letters 25 letras por página Omite páginas con muy poco texto alfabético Objeto Python Options; no hay flag CLI actual
max_file_bytes 67.108.864 bytes, 64 MiB Rechaza archivos que superan ese límite de entrada Options
max_pages 2.000 páginas por PDF Acota extracción por archivo Options
max_page_chars 200.000 caracteres extraídos por página Rechaza una extracción excesiva antes de normalizar Options
previous Reutilización de 49 PDF en el build final Evita repetir extracción de contenido idéntico CLI --previous, con iguales versiones y opciones

Definición y validación: Options y validate. Opciones realmente expuestas por el comando: main: index y vectorize. Un valor en el código Python no equivale automáticamente a una opción disponible en la pantalla o en Compose.

Este fragmento literal del algoritmo muestra el corte. Busca el último espacio en la segunda mitad del tramo para evitar partir una palabra. El código siguiente a este fragmento ajusta también el comienzo de la siguiente unidad:

end = min(start + size, len(text))
if end < len(text):
    boundary = text.rfind(" ", start + size // 2, end)
    if boundary > start:
        end = boundary
yield start, end, text[start:end]
if end == len(text):
    break
next_start = max(start + 1, end - overlap)

Fuente: chunks, líneas 49–67. La normalización usa NFC y colapsa espacios; conserva mayúsculas, acentos y palabras con guiones. No hace lematización ni recompone automáticamente palabras cortadas al final de línea: normalize.

En la entrega final se reconstruyeron los archivos del índice reutilizando la extracción anterior de 49 documentos, según files_reused=49. No se extrajeron de nuevo esos 49 PDF durante ese build. Para reutilizar, index_pdfs exige la misma huella de extractor/opciones y el mismo SHA-256 de cada original; después escribe el nuevo SQLite y JSONL. La vectorización sí aplica los pesos fijos del encoder de la entrega a esos fragmentos. Código: index_pdfs: reutilización por huella y vectorize: inferencia con pesos existentes.

Estado registrado Archivos Lectura correcta del resultado
text_extracted 17 Todas sus páginas superaron las comprobaciones automáticas de texto; falta valorar fidelidad humana
partial_text 32 Algunas páginas quedaron sin fragmentos por ausencia o escasez de texto
no_extractable_text 1 FAQS1.pdf no produjo fragmentos
Total 50 49 aportan al menos un fragmento

El manifest suma 202 páginas sin texto y 105 con menos de 25 letras: 307 de las 5.034 páginas no originaron fragmentos. Las 4.727 restantes produjeron 6.541 unidades. Por tanto, «49 PDF indexados» significa cobertura parcial o total del texto extraíble de esos archivos, no lectura íntegra garantizada. Los títulos obtenidos de metadata PDF tampoco son metadatos bibliográficos validados. Fuente de las reglas y estados: extract: extracción por página y estados; evidencia de esta ejecución: pdf/manifest.json incluido en la Release de artefactos.

El indexador omite enlaces simbólicos, contenido duplicado y archivos que exceden límites; respeta el permiso de copiar texto y no abre archivos que requieren clave. Verifica que el original no cambie durante la extracción y publica en un destino nuevo. Estas comprobaciones protegen la coherencia de los datos; no interpretan imágenes, tablas o fórmulas: index_pdfs: controles e índice nuevo.

Volver al índice ↑

CAPÍTULO 07Qué es BM25 en este proyecto

BM25 —Best Matching 25— es un método de ranking léxico: ordena coincidencias entre palabras de la consulta y texto indexado. Se eligió como referencia de recuperación implementable sin entrenamiento neuronal y capaz de buscar términos presentes en los datos. Su existencia en el laboratorio permite comparar la búsqueda densa con un método léxico real; no demuestra que uno sea superior en todas las preguntas.

Qué entra al índice y qué entra a la consulta

El catálogo guarda title y body en la tabla virtual FTS5 docs; body contiene título, autores, materias, grado, facultad y año original. El ID queda UNINDEXED: sirve para unir el resultado con su ficha. PDF usa la tabla fts, con el texto completo de cada fragmento y su ID sin indexar. Ambos declaran tokenize='unicode61 remove_diacritics 2'. Código aplicado: build: columnas y carga del catálogo y index_pdfs: esquema PDF.

Hay dos tokenizadores distintos. FTS5 indexa todo el texto que se guarda en sus columnas. La función Python tokens() prepara la consulta y también las entradas del encoder: normaliza mayúsculas y acentos, descarta palabras de parada y términos de dos caracteres o menos, y conserva los primeros 64 términos útiles. Por tanto, el límite 64 no recorta el texto almacenado en el índice BM25. No se aplicó stemming, lematización ni expansión por sinónimos.

Código literal de tokens, con su lista de palabras excluidas:

STOP=set('de la el los las en y a un una para por con del al que sobre entre como su se es tesis estudio analisis'.split())
def tokens(s):return [w for w in re.findall(r'\w+',fold(s)) if len(w)>2 and w not in STOP][:64]

Fuentes: tokens, líneas 6–7 y fold: normalización Unicode. Ejemplo didáctico: El problema habitacional en México produce problema, habitacional, mexico. El código forma "problema" OR "habitacional" OR "mexico"; basta coincidir con parte de la consulta. La pantalla no ofrece una sintaxis avanzada de frases, AND o sinónimos aunque FTS5 tenga capacidades adicionales.

Cómo se calcula y ordena la puntuación

La contribución de cada término depende de su frecuencia en el documento, su rareza en la colección y la longitud del documento. La repetición se satura: cien apariciones no aportan cien veces más utilidad. Para entender el cálculo positivo mostrado por esta API:

aporte = IDF × f × (k1 + 1) / (f + k1 × (1 - b + b × longitud / longitud_media))
score = suma de aportes de los términos consultados

f es frecuencia ponderada por columna; IDF expresa rareza, y longitud cuenta tokens FTS. FTS5 fija internamente k1=1,2 y b=0,75; no son argumentos de este buscador. SQLite devuelve el signo inverso para ordenar de menor a mayor y nuestra API lo cambia al publicar. La definición exacta de FTS5 especifica su IDF y la ponderación por columna.

Parámetro o decisión Catálogo PDF Estado en este proyecto
Función de puntuación bm25(docs,0,2,1) bm25(fts) Aplicada en SQL
Peso de columna ID 0; título 2; cuerpo 1 Texto con peso predeterminado 1 Fijo en código; no aprendido
Operador de consulta OR entre tokens citados Igual Fijo en código
Resultados top_k 5 por defecto; API 1–50 Igual Configurable por solicitud
Ámbito tenant fijado en servidor Carpeta PDF configurada Colecciones independientes
Filtros Años inclusivos y facultad normalizada exacta doc_id exacto Se aplican antes de elegir resultados
Desempate ID ascendente ID de fragmento ascendente Determinista para esos candidatos

El título del catálogo participa en su columna y también en body; no interpretar el peso 2 como una garantía de «doble relevancia» de una tesis. Los filtros de año excluyen fichas sin año cuando no pueden satisfacer la condición. La facultad se compara completa tras normalización, no por coincidencia parcial. PDF no tiene filtros de año ni facultad en esta API. Fuentes: LexicalIndex.search, Engine.lexical y Query: límites de la API.

Este SQL es una presentación equivalente de la consulta implementada, con valores ilustrativos y sin filtros opcionales:

SELECT r.payload, bm25(docs, 0, 2, 1)
FROM docs JOIN records r ON r.id = docs.id
WHERE docs MATCH ? AND r.tenant = ?
ORDER BY bm25(docs, 0, 2, 1), r.id
LIMIT ?;
-- Parámetros: '"problema" OR "habitacional" OR "mexico"', 'global_unam', 5

LexicalIndex.search añade filtros mediante parámetros SQL; Llama no escribe ni ejecuta esa consulta. El índice busca coincidencias de los datos existentes: que una ficha incluya «México» no prueba que responda toda la pregunta. El score BM25 no es una probabilidad ni un porcentaje de verdad y no debe sumarse directamente con el coseno de otro método.

Qué se podría comparar en investigación

Quedan como experimentos propuestos: OR frente a coincidencia de todos los términos, otras ponderaciones de título/cuerpo y normalización lingüística. Exigen cambios de código o nuevos índices, casos de relevancia revisados y comparación con esta configuración fija. No se ejecutaron esas variantes ni se eligieron los valores actuales mediante una optimización de relevancia del catálogo completo.

Volver al índice ↑

CAPÍTULO 08Cómo se entrena el encoder

Qué significa entrenar en este laboratorio

Entrenar consiste en modificar una matriz de números, W, para reducir una función de pérdida. La entrada es la representación TF-IDF de un título; la salida es un vector de 1.024 coordenadas. El modelo aprende a aproximar dos versiones incompletas del mismo título y distinguirlas de otros títulos del lote. No aprende de preguntas respondidas por bibliotecarios ni de párrafos PDF. Es una red lineal didáctica construida desde cero, sin conocimiento lingüístico preentrenado.

El proceso histórico quedó registrado en BUILD_REPORT.json, sectesis/neural/encoder.json, training_sample.jsonl y splits.json. El kit declara training.performed=true, cuatro épocas y la tercera como seleccionada. En esta revisión del manual se verificaron esos archivos; no se repitió el entrenamiento. El código de ingesta y encoder conserva las mismas huellas que la construcción del commit 9193ce88f586783d3d8ab5dd93ec6880f49116a9.

Paso 1: elegir datos, filtrar y separar particiones

reservoir() recorre el JSONL completo y selecciona 12.000 registros con semilla 20260909. Cada registro tiene la misma oportunidad de entrar, pero no se garantiza equilibrio por facultad o año. El orden de entrada también importa para reproducir la muestra. Selección: ia_artefactos.py, 81–116.

Se volvió a contar la muestra real aplicando las reglas del programa, sin entrenar:

Operación Resultado comprobado Para qué sirve
Selección inicial 12.000 registros Acotar el coste de aprendizaje
Excluir títulos con menos de cuatro tokens útiles 988 excluidos; 11.012 elegibles Exigir algo de contenido para crear dos vistas
Conservar el primero de cada fold(título) 2 duplicados excluidos; 11.010 títulos únicos Evitar repetir esa misma clave entre particiones
Partición de entrenamiento 8.750 títulos Ajustar TF-IDF y actualizar W
Partición de validación 1.144 títulos Elegir la época por pérdida
Partición de prueba 1.116 títulos Reservada; no participa en ajuste ni selección

La pertenencia y el orden recalculados coinciden con splits.json. El filtro y la deduplicación se ejecutan en ese orden. La clave fold(título) conserva puntuación; no agrupa automáticamente títulos parecidos ni todos los documentos del mismo tema. Filtro y particiones: neural.py, 43–50.

La función split_of() toma los primeros ocho caracteres hexadecimales del SHA-256 de esa clave y aplica módulo diez. Es una regla aproximada 80/10/10, no tres porcentajes exactos ni una partición por año. No utiliza la semilla del muestreo. Código: neural.py, 20–22.

n=int(hashlib.sha256(key.encode()).hexdigest()[:8],16)%10
return 'test' if n==0 else ('validation' if n==1 else 'train')

Paso 2: convertir el título a TF-IDF

TF-IDF asigna un valor a cada término del vocabulario: considera cuánto aparece en el título y en cuántos títulos de entrenamiento aparece. Un vector de 4.096 posiciones usa el mismo orden de términos para todos los textos. Una posición cero indica que ese término no aporta señal en esa entrada; no significa que todo el título sea irrelevante.

El proyecto utiliza frecuencia sublineal: para un término presente, tf = 1 + ln(frecuencia). El IDF suavizado es ln((1+n)/(1+df)) + 1, donde n=8750 y df cuenta títulos de entrenamiento que contienen el término. Multiplica ambos valores y normaliza el vector a longitud L2 uno. Las palabras fuera del vocabulario se omiten. Fórmulas de scikit-learn 1.8.

Fragmento literal: fit_transform aprende vocabulario e IDF sólo con entrenamiento; transform aplica esos mismos valores a validación. Código: neural.py, 49–50.

v=TfidfVectorizer(max_features=4096,min_df=2,tokenizer=tokens,token_pattern=None,lowercase=False,sublinear_tf=True,dtype=np.float32)
x=v.fit_transform([d['title'] for d in splits['train']]); xv=v.transform([d['title'] for d in splits['validation']])
Parámetro de representación Valor aplicado y origen Efecto y límite
max_features 4096, fijado en código; vocabulario real de 4096 Acota cobertura y memoria
min_df 2, fijado en código Un término debe aparecer en al menos dos títulos de train
tokenizer tokens() del proyecto Quita acentos/mayúsculas, unidades cortas y lista fija; máximo 64 tokens útiles
token_pattern / lowercase None / False, explícitos Delega esa normalización al tokenizer propio
sublinear_tf True, explícito Reduce el efecto de repetir términos
dtype float32, explícito Cuatro bytes por valor; cálculo aproximado de precisión finita
ngram_range (1,1), defecto de scikit-learn Unigramas; no características de parejas de palabras
norm, use_idf, smooth_idf 'l2', True, True, defectos de scikit-learn Normalización e IDF suavizado
max_df, binary 1.0, False, defectos de scikit-learn No excluye por frecuencia máxima menor al 100 % ni convierte todas las frecuencias a uno

Los defaults corresponden a scikit-learn 1.8.0, versión registrada en la construcción. Se distinguieron de los argumentos que el proyecto fija expresamente. Firma de TfidfVectorizer 1.8. En esta auditoría los 4.096 IDF guardados coinciden con el cálculo sobre train dentro de un error máximo de 5,69 × 10⁻⁷, compatible con float32.

Paso 3: transformar, comparar y ajustar los pesos

La red tiene una capa 4096 → 1024 sin sesgo. Es una multiplicación por la matriz W, seguida de normalización L2; no tiene bloques de atención ni arquitectura Transformer. Clase Encoder, neural.py, 16–19.

class Encoder(nn.Module):
    def __init__(self,n:int,dim:int=1024):
        super().__init__();self.linear=nn.Linear(n,dim,bias=False)
    def forward(self,x):return nn.functional.normalize(self.linear(x),p=2,dim=1)

Por cada lote de hasta 96 títulos, el software crea dos máscaras independientes que ocultan cada característica con probabilidad 0,15. No elimina exactamente el 15 % de palabras de cada título. Las dos vistas del mismo título son el par positivo; los otros títulos del lote actúan como negativos, sin que una persona haya juzgado su relación temática.

aa=a*(torch.rand_like(a)>.15); bb=a*(torch.rand_like(a)>.15)
z1=model(aa);z2=model(bb);sim=z1@z2.T/.1; labels=torch.arange(len(a))
return .5*(nn.functional.cross_entropy(sim,labels)+nn.functional.cross_entropy(sim.T,labels))

Este fragmento procede de loss_batch(), líneas 53–58. a tiene hasta 96 filas por 4.096 entradas. z1 y z2 tienen hasta 96 por 1.024. La matriz sim compara cada fila de una vista con todas las de la otra; su diagonal contiene los pares positivos. La temperatura 0.1 escala esas similitudes antes de la pérdida. La entropía cruzada penaliza que otra fila reciba más apoyo que la correspondiente; se calcula en ambos sentidos y se promedia. No es una probabilidad de relevancia de una tesis.

La actualización se ejecuta con esta línea literal. Bucle real: neural.py, 59–66.

a=torch.from_numpy(x[ix].toarray());opt.zero_grad();loss=loss_batch(a);loss.backward();opt.step();losses.append(loss.item())

Su lectura paso a paso: convertir el lote a números para PyTorch; borrar gradientes anteriores; calcular pérdida; backward() calcula cómo influye cada peso en esa pérdida; step() modifica los pesos con AdamW; guardar el valor de pérdida. La red aprende una transformación compartida, no un peso por tesis.

Parámetro de aprendizaje Aplicado / procedencia Para qué sirve
Dimensión 1024, fijada en la red Tamaño del vector de salida
Sesgo bias=False, explícito No añade un vector independiente a W @ x
Parámetros aprendidos 4.194.304 = 1024 × 4096 Coeficientes de W modificados por gradientes
Optimizador AdamW Calcula actualizaciones a partir de los gradientes
Tasa lr 0.002, explícita Escala del paso de actualización
weight_decay 0.001, explícito Decaimiento separado de los pesos durante optimización
betas / eps (0.9,0.999) / 1e-8, defectos de PyTorch 2.10 Promedios del gradiente y su cuadrado; estabilidad numérica
Tamaño de lote Hasta 96, fijado en código Memoria por paso y cantidad de negativos
Ocultación Probabilidad 0.15, fija Dos vistas incompletas de cada entrada
Temperatura contrastiva 0.1, fija Escala de las comparaciones dentro de la pérdida
Épocas 4, opción --epochs; rango 1…100 Recorridos sobre entrenamiento
Semilla 20260909 Inicialización, máscaras y mezcla de entrenamiento
Hilos PyTorch 2, fijados en código Paralelismo de esta ejecución CPU

Construcción de AdamW y controles: neural.py, 39–58. Las betas y epsilon son defaults heredados de la versión, no valores seleccionados mediante experimentos de este proyecto. AdamW 2.10. La normalización de salida divide entre el máximo de la norma y 1e-12, epsilon predeterminado de PyTorch; un vector exactamente cero permanece cero. Normalización L2.

La función de cada valor sí está explicada; su optimalidad no está demostrada. No se encontró una búsqueda de hiperparámetros que justificara 4096, 1024, 96 o las tasas como los mejores valores para el corpus. Variarlos es una línea de investigación. La CLI actual no ofrece --dimension, --learning-rate ni --temperature para este encoder: modificar esas constantes exige una versión de código y nuevos artefactos.

Paso 4: validar y conservar una época

Cada época mezcla sólo los 8.750 títulos de entrenamiento. La validación usa 1.144 títulos, sin gradientes, con máscaras reproducibles mediante seed + 77. Se guarda el estado con menor pérdida de validación y se recupera después de completar las cuatro épocas. No se detuvo el entrenamiento en la tercera ni se eligió usando test. Validación y selección: neural.py, 67–87.

Época Pérdida de entrenamiento Pérdida de validación
1 0,136437 0,153787
2 0,109401 0,154933
3, seleccionada 0,098081 0,152873
4 0,095806 0,152960

La pérdida mide el objetivo contrastivo elegido, no porcentaje de acierto ni satisfacción de usuarios. El código promedia pérdidas de lotes sin ponderarlas por su tamaño. En esta partición hay 92 lotes de train por época —el último de 14 títulos— y 12 de validación —el último de 88—. Cambiar lote o objetivo modifica la interpretación de la pérdida; no se deben comparar esos números como una escala universal de calidad.

encoder.json registra training_seconds=21.916: el intervalo medido comienza antes de preparar la muestra en train() y termina al crear metadatos. No incluye la posterior vectorización de los 593.067 registros, la ingesta PDF ni la entrega completa. Es una lectura histórica de una ejecución, no un benchmark repetido. Se usaron Python 3.11.16, PyTorch 2.10.0+cpu, NumPy 2.3.5 y scikit-learn 1.8.0.

Qué se puede configurar y qué se reutiliza

El kit acepta --train-sample 12000 —límite 20…50.000—, --epochs 4 y --seed 20260909. La función de entrenamiento directo rechaza más de 50.000 registros; por eso el kit entrena una muestra y después codifica el corpus completo. Desde --txt exige --retrain; con --catalog y sin --retrain comprueba y reutiliza los pesos existentes. Opciones: ia_artefactos.py, 491–503; decisión entrenar/reutilizar: 299–333.

La entrega histórica sí entrenó el encoder de títulos. Después se fijó esa transformación para todo el catálogo y los PDF. Las consultas de la página no actualizan sus pesos. Tampoco entrenan Llama.

Volver al índice ↑

CAPÍTULO 09Del texto a números: ejemplo real de pesos y vectores

Seguir un registro desde el texto hasta su fila numérica

El título real sectesis:000000001, mostrado en la ruta TXT, produce nueve características TF-IDF no nulas. Algunas son causas = 0,345828, evolucion = 0,310049, habitacional = 0,407594 y mexico = 0,158565. El vector tiene 4.096 posiciones; las demás pueden ser cero. Estos valores describen la entrada calculada para ese texto: no son los parámetros aprendidos de la capa lineal.

La siguiente notación explica la operación; no es un nuevo entrenamiento ni una transcripción de una función del repositorio:

x = TF-IDF(título)               # 4096 valores
z = W @ x                       # W: 1024 filas × 4096 columnas
embedding = z / max(||z||₂, ε)   # 1024 valores; ε = 1e-12

@ representa multiplicación de matriz por vector. Cada coordenada de salida combina muchas entradas con diferentes pesos. L2 mide la longitud numérica del vector; normalizar facilita comparar su orientación. Una coordenada no es una palabra o categoría humana identificada.

Primeros seis pesos de la fila 0 de la matriz real W, redondeados:

[0.032730, -0.009829, 0.007284, 0.007827, 0.037297, -0.044680, ...]

Primeros seis valores del vector guardado para sectesis:000000001:

[0.053612, -0.016303, 0.021445, 0.016783, 0.001762, -0.007057, ...]

Primeros seis valores del vector del fragmento PDF …:p33:c0, fila 906 de los vectores PDF:

[-0.003362, 0.006055, 0.056237, 0.017407, -0.037716, 0.040301, ...]

Son extractos numéricos reales, no matrices inventadas ni archivos completos de pesos. En la comprobación guardada como artifacts/ejemplo-encoder-real.json, volver a codificar el título reprodujo el vector almacenado dentro de tolerancia numérica. El ejemplo del PDF pertenece a otro documento: no se afirma una correspondencia automática entre esa tesis y la ficha del TXT.

Pesos, metadatos, IDs y vectores cumplen funciones distintas

Archivo Qué guarda Cómo se utiliza
weights.pt Estado de la matriz lineal aprendida Aplica la misma transformación a títulos, preguntas y fragmentos
encoder.json Vocabulario e IDF, dimensión, historial, particiones resumidas, semilla y huellas Reconstruye el preprocesamiento y documenta la procedencia
splits.json IDs agrupados en train, validation y test Permite auditar quién participó en cada etapa
training_sample.jsonl Los 12.000 registros seleccionados antes del filtro Permite reconstruir la entrada histórica al aprendizaje
vectors.npy Salida numérica de codificar cada texto Permite buscar sin recodificar todos los documentos por consulta
ids.json ID correspondiente a cada fila de vectors.npy ids[906] dice de qué fragmento es vectors[906]
integrity.json Huellas de archivos del conjunto Detecta corrupción o mezcla; no certifica relevancia de resultados

El kit conserva por separado training_dataset_sha256, huella de la muestra, y source_sha256, huella del catálogo completo vectorizado. Que esta última identifique 593.067 registros no significa que todos entrenaron la matriz. Procedencia y sellado: ia_artefactos.py, 148–163.

Vectorizar es inferencia con pesos fijos

LocalEncoder reconstruye TF-IDF usando el vocabulario e IDF guardados, carga weights.pt en CPU con weights_only=True y pone la red en modo evaluación. Su método encode() trabaja por lotes de 96 con torch.inference_mode(), sin gradientes ni pasos del optimizador. Fragmento literal del recorrido de inferencia: neural.py, líneas 27–38.

with torch.inference_mode():
    for i in range(0,len(texts),batch_size):
        x=torch.from_numpy(self.vectorizer.transform(texts[i:i+batch_size]).toarray())
        out.append(self.model(x).numpy())

Después de entrenar, train_and_encode() recorre todos los 593.067 títulos y escribe una matriz float32 mediante open_memmap: permite producir un archivo grande por bloques. Cada lote aporta sus filas y sus IDs en el mismo orden; se rechazan números no finitos e IDs duplicados. Construcción de la matriz completa: ia_artefactos.py, 121–147.

Los 6.541 fragmentos PDF pasan por el mismo encoder de títulos con pesos fijos, como documenta training_performed=false de la colección PDF. El texto de entrada ahora es un fragmento; no cambia mágicamente el vocabulario ni añade conocimiento a W. Es una transferencia de representación que requiere evaluar su calidad en ese nuevo tipo de texto. Orquestación de PDF: ia_artefactos.py, 334–347.

Matriz entregada Forma Valores almacenados
Catálogo (593067, 1024) 593.067 filas × 1.024 números float32
PDF (6541, 1024) 6.541 filas × 1.024 números float32

Sólo los valores del catálogo ocupan aproximadamente 2,43 GB decimales (593067 × 1024 × 4 bytes), además de la pequeña cabecera NPY. Es tamaño de datos, no RAM adicional medida por consulta ni tamaño de los pesos. La matriz W tiene 4.194.304 parámetros; la matriz de documentos tiene muchísimas más filas porque guarda sus resultados.

Límites visibles de esta representación

El tokenizer conserva como máximo 64 tokens útiles por texto. Un fragmento de 1.600 caracteres puede superar ese límite: la representación densa no necesariamente incorpora todo su contenido. BM25 y la lectura humana sí disponen del texto almacenado del fragmento. Los términos fuera del vocabulario tampoco aportan coordenadas de entrada.

La entrega reporta 7.087 vectores de títulos y 146 de fragmentos PDF con norma inferior a 1e-8, criterio de conteo del kit. Los textos sin señal en el vocabulario pueden producir vectores cero; no deben interpretarse como «tesis sin valor». Permanecen en la colección léxica. Investigar vocabulario, truncamiento y alternativas de encoder puede mejorar cobertura, pero esa mejora todavía debe medirse con consultas y juicios de relevancia.

Volver al índice ↑

CAPÍTULO 10Consulta, búsqueda densa, híbrida y RAG

Cómo se transforma una pregunta en candidatos densos

La búsqueda densa aplica a la consulta el mismo encoder que creó los vectores de la colección y compara números en el mismo espacio de 1.024 dimensiones. Para dos vectores q y d, el producto escalar suma q[i] × d[i] en todas las coordenadas. Con norma L2 igual a 1, ese producto coincide con la similitud coseno. El modelo normaliza sus salidas; un vector cero conserva norma cero y no aporta una coincidencia densa positiva.

Se usa búsqueda exacta por bloques, sin FAISS, ANN ni GPU. Es una implementación inspeccionable: recorre los vectores elegibles y conserva los mejores. Su coste aumenta con el número de registros y dimensiones; los bloques limitan el trabajo intermedio, pero no eliminan el recorrido. Código: DenseIndex.search y Engine.search.

Parámetro o paso Valor aplicado Función y límite
Dimensión 1.024 Debe coincidir entre consulta, pesos y matriz
Almacenamiento float32, vectors.npy Cuatro bytes por coordenada
Lectura mmap_mode='r', allow_pickle=False Matriz en disco con acceso por memoria; no modifica vectores
Tamaño de bloque 4.096 filas Procesa grupos y reúne sus mejores candidatos
Umbral de publicación score > 0 Descarta similitudes no positivas; no es confianza calibrada
Comprobación de normas Cero menor que 1e-8 o próximo a 1 con atol=1e-4 Detecta matrices inválidas antes de usarlas
Orden final Score descendente, desempate por ID Ordenación dentro de la representación aplicada
Cantidad final top_k=5 por defecto; 1–50 en API Número máximo de fuentes, no configuración de muestreo del LLM

Al cargar se comprueban hashes, IDs alineados, forma, tipo numérico y valores finitos. La primera búsqueda puede incluir lectura y verificación costosas. La matriz se abre con mmap, pero el catálogo carga sus metadatos en una lista Python y los IDs permanecen en RAM: no todo el buscador funciona sin ocupar memoria. Código: DenseIndex.init y Engine.load_dense.

En catálogo se eligen los registros permitidos por ámbito, años y facultad antes de multiplicar. El siguiente extracto literal muestra el producto y la retención por bloque:

scores = self.vectors[allowed] @ q
for j in np.argsort(-scores, kind='stable')[:k]:
    if scores[j] > 0:
        candidates.append((float(scores[j]), allowed[j]))

Fuente: DenseIndex.search: producto y candidatos. PDF calcula el producto del bloque y enmascara los fragmentos ajenos a doc_id antes de seleccionar resultados. Una consulta sin representación útil puede no recuperar nada en densa y aun así encontrar coincidencias por BM25. Tampoco un coseno alto demuestra que un fragmento responda la pregunta: esa afirmación necesita evaluación.

Cómo se fusionan las dos listas con RRF

La búsqueda híbrida recupera hasta 50 candidatos BM25 y 50 densos dentro de la colección elegida. RRF —fusión recíproca de rangos— suma una contribución por posición; los rangos comienzan en 1:

RRF(documento) = suma, por cada lista donde aparece, de 1 / (60 + rango)

Un documento ausente de una lista no suma por ella. Ejemplo didáctico, no puntuación medida: posiciones 1 y 3 dan 1/61 + 1/63 ≈ 0,032266. Esto permite combinar órdenes sin mezclar directamente BM25 y coseno. Ambas listas tienen el mismo peso; la constante 60 y la profundidad 50 están fijas en los servicios, no en el formulario.

Código literal que evita sumar dos veces un mismo ID en una lista y conserva las señales originales:

for rank,d in enumerate(ranking,1):
    if d['id'] in used:continue
    used.add(d['id']);scores[d['id']]+=1/(constant+rank);docs[d['id']]=d
    signals[d['id']].append({'method':d.get('retrieval_method'),'rank':rank,'score':d.get('score')})

Fuentes: rrf, líneas 49–57, Service.search: profundidad de las listas y Engine.search: fusión PDF. Después se ordenan contribuciones y se devuelven los primeros top_k. En PDF varios fragmentos de una tesis pueden ocupar distintas posiciones: no se aplicó diversificación por documento. Híbrida tampoco busca simultáneamente en TXT y PDF: fusiona dos métodos dentro de la colección seleccionada. La combinación es una técnica implementada; su mejora de relevancia no está demostrada para toda esta entrega.

Qué recibe el servidor y qué recibe Llama

Las rutas de catálogo y PDF aceptan message entre 2 y 2.000 caracteres, method (bm25, dense, hybrid), top_k de 1–50 y generate falso por defecto. El catálogo admite años y facultad; PDF admite doc_id. Rechazan campos desconocidos y cuerpos mayores que 32 KiB. Las consultas de cada colección tienen dos slots simultáneos por proceso; si se ocupan devuelven HTTP 429. Esto es control de concurrencia, no una capacidad multiusuario certificada. Código: Query y BodyLimit, PDFQuery e install y Service: slots y respuesta.

En la web se muestran primero referencias o fragmentos mediante generate:false. Si se selecciona «Redactar con Ollama», después se envía otra solicitud con generate:true; el servidor vuelve a recuperar y prepara contexto. Las fuentes iniciales permanecen visibles durante la espera y se conservan si la redacción falla. Este flujo puede leerse en SectesisRequests.search.

RAG significa generación aumentada con recuperación: pregunta → búsqueda → evidencia → contexto enviado a Llama → respuesta con citas. Para catálogo, se envían metadatos acotados de hasta diez referencias; para PDF, hasta ocho fragmentos y hasta 2.000 caracteres por extracto. La interfaz pide cinco resultados por defecto. No se envían los 593.067 registros ni todas las tesis a cada pregunta. Construcción del contexto: generate del catálogo y generate de PDF.

Llama redacta; el encoder prepara representaciones para buscar. El LLM se reutiliza ya entrenado y no se ajustaron sus pesos con estas tesis. Las preguntas tampoco entrenan automáticamente modelos. El control de citas comprueba marcas como [1] y pertenencia al conjunto disponible, no respaldo semántico de cada afirmación. Al abrir un PDF, la API compara su SHA con el índice y rechaza una versión modificada; esa trazabilidad permite revisar el original, pero no reemplaza la revisión: file: apertura y huella del original.

Variantes para estudiar después

Comparar otro umbral de similitud, profundidad o constante RRF, ponderaciones distintas por método, agrupación por tesis o recuperación de fragmentos vecinos requiere un experimento nuevo. El adaptador index_pretrained permite preparar catálogo con un SentenceTransformer local, revisión y hashes registrados; no se aplicó en estos artefactos. La vectorización PDF actual sólo acepta el encoder didáctico. ANN, rerankers y embeddings de contexto largo son posibles ampliaciones, no componentes ejecutados aquí. Código de la opción existente: index_pretrained: adaptador local de catálogo; restricción PDF: vectorize: modelo admitido.

Volver al índice ↑

CAPÍTULO 11Evaluación: qué se comprobó y cómo medir relevancia

Funcionamiento, aprendizaje y calidad son evidencias distintas

Evidencia disponible Qué permite afirmar Qué no demuestra
Pruebas de código con datos artificiales Se verifican reglas, errores, contratos y casos controlados Calidad de las tesis entregadas
Pérdida de entrenamiento y validación El encoder optimizó su objetivo contrastivo y se seleccionó una época Porcentaje de respuestas correctas del chat
BUILD_REPORT.json de la entrega Ambas colecciones responden con los tres métodos y se abrió un PDF Relevancia humana, respuesta de Ollama o capacidad concurrente
Comprobaciones HTTPS y navegador Funcionamiento del servicio publicado en las consultas comprobadas Calidad general para cualquier pregunta
Mediciones reales de Ollama Duraciones bajo las condiciones registradas Exactitud científica o velocidad garantizada

El build final utilizó dos consultas derivadas de cada fuente, cada una con hasta cinco de sus primeros tokens útiles. Solicitó tres resultados con generate:false para BM25, densa e híbrida: seis combinaciones colección/método, con dos respuestas en cada una. El reporte registra HTTP 200 y tres citas en cada respuesta, más apertura de un original PDF. La función exige modo extractivo y alguna evidencia por combinación; no compara los resultados contra juicios de relevancia. Se ejecuta mediante ASGI TestClient en el mismo proceso, sin inferencia Ollama. Fuentes: sample_queries: consultas derivadas de fuente y functional_check: comprobación ASGI; resultados en BUILD_REPORT.json dentro de la Release privada.

Evaluador de ranking implementado

evaluate compara recuperación de catálogo con positivos conocidos. Un positivo es un ID que el archivo de casos declara relevante para una pregunta. No evalúa PDF ni generación. Recibe entre 1 y 10.000 casos, verifica IDs existentes en el ámbito y calcula métricas con k=10 por defecto, configurable entre 1 y 50. Un esquema ilustrativo —no un juicio nuevo validado— es:

{
  "id": "pregunta-ejemplo",
  "query": "problema habitacional en México",
  "relevant_ids": ["sectesis:000000001"],
  "human_validated": false
}

Antes de cada medición hace una consulta de calentamiento. Mide después otra llamada secuencial al buscador, sin LLM ni concurrencia. Guarda IDs recuperados, hashes de corpus/casos, métricas por pregunta y promedios. human_validation_declared sólo refleja lo declarado en el JSON; no certifica que una persona revisó las fuentes. Código: evaluate: validación de casos y ejecución; argumentos disponibles: CLI evaluate.

Para leer las fórmulas, k es el máximo de resultados evaluados, H los positivos encontrados entre ellos, P el número total de positivos conocidos y r una posición que comienza en 1:

Métrica implementada Fórmula o cálculo Qué pregunta responde
Recall@k H / P ¿Qué fracción de los positivos conocidos recuperó?
Precision@k H / k ¿Qué proporción de los k lugares corresponde a positivos conocidos?
MRR@k 1 / r del primer positivo; cero si falta ¿Cuán pronto aparece la primera referencia positiva?
DCG@k Suma 1 / log2(r + 1) para cada positivo ¿Los positivos aparecen en lugares altos?
nDCG@k DCG / DCG_ideal ¿Qué tan cercano quedó al mejor orden posible de esos positivos?

El evaluador usa relevancia binaria y elimina IDs duplicados antes de tomar k; Precision@k conserva denominador k aunque haya menos resultados. Ejemplo didáctico: con k=5, tres positivos conocidos y dos recuperados en posiciones 2 y 4, Recall es 2/3, Precision 2/5 y MRR 1/2. DCG es 1/log2(3)+1/log2(5); el ideal sitúa los tres positivos en 1, 2 y 3. Este ejemplo explica el cálculo y no es una medición del proyecto. Implementación literal: metrics, líneas 11–18.

hit=sum(i in positive for i in ids)
dcg=sum(1/math.log2(j+2) for j,i in enumerate(ids) if i in positive)
ideal=sum(1/math.log2(j+2) for j in range(min(k,len(positive))))

Para tiempos, el código guarda p50 como elemento central inferior de las latencias ordenadas y p95 como el elemento ceil(0.95 × n) - 1, usando índices desde cero. Esos percentiles describen sus llamadas secuenciales calientes; no representan carga inicial, generación o múltiples usuarios. No calcula intervalos de confianza ni significancia estadística. Fuente: evaluate: resumen y límites.

Qué resultados históricos se pueden citar

El documento VALIDACION.md: comparación proxy histórica registra 100 consultas proxy de títulos conocidos para el laboratorio de 12.000 registros del 10 de septiembre, con otro hash de muestra. Allí figuran Recall@10/MRR@10/nDCG@10: BM25 1,000/0,995/0,996, densa 0,960/0,874/0,895 e híbrida 1,000/0,964/0,973. Es una comparación histórica declarada, no un benchmark nuevo del catálogo de 593.067 ni de los PDF. Sus consultas conservan palabras de títulos y sólo tienen un positivo conocido, por lo que favorecen coincidencias léxicas y no juzgan exhaustivamente otros documentos útiles.

No se encontró en la entrega actual un benchmark humano de relevancia del catálogo completo ni de los fragmentos. Para demostrar mejora, hace falta construir preguntas independientes, revisar positivos y negativos, fijar particiones y criterios antes de comparar variantes, y publicar también errores y limitaciones. El evaluador existente aporta un punto de partida para catálogo; evaluar PDF y respaldo de afirmaciones de Llama necesita ampliar el protocolo y las herramientas.

Las pruebas test_pdf_extension.py crean PDF artificiales y pesos controlados; test_pdf_repository.py usa data/PDF si existe y verifica extracción, BM25, apertura y reutilización. Separar esos tipos de prueba impide presentar un fixture como resultado de las tesis. Código: test_pdf_extension: creación de datos de prueba y test_pdf_repository: integración con data/PDF.

Volver al índice ↑

CAPÍTULO 12Llama: funcionamiento, parámetros y papel en el proyecto

Llama es el modelo que redacta; Ollama es el programa que lo carga y ejecuta. El buscador entrega unas pocas referencias o pasajes a Llama mediante la API local /api/chat. Los pesos del LLM permanecen fijos durante la consulta. El entrenamiento del encoder de títulos, explicado antes, es un proceso distinto.

Llama 3 es un Transformer autorregresivo: calcula el siguiente token a partir de los anteriores y repite ese paso hasta terminar. La variante Instruct fue preparada por Meta para seguir instrucciones mediante ajuste supervisado y retroalimentación humana; ese trabajo previo no fue realizado por este proyecto. La ficha original describe principalmente uso y evaluación en inglés, por lo que la calidad en español necesita comprobarse con nuestras preguntas. Ficha oficial de Meta Llama 3.

Ficha del modelo realmente instalado

Lectura local del 14 de septiembre de 2026 mediante /api/show y /api/tags, guardada en LLAMA_FICHA_MODELO.json:

Propiedad Valor comprobado Para qué sirve
Nombre configurado llama3:latest Selecciona el modelo local para redactar
Parámetros aprendidos 8.030.261.248, aproximadamente 8,03 mil millones Coeficientes de las transformaciones que procesan y generan texto
Bloques Transformer 32 Aplican sucesivas transformaciones de atención y redes internas
Dimensión interna 4.096 Tamaño de la representación interna de cada token; no es el vector de búsqueda de 1.024 dimensiones
Cabezas de atención / de claves y valores 32 / 8 Atención agrupada GQA; varias cabezas comparten claves y valores
Anchura de la red interna de cada bloque 14.336 Dimensión intermedia de su transformación no lineal
Vocabulario 128.256 tokens Unidades que reconoce o puede emitir; no son 128.256 palabras completas
Contexto declarado por el modelo 8.192 tokens Capacidad nominal; no demuestra que el servidor reserve esa ventana en cada petición
Formato y cuantización GGUF, Q4_0 Empaquetado de tensores y representación de menor precisión
Tamaño instalado 4.661.224.676 bytes, unos 4,66 GB Almacenamiento del paquete; no equivale a RAM total de ejecución
Huella del paquete 365c0bd3c000a25d28ddbf732fe1c6add414de7275464c4e4d1c3b5fcb5d8ad1 Permite identificar exactamente esta versión aunque cambie el alias latest

El mismo registro local informa la opción heredada num_keep=24 y tres marcas stop: <|start_header_id|>, <|end_header_id|> y <|eot_id|>. Son configuración del paquete; el proyecto no las aprendió ni las cambió. La aplicación tampoco envía num_keep explícitamente. Para un ensayo que altere la ventana o la plantilla, habría que registrar estas opciones heredadas además de las que se envían en cada petición.

Un parámetro aprendido es un número de una matriz o vector del modelo. Por ejemplo, los pesos de atención transforman una representación en consultas, claves y valores; las redes internas combinan características; la salida asigna puntuaciones al vocabulario. No existe una correspondencia «un peso = una tesis»: el conocimiento aprendido está distribuido. Los 8,03 mil millones de Llama tampoco son los 4.194.304 parámetros del encoder local.

Recorrido de una respuesta

  1. El sistema recupera evidencia con BM25, vectores o RRF y asigna marcas como [1] y [2].
  2. El código construye un mensaje de sistema y un JSON con la pregunta y esa evidencia. Para catálogo incluye metadatos; para PDF incluye pasajes y páginas.
  3. Ollama aplica la plantilla de conversación del modelo. El tokenizer convierte el texto en IDs; Llama los transforma en representaciones internas y procesa el contexto.
  4. La atención relaciona cada posición con posiciones anteriores. La caché KV conserva claves y valores para evitar recalcular todo el prefijo en cada paso de generación.
  5. Llama produce puntuaciones para el siguiente token; la estrategia de decodificación elige uno, lo añade a la secuencia y continúa. Un token puede ser parte de una palabra, un signo o una unidad especial.
  6. El servicio recibe el texto y comprueba la sintaxis de las citas. Esa comprobación no demuestra que una afirmación esté respaldada: la persona debe revisar las fuentes.

En forma esquemática: pregunta + evidencia → tokens → representaciones → atención y redes internas → siguiente token → respuesta citada. Una salida fluida no implica comprensión humana, veracidad o lectura de todas las tesis.

Por qué se usa Llama aquí

La razón operativa comprobable es que llama3:latest ya estaba disponible y funcionando en el servidor. Permite mantener la generación local, inspeccionar la configuración y reutilizar la misma API para ensayar otros modelos. Sirve como referencia inicial reproducible y evita introducir una dependencia de una API externa para redactar. No se eligió mediante una comparación que demostrara que es el mejor modelo para bibliotecas o para español.

Para el usuario, su aporte es organizar una explicación breve apoyada en los resultados. Para la investigación, permite separar dos preguntas: «¿recuperamos los pasajes correctos?» y «¿el modelo los usa fielmente?». Entrenar otro LLM desde cero no es un requisito para estudiar esas preguntas; aquí se reutilizan sus pesos y se controla el contexto.

Volver al índice ↑

CAPÍTULO 13Configuración de Llama: qué se ajusta y qué permanece fijo

Los parámetros de ejecución son opciones; cambiarlos no entrena ni modifica los pesos. Esta tabla distingue lo que el código envía de lo que sería una variable experimental:

Opción Estado en este proyecto Uso e interpretación
model OLLAMA_MODEL=llama3:latest Elige el paquete local que redactará
messages Un mensaje de sistema y otro con pregunta/evidencia Define instrucciones y material disponible
temperature 0 en ambas colecciones Favorece una selección determinista; no garantiza exactitud ni igualdad entre versiones/hardware
num_predict 400 para catálogo, 500 para PDF Límite de tokens nuevos; puede terminar antes o truncar la respuesta al alcanzarlo
stream false La aplicación recibe la redacción completa; mostrar antes las fuentes no es streaming de tokens
keep_alive 15 minutos en este servidor Conserva temporalmente el modelo cargado
num_ctx No enviado explícitamente por el código Ventana efectiva de trabajo; depende del runtime/configuración y debe registrarse al experimentar
seed No enviado explícitamente Control de aleatoriedad para ensayos reproducibles
top_k del modelo No enviado explícitamente Limita los candidatos de token durante muestreo
top_p No enviado explícitamente Restringe candidatos por masa de probabilidad acumulada
repeat_penalty No enviado explícitamente Modula la repetición; un valor excesivo puede perjudicar nombres o citas
stop Heredado del paquete instalado Marcas especiales que terminan un turno; no sustituyen el control de citas

Las opciones de muestreo se describen en la referencia de parámetros de Ollama; messages, stream y keep_alive, en la API de conversación. No se atribuyen valores predeterminados actuales a una versión antigua del runtime sin medirlos.

Hay dos top_k diferentes: el campo «Resultados» de esta web selecciona referencias del buscador; no cambia options.top_k del modelo. De forma similar, temperature=0 decide cómo generar texto, mientras que la temperatura 0,1 del entrenamiento contrastivo pertenecía a la pérdida del encoder.

Ejemplo reducido de la estructura que prepara el código —las descripciones entre corchetes son marcadores explicativos, no una petición de prueba ejecutada—:

{
  "model": "llama3:latest",
  "stream": false,
  "keep_alive": "15m",
  "options": {"temperature": 0, "num_predict": 500},
  "messages": [
    {"role": "system", "content": "[Instrucciones: español, sólo evidencia y citas]"},
    {"role": "user", "content": "[JSON con pregunta, fragmentos reales y páginas]"}
  ]
}

Cambiar OLLAMA_MODEL seleccionaría otro modelo ya instalado. El formulario actual no expone num_ctx, seed ni muestreo; experimentar con ellos requiere un cliente de laboratorio o una modificación explícita del código. Esta revisión de documentación no cambia esas opciones ni descarga modelos.

Volver al índice ↑

CAPÍTULO 14Recursos y comparación de modelos candidatos

La consulta local observó 8 CPU lógicas y aproximadamente 31,2 GiB de RAM física. Son recursos compartidos: el proceso Ollama vive en el host y el contenedor del buscador tiene su propio límite de 4 CPU y 14 GiB. No debe interpretarse ese límite del contenedor como RAM disponible para Llama. El índice vectorial de catálogo contiene unos 2,43 GB y también compite por memoria y caché del sistema.

La cuantización reduce los bits usados para muchos pesos. Como cálculo orientativo, memoria de pesos ≈ parámetros × bits / 8: 8,03 mil millones a 16 bits serían unos 16,06 GB y a 4 bits unos 4,02 GB, antes de escalas, metadatos, tensores con otra precisión y otros gastos. Por eso Q4_0 no significa que todo el proceso ocupe exactamente cuatro bits por parámetro. En una medición anterior el runtime reportó unos 5,34 GB cargados y 0 bytes de VRAM; se utilizó CPU. En la inspección de metadatos de esta revisión no había un modelo cargado: no se confunde esa lectura con un nuevo ensayo de rendimiento.

La RAM/VRAM necesaria suma pesos, caché KV, buffers y concurrencia. Un contexto mayor consume más memoria; anunciar una ventana larga no prueba que quepa con el resto de la aplicación. La ventana efectiva se debe consultar con el modelo cargado, por ejemplo mediante /api/ps, y no deducirla sólo de la ficha. Contexto en Ollama, modelos cargados y contexto efectivo.

Comparación documental, no benchmark ejecutado

Los tamaños siguientes corresponden a los tags consultados el 14 de septiembre de 2026. Son paquetes en disco, no requisitos certificados de RAM. Los candidatos son una selección manejable para comparar familias y tamaños; no se presentan como los modelos más recientes ni como un ranking universal.

Modelo y fuente Parámetros / cuantización / paquete Ventaja para investigar Limitación o hipótesis por comprobar
Llama 3 8B, referencia actual 8,03B · Q4_0 · 4,66 GB locales Ya integrado; permite comparar contra un punto de partida medido Usa CPU y la ficha original se centra en inglés; falta evaluar fidelidad en español
Llama 3.2 3B 3,21B · Q4_K_M · 2,0 GB publicados Menor paquete y español entre sus idiomas documentados Hipótesis de menor consumo; falta medir si conserva citas y detalles con menos parámetros
Qwen 2.5 3B 3,09B · Q4_K_M · 1,9 GB publicados Otra familia multilingüe para contrastar seguimiento de instrucciones y datos estructurados Su rendimiento sobre estas tesis es desconocido; revisar plantilla y condiciones específicas de la variante 3B
Gemma 3 4B 4,3B · Q4_K_M · 3,3 GB publicados Otra arquitectura multilingüe, con capacidad de imagen además de texto El proyecto envía texto extraído, por lo que no aprovecha visión; falta medir memoria y calidad local
Llama 3.1 8B 8,03B · Q4_K_M · 4,9 GB publicados Comparación de generación y soporte multilingüe con tamaño cercano al actual No se presupone una reducción de latencia; una ventana mayor exige más recursos

Menos parámetros no garantizan mejor tiempo ni peor calidad. También influyen arquitectura, cuantización, tokenizer, longitud generada, implementación y hardware. Q4_0 y Q4_K_M no son formatos idénticos: una comparación entre esos paquetes evalúa la configuración completa, no aísla sólo la familia. El número de tokens tampoco es directamente comparable entre tokenizers; registrar además longitud de respuesta y tarea resuelta.

Como secuencia de investigación, probar primero Llama 3.2 3B contra la referencia permitiría explorar el intercambio entre consumo y fidelidad; Qwen y Gemma ampliarían la comparación entre familias, y Llama 3.1 ayudaría a estudiar una variante de tamaño cercano. Es una propuesta, no una recomendación basada en resultados locales de esos modelos. Reutilizar la API no garantiza compatibilidad de todas las plantillas ni de las citas sin comprobarlas.

Volver al índice ↑

CAPÍTULO 15Mediciones de Llama y condiciones de ejecución

La evidencia disponible es de llama3:latest en CPU. Dos solicitudes breves consecutivas, con el mismo contexto y el modelo ya cargado, dieron:

Medición real Primera solicitud Repetición del mismo contexto
Tiempo total 12,841 s 8,412 s
Carga reportada 0,084 s 0,074 s
Procesar contexto 4,636 s 0,178 s
Generar 47 tokens 8,120 s 8,158 s
Velocidad de salida 5,79 tokens/s 5,76 tokens/s

Es una medición breve, no una promesa para cualquier consulta. La segunda solicitud puede reutilizar contexto; aquí no se midió una carga inicial en frío. Varios fragmentos largos requieren procesar muchos más tokens. Los límites existentes permiten hasta 400 tokens de salida para catálogo y 500 para PDF; no siempre se llega a esos máximos. Las métricas separan carga, contexto y generación. Métricas de Ollama.

La web anterior cancelaba catálogo a los 65 segundos y PDF a los 90, mientras el servidor permitía 180 segundos a Ollama. La web ahora separa recuperación y redacción: concede 90 segundos a recuperar y 210 a la solicitud de generación, y muestra el tiempo transcurrido. El servidor conserva su timeout de 180 segundos para Ollama y Nginx permite 210. Mostrar fuentes antes reduce la espera para comenzar a leer; no hace que la CPU genere tokens más rápido.

OLLAMA_KEEP_ALIVE=15m permite conservar temporalmente el modelo cargado entre consultas de este servidor. El código mantiene 5m como valor predeterminado para otras instalaciones. Retenerlo reduce recargas, a cambio de ocupar RAM durante más tiempo; no cambia ni entrena el modelo. La opción se transmite a la API local. Parámetro keep_alive de Ollama.

Las duraciones que devuelve Ollama se expresan en nanosegundos. La velocidad de salida se calcula como eval_count / (eval_duration / 1e9); la del contexto usa prompt_eval_count y prompt_eval_duration. El tiempo total observado también puede incluir sobrecarga y espera. La medición anterior de 24,577 s desde el navegador correspondió a otra pregunta de catálogo con una referencia; no se mezcla con estas dos ejecuciones como si fueran el mismo ensayo. Interpretación de las métricas.

No hay tiempos locales medidos para los cuatro candidatos de la tabla. No se inventa una estimación del tipo «un modelo 3B tardará la mitad»: eso es precisamente lo que debe comprobar el experimento.

Volver al índice ↑

CAPÍTULO 16Experimentos propuestos: calidad, recursos y tiempos

Estado: protocolo propuesto; no ejecutado en esta actualización. Las dos mediciones anteriores de Llama sí se ejecutaron; la comparación entre modelos, la evaluación humana y las pruebas de carga siguientes están pendientes. No se descargaron modelos, no se entrenó Llama y no se cambió el modelo del chat.

Preguntas e hipótesis

Experimento Qué se cambia Qué se conserva Hipótesis que se pondría a prueba
E0 · Fuentes sin LLM Desactivar redacción Pregunta, recuperación y evidencia El usuario puede resolver ciertas consultas sólo con fuentes, con menor espera
E1 · Modelo Referencia y cuatro candidatos Evidencia ordenada, instrucciones y objetivo de respuesta breve Un modelo menor podría reducir recursos manteniendo fidelidad suficiente
E2 · Cantidad de contexto 1, 3 y 5 referencias/pasajes de una lista fijada Modelo, consulta y configuración de generación Más evidencia podría mejorar cobertura, pero aumentar preprocesamiento y distracciones
E3 · Decodificación Una opción a la vez: límite de salida o temperatura Modelo, pregunta y contexto La longitud permitida afecta coste; más variación no implica mejor respaldo
E4 · Estado de carga Primera carga, modelo caliente con contexto nuevo y repetición del mismo contexto Hardware y versión del paquete Separar coste de carga, procesamiento del contexto y reutilización del prefijo
E5 · Concurrencia 1 y 2 solicitudes simultáneas Modelo, preguntas y límites La capacidad compartida puede afectar latencia, colas y errores

Los ensayos que descarguen paquetes, retiren el modelo de RAM o generen carga se prepararían en un entorno de laboratorio separado del servicio público. Cambiar la familia de LLM en E1 no necesita reconstruir los índices: se congelan los IDs, el orden y el texto de la evidencia para aislar la redacción. En E2 sí cambia deliberadamente la cantidad de evidencia.

Protocolo reproducible

  1. Preparar 30 preguntas reales: 12 respondibles mediante catálogo, 12 mediante pasajes PDF y 6 donde falte evidencia. Un bibliotecario define qué datos o pasajes respaldan cada respuesta y cuándo debería abstenerse el modelo.
  2. Reservar 20 para exploración y 10 para evaluación final, con distribución 8/8/4 y 4/4/2 por esos grupos. Guardar la lista y una semilla de partición. No elegir el modelo por el resultado del conjunto final.
  3. Registrar commit, hashes de índices/evidencia, digest del modelo, cuantización, plantilla, versión de Ollama, CPU/GPU, RAM/VRAM y opciones efectivas. Para cada condición, mantener estable el resto del sistema.
  4. Usar instrucciones equivalentes en español y el mismo contenido de evidencia. Fijar una ventana que acomode pregunta, instrucciones, evidencia y salida en todos los modelos; medir tokens con cada tokenizer y registrar cualquier truncamiento. En la primera comparación se podría ensayar num_ctx=4096, sólo después de comprobar que todos los ejemplos caben.
  5. Hacer tres repeticiones por pregunta y condición, alternando el orden de los modelos. Separar carga inicial, contexto nuevo y contexto repetido. Conservar también errores y respuestas truncadas, no sólo las ejecuciones rápidas que salieron bien.
  6. Medir tiempo hasta las fuentes, tiempo total de la respuesta, carga, preprocesamiento, generación, tokens de entrada/salida y memoria máxima. Para tiempo al primer token haría falta un cliente con stream=true; la interfaz actual no lo mide.
  7. Presentar las respuestas sin identificar el modelo a dos revisores cuando sea posible. Evaluar respaldo de afirmaciones, correspondencia de citas, cobertura, claridad y abstención; resolver desacuerdos y conservar denominadores.
  8. Publicar mediana y percentil 95 por condición junto con dispersión y fallos. Con pocos casos el percentil 95 es inestable; no extrapolar a toda la población de usuarios. Comparar calidad y tiempo por la misma pregunta, no sólo promedios de textos diferentes.

Ejemplos para construir preguntas: el registro real sectesis:000000001 permite pedir autor y año; el pasaje de 0609550.pdf, página 33, permite preguntar qué actividades menciona. Pedir resultados o conclusiones ausentes de esos materiales sirve para evaluar abstención. Las respuestas esperadas deben revisarse sobre la fuente completa disponible antes de usarlas como referencia.

Cómo interpretar un resultado

Una cita con formato correcto no basta. Una medida útil es «afirmaciones respaldadas / afirmaciones verificables», acompañada de cobertura de los puntos esperados y abstenciones correctas. Registrar también la proporción de citas que llevan al pasaje que respalda la afirmación. Una mejora de velocidad sería aceptable sólo si cumple criterios de calidad definidos antes del ensayo; los umbrales deben acordarse con quienes usarán el sistema, no inventarse después de ver los resultados.

El entregable del experimento sería una tabla por modelo con tiempo, memoria, respaldo, cobertura y errores, más ejemplos revisados y configuración reproducible. Hoy las casillas de los candidatos se marcarían «pendiente de medir». La ventaja de este proyecto para investigar IA aplicada es que ya separa fuentes, recuperación, representación y generación, lo que permite estudiar cada parte sin atribuir a Llama el trabajo del índice.

Volver al índice ↑

CAPÍTULO 17Contrato de la API y recorrido de una petición

Una API es la interfaz que permite a un programa pedirle una operación a otro mediante mensajes definidos. En este proyecto, el navegador envía JSON a FastAPI; el servidor valida sus campos, consulta la colección elegida y devuelve otro JSON con resultados y procedencia. Esta separación permite probar la recuperación sin abrir la página y estudiar sus parámetros de forma reproducible.

Entrada admitida y valores por defecto

Campo o control Valor y rango implementado Dónde se aplica
message Texto de 2 a 2.000 caracteres; se recortan extremos y se rechazan controles no admitidos Ambas colecciones
method bm25, dense o hybrid; la API usa bm25 si se omite La web selecciona inicialmente híbrida, por lo que el valor inicial del formulario difiere del valor por defecto de la API
top_k Entero, 1–50; predeterminado 5 Cantidad de resultados; no es el top_k del muestreo de Llama
generate Booleano; predeterminado false Sólo se solicita Llama al activarlo y si hay resultados
year_min, year_max Opcionales, enteros 1500–9999; se rechaza rango invertido Sólo catálogo
faculty Opcional, 1–300 caracteres; coincidencia exacta normalizada Sólo catálogo
doc_id Opcional, pdf- seguido de 64 caracteres hexadecimales Sólo PDF; limita la búsqueda a ese documento
Campos adicionales extra='forbid' Se rechazan; no se interpretan como instrucciones nuevas para el servidor
Cuerpo HTTP Hasta 32.768 bytes, incluidos cuerpos enviados por partes Middleware de las rutas /api/v1/sectesis; responde 413 si se excede

Los modelos Query y BodyLimit y PDFQuery implementan estos controles. El valor inicial de la web está en index.html. Fragmento literal del contrato de catálogo:

class Query(BaseModel):
    model_config=ConfigDict(extra='forbid')
    message:str=Field(min_length=2,max_length=2000)
    method:Literal['bm25','dense','hybrid']='bm25'
    top_k:int=Field(default=5,ge=1,le=50,strict=True)

Ejemplo de petición reproducible a la instalación local sin clave, no una respuesta inventada ni un ensayo nuevo ejecutado al escribir este ejemplo:

curl --fail http://127.0.0.1:8911/api/v1/sectesis/search \
  -H 'Content-Type: application/json' \
  -d '{"message":"problema habitacional México","method":"hybrid","top_k":5,"generate":false}'

Para PDF se usa /api/v1/sectesis/pdf/search, con el mismo núcleo de campos y, opcionalmente, doc_id. La separación de endpoints conserva la distinción entre una ficha bibliográfica y un pasaje del texto. No hay una consulta simultánea de las dos colecciones en ese contrato.

Del envío a las fuentes visibles

  1. Validación y acceso. FastAPI valida el JSON. Si la instalación configura una clave, authorize comprueba su Bearer; con clave vacía, como en este servidor público, permite consultar sin ella. Ese mecanismo de acceso no es tokenización lingüística.
  2. Capacidad disponible. El catálogo admite hasta dos peticiones simultáneas en su semáforo; PDF tiene otro semáforo de dos. El control cubre recuperación y generación. Si se ocupa, responde 429 con Retry-After: 2; no crea una cola persistente. Son límites del software, no una demostración de cuántos usuarios soportará sin degradarse.
  3. Recuperación. El servicio ejecuta el método y los filtros. El índice denso se carga bajo demanda, verifica correspondencia de corpus y vectores y conserva el objeto en memoria. Por eso la primera búsqueda puede incluir trabajo que no aparece en las siguientes.
  4. Respuesta extractiva. Devuelve referencias o pasajes aunque no se haya solicitado generación. Cada fuente lleva ID y número de cita; PDF añade página y endpoint del original. El JSON identifica answer_mode, dataset_sha256, retrieved_count, source_status, trace_id y latency_ms.
  5. Redacción opcional. Si se pidió, se prepara el contexto acotado de la siguiente subsección. Una falla de generación devuelve advertencia y conserva la respuesta extractiva.

Implementación: authorize y rutas de catálogo, Service.search y Service.answer y ruta PDF search. El UUID trace_id identifica una petición; no demuestra almacenamiento de conversaciones. latency_ms incluye las operaciones del servidor para esa petición, incluida generación cuando procede; no es exclusivamente tiempo de cómputo del modelo.

La web aplica una secuencia adicional: primero envía generate:false, muestra fuentes y sólo después envía generate:true si fue solicitado. Extracto literal de SectesisRequests.search:

const evidence = await post(endpoint, {...body, generate:false}, headers, retrievalTimeout);
onResult(evidence);
if (!body.generate || !evidence.citations.length) return evidence;

La segunda petición vuelve a recuperar en el servidor: no recibe los primeros resultados mediante un identificador de caché. El comportamiento es coherente con artefactos inmutables, pero el programa no compara automáticamente la identidad de las dos listas. El navegador espera hasta 90 segundos en recuperación y 210 en generación, muestra el tiempo transcurrido cada segundo y conserva la primera evidencia si falla la segunda petición. No recibe tokens del LLM uno a uno: el cuerpo enviado a Ollama utiliza stream:false.

Evidencia realmente enviada a Llama

La aplicación no manda toda la base de datos. Selecciona y limita campos antes de preparar el JSON:

Colección Máximo de fuentes enviadas Campos y recortes
Catálogo 10 Título hasta 1.500 caracteres, hasta 5 autores de 150 caracteres, año y hasta 5 materias de 200 caracteres
PDF 8 Número de cita, archivo, página y pasaje hasta 2.000 caracteres; los fragmentos activos tienen límite de preparación de 1.600

Por tanto, pedir 50 resultados no hace que Llama lea 50 referencias. En catálogo no se envían el PDF completo, facultad, grado ni lexical_text en ese contexto. En PDF no se envían imágenes o el archivo binario. Estos recortes controlan longitud por caracteres y por cantidad de fuentes; no calculan el presupuesto exacto de tokens del modelo ni garantizan por sí solos que quepa cualquier contexto.

Extracto literal de generate del catálogo:

context = [{'citation':i, 'title':(d.get('title') or '')[:1500],
            'authors':[str(a)[:150] for a in (d.get('authors') or [])[:5]],'year':d.get('year'),
            'subjects':[str(s)[:200] for s in (d.get('subjects') or [])[:5]]}
           for i,d in enumerate(evidence[:10],1)]

Extracto literal de generate de PDF:

context = [{"citation": c["citation"], "file": c["file"], "page": c["page"], "passage": c["excerpt"][:2000]}
           for c in citations[:8]]

La petición de PDF incorpora esas fuentes y las opciones de ejecución mediante este extracto literal de la misma función:

body = {"model": model, "keep_alive": os.getenv("OLLAMA_KEEP_ALIVE", "5m"), "stream": False, "options": {"temperature": 0, "num_predict": 500},
        "messages": [{"role": "system", "content": system},
                     {"role": "user", "content": json.dumps({"question": question, "evidence": context}, ensure_ascii=False)}]}

Se envían un mensaje de sistema y otro con {question, evidence}. No se envía historial de turnos, aunque la interfaz tenga apariencia de chat. keep_alive conserva temporalmente el modelo cargado; no agrega memoria conversacional a la aplicación. El mensaje de sistema pide español, evidencia y citas y declara que los documentos son datos, no instrucciones. Es una medida de diseño, no una garantía formal contra cualquier respuesta incorrecta o instrucción maliciosa dentro de una fuente.

Qué verifica la respuesta y qué debe revisar una persona

Las funciones limitan la respuesta HTTP a 2.000.000 de bytes acumulados y el texto a 20.000 caracteres. Exigen al menos una marca de cita y que los números correspondan a las fuentes enviadas. Fragmento literal de validación en catálogo:

citations = [int(v) for v in re.findall(r'\[(\d+)\]',text)]
if not citations or any(c<1 or c>len(context) for c in citations):
    return {'ok':False,'error':'Citas ausentes o fuera de la evidencia'}
return {'ok':True,'text':text,'model':model,'citation_check':'syntax_only'}

PDF comprueba pertenencia a sus números de cita con la misma limitación conceptual. Esto no verifica que cada afirmación tenga respaldo, que todas estén citadas, que no se contradigan las fuentes o que se responda bien la pregunta. El modo llm_generated_unreviewed lo identifica como texto sin revisión humana. Una cita sintácticamente válida puede acompañar una afirmación errónea; la comprobación científica requiere contrastar contenido y fuente.

Al abrir el PDF, la ruta files busca un documento indexado, comprueba que la ruta esté dentro del directorio configurado, rechaza enlaces simbólicos y compara SHA-256 del archivo con el índice. Un PDF cambiado devuelve 409 para evitar presentar otra versión como la fuente indexada. El hash prueba correspondencia de bytes, no calidad de extracción o veracidad del contenido.

Volver al índice ↑

CAPÍTULO 18Implementación en el servidor: instalación, configuración y entrega

El proyecto funciona en https://209.126.127.144/demo/sectesis. El código está en VMOsornio/IA, rama main, dentro de /opt/sites/ia-asistente-virtual/ia-unam. La aplicación se ejecuta en un contenedor; los índices y pesos se prepararon previamente y se montan como archivos persistentes. Consultar no vuelve a entrenar ni a indexar.

Cómo se implementó en este servidor

  1. Código y entorno. Se trabajó desde la rama que incorporaba PDF y después se integró la versión validada en main. Se reutilizaron Docker y el Ollama local existente. Dockerfile.final construye un entorno Python 3.11 con las dependencias fijadas del proyecto; WITH_NEURAL=1 incluye el soporte del encoder local.
  2. Preparación de datos. Se conservó el catálogo completo de 593.067 registros. Se preparó la selección de 12.000 para el entrenamiento, se filtraron y dividieron los títulos, y 8.750 actualizaron los pesos durante cuatro épocas. Se eligió la tercera por validación y se vectorizó todo el catálogo con esos pesos fijos.
  3. Preparación de PDF. Se revisaron 50 originales; la extracción previa produjo 6.541 fragmentos de 49 archivos. El build final reutilizó esas 49 extracciones compatibles, reconstruyó el SQLite BM25 y calculó vectores con el encoder de títulos recién entrenado. La cobertura es parcial en 32 PDF. FAQS1.pdf no produjo texto extraíble. Los PDF y el LLM no se usaron para entrenar otro modelo.
  4. Persistencia y contenedores. La entrega preparada quedó en artifacts/entrega-servidor-entrenada-v1/payload. El servicio monta sus archivos como sólo lectura. El contador de visitas usa otra base SQLite en un volumen escribible, sin modificar los índices bibliográficos.
  5. Publicación del servicio. Docker Compose ejecuta el buscador y un relay privado hacia Ollama. Nginx recibe HTTPS y reenvía a la aplicación en loopback. El acceso público sin clave fue autorizado por el propietario.
  6. Comprobación y traslado. Se comprobaron ambas colecciones con BM25, densa e híbrida, la apertura de un PDF original y la generación real de Ollama por separado. El kit empaquetó índices y pesos en una Release privada, verificó los archivos descargados con SHA-256 y se comprobó una restauración en un directorio nuevo.

Qué hace cada componente

Componente Función en la instalación real Dónde se conecta o persiste
Nginx y HTTPS Recibir al navegador y publicar la web/API IP pública 209.126.127.144, puerto 443; renovación automática del certificado
Servicio sectesis Servir la interfaz, buscar y preparar el contexto de generación 127.0.0.1:8911 en el host → puerto 8911 del contenedor
SQLite e índices vectoriales Recuperar registros y fragmentos ya preparados Montajes de sólo lectura /data, /pdf_index y /pdf_vectors
PDF originales Permitir abrir la fuente y su página /pdf_raw, sólo lectura
Relay privado Conectar el contenedor con Ollama en el host 172.17.0.1:11435127.0.0.1:11434, direcciones propias de este servidor
Ollama y Llama Generar texto opcional a partir de la evidencia Servicio local existente; llama3:latest
Contador de visitas Conservar visitas y presencia reciente Volumen ia-unam-servidor_visitors/state/visitors.sqlite

Recorrido de una consulta: navegador → HTTPS/Nginx → API → índice seleccionado → fuentes visibles → Ollama opcional → respuesta. El PDF completo permanece disponible para abrirlo, pero a Llama se envían únicamente los pasajes recuperados y acotados.

La aplicación usa un usuario sin privilegios, raíz de contenedor de sólo lectura y permisos limitados. La configuración local limita el buscador a 4 CPU y 14 GiB; Ollama es otro proceso del host. Los certificados, la configuración particular del servidor y los registros operativos permanecen fuera del repositorio y de la Release. La IP de Docker y el relay deben adaptarse si se instala en otro equipo.

Parámetros de ejecución y separación de recursos

Configuración Valor común del repositorio Valor aplicado o precisión de esta instalación
Python / neural python:3.11-slim, WITH_NEURAL=1 El reporte de construcción registró Python 3.11.16 y PyTorch 2.10.0+cpu
Hilos de cálculo OMP, OpenBLAS y MKL: 2 Son variables de las bibliotecas del contenedor, no límite de todos los procesos del host
Usuario UID/GID 10001 La aplicación no corre como root dentro del contenedor
Red del buscador 127.0.0.1:8911 en el host Nginx publica el acceso HTTPS; el puerto de aplicación no se enlaza a todas las interfaces
Recursos del buscador Sin límites CPU/RAM en compose.final.yml Override local: 4 CPU y 14 GiB; Ollama corre aparte en el host
Sistema de archivos Raíz y corpus de sólo lectura; /tmp temporal /state escribible exclusivamente para el contador
Procesos / capacidades pids_limit:256, cap_drop:ALL, no-new-privileges:true Límites del contenedor principal
Healthcheck Cada 30 s; timeout 5 s; 3 reintentos Llama a /health; comprueba vida del proceso, no ejecuta todos los métodos
OLLAMA_TIMEOUT_SECONDS 45 s 180 s en .env.servidor; opción de HTTPX para operaciones de conexión/lectura/escritura, no plazo global de toda interacción web
OLLAMA_KEEP_ALIVE 5m 15m en este host; retiene el modelo sin entrenarlo
Relay de Ollama No incluido en Compose común Local: espera conexión hasta 5 s y transferencia hasta 180 s; puente privado hacia loopback
Acceso Clave opcional, vacía por defecto Público sin clave por autorización del propietario

Fuentes versionadas: Dockerfile.final, Compose: variables, montajes y límites y cliente HTTP de generación. Los valores particulares proceden de los archivos locales compose.servidor.local.yml, .env.servidor —sólo variables no secretas revisadas— y operacion-local/ollama_relay.py.

La distinción entre liveness y readiness evita una conclusión equivocada: health devuelve status:'alive' y production_ready:false. El estado del catálogo verifica el manifiesto y muestra dense_loaded; el de PDF comprueba integridad y conteos. Un ready:true no certifica relevancia, funcionamiento del LLM o preparación institucional completa. Es necesario ejecutar las comprobaciones correspondientes a cada capa.

El reporte de artefactos registra además NumPy 2.3.5, scikit-learn 1.8.0, PyMuPDF 1.26.7, FastAPI 0.128.2 y HTTPX 0.28.1. Identifican la ejecución que generó la entrega. Reproducir la misma metodología no implica que cualquier reconstrucción futura tenga bytes idénticos: también deben registrarse commit, versiones, hashes e imagen; un tag como python:3.11-slim puede cambiar con el tiempo.

Visitas, presencia reciente y persistencia

El contador es un componente operativo distinto de la IA. El navegador crea un identificador aleatorio en localStorage y otro por apertura de página. Envía presencia cada 30 segundos mientras está visible; el servidor cuenta como «en línea» las identidades vistas en los últimos 90 segundos. Una recarga puede sumar otra visita. Varias pestañas con el mismo almacenamiento comparten identidad; otro navegador o dispositivo puede contar aparte.

VisitorCounter.record utiliza SQLite con transacción BEGIN IMMEDIATE, hashes de los IDs, un total acumulado, presencia temporal y deduplicación de aperturas durante 86.400 segundos. El cliente visitors.js controla almacenamiento, visibilidad y periodicidad. Fragmento literal del cálculo de presencia:

ONLINE_SECONDS = 90
VIEW_RETENTION_SECONDS = 86400

Por tanto, «personas en línea» es una estimación por navegador y ventana temporal, no personas verificadas o usuarios únicos. Estas tablas no guardan consultas ni IP; tampoco eliminan por sí mismas bots o automatizaciones. Las pruebas de navegador cuentan como aperturas. El volumen ia-unam-servidor_visitors conserva el total entre reinicios; no se mezcla con el SQLite bibliográfico ni con pesos o vectores.

Rutas y variables de esta instalación

En la tabla, las rutas relativas parten de /opt/sites/ia-asistente-virtual/ia-unam. El archivo local .env.servidor contiene las rutas absolutas y tiene permisos 0600.

Variable Ruta de origen en este servidor Ruta dentro de la aplicación
SECTESIS_ARTIFACTS artifacts/entrega-servidor-entrenada-v1/payload/sectesis /data
SECTESIS_PDF_ARTIFACTS artifacts/entrega-servidor-entrenada-v1/payload/pdf /pdf_index
SECTESIS_PDF_INPUT_DIR data/PDF /pdf_raw
SECTESIS_PDF_VECTORS artifacts/entrega-servidor-entrenada-v1/payload/pdf-vectors /pdf_vectors
SECTESIS_PDF_DENSE_DIR Valor de configuración /pdf_vectors Indica dónde leer los vectores PDF
SECTESIS_VISITORS_DB Volumen persistente administrado por Docker /state/visitors.sqlite

compose.final.yml aporta la configuración común. Este servidor añade compose.servidor.local.yml para la imagen publicada, los límites y el relay. operacion-local/compose.sh fija el nombre ia-unam-servidor, carga .env.servidor y ambos archivos Compose. Los archivos bajo operacion-local/ son de operación de este host y no se obtienen al clonar.

Valores de generación utilizados aquí: OLLAMA_HOST=http://host.docker.internal:11435, OLLAMA_MODEL=llama3:latest, OLLAMA_TIMEOUT_SECONDS=180 y OLLAMA_KEEP_ALIVE=15m. La primera dirección sólo funciona porque el relay está configurado. SECTESIS_API_TOKEN= vacío corresponde al acceso público de esta instalación; no es una clave que deba compartirse.

Para comprobar y operar este servidor ya instalado, desde su directorio:

cd /opt/sites/ia-asistente-virtual/ia-unam
./operacion-local/compose.sh config --quiet
./operacion-local/compose.sh ps
./operacion-local/compose.sh logs --tail 50 sectesis

Al publicar una imagen previamente construida y validada, se actualiza su referencia local y se aplica con ./operacion-local/compose.sh up -d --no-build --pull never --wait sectesis. Esa operación recrea el servicio cuando cambia la imagen; no reconstruye índices. Un git pull por sí solo actualiza archivos del checkout, pero no sustituye la imagen que ya está ejecutándose.

Cómo el kit convirtió los artefactos en una entrega verificable

El kit es una automatización de los módulos explicados en esta guía. No reemplaza el entrenamiento o los índices con una técnica distinta: invoca sus funciones y ordena preparación, comprobación, empaquetado, publicación y restauración.

Etapa y función Acción implementada Evidencia o límite
source_identity Verifica raíz Git y ausencia de cambios rastreados pendientes; registra commit y hashes de fuentes seleccionadas No es un inventario de todos los archivos del host; la lista SHA incluye Python, requirements, Compose y Dockerfile
build Exige destino nuevo; reutiliza catálogo/pesos o prepara TXT y entrena si se solicita; indexa y vectoriza PDF --retrain es explícito; en esta entrega sí se entrenaron títulos. No modifica pesos del LLM
functional_check ASGI TestClient contra la aplicación real, dos consultas derivadas de títulos/pasajes por colección, tres métodos, top_k:3, generate:false Comprueba funcionamiento sobre datos reales; no es un benchmark humano ni ejecución de Ollama
package Tar.gz, nivel de compresión 3; partes de 128 MiB; manifiesto de tamaños y SHA-256 No afirma que un hash certifique calidad de contenido; conserva procedencia de los bytes empaquetados
upload Exige confirmación y repo privado; verifica commit remoto; crea borrador, descarga y compara assets; publica con --publish Registra URL real y download_sha256_verified; datos grandes fuera del historial Git
restore Valida partes, nombres y tamaños; extrae a temporal; verifica hashes internos y cambia al destino nuevo No instala Docker/Ollama, no entrena y no sobrescribe una entrega anterior

El chequeo funcional deriva preguntas de los primeros textos útiles, pide tres resultados y exige alguna evidencia en cada combinación de colección y método. Una de las comprobaciones abre un PDF original y verifica la cabecera %PDF. Sirve para detectar índices rotos o incompatibles; no demuestra que el buscador encuentre lo que necesitan las personas en un conjunto independiente de preguntas. Extracto literal de la petición en functional_check:

responses = [client.post(endpoint, headers=h,
    json={"message": q, "method": method, "top_k": 3, "generate": False}) for q in qs]

Los archivos de reporte tienen momentos y significados diferentes:

Registro Qué documenta en esta entrega
payload/BUILD_REPORT.json Conteos, entrenamiento, PDF, versiones y pruebas ASGI; llm_inference:false se refiere a esa fase
BUILD_STATUS.json prepared_verified_not_uploaded: estado al terminar build, anterior a upload; el uploader no lo reescribe
UPLOAD_RESULT.json Release publicada (isDraft:false), tag, commit, URL y verificación de descarga; acredita la fase posterior
operacion-local/VERIFICACION_HTTPS_ENTREGA.json Comprobación real independiente que sí ejecutó Ollama en ambas colecciones
operacion-local/VERIFICACION_HTTPS_GUIA_COMPLETA.json Comprobación posterior de página/API públicas, seis búsquedas y hash del PDF; esa corrida no volvió a generar con Ollama
operacion-local/VERIFICACION_GUIA_COMPLETA_PUBLICA.json Contenido visible y navegación móvil/escritorio; no una evaluación humana de relevancia

Que BUILD_REPORT diga «No Ollama inference» y un reporte posterior pruebe generación no es una contradicción: describen fases distintas. Las pruebas unitarias pueden usar fixtures y transportes simulados para comprobar lógica; no se presentan como los PDF de entrega. Las evidencias locales no se copian indiscriminadamente al Git porque pueden contener información de operación; el manual resume su alcance y mantiene sus nombres para auditoría del propietario.

El artefacto de datos procede del commit 9193ce88f586783d3d8ab5dd93ec6880f49116a9; la revisión de código citada en este manual es 01bbc0ccb4d79ff5b829420e0d67f032827c099b. Las mejoras posteriores de documentación/interfaz no equivalen a un nuevo entrenamiento. Esa separación permite actualizar la aplicación y seguir utilizando una entrega de datos identificada por hashes.

Cómo instalar una copia en otro equipo

Se necesita Git, GitHub CLI autenticado con acceso al repositorio privado, Python 3.11 o posterior para restaurar y Docker con Compose para ejecutar. Los comandos siguientes se usan en un directorio nuevo del equipo de destino; no se vuelven a clonar encima de la instalación existente. La construcción puede descargar dependencias de software, pero la restauración no descarga modelos ni entrena.

gh auth status
gh repo clone VMOsornio/IA IA-unam -- --branch main --single-branch
cd IA-unam
gh release download ia-unam-artifacts-20260914-0428 --repo VMOsornio/IA --dir artifacts/descarga-artefactos
python3 artifacts/descarga-artefactos/ia_artefactos.py restore --assets artifacts/descarga-artefactos --out artifacts/restaurados-v1

Los destinos de descarga y restauración deben ser nuevos. El paquete contiene 20 partes, 2.660.066.142 bytes comprimidos y 4.760.857.985 bytes restaurados. El restaurador comprueba SHA-256 de las partes y de los archivos. Antes de extraer exige espacio libre para el tamaño expandido más 15 % de margen y una copia temporal del comprimido; también ocupa espacio la descarga ya existente. La Release conserva el commit con el que se prepararon sus datos; main contiene la interfaz actualizada compatible. No incluye el TXT bruto ni el modelo de Ollama. Los 50 PDF originales ya versionados bajo data/PDF se obtienen con el clone; no se añadieron originales nuevos a la Release. Debe conservarse ese montaje para abrir las fuentes, además de restaurar sus datos derivados. El payload tiene 19 archivos; las 20 partes son segmentos del archivo comprimido, no 20 modelos o 20 datasets distintos.

Crear un archivo .env.servidor nuevo sustituyendo /ruta/IA-unam por la ruta absoluta real de esa copia:

SECTESIS_ARTIFACTS=/ruta/IA-unam/artifacts/restaurados-v1/sectesis
SECTESIS_PDF_ARTIFACTS=/ruta/IA-unam/artifacts/restaurados-v1/pdf
SECTESIS_PDF_INPUT_DIR=/ruta/IA-unam/data/PDF
SECTESIS_PDF_VECTORS=/ruta/IA-unam/artifacts/restaurados-v1/pdf-vectors
SECTESIS_PDF_DENSE_DIR=/pdf_vectors
SECTESIS_PORT=8911
WITH_NEURAL=1
SECTESIS_API_TOKEN=
OLLAMA_MODEL=
OLLAMA_TIMEOUT_SECONDS=180
OLLAMA_KEEP_ALIVE=15m

Con OLLAMA_MODEL vacío se puede probar la recuperación sin redacción. Para añadir generación, configurar un Ollama local con un modelo ya disponible y una dirección accesible desde el contenedor. En Linux, un servicio ligado sólo a 127.0.0.1 del host no se vuelve accesible automáticamente mediante host.docker.internal; hace falta una conexión privada adecuada, como el relay de la instalación descrita. Las capacidades del modelo y sus plantillas deben comprobarse antes de usar otro paquete.

chmod 600 .env.servidor
docker compose -p ia-unam --env-file .env.servidor -f compose.final.yml config --quiet
docker compose -p ia-unam --env-file .env.servidor -f compose.final.yml build sectesis
docker compose -p ia-unam --env-file .env.servidor -f compose.final.yml up -d --wait sectesis

La copia queda disponible localmente en http://127.0.0.1:8911/demo/sectesis. El valor vacío de SECTESIS_API_TOKEN deja las consultas sin clave en ese acceso local; una instalación privada puede establecer una clave propia. Publicar en Internet requiere configurar el proxy HTTPS y el acceso del nuevo servidor; clonar no instala sus certificados ni copia automáticamente la infraestructura del servidor de origen.

Comprobaciones iniciales para esa configuración local sin clave:

curl --fail http://127.0.0.1:8911/health
curl --fail http://127.0.0.1:8911/api/v1/sectesis/status
curl --fail http://127.0.0.1:8911/api/v1/sectesis/pdf/status
curl --fail http://127.0.0.1:8911/api/v1/sectesis/visitors

Después, realizar consultas en ambas colecciones con los tres métodos, revisar fragmentos y abrir un PDF. /health sólo comprueba que el proceso responde, no la relevancia de las respuestas ni el estado completo de los datos. Para una instalación con clave, las rutas de API necesitan su autorización correspondiente. Los contadores se conservan en el volumen del proyecto Compose; no eliminarlo con down -v si se quieren mantener las visitas. No agregar bases, índices, pesos o archivos de configuración privada al historial Git.

Volver al índice ↑

CAPÍTULO 19Aportes, beneficios, limitaciones y aprendizajes del laboratorio

El resultado es un laboratorio funcional de IA aplicada a datos de bibliotecas y tesis, con una interfaz pública y un proceso reproducible. Permite observar cómo los datos se convierten en índices, cómo un encoder aprende una representación y cómo un LLM utiliza evidencia recuperada para redactar.

Aportes disponibles y utilidad

Aporte disponible Beneficio Limitación
Catálogo normalizado de 593.067 registros Buscar fichas por tema, autor y filtros, reutilizando una estructura común Los metadatos pueden ser incompletos; una ficha no contiene el texto completo de la tesis
6.541 fragmentos de 49 PDF con texto Llegar a pasajes concretos y abrir el documento en la página correspondiente No se aplicó OCR nuevo; tablas, imágenes, orden de lectura y documentos sin texto requieren revisión
Búsqueda BM25 Encontrar coincidencias léxicas sobre metadatos o pasajes Depende de los términos usados y puede perder relaciones expresadas con otras palabras
Encoder local y búsqueda densa Estudiar una representación aprendida, inspeccionar pesos y comparar similitudes Entrenado con títulos, vocabulario limitado, máximo de 64 tokens útiles y presencia de vectores cero
Búsqueda híbrida RRF Combinar las posiciones de BM25 y densa en la colección elegida Combinar métodos no garantiza mejores resultados en todas las consultas
Redacción local con Llama y fuentes visibles Organizar una orientación inicial y permitir revisar la evidencia mientras se redacta Puede omitir, interpretar mal o inventar; una cita sintácticamente válida no asegura respaldo
IDs, páginas, hashes y manifiestos Relacionar cada resultado con sus datos de origen y comprobar versiones La integridad del archivo no certifica veracidad, relevancia ni calidad de extracción
API, contenedores y Release privada Ejecutar y trasladar código, índices y pesos de forma separada Los recursos y la infraestructura del equipo de destino condicionan el funcionamiento
Guía técnica, ejemplos y glosario Explicar y reproducir decisiones del laboratorio con materiales reales Es documentación del prototipo; no sustituye formación, evaluación de usuarios o revisión especializada
Contadores persistentes Observar aperturas de página y presencia reciente Son estimaciones de navegadores, no personas identificadas ni estadísticas auditadas

Qué se desarrolló y qué se logró

Se desarrollaron el parser bibliográfico, la normalización a JSONL, los índices SQLite FTS5, el entrenamiento y la inferencia del encoder, la extracción y fragmentación PDF, la recuperación léxica/densa/híbrida, la API y la interfaz con fuentes. Se integró generación local mediante Ollama, validación sintáctica de citas, publicación y restauración de artefactos, contenedores y contadores persistentes.

La entrega logró hacer consultable el catálogo completo de este TXT y los fragmentos extraídos de los PDF disponibles. El encoder tiene 1.024 dimensiones y se entrenó durante cuatro épocas con la partición de títulos indicada; todos los títulos del catálogo y los fragmentos PDF se representaron después con pesos fijos. El servicio ya permite consultar, comparar métodos y abrir fuentes. Las comprobaciones de API, navegador, integridad, persistencia y generación confirman ese funcionamiento; no son una medición humana de exactitud o utilidad institucional.

Qué se aprendió y qué conocimiento se practicó

El aprendizaje técnico documentado se expresa en decisiones y resultados que pueden revisarse en el código:

Conocimiento trabajado Aplicación concreta en el laboratorio
Preparar datos antes de aplicar IA Interpretar ALEPH, normalizar campos y conservar procedencia y faltantes
Distinguir documento, registro, página y fragmento No presentar 593.067 fichas como tesis leídas ni 6.541 fragmentos como documentos diferentes
Construir recuperación de información Usar índices invertidos, BM25, filtros y criterios de ordenación
Comprender el entrenamiento Separar muestra, particiones, pérdida, gradientes, épocas y selección por validación
Distinguir pesos de representaciones Entrenar una transformación y después calcular vectores para todo el corpus sin seguir ajustándola
Entender la transferencia y sus límites Aplicar a pasajes PDF un encoder entrenado con títulos y reconocer el vocabulario y los tokens que no cubre
Integrar RAG Enviar evidencia acotada al LLM, mantener fuentes y distinguir recuperar de redactar
Evaluar con cuidado Separar pruebas funcionales, mediciones de rendimiento y juicios humanos de relevancia
Operar una aplicación reproducible Separar código, configuración, datos persistentes e imágenes de contenedor; verificar hashes al trasladar
Formular experimentos comparables Fijar preguntas, evidencia y condiciones antes de comparar modelos, calidad, tiempos y memoria

Estos conocimientos se pueden revisar en el código, la configuración y los ejemplos. El laboratorio permite recorrer el proceso completo y detectar dónde se origina un resultado: extracción, segmentación, índice, representación, recuperación o generación.

Alcance del proyecto y valor del laboratorio

El alcance actual incluye consulta de fichas y pasajes, comparación de recuperación, redacción opcional con fuentes y despliegue reproducible. No abarca todo el acervo UNAM, todos los procesos de gestión bibliotecaria, lectura visual de páginas, OCR general, entrenamiento de un LLM propio ni respuestas cuya exactitud haya sido garantizada. Tampoco une automáticamente cada ficha del catálogo con un PDF ni consulta ambas colecciones a la vez.

Su utilidad como laboratorio es ofrecer una base real para aprender, demostrar y experimentar: un estudiante puede seguir la transformación de los datos; un desarrollador puede reproducir el servicio; una persona usuaria puede buscar y comprobar fuentes; y un bibliotecario puede ayudar a evaluar si los resultados responden a necesidades reales. Es una base para investigar mejoras y no una afirmación de que la IA sustituye el criterio bibliotecario.

El conocimiento central adquirido en el proceso es que aplicar IA exige organizar los datos, definir qué se recupera, controlar qué recibe el modelo y comprobar el resultado. Una representación aprendida o una respuesta bien escrita sólo aporta valor cuando se entiende su procedencia y sus límites. Los experimentos de la sección anterior permiten estudiar ese valor sin confundir funcionamiento técnico con calidad demostrada.

Código de consulta: search.py, dense.py, service.py, app/pdf_rag/engine.py y ambos generation.py. Evidencia local: BUILD_REPORT.json, registros de verificación por HTTPS y navegador, OLLAMA_DIAGNOSTICO_TIEMPOS.json, EJEMPLOS_PROCESO_REAL.json, EJEMPLO_FRAGMENTOS_CONSECUTIVOS.json y ejemplo-encoder-real.json. Consultar el glosario completo.

Volver al índice ↑

CAPÍTULO 20Investigación pendiente: qué cambiar, para qué y cómo comprobarlo

Esta sección es un programa de investigación, no una descripción de trabajo ya realizado. Los valores empleados constituyen una configuración reproducible del laboratorio. No se encontró una comparación controlada que demuestre que 1.600 caracteres, 1.024 dimensiones, temperatura contrastiva 0,1 o cuatro épocas sean óptimos para estas tesis. Explicar la función de un parámetro justifica su papel técnico; demostrar que su valor es el mejor exige ensayos.

Opciones existentes que aún no tienen resultados en esta entrega

El repositorio incluye index_pretrained: permite crear un índice de catálogo con un SentenceTransformer ya disponible localmente, identificando modelo, revisión y archivos. Usa CPU, local_files_only=True, trust_remote_code=False, lotes de codificación de 8 y un máximo de secuencia de hasta 512 tokens, limitado además por el propio modelo. Puede usar semantic_text y un prefijo de consulta. Esa ruta no se activó aquí. Tampoco es un reemplazo directo para todo el sistema: la vectorización PDF y el kit de esta entrega esperan el encoder didáctico. Aplicarla a PDF o empaquetarla requiere adaptar y probar esas rutas. Código: index_pretrained, líneas 78–110, comprobación del modelo PDF.

También se puede cambiar el método de consulta, top_k, los filtros o activar/desactivar la redacción sin entrenar. Cambiar el texto usado por el encoder, su vocabulario o sus pesos exige calcular vectores compatibles de nuevo. Cambiar la fragmentación cambia las unidades de recuperación: obliga a reconstruir el índice PDF y sus vectores en una versión nueva, y revisar los juicios de relevancia cuyos IDs dependían de los fragmentos anteriores.

Hipótesis por etapa

Los valores alternativos de la tabla son candidatos ilustrativos para diseñar un ensayo, no una recomendación validada ni comandos que se hayan ejecutado.

Pregunta de investigación Variable o técnica candidata Qué habría que desarrollar o preparar Cómo comprobar su utilidad
¿Se recupera información de las páginas omitidas? OCR sólo en páginas sin texto o con extracción defectuosa Integrar OCR, registrar motor/idioma y conservar procedencia; crear nuevos fragmentos e índices Comparar transcripción contra páginas revisadas y medir relevancia; contar errores y coste por página
¿Los cortes actuales separan evidencia necesaria? Fragmentos de 800/1.600/2.400 caracteres y solapamientos 100/200; o cortes por párrafo Nuevas versiones por condición, sin mezclar matrices; revisar cómo se trasladan las anotaciones Recall y nDCG de pasajes, respaldo de respuestas y coste de contexto con las mismas preguntas
¿La representación conserva suficiente información? Títulos frente a título + materias/resumen; límite de 64 frente a otro límite definido Modificar/versionar representación, entrenar sólo con train, generar todas las matrices de nuevo Cobertura del vocabulario, vectores cero y recuperación por tema, sin elegir condiciones con test
¿Aporta valor la capa entrenada? BM25, coseno de TF-IDF sin red y encoder contrastivo Implementar una comparación con las mismas fuentes, particiones, filtros y consultas Medir relevancia y coste por método; la reducción de pérdida contrastiva no basta
¿Es mejor otra capacidad del encoder? 256/512/1.024 dimensiones, distintas tasas de aprendizaje o temperaturas Entrenamientos independientes con semillas y recursos registrados; nueva vectorización Seleccionar con validación; evaluar test reservado una vez fijada la configuración; informar memoria y dispersión
¿Ayuda otra fusión? Variar candidatos por método y constante RRF; comparar cada método aislado Versionar la configuración, conservar resultados y tiempos por consulta Medir mejoras y regresiones pareadas, especialmente consultas que sólo un método resuelve
¿Puede reducirse el tiempo de búsqueda con un corpus mayor? Índice aproximado ANN frente a búsqueda exacta Integrar un motor, versionar su índice y controlar compatibilidad con filtros Recuperación respecto del ranking exacto, relevancia humana, latencia y memoria; no sólo velocidad
¿Mejora el orden con una segunda lectura de candidatos? Reranker que puntúa pregunta y pasaje conjuntamente Añadir una etapa sobre los primeros candidatos y un modelo compatible nDCG/recall y tiempo extra; no puede recuperar evidencia que nunca entró en sus candidatos
¿Puede navegarse de ficha a tesis? Vinculación explícita catálogo–PDF por identificadores verificables Crear y revisar una tabla de correspondencias con procedencia y casos ambiguos Exactitud de los enlaces y cobertura; no inferir equivalencia sólo por un nombre parecido
¿Qué gana una persona al usarlo? Tareas de búsqueda con y sin el asistente Definir tareas, consentimiento, revisión bibliotecaria y protocolo de comparación Tiempo hasta una fuente válida, éxito de tarea y errores; separar visitas web de personas y de utilidad

El OCR propuesto puede investigarse mediante Page.get_textpage_ocr, una capacidad de PyMuPDF que requiere Tesseract; el código actual llama a extracción de texto y no a ese método. Referencia oficial de OCR.

Faiss ofrece familias de búsqueda exacta y aproximada con distintos compromisos de memoria, coste y recuperación. Aquí no está integrado; una eventual incorporación debe compararse con los productos escalares exactos del proyecto y respetar los filtros antes de devolver resultados. Documentación oficial de Faiss.

Un cross-encoder calcula una puntuación procesando consulta y candidato juntos; puede estudiarse como segunda etapa para pocos candidatos, con coste adicional por pareja. No se aplicó en esta instalación. Documentación oficial de Sentence Transformers.

Protocolo común para no confundir cambios con mejoras

  1. Formular una pregunta y una hipótesis antes de ejecutar: por ejemplo, si otro corte recupera más pasajes relevantes sin superar un límite de tiempo acordado.
  2. Fijar datos y procedencia: commit, hashes, versión del parser, fragmentos, encoder, dependencias y recursos. Separar conjunto de desarrollo y prueba; agrupar preguntas del mismo documento cuando corresponda para evitar contaminación entre grupos.
  3. Preparar juicios de relevancia revisados, con criterios explícitos y desacuerdos registrados. Para PDF, anotar documento/página y evidencia; si cambian los fragmentos, revisar su correspondencia. Incluir preguntas sin evidencia en un protocolo separado: el evaluador actual requiere al menos un positivo por caso.
  4. Cambiar una variable por ensayo inicial y mantener las demás comparables. Cuando se estudien interacciones, declarar un diseño factorial; no atribuir un resultado a una sola variable si cambiaron varias.
  5. Registrar cada ejecución, errores incluidos. Para latencia, distinguir arranque/carga diferida de consultas con datos ya cargados, alternar el orden de los métodos y repetir. El calentamiento de una búsqueda puede beneficiar a la siguiente.
  6. Comparar relevancia, coste y fallos por pregunta; informar tamaño del conjunto, dispersión y ejemplos de regresión. Si se estiman intervalos mediante remuestreo, documentar la unidad de muestreo y el procedimiento. No existe en esta entrega un cálculo automático de significación estadística.
  7. Elegir configuraciones con desarrollo/validación; consultar test cuando estén fijadas. Conservar el sistema anterior para contrastar, y publicar la conclusión junto con sus límites y los archivos reproducibles.

El resultado científico posible es una respuesta acotada a una pregunta, con evidencia verificable. Puede mostrar una mejora, un empate o que una técnica añade coste sin beneficio; los tres resultados son informativos. Los experimentos de Llama descritos antes complementan estas pruebas de recuperación y preparación, sin convertirlas en entrenamiento del LLM.

Volver al índice ↑

CAPÍTULO 21Mapa de archivos, funciones y evidencias para estudiar el código

Las referencias siguientes fijan el commit auditado 01bbc0c. Los nombres de funciones permiten localizar la acción; los archivos de salida permiten comprobar qué produjo. La configuración y los manifiestos determinan qué ruta se utilizó realmente.

Etapa Archivo y punto de entrada Evidencia o salida que hay que inspeccionar
Parsear y normalizar TXT ingest.py: records, canonical, prepare sample.jsonl y profile.json; conteo y hash de la fuente
Índice de catálogo y BM25 search.py: build, LexicalIndex.search, rrf catalog.sqlite: tablas records, docs, manifest; orden y filtros
Entrenar y codificar títulos neural.py: Encoder, split_of, train encoder.json, weights.pt, splits.json, ids.json, vectors.npy
Selección y vectorización del catálogo completo ia_artefactos.py: reservoir, train_and_encode Selección de 12.000, huella de entrenamiento y matriz final de 593.067 filas
Recuperación densa de catálogo dense.py: DenseIndex Correspondencia IDs/vectores, integridad, filtros, producto escalar y ranking
Extraer y fragmentar PDF pdf_rag/ingest.py: Options, extract, chunks, index_pdfs pdf/manifest.json, chunks.jsonl y SQLite; estados, páginas, posiciones y reutilización
Vectorizar y buscar PDF pdf_rag/engine.py: vectorize, Engine.search pdf-vectors/manifest.json, encoder copiado y matriz de 6.541 filas
Coordinar catálogo y generación service.py: search, answer Método, evidencias, answer_mode, avisos y latencia de la respuesta API
Contexto y validación de citas sectesis_ai/generation.py y pdf_rag/generation.py Dos mensajes al LLM, opciones, texto devuelto, control sintáctico de [n]
Evaluar ranking de catálogo evaluation.py: metrics, evaluate Casos y positivos revisados, métricas por caso/agregadas, tiempos; no certificado humano
Empaquetar, comprobar, restaurar y subir ia_artefactos.py: functional_check hasta upload BUILD_REPORT.json, BUILD_STATUS.json, manifiesto y sumas de Release, UPLOAD_RESULT.json
Ejecutar y montar persistencia compose.final.yml y Dockerfile.final Imagen, rutas montadas, variables permitidas, estado de contenedor y volumen de visitas

Para repetir una inspección local sin imprimir el corpus completo, desde la raíz del repositorio:

# Leer las funciones auditadas y la configuración común.
sed -n '39,91p' app/sectesis_ai/neural.py
sed -n '49,67p' app/pdf_rag/ingest.py
sed -n '170,205p' app/pdf_rag/engine.py
cat compose.final.yml
# Leer reportes de esta instalación; adaptar la ruta en una copia restaurada.
python3 -m json.tool artifacts/entrega-servidor-entrenada-v1/payload/BUILD_REPORT.json
python3 -m json.tool artifacts/entrega-servidor-entrenada-v1/BUILD_STATUS.json
python3 -m json.tool artifacts/entrega-servidor-entrenada-v1/UPLOAD_RESULT.json

Los reportes del kit describen una entrega, no se actualizan con cada visita ni con cada cambio de documentación. La imagen en ejecución, el commit de la interfaz y el commit de preparación de datos pueden ser distintos; deben registrarse por separado. Las dependencias fijadas están en requirements-final.txt y requirements-final-ml.txt. La reproducibilidad numérica también depende de plataforma y versiones: una semilla sola no promete identidad bit a bit en cualquier equipo.

Volver al índice ↑

CONSULTA RÁPIDAGlosario: términos y procesos de este proyecto

Definiciones ligadas al código de este proyecto. «Token» en texto y «token de acceso» son conceptos diferentes. El chat público actual no pide una clave de acceso.

Término Qué significa aquí Ejemplo o relación con el proceso
Acervo Conjunto de materiales de una biblioteca o institución. Este TXT y 50 PDF son los insumos disponibles; no se afirma cubrir todo el acervo UNAM.
Catálogo bibliográfico Descripciones organizadas de documentos. Título, autor, año, grado, facultad y enlaces de una tesis.
IATESIS Nombre actual del proyecto de Víctor Manuel Osornio Luna. Laboratorio de IA aplicada a fichas bibliográficas y fragmentos de tesis; usa la fuente sectesis.txt.
SECTESIS Nombre técnico de fuente y espacio de identificadores utilizado por el repositorio. sectesis.txt y sectesis:000000001; no es un algoritmo ni se inventa una expansión oficial.
Registro Unidad que describe un documento en el catálogo. Una línea canónica de sample.jsonl.
Metadatos Datos que describen un documento. Un año de publicación no es el texto de la tesis.
Dataset / conjunto de datos Datos organizados para un procesamiento. JSONL completo del catálogo o conjunto de fragmentos PDF.
Corpus Colección de textos o registros usada en un sistema. 593.067 registros de este TXT.
ALEPH secuencial Formato de exportación que el parser reconoce por líneas, ID, etiquetas y subcampos. 000000001 24510 L $$a…; este parser no lee ISO2709 binario.
Etiqueta y subcampo Marcadores que indican el significado de un dato bibliográfico. 245 $$a para título; 100 $$a para autor.
Parser Código que interpreta la estructura del archivo. records() agrupa líneas; canonical() asigna campos.
Ingesta Incorporación controlada de datos al proceso. Leer, comprobar formato, normalizar y guardar.
Normalización Aplicar reglas consistentes al texto. Espacios y Unicode; en búsqueda se normalizan mayúsculas y acentos.
Registro canónico Representación interna coherente del registro original. Campos id, title, authors, year y banderas de calidad.
JSON Formato de datos con objetos, listas, cadenas y números. Una respuesta de la API.
JSONL Un objeto JSON por línea. Permite recorrer el dataset sin leerlo completo en memoria.
SQLite Motor de base de datos que guarda tablas e índices en un archivo. catalog.sqlite; no es una red neuronal.
SQL Lenguaje para consultar y organizar la base. SELECT … WHERE … ORDER BY … LIMIT.
FTS5 Extensión de SQLite para búsqueda de texto completo. Tablas virtuales docs y fts.
Índice invertido Estructura que relaciona términos con documentos y ocurrencias. Permite localizar registros que contienen «habitacional».
Indexar Construir estructuras para recuperar datos. Crear FTS5 y sus estadísticas; no implica entrenar.
BM25 Best Matching 25: ordena coincidencias léxicas según frecuencia, rareza y longitud. Recuperador de palabras; no redacta respuestas.
Recuperación léxica Búsqueda basada en términos del texto. BM25 puede encontrar un apellido presente en metadatos.
Token de texto Unidad de texto usada por un procesador. El encoder usa palabras normalizadas; Llama usa su propia tokenización, que no equivale a palabras completas.
Tokenización Dividir el texto en unidades. tokens() descarta términos cortos y algunas palabras comunes; devuelve hasta 64.
Stopwords / palabras excluidas Palabras que el tokenizer de consulta y encoder omite. «de», «la», «tesis»; FTS5 tiene su propio tokenizer y no usa esa misma lista.
Vocabulario Conjunto de características que el vectorizador reconoce. Hasta 4.096 términos aprendidos de los títulos de entrenamiento.
TF Frecuencia de un término en un texto. Cuántas veces aparece una palabra; el TF-IDF activo usa escalado sublineal.
IDF Peso que depende de la frecuencia del término en el conjunto. En el vectorizador, se ajusta sólo con títulos de entrenamiento.
TF-IDF Representación numérica que combina TF e IDF. Entrada de 4.096 características a la capa lineal.
Característica / feature Variable de entrada de un modelo. Una posición del vector TF-IDF corresponde a un término del vocabulario.
Vector disperso Vector con muchas posiciones cero. El título del ejemplo tiene nueve características TF-IDF no nulas.
Encoder Modelo que transforma una entrada en una representación. TF-IDF → capa lineal → vector normalizado; no genera prosa.
Capa lineal Multiplicación por una matriz de parámetros. y = W @ x, sin sesgo en este encoder.
Peso / parámetro Número que el entrenamiento ajusta dentro de la red. weights.pt guarda 4.194.304 parámetros del encoder. Los pesos de columna de BM25 son otra cosa: se configuran, no se aprenden aquí.
Hiperparámetro Elección del procedimiento de entrenamiento. Cuatro épocas, lote 96, tasa 0,002; no es un peso aprendido.
Embedding Vector resultante que representa un texto. 1.024 números por título o fragmento.
Dimensión Cantidad de componentes de un vector. 1.024 no significa 1.024 documentos ni palabras de una respuesta.
Normalización L2 Ajuste de la longitud euclídea del vector. Vectores no nulos se llevan a norma 1.
Vector cero Representación sin señal numérica útil. No permite comparación densa útil; el texto puede seguir en BM25.
Muestra Subconjunto elegido del dataset. Los 12.000 registros iniciales para preparar entrenamiento.
Reservoir sampling Muestreo uniforme mientras se recorre un archivo. Evita necesitar todo el catálogo en RAM para seleccionar 12.000.
Semilla Valor que permite reproducir operaciones aleatorias. 20260909 para selección y entrenamiento.
Deduplicación Agrupar o detectar entradas repetidas según una regla. Títulos normalizados iguales se agrupan antes de dividir el entrenamiento.
Train / entrenamiento Partición que se usa para ajustar el modelo. 8.750 títulos en esta entrega.
Validación Partición usada para comparar decisiones del entrenamiento. 1.144 títulos; permite seleccionar la época 3.
Test / prueba Partición reservada para evaluación aparte. 1.116 títulos; no se usó para elegir la época.
Fuga de datos / leakage Uso indebido de información de evaluación al entrenar o seleccionar. Ajustar TF-IDF con todos los títulos antes de dividir produciría contaminación; aquí se ajusta sólo con train.
Época Recorrido de los ejemplos de entrenamiento. Cuatro recorridos; no cuatro lecturas completas de todos los PDF.
Lote / batch Grupo procesado en un paso. Hasta 96 títulos.
Autosupervisión Construcción de señales de aprendizaje a partir de los propios datos. Dos vistas del mismo título, sin etiquetas de relevancia humanas.
Aprendizaje contrastivo Objetivo que acerca representaciones positivas y distingue otras. Vistas enmascaradas del mismo título frente a otros títulos del lote.
Enmascaramiento / augmentation Alteración controlada para crear una vista de entrenamiento. Ocultar características con probabilidad 0,15.
Pérdida / loss Medida del error del objetivo de entrenamiento. Menor pérdida de validación selecciona un checkpoint; no es un porcentaje de precisión.
Entropía cruzada Función de pérdida para comparar predicciones con objetivos. Se aplica en ambas direcciones entre vistas del lote.
Temperatura Parámetro que cambia una distribución de puntuaciones. 0,1 en el objetivo contrastivo; la temperatura 0 de generación pertenece a otro proceso.
Gradiente / backpropagation Derivadas que indican cómo ajustar los parámetros. loss.backward() calcula gradientes.
Optimizador AdamW Regla que actualiza los pesos usando gradientes y regularización. opt.step() durante el entrenamiento.
Tasa de aprendizaje Tamaño de referencia de los pasos del optimizador. 0,002 en esta ejecución.
Regularización / weight decay Penalización o restricción que ayuda a controlar el ajuste. Decaimiento de pesos 0,001 con AdamW.
Checkpoint Estado guardado de los pesos del modelo. Los pesos elegidos al terminar la época 3.
Inferencia Uso de un modelo ya fijado para producir una salida. Vectorizar todos los títulos y PDF; también generar texto con Llama.
Vectorizar Calcular vectores para entradas. Aplicar los pesos fijos a 6.541 fragmentos no los entrena otra vez.
Similitud coseno Comparación de dirección entre dos vectores. Con norma 1, coincide con su producto escalar.
Recuperación densa Búsqueda comparando representaciones vectoriales. Comparación exacta con NumPy en este proyecto.
ANN Búsqueda aproximada de vecinos más cercanos. No se utiliza en el backend activo; éste compara por bloques.
Ranking / score Lista ordenada y puntuación usada para ordenar. El score no es probabilidad de verdad ni relevancia certificada.
Top-k Número máximo de resultados solicitados. Cinco referencias por defecto en la pantalla.
Filtro Restricción sobre qué registros pueden competir. Año y facultad exacta en catálogo; no están disponibles en la búsqueda PDF.
RRF / búsqueda híbrida Fusión de posiciones de rankings. Suma 1/(60+posición) de BM25 y densa en la misma colección.
PyMuPDF Biblioteca que abre PDF y extrae su texto disponible. Extracción por página con get_text.
OCR Reconocimiento de caracteres en imágenes. No se ejecutó OCR adicional; una imagen sin capa de texto puede quedar sin indexar.
Fragmento / chunk Unidad de texto que se indexa para recuperación. Hasta 1.600 caracteres de una página PDF.
Solapamiento / overlap Texto compartido entre fragmentos contiguos. Objetivo de 200 caracteres, ajustado a palabras.
Página física / page label Posición dentro del archivo / etiqueta que el PDF puede declarar. La página física 33 puede tener otra numeración impresa.
Offset Posición de un carácter en el texto normalizado. char_start=0, char_end=1596, final exclusivo.
ID Identificador de un registro o fragmento. sectesis:000000001 o pdf-…:p33:c0.
SHA-256 / hash Huella calculada sobre bytes. Detecta cambios; no acredita verdad, calidad ni autoría.
Manifiesto / integridad Archivo que registra contenido, versiones, conteos y huellas. Impide mezclar inadvertidamente vectores de otra versión.
Artefacto Archivo producido por preparación, entrenamiento o indexación. SQLite, JSONL, pesos, vectores y reportes.
float32 / NumPy NPY Número de 32 bits / formato de matriz. Cada valor ocupa cuatro bytes; vectors.npy conserva forma y tipo.
mmap Lectura de archivos mediante mapeo de memoria. Permite acceder por bloques a la matriz de vectores.
LLM Modelo de lenguaje que produce secuencias de tokens. Llama 3 redacta con el contexto recibido.
Transformer Arquitectura que combina atención y transformaciones internas de las representaciones. Llama tiene 32 bloques; el encoder local es una capa lineal, no un Transformer.
Autorregresivo Genera cada token condicionado por los anteriores. La respuesta se construye paso a paso hasta terminar.
Atención Operación que pondera información de otras posiciones permitidas del texto. Relaciona los tokens de la pregunta con el contexto disponible.
GQA Atención con consultas agrupadas que comparten claves y valores. La ficha local de Llama registra 32 cabezas de consulta y 8 de claves/valores.
Caché KV Memoria temporal de claves y valores usada durante la generación. Evita recalcular el prefijo completo; no es entrenamiento ni una base bibliográfica.
Logits Puntuaciones del vocabulario antes de elegir el siguiente token. No son puntuaciones de relevancia BM25 ni probabilidades de verdad.
Decodificación Procedimiento para elegir tokens a partir de las puntuaciones del modelo. Las opciones de generación pertenecen a esta etapa y no actualizan pesos.
Prefill / procesamiento del contexto Cálculo inicial sobre los tokens de entrada. Se mide aparte de la generación de tokens de salida.
Ventana de contexto / num_ctx Espacio de trabajo para la secuencia de tokens. Los 8.192 declarados por el modelo no prueban la ventana efectiva del runtime.
num_predict Límite de tokens nuevos que puede generar la respuesta. El código envía 400 para catálogo y 500 para PDF.
top_k de muestreo Número de candidatos considerados para elegir un token. Es distinto del top_k del buscador, que selecciona referencias.
top_p Restricción de candidatos por probabilidad acumulada durante muestreo. No se configura explícitamente en los clientes actuales.
Streaming de tokens Entrega progresiva del texto mientras se genera. La aplicación usa stream=false; mostrar fuentes antes es otro mecanismo.
TTFT Tiempo hasta el primer token de salida observado. Requiere instrumentación de streaming; no está medido por esta interfaz.
Percentil 95 / p95 Valor bajo el que cae aproximadamente el 95 % de las mediciones. Con pocas ejecuciones es inestable; acompañarlo de número de casos y fallos.
Abstención Reconocer que la evidencia no permite responder. Es preferible a atribuir conclusiones a un pasaje que no las contiene.
Llama / Ollama Modelo de lenguaje / programa que lo ejecuta localmente. llama3:latest es el modelo; Ollama expone /api/chat.
Cuantización Q4_0 Representación compacta de pesos del modelo Llama. Reduce memoria respecto de pesos con mayor precisión; no describe los float32 del encoder.
CPU / GPU / VRAM Procesador general / acelerador / memoria del acelerador. Llama usa CPU y reportó 0 bytes de VRAM en esta instalación.
Prompt / contexto Instrucciones y datos que se envían al LLM. Pregunta y referencias seleccionadas, no el catálogo entero.
RAG Generación aumentada con recuperación. Buscar evidencia antes de pedir una respuesta a Llama.
Fine-tuning Ajustar un modelo previamente entrenado con nuevos datos. No se hizo fine-tuning de Llama; el encoder didáctico se entrenó desde cero.
Alucinación Afirmación generada sin respaldo suficiente. Una cita con formato válido no garantiza que el pasaje apoye la afirmación.
Cita / trazabilidad Referencia que permite volver a la fuente. Documento, página, fragmento e ID.
Latencia / timeout Tiempo de respuesta / límite de espera. La generación tiene más margen que la recuperación; fuentes visibles primero.
keep_alive Tiempo de retención temporal del modelo cargado. Evita algunas recargas, pero mantiene RAM ocupada.
API / ASGI Interfaz HTTP / protocolo de comunicación de la aplicación Python. FastAPI y TestClient verifican solicitudes; ASGI no prueba calidad humana.
Token de acceso Clave usada para autorizar solicitudes. Es distinto de los tokens de texto; el chat público actual no requiere esa clave.
HTTPS / Nginx Transporte cifrado / servidor que recibe conexiones públicas y las reenvía. La IP pública llega al backend que escucha en loopback.
Docker / Compose Contenedores / definición conjunta de servicios y montajes. Ejecutan la app con los artefactos en rutas de sólo lectura.
Git / commit / rama Historial del código / revisión identificable / línea de trabajo. El código de la UI puede evolucionar sin volver a entrenar.
Release / tag Entrega descargable / referencia a una revisión del código. Los pesos grandes están en assets de la Release privada, no en commits.
Fixture Datos controlados para probar código. No se usan como tesis reales de esta entrega.
Precision@k / Recall@k Fracción de resultados relevantes / fracción de relevantes recuperados. Requieren definir qué resultados son relevantes para cada pregunta.
MRR / nDCG Métricas de posición del primer relevante / calidad del orden normalizada. El evaluador usa relevancia binaria, no graduada; requiere positivos por consulta.
Evaluación humana Revisión de utilidad, relevancia y respaldo por personas. Preguntas y juicios de bibliotecarios siguen siendo necesarios.
min_df Mínimo de documentos que deben contener una característica para conservarla. Vale 2 títulos de train al ajustar TF-IDF; no es frecuencia total de la palabra.
max_features Límite del vocabulario del vectorizador. Se fijó en 4.096; limita la entrada a la capa lineal.
sublinear_tf Transformación de frecuencia positiva a 1 + ln(frecuencia). Activa en TF-IDF; reduce el crecimiento debido a repetición.
smooth_idf Suavizado del cálculo IDF para evitar divisiones problemáticas. Se usa ln((1+n)/(1+df)) + 1, con documentos de train.
Betas de AdamW Coeficientes de las medias móviles del gradiente y de su cuadrado. El optimizador conserva los valores por defecto 0,9 y 0,999.
Épsilon / eps Constante pequeña para estabilidad numérica; depende de la operación. AdamW usa 1e-8; normalización PyTorch usa 1e-12; no son el umbral de relevancia.
Búsqueda exacta Calcula la comparación sobre todos los candidatos elegibles. El buscador denso recorre bloques de 4.096 vectores; no usa ANN.
Reranker / cross-encoder Segunda etapa que reordena candidatos; un cross-encoder procesa consulta y candidato juntos. Propuesta futura, sin modelo ni etapa de este tipo ejecutados aquí.
Ablación Comparación que retira o sustituye un componente para estudiar su aportación. Comparar TF-IDF sin red frente al encoder sería un ensayo pendiente sobre este corpus.
Benchmark / evaluación proxy Conjunto de tareas para comparar / sustituto simplificado de tareas reales. Consultas derivadas de títulos no sustituyen juicios de usuarios; el proxy histórico pertenece a otro corpus.
Arranque en frío / calentamiento Primera carga / preparación previa de recursos antes de medir. La primera consulta densa incluye verificaciones; medir después puede dar tiempos menores.
Semáforo de concurrencia Mecanismo que limita operaciones simultáneas. Dos plazas por colección y proceso; ocupado devuelve HTTP 429, no una cuota por persona.
Liveness / readiness Proceso vivo / componentes preparados para una operación. /health y los estados de índices tienen alcances distintos; densa se comprueba al cargarla.
Hipótesis experimental Afirmación acotada que un ensayo puede apoyar o refutar. Por ejemplo, otro corte podría mejorar recuperación; aún habría que medirlo.
Comparación pareada Compara métodos usando las mismas preguntas o unidades. Permite observar mejoras y regresiones por caso sin mezclar tareas distintas.
Diseño factorial Ensayo que combina niveles de varias variables para estudiar efectos e interacciones. Propuesta metodológica; no se realizó una búsqueda factorial de hiperparámetros en esta entrega.

La guía del proceso muestra cómo se conectan estos términos con los datos reales y los archivos producidos. Las definiciones de implementación se apoyan en el código del proyecto; para FTS5 y las métricas de Ollama, véanse las referencias enlazadas en esa guía.