Tag: RAG

  • Cómo funciona un vector database por dentro: HNSW y IVF

    Cómo funciona un vector database por dentro: HNSW y IVF

    Un cliente me escribió porque su buscador "se había vuelto tonto". Metías la frase exacta de un documento y ese documento no aparecía entre los primeros cinco resultados. A veces ni entre los primeros veinte.

    El equipo sospechó del modelo de embeddings. Lo cambiaron dos veces. Mismo comportamiento.

    Nadie se preguntó cómo funciona un vector database por dentro. Daban por hecho que "buscar por similitud" era comparar la consulta contra todos los vectores guardados y devolver los más parecidos. Eso sí habría encontrado el documento, siempre.

    El problema era el contrario: no estaban comparando contra todos. Usaban un índice que recorre solo una fracción del dataset, y nadie sabía que puede —por diseño, no por bug— dejar fuera al vecino real.

    En corto: un vector database no compara tu consulta contra cada vector guardado. Usa un índice de approximate nearest neighbor (ANN) —típicamente HNSW o IVF— que navega una estructura para encontrar vecinos muy probablemente cercanos sin tocar el resto del dataset. A cambio de esa velocidad, el resultado deja de estar garantizado al 100%.

    ¿Qué es un vector database?

    Un vector database indexa vectores de alta dimensión —cientos o miles de números por registro— para responder "¿qué está más cerca de esto?" en milisegundos, sin recorrer todo el dataset.

    La palabra clave es "cerca". Una base relacional indexa para responder "¿qué fila es igual a X?" o "¿qué está entre A y B?". Un B-tree hace eso bien porque los números tienen orden total: 5 va antes que 8, siempre.

    Un embedding de 768 o 1536 dimensiones no tiene ese orden. No hay un "antes" entre dos vectores, solo una distancia en un espacio que no puedes dibujar. Por eso un B-tree no sirve aquí: no es lento, resuelve otro problema.

    Fuerza bruta: por qué no escala comparar contra todos

    La forma honesta es la más simple: calculas la distancia de tu consulta contra cada vector guardado, ordenas y te quedas con los k más cercanos.

    # búsqueda exacta por fuerza bruta — siempre correcta, siempre O(n)
    def buscar_exacto(consulta, vectores, k):
        distancias = [(distancia(consulta, v), v) for v in vectores]
        distancias.sort(key=lambda x: x[0])
        return distancias[:k]
    

    Es exacta, nunca se equivoca, y es inviable a escala porque el coste es O(n) por consulta: recalcula distancia contra cada vector, sin excepción.

    Con mil vectores no lo notas. Con diez millones, cada consulta son diez millones de cálculos de distancia antes del primer resultado. Los índices ANN existen para que un vector database evite ese barrido completo.

    HNSW: el grafo en capas que evita mirarlo todo

    HNSW (Hierarchical Navigable Small World) organiza los vectores de tu vector database en un grafo de varias capas, del paper original de Malkov y Yashunin (2016, arXiv:1603.09320). No todos los vectores se conectan entre sí, solo con sus vecinos aproximados —y algunos, al azar, se replican también en capas superiores más dispersas.

    La capa de arriba tiene pocos nodos con conexiones largas. Cada capa hacia abajo tiene más nodos y conexiones más cortas, hasta la capa base, que contiene todos los vectores.

    Buscar es navegar de arriba a abajo: entras por un punto fijo en la capa superior, saltas al vecino más cercano hasta que ninguno mejora la distancia, y bajas una capa. Repites hasta la capa base, donde exploras un grupo más amplio y devuelves los k mejores. Es la lógica de una skip list: saltos largos para acercarte, cortos para afinar. Así el paper logra una complejidad de búsqueda cercana a logarítmica, no lineal.

    Y aquí el trato que nadie lee hasta que le muerde: HNSW hace ANN, no exact nearest neighbor. La navegación golosa —saltar siempre al vecino más próximo— puede quedarse en un óptimo local y devolver el segundo o tercer vecino real, no el primero. No es un bug: es el precio de no comparar contra todo.

    El parámetro que controla cuánto exploras en la capa base suele llamarse ef_search: más candidatos, más cerca del resultado exacto, más lenta la consulta. Cuántos vecinos conecta cada nodo se controla con m. No hay un valor universal para ninguno: depende de tu dataset.

    IVF: particionar el espacio en clusters

    IVF (Inverted File Index) agrupa los vectores en clusters —normalmente con k-means— y guarda, por cada cluster, la lista de vectores que le pertenecen. En la búsqueda comparas primero tu consulta contra los centroides, no contra los vectores, y solo entras en detalle dentro de los clusters más cercanos.

    El parámetro equivalente a ef_search aquí es nprobe (en pgvector, ivfflat.probes): cuántos clusters revisas por consulta. Con nprobe = 1 solo miras el más cercano, con riesgo real de que el vecino real esté en el cluster de al lado. Subir nprobe sube el recall y baja la velocidad — el mismo trade-off que en HNSW, con otro nombre.

    IVF suele pesar menos en memoria porque no guarda un grafo con punteros por vecino en cada capa. A cambio, un buen índice depende de que el k-means inicial refleje bien la forma real de tus datos.

    Qué métrica de distancia usar

    "Cerca" no significa lo mismo según qué mides:

    • Euclidiana (L2): distancia en línea recta. Sensible a la magnitud del vector.
    • Similitud coseno: mide el ángulo entre dos vectores, ignorando su magnitud.
    • Producto interno: combina ángulo y magnitud. Un vector más "largo" puede ganar aunque apunte peor.

    La trampa habitual: si tus embeddings están normalizados (magnitud = 1, lo que hacen muchos modelos por defecto), coseno y producto interno dan el mismo ranking, porque la magnitud es idéntica para todos. Ahí la elección es cuestión de coste computacional, no de cuál es "más correcta". Sin normalizar, euclidiana y coseno sí pueden ordenar distinto —revisa qué asume tu modelo antes de fijar la métrica.

    HNSW vs IVF, cara a cara

    Criterio HNSW IVF
    Velocidad de búsqueda Muy alta, escala cerca de forma logarítmica Alta, depende directamente de nprobe
    Memoria Mayor — grafo de vecinos en cada capa, más los vectores Menor — solo vectores agrupados por cluster
    Recall "de fábrica" Buena incluso con parámetros conservadores Depende mucho de clusters y nprobe
    Coste de construir el índice Alto — cada inserción busca y conecta vecinos Más barato — k-means inicial y asignación por vector
    Actualizaciones (insert/delete) Delicado — en pgvector las tuplas eliminadas quedan en el grafo hasta el VACUUM (issue #244) Más simple, aunque un cambio grande pide re-clusterizar
    Cuándo usarlo Cabe en memoria, latencia mínima consistente Datasets enormes donde la memoria manda

    Ninguno es "mejor" en abstracto: reparten distinto la memoria, la velocidad de construcción y el recall. Es justo el tipo de decisión de arquitectura que discutimos cada semana en Dominicode Labs, donde el trade-off cambia según el caso real, no según la benchmark del paper.

    El trade-off que gobierna todo

    Todo parámetro de un índice ANN —ef_search, m, nprobe, el número de clusters— mueve el mismo dial en direcciones opuestas. Explorar más candidatos sube el recall y sube la latencia. Un grafo más denso o más clusters mejoran la búsqueda, y pesan más en memoria y tardan más en construirse.

    No hay un valor correcto en general para ningún vector database: depende de tu dataset, tu distribución de consultas y cuánta latencia puedes pagar. Sin conocer tu caso, cualquier número es solo un punto de partida para medir tú mismo.

    El how-to práctico de montar búsqueda híbrida con embeddings en Supabase entra en el paso a paso de producción. Este post es el mecanismo de por qué esos números existen.

    Cuándo un vector database NO es la respuesta

    Para filtros exactos y booleanos sigue haciendo falta un índice tradicional. "Dame los documentos del usuario 4821 publicados después del 1 de marzo" no es una pregunta de similitud, es de igualdad y rango, y un B-tree la resuelve mejor. Casi todo motor serio combina ambos.

    ANN nunca garantiza el resultado exacto, ni con parámetros altos. Es la consecuencia directa de no comparar contra todo el dataset: HNSW puede quedarse en un óptimo local, IVF puede dejar el vecino real fuera de los clusters revisados. Sube ef_search o nprobe todo lo que quieras, seguirá siendo una apuesta, no una garantía.

    Reindexar a escala no es gratis. Un HNSW con millones de vectores tarda en construirse, y si tu pipeline lo reconstruye en cada actualización, ese coste se paga en cada despliegue. Es justo lo que describe el hilo de "The Case Against PGVector" en Hacker News: construir el índice puede consumir más de 10 GB de RAM durante horas, y mantenerlo sincronizado con inserciones continuas complica el pipeline entero. El issue de pgvector sobre tuplas muertas es el mismo problema desde otro ángulo.

    Con poco volumen, la fuerza bruta gana. Con unos pocos miles de vectores, mantener un índice ANN puede costar más que comparar contra todo. Mide antes de asumir que necesitas HNSW.

    Qué hacer con esto hoy

    Si tienes un RAG o un agente con búsqueda vectorial en producción, ve a la configuración del índice ahora. Busca ef_search, nprobe o el equivalente en tu motor. Si está en el valor por defecto y nadie lo ha tocado, ese es tu primer sospechoso la próxima vez que un resultado "obvio" no aparezca.

    Y si ese índice te lo montó un agente de IA sin que nadie revisara qué parámetros eligió, ese es el tipo de decisión silenciosa que cubro en el ebook gratuito Revisión por Contrato: límites explícitos a lo que un agente decide por ti sin que lo notes.

    Entender el mecanismo no te ahorra elegir los parámetros, pero deja de sorprenderte que la búsqueda sea aproximada: elegiste velocidad sobre fuerza bruta.

    Si estás construyendo el sistema completo —agente, ingestión, capa de recuperación—, es la decisión de arquitectura que trabajamos en el curso Construye con IA. Y si todavía dudas entre montar un vector database o resolverlo de otra forma, en RAG vs fine-tuning vs contexto: cuándo usar cada uno explico cuándo compensa cada camino.

    Preguntas frecuentes

    ¿Qué es un índice HNSW?

    Un grafo en varias capas que organiza los vectores para no compararlos todos contra todos. La capa superior tiene pocos nodos con conexiones largas; hacia abajo hay más nodos y conexiones más cortas, hasta la capa base con todos los vectores. Del paper original de Malkov y Yashunin (arXiv:1603.09320).

    ¿Por qué la búsqueda vectorial a veces no encuentra el resultado "correcto"?

    Porque HNSW e IVF son índices de approximate nearest neighbor: sacrifican precisión por velocidad. HNSW puede quedarse en un óptimo local; IVF puede dejar el vecino real en un cluster no revisado. No es un fallo, es la consecuencia de no comparar contra todo el dataset.

    ¿Qué métrica de distancia debo usar: coseno, euclidiana o producto interno?

    Con embeddings normalizados (magnitud 1), coseno y producto interno dan el mismo ranking: la elección es cuestión de coste computacional. Sin normalizar, euclidiana y coseno pueden ordenar distinto porque uno considera la magnitud y el otro la ignora.

    ¿Un vector database sustituye a mi base de datos relacional?

    No. Para filtros exactos o por rango —igualdad, fechas, IDs— un índice tradicional sigue siendo más rápido. La mayoría de los sistemas en producción combinan ambos: filtran con índices relacionales y comparan por similitud dentro de ese subconjunto.

    ¿Cuándo compensa usar fuerza bruta en vez de HNSW o IVF?

    Con datasets pequeños —unos pocos miles de vectores— mantener un índice ANN puede costar más que comparar contra todos directamente. La fuerza bruta es O(n) por consulta, pero exacta y sin parámetros que ajustar.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Zettelkasten para developers: ahora tus notas las lee un agente

    Zettelkasten para developers: ahora tus notas las lee un agente

    Hace unos meses le pedí a uno de mis agentes que redactara una sección concreta de una guía. Devolvió algo correcto, genérico y absolutamente vacío.

    Lo incómodo es que yo ya había escrito eso. Meses antes. Con el contexto, la decisión y el motivo por el que descarté la alternativa.

    El agente no lo encontró. Y cuando fui a buscarlo yo, tardé veinte minutos: estaba enterrado en la mitad de un markdown gigante sin título propio.

    Ahí entendí que mi problema con el Zettelkasten para developers no era de disciplina. Era de formato.

    La tesis de este post, después de un año largo escribiendo así: el método no ha cambiado nada. Lo que ha cambiado es quién lee las notas.

    En 2021 tomabas notas atómicas para tu yo futuro. En 2026 las tomas también para el agente que las va a recuperar, y ese lector es mucho menos indulgente que tú.

    Por si llegas sin contexto: el método Zettelkasten son las fichas que el sociólogo Niklas Luhmann usó durante décadas para escribir, con una caja de papel y ningún ordenador. Aplicado a programadores, el Zettelkasten para developers es esto: una idea por archivo, en markdown plano, con un título que afirma algo, autocontenida y enlazada a las demás. No es una carpeta de apuntes ordenada. Es un grafo consultable.

    Crédito: en esto me metió Tomas Vik, con su introducción al método y su retrospectiva un año largo después. Lo que viene es mi versión, con mis cicatrices.


    El conocimiento que solo entiendes tú ahora muere dos veces

    Durante años tomé notas como casi todos los programadores que conozco: capturas de pantalla, fragmentos copiados de la documentación, bullets sin verbo, enlaces con un "revisar esto" al lado.

    Eso no es una base de conocimiento, ni gestión del conocimiento personal (PKM). Es un vertedero ordenado alfabéticamente.

    El problema clásico ya era grave: un formato que solo entiendes tú, en el momento exacto en que lo escribiste, deja de entenderse a los tres meses.

    Lo nuevo es la segunda muerte. Si trabajas solo —yo opero Dominicode sin equipo— tus agentes son literalmente tu equipo, y ese equipo lee lo que dejaste escrito. Si lo que dejaste son bullets sin sujeto, tu equipo entero trabaja a ciegas.

    Una nota mal escrita ya no solo te penaliza a ti dentro de seis meses. Penaliza cada ejecución de cada agente que consulta ese repositorio, hoy.


    El Zettelkasten para developers ya no es productividad, es infraestructura

    La nota atómica pasó de consejo de productividad a requisito técnico el día en que dejó de leerte solo tú. Este es el giro que hace que esto merezca un post en 2026 y no en 2015.

    "Sé conciso" y "una idea por nota" eran consejos de higiene mental. Sonaban a gurú de productividad. Se podían ignorar sin consecuencias visibles.

    Hoy son requisitos técnicos de recuperación.

    Cuando indexas tu conocimiento para que un modelo lo consulte —lo que todo el mundo llama RAG— el texto se parte en fragmentos (chunking), esos fragmentos se convierten en embeddings y se recuperan por similitud semántica. Una nota de un solo tema, autocontenida y con un título que afirma algo es exactamente la unidad que ese índice recupera bien: cabe en muy pocos fragmentos y cada uno de esos fragmentos sigue significando algo por su cuenta.

    Una nota-cajón de sastre de cuatro mil palabras hace lo contrario. Se parte por la mitad de un razonamiento. El fragmento recuperado empieza con "por eso lo descartamos" y el sujeto de esa frase se quedó tres páginas más arriba. El modelo recibe un texto gramaticalmente correcto y semánticamente huérfano, y con eso improvisa. A eso lo llamamos alucinación, pero muchas veces es mala segmentación.

    Cómo se recuperan de verdad esos fragmentos —y por qué la búsqueda vectorial pura falla— lo desmonté en búsqueda híbrida y embeddings en producción.

    Nota-cajón Nota atómica
    Temas por archivo Varios Uno
    Título Una etiqueta: "Angular signals" Una afirmación: "Signals no sustituye a RxJS cuando necesitas cancelar una petición en vuelo"
    Al partirse en fragmentos El fragmento pierde el sujeto Cada fragmento sigue significando lo mismo
    Tú dentro de seis meses Hay que releer el archivo entero Decides si la abres sin abrirla
    Tu agente Recupera contexto huérfano e improvisa Recupera la unidad completa

    Mi ancla concreta: la base de conocimiento de Dominicode es un índice de búsqueda sobre markdown plano. Ahora mismo son 118 documentos y 455 fragmentos, repartidos en colecciones por ámbito —agents, _brand, _research, labs, videos, books, revenue— que los agentes consultan antes de escribir sobre cualquiera de esos ámbitos.

    Haz la división: sale por debajo de cuatro fragmentos por documento de media. No es un número mágico —los capítulos de libro tiran de esa media hacia arriba—, pero sí es el síntoma de la política editorial: documentos cortos y monotema. Cuando un archivo se dispara muy por encima de esa media, dentro hay dos o tres notas peleándose por el mismo sitio.

    La infraestructura de todo esto —el repo, la estructura, el gobierno vía CLAUDE.md— la conté en cómo montar un segundo cerebro con Claude Code. Este post va de la capa de encima: qué escribes dentro de cada archivo.


    El título es la nota

    Si me quedo con una sola regla de este año, es esta: el título tiene que afirmar algo, no nombrar un tema.

    No "Angular signals". Eso es una etiqueta.

    Sí "Signals no sustituye a RxJS cuando necesitas cancelar una petición en vuelo". Eso es una nota que puedes recuperar, contradecir o confirmar.

    Un título-afirmación hace tres cosas a la vez. Te obliga a tener una conclusión antes de escribir, en vez de acumular material. Le da al índice la señal más fuerte que va a recibir de ese documento. Y te permite decidir meses después si abres la nota sin abrirla.

    La segunda regla es más dura de lo que parece: la nota tiene que sobrevivir a ser leída sola. Sin la nota anterior. Sin la pestaña que tenías abierta ese día. Sin acordarte del proyecto.

    En la práctica eso significa desterrar los pronombres huérfanos. "Esto no funciona en producción" no es una nota. "El provideHttpClient con interceptores funcionales no captura errores lanzados dentro del resolver de una ruta" sí lo es.

    Así se ve el cambio en un archivo real:

    <!-- Antes: nota-cajón -->
    # Errores HTTP
    
    - esto no funciona en producción
    - revisar interceptores
    - preguntar a alguien
    
    <!-- Después: nota atómica -->
    # El interceptor funcional no captura errores lanzados dentro del resolver de una ruta
    
    `provideHttpClient(withInterceptors([...]))` solo ve lo que pasa por
    `HttpClient`. Si el resolver falla antes de lanzar la petición, el error
    sale por el router y el interceptor nunca se entera.
    
    Alternativa descartada: mover el try/catch a cada componente. Funciona,
    pero hay que repetirlo en cada ruta.
    

    El de abajo es más largo de escribir. Es el único de los dos que sigue sirviendo dentro de un año, y el único que un agente puede recuperar suelto.

    La autocontención es la misma propiedad que hace útil a un fragmento recuperado. Escribes para ti dentro de un año y, sin proponértelo, escribes para un recuperador vectorial. Es el mismo requisito con dos nombres.

    Es la lógica que aplicamos en el curso Construye con IA al montar la capa de contexto de un producto: el modelo no falla por falta de inteligencia, falla porque le llega texto sin sujeto.


    Los enlaces son el trabajo que no puedes delegar

    Enlazar es la única parte del Zettelkasten que no puedes automatizar, y es donde está todo el retorno. Aquí es donde mucha gente se baja.

    Enlazar una nota nueva con las que ya tienes te obliga a responder dos preguntas que no se contestan en piloto automático: ¿dónde encaja esto? y ¿contradice algo que ya escribí?

    La primera es de arquitectura. La segunda es la buena.

    Cuando una nota nueva choca de frente con una de hace ocho meses, ha pasado algo real: o aprendiste, o una de las dos estaba mal, o —lo más habitual— las dos son ciertas en contextos distintos y nunca habías delimitado cuál era cuál. Resolver ese choque suele producir una tercera nota, y esa tercera nota es la única de las tres que vale dinero.

    Ese momento no te lo da un agente. Un modelo te sugiere enlaces plausibles todo el día, y como sugerencia sirven. Lo que no tiene es la sensación de "un momento, esto no cuadra con lo que decidí en marzo". Esa fricción es el aprendizaje. Externalizarla es quedarte con el grafo y perder el motivo por el que existe.

    Lo que sale de ahí es un grafo, con las relaciones como dato de primera clase. Es la idea detrás de qué es graph engineering: la estructura entre las piezas transporta tanta información como las piezas.

    Con un efecto secundario que no esperaba: es el mejor antídoto contra el context drift que he encontrado. Cuando la conversación con el agente lleva dos horas derivando, las notas enlazadas son el punto fijo al que volver. No lo que el modelo cree recordar, sino lo que tú decidiste y escribiste.

    Y si viven en markdown, acaban siendo consultables como datos: notas huérfanas, enlaces rotos, temas que lo acaparan todo. Salud del grafo con una consulta, como conté en DuckDB + Obsidian.


    El coste honesto: esto es lento

    El coste real del método es el tiempo, y toca la parte que casi nadie escribe.

    Escribir así es lento. Mucho más lento que guardar el enlace y seguir. Leer para destilar una nota es más lento que leer, y bastante menos placentero. Dejé libros y documentación técnica a medias porque procesarlos a ese ritmo se volvió un trabajo, no una lectura.

    Y algo dejó de funcionar del todo: la ambición de capturarlo todo. Durante meses convertí en nota cosas que no lo merecían y el grafo se llenó de ruido que empeoraba la recuperación. Más notas no es mejor.

    Ahora tengo tres filtros. Si la respuesta a cualquiera de los tres es sí, no hay nota:

    • ¿Caduca en menos de dos semanas? Es un recordatorio, no conocimiento. Va a un issue, no al grafo.
    • ¿Existe upstream, es estable y está bien escrito? Enlázalo. Reescribir la documentación oficial de una librería es trabajo perdido que además envejece mal.
    • ¿Podrías reconstruirlo en cinco minutos buscando? No es tuyo todavía. Es información disponible, no conocimiento propio.

    Lo que sí merece nota casi siempre es lo mismo: la decisión, la alternativa que descartaste y el porqué. Eso no existe upstream, no está en la documentación de nadie, y es exactamente lo que te vuelven a preguntar dentro de un año.

    Es la misma forma de pensar que sostiene el libro de Spec-Driven Development: escribir la decisión antes que el código, porque la decisión es la parte cara.

    Y un último coste, más traicionero: optimizar el sistema en vez de usarlo. Reorganizar carpetas, cambiar de herramienta, diseñar la taxonomía perfecta. Todo eso se siente productivo y no produce nada.


    Qué cambiaría de mi Zettelkasten si empezara hoy de cero

    Cuatro cosas, y ninguna es instalar nada.

    1. Títulos-afirmación desde la primera nota. Renombrar cientos de archivos después es un trabajo horrible, y hasta que no lo haces la recuperación no mejora.
    2. Escribir pensando en el fragmento, no en el documento. Antes de guardar, pregúntate si un trozo suelto de esa nota, leído sin nada alrededor, sigue significando lo mismo. Si no, pártela.
    3. Colecciones por ámbito, no carpetas por tema. Los temas se solapan siempre y acabas con la misma nota en tres sitios. El ámbito casi nunca es ambiguo, y es lo que te permite filtrar la búsqueda.
    4. La nota de decisión antes que la nota de resumen. Los resúmenes de lo que leí envejecen. Las decisiones que tomé, con su alternativa descartada, siguen valiendo años después.

    Y una cosa que no cambiaría: no delegar los enlaces.


    Empieza por la siguiente nota

    No migres nada. No reorganices el vault. No elijas herramienta.

    Coge la última decisión técnica que tomaste esta semana —esa que discutiste contigo mismo durante veinte minutos— y escríbela como una sola nota, con un título que afirme algo y sin un solo pronombre sin sujeto. Después enlázala con lo más parecido que ya tengas escrito.

    Eso es todo. Repítelo cuando vuelva a pasar.

    Dentro de un año esa nota la vas a leer tú, medio dormido, buscando por qué hiciste lo que hiciste. Y la va a leer un agente que no tiene tu memoria, ni tu contexto, ni tu paciencia. Escríbela para el segundo: el primero sale beneficiado gratis.

    Si quieres el paso siguiente —cómo hacer que un agente use ese conocimiento sin inventarse la mitad— te lo dejé en el ebook gratuito Revisión por Contrato. Y si prefieres verlo montado sobre proyectos reales, con el repo y los agentes funcionando, está en Dominicode Labs.


    Preguntas frecuentes

    ¿Qué es exactamente el Zettelkasten para developers y en qué se diferencia de tomar apuntes?

    El Zettelkasten para developers es un sistema de notas atómicas en markdown: una idea por archivo, con un título que afirma algo, autocontenida y enlazada con las demás. La diferencia con tomar apuntes es el enlace. Los apuntes se acumulan en carpetas y se consultan por nombre de archivo; el Zettelkasten forma un grafo donde la relación entre dos notas transporta tanta información como las notas.

    En 2026 hay una segunda diferencia: una nota atómica es también la unidad que un índice vectorial recupera entera. Un apunte largo no.

    ¿Sigue teniendo sentido el Zettelkasten ahora que la IA me resume cualquier cosa?

    Tiene más sentido que antes, y por un motivo distinto al de siempre. La IA resume bien lo que está publicado; no puede resumir la decisión que tomaste tú, con la alternativa que descartaste y el motivo. Eso no existe en ningún corpus.

    Lo que sí cambió es el destinatario. Antes escribías notas atómicas para tu yo futuro. Ahora las escribes también para el agente que las va a recuperar, y ese lector no perdona un pronombre sin sujeto.

    ¿Necesito una herramienta de Zettelkasten o me vale markdown en un repo?

    Te vale markdown en un repo, y es lo que uso. La única condición innegociable es que los archivos sean texto plano y tuyos: eso te da git, búsqueda y la posibilidad de indexarlos para que los lea un agente.

    Las herramientas específicas —Obsidian, Logseq, Roam— aportan comodidad al enlazar y visualizar el grafo. Pero elegir herramienta antes de tener notas es la forma más elegante de no empezar nunca.

    ¿Cuántas notas hacen falta para que esto empiece a servir?

    Menos de las que imaginas para el uso individual, y bastantes más para el efecto de red, que es cuando el grafo te sugiere conexiones que no habías visto.

    No te fijes un número, fíjate una señal: el sistema funciona el día que buscas algo, lo encuentras, y esa nota te resuelve el problema sin abrir nada más.

    ¿No puede el agente resumir mis notas largas y ahorrarme el trabajo?

    Puede resumir. El problema es cuándo. En el momento de la recuperación el agente ya no ve la nota entera: ve los fragmentos que el índice le devolvió —salvo que montes recuperación por documento padre, que es trabajo aparte—, y si están mal cortados el resumen es una reconstrucción sobre material incompleto.

    Usar un modelo para partir notas viejas en notas atómicas sí es buena idea. Lo que no puedes delegar es decidir qué idea va en cada nota, porque esa decisión es el contenido.

    ¿Esto sirve para documentación de equipo o solo para notas personales?

    Sirve para ambas, pero no son lo mismo. La documentación de equipo describe cómo funciona el sistema hoy y se actualiza cuando el sistema cambia. Las notas atómicas capturan por qué se decidió algo y se acumulan sin borrarse.

    Si trabajas en solitario la frontera casi desaparece: tu grafo de decisiones acaba siendo el onboarding de tus propios agentes.

    ¿Qué hago con las notas largas que ya tengo escritas?

    Nada, hasta que las necesites. Migrar el archivo entero es el clásico proyecto que se abandona a la tercera tarde.

    Aplica la regla al vuelo: la próxima vez que abras una nota vieja y te cueste encontrar dentro lo que buscabas, ese es el momento de partirla en dos o tres notas con título propio. Migras solo lo que demuestra que se usa.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Local-first con PGlite: Postgres en el navegador, ¿te toca?

    Local-first con PGlite: Postgres en el navegador, ¿te toca?

    Abro una app de notas con IA, escribo una pregunta y espero.

    No espero al modelo. Eso lo entiendo: el modelo piensa. Espero a que la app viaje al servidor para leer tres filas de mi propio historial, las traiga de vuelta y solo entonces monte el prompt. Doscientos milisegundos de red para leer datos que ya estaban en mi portátil.

    Ese es el patrón por defecto de casi todas las apps de IA que reviso. La arquitectura local-first le da la vuelta: la base de datos vive en el dispositivo del usuario, la app lee de ahí a velocidad de memoria, y el servidor pasa de ser la fuente de toda verdad a ser un destino de sincronización.

    Aquí va la tesis. Mover la base de datos al cliente no es una optimización: es un cambio de arquitectura que reparte de otra forma la latencia, el coste de tokens y la privacidad, y a cambio te devuelve tres problemas que en el servidor no tenías — una sola conexión, migraciones que corren en máquinas que no controlas, y un camino de escritura hacia el servidor que nadie te da hecho.

    Si terminas de leer sabiendo si te toca o no, el post ha cumplido.

    Qué es la arquitectura local-first

    Local-first es una arquitectura en la que la base de datos vive en el dispositivo del usuario: la aplicación lee y escribe siempre contra esa copia local, a velocidad de memoria, y el servidor deja de ser la fuente de toda verdad para convertirse en un destino de sincronización. La app funciona sin red por defecto, no como caso degradado.

    No es lo mismo que cachear. Una caché es una copia que aceleras y que puedes tirar; en local-first la copia local es donde de verdad ocurre todo, y la red es un detalle de implementación.

    Qué es PGlite y por qué no es "otra base de datos en el navegador"

    PGlite es Postgres compilado a WebAssembly y empaquetado como librería de TypeScript. Corre en el navegador, en Node.js y en Bun, sin dependencias externas y sin proceso servidor.

    La distinción que importa está en su propia documentación: "Unlike previous 'Postgres in the browser' projects, PGlite does not use a Linux virtual machine – it is simply Postgres in WASM."

    Eso no es marketing, es la decisión arquitectónica del proyecto. Los intentos anteriores emulaban una máquina Linux entera para arrancar encima un Postgres normal: pagabas el peso de un sistema operativo simulado para ejecutar un SELECT. PGlite elimina esa capa.

    Los números, para que no tengas que buscarlos:

    • PGlite pesa menos de 3 MB gzipped y corre Postgres compilado a WASM, sin máquina virtual Linux.
    • Persiste en memoria (efímero), en IndexedDB en el navegador, o en el sistema de ficheros en Node y Bun.
    • La versión actual de @electric-sql/pglite es la 0.5.8, publicada el 26 de agosto de 2026, y el plugin de sincronización @electric-sql/pglite-sync va por la 0.6.9.

    Retén ese "0.x". Vuelvo a ello más abajo.

    Qué gana una app de IA con arquitectura local-first

    Una app de IA gana tres cosas al pasar a local-first: latencia de microsegundos en lugar de un round-trip de red, menos tokens por turno porque filtras el contexto donde ya están los datos, y una garantía de privacidad real porque el historial no tiene por qué salir del dispositivo. Ninguno de los tres es "va más rápido" a secas.

    Latencia: microsegundos frente a un round-trip de red

    Los benchmarks oficiales, medidos en un MacBook Air M2 con PGlite en memoria, dan estos tiempos de ida y vuelta por operación: insert de una fila pequeña en 0,058 ms, select en 0,088 ms, update en 0,073 ms y delete en 0,145 ms.

    Compáralo con los 50-200 ms de una llamada HTTP a tu API y el cambio deja de ser cuantitativo: pasa a ser de diseño.

    Cuando leer cuesta microsegundos dejas de diseñar para minimizar consultas. Se acabó cachear tres pantallas por delante, montar endpoints agregados y pintar skeletons.

    Coste de tokens: RAG en el navegador con pgvector

    Este es el eje que más se subestima, y el que de verdad justifica local-first en una app de IA.

    El patrón habitual del chatbot es incómodo cuando lo miras de frente: mandas todo el historial al servidor en cada turno para que el backend reconstruya un contexto que ya estaba entero en el dispositivo del usuario. Pagas tokens por transportar información que no había salido de casa.

    Con un Postgres real en el cliente filtras el contexto en local —con SQL de verdad, joins y filtros por fecha— y mandas al modelo solo lo que importa. Y como PGlite soporta pgvector, ese filtro puede ser búsqueda semántica y no solo un WHERE:

    import { PGlite } from '@electric-sql/pglite'
    import { vector } from '@electric-sql/pglite-pgvector'
    
    const pg = new PGlite({
      extensions: { vector },
    })
    
    await pg.exec('CREATE EXTENSION IF NOT EXISTS vector;')
    

    La consulta por distancia de embeddings corre en el navegador y solo los cinco fragmentos relevantes viajan al modelo:

    SELECT id, content
    FROM chunks
    ORDER BY embedding <-> $1::vector
    LIMIT 5;
    

    Ese <-> es distancia euclídea; si tus embeddings vienen de OpenAI, lo habitual es coseno con <=>. Elige el operador que case con el índice que crees, no el del ejemplo que copiaste.

    Decidir qué entra en el contexto antes de escribir el primer prompt es justo el trabajo que hacemos en el curso de Construye con IA: el prompt no es el sistema, es la última capa.

    Si vienes de montar RAG en servidor, el contraste está en Búsqueda híbrida y embeddings en Supabase — misma técnica, distinto sitio. Y si aún dudas de si tu caso pide RAG, contexto o fine-tuning, esa decisión va antes que esta y la tienes en RAG vs fine-tuning vs contexto.

    Privacidad: los datos no salen del dispositivo

    En una app de notas es un argumento de venta. En salud, legal o finanzas es el requisito que decide si el proyecto existe.

    Con la base de datos en el cliente decides tú qué se sincroniza. Y puedes decidir que nada: si esa tabla no entra en ningún shape, el historial del usuario nunca toca tu infraestructura. Solo sale lo que mandas al modelo, y eso lo controlas con una consulta, no con una política de retención.

    Es la diferencia entre "prometemos que no miramos tus datos" y "no los tenemos".

    Pero ese modo máximo se paga dos secciones más abajo: sin copia en servidor no hay red de seguridad para las migraciones ni multi-dispositivo. Privacidad total y recuperación ante desastres son dos posiciones del mismo mando.

    Cómo queda la arquitectura: PGlite como fuente de verdad local

    PGlite es la fuente de verdad para la UI: la aplicación lee y escribe siempre contra la base local, nunca contra la red. El servidor mantiene su Postgres, que sigue siendo la fuente de verdad del negocio, y las escrituras llegan hasta él por un camino que montas tú. Ahora vuelvo a eso.

    Entre los dos, sincronización basada en shapes. Un shape es un subconjunto de una tabla: no te bajas messages entera, te bajas "los mensajes de las conversaciones de este usuario de los últimos 90 días". El cliente declara qué porción del mundo le interesa y el sync la mantiene al día.

    Y aquí va el matiz que decide presupuestos: ese sync va en una sola dirección. La documentación de pglite-sync no se esconde — "We don't yet support local writes being synced out, or conflict resolution" — y Electric lo remata: hace read-path sync, y no hace write-path sync.

    Traducido: el camino servidor → cliente te lo dan hecho. El camino cliente → servidor lo escribes tú. Cola de escrituras local, reintentos, orden, idempotencia y qué pintas mientras la escritura está en vuelo. Electric documenta cuatro patrones para eso, pero son patrones, no un paquete que instalas.

    Dos límites más de los shapes que conviene saber antes de diseñar: no se pueden sincronizar varios shapes sobre la misma tabla, porque una suscripción necesita poder tirar todos los datos y empezar de cero; y para garantizar consistencia transaccional los datos se agregan en memoria, lo que con shapes muy grandes se nota.

    Encima van las live queries del módulo @electric-sql/pglite/live: registras una consulta y los resultados se actualizan solos cuando cambian los datos, vengan del usuario o del sync. Sin polling y sin invalidación manual de caché.

    Ahí está el efecto secundario grande: desaparece la mitad de tu capa de gestión de estado. El estado del servidor deja de ser algo que cacheas a mano y pasa a ser una tabla que se actualiza. Esa reorganización de responsabilidades la traté en Clean Architecture en Frontend.

    Lo que se te rompe al llevar Postgres al navegador

    Llevar Postgres al navegador te devuelve seis problemas que en el servidor no tenías: una sola conexión, migraciones que corren en dispositivos ajenos, resolución de conflictos sin librería que la resuelva por ti, el peso de arranque, benchmarks que solo valen en memoria y una API todavía en 0.x. Esta es la sección que importa.

    Una sola conexión: el worker no es opcional

    La documentación lo dice sin adornos: "PGlite is single connection only". Y hay un segundo problema encima: si ejecutas PGlite en el hilo principal, bloqueas la UI.

    La solución oficial es el multi-tab worker: una única instancia de PGlite dentro de un Web Worker y una elección de líder que hace de proxy para las peticiones de todas las pestañas abiertas. Cuando la pestaña líder se cierra, se elige otra y se levanta una instancia nueva.

    El worker:

    // my-pglite-worker.js
    import { PGlite } from '@electric-sql/pglite'
    import { worker } from '@electric-sql/pglite/worker'
    
    worker({
      async init() {
        return new PGlite()
      },
    })
    

    Y el cliente:

    import { PGliteWorker } from '@electric-sql/pglite/worker'
    
    const pg = new PGliteWorker(
      new Worker(new URL('./my-pglite-worker.js', import.meta.url), {
        type: 'module',
      }),
    )
    

    Son quince líneas, pero no las trates como boilerplate. Tu base de datos vive ahora detrás de una frontera asíncrona con elección de líder, y eso condiciona cómo pruebas la app y qué ocurre en el segundo en que el usuario cierra la pestaña líder.

    Migraciones de esquema en dispositivos que no controlas

    En el servidor una migración es un evento: la lanzas, corre, se acabó. Hay una base de datos y tú tienes la llave.

    En local-first tienes N versiones del esquema repartidas por dispositivos ajenos. Un usuario abrió la app en marzo y no ha vuelto. Cuando vuelva, su base local está seis migraciones por detrás y esas seis tienen que aplicarse en orden, en su navegador, sin romperse a mitad.

    Esto es lo que se lleva por delante los planes de rollback: no puedes revertir una migración en 8.000 portátiles.

    La consecuencia práctica es que el esquema local evoluciona de forma aditiva casi siempre. Columnas nuevas, no renombradas. Tablas nuevas, no reestructuradas. Y una tabla de versión de esquema desde el día uno, antes de tener usuarios.

    La red de seguridad que sí funciona no es el rollback, es el reset. Si el servidor es la fuente de verdad del negocio, la base local es desechable: ante una migración que no aplica, la borras del dispositivo y vuelves a sincronizar los shapes desde cero. Es feo, tarda y hay que pintarlo bien, pero funciona.

    La letra pequeña: eso solo existe si hay copia en servidor. Si elegiste el modo máximo de privacidad, no hay de dónde resincronizar y cada migración es un disparo único sobre datos irrecuperables. Ahí el esquema aditivo deja de ser buena práctica y pasa a ser la única opción.

    Resolución de conflictos: aquí no hay magia

    Dos dispositivos offline. Los dos editan el mismo registro. Los dos recuperan la red.

    Sí existen librerías que deciden por ti: los CRDT de Yjs, Automerge o Loro convergen sin preguntarte. Pero convergen a una respuesta, no necesariamente a la que tu negocio considera correcta. Un CRDT te garantiza que dos dispositivos acaban iguales; no te garantiza que el saldo resultante sea el que el usuario esperaba. Esa decisión no la delegas.

    Tus opciones reales son tres: last write wins y perder ediciones en silencio, guardar ambas versiones y preguntar al usuario, o modelar los datos para que los conflictos sean estructuralmente imposibles — append-only, eventos en vez de estado, campos con un único dueño. La tercera es la buena, y es una decisión de modelado que tomas antes de escribir código.

    Su contrapartida: una base append-only crece sin techo, y eso choca con la pregunta que cierra este post — si los datos caben en el dispositivo. Compacta por antigüedad o materializa el estado cada N eventos, desde el principio.

    Un detalle que se olvida: lo que llega del sync es entrada externa y merece validarse como el body de una API. Un payload con un campo cambiado por una versión antigua del cliente puede corromper la base local del usuario, y ahí ya no tienes acceso para arreglarlo. Es el escenario exacto para el que trabajamos schemas en el curso de Zod: validar en la frontera, no confiar en el tipo.

    3 MB antes de que el usuario vea nada

    PGlite pesa menos de 3 MB gzipped, y es un coste de arranque real que pagas en el primer render.

    En una app que el usuario abre a diario se amortiza en el primer uso. En una landing con formulario es inaceptable. Cárgalo diferido, después del primer pintado, con un estado de "preparando" que no sea una pantalla en blanco.

    Los 0,058 ms son en memoria

    Aquí es donde muchos posts sobre PGlite venden humo, así que lo digo claro: los benchmarks de PGlite están medidos en memoria; en cuanto persistes a IndexedDB, la foto cambia.

    La propia documentación lo reconoce: "An fsync or flush to the underlying storage can be quite slow, particularly in the browser with IndexedDB for PGlite, or OPFS for wa-sqlite."

    Y es igual de honesta comparándose con SQLite en WASM: "wa-sqlite is faster than PGlite when run purely in memory", aunque "For single row CRUD inserts and updates, PGlite is faster then wa-sqlite", por usar Write-Ahead Log frente al rollback journal de SQLite.

    La doc avisa además de que comparar Postgres con SQLite es difícil y de que sus benchmarks son un punto de partida, no una sentencia.

    Traducción: sigue siendo órdenes de magnitud más rápido que la red, pero mide tus escrituras con persistencia activada antes de prometer nada.

    Sigue en 0.x

    @electric-sql/pglite está en la 0.5.8 y @electric-sql/pglite-sync en la 0.6.9. Pre-1.0 significa que la API puede moverse entre versiones menores.

    No es razón para descartarlo. Es razón para fijar la versión, leer los changelogs antes de actualizar y no esparcir PGlite por medio proyecto sin una capa propia delante.

    Cuándo NO usar local-first (y qué hacer en su lugar)

    La respuesta honesta es que a la mayoría de las apps no les toca.

    Tu situación Qué hacer
    App de IA de uso diario, con historial largo y propio de cada usuario Local-first con PGlite. Es tu caso.
    Datos sensibles que no deberían tocar tu servidor (salud, legal, finanzas) Local-first, y aquí es requisito, no optimización.
    Necesitas funcionar offline de verdad Local-first. No hay alternativa real.
    Datos compartidos que muchos usuarios editan a la vez Servidor. El coste de resolver conflictos se come la ganancia.
    Landing, e-commerce o cualquier app de sesión corta Servidor. 3 MB de arranque para dos consultas no sale.
    Necesitas consultar millones de filas que no caben en el cliente Servidor, con RAG clásico. Los shapes tienen un límite práctico.
    Equipo sin experiencia en sincronización de datos Servidor, hasta que el dolor justifique la curva.
    Tests de integración y CI que hoy levantan Docker con Postgres PGlite en Node o Bun. Sin migraciones ni sync, pero sigue siendo de una sola conexión.

    Esa última fila merece una nota. Aunque tu app no sea local-first, PGlite te sirve hoy en el pipeline: es un Postgres real, arranca en milisegundos y no necesita contenedor. Cambiar docker compose up por una instancia en memoria en tus tests es la puerta de entrada barata a esta tecnología. Con dos límites: al ser de una sola conexión ahí no vas a reproducir deadlocks, bloqueos entre sesiones ni el comportamiento de tu pool; y PGlite trae un catálogo concreto de extensiones, así que comprueba que las de tu esquema estén en la lista antes de tirar el Docker.

    Cómo decidir esto hoy, en diez minutos

    Responde a una sola pregunta: ¿los datos que tu IA necesita para responder son de un único usuario y caben en su dispositivo?

    Si es que sí, local-first con PGlite te saca el round-trip de red de la ruta crítica, te baja los tokens por turno y te da una historia de privacidad que tus competidores no pueden contar. Empieza por el worker, el esquema versionado, el camino de escritura y una estrategia de conflictos escrita antes de crear la primera tabla.

    Si es que no, quédate en el servidor y duerme tranquilo.

    Y si quieres ver este tipo de decisiones discutidas con proyectos reales delante, es lo que hacemos cada semana en Dominicode Labs.

    Preguntas frecuentes sobre local-first con PGlite

    ¿PGlite sustituye a mi Postgres del servidor?

    No. PGlite es un Postgres embebido de una sola conexión, pensado para vivir junto a la aplicación y no para servir a muchos clientes concurrentes. En una arquitectura local-first, PGlite es la fuente de verdad local del dispositivo y tu Postgres del servidor sigue siendo la del negocio: entre ambos hay sincronización —de servidor a cliente te la dan hecha, de cliente a servidor la montas tú—, no sustitución.

    ¿Puedo hacer RAG entero en el navegador con PGlite?

    Sí, siempre que el corpus sea del usuario y quepa en su dispositivo. PGlite soporta pgvector a través del paquete @electric-sql/pglite-pgvector, así que puedes guardar embeddings y hacer búsqueda por similitud en local sin que los documentos salgan del navegador. Lo que no puedes hacer en el cliente es RAG sobre un corpus corporativo de millones de documentos: eso sigue siendo trabajo de servidor.

    ¿Cuánto pesa PGlite y cómo afecta al arranque de la app?

    PGlite pesa menos de 3 MB gzipped. Se carga una vez y luego queda cacheado, pero es un coste real en el primer render, así que conviene cargarlo diferido después del primer pintado. En una app de uso diario se amortiza sin problema; en una página de sesión corta no compensa.

    ¿Qué pasa si el usuario abre la app en dos pestañas?

    PGlite admite una sola conexión, así que dos pestañas no pueden abrir dos instancias sobre la misma base de datos. La solución oficial es el multi-tab worker: una única instancia dentro de un Web Worker y una elección de líder que hace de proxy para todas las pestañas. Cuando la pestaña líder se cierra, se elige otra automáticamente y se levanta una instancia nueva.

    ¿Está listo para producción si sigue en 0.x?

    Depende de tu tolerancia a que la API cambie. @electric-sql/pglite está en la 0.5.8 y el plugin de sync en la 0.6.9, y pre-1.0 significa que puede haber cambios de API entre versiones menores. Hay proyectos en producción con PGlite, pero si entras, fija la versión exacta, lee los changelogs antes de cada actualización y aísla PGlite detrás de una capa propia para que un cambio de API no te toque cincuenta ficheros. Y si vas a hacer RAG en el navegador, mira el eslabón más verde de la cadena: el paquete de pgvector, @electric-sql/pglite-pgvector, va por la 0.0.9.

    ¿En qué se diferencia PGlite de IndexedDB o de SQLite en WASM?

    IndexedDB es un almacén clave-valor sin lenguaje de consultas: cualquier filtro o join lo escribes tú en JavaScript. SQLite compilado a WASM sí te da SQL y en memoria pura es más rápido que PGlite, pero es SQLite: otro dialecto y otras extensiones que las de tu servidor. PGlite es Postgres compilado a WASM sin máquina virtual Linux, así que ejecutas el mismo dialecto y un catálogo de extensiones que se solapa con el de tu servidor —pgvector incluida—, aunque no estén todas las de Postgres. En local-first, esa paridad es lo que evita mantener dos modelos de datos distintos.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Qué es el graph engineering: el mapa que tu agente no tiene

    Qué es el graph engineering: el mapa que tu agente no tiene

    Le pedí a un agente que renombrara una función. getUserData → fetchUserProfile. Dos minutos de trabajo.

    Hizo grep, encontró siete referencias, las cambió, corrió los tests. Verde. Commit.

    Reventó al día siguiente. La función también se invocaba desde un mapa de handlers, handlers[action], con el nombre viajando como string dentro de un JSON de configuración. Grep encontró siete referencias. Había doce.

    El agente no falló por falta de contexto ni por usar un modelo flojo. Falló porque grep solo compara cadenas y nadie le dio un mapa de relaciones. De eso va el graph engineering.

    Qué es el graph engineering (y el lío que hay con el nombre)

    Graph engineering es la práctica de representar tu código y tu documentación como un grafo explícito de relaciones: los nodos son símbolos —archivos, funciones, clases, conceptos— y las aristas son las relaciones reales entre ellos: importa, llama, hereda, contiene, referencia.

    En vez de que el agente busque texto y adivine, navega aristas.

    Antes de seguir, un aviso honesto: el término no tiene una definición canónica única en 2026. Se usa para dos cosas distintas.

    La primera es el grafo de orquestación. Nodos como unidades de ejecución, aristas como flujo de control: LangGraph, org graphs, work graphs. Ahí el "graph engineering" es diseñar cómo se conectan varios agentes. Es la conversación que arrancó Peter Steinberger en julio de 2026 con una pregunta de seis palabras — "Are we still talking loops or did we shift to graphs yet?" — y que es la continuación natural de lo que conté en loop engineering.

    La segunda es el grafo de recuperación. Nodos como símbolos de tu código, aristas como dependencias reales. Aquí no se decide qué agente actúa después: se decide qué sabe el agente antes de tocar nada.

    Este post va de la segunda. Y no compiten: una es control de flujo, la otra es recuperación. Misma palabra, dos capas del stack.

    Si necesitas el atajo: cuando hables de LangGraph o de coordinar varios agentes, es la primera. Cuando hables de qué código ve tu agente antes de editar, es la segunda.

    Las tres preguntas que ni grep ni los embeddings responden

    Hay tres preguntas sobre tu código que ni la búsqueda por texto ni la búsqueda semántica pueden responder:

    1. Si cambio esto, ¿qué se rompe? El radio de impacto a uno, dos o tres saltos. Grep te da el primer nivel. El transitivo no lo ve nadie.
    2. ¿Quién llama a quién? El call graph completo, con su dirección. Grep te dice que dos archivos mencionan AuthService. No te dice cuál lo consume y cuál lo define.
    3. ¿Qué depende de qué — y qué no depende de nada? Los nodos con grado cero son código muerto, y salen solos. Buscar código muerto con grep es un ejercicio de paciencia.

    Las tres son preguntas sobre topología, no sobre contenido. Por eso hacen falta aristas — y por eso las dos herramientas que usas hoy se quedan cortas.

    Tu agente tiene dos formas de encontrar código, y las dos tienen el mismo agujero.

    Grep busca coincidencia exacta de texto. Es preciso, rápido y determinista. No sabe nada de significado ni de estructura. Si la referencia está construida en runtime, no existe para grep.

    Los embeddings buscan parecido semántico. Encuentran la función de autenticación aunque se llame verificarCredenciales. Pero "se parece" no es "está conectado con". Un chunk sobre logging y otro sobre logging viven cerca en el espacio vectorial aunque uno nunca llame al otro. Es la limitación estructural de RAG que ya toqué en RAG vs fine-tuning.

    Las dos herramientas responden "¿dónde aparece esto?". Ninguna responde "¿con qué está conectado esto?".

    Resumido, con la tercera vía al lado:

    Grep Embeddings Grafo de código
    Pregunta que responde ¿Dónde aparece esta cadena? ¿Dónde hay algo parecido a esto? ¿Con qué está conectado esto?
    Qué necesitas saber antes El nombre exacto Una descripción aproximada Que el símbolo exista
    Ve el segundo salto No No Sí — affected --depth 2
    Ve llamadas indirectas No No Sí, marcadas como INFERRED
    Nivel de certeza Binario: aparece o no aparece Puntuación de similitud EXTRACTED o INFERRED
    Coste de mantenerlo Cero Reindexar + coste de embeddings Re-extracción AST, sin LLM
    Dónde se rompe La referencia se construye en runtime Dos cosas se parecen pero no se llaman Código muy dinámico: DI por string, metaprogramación

    Anatomía del grafo: nodos, aristas y confianza

    Un grafo de código tiene tres piezas: nodos (los símbolos: archivos, funciones, clases), aristas (las relaciones entre ellos) y un nivel de confianza por arista.

    Lo concreto. Construí un grafo con graphify —CLI open source, parseo AST local con tree-sitter, sin vector store— sobre un proyecto pequeño que tengo por ahí. Pequeño a propósito: quería poder verificar a mano cada arista antes de creerme nada. Salieron 115 nodos y 240 aristas.

    Los nodos llevan poco: id, etiqueta, archivo de origen y línea. Lo interesante está en las aristas.

    {
      "source": "src_chunker",
      "target": "src_chunker_needs_chunking",
      "relation": "contains",
      "confidence": "EXTRACTED",
      "source_file": "src/chunker.py",
      "source_location": "L10"
    }
    

    Los ocho tipos de relación que aparecieron en ese grafo:

    Relación Qué conecta ¿La ve grep?
    imports / imports_from Archivo → módulo o símbolo importado Sí, si el nombre aparece literal
    contains Archivo → función o clase que declara Parcialmente
    calls Función → función que invoca Solo el primer nivel
    references Símbolo usado sin invocarlo Sí, si el nombre aparece literal
    inherits Clase → clase base Sí
    method Clase → método que le pertenece Sí
    indirect_call Llamada resuelta en runtime No

    Fíjate en la última fila. indirect_call es exactamente la llamada que grep no ve.

    Y ahora el campo que más me interesa de todo esto, el que casi nadie menciona: confidence. Cada arista viene marcada como EXTRACTED o INFERRED. En mi grafo: 197 extraídas, 43 inferidas.

    EXTRACTED significa que la relación está literalmente en el AST. El parser la leyó, no la dedujo. INFERRED significa que la resolvió el motor uniendo puntos — una llamada cuyo destino tuvo que deducirse.

    Eso cambia cómo usas el resultado. Una arista EXTRACTED la das por buena. Una INFERRED es una hipótesis con nombre y apellidos que puedes ir a verificar al archivo y la línea que te da. Ni los embeddings ni grep te dan esa distinción: grep afirma sin matices, y el score de un embedding te dice cuánto se parece algo, nunca de dónde sale la relación. Aquí lo que se etiqueta es la procedencia.

    Un explain sobre un nodo devuelve esto:

    Node: needs_chunking()
      Source:    src/chunker.py L10
      Degree:    6
    
    Connections (6):
      <-- main() [calls] [INFERRED]
      <-- transcribe() [calls] [INFERRED]
      <-- chunker.py [contains] [EXTRACTED]
      --> Path [references] [EXTRACTED]
      <-- test_needs_chunking_false_for_small_file() [calls] [INFERRED]
      <-- test_needs_chunking_true_for_large_file() [calls] [INFERRED]
    

    Seis líneas. Ahí está el vecindario directo de esa función, con la dirección de cada arista y el nivel de confianza de cada una. Para llegar a lo mismo con grep necesitas varias pasadas y saber de antemano qué buscar.

    Pero el vecindario directo no es el radio de impacto. Para eso hay un comando aparte, que es el que responde literalmente a la pregunta 1: un recorrido inverso por las aristas que tú elijas, a la profundidad que tú digas.

    graphify affected "needs_chunking" --depth 2 --relation calls
    
    Affected nodes for needs_chunking()
    Relations: calls
    Depth: 2
    - test_needs_chunking_false_for_small_file() [calls] tests/test_chunker.py:L24
    - test_needs_chunking_true_for_large_file() [calls] tests/test_chunker.py:L30
    - main() [calls] transcribe.py:L17
    - transcribe() [calls] watch.py:L36
    - test_output_flag_saves_to_specified_path() [calls] tests/test_integration.py:L34
    - test_file_not_found_exits_with_code_1() [calls] tests/test_integration.py:L48
    - test_unsupported_format_exits_with_code_1() [calls] tests/test_integration.py:L55
    - test_api_key_not_in_output() [calls] tests/test_integration.py:L65
    - .on_created() [calls] watch.py:L72
    

    Mira la diferencia. De las seis conexiones del explain, solo cuatro eran llamadas entrantes. El affected a dos saltos da nueve, y las cinco nuevas son las interesantes: los cuatro tests de integración y el handler .on_created() del watcher no tocan needs_chunking directamente, llegan a través de main() y transcribe().

    Ese es el segundo nivel. El que revienta en producción al día siguiente y el que ninguna búsqueda por texto te va a dar, porque no hay ninguna cadena que buscar: la relación existe en la topología, no en el código fuente de esos archivos.

    Aquí está la tesis, y quiero decirla sin vender humo: el grafo no te garantiza encontrar la referencia indirecta. Te da una categoría donde esa relación puede existir y quedar marcada. Grep ni siquiera tiene esa categoría. Esa es toda la diferencia, y es suficiente.

    Cómo usar un grafo de código con un agente de coding, en 3 pasos

    Tres piezas.

    Uno: construyes el grafo y lo dejas en el repo. graphify-out/graph.json más un reporte en markdown. Es un artefacto de tu proyecto, como el lockfile.

    uv tool install graphifyy   # doble "y" mientras reclaman el nombre en PyPI;
                                # el comando y el skill siguen siendo graphify
    graphify install            # registra el skill en tu agente
    graphify update .           # re-extrae solo lo que cambió, sin LLM
    

    Dos: le das al agente una regla de precedencia. Sin esto no sirve de nada, porque el modelo tira de grep por costumbre. En el CLAUDE.md del proyecto:

    - Para preguntas sobre el código, ejecuta primero `graphify query "<pregunta>"`.
      Usa `graphify path "<A>" "<B>"` para relaciones, `graphify explain "<X>"`
      para un concepto concreto y `graphify affected "<X>"` antes de modificar o
      borrar algo. Devuelven un subgrafo acotado, mucho más pequeño que el reporte
      completo o la salida cruda de grep.
    - Después de modificar código, ejecuta `graphify update .`.
    

    Esa regla es la diferencia entre tener un grafo y usarlo. Es la misma idea de fondo que trabajo en el curso de Construye con IA: el agente no es más listo por tener más herramientas, sino por tener reglas claras de cuándo usar cuál.

    Tres: el grafo entra en la ventana como subgrafo, no como volcado. Un explain devuelve seis líneas donde un grep te vuelca cada aparición del término y tú decides después: recuperas menos tokens y mejores, que es el objetivo del context engineering.

    Ojo con una cosa: graphify query no devuelve una respuesta en prosa. Devuelve un recorrido BFS con los nodos encontrados. Es una herramienta de recuperación dentro del harness, no un chatbot. Quien interpreta el subgrafo sigue siendo el modelo.

    Y un apunte de higiene: el proyecto publica cifras de benchmark en su README. Son autoreportadas. Trátalas como lo que son y mide en tu repo.

    Cuándo NO merece la pena montar un grafo de código

    No todo proyecto necesita esto. Cuatro casos donde el grafo estorba más de lo que ayuda.

    Proyectos pequeños. Si el código entra entero en la ventana, el agente ya tiene el grafo en la cabeza y mejor resuelto. Montar recuperación para veinte archivos es sobreingeniería.

    Código muy dinámico. Metaprogramación intensa, inyección de dependencias por string, event buses, decoradores que reescriben comportamiento en runtime. El AST no puede ver lo que solo existe cuando el proceso arranca. El grafo saldrá con más aristas INFERRED que EXTRACTED, o directamente con huecos. Sigue siendo mejor que grep, pero baja mucho el techo.

    Y sí: el bug con el que abrí este post vive justo en esta frontera. Un nombre viajando dentro de un JSON no está en ningún AST. Lo que cambia es que el grafo marca ese hueco como INFERRED o lo deja sin arista, y eso es una señal que puedes leer. Grep te devuelve siete referencias con la misma cara de seguridad que si fueran las doce.

    Si no puedes mantenerlo actualizado. Un grafo obsoleto es peor que no tener grafo, porque el agente confía en él. Necesitas graphify update en un hook de pre-commit, en CI o con graphify watch. Si esto no está automatizado, no lo montes: en dos semanas tienes un mapa de un territorio que ya no existe.

    Si lo que buscas es "qué debería hacer este sistema". El grafo describe el código que existe, no la intención. Para eso el artefacto es la spec — que es, por cierto, otra forma de estructura explícita, y la razón por la que escribí el libro de Spec-Driven Development. El grafo cuenta el presente. La spec define el futuro.

    Y un apunte de madurez: graphify va por la 0.9.x. No es 1.0 todavía, y se nota. Herramienta útil, no infraestructura estable.

    Cómo empezar con graph engineering hoy

    Coge tu repo más feo. El que da miedo tocar.

    Construye el grafo, ejecuta un explain sobre la función que más te intimida y mira su grado. Si el número te sorprende, acabas de descubrir por qué ese refactor lleva meses aplazado.

    El mismo razonamiento sale del código y funciona con lo que sabes: si tus notas son un grafo en vez de una carpeta, se recuperan igual de bien. Lo conté en Zettelkasten para developers.

    Si quieres ver cómo encaja esto con el resto del stack —agentes, MCP, memoria, specs— lo trabajamos a fondo en Dominicode Labs, con proyectos reales y no con ejemplos de juguete.

    Preguntas frecuentes

    ¿Graph engineering es lo mismo que GraphRAG?

    No exactamente. GraphRAG es la implementación de Microsoft que usa un LLM para extraer entidades y relaciones de texto no estructurado, detectar comunidades y resumirlas. Está pensado para corpus documentales.

    Graph engineering es el concepto general de estructurar conocimiento como grafo. Aplicado a código, el grafo se extrae del AST de forma determinista, sin LLM y sin coste por token. GraphRAG es una implementación posible, no la única ni la más barata para código.

    ¿Qué diferencia hay entre graph engineering y loop engineering?

    El loop engineering diseña el bucle de ejecución del agente: qué hace, cómo verifica el resultado y cuándo vuelve a intentarlo. El graph engineering, en la acepción de este post, diseña lo que el agente sabe antes de entrar en ese bucle: un mapa de relaciones de tu código en vez de una búsqueda de texto.

    No compiten. Un agente con un buen bucle y sin mapa repite el mismo error más rápido. Si tus fallos vienen de contexto estructural incompleto, el grafo rinde antes que otra iteración del loop.

    ¿Funciona con TypeScript o solo con Python?

    Los ejemplos de este post salen de un proyecto en Python, pero la extracción es por AST con tree-sitter y las gramáticas que trae cubren los lenguajes habituales: Python, TypeScript, JavaScript, Go, Rust, Java, C, C++, Ruby, C#, Kotlin, Scala y PHP.

    Con TypeScript hay un matiz: cuanto más tira el proyecto de inyección por token, decoradores y factories, más aristas caen en INFERRED. El grafo sigue siendo mejor que grep, pero léelo sabiendo qué parte es hipótesis.

    ¿Necesito una base de datos de grafos como Neo4j?

    Para un repo, no. El grafo de un proyecto normal cabe en un JSON en disco y se recorre con un BFS en memoria. Herramientas como graphify funcionan así, sin servidor y sin dependencias externas.

    Neo4j tiene sentido cuando el grafo es un producto en sí mismo, se consulta desde varios servicios o supera lo que quieres cargar en memoria. Para dar contexto estructural a un agente en tu máquina, es infraestructura que no necesitas.

    ¿El grafo sustituye a los embeddings y a la búsqueda semántica?

    No, y montarlo como sustituto es un error. Responden preguntas distintas.

    Los embeddings responden "¿dónde hay algo parecido a esto?" y toleran que no sepas los nombres exactos. El grafo responde "¿con qué está conectado esto?" y exige que el símbolo exista. Lo razonable es tener las dos vías y una regla de precedencia: para preguntas de estructura, grafo; para exploración difusa, semántica; para strings literales, grep.

    ¿Cada cuánto hay que reconstruir el grafo?

    En cada cambio de código relevante, y automatizado. La re-extracción incremental de código no necesita LLM, así que el coste es tiempo de CPU, no dinero.

    Lo práctico es un hook de pre-commit, un paso en CI o un proceso en watch mientras trabajas. Reconstruirlo a mano cuando te acuerdas es la vía rápida a un grafo obsoleto, y un grafo obsoleto le miente al agente con toda la confianza del mundo.

    ¿Sirve en monorepos grandes?

    Es donde más rinde, precisamente porque el código ya no cabe en la ventana de contexto y grep devuelve ruido. La pega es operativa: la visualización HTML se vuelve pesada por encima de unos miles de nodos, y para eso está la opción de saltarla y quedarte solo con el JSON consultable, que es lo que consume el agente.

    Y si tu organización tiene varios repos en vez de uno solo, puedes fusionar sus grafos en uno para cruzar dependencias entre paquetes.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Por qué la IA se inventa cosas — y por qué no es un fallo

    Por qué la IA se inventa cosas — y por qué no es un fallo

    Le pides las tres sentencias más relevantes sobre un asunto. Te devuelve tres: tribunal, número, año, en el formato exacto en que se citan estas cosas.

    Dos existen.

    La tercera no. Y es indistinguible de las otras dos.

    No está peor escrita. No lleva una nota al pie que diga "esta me la he inventado". No hay cambio de tono, ni duda, ni titubeo. Tiene la misma pinta de ser verdad que las otras dos.

    Y aquí está lo que casi nadie cuenta cuando explica por qué la IA se inventa cosas: no falló nada. El sistema hizo exactamente lo mismo que hace cuando acierta.

    Esto no es un ejercicio teórico. El 22 de junio de 2023, el juez P. Kevin Castel impuso una sanción de 5.000 dólares —solidariamente a dos abogados y a su despacho— en el caso Mata v. Avianca, nº 1:22-cv-01461 del Distrito Sur de Nueva York (678 F. Supp. 3d 443). Habían presentado un escrito con seis sentencias inventadas de principio a fin, con citas internas a resoluciones que tampoco existen.

    Lo mejor viene ahora: uno de ellos le preguntó a ChatGPT si esos casos eran reales. Respondió que sí, y añadió que podían encontrarse en Westlaw, LexisNexis y el Federal Reporter.

    Y aquí está el detalle que casi nadie cuenta: el juez no sancionó por el error. Escribió que usar una herramienta de IA no tiene "nada de intrínsecamente impropio". Sancionó porque, después de que la parte contraria y dos órdenes del tribunal cuestionaran la existencia de esas sentencias, siguieron defendiéndolas.

    Nadie hackeó nada. El modelo no se rompió. Solo hizo su trabajo.


    ¿Por qué la IA se inventa cosas?

    Porque un modelo de lenguaje no busca la respuesta correcta: genera la continuación más probable, pieza a pieza, sobre una distribución de probabilidad aprendida en el entrenamiento. Cuando lo plausible coincide con lo cierto, decimos que acierta. Cuando no coincide, decimos que alucina. Es el mismo mecanismo, el mismo nivel de seguridad en el tono y el mismo aspecto en la pantalla.

    Una alucinación de la IA es exactamente eso: una salida plausible y falsa —una cita, una fecha, un identificador, un método de librería— generada con el mismo procedimiento y con la misma confianza aparente que una salida correcta.

    Lo importante es lo que no hay: en ningún punto del proceso existe un paso que pregunte "¿esto es verdad?".

    No es que se salte la comprobación. Es que la comprobación no está en el diseño. Nadie la quitó porque nunca estuvo.

    Si lo tienes claro en términos de predicción — la misma idea que hay detrás de cualquier algoritmo de machine learning — deja de ser sorprendente. Un sistema entrenado para que la salida sea verosímil produce salidas verosímiles. Ni más ni menos.

            el modelo optimiza UNA cosa:
         que la continuación sea plausible
                        │
            ┌───────────┴─────────────┐
       coincide con              no coincide
        la verdad                con la verdad
            │                         │
        "acierta"                 "alucina"
            └────── el mismo ─────────┘
                   mecanismo
                        │
            y en ningún punto de este
            recorrido hay un paso que
            pregunte: ¿esto es verdad?
    

    Dos nombres distintos para el mismo comportamiento. La diferencia no la pone el modelo: la pone el mundo, al coincidir o no con lo que salió.


    Qué es una alucinación de la IA: el retrato robot que no se parece a nadie

    La analogía que mejor me funciona cuando lo explico en una reunión: un dibujante de retratos robot buenísimo que nunca ha visto al sospechoso.

    Tiene una técnica excelente. Conoce las proporciones, sabe qué rasgos aparecen juntos. Le das una descripción vaga y te devuelve un retrato limpio, coherente, con una nariz que encaja con esos pómulos.

    El retrato está bien hecho. Es convincente. Y puede no parecerse a nadie.

    Eso es una alucinación. No un borrón, no un garabato: un dibujo correcto de una persona que no existe.


    Lo contraintuitivo: alucina más cuando la pregunta tiene forma de respuesta

    Aquí es donde casi todo el mundo tiene el modelo mental invertido.

    La intuición dice: alucina cuando no sabe. Falso. O al menos, insuficiente.

    Alucina más cuando la pregunta parece tener una respuesta con una forma muy clara. Una referencia bibliográfica tiene una forma reconocible. Un artículo de una ley tiene una forma. Una fecha tiene una forma. Un número de sentencia tiene una forma.

    Y lo plausible es exactamente lo que el modelo optimiza. Dale un hueco con forma nítida y lo rellenará con algo que tenga esa forma.

    Corolario práctico y algo perverso: preguntar por una normativa que no existe es la manera más fiable de provocar una alucinación. No porque el modelo sea tonto, sino porque la pregunta le da la plantilla y él es muy bueno rellenando plantillas.

    Hay datos que lo respaldan. En Why Language Models Hallucinate (Kalai, Nachum, Vempala y Zhang, 4 de septiembre de 2025) —tres de los cuatro autores firman por OpenAI; Vempala, por Georgia Tech— los autores le preguntaron tres veces a DeepSeek-V3 por el cumpleaños de uno de ellos, indicándole explícitamente que respondiera solo si lo sabía. Obtuvieron tres fechas: "03-07", "15-06" y "01-01". Ninguna correcta.

    Con el título de su tesis doctoral, tres modelos distintos devolvieron tres títulos distintos. Ninguno acertó ni el título ni el año. Y el detalle que más dice: los tres inventados sonaban mejor que el de verdad.

    Y por si crees que esto solo pasa con datos oscuros: al preguntar cuántas D hay en "DEEPSEEK", los modelos del estudio respondieron 2, 3, y en algunos casos 6 y 7. La respuesta es 1. No es un problema de que le falte información. Está delante.


    No es mentir, y no es un disparate

    Dos precisiones que cambian la conversación con cualquier stakeholder.

    No es mentir. Mentir exige dos cosas: saber la verdad y decir otra cosa a propósito. Aquí no hay ninguna de las dos. No hay intención, y no hay una representación interna de "la verdad" separada de la salida que se pueda contradecir.

    Y las alucinaciones nunca son disparates. Esto es lo que las hace peligrosas. Si el modelo te dijera que el artículo aplicable es el 4.912 de una ley con 90 artículos, lo cazarías al instante.

    Lo que hace es devolverte el artículo 27.3. Verosímil. Bien formateado. En medio de tres párrafos que sí son correctos.

    Ese es el riesgo real: no la barbaridad evidente, sino el dato razonable que sobrevive a la revisión rápida y acaba en producción, en un informe o delante de un cliente. Y se agrava con el tiempo, porque cuando la herramienta lleva doscientos aciertos seguidos, revisar se convierte en echar un vistazo.


    Excelente cuando basta lo plausible. Peligroso cuando tiene que ser exacto

    Esta es la línea que de verdad importa, y la que deberías tener pegada al monitor antes de decidir dónde metes un LLM.

    Lo que le pides ¿Basta con que sea plausible? Veredicto
    Redactar, reformular, dar forma a un borrador Sí — lo plausible es lo bueno Úsalo sin miedo
    Resumir un documento con datos dentro La prosa sí; las cifras y los nombres, no Comprueba cada dato contra el original
    Traducir, ordenar ideas, dar nombre a cosas Sí, con repaso Úsalo
    Generar código que luego compila y se testea Sí — tienes verificador Úsalo, el compilador es tu red
    Una fecha, una cifra, un artículo de una ley No Verificador obligatorio
    Una referencia, un ID, una versión de librería No Verificador obligatorio

    Fíjate en la fila del código, porque es la que explica por qué los modelos funcionan tan bien escribiendo código y tan mal citando fuentes. En código tienes un verificador que se ejecuta: compilador, tipos, tests, linter. La alucinación se cae sola en dos segundos.

    En una cita bibliográfica no hay compilador. Nadie la ejecuta. Solo alguien leyéndola y asintiendo.

    Si tu caso de uso no tiene verificador, tú eres el verificador. Y tú te cansas.


    ¿Se arregla? El paper que medio internet cita al revés

    Existe la versión pesimista: "es intrínseco, no esperes que se arregle". Y existe el paper de Kalai y compañía, que se cita constantemente para apoyar esa frase, diciendo lo contrario.

    Lo que sostienen es esto: las alucinaciones no son un misterio. Empiezan como errores de clasificación binaria bajo presión estadística — si en los datos de entrenamiento un hecho aparece una sola vez, el modelo no tiene con qué distinguirlo de una invención. Su ejemplo: si el 20% de las fechas de nacimiento aparecen exactamente una vez en el preentrenamiento, cabe esperar que el modelo base alucine en al menos el 20% de esas fechas.

    Y luego viene la parte incómoda. Persisten, dicen, por cómo se puntúa a los modelos. Los benchmarks que dominan los leaderboards corrigen en binario: acierto o fallo. Un "no lo sé" puntúa igual que un fallo — cero. Revisaron las diez evaluaciones que dominan esos leaderboards —GPQA, MMLU-Pro, IFEval, Omni-MATH, BBH, MATH, MuSR, SWE-bench, HLE y WildBench— y en nueve de las diez reconocer incertidumbre no da ningún crédito. La única que da algo es WildBench, y con un matiz cruel: su rúbrica puede puntuar más bajo un "no lo sé" que una respuesta mediocre con datos inventados.

    Con esa regla, adivinar siempre es la estrategia óptima. Estamos entrenando buenos examinandos, no sistemas fiables.

    Su propuesta es tan poco glamurosa que por eso nadie la tuitea: cambiar la puntuación de los benchmarks que ya existen, declarando en el enunciado el umbral de confianza — responde solo si tienes más de t de confianza, el fallo resta t/(1−t) puntos, el acierto suma 1 y el "no lo sé" suma 0. Con t = 0.9, cada fallo cuesta nueve.

    Así que sí: la parte del problema que viene de los incentivos es corregible, y eso es una buena noticia.

    Lo que no cambia es el diseño de fondo. Puedes premiar la abstención y conseguir que el modelo diga "no lo sé" muchísimo más a menudo. No conviertes eso en un paso de comprobación de hechos que antes no existía. Mientras la salida se genere por probabilidad, tu arquitectura tiene que contemplar que a veces será plausible y falsa.

    No es una razón para no usarlo. Es una razón para diseñar con eso dentro.


    Cómo evitar alucinaciones en tu código: 5 decisiones para mañana

    Aquí es donde este post se separa de los cincuenta artículos que explican qué son las alucinaciones y terminan con un "revisa siempre las respuestas". Gracias, muy útil.

    Cinco decisiones concretas. Y antes, lo que caza cada una — porque ninguna las caza todas:

    Defensa Qué caza Qué NO caza
    Schema de salida (Zod) Respuestas con la forma equivocada Un ID inventado con la forma correcta
    Consulta a la fuente SKUs, IDs y referencias que no existen Datos que existen pero no aplican
    Cita literal verificada Atribuir algo real a una fuente que no lo dice Una fuente que dice algo falso
    Rama "no lo sé" en el schema El relleno por campo obligatorio La invención cuando el modelo "cree" saber
    Test de abstención Que el sistema invente en preguntas sin respuesta Errores en preguntas que sí tienen respuesta

    1. Verificar en lugar de confiar

    Cada dato factual que salga del modelo y entre en tu sistema pasa por tres filtros, en este orden: schema, tipos, fuente.

    El schema te da forma. Los tipos te dan garantías en compilación. Y la fuente te da la única cosa que el modelo no puede darte: verdad.

    import { z } from 'zod';
    
    // El schema valida la forma. NO valida que el ID exista.
    const Producto = z.object({
      sku: z.string().regex(/^[A-Z]{3}-\d{6}$/),
      precio: z.number().positive(),
    });
    
    const extraido = Producto.parse(salidaDelModelo); // ✅ forma correcta
    const real = await db.productos.findBySku(extraido.sku); // ✅ existencia
    if (!real) throw new SkuInventadoError(extraido.sku); // error tuyo, no de Zod
    

    Un SKU inventado pasa el regex sin problema. Tiene exactamente la forma de un SKU — recuerda: forma clara, invención fiable. La única defensa es la consulta.

    2. Grounding: dale las fuentes delante y exígele la cita

    Si el modelo tiene el texto real en la ventana de contexto, no necesita inventar. Eso es RAG y por qué gana a fine-tuning para problemas de conocimiento — y si quieres verlo montado con código, tienes la implementación completa aquí.

    Pero pedir la cita no basta. Hay que comprobarla:

    // Sin normalizar, un espacio doble o una tilde tumban la comprobación.
    const normalizar = (s: string) =>
      s.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
        .replace(/\s+/g, ' ')
        .trim()
        .toLowerCase();
    
    const cita = normalizar(respuesta.citaLiteral);
    
    // Ojo: includes('') es true. Una cita vacía "aparece" en cualquier documento.
    const citaVerificada =
      cita.length >= 30 &&
      fuentes.some(f =>
        f.id === respuesta.fuenteId && normalizar(f.texto).includes(cita)
      );
    

    Determinista, barato, sin llamadas extra. Si la cita literal no aparece en el documento que dice citar, la respuesta se descarta. Con esto cazas la clase de fallo más caro que existe: la respuesta correcta atribuida a una fuente que no dice eso.

    3. Déjale una salida: "no lo sé" tiene que ser una opción legal

    Este es el error más repetido en las integraciones que reviso: un schema con todos los campos obligatorios y ninguna rama para la ignorancia.

    Cada campo obligatorio sin salida es una invitación a rellenar. Si tu tipo dice que articulo: string es obligatorio, has convertido "no lo sé" en una respuesta imposible de expresar.

    const Respuesta = z.discriminatedUnion('estado', [
      z.object({
        estado: z.literal('encontrado'),
        articulo: z.string(),
        citaLiteral: z.string().min(30), // una cita de cinco caracteres "aparece" en casi cualquier documento
        fuenteId: z.string(),
      }),
      z.object({
        estado: z.literal('no_esta_en_las_fuentes'),
        queFaltaria: z.string(),
      }),
    ]);
    

    Y dilo también en el prompt, explícito: si la información no está en los documentos proporcionados, responde con estado no_esta_en_las_fuentes. No completes con conocimiento propio.

    Dos frases. Baja las invenciones de forma muy visible. Es la misma lógica de los umbrales de confianza del paper, aplicada a tu endpoint.

    4. Dónde no lo metes sin verificador

    Ninguna de estas cosas entra a un sistema por generación directa: identificadores, precios, cantidades, fechas límite, versiones de dependencias, nombres de métodos de una librería, artículos de normativa, referencias.

    Lo de los nombres de métodos merece un párrafo. Cuando le pides código con una librería poco común, el modelo te devuelve el método que debería existir según todas las APIs parecidas que ha visto. Bien nombrado, con la firma coherente, perfectamente plausible. Y no existe.

    Y hay una variante peor con los nombres de paquete: el compilador no te salva de un npm install de una dependencia que el modelo se ha inventado y que alguien ya ha registrado con ese nombre exacto, esperando precisamente eso.

    Da igual que bajes la temperatura a 0. Eso te quita variabilidad, no te da verdad: te devuelve la misma respuesta plausible casi siempre. Si era falsa, ahora es falsa de forma reproducible. Y ni el "casi siempre" está garantizado: en una API la salida a temperatura 0 todavía puede cambiar según cómo se agrupen las peticiones concurrentes en el servidor.

    5. Que los tests no comparen la salida literal

    Un test que hace expect(salida).toBe("...") sobre una respuesta generada está roto de nacimiento. Cambias de modelo o de versión y se cae sin que nada haya empeorado.

    Testea propiedades, no cadenas:

    • La salida valida contra el schema, siempre.
    • Toda cita literal aparece en la fuente que dice citar.
    • Ningún fuenteId sale del conjunto de fuentes inyectadas.
    • Y el que más información da: un conjunto de preguntas cuya respuesta no está en el corpus, donde lo que se comprueba es que el sistema se abstiene. Si tu tasa de abstención en ese conjunto es baja, tu sistema está inventando y todavía no lo sabes.

    Esa cuarta propiedad es el equivalente en tu repo de lo que propone el paper para los benchmarks: dejar de premiar el acierto por adivinar.

    Decidir esto antes de escribir código —qué salida es aceptable y cómo se comprueba— en lugar de parchearlo cuando ya ha explotado, es exactamente el trabajo que describo en Spec-Driven Development.

    Y en el curso Construye con IA montamos estos verificadores dentro del flujo. Es la diferencia entre un prototipo que impresiona en la demo y algo que puedes dejar corriendo.


    La única conclusión que importa

    Deja de preguntarte si el modelo alucina. Alucina, porque es la misma operación con la que acierta.

    Y no es que sea inevitable —el propio paper describe un sistema que puede abstenerse—. Es que tú no puedes construir asumiendo que ya está resuelto.

    La pregunta correcta es otra: ¿en qué punto de mi sistema se detecta un dato falso, y qué pasa si ese punto no existe?

    Si la respuesta es "lo detecta la persona que lo lea", no tienes un sistema. Tienes un borrador con muy buena presentación.

    Elige hoy el dato factual más crítico que salga de un modelo en tu código y ponle un verificador determinista. Uno. Media hora de trabajo. Vas a dormir mejor.

    Y si te has quedado con ganas de ordenar el resto del terreno — grounding, RAG, salida estructurada, evaluación y el resto de los 120 conceptos colocados por zonas, con sus conexiones dibujadas — tengo un mapa de la IA en una hoja para imprimir, gratis aquí. Funciona muy bien para pasársela a quien aprueba estos presupuestos.


    Preguntas frecuentes

    ¿Por qué la IA se inventa cosas?

    Porque un modelo de lenguaje genera la continuación más probable de un texto, token a token, sobre una distribución de probabilidad aprendida en el entrenamiento. Su objetivo es que la salida resulte plausible, no que sea verdadera. Cuando lo plausible coincide con lo cierto, acierta; cuando no coincide, alucina. En ningún momento del proceso hay un paso que verifique si lo generado es verdad: esa comprobación no forma parte del diseño, así que hay que añadirla fuera del modelo.

    ¿Qué son las alucinaciones de la IA exactamente?

    Son salidas plausibles y falsas: una referencia con el formato exacto de una referencia real, una cifra verosímil, una cita bien construida, un método de librería que podría existir. Nunca son disparates evidentes, y por eso son peligrosas. El riesgo no es que el modelo diga algo absurdo, que cualquiera detectaría, sino que introduzca un dato razonable en medio de varios párrafos correctos y ese dato sobreviva a la revisión rápida hasta llegar a producción.

    ¿La IA miente cuando alucina?

    No. Mentir exige saber la verdad y decir otra cosa de forma deliberada, y en un modelo de lenguaje no se da ninguna de las dos condiciones: no hay intención, y no existe una representación interna de la verdad separada de la salida que se pueda contradecir. Por eso tampoco sirve enfadarse con el modelo ni pedirle que "no invente". Lo que sirve es cambiar el diseño alrededor: darle las fuentes, exigirle cita y verificarla con código.

    ¿Cuándo alucina más un modelo de lenguaje?

    Contra lo que parece, no solo cuando no sabe algo. Alucina más cuando la pregunta parece tener una respuesta con una forma muy clara y reconocible: una fecha, un artículo de una ley, un número de sentencia, una referencia bibliográfica. Como el modelo optimiza plausibilidad, un hueco con forma nítida se rellena con algo que tenga esa forma. Preguntar por una normativa que no existe es una de las maneras más fiables de provocar una invención.

    ¿Se pueden eliminar las alucinaciones del todo?

    Se reducen mucho y no se eliminan. Funcionan tres palancas: grounding (darle las fuentes en el contexto), exigir cita literal y verificarla contra el documento, y permitir explícitamente "no lo sé" en el prompt y en el schema de salida. El paper Why Language Models Hallucinate (2025) añade una cuarta a nivel de industria: cambiar la puntuación de los benchmarks, que hoy casi todos dan cero tanto al fallo como al "no lo sé" y por tanto premian adivinar.

    ¿Bajar la temperatura a 0 evita las alucinaciones?

    No. La temperatura controla cuánta variabilidad hay al muestrear el siguiente token, no si el contenido es cierto. Con temperatura 0 el modelo elige en cada paso el token más probable, así que tiendes a obtener siempre la misma respuesta —aunque ni eso está garantizado en una API, donde el resultado depende de cómo se agrupen las peticiones concurrentes—. Si esa respuesta es falsa, ahora es falsa de forma consistente, que engaña más porque parece estabilidad.

    ¿Alucinan menos los modelos de razonamiento?

    Menos en lo que se puede calcular: si la respuesta se deduce paso a paso, más cómputo ayuda. Pero razonar no añade un paso de comprobación contra el mundo, así que en datos que solo se pueden saber —una fecha, una referencia, un identificador— el problema es idéntico. Why Language Models Hallucinate lo atribuye a los incentivos de evaluación, no a la capacidad: mientras un "no lo sé" puntúe igual que un fallo, adivinar sigue siendo la estrategia óptima para cualquier modelo, razone o no.

    ¿Cómo pruebo que mi integración con un LLM no inventa datos?

    No compares la salida literal con una cadena esperada: se rompe en cada cambio de modelo sin que nada haya empeorado. Testea propiedades. Que la salida valide contra el schema, que toda cita literal aparezca en la fuente que dice citar, que ningún identificador de fuente esté fuera del conjunto inyectado, y sobre todo mantén un conjunto de preguntas cuya respuesta no exista en tu corpus, midiendo que el sistema se abstiene en lugar de rellenar.


    Si quieres ver estos verificadores funcionando dentro de proyectos reales, con la arquitectura y el código completos, es parte de lo que trabajamos en Dominicode Labs. Problemas de producción, decisiones que puedes aplicar esta semana.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • RAG vs fine-tuning vs contexto: cuándo usar cada uno

    RAG vs fine-tuning vs contexto: cuándo usar cada uno

    Un cliente me mostró su arquitectura hace unos meses. Había pasado seis semanas haciendo fine-tuning de un modelo para que respondiera preguntas sobre la documentación interna de su empresa.

    Seis semanas. Un dataset de 4.000 pares de pregunta-respuesta construidos a mano. Costes de entrenamiento en GPU. Y al final, el sistema seguía inventándose respuestas cuando la pregunta tocaba un documento que no estaba en el training data.

    Le pregunté por qué no había usado RAG. Me dijo que pensó que fine-tuning era "la solución profesional". Que RAG era para hacer demos rápidas.

    Esa diferencia entre RAG y fine-tuning se malentiende constantemente, y el malentendido sale caro. Pero hay algo peor: casi nadie considera la tercera opción, que es la más barata de las tres y resuelve más casos de los que parece.


    ¿Cuál es la diferencia entre RAG, fine-tuning y contexto?

    La diferencia entre RAG y fine-tuning es qué problema resuelve cada uno: RAG le da al modelo información que no tiene, fine-tuning le cambia la forma de comportarse. Y hay una tercera vía que va antes de las dos: pegar el material en el contexto de la petición. Esta es la tabla que conviene tener delante en una reunión de presupuesto.

    Qué es Cuándo Coste Dónde se rompe
    Contexto Se lo pegas tú en la petición Poco material, uso puntual Bajo, pero lo pagas en cada llamada No cabe, o se pierde lo del medio
    RAG Lo busca en tus documentos al preguntar Mucho material, y que cambia Medio, sobre todo el pipeline Si busca mal, responde mal
    Fine-tuning Ajustas el modelo con ejemplos Formato y tono muy propios, o clasificar y extraer en tu dominio Alto, y se repite con cada modelo nuevo No sirve para meter datos

    La regla, en una línea: contexto para lo puntual, RAG para lo que sabes, fine-tuning para cómo lo dices.

    Si alguien propone fine-tuning para que el modelo conozca vuestros datos, hay una conversación pendiente antes de firmar nada.


    El error conceptual que lo complica todo

    La mayoría de developers que se acercan a este problema lo enmarcan mal desde el principio.

    Piensan en términos de "qué técnica es más potente". Y ahí ya van por el camino equivocado.

    La pregunta correcta no es cuál es más potente. Es: ¿qué problema tienes exactamente?

    Si tu modelo no sabe cosas que necesita saber — información privada, documentos internos, datos recientes — tienes un problema de conocimiento. Contexto o RAG lo resuelven.

    Si tu modelo sabe las cosas pero no las comunica como necesitas — tono diferente, formato específico, comportamiento distinto al por defecto — tienes un problema de comportamiento. Fine-tuning lo resuelve.

    Son problemas distintos. Las soluciones no son intercambiables. Y aquí está la frase que ahorra más dinero de todo este terreno:

    El fine-tuning enseña comportamiento, no información.

    Si le haces fine-tuning con el catálogo de productos, no acabas con un modelo que se sepa el catálogo. Acabas con uno que habla como tu catálogo e inventa referencias con el estilo exacto de las tuyas. Bastante peor que no hacer nada, porque las invenciones son más creíbles.


    Contexto: la opción que deberías agotar primero

    Es la que se salta todo el mundo, y en muchos proyectos es la única que hacía falta.

    Contexto es, literalmente, pegar la información en la petición. El manual, el fragmento de código, las tres facturas de ejemplo. Sin infraestructura, sin pipeline, sin base de datos vectorial. Es lo que la literatura llama in-context learning y lo que en la práctica se acaba llamando prompt stuffing.

    Sus dos límites reales:

    No cabe todo. La ventana de contexto es el número máximo de tokens que entran en una sola petición, sumando lo que envías y lo que el modelo genera. Los modelos actuales manejan ventanas grandes, pero grande no es infinito y el coste sube con lo que metes.

    Se paga en cada llamada. No es una inversión que amortices: es un peaje por petición. Mil consultas al día con el mismo manual de 20.000 tokens delante son veinte millones de tokens al día de material repetido.

    Ese segundo problema tiene solución y casi nadie la aplica, así que le dedico una sección propia más abajo.

    Y una confusión que conviene cortar aquí: la ventana de contexto no es memoria. Una ventana enorme te deja meter mucho de una vez. Memoria es que algo sobreviva a cerrar la sesión. Son cosas distintas y una no da la otra. Cuando un proveedor te venda un asistente "con memoria", la pregunta útil no es si la tiene, sino qué guarda, dónde y durante cuánto tiempo.


    Qué es RAG (Retrieval-Augmented Generation) y cuándo usarlo

    RAG no modifica el modelo. El modelo base sigue siendo exactamente el mismo.

    Lo que hace es intervenir en el momento en que llega una pregunta. Antes de pasársela al modelo, busca en una base de datos vectorial los fragmentos de tus documentos más relevantes para esa consulta, y los inyecta en el prompt. El modelo entonces responde con acceso real a esa información.

    Usuario pregunta: "¿Cuál es la política de devoluciones?"
                             ↓
                 Sistema RAG busca en vectorDB
                             ↓
          Encuentra: chunk del doc "politica-devoluciones-2026.pdf"
                             ↓
        Prompt al modelo: "Contexto: [chunk]. Pregunta: ¿Cuál es...?"
                             ↓
                Modelo responde con información real
    

    La ventaja clave: tus documentos pueden cambiar mañana. Actualizas la base vectorial. El modelo ya tiene acceso a la nueva información. Sin reentrenar nada.

    Visto así, RAG es contexto automatizado: en vez de pegar tú el fragmento correcto, un buscador lo elige por ti en cada petición. Por eso la pregunta de diseño en RAG no es qué modelo usas, es si tu buscador encuentra lo que hace falta.

    Si quieres verlo montado con código, tengo el paso a paso en implementación de RAG en Angular, y las estrategias de búsqueda híbrida en RAG avanzado. Y si estás eligiendo el modelo para el componente generativo, este análisis sobre el mejor modelo LLM local en 2026 te ayuda a no sobreingenierizar la infraestructura.

    Esto es lo que lo hace ideal para documentación interna, bases de conocimiento, FAQs, soporte técnico — cualquier caso donde la información cambia y necesitas que el modelo cite fuentes reales en lugar de fabricar respuestas.

    El límite de RAG está en que no cambia cómo se comporta el modelo. Si necesitas que responda en un tono muy específico, siga un formato exacto, o haga razonamientos que el modelo base no hace bien de forma natural, RAG no te ayuda. Solo le das más información. No lo entrenas.


    Qué es fine-tuning de LLMs y cuándo tiene sentido aplicarlo

    Fine-tuning sí modifica el modelo. Tomas un modelo base preentrenado y lo sigues entrenando con tu propio dataset, ajustando sus pesos para que aprenda los patrones que te interesan.

    El resultado es un modelo diferente. Uno que ha interiorizado un estilo, un formato, un tipo de razonamiento específico. No necesitas darle instrucciones en el prompt porque ya las tiene grabadas en sus pesos.

    # Sin fine-tuning: necesitas el prompt completo
    prompt = """Eres un asistente técnico especializado en Kubernetes.
    Responde siempre con: 1) causa del problema, 2) solución paso a paso,
    3) cómo prevenirlo. Usa terminología técnica precisa. No añadas
    disclaimers. El tono es directo, de senior a senior.
    
    Problema: Mi pod no arranca después de actualizar la imagen..."""
    
    # Con fine-tuning: el modelo ya sabe cómo comportarse
    prompt = "Problema: Mi pod no arranca después de actualizar la imagen..."
    

    El modelo fine-tuneado responde directamente en el formato correcto porque ese comportamiento está en sus pesos. No porque se lo estés recordando en cada llamada.

    Lo que fine-tuning no resuelve: inyectar conocimiento factual nuevo. Si entrenas el modelo en el estilo de tu empresa pero no en los documentos de tu empresa, seguirá sin saber qué contienen esos documentos. Habrá aprendido a comunicarse como tú quieres, pero no a responder con información real que no tenía.

    Y no es entrenar desde cero. Eso cuesta una fortuna y lo hacen muy pocas organizaciones en el planeta. Esto es un retoque sobre un modelo existente.


    La noticia que cambia el cálculo: OpenAI está cerrando su fine-tuning

    Si tomaste esta decisión antes de mayo de 2026, vuelve a tomarla. El tablero ha cambiado.

    OpenAI está retirando su plataforma de fine-tuning self-serve. Lo dice en su propia página de deprecations, con fechas:

    Fecha Qué pasa
    7 de mayo de 2026 Las organizaciones que nunca habían hecho fine-tuning ya no pueden empezar
    2 de julio de 2026 Y tampoco las que llevan 60 días sin ejecutar inferencia sobre un modelo fine-tuneado
    6 de enero de 2027 Los clientes activos dejan de poder crear jobs de fine-tuning

    La inferencia sobre modelos ya fine-tuneados sigue funcionando hasta que se deprecie el modelo base. Y en la guía de fine-tuning la propia OpenAI dice que la plataforma "no es accesible para usuarios nuevos", con una lista de modelos soportados que se ha quedado en la familia gpt-4.1 más gpt-4o para visión y o4-mini para refuerzo.

    Lo interesante es a dónde te manda su propia documentación: al ciclo de evals más prompt engineering. Contexto relevante, instrucciones claras, ejemplos few-shot y, textualmente, "start with gpt-5.6 for new work". Es decir, la primera columna de la tabla de arriba, ejecutada sobre el modelo más nuevo que tengas a mano.

    Y un matiz antes de que alguien lo lea como "el fine-tuning ya no sirve": la misma guía sigue listando como caso válido entrenar un modelo más pequeño, más barato y más rápido para una tarea concreta donde uno grande no sale a cuenta. Eso es exactamente el Caso 4 de más abajo.

    Qué significa esto en la práctica:

    • Si estabas planteándote fine-tuning en OpenAI y no lo has hecho nunca, esa puerta ya está cerrada. No es una decisión pendiente.
    • El fine-tuning sigue existiendo fuera de OpenAI — modelos abiertos con LoRA o QLoRA, y otros proveedores.
    • Pero cuando el proveedor más grande te dice que uses un modelo mejor con mejores prompts en lugar de ajustar uno peor, merece la pena escuchar el argumento antes de montar un pipeline de entrenamiento.

    No es que el fine-tuning haya dejado de servir. Es que el rango de casos donde gana se ha estrechado, y los modelos base han absorbido buena parte de lo que antes justificaba entrenarlos.


    RAG vs fine-tuning: la matriz de decisión con cuatro casos reales

    Hay cuatro combinaciones que aparecen una y otra vez en proyectos reales.

    Caso 1: Chatbot sobre documentación interna

    Necesitas que el modelo responda preguntas sobre tus PDFs, wikis, Notion, Confluence. La información cambia regularmente. El tono puede ser el del modelo base.

    Solución: RAG. Indexas los documentos en una vectorDB (pgvector, Pinecone, Weaviate), configuras el pipeline de retrieval, y el modelo responde con fuentes reales.

    Pero antes de montarlo: si son cuatro documentos que caben en el contexto y cambian una vez al trimestre, empieza por contexto y ahórrate el pipeline. RAG paga cuando el material no cabe o cambia a diario.

    Caso 2: Generador de código en el estilo de tu empresa

    Quieres que el modelo genere código que siga tus convenciones internas, use tus abstracciones propias, evite los patrones que prohíbes.

    Solución clásica: fine-tuning. Pero prueba primero con un documento de convenciones en el contexto y tres o cuatro ejemplos antes/después. Los modelos actuales siguen instrucciones de formato mucho mejor que los de hace dos años.

    Veredicto hoy: contexto primero. Fine-tuning solo si el prompt con convenciones y ejemplos falla de forma medible, no porque el resultado te parezca mejorable.

    Caso 3: Asistente de soporte que responde sobre tus productos Y en tu tono

    Quieres las dos cosas: información factual que cambia, y un comportamiento de comunicación muy específico.

    Solución: RAG para la información, y el comportamiento en el prompt de sistema. Si tras iterar el prompt el formato sigue siendo inconsistente y tienes miles de ejemplos buenos, entonces fine-tuning para la parte de comportamiento. En ese orden, no al revés.

    Caso 4: Clasificador de texto o extractor de entidades

    Necesitas clasificar tickets, extraer entidades de contratos, tareas de NLP muy específicas.

    Aquí es donde fine-tuning sigue defendiéndose mejor: para clasificación y extracción, un modelo pequeño ajustado a tu dominio suele salir más barato en inferencia que uno grande con prompts largos, y más consistente. Es el caso con mejor retorno de los cuatro.


    Los costes reales

    Contexto:

    • Desarrollo: prácticamente nulo. Es construir un prompt.
    • Inferencia: pagas los tokens de entrada en cada petición, así que el coste escala con el volumen, no con el tamaño del proyecto.
    • Mantenimiento: cambiar el texto.
    • Problema principal: el material repetido se paga una y otra vez — salvo que uses caché.

    RAG:

    • Configurar el pipeline de chunking, embedding y retrieval: días de desarrollo, no semanas.
    • Inferencia: coste del modelo base más las llamadas a la vectorDB, que son baratas.
    • Mantenimiento: actualizar la base vectorial cuando cambian los documentos, y es automatizable.
    • Problema principal: la calidad del retrieval. Si buscas mal, el modelo responde mal aunque los documentos sean perfectos.

    Fine-tuning:

    • Construir el dataset de entrenamiento: semanas. Es el cuello de botella real, no la GPU.
    • Entrenamiento: la estructura del coste es por tokens procesados o por horas de GPU según proveedor. Los precios concretos cambian cada pocos meses, así que consúltalos en el proveedor el día que decidas: cualquier cifra que leas en un post de hace medio año está mal.
    • Inferencia: más cara que el mismo modelo sin ajustar. En un proveedor gestionado el modelo fine-tuneado tiene su propia tarifa por token, más alta que la del base, sin que tú hostees nada; con modelos abiertos lo pagas en infraestructura. Por los dos caminos, más que el punto de partida.
    • Problema principal: te ancla a una versión. Cada modelo nuevo obliga a repetir el proceso entero mientras el resto del mundo avanza gratis.

    Ese último punto es el que más se subestima. El coste del fine-tuning no es el entrenamiento: es quedarte fuera de la siguiente generación de modelos.


    La caché de contexto: la palanca que casi nadie activa

    Si tienes un sistema que atiende mil consultas al día y en cada una envía las mismas instrucciones, el mismo manual y los mismos ejemplos, estás pagando por procesar ese material mil veces.

    La caché de contexto guarda el trabajo ya hecho sobre la parte que se repite y lo reutiliza. La parte repetida sale bastante más barata a partir de la segunda vez.

    La condición es exigente y es donde falla todo el mundo: lo repetido tiene que ir siempre al principio y sin variar ni un carácter.

    ✅ CACHEABLE
       [instrucciones fijas][manual fijo][ejemplos fijos][consulta variable]
    
    ❌ NO CACHEABLE
       [fecha y hora][instrucciones fijas][manual fijo][consulta variable]
        ↑ un timestamp al principio invalida el bloque entero
    

    Meter la fecha, un identificador de sesión o el nombre del usuario al comienzo del bloque fijo lo invalida todo. Y nadie te avisa: simplemente sigues pagando el precio completo.

    Hay además un suelo del que casi no se habla: por debajo de unos cientos o unos miles de tokens —el umbral cambia por proveedor y por modelo— la caché no se activa. Tampoco salta un error. Se procesa a precio completo y a otra cosa.

    Las condiciones exactas —qué se cachea, cuánto dura, cuánto descuenta— son distintas en cada proveedor y cambian cada pocos meses. Contrasta antes de contar con el ahorro.

    Si tu factura de IA empieza a crecer, esta es la primera pregunta que hay que hacer en la reunión, antes de discutir de modelos: ¿estamos aprovechando la caché de contexto?


    Qué pasa cuando las combinas

    La combinación que se ve en sistemas de producción serios sigue un patrón concreto. Y es parte de una arquitectura más amplia — si quieres entender cómo el LLM encaja con el resto del sistema, el post sobre qué es un agent harness lo explica con detalle.

    • Contexto para las instrucciones y el comportamiento, con la parte fija cacheada
    • RAG para la información factual que cambia
    • Fine-tuning solo si el comportamiento sigue siendo inconsistente después de agotar las dos anteriores

    Un ejemplo real: un asistente jurídico. El prompt de sistema fija el formato del análisis y la terminología, y va cacheado porque no cambia. RAG conectado a la base de legislación actualizada y a los expedientes del despacho. Fine-tuning, en este caso, ni aparece: el modelo base ya redacta en registro jurídico si se le pide bien.

    Esa es la arquitectura que más veo en productos de IA que funcionan. No es glamorosa. En el curso Construye con IA: de la idea al producto con Claude Code trabajo estas decisiones desde la fase de especificación — antes de escribir una línea de código — para que no llegues a la semana seis arrepintiéndote de la técnica que elegiste.


    El árbol de decisión que uso en consultoría

    Cuando alguien me pregunta qué usar, estas son las preguntas en orden:

    1. ¿Cabe el material en el contexto y cambia poco?

    • Sí → contexto, y cachea la parte fija. Ya está, no montes nada más.
    • No cabe, o cambia a diario → siguiente pregunta.

    2. ¿El problema es que el modelo no tiene la información, o que no se comporta como quieres?

    • No tiene la información → RAG
    • No se comporta bien → siguiente pregunta

    3. ¿Has iterado el prompt de sistema en serio, con ejemplos few-shot?

    • No → hazlo antes. Es gratis y resuelve más de lo que la gente espera.
    • Sí, y sigue inconsistente → siguiente pregunta

    4. ¿Tienes miles de ejemplos de calidad y un problema bien definido que no va a cambiar?

    • No → sigue con prompting. Fine-tuning sin dataset bueno es dinero quemado.
    • Sí → fine-tuning es defendible, siempre que el proveedor te deje.

    La mayoría de los casos que veo en producción se resuelven en los dos primeros escalones. Fine-tuning es potente, pero exige un problema muy bien definido, datos de calidad y tiempo para construirlos.


    Tabla comparativa detallada: RAG vs fine-tuning

    RAG Fine-tuning
    Problema que resuelve El modelo no tiene la información El modelo no se comporta como quieres
    Modifica el modelo No Sí
    Cuándo usar Datos dinámicos, documentos, bases de conocimiento Estilo, formato, clasificación de dominio
    Coste de inicio Medio (pipeline) Alto (dataset + entrenamiento)
    Mantenimiento Fácil (actualizar vectorDB) Costoso (reentrenar cuando cambia el problema)
    Tiempo hasta producción Días Semanas
    Te ancla a un modelo No Sí
    Combinar con el otro Sí Sí
    Disponible en OpenAI (julio 2026) Sí Cerrado a usuarios nuevos; los activos, hasta el 6 de enero de 2027

    Guarda esta tabla. Te va a ahorrar más de una conversación.

    Y si te ha servido ordenar estas tres piezas, hay un mapa con las otras ciento diecinueve: conceptos de IA colocados por zonas, con las conexiones dibujadas, en una hoja para imprimir. Está aquí, gratis — funciona especialmente bien para pasársela a la gente de producto que aprueba estos presupuestos.


    Preguntas frecuentes

    ¿Cuál es la diferencia entre RAG y fine-tuning?

    RAG no toca el modelo: busca fragmentos relevantes en tus documentos y los inyecta en el prompt antes de generar, así que resuelve problemas de conocimiento y se actualiza cambiando un documento. Fine-tuning sí modifica el modelo, ajustando sus pesos con tus ejemplos, y resuelve problemas de comportamiento — tono, formato, criterio de clasificación. La confusión más cara del sector es usar fine-tuning para meter información: no acabas con un modelo que sepa tus datos, sino con uno que inventa datos con tu estilo.

    ¿Cuándo usar fine-tuning de verdad?

    Cuando se cumplen las cuatro condiciones a la vez: el problema es de comportamiento y no de información, ya has iterado el prompt de sistema con ejemplos few-shot y sigue inconsistente, tienes miles de ejemplos de calidad, y la tarea está lo bastante definida como para que no cambie en unos meses. El caso con mejor retorno es la clasificación o extracción de dominio, donde un modelo pequeño ajustado sale más barato en inferencia que uno grande con prompts largos.

    ¿Es verdad que OpenAI está cerrando el fine-tuning?

    Sí, la plataforma self-serve. Según su página de deprecations, desde el 7 de mayo de 2026 las organizaciones que no habían hecho fine-tuning antes ya no pueden crear jobs, y el 6 de enero de 2027 dejarán de poder hacerlo también los clientes activos. La inferencia sobre modelos ya ajustados sigue hasta que se deprecie el modelo base. OpenAI redirige en su lugar a su ciclo de evals y prompt engineering —contexto relevante, instrucciones claras, ejemplos few-shot— y a empezar por su modelo más reciente. El fine-tuning sigue disponible fuera de OpenAI, con modelos abiertos y otros proveedores.

    ¿Qué es la caché de contexto y cuánto ahorra?

    Es guardar el trabajo de procesamiento ya hecho sobre la parte del prompt que se repite en todas las peticiones —instrucciones, manuales, ejemplos— para no volver a pagarla entera cada vez. El requisito es que ese bloque vaya al principio y sea idéntico carácter por carácter: meter un timestamp o un ID de sesión delante lo invalida y sigues pagando el precio completo sin que nadie te avise. Cuánto descuenta y cuánto dura la caché varía por proveedor y cambia cada pocos meses, así que conviene comprobarlo en la documentación antes de meter el ahorro en un presupuesto.

    ¿Qué sale más barato, RAG o fine-tuning?

    RAG, casi siempre, y por dos motivos distintos: el arranque son días de pipeline frente a semanas de dataset, y no te ancla a una versión del modelo, así que no repites el gasto con cada generación nueva. Fine-tuning solo gana en coste cuando la tarea es de clasificación o extracción y puedes servir un modelo pequeño en lugar de uno grande con prompts largos. Antes de comparar las dos, mira si tu prompt fijo es cacheable: en sistemas con mucho volumen esa palanca sola suele mover más la factura que la elección de técnica.

    ¿Cuánto cuesta hacer fine-tuning?

    Tiene tres partidas y solo una no caduca. El dataset son semanas de trabajo humano y es el cuello de botella real. El entrenamiento se factura por tokens procesados o por horas de GPU según el proveedor. La inferencia va a una tarifa más alta que la del modelo base. Cualquier cifra concreta que leas en un post de hace medio año está mal, así que consulta el pricing del proveedor el día que decidas — pero cuenta con que la partida del dataset no la abarata nadie.

    ¿Cambia algo con los modelos de razonamiento?

    Bastante, y en la dirección de necesitar menos fine-tuning. Siguen instrucciones complejas mucho mejor que las generaciones anteriores, así que se comen buena parte de los casos de comportamiento que antes justificaban entrenar un modelo. Lo que no cambia es el acceso a la información: un modelo de razonamiento no conoce tus documentos privados ni lo que pasó esta semana. Para eso RAG sigue siendo la única vía.

    ¿Puedo usar RAG con cualquier LLM?

    Sí. RAG es agnóstico al modelo: funciona con cualquiera que acepte un prompt de texto, sea de OpenAI, Anthropic, Google o un modelo abierto que corras tú. Lo único que necesita es una ventana de contexto suficiente para recibir los fragmentos recuperados junto con la pregunta, y los modelos actuales van sobrados para eso en la mayoría de casos.

    ¿La ventana de contexto es lo mismo que memoria?

    No, y se confunden constantemente. La ventana de contexto es cuánto cabe en una sola petición. Memoria es que algo sobreviva a cerrar la sesión, y eso no está dentro del modelo: es un almacén externo que alguien montó, más una decisión sobre qué merece la pena guardar y recuperar. Una ventana enorme no te da memoria, y por eso un asistente puede seguirte el hilo durante media hora y no conocerte de nada al día siguiente.

    ¿RAG siempre alucina menos que el modelo base?

    Reduce las alucinaciones sobre hechos concretos de tus documentos, porque el modelo tiene el texto real delante. Pero no elimina las que vienen de razonamientos o inferencias mal hechas: si el modelo falla razonando, RAG no le ayuda, porque eso es un problema de capacidad y no de conocimiento. Recuperar bien y responder bien son dos cosas distintas.

    ¿Qué vectorDB conviene para empezar?

    pgvector si ya usas PostgreSQL, porque no añade infraestructura nueva, o un servicio gestionado como Pinecone si prefieres no operar nada. Weaviate y Chroma son buenas opciones open-source para auto-hosting. Evita sobreingenierizar esto al principio: pgvector resuelve la mayoría de los casos sin complejidad extra, y su documentación oficial cubre la instalación en unos minutos.


    Si quieres ver estos patrones aplicados en proyectos reales con código y arquitectura completa, en Dominicode Labs trabajamos este tipo de decisiones técnicas con la comunidad. Proyectos reales, problemas reales, decisiones que puedes aplicar esta semana.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Implementación de RAG en el Frontend con Angular para Chat con PDFs

    Implementación de RAG en el Frontend con Angular para Chat con PDFs

    Implementación de RAG (Retrieval-Augmented Generation) en el Frontend con Angular

    Tiempo estimado de lectura: 5 min

    • Ideas clave:
    • RAG combina recuperación semántica (vector DB) y generación (LLM); en el frontend debe evitar exponer claves y delegar embeddings/search/generation a una BFF/Edge Function.
    • Flujo: preprocesado offline → indexación (vector DB) → consulta runtime a Edge Function → streaming del LLM al frontend.
    • Angular actúa como orquestador UI: JWT al BFF, consumo de ReadableStream, y Signals para estado y streaming.
    • Seguridad multi-tenant: RLS/metadata.filter y nunca incluir keys en el bundle cliente.

    Implementación de RAG (Retrieval-Augmented Generation) en el Frontend con Angular: aquí verás un diseño pragmático y seguro para que tus usuarios “chateen” con PDFs y bases de conocimiento sin exponer claves privadas, y con una experiencia de streaming y baja latencia.

    Resumen rápido (lectores con prisa)

    RAG une un índice vectorial para recuperar contexto relevante con un LLM que genera respuestas. Úsalo cuando necesites respuestas ancladas en documentación privada. Importa porque permite precisión y control de costos. Funciona: preprocesado e indexado server-side, BFF/Edge Function para retrieval + prompt building + streaming, y frontend que consume el stream y mantiene estado con Signals.

    Implementación de RAG (Retrieval-Augmented Generation) en el Frontend con Angular — resumen arquitectónico

    RAG combina recuperación semántica (vector DB) y generación (LLM). En el frontend esto se traduce en tres responsabilidades claras:

    • Orquestar la UI y el estado reactivo.
    • Llamar a una capa segura (BFF / Edge Function) que haga embeddings, búsqueda y generación.
    • Mostrar la respuesta en streaming con Signals para una UX fluida.

    Nunca pongas claves de OpenAI, Pinecone o Supabase en el bundle. Usa BFF/Edge Functions. Patrones y herramientas: Supabase (pgvector + Edge Functions + RLS), Pinecone, OpenAI Embeddings, SSE/Streams MDN, Angular Signals.

    Paso a paso: flujo de datos y responsabilidades

    Preprocesado (server-side, offline)

    • Extrae texto del PDF (pdfminer, tika, or pdf-lib).
    • Segmenta en chunks (200–1000 tokens según modelo).
    • Calcula embeddings y guarda vectores con metadata: { documentId, chunkId, text, userId }.
    • Upsert en la vector DB (Pinecone o Supabase pgvector).

    Consulta desde Angular (runtime)

    • Usuario pregunta en la UI.
    • Angular envía la petición a la Edge Function (BFF) con el JWT del usuario.
    • La Edge Function:
      • a) crea embedding de la consulta,
      • b) hace búsqueda semántica filtrada por metadata (userId) en la vector DB,
      • c) construye prompt con los top-K chunks,
      • d) llama al LLM (streaming) y reenvía el stream al cliente.

    Presentación (cliente)

    • Angular consume el stream y muestra la respuesta en tiempo real.
    • Signals mantiene conversación, estados y métricas.

    Código práctico: servicio Angular para consumir stream RAG

    Este es el patrón cliente: Angular delega todo a una URL segura y procesa un ReadableStream en Signals.

    <!-- rag-client.service.ts -->
    import { Injectable, signal } from '@angular/core';
    
    export interface ChatMessage { role: 'user'|'assistant'; content: string; }
    
    @Injectable({ providedIn: 'root' })
    export class RagClientService {
      public convo = signal<ChatMessage[]>([]);
      public loading = signal(false);
    
      async ask(question: string, docId: string, token: string) {
        this.convo.update(c => [...c, { role: 'user', content: question }]);
        this.convo.update(c => [...c, { role: 'assistant', content: '' }]);
        this.loading.set(true);
    
        const res = await fetch(`/api/rag?doc=${docId}`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
          body: JSON.stringify({ question })
        });
    
        if (!res.body) { this.loading.set(false); throw new Error('No stream'); }
        const reader = res.body.getReader();
        const dec = new TextDecoder();
        let done = false;
    
        while (!done) {
          const { value, done: rDone } = await reader.read();
          done = rDone;
          if (value) {
            const chunk = dec.decode(value, { stream: true });
            this.append(chunk);
          }
        }
        this.loading.set(false);
      }
    
      private append(text: string) {
        this.convo.update(c => {
          const last = [...c];
          const idx = last.length - 1;
          last[idx] = { ...last[idx], content: last[idx].content + text };
          return last;
        });
      }
    }
    

    Seguridad y multi-tenant: cómo proteger datos y consultas

    • Edge Functions (Supabase / Vercel) firman y validan JWT. Angular solo envía el token del usuario.
    • En Supabase usa Row Level Security (RLS) para que la consulta vectorial devuelva solo vectores del userId (https://supabase.com/docs).
    • En Pinecone añade metadata.filter (userId) en las queries y realiza autorización en tu backend.
    • Nunca aceptes keys “hosted” en el cliente; si necesitas BYOK (Bring Your Own Key), que el usuario lo suministre y limite permisos.

    Buenas prácticas de diseño

    • Chunking y contexto: corta por sentencias y 200–500 tokens. Guarda overlap para preservar contexto.
    • Top-K + score threshold: recupera 3–8 chunks y descarta con score bajo para evitar ruido.
    • Fallback y control de costos: si la DB no devuelve contexto útil, responde con “No encontré información” en lugar de llamar al LLM.
    • Telemetría: registra latencia de retrieval vs generation, porcentaje de respuestas sin contexto.
    • UX: muestra progreso de retrieval y luego streaming del LLM; evita spinners largos.

    Resumen y criterio

    Implementación de RAG en el frontend con Angular no es “cliente habla con Pinecone”. Es dividir responsabilidades: preprocesado e indexación en backend, retrieval y prompt-building en BFF/Edge Function, y presentación + streaming en Angular. Esa separación protege claves, permite RLS/tenant isolation y ofrece una UX moderna con Signals y ReadableStreams.

    Si quieres ejemplos de Edge Functions (Supabase) o plantillas para indexado de PDF, lo siguiente es lo lógico: un script server-side que extrae texto, chunkea, crea embeddings (OpenAI) y hace upsert a la vector DB. Cuando tengas ese bloque, el frontend es trivial: JWT + fetch streaming + Signals. Implementa eso y tendrás un “chat” con tus PDFs que realmente sirve en producción.

    Para recursos citados en el artículo: Supabase (pgvector + Edge Functions + RLS), Pinecone, OpenAI Embeddings, SSE/Streams MDN, Angular Signals.

    Continúa explorando plantillas y laboratorios técnicos en Dominicode Labs para ver implementaciones prácticas y scripts de indexado que complementan este flujo.

    FAQ

    ¿Qué es RAG y cuándo debería usarlo?

    RAG (Retrieval-Augmented Generation) combina una base de conocimiento indexada en vectores para recuperar contexto relevante y un LLM para generar respuestas. Úsalo cuando necesites respuestas ancladas en documentación privada, control de factualidad y reducción de costos frente a enviar todo el prompt al LLM.

    ¿Dónde deben vivír las claves de OpenAI / Pinecone?

    Las claves deben residir en el backend (BFF o Edge Functions). Nunca las incluyas en el bundle cliente. El frontend solo envía el JWT del usuario y la Edge Function realiza llamadas a los servicios con las claves seguras.

    ¿Cómo evito fugas de datos entre tenants?

    Usa Row Level Security (RLS) en Supabase o filtros de metadata (por ejemplo userId) en Pinecone y valida JWT en la Edge Function para asegurar que las queries devuelvan solo vectores autorizados.

    ¿Qué tamaño de chunk es recomendado?

    Segmenta entre 200–1000 tokens según el modelo; una recomendación práctica es 200–500 tokens con overlap para preservar contexto y mantener relevancia.

    ¿Qué hacer si la vector DB no devuelve contexto útil?

    Implementa un fallback: responde con “No encontré información” en lugar de llamar al LLM para evitar costos y respuestas potencialmente incorrectas sin contexto.

    ¿Cómo mostrar streaming en Angular?

    Consume el ReadableStream desde fetch, lee chunks con un TextDecoder y actualiza el estado reactivo (Signals) a medida que llegan datos para una experiencia en tiempo real.

    ¿Qué telemetría es esencial?

    Registra latencia de retrieval vs generation, porcentaje de respuestas sin contexto y métricas de coste por llamada al LLM para evaluar trade-offs y optimizaciones.

  • Mejorando la Recuperación de Información con RAG Avanzado

    Mejorando la Recuperación de Información con RAG Avanzado

    RAG Avanzado: Híbrido, Jerárquico y Multi-Vector

    RAG Avanzado: Híbrido, Jerárquico y Multi-Vector. Si eso suena a demasiada ingeniería comparado con “chunk + embeddings”, es porque lo es —y la diferencia entre demo y producto está justo ahí. En producción no vale con que el LLM “suene bien”; necesitas precisión, contexto y control de coste. Este artículo explica qué técnicas añadir, por qué y cuándo.

    Tiempo estimado de lectura: 4 min

    • Ideas clave:
    • La recuperación es el 80% de una buena respuesta: combina sparse (BM25) y dense (embeddings).
    • Arquitectura jerárquica (child + parent) equilibra precisión de fragmento y contexto coherente.
    • Re-ranking con cross-encoders y compresión de contexto reducen hallucinations y coste token.

    Introducción

    La calidad de las respuestas generadas por un sistema RAG depende mayoritariamente de la recuperación. Los vectores aportan semántica; los métodos sparse (BM25) aportan exactitud. En producción necesitas precisión, contexto y control de coste; esto implica añadir capas: híbrido, jerarquía, re-ranking, rewriting y compresión de contexto.

    Resumen rápido (lectores con prisa)

    RAG avanzado combina búsquedas sparse (BM25) y dense (embeddings) para precisión y semántica. Usa indexing jerárquico child→parent para fragmentos precisos con contexto. Re-rank con cross-encoders para reducir ruido. Reescribe y descompone queries; comprime contexto antes del LLM.

    RAG Avanzado: implementación práctica

    1) Hybrid retrieval: BM25 + embeddings

    Problema: buscas “ERR-9921” y el vector devuelve “error de sistema” porque semánticamente es parecido. Solución: híbrido.

    • Sparse: BM25/Elasticsearch para coincidencias literales.
    • Dense: embeddings (OpenAI, Cohere, etc.) para intención.
    • Fusión: Reciprocal Rank Fusion (RRF) o combinación ponderada. Pinecone hybrid search

    Patrón práctico:

    • Ejecuta BM25 y vector search en paralelo.
    • Normaliza scores.
    • Fusiona con RRF.
    • Si BM25 tiene match exacto para tokens sensibles (IDs, SKUs), priorízalo.

    2) Multi-vector / Parent-Child indexación

    Dilema clásico: chunks pequeños = mejor match; chunks grandes = mejor contexto. La arquitectura jerárquica arregla ambos.

    • Indexa embeddings a nivel child (p. ej. 200 tokens).
    • Mantén parent docs grandes (p. ej. 2000 tokens) con metadata.
    • Al recuperar un child relevante, sube el parent completo al contexto.

    Implementación: LangChain ParentDocumentRetriever — LangChain ParentDocumentRetriever

    Beneficio: precisión de fragmento + contexto coherente para razonamiento.

    3) Re-ranking con cross-encoders

    Bi-encoders: rápidos, aproximados. Cross-encoders: lentos, precisos.

    Flujo:

    1. Fast retrieval → top N (50).
    2. Cross-encoder rerank en top N.
    3. Selecciona top K final para el LLM.

    Herramientas: sentence-transformers, Cohere Rerank. Costo: aumenta latencia; recompensa: reduce ruido que provoca hallucinations.

    4) Query rewriting y decomposition

    Muchos fallos vienen por queries ambiguas. No todos los problemas se arreglan en el índice.

    • Query rewrite: un LLM rápido reescribe la consulta con contexto de la sesión.
    • Multi-query: genera 3–5 variantes de la pregunta y busca por cada una.
    • Decomposition: divide preguntas complejas en sub-queries paralelas.

    Técnica HyDE (Hypothetical Document Embeddings) es útil: genera la “respuesta hipotética”, embeddea eso y busca. Idea en práctica: mejora recall sin cambiar el índice.

    5) Context compression antes del LLM

    Enviar 10 documentos de 8k tokens es suicida. Comprime:

    • Filtrado selectivo: extrae párrafos con más evidencia.
    • Summarization condensado (con cuidado: pierde citas).
    • Algoritmos de compresión semántica como LLMLingua

    Objetivo: maximizar densidad informativa dentro de la ventana del LLM.

    Criterio para decidir qué añadir (roadmap pragmático)

    No implementes todo a la vez. Sigue este orden iterativo:

    1. Naive RAG. Mide recall@5 y tasa de hallucination.
    2. Si fallan exact matches → añade Hybrid (BM25).
    3. Si falta contexto coherente → añade Parent-Child multi-vector.
    4. Si llega ruido que confunde al LLM → añade Re-ranking.
    5. Si tokens exceden la ventana → añade Context Compression.

    Mide antes y después. No hay excusas.

    Operacional: latencia, coste y caching

    • Re-ranking y cross-encoders aumentan latencia 10x en la etapa de ranking. Mitiga con caching de top-N por query fingerprint.
    • Hybrid search añade coste infra (Elasticsearch + vector DB). Mide Cost/Accuracy.
    • Batch embeddings nocturnos para contenido estático. Mantén refresh policies.
    • Telemetría: trace request → retrieval steps → rerank → LLM call. LangFuse y LangSmith ayudan a visualizar traces (LangFuse, LangSmith).

    Integración con agentes y workflows (n8n)

    Pipeline ejemplo en n8n:

    1. Node: Query Rewrite (LLM pequeño)
    2. Node: Hybrid Search (ES + Pinecone/Qdrant)
    3. Node: Rerank (Cross-Encoder)
    4. Node: Parent Expander + Context Compressor
    5. Node: LLM final (generation)
    6. Node: Post-check (schema validation / guardrails)

    Versiona prompts, registra fingerprints y alertas en drift.

    Referencias operativas

    Implementar RAG avanzado es menos glamour y más disciplina: medir, añadir la capa correcta y repetir. Hazlo así y tu sistema dejará de improvisar respuestas y empezará a dar respuestas que puedes explicarle al CTO.

    Dominicode Labs

    Si trabajas con pipelines de agentes, workflows o IA aplicada, considera explorar integraciones y experimentos prácticos en Dominicode Labs. Es una continuación lógica para prototipos operacionales y pruebas de telemetría.

    FAQ

     

    Respuesta: ¿Por qué combinar BM25 con embeddings?

    BM25 proporciona coincidencias literales útiles para IDs, SKUs y frases exactas; los embeddings capturan intención y sinónimos. El híbrido reduce falsos positivos semánticos y mejora precisión en búsquedas sensibles.

     

    Respuesta: ¿Qué ventaja tiene la indexación parent-child?

    Permite mantener chunks pequeños para alta precisión en matching mientras se conserva contexto amplio subiendo el documento parent al LLM cuando un child es relevante.

     

    Respuesta: ¿Cuándo usar cross-encoders para re-ranking?

    Úsalos cuando el top-N recuperado contenga ruido que provoca hallucinations o respuestas incorrectas. Son adecuados para reducir falsos positivos aunque aumenten latencia y coste.

     

    Respuesta: ¿Qué es HyDE y por qué usarlo?

    HyDE (Hypothetical Document Embeddings) genera una “respuesta hipotética” desde un LLM, la embeddea y busca con esa representación. Mejora recall sin tocar el índice.

     

    Respuesta: ¿Cómo mitigar la latencia al re-rankear?

    Cachea top-N por huella de consulta, ejecuta re-ranking asíncrono donde sea posible y usa cross-encoders solo en escenarios críticos o por lotes nocturnos para contenido estático.

     

    Respuesta: ¿Qué técnicas de compresión de contexto son seguras?

    Filtrado selectivo de párrafos con evidencia, resúmenes condensados con control de citas y algoritmos semánticos como LLMLingua. Ten cuidado: la compresión puede perder citas y detalles verificables.