TL;DR
Estoy canalizando eso que compila una carpeta de notas de texto sin procesar y desordenadas en una wiki de rebajas vinculada y con pelusas. Sin llamadas LLM, sin incrustaciones, sin API externas, solo biblioteca estándar. La canalización tiene cuatro etapas: un extractor de expresiones regulares, un generador de gráficos que detecta referencias cruzadas, una reescritura con reconocimiento de secciones que conserva todo lo que escribe a mano y un linter que verifica su propia salida. Encontré dos errores reales mientras construía esto: un generador de gráficos que escalaba mal y un linter que silenciosamente subestimaba las páginas huérfanas. Ambos se encuentran en este artículo tal como sucedieron, junto con las correcciones. Realicé una evaluación comparativa del proceso completo en tres tamaños de corpus en dos máquinas diferentes (Linux y Windows) y verifiqué si las salidas deterministas realmente coincidían en ambas. Lo hicieron, exactamente. A continuación se incluyen el código completo, las 17 pruebas y la salida del terminal sin redondear para que pueda volver a ejecutar todo usted mismo.
¿Por qué escribí esto?
Intenté crear una wiki de LLM al estilo Karpathy. Bucles de agentes. Llamadas LLM recursivas. Incrustaciones para todo.
La entrada fue una carpeta de archivos de rebajas locales que ya tenía, en mi propio disco.
Y a mitad de camino, me di cuenta: estaba pagando tokens para reorganizar el texto que ya tenía.
Entonces reemplacé todo el proceso con un compilador de Python puro.
Este artículo analiza ese sistema en su totalidad: convierta una carpeta de notas de texto sin formato y con formato inconsistente en un wiki de rebajas vinculado y con enlaces, sin llamadas LLM, cero API externas y cero dependencias de terceros. Cada número de referencia a continuación es real, se ejecutó en dos máquinas diferentes (un contenedor Linux y mi propia PC con Windows) y he incluido los dos errores reales que encontré mientras lo construía.
Si está buscando un compilador wiki de rebajas de Python puro, una alternativa determinista a las herramientas de base de conocimientos basadas en agentes o un desglose práctico de la creación de una alternativa RAG local, este es el artículo.
Código completo: https://github.com/Emmimal/wiki-compiler/
La mentalidad del compilador
Aquí está la reformulación en la que se basa el resto de este artículo:
Un agente decide cómo se verá tu wiki. Un compilador garantiza cómo debe verse.
Quería que esta wiki fuera predecible. A diferencia de un LLM que varía su resultado, un compilador le brinda el mismo resultado cada vez que lo ejecuta. Esa coherencia es esencial para mis notas de referencia personales. He estructurado este sistema para que las páginas de rebajas actúen como archivos objeto. Se generan desde la fuente y se pueden reconstruir a voluntad. Mantengo el contenido editado a mano separado de las secciones generadas por máquina. Así es como se configura la canalización:
Dividí esto en cuatro etapas. Cada uno maneja una única tarea determinista y puede probarse por sí solo. Evité cualquier paso que dependa de un modelo para tomar una decisión.
Por qué las dependencias cero son importantes aquí, específicamente
Todo en este código base se ejecuta únicamente en la biblioteca estándar de Python. Sin transformadores de oraciones, sin bases de datos vectoriales, sin cliente HTTP para una API integrada. Esa no es una prueba de pureza en sí misma. Es una consecuencia directa del problema que resuelve este oleoducto.
Una vez que elimina las llamadas de LLM, lo que realmente queda por hacer es el análisis de texto, la manipulación de cadenas y el recorrido de gráficos en un diccionario en memoria. Esos son exactamente los tipos de problemas para los que se crearon las estructuras de datos re, os y simples de Python. Alcanzar una dependencia más pesada aquí no compraría corrección, solo compraría fricción de instalación y una cosa más que puede romperse por razones que no tienen nada que ver con sus notas reales. Si alguna vez se le bloqueó la instalación de pip en Windows porque una rueda compilada para una biblioteca de aprendizaje automático no estaba disponible para su versión de Python, ya sabe por qué vale la pena proteger "simplemente se ejecuta".
El problema con los wikis controlados por agentes
La idea de utilizar un LLM para crear y mantener una wiki personal no es nueva y no es mía. Ganó mucha fuerza después de que Andrej Karpathy describió el patrón en una publicación ampliamente compartida, donde explicó que estaba gastando menos de su presupuesto simbólico en generar código y más en construir bases de conocimiento estructuradas y persistentes a partir de sus notas de investigación. Continuó con un “archivo de ideas” público que presenta la arquitectura con mayor profundidad y comparó explícitamente el proceso con la compilación: entran las fuentes sin procesar, sale una wiki estructurada con referencias cruzadas y el LLM es quien realiza la compilación.[1][2].
Creo que el encuadre de la compilación es exactamente correcto. Simplemente no creo que un LLM deba ser el compilador.
Aquí está el problema práctico. Si su fuente sin procesar ya es local, ya es texto y ya es determinista, enrutarla a través de un sistema probabilístico para organizarla introduce tres costos que un analizador o un compilador simplemente no tiene:
Costo: cada vez que agrega un documento nuevo, un wiki controlado por agentes vuelve a leer el contenido, decide qué cambió y reescribe las páginas. Se trata de un gasto simbólico en trabajo organizativo, no de síntesis. Se acumula rápidamente una vez que su carpeta de origen tiene cientos de archivos en lugar de una docena.
Latencia: cada ciclo de lectura, decisión y escritura es un viaje de ida y vuelta en la red si utiliza un modelo alojado y un costo de cálculo real incluso si ejecuta algo local. Para un trabajo que consiste fundamentalmente en reestructurar texto existente, esa latencia no tiene razón de existir.
No determinismo: este es el que realmente me mordió. Ejecuté dos veces la misma carpeta a través de uno de los primeros prototipos basados en agentes y obtuve dos estructuras de enlaces diferentes. Nada había cambiado en los archivos fuente. El modelo simplemente hizo juicios ligeramente diferentes en ambas ocasiones sobre lo que se consideraba relacionado. Para el código, eso a veces resulta encantador. Para lo que estás usando como fuente de verdad, es un problema.
Nada de esto significa que los LLM sean la herramienta equivocada para el trabajo del conocimiento en general. Significa que son la herramienta equivocada para la parte específica del trabajo que en realidad es determinista: tomar entradas conocidas y producir una estructura conocida y reproducible. Esa parte es un problema de análisis, no de razonamiento.
Paso 1: el extractor de metadatos de expresiones regulares
Las carpetas de notas reales son un desastre. Algunos archivos usan un encabezado #, algunos usan una línea mayúscula simple y otros no tienen ningún encabezado. Es posible que falten metadatos como "creado:" o "alias:" o que estén ocultos en medio del archivo. Cualquier extractor que espere un formato consistente se rompe en el momento en que llega a un archivo del mundo real, así que construí el mío para manejar el desorden.
Primero busca un encabezado #. Si eso falla, busca una línea en mayúsculas. Si aún no encuentra nada, el valor predeterminado es el nombre del archivo. Busca campos de metadatos dondequiera que existan en lugar de exigir que estén en un lugar específico.
Esto significa que la canalización no falla debido a una nota con formato incorrecto; simplemente funciona con lo que encuentra. Es la parte más aburrida del proyecto, pero limpia la mayor parte del desorden, razón por la cual dediqué la mayor parte del tiempo a lograr que esta etapa realmente se mantuviera.
Paso 2: el generador de gráficos
Esta etapa maneja los enlaces entre notas. Me topé con un muro de rendimiento aquí.
Mi primera versión ejecutó una expresión regular separada para cada entidad frente a todos los demás archivos. Fue un enfoque O (n ^ 2). Con 100 archivos, estuvo bien. Con 1.000 archivos, tardó 4,4 segundos. Con 5.000 archivos, tomó 107 segundos. Supuse que no llamar a una API significaba que el código sería rápido de forma predeterminada. Me equivoqué. El algoritmo importa más que la falta de llamadas de red.
Reemplacé la coincidencia de expresiones regulares por pares con un comparador de frases indexadas por palabras. Ahora tokenizo cada archivo una vez. A medida que me muevo por los tokens, utilizo una búsqueda en el diccionario para comprobar solo los nombres de entidades que comienzan con la palabra actual. En lugar de probar cada nombre de entidad con cada puesto, solo verifico los candidatos que realmente podrían coincidir. Convierte un proceso que creció cuadráticamente en uno que escala mucho más eficientemente a medida que el corpus se expande.
Los resultados cambiaron drásticamente. La ejecución de 1.000 archivos se redujo de 4,4 segundos a menos de 50 milisegundos. La ejecución de 5.000 archivos pasó de 107 segundos a menos de un segundo. Estos números específicos provienen de las primeras pruebas en mi entorno de desarrollo de Linux, antes de ejecutar el proceso en Windows, así que no espere que coincidan exactamente con los números de Windows que aparecen más adelante en este artículo; la sección de referencia más abajo tiene las cifras reales de Windows. Los números importan, así que aquí está la progresión real:
Vale la pena destacar la fila del medio de mi punto de referencia, porque fue mi primer intento de solucionarlo y falló. Intenté combinar cada nombre de entidad en un patrón de alternancia masivo (Nombre1|Nombre2|Nombre3…). Si bien esto redujo la cantidad de objetos de expresiones regulares, el motor subyacente aún tenía que verificar la lista completa en cada posición de carácter que escaneaba. Con 5.000 entidades, eso sigue siendo efectivamente un comportamiento cuadrático disfrazado de lineal. El comparador indexado de palabras fue la única versión que realmente solucionó la complejidad, en lugar de simplemente ocultarla detrás de un factor constante más rápido.
Así es como se ve realmente el gráfico resultante para tres entidades reales de mi corpus de prueba:
La detección aquí es estrictamente léxica, no semántica. Sólo coincide si el nombre exacto aparece en el texto. No sabe que “la empresa” y “mi empleador” pueden ser la misma cosa. Esa es una limitación genuina y abordaré su costo real más adelante en este artículo.
Paso 3: la reescritura con reconocimiento de secciones
Esta etapa escribe la rebaja real. No es un analizador de árbol de sintaxis abstracta. No camina por ningún árbol. Simplemente reemplaza cadenas específicas entre etiquetas de encabezado ## específicas, por lo que lo llamo reescritor con reconocimiento de secciones.
No quería que el compilador simplemente destruyera cada archivo y comenzara desde cero. Necesitaba una manera de conservar todo lo que escribí en la sección de Notas de una página durante una recompilación. La lógica es muy simple: antes de escribir una página, verifica si ya existe un archivo en el disco. Si lo hace, toma lo que esté debajo del encabezado ## Notas de esa página. Luego, las secciones propiedad del compilador (Metadatos, Relacionados, Referenciados por y Cuerpo) se borran y se regeneran desde el origen para mantenerlas precisas, mientras que el contenido de Notas se vuelve a escribir tal cual.
No sólo confiaba en que esto funcionaría; Lo probé. Agregué manualmente una nota a una página generada, modifiqué la fuente y ejecuté el compilador. Confirmé que la nota permaneció intacta mientras se actualizaban las otras secciones. Volví a realizar la misma prueba en mi máquina con Windows unos días después y funcionó exactamente de la misma manera.
Así es como se ve realmente, de principio a fin, en una entidad real de mi corpus de prueba:
FUENTE SIN PROCESAR (raw_notes/attention_mechanism.txt) ———————————————— MECANISMO DE ATENCIÓN creado: 2026-02-27 Un error común es ajustar el mecanismo de atención sin verificar primero el programa de tasa de aprendizaje. Esta sección necesita un ejemplo más claro antes de que se considere definitiva. SALIDA COMPILADA (compiled_wiki/attention_mechanism.md) ———————————————— # Mecanismo de atención ## Metadatos – creado: 2026-02-27 – alias: ninguno – fuente: raw_notes/attention_mechanism.txt ## Relacionado – [[Programa de tasa de aprendizaje]] ## Referenciado por – [[Descenso de gradiente]] ## Cuerpo Un error común es ajustar el mecanismo de atención sin primero verifique el Programa de tasa de aprendizaje. Esta sección necesita un ejemplo más claro antes de que se considere definitiva. ## Notas _(agregue sus propias notas aquí – se conservan al volver a compilar)_
Nada en el archivo sin formato le dijo al compilador que Gradient Descent hace referencia a esta página. Ese enlace se agregó automáticamente, porque la propia nota sin procesar de Gradient Descent menciona "Mecanismo de atención" en el cuerpo del texto, y el creador de gráficos lo captó. Ese es todo el proceso en un ejemplo concreto: entrada desordenada, salida estructurada y con referencias cruzadas, sin vinculación manual.
Paso 4: El Linter (y el segundo error)
El linter es simple: recorre la salida, señala enlaces rotos y señala páginas que nadie más está señalando. La gente podría confundir esto con un paso de “juicio” del LLM, pero no lo es. Es un control estructural tonto con reglas fijas.
Mi primera versión tenía un error enorme. Lo menciono porque es el tipo de cosas que te perderás a menos que escribas una prueba que realmente pruebe la lógica. El linter contó los enlaces entrantes escaneando cada [https://towardsdatascience.com/llm-wikis-are-over-engineered-i-replaced-mine-with-a-pure-python-compiler/] en el archivo. Aquí está el problema: la sección Referenciado por también contiene [[enlaces]]. Esos enlaces rastrean las páginas que apuntan al archivo actual, no las páginas a las que el archivo apunta a sí mismo. Mi linter los contaba como enlaces salientes, lo que aumentó el recuento de cada página.
¿El resultado? En mi corpus de prueba de 100 archivos, con 13 huérfanos confirmados, el linter con errores informó cero. No fue sólo un conteo insuficiente, me dijo que el wiki estaba perfectamente vinculado cuando no lo estaba. Habría enviado ese error si no hubiera verificado dos veces la lógica con una segunda fuente de verdad.
La solución fue limitar el recuento únicamente a la sección Relacionada. Ése es el único lugar donde realmente viven los auténticos bordes extrovertidos.
texto_relacionado = _extract_section(texto, "Relacionado") para coincidencia en LINK_RE.finditer(texto_relacionado): target_slug = _slugify(match.group(1)) si target_slug en incoming_count: incoming_count[target_slug] += 1
Después de ese cambio, el recuento de huérfanos del linter coincidió exactamente con el del creador de gráficos. Cada vez. Se mantuvo independientemente del tamaño del corpus. Agregué una prueba de regresión para esto, que lleva el nombre del error, para que nunca pueda volver a aparecer silenciosamente.
El conjunto de pruebas completo
Tengo 17 pruebas, usando solo stdlib unittest, que cubren cada etapa más el proceso completo de un extremo a otro. Aquí hay una porción representativa:
test_linter_does_not_miscount_referenced_by… ok test_human_notes_preserved_across_recompile… ok test_recompile_is_idempotent_on_compiler_owned_sections… ok test_deterministic_output… ok Ejecuté 17 pruebas en 0,020 s OK
Ese test_linter_does_not_miscount_referenced_by es la prueba de regresión para el error que acabo de atravesar. Es el nombre más feo del expediente, pero es el más importante.
No elegí el 17 simplemente porque me parecía bien; la estructura importa. Cada etapa tiene sus propias pruebas aisladas que utilizan objetos Entity construidos a mano en lugar del generador sintético. Hice esto para que una falla apunte exactamente a una etapa. No quiero pasar una hora depurando un proceso completo sólo para encontrar un error tipográfico de una línea.
Las pruebas completas en la parte inferior son diferentes. Detectan problemas de integración que las pruebas unitarias no pueden ver. La prueba de idempotencia es un buen ejemplo: recompila el mismo corpus dos veces y se asegura de que la salida sea idéntica en bytes en ambas ocasiones. Si el reescritor hubiera introducido accidentalmente algún no determinismo, como una marca de tiempo deshonesta, esa prueba me habría gritado de inmediato.
Prefiero tener 17 pruebas, cada una de las cuales falla por una razón obvia y específica, que una prueba de integración masiva que falla y me deja adivinando cuál de las cuatro etapas realmente falló.
El punto de referencia: dos máquinas, los mismos números
Ejecuté el proceso completo en tres escalas diferentes, tanto en un contenedor de Linux como en mi máquina local con Windows 10, usando la misma semilla para mantener el material fuente idéntico.
Salida terminal, sin procesar:
TIEMPO COMPLETO DEL PIPELINE POR ETAPA EN 5000 ARCHIVOS (12,44 s en total) ==================================================================== extracto [==] 1,40 s (11 %) gráfico [=] 0,63 s (5 %) reescribir [=======] 3,45 s (28 %) pelusa [==============] 6,97 s (56 %) ================================================================
Lint es, con diferencia, la etapa más cara. Con 5000 archivos, cuesta más que las etapas de extracción, gráfico y reescritura combinadas.
Eso me sorprendió. Lint tiene la lógica más ligera de todo el proceso: simplemente abre cada archivo una vez para escanear con expresiones regulares dos secciones pequeñas. El cuello de botella no es el código; es la E/S del disco. Lint realiza una nueva lectura en cada archivo y Windows es significativamente más lento que Linux aquí. Es probable que Windows Defender contribuya a esto, verificando cada archivo a medida que se abre, aunque no lo he verificado directamente con los propios registros de Defender.
El generador de gráficos no tiene E/S de disco, por lo que escala mejor, pero no es perfectamente lineal.
Pasar de 100 a 1000 archivos (un aumento de datos de 10 veces mayor) tomó 15,2 veces más tiempo. Pasar de 1000 a 5000 archivos (otro aumento de 5 veces) tomó 13,3 veces más tiempo. Ese es el costo real y mensurable de que el comparador indexado de palabras tenga que verificar más nombres de candidatos por token a medida que crece la lista de entidades, y significa que la escala no es del todo lineal.
Los recuentos de huérfanos (13, 133, 644) se mantuvieron idénticos en cada ejecución, independientemente del sistema operativo. Eso no es una coincidencia. Es el objetivo de construir esto como un compilador en lugar de un agente: los resultados son deterministas. No se mueven. Solo cambia la hora del reloj de pared, lo cual es solo un reflejo del hardware y el sistema operativo, no del algoritmo.
Esos números huérfanos son estadísticas de conectividad de un corpus sintético sembrado, no una medida de "calidad". Los verifiqué de forma independiente; el generador de gráficos y el linter calculan el estado de huérfano a través de rutas de código completamente diferentes, y coincidieron en cada escala que probé.
Lo que esto significa para el uso en el mundo real: con 5.000 notas, una recompilación completa tarda unos 12 segundos en hardware estándar de Windows: coste de token cero, cero llamadas de red. Si está ejecutando a la escala de la mayoría de las bases de conocimiento personales (de unos cientos a un par de miles de notas), una recompilación completa finaliza en menos de dos segundos.
donde esto se rompe
Datos de origen no estructurados o tremendamente inconsistentes. Mi extractor maneja dos estilos de encabezado y algunos metadatos opcionales porque eso es lo que requería mi corpus de prueba. Si arroja una carpeta de texto caótico y confuso o notas en varios idiomas sin estructura, la expresión regular por sí sola no será suficiente. Necesitaría una capa de extracción significativamente más sofisticada.
Vinculación semántica. Éste es el grande. Mi generador de gráficos utiliza una coincidencia exacta de nombres. Si una nota habla de "descenso de gradiente" y otra describe "el paso de optimización" sin usar la frase literal, no se vincularán. Nada en este tubo entiende significado. Ese es el límite honesto de lo que puede hacer un compilador determinista. Si tuviera que ampliar esto, agregaría una capa semántica como una mejora claramente separada; no la incluiría en el camino determinista central.
El marco no es que los LLM sean la herramienta equivocada para crear una wiki personal. Es que son la herramienta equivocada para el 90 por ciento del trabajo que es puramente mecánico, y posiblemente la herramienta adecuada para el 10 por ciento que requiere realmente entender lo que significa el texto en lugar de simplemente coincidir con cómo está escrito.
Pensamiento final
Si su aporte es determinista, su canalización también debería serlo. De lo contrario, simplemente estás agregando aleatoriedad donde no existía.
Ese es el argumento de todo este artículo. No todos los problemas de gestión del conocimiento necesitan un bucle de agente. A veces, sólo necesita un analizador, un gráfico y un linter que le diga la verdad sobre su propia producción, incluidas las partes en las que está equivocada.
El código fuente completo, incluido el generador, el extractor, el generador de gráficos, la reescritora, el linter, el arnés de referencia y las 17 pruebas, se encuentra en: https://github.com/Emmimal/wiki-compiler/
Recursos y citas
Este artículo hace referencia a las siguientes fuentes primarias. El material citado se limita a frases cortas de menos de quince palabras por fuente, de acuerdo con la práctica estándar de uso legítimo para comentarios y redacción técnica; Recomiendo leer los originales directamente en lugar de confiar en mi resumen de ellos.
[1]Andrej Karpathy, publicación original que describe las bases de conocimiento personal impulsadas por LLM, X, abril de 2026. https://x.com/karpathy/status/2039805659525644595
[2]Andrej Karpathy, archivo de ideas “LLM Wiki” (GitHub Gist), abril de 2026, que describe el patrón wiki como artefacto compilado al que se hace referencia en este artículo. https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f
Todo el código, los números de referencia y los resultados de las pruebas de este artículo son míos y se generan ejecutando directamente el código base incluido. No se utilizaron conjuntos de datos propietarios, textos protegidos por derechos de autor ni códigos de terceros para crear o comparar este sistema.