Por qué el primer paso de un RAG no es la inteligencia artificial

Segunda parte de la serie «RAG personal».

En el primer artículo de esta serie expliqué por qué empecé a construir un RAG personal y cómo quiero convertir años de documentación en una memoria que pueda consultar.

Nota sobre los ejemplos: los nombres, rutas, identificadores, estructuras y fragmentos de código utilizados en este artículo son ilustrativos, ficticios o simplificados con fines didácticos. No representan documentos, sistemas ni configuraciones reales.

Aunque este proyecto se plantea como un RAG personal, cuestiones como los duplicados, los formatos inconsistentes, la trazabilidad, la reanudación y el control de estados son comunes a cualquier sistema documental que pretenda ser fiable.

Pero antes de generar embeddings, dividir documentos en fragmentos o instalar una base vectorial tuve que resolver un problema bastante menos vistoso.

Hace unos meses me hice una pregunta aparentemente sencilla:

¿Cuánta documentación tengo realmente?

Pensé que la respuesta sería tan simple como recorrer unas cuantas carpetas y contar archivos.

No tardé en descubrir que estaba completamente equivocado.

Durante años había ido acumulando manuales, artículos, notas personales, apuntes técnicos, exportaciones de páginas web, conversaciones, pruebas, scripts, copias de seguridad y archivos relacionados con herramientas y proyectos personales que ya ni siquiera utilizaba.

La información estaba ahí.

Y, al menos aparentemente, estaba organizada.

Había carpetas por proyectos personales, tecnologías, años y temas. Muchos archivos tenían nombres reconocibles y conservaban la estructura con la que los había organizado originalmente.

Sin embargo, cuando intenté construir mi propio RAG apareció una realidad bastante diferente.

Había documentos duplicados, versiones antiguas, copias parciales, formatos obsoletos, imágenes sin texto extraíble, ficheros temporales creados por herramientas de sincronización y archivos que no podían abrirse correctamente.

Otros tenían nombres distintos, pero contenían exactamente la misma información.

También encontré documentos cuya extensión decía una cosa y cuyo contenido era otra completamente diferente.

La colección parecía organizada cuando la observaba desde fuera.

Pero no podía responder con seguridad a preguntas tan básicas como estas:

  • ¿Cuántos documentos existen realmente?
  • ¿Cuáles están repetidos?
  • ¿Qué tipo de contenido parece tener realmente cada archivo?
  • ¿Cuáles siguen siendo útiles y cuáles necesitan una revisión?
  • ¿Qué archivos parecen dañados, están incompletos o no pueden abrirse correctamente?
  • ¿Qué parte de la colección podría procesarse?
  • ¿Qué contenidos no merece la pena incorporar?

Aquello no era todavía una biblioteca de conocimiento.

Era un almacén.

Y un almacén puede estar perfectamente ordenado por estanterías y seguir siendo un caos cuando intentas comprender qué contiene.

Una carpeta llena de documentos no constituye todavía una base de conocimiento.


El error que casi todos cometemos al empezar un RAG

Cuando alguien empieza a interesarse por los sistemas RAG suele hacerse preguntas como estas:

  • ¿Qué modelo de lenguaje debería utilizar?
  • ¿Qué base vectorial es mejor?
  • ¿Qué modelo de embeddings ofrece mejores resultados?
  • ¿Cuántos fragmentos debería recuperar?
  • ¿Necesito una GPU?
  • ¿Debería utilizar una búsqueda híbrida?

Yo mismo empecé pensando en muchas de esas cuestiones.

Es lógico.

Son las piezas más visibles del sistema y también las que más aparecen en tutoriales, demostraciones y comparativas.

Pero con el tiempo descubrí que todas esas preguntas estaban llegando demasiado pronto.

Antes existía otra mucho más importante:

¿Qué documentación tengo realmente?

Puede parecer una cuestión administrativa.

No lo es.

Es un problema de ingeniería.

Antes de pensar en búsqueda semántica necesitaba saber qué archivos existían, dónde estaban, cuál parecía ser su formato real, si habían cambiado y si podían procesarse con confianza.

Hasta que no fui capaz de responder a esas preguntas comprendí que todavía no estaba construyendo un RAG.

Estaba intentando levantar una casa sin haber hecho antes el inventario de los materiales.


El primer componente de mi RAG no fue un modelo

Cuando hablamos de inteligencia artificial solemos imaginar modelos enormes, GPUs y millones de parámetros.

Curiosamente, el primer componente realmente importante de mi RAG fue mucho más humilde.

Fue un inventario documental.

No hacía búsquedas semánticas.

No generaba respuestas.

No calculaba embeddings.

Ni siquiera entendía el contenido de los documentos.

Su misión era responder de forma fiable a una pregunta muy concreta:

¿Qué documentación existe realmente?

Responder correctamente resultó bastante más difícil de lo que había imaginado.

Porque un archivo no es necesariamente un documento.

Y un documento tampoco es necesariamente una fuente válida de conocimiento.

Antes de que un RAG pueda responder, necesita saber con qué documentación está trabajando.


Dejé de mirar archivos y empecé a ver objetos documentales

Hasta entonces daba muchas cosas por supuestas.

Pensaba que un PDF era un PDF.

Que un DOCX era un documento Word.

Que un TXT contenía texto.

Que dos nombres diferentes significaban dos documentos distintos.

La realidad era más complicada.

Un archivo tenía una ruta, un nombre y una extensión, pero eso no bastaba para describirlo.

También podía tener:

  • una identidad basada en su contenido;
  • un tipo detectado que no coincida con el declarado;
  • una relación con otras copias;
  • un estado de procesamiento;
  • unos metadatos;
  • una versión;
  • y uno o varios archivos derivados.

Aquello me obligó a cambiar la unidad con la que estaba trabajando.

Dejé de mirar únicamente archivos.

Empecé a tratar cada elemento como un posible objeto documental.

La diferencia parece pequeña.

Terminó cambiando toda la arquitectura del sistema.


El trabajo real sucede antes de los embeddings

Cuando alguien utiliza un RAG, lo que ve es una conversación.

Hace una pregunta.

El sistema recupera documentación.

El modelo responde.

Pero esa es únicamente la última etapa.

Antes han tenido que ocurrir muchas otras cosas:

  1. descubrir los archivos;
  2. identificarlos correctamente;
  3. comprobar si ya existían;
  4. detectar cambios;
  5. calcular una identidad de contenido;
  6. registrar metadatos;
  7. clasificarlos;
  8. determinar si pueden procesarse;
  9. decidir si merecen entrar en la memoria;
  10. y conservar trazabilidad sobre todo el recorrido.

Todo eso sucede mucho antes de generar un solo embedding.

Y precisamente ahí empezó realmente mi proyecto.


Construir un inventario parece una tarea aburrida

La palabra inventario no resulta especialmente atractiva.

No suena a inteligencia artificial, búsqueda semántica ni modelos generativos.

Suena a listado.

A hoja de cálculo.

A una tarea previa que conviene terminar cuanto antes para pasar a lo verdaderamente interesante.

Sin embargo, en mi caso acabó convirtiéndose en una de las piezas más importantes de todo el sistema.

Un inventario fiable permite saber:

  • qué existe;
  • qué ha cambiado;
  • qué se ha procesado;
  • qué ha fallado;
  • qué se ha omitido;
  • y qué necesita revisión.

También evita algo muy peligroso: creer que el RAG conoce toda la documentación cuando, en realidad, una parte nunca llegó a procesarse.

Un sistema puede responder de forma brillante y seguir teniendo enormes agujeros de conocimiento.

Si los fallos de ingesta no están registrados, el usuario ni siquiera sabrá que esos agujeros existen.

El inventario no es una lista auxiliar. Es el mapa de todo lo que el RAG conoce, desconoce o todavía no ha conseguido interpretar.


Mi primer recorrido recursivo

La primera necesidad fue localizar todos los archivos existentes bajo una ruta.

Para eso no hacía falta una plataforma compleja.

Bastaba con un pequeño script en Python.

Uno de los primeros ejemplos que cualquier persona puede probar es este:

from pathlib import Path

raiz = Path("/ruta/a/mis/documentos")

for archivo in raiz.rglob("*"):
    if archivo.is_file():
        print(archivo)

pathlib permite trabajar con rutas de una forma clara y bastante portable.

El método rglob("*") realiza una búsqueda recursiva por la carpeta indicada y sus subdirectorios accesibles.

En una herramienta más completa también conviene decidir expresamente cómo tratar los enlaces simbólicos, las rutas excluidas y los errores de permisos.

Este script no clasifica, valida ni procesa nada.

Únicamente descubre archivos.

Pero ya resuelve el primer problema: dejar de depender de lo que creemos que existe y empezar a observar lo que realmente hay en el disco.

El siguiente paso fue registrar algunos datos básicos:

from pathlib import Path
from datetime import datetime

raiz = Path("/ruta/a/mis/documentos")

for archivo in raiz.rglob("*"):
    if not archivo.is_file():
        continue

    estadisticas = archivo.stat()

    print({
        "ruta": str(archivo),
        "nombre": archivo.name,
        "extension": archivo.suffix.lower(),
        "tamano_bytes": estadisticas.st_size,
        "modificado": datetime.fromtimestamp(
            estadisticas.st_mtime
        ).isoformat()
    })

Con unas pocas líneas ya tenía una primera fotografía de la colección:

  • ruta completa;
  • nombre;
  • extensión;
  • tamaño;
  • fecha de modificación.

Era un comienzo.

Pero todavía no era un inventario fiable.


La extensión no es una prueba suficiente

Uno de los primeros errores habría sido clasificar los archivos únicamente por su extensión.

La extensión es útil.

Pero no es una prueba definitiva.

Un archivo llamado informe.pdf puede contener realmente una página HTML descargada incorrectamente, ser una imagen renombrada, estar vacío o haber quedado incompleto.

Lo mismo ocurre con documentos Word, imágenes, archivos comprimidos y muchos otros formatos.

Por eso empecé a distinguir entre dos datos:

  • la extensión declarada;
  • el tipo MIME estimado a partir de las firmas y del contenido del archivo.

En sistemas Linux puede utilizarse la biblioteca libmagic mediante módulos como python-magic.

Un ejemplo simplificado sería:

from pathlib import Path
import magic

archivo = Path("/ruta/documento.pdf")

tipo_mime = magic.from_file(
    str(archivo),
    mime=True
)

print(tipo_mime)

El resultado podría ser:

application/pdf

Pero también podría indicar que un supuesto PDF parece contener realmente:

text/html

Una comprobación básica podría escribirse así:

from pathlib import Path
import magic

archivo = Path("/ruta/documento.pdf")

extension = archivo.suffix.lower()
mime = magic.from_file(str(archivo), mime=True)

if extension == ".pdf" and mime != "application/pdf":
    print("La extensión no coincide con el contenido")

En una implementación práctica, la correspondencia debe ser más flexible.

Un DOCX, por ejemplo, es internamente un contenedor ZIP con una estructura determinada, y no todos los detectores devuelven exactamente el mismo resultado.

Lo importante no es crear una tabla perfecta desde el primer día.

Lo importante es dejar de confiar ciegamente en el nombre del archivo.

El nombre y la extensión describen lo que alguien dijo que era el archivo. El contenido aporta una segunda señal para comprobar qué puede contener realmente.


Python se convirtió en mi cuaderno de laboratorio

No elegí Python porque pensara que todo el sistema debía construirse con ese lenguaje.

Lo elegí porque me permitía probar ideas con rapidez.

Necesitaba automatizar tareas pequeñas:

  • recorrer carpetas;
  • leer metadatos;
  • calcular hashes;
  • detectar tipos de archivo;
  • contar extensiones;
  • buscar duplicados;
  • generar CSV;
  • consultar SQLite;
  • comparar ejecuciones;
  • y lanzar comprobaciones rápidas.

Python se convirtió en mi cuaderno de laboratorio.

No era el producto final.

Era el lugar donde formular una hipótesis, escribir unas líneas y observar qué ocurría.

Muchos scripts empezaron con veinte o treinta líneas.

Después aparecieron funciones reutilizables.

Más tarde llegaron los archivos de configuración, el registro de errores, las pruebas, SQLite y los estados persistentes.

El sistema no nació completo.

Fue creciendo a medida que aparecían nuevas necesidades y casos que no había previsto.

Esta forma de trabajar tiene una ventaja importante.

No obliga a diseñar una arquitectura enorme antes de comprender el problema.

Primero se observa.

Después se automatiza.

Y solo cuando una solución demuestra ser útil se convierte en una pieza estable.


La IA cambió mi forma de programar estas herramientas

Construir este sistema hoy tiene una diferencia importante respecto a cómo habría abordado el mismo proyecto hace algunos años.

Ahora desarrollo muchas de estas utilidades con el apoyo de modelos de lenguaje. Siempre que puedo trabajo con modelos locales, porque me permiten mantener la documentación, el código y el contexto dentro de mi propio equipo, sin enviarlos a servicios externos.

Los utilizo para explorar ideas, revisar pequeñas funciones, preparar pruebas, detectar casos límite y convertir problemas concretos en primeras versiones ejecutables.

Cuando necesito una revisión de código más exigente, contrastar una decisión de arquitectura o resolver un problema que requiere mayor capacidad de razonamiento, también recurro a asistentes como Claude, Codex o ChatGPT.

En esos casos no utilizo directamente la documentación de origen. Trabajo con problemas acotados, ejemplos controlados y contenidos preparados previamente. Si necesito utilizar algún fragmento relevante, lo simplifico y anonimizo antes con mis propias herramientas y modelos locales, eliminando nombres, rutas, identificadores y cualquier referencia que no sea necesaria para el análisis.

Pero utilizar asistentes de IA no consiste en escribir:

Constrúyeme un RAG completo para todos mis documentos.

Una petición tan amplia suele producir una arquitectura genérica, demasiadas suposiciones y código difícil de validar.

Mi forma de trabajar es bastante distinta.

Intento dividir cada problema en piezas pequeñas y verificables.

Por ejemplo:

  • recorrer una carpeta definiendo cómo deben tratarse los enlaces simbólicos;
  • calcular SHA-256 leyendo el archivo en bloques;
  • registrar errores de permisos sin detener toda la ejecución;
  • generar un CSV con rutas, tamaños y fechas;
  • comparar dos inventarios;
  • crear una tabla SQLite para registrar estados;
  • identificar extensiones desconocidas;
  • escribir una prueba para comprobar la idempotencia.

La IA puede proponer una primera implementación.

Después hay que leerla, entenderla, probarla y adaptarla.

La calidad del resultado depende menos de la IA utilizada que de la precisión con la que se define el problema y del rigor con el que se valida la solución.


Cómo planteo una petición útil a un asistente

En lugar de pedir un sistema entero, planteo una tarea concreta:

Escribe una función en Python que reciba una ruta, calcule su SHA-256 leyendo el archivo en bloques de un megabyte y devuelva el hash hexadecimal. No debe cargar el contenido completo en memoria.

La respuesta podría producir una función parecida a esta:

from pathlib import Path
import hashlib


def calcular_sha256(
    ruta: Path,
    tamano_bloque: int = 1024 * 1024
) -> str:
    hash_sha256 = hashlib.sha256()

    with ruta.open("rb") as fichero:
        while bloque := fichero.read(tamano_bloque):
            hash_sha256.update(bloque)

    return hash_sha256.hexdigest()

Después puedo pedir una revisión adicional:

Revisa la función anterior. Añade una excepción propia, manejo explícito de archivos inaccesibles y una prueba automatizada con un archivo temporal.

La IA no sustituye el criterio.

Ayuda a avanzar más rápido entre una idea y una primera versión ejecutable.

También resulta especialmente útil para tareas que consumen tiempo y atención:

  • crear pruebas unitarias;
  • documentar funciones;
  • interpretar excepciones;
  • refactorizar código repetido;
  • proponer estructuras de datos;
  • detectar casos límite;
  • comparar alternativas.

Pero hay una regla que intento mantener:

No incorporo al sistema código que no pueda explicar.

Si no entiendo cómo maneja los errores, qué supuestos introduce o qué ocurre ante una ruta extraña, todavía no está listo para formar parte de una canalización documental.

Una advertencia sobre los ejemplos

Los fragmentos de código de este artículo están pensados para explicar principios y servir como punto de partida. Antes de utilizarlos sobre una colección propia conviene añadir registro de errores, exclusiones, pruebas, control de permisos y copias de seguridad. Mi objetivo no es proporcionar un framework terminado, sino mostrar cómo fui convirtiendo problemas pequeños en componentes verificables.


De un listado a un inventario reproducible

Imprimir rutas en pantalla sirve para empezar.

Pero un inventario debe persistir.

Necesitaba guardar los resultados para poder compararlos, consultarlos y analizarlos después.

Una primera opción sencilla es generar un CSV:

from pathlib import Path
from datetime import datetime
import csv

raiz = Path("/ruta/a/mis/documentos")
salida = Path("inventario.csv")

with salida.open(
    "w",
    newline="",
    encoding="utf-8"
) as fichero_csv:

    campos = [
        "ruta",
        "nombre",
        "extension",
        "tamano_bytes",
        "modificado"
    ]

    escritor = csv.DictWriter(
        fichero_csv,
        fieldnames=campos
    )

    escritor.writeheader()

    for archivo in raiz.rglob("*"):
        if not archivo.is_file():
            continue

        datos = archivo.stat()

        escritor.writerow({
            "ruta": str(archivo),
            "nombre": archivo.name,
            "extension": archivo.suffix.lower(),
            "tamano_bytes": datos.st_size,
            "modificado": datetime.fromtimestamp(
                datos.st_mtime
            ).isoformat()
        })

Ese CSV puede abrirse con una hoja de cálculo, filtrarse por extensión, ordenarse por tamaño y utilizarse para detectar anomalías.

También permite responder pequeñas preguntas:

  • ¿Cuántos archivos declaran la extensión .pdf?
  • ¿Qué extensiones no reconozco?
  • ¿Qué archivos pesan cero bytes?
  • ¿Cuáles son los documentos más grandes?
  • ¿Qué rutas contienen más elementos?

Las pequeñas comprobaciones descubren problemas grandes

Una de las ventajas de trabajar con scripts pequeños es que permiten lanzar comprobaciones muy concretas.

Con pequeñas variaciones sobre el mismo recorrido podía detectar archivos vacíos, contar extensiones, localizar documentos excepcionalmente grandes o identificar nombres temporales creados por herramientas de sincronización.

Ninguna de estas comprobaciones era sofisticada por separado.

Pero juntas empezaron a revelar la forma real de la colección y las reglas que necesitaría la ingesta.

Si aparecían miles de ficheros temporales, habría que excluirlos.

Si existían muchos formatos antiguos, quizá sería necesaria una conversión previa.

Si abundaban las imágenes, el OCR tendría más importancia.

Si había archivos enormes, habría que establecer límites o rutas específicas.

Las reglas de una ingesta no deberían inventarse en abstracto. Deberían nacer de lo que el inventario revela sobre la colección que se quiere procesar.


El nombre del archivo no puede ser su identidad

Uno de los problemas más evidentes apareció cuando encontré archivos con nombres diferentes y contenido idéntico.

Por ejemplo:

guia_copias_seguridad_final.docx
guia_copias_seguridad_definitiva.docx
copia_guia_copias_seguridad.docx

Los nombres sugerían tres documentos.

Pero podían ser exactamente el mismo.

También podía ocurrir lo contrario.

Dos archivos podían compartir nombre y contener versiones diferentes.

La ruta tampoco resolvía el problema.

Un documento podía moverse de carpeta sin cambiar su contenido.

Necesitaba una forma de identificarlo independientemente del nombre y de la ubicación.

Ahí aparecieron los hashes.


El hash como huella del contenido

Una función hash recibe datos y genera una huella de longitud fija.

En mi inventario utilizo hashes criptográficos, como SHA-256, para obtener una identidad basada en el contenido del archivo.

from pathlib import Path
import hashlib

def sha256_archivo(ruta: Path) -> str:
    hash_objeto = hashlib.sha256()

    with ruta.open("rb") as fichero:
        for bloque in iter(
            lambda: fichero.read(1024 * 1024),
            b""
        ):
            hash_objeto.update(bloque)

    return hash_objeto.hexdigest()

El archivo se lee en bloques para evitar cargarlo completamente en memoria.

Esto resulta especialmente importante cuando la colección contiene documentos grandes.

Dos archivos con el mismo SHA-256 pueden considerarse, a efectos prácticos del inventario, copias del mismo contenido.

El inventario conserva además el tamaño y la ruta, y puede realizar una comprobación byte a byte si alguna situación exige una confirmación adicional.

Esto permite detectar duplicados incluso cuando:

  • el nombre es diferente;
  • la ruta ha cambiado;
  • la fecha de modificación es distinta;
  • o el archivo se ha copiado entre varios proyectos.

El hash no explica qué contiene el documento.

Pero ayuda a responder una pregunta fundamental:

¿He visto ya exactamente estos mismos bytes?


Identidad física e identidad de contenido

Con el tiempo descubrí que convenía distinguir dos conceptos.

La identidad física describe una aparición concreta del archivo:

  • ruta;
  • nombre;
  • tamaño;
  • fechas;
  • permisos;
  • ubicación.

La identidad de contenido representa los bytes almacenados en ese archivo.

Varias rutas físicas pueden contener exactamente el mismo contenido.

Por ejemplo, una guía técnica puede estar copiada en tres carpetas diferentes y conservar el mismo hash.

Eso no significa necesariamente que deba borrar automáticamente dos copias.

La ubicación también puede tener significado.

Una copia dentro de una carpeta temática puede ayudar a entender en qué contexto guardé o utilicé ese documento.

Deduplicar no es borrar sin pensar. Es reconocer qué elementos son iguales y decidir qué relación debe conservarse entre ellos.


La deduplicación no termina en las carpetas

La identidad por contenido no debería limitarse a los archivos encontrados directamente durante el recorrido inicial.

Un mismo contenido puede reaparecer como adjunto de un correo, dentro de un archivo comprimido, como copia en otro proyecto o como resultado de una conversión.

El inventario debe reconocer que esas apariciones comparten contenido sin perder la procedencia de cada una.

Por eso la deduplicación no consiste únicamente en evitar procesar dos veces el mismo archivo. También debe conservar la relación entre el contenido y todos los lugares, contenedores o procesos desde los que apareció.

La igualdad se determina por contenido. La trazabilidad se conserva por procedencia.


Generar una referencia manejable sin perder la identidad completa

El SHA-256 completo debe conservarse como identidad canónica del contenido.

Para registros, nombres de carpetas o visualización puede resultar útil generar además una referencia abreviada de uso operativo:

def crear_referencia_contenido(hash_sha256: str) -> str:
    return f"doc_{hash_sha256[:32]}"

Un resultado podría ser:

doc_a4e71b6c1f3290de72c8f4390b61d547

Esa referencia resulta más cómoda para trabajar, pero no sustituye al hash completo.

El inventario conserva ambos: la referencia abreviada para el uso operativo y el SHA-256 completo para verificar la identidad del contenido.

Si el contenido cambia, también cambia el hash. Por tanto, una nueva versión del documento tendrá una identidad de contenido diferente, aunque pueda seguir vinculada a la misma línea documental.


Los metadatos convierten un archivo en algo consultable

Un hash identifica contenido, pero no aporta contexto suficiente.

Para construir un inventario útil necesitaba conservar metadatos.

Algunos proceden directamente del sistema de archivos:

  • ruta;
  • nombre;
  • extensión;
  • tamaño;
  • fecha de modificación.

Otros se obtienen mediante análisis:

  • hash;
  • tipo MIME detectado o estimado;
  • estado de validación;
  • presencia de duplicados;
  • posible corrupción;
  • procesador previsto o ruta de tratamiento pendiente.

Y otros ayudan a describir el contexto en el que guardé cada contenido:

  • tema;
  • colección;
  • categoría;
  • idioma;
  • fecha o periodo;
  • grado de privacidad;
  • carácter histórico;
  • procedencia.

En los ejemplos siguientes acorto visualmente el SHA-256 para facilitar la lectura, aunque el inventario conserva siempre sus 64 caracteres hexadecimales.

Un registro simplificado podría representarse así:

archivo = {
    "archivo_id": 1842,
    "content_ref": "doc_a4e71b6c1f3290de72c8f4390b61d547",
    "ruta": "/biblioteca/tecnologia/manual.pdf",
    "nombre": "manual.pdf",
    "extension": ".pdf",
    "mime": "application/pdf",
    "tamano_bytes": 483920,
    "sha256": "<SHA-256 completo de 64 caracteres>",
    "estado": "descubierto",
    "categoria": "manual",
    "es_duplicado": False
}

A partir de ese momento, cada aparición física deja de ser únicamente una ruta y queda relacionada con una identidad de contenido verificable.

Se convierte en un objeto que puede consultarse, compararse y seguirse a lo largo de toda la canalización.


Los documentos necesitan estados

Al principio podía parecer suficiente distinguir entre archivos procesados y no procesados.

Pronto descubrí que la realidad era más compleja.

Un documento puede existir y todavía no haber sido analizado.

Puede haber sido identificado, pero contener un formato no soportado.

Puede necesitar OCR.

Puede haber fallado por corrupción.

Puede haberse extraído correctamente, pero quedar pendiente de revisión.

Puede estar duplicado, obsoleto, restringido o excluido por una regla.

Por eso empecé a utilizar estados explícitos.

Por ejemplo:

  • descubierto;
  • identificado;
  • pendiente;
  • procesando;
  • extraido;
  • aprobado;
  • revision;
  • cuarentena;
  • omitido;
  • error;
  • obsoleto;
  • retirado.

Estos nombres son ilustrativos. Cada sistema debe definir el conjunto mínimo de estados que necesita, qué significa exactamente cada uno y qué transiciones están permitidas.

No basta con registrar el estado actual. También conviene impedir que un documento pase, por ejemplo, de descubierto a aprobado sin atravesar las comprobaciones intermedias necesarias.

Por ejemplo, extraido no debería equivaler automáticamente a aprobado.

Conseguir texto de un archivo no significa que ese texto deba entrar en la memoria.

La extracción es una operación técnica. La aprobación es una decisión documental.


Los originales no deben modificarse

Una de las decisiones que adopté desde el principio fue tratar los documentos originales como inmutables.

El inventario puede leerlos.

Puede calcular su hash.

Puede extraer metadatos.

Puede registrar las copias derivadas que genere la canalización.

Pero no debería modificarlos.

Esto resulta especialmente importante cuando aparecen procesos como:

  • conversión de formatos antiguos;
  • OCR;
  • limpieza;
  • anonimización o eliminación de información innecesaria;
  • normalización de texto;
  • generación de Markdown;
  • creación de imágenes intermedias.

El resultado de esas operaciones debe almacenarse como un derivado.

Una estructura conceptual podría ser:

originales/
    apuntes.doc
    manual.pdf

derivados/
    apuntes/
        convertido.docx
        contenido.md
        metadata.json

    manual/
        contenido.md
        paginas/
        metadata.json

Así puedo repetir el procesamiento con otras herramientas sin perder la fuente original.

También puedo auditar qué resultado se obtuvo, con qué versión del procesador y a partir de qué documento.

El inventario registra los originales. La canalización genera y transforma derivados sin alterar la fuente.


La relación entre original y derivados

Para mantener trazabilidad, cada derivado debe indicar claramente de qué original procede.

Un archivo de metadatos podría contener:

{
  "content_ref": "doc_a4e71b6c1f3290de72c8f4390b61d547",
  "source_path": "/originales/manual.pdf",
  "source_sha256": "<SHA-256 completo de 64 caracteres>",
  "processor": "procesador_pdf",
  "processor_version": "1.4.0",
  "generated_files": [
    "contenido.md",
    "metadata.json"
  ],
  "status": "extraido"
}

Esta información permite responder preguntas que más adelante resultan fundamentales:

  • ¿Con qué herramienta se procesó este documento?
  • ¿Qué versión se utilizó?
  • ¿El original ha cambiado desde entonces?
  • ¿Qué derivados pertenecen a esa versión?
  • ¿Es necesario volver a procesarlo?

Sin esta relación, una carpeta de resultados puede convertirse rápidamente en otro almacén difícil de interpretar.


La cuarentena no es una papelera

Durante el inventario aparecen archivos que no deberían procesarse automáticamente.

Por ejemplo:

  • documentos corruptos;
  • extensiones desconocidas;
  • archivos incompletos;
  • contenidos sospechosos;
  • ficheros con errores de estructura;
  • documentos protegidos;
  • formatos para los que todavía no existe un procesador;
  • elementos que requieren una decisión humana.

Es tentador ignorarlos.

Pero ignorar silenciosamente es una mala práctica.

Prefiero enviarlos a un estado de cuarentena.

La cuarentena no significa que el archivo sea inútil.

Significa que el sistema no puede procesarlo con suficiente confianza.

Cada elemento debería conservar:

  • la ruta original;
  • el hash;
  • el tipo detectado;
  • el motivo de la cuarentena;
  • la fecha;
  • la excepción técnica;
  • la acción recomendada.
{
  "content_ref": "doc_91d0ca2035f62b114c21e8f0192a783d",
  "status": "cuarentena",
  "reason": "extension_mime_mismatch",
  "declared_extension": ".pdf",
  "detected_mime": "text/html",
  "recommended_action": "revisar_origen"
}

La cuarentena convierte un fallo silencioso en una tarea visible.

Y eso mejora la fiabilidad del sistema.


Repetir una ejecución no debería duplicar el trabajo

Cuando ejecuté los primeros scripts, cada recorrido comenzaba desde cero.

Para una colección pequeña no era un problema.

Pero, a medida que aumentaba el número de documentos, dejó de tener sentido volver a calcularlo todo en cada ejecución.

Necesitaba que repetir el inventario sobre los mismos archivos no creara registros duplicados ni obligara a rehacer todo el trabajo.

Si nada había cambiado, el resultado final debía seguir siendo el mismo. En ingeniería de software, esta propiedad se conoce como idempotencia.

Si un archivo no ha cambiado, no debería tratarse como nuevo.

Si ya se procesó correctamente con la misma versión del procesador y la misma configuración relevante, debería poder reutilizarse el resultado.

Si falló, debería reintentarse de forma controlada.

Si deja de encontrarse dentro del alcance de una ejecución, el inventario debería registrarlo sin asumir automáticamente que ha sido eliminado.

Una ejecución repetida debe actualizar el conocimiento del sistema, no reconstruirlo todo ciegamente.


Comparar el estado anterior con el actual

Una estrategia sencilla consiste en guardar el inventario anterior y compararlo con el nuevo.

Utilizando el hash y la ruta se pueden detectar varios casos:

  • nuevo: aparece una ruta que antes no existía;
  • sin cambios: la ruta y el hash siguen iguales;
  • modificado: la ruta existe, pero el hash ha cambiado;
  • posiblemente movido: el contenido aparece en una ruta nueva y deja de encontrarse en la anterior;
  • no encontrado: una ruta registrada anteriormente ya no aparece dentro del alcance de la ejecución actual;
  • duplicado: varias rutas comparten el mismo hash.

Un ejemplo conceptual:

def clasificar_cambio(
    registro_anterior,
    registro_actual
):
    if registro_anterior is None:
        return "nuevo"

    if (
        registro_anterior["sha256"]
        == registro_actual["sha256"]
    ):
        return "sin_cambios"

    return "modificado"

La lógica real puede ser bastante más compleja, pero el principio es sencillo.

No hace falta volver a extraer un documento para descubrir si su contenido sigue siendo el mismo.


SQLite apareció de forma natural

Los CSV son muy útiles para inspeccionar y compartir resultados.

Pero cuando el inventario empezó a necesitar consultas, actualizaciones y relaciones, SQLite se convirtió en una opción natural.

La base de datos principal se almacena en un archivo local y no requiere desplegar un servidor independiente.

Python incluye soporte mediante el módulo sqlite3.

Como había distinguido entre la identidad del contenido y cada aparición física del archivo, la base de datos debía conservar también esa separación.

Además del SHA-256 completo, decidí guardar un identificador abreviado para utilizarlo en registros, rutas de derivados y referencias operativas.

import sqlite3

conexion = sqlite3.connect("inventario.db")

# Las claves foráneas deben habilitarse
# explícitamente en cada conexión.
conexion.execute("PRAGMA foreign_keys = ON")

conexion.executescript("""
CREATE TABLE IF NOT EXISTS contenidos (
    sha256 TEXT PRIMARY KEY,
    content_ref TEXT NOT NULL UNIQUE,
    tamano_bytes INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS archivos (
    archivo_id INTEGER PRIMARY KEY AUTOINCREMENT,
    sha256 TEXT NOT NULL,
    ruta TEXT NOT NULL UNIQUE,
    nombre TEXT NOT NULL,
    extension TEXT,
    mime TEXT,
    estado TEXT NOT NULL,
    fecha_modificacion TEXT,
    fecha_inventario TEXT NOT NULL,
    processor_version TEXT,
    FOREIGN KEY (sha256)
        REFERENCES contenidos (sha256)
);

CREATE INDEX IF NOT EXISTS idx_archivos_sha256
ON archivos (sha256);

CREATE INDEX IF NOT EXISTS idx_archivos_estado
ON archivos (estado);
""")

conexion.commit()
conexion.close()

Este esquema es deliberadamente simplificado. En una implementación más completa, las altas y actualizaciones del inventario deberían ejecutarse dentro de transacciones controladas para evitar estados parciales si el proceso se interrumpe.

Esta estructura representa dos realidades diferentes:

  • contenidos conserva el SHA-256 completo, el identificador operativo abreviado y el tamaño del contenido;
  • archivos registra cada aparición física, su ubicación y su estado de procesamiento.

Por ejemplo, un mismo contenido podría quedar identificado así:

contenido = {
    "sha256": "<SHA-256 completo de 64 caracteres>",
    "content_ref": "doc_a4e71b6c1f3290de72c8f4390b61d547",
    "tamano_bytes": 483920
}

El SHA-256 completo sigue siendo la identidad canónica del contenido.

content_ref es únicamente una referencia abreviada y manejable. Resulta útil para nombrar carpetas de derivados, relacionar artefactos y mostrar identificadores en registros, pero no sustituye al hash completo.

Así, varias rutas pueden apuntar al mismo contenido sin que el inventario pierda el contexto de cada copia.

SQLite permitió hacer preguntas como estas:

  • ¿Cuántos documentos están en cuarentena?
  • ¿Qué contenidos aparecen en más de una ruta?
  • ¿Qué documentos fallaron con un procesador concreto?
  • ¿Cuáles fueron modificados desde la última ejecución?
  • ¿Qué formatos siguen sin soporte?

Por ejemplo, para localizar contenidos presentes en varias rutas:

SELECT
    sha256,
    COUNT(*) AS apariciones
FROM archivos
GROUP BY sha256
HAVING COUNT(*) > 1;

También podría recuperar la referencia abreviada asociada a cada contenido:

SELECT
    contenidos.content_ref,
    contenidos.sha256,
    COUNT(archivos.archivo_id) AS apariciones
FROM contenidos
JOIN archivos
    ON archivos.sha256 = contenidos.sha256
GROUP BY
    contenidos.content_ref,
    contenidos.sha256
HAVING COUNT(archivos.archivo_id) > 1;

En ese momento el inventario dejó de ser únicamente un archivo de resultados.

Se convirtió en una memoria operativa del proceso documental.


Registrar cada ejecución

Además de los documentos, empecé a registrar las ejecuciones.

Eso permite saber:

  • cuándo comenzó cada proceso;
  • cuándo terminó;
  • cuántos archivos descubrió;
  • cuántos eran nuevos;
  • cuántos habían cambiado;
  • cuántos fallaron;
  • qué versión del script se utilizó;
  • si la ejecución finalizó correctamente.

Una tabla simplificada podría ser:

CREATE TABLE ejecuciones (
    run_id TEXT PRIMARY KEY,
    inicio TEXT NOT NULL,
    fin TEXT,
    version_script TEXT,
    descubiertos INTEGER DEFAULT 0,
    nuevos INTEGER DEFAULT 0,
    modificados INTEGER DEFAULT 0,
    errores INTEGER DEFAULT 0,
    estado TEXT NOT NULL
);

Esta trazabilidad ayuda a interpretar patrones.

Si una ejecución comienza a acumular muchos fallos repentinos en formatos distintos, quizá el problema no esté en los documentos.

Puede haber fallado una herramienta de extracción, una dependencia o el entorno en el que se está ejecutando el proceso.

Sin el contexto de la ejecución, todos esos errores parecerían casos aislados.


Reanudar es casi tan importante como procesar

Una canalización documental puede tardar horas.

Puede detenerse por un reinicio, una pérdida de conexión, un archivo problemático o un fallo del servicio encargado de analizar documentos.

Si cada interrupción obliga a empezar desde cero, el sistema no escala.

Por eso cada documento debe conservar su estado.

Al reiniciar, el proceso puede seleccionar únicamente:

  • documentos nuevos;
  • documentos modificados;
  • fallos reintentables;
  • elementos pendientes;
  • documentos procesados con una versión antigua.
SELECT *
FROM archivos
WHERE estado IN (
    'pendiente',
    'error_reintentable'
);

También podría localizar documentos que deban regenerarse porque cambió el procesador:

SELECT *
FROM archivos
WHERE processor_version IS NULL
   OR processor_version != '2.0.0';

En una canalización documental, reanudar bien es casi tan importante como procesar bien.


El inventario también necesita pruebas

Un script que termina sin mostrar errores no es necesariamente correcto.

Puede haber omitido carpetas.

Puede haber seguido enlaces simbólicos y entrado en un ciclo.

Puede haber duplicado registros.

Puede haber cambiado identificadores que deberían ser estables.

Por eso empecé a crear pequeñas pruebas.

Por ejemplo, comprobar que el hash de un contenido conocido es siempre el mismo:

from pathlib import Path

def test_hash_estable(tmp_path: Path):
    archivo = tmp_path / "prueba.txt"
    archivo.write_text(
        "contenido de prueba",
        encoding="utf-8"
    )

    primer_hash = sha256_archivo(archivo)
    segundo_hash = sha256_archivo(archivo)

    assert primer_hash == segundo_hash

O verificar que una segunda ejecución no crea otro registro:

def test_inventario_idempotente(tmp_path):
    archivo = tmp_path / "documento.txt"
    archivo.write_text("hola", encoding="utf-8")

    ejecutar_inventario(tmp_path)
    ejecutar_inventario(tmp_path)

    cantidad = contar_registros(
        ruta=str(archivo)
    )

    assert cantidad == 1

También pueden prepararse colecciones de prueba con casos controlados:

  • dos archivos idénticos con nombres diferentes;
  • un archivo vacío;
  • una extensión incorrecta;
  • un documento sin permisos;
  • enlaces simbólicos a archivos y directorios;
  • un archivo modificado entre ejecuciones;
  • un fichero registrado anteriormente que deja de encontrarse;
  • un formato desconocido.

Estas pruebas permiten cambiar el código sin depender únicamente de revisar manualmente grandes cantidades de registros.


Pequeños scripts, responsabilidades claras

Una de las decisiones que más me ha ayudado ha sido evitar que un único script hiciera absolutamente todo.

Es tentador crear una herramienta que descubra archivos, calcule hashes, convierta formatos, extraiga texto, genere fragmentos, calcule embeddings y actualice la base vectorial.

Pero cuando algo falla resulta mucho más difícil saber dónde está el problema.

Prefiero separar responsabilidades.

Por ejemplo:

discover.py
identify.py
hash_files.py
classify.py
inventory_db.py
validate.py
report.py

En un proyecto más maduro estas funciones pueden formar parte de un mismo paquete.

Pero conceptualmente siguen representando operaciones diferentes.

La separación permite:

  • probar cada etapa;
  • repetir solo lo necesario;
  • sustituir una herramienta sin rehacer todo;
  • comparar resultados;
  • entender mejor los errores.

También facilita trabajar con asistentes de IA.

Es mucho más sencillo pedir a Claude, Codex o ChatGPT que revise una función con una responsabilidad concreta que entregar un programa de miles de líneas y preguntar si está bien.


Cómo puede empezar un lector su propio inventario

No hace falta construir desde el primer día una base de datos completa con estados, versiones y procesadores.

Empezaría con una carpeta pequeña y representativa y seguiría este recorrido:

  1. generar un listado con ruta, nombre, extensión, tamaño y fecha;
  2. contar las extensiones y, cuando sea posible, los tipos MIME detectados;
  3. calcular hashes para detectar copias idénticas;
  4. comparar la extensión con el tipo estimado del contenido;
  5. asignar estados sencillos como descubierto, duplicado, vacío, desconocido o pendiente;
  6. guardar el resultado inicialmente en CSV;
  7. migrarlo a SQLite cuando empiece a necesitar relaciones y consultas;
  8. repetir la ejecución y comprobar que no genera registros duplicados.

Ese pequeño proyecto ya permite descubrir muchos de los problemas habituales de una colección. La arquitectura puede crecer después, cuando el propio inventario muestre qué necesidades existen.


Utilizar la IA también para pensar el diseño

No siempre pido código inmediatamente.

A veces utilizo estos asistentes para identificar casos que todavía no he considerado.

Por ejemplo:

Necesito distinguir entre archivos nuevos, modificados, posiblemente movidos y no encontrados al comparar dos inventarios. Propón primero la estructura de datos, los casos ambiguos y las pruebas necesarias, sin escribir todavía el programa completo.

Este tipo de petición obliga a definir el problema antes de implementar una solución.

La IA puede ayudarme a enumerar riesgos, detectar supuestos y proponer pruebas. Después sigo necesitando contrastar sus propuestas con casos controlados y con el comportamiento observable del sistema.

La IA resulta más útil cuando participa en el razonamiento y en la revisión, no solo cuando genera líneas de código.


Lo que aprendí construyendo el inventario

Al empezar pensaba que el inventario sería una fase breve.

Un paso previo antes de entrar en la inteligencia artificial.

Terminó siendo el lugar donde comprendí realmente mi colección documental.

Descubrí formatos que había olvidado, copias que no sabía que existían, documentos dañados y áreas donde el nombre de los archivos ya no permitía entender nada.

También descubrí que una colección documental tiene una historia.

Las herramientas cambian.

Los formatos evolucionan.

Los proyectos generan copias.

Los sistemas de sincronización dejan ficheros temporales.

Las personas utilizan nombres diferentes para describir lo mismo.

El inventario no elimina esa historia.

La hace visible.


Una carpeta no equivale a conocimiento controlado

Una carpeta puede indicar dónde se encuentra un archivo.

Pero no responde por sí sola a preguntas como estas:

  • ¿es la versión vigente?
  • ¿está duplicado?
  • ¿puede procesarse?
  • ¿contiene información que prefiero no incorporar?
  • ¿se ha extraído correctamente?
  • ¿debería aparecer en las respuestas?
  • ¿qué versión del procesador se utilizó?
  • ¿qué derivados se generaron?

Para responderlas hace falta un catálogo.

Ese catálogo no sustituye a la estructura de carpetas.

La complementa.

La ruta sigue siendo un metadato valioso.

Puede indicar tema, colección, etapa o contexto.

Pero deja de ser la única forma de comprender el documento.

Una carpeta organiza ubicaciones. Un inventario organiza conocimiento sobre los documentos.


El catálogo es el verdadero primer componente

Con el inventario construido, cada documento empieza a tener una identidad y una historia.

Sé dónde apareció.

Sé si tiene duplicados.

Sé qué formato declara y cuál parece ser su tipo real.

Sé si está pendiente, extraído, aprobado, omitido o en cuarentena.

Sé qué resultados derivaron de él.

Y puedo saber si ha cambiado desde la última ejecución.

Eso no es todavía un RAG completo.

Pero ya es una base sólida sobre la que construirlo.

Sin ese catálogo, cada etapa posterior trabaja con suposiciones.

La extracción supone que el archivo es válido.

El chunking supone que el texto está completo.

El modelo de embeddings supone que el fragmento merece indexarse.

La base vectorial supone que el contenido sigue vigente.

Y el usuario supone que el sistema ha buscado en toda la documentación relevante.

Demasiadas suposiciones para un sistema que pretende convertirse en memoria.


El inventario también enseña cuándo no utilizar inteligencia artificial

Durante este proceso confirmé algo importante.

No todos los problemas necesitan un modelo.

Para descubrir archivos utilizo el sistema de archivos.

Para calcular identidades utilizo hashes.

Para detectar tamaños utilizo metadatos.

Para contar extensiones utilizo reglas deterministas.

Para comparar ejecuciones utilizo una base de datos.

La inteligencia artificial puede ayudar a escribir, revisar o mejorar esas herramientas.

Pero la operación final no necesita ser probabilística.

Los sistemas fiables suelen combinar varios enfoques:

  • reglas deterministas donde la respuesta debe ser exacta;
  • parsers especializados donde existe una estructura conocida;
  • modelos de visión u OCR donde el contenido no es directamente accesible;
  • embeddings donde interesa recuperar por significado;
  • modelos generativos donde hace falta interpretar o redactar.

Utilizar inteligencia artificial para todo no hace necesariamente mejor el sistema.

A veces solo lo hace más difícil de explicar, probar y mantener.

La buena ingeniería no consiste en introducir IA en cada etapa. Consiste en elegir la herramienta adecuada para cada problema.


Mi criterio hoy

Si hoy tuviera que empezar de nuevo, no comenzaría instalando una base vectorial.

Tampoco elegiría primero un modelo de embeddings.

Empezaría construyendo una fotografía fiable de la documentación.

El orden sería parecido a este:

  1. recorrer una colección pequeña;
  2. registrar rutas y metadatos básicos;
  3. contrastar la extensión con el tipo detectado;
  4. calcular hashes;
  5. detectar duplicados;
  6. definir estados;
  7. separar originales y derivados;
  8. registrar errores y cuarentenas;
  9. hacer el proceso idempotente;
  10. comprobar que puede reanudarse.

Solo después empezaría a pensar en cómo extraer el contenido.

Puede parecer un camino más lento.

En realidad evita rehacer gran parte del trabajo cuando la colección crece.

También evita una de las peores situaciones posibles: tener un RAG aparentemente operativo sin saber qué documentos quedaron fuera.


El inventario resolvió una pregunta y abrió la siguiente

Al terminar esta etapa ya no tenía únicamente una colección de carpetas.

Tenía un catálogo capaz de decirme qué archivos existían, cuáles compartían el mismo contenido, qué tipo de archivo parecía corresponder a cada uno, qué elementos necesitaban revisión y qué había ocurrido en cada ejecución.

Eso cambió la naturaleza del proyecto.

Por primera vez podía saber qué parte de mi documentación estaba inventariada y bajo control técnico, y qué parte seguía siendo desconocida o necesitaba revisión.

Pero el inventario tenía un límite evidente.

Ya podía identificar cada aparición física y relacionar los archivos que compartían exactamente el mismo contenido, pero todavía no podía extraer correctamente el conocimiento que guardaban.

Un PDF con texto, un PDF escaneado, una hoja de cálculo, un documento Word, un XML o una imagen pueden contener información útil. Sin embargo, su estructura y sus necesidades de procesamiento son diferentes.

No tenía sentido obligarlos a recorrer una única cadena de extracción.

El siguiente paso fue construir un sistema que utilizara el inventario para decidir qué tratamiento debía recibir cada archivo según su extensión declarada, el tipo detectado o estimado, su estructura, su estado y las necesidades de extracción.

Ese sistema se convirtió en el router documental.


📖 Siguiente artículo de la serie: El router documental: decidir cómo procesar cada archivo antes de tocarlo

Explicaré cómo separé las rutas de procesamiento para PDF, documentos Word, hojas de cálculo, XML, imágenes y archivos escaneados; por qué el OCR no debe aplicarse indiscriminadamente; y cómo aprendí a distinguir entre el fallo de un documento concreto, el fallo del procesador asociado a un formato y un problema general del entorno de extracción.


Un RAG no empieza cuando un modelo responde una pregunta. Empieza cuando puedes explicar qué documentación existe, cómo has identificado cada archivo y cuánto puedes confiar en el proceso que lo convierte en conocimiento recuperable.

Continúa leyendo

Monitoriza todo tu homelab con Grafana, Prometheus y Uptime Kuma

¿Quieres seguir explorando?

Puedes descubrir más artículos, seguir mis publicaciones en LinkedIn o escribirme.

Deja un comentario

Julio Rodas

Tecnología sin humo. Cómo pienso, pruebo y tomo decisiones tecnológicas.

Cómo trabajo  ·  Sobre mí  ·  Artículos  ·  Contacto

Invítame a un café

Aviso Legal  ·  Política de Privacidad  ·  Política de Cookies

Las opiniones expresadas en esta web son personales y no representan necesariamente la posición de mi empleador ni de ninguna organización con la que mantenga una relación profesional.

© 2026 Julio Rodas