#Tres significados que conviene separar
El historial registra lo sucedido. La memoria de trabajo conserva lo relevante para la tarea actual. La memoria de largo plazo mantiene información útil entre tareas: convenciones de un proyecto, preferencias consentidas o decisiones validadas. RAG significa recuperar conocimiento e incorporarlo a la entrada antes de generar una respuesta; puede consultar memoria o documentos externos.
Ninguna de estas piezas garantiza verdad. Si guardas una conclusión equivocada como regla permanente, multiplicas el error. Una entrada de memoria necesita ámbito, procedencia, fecha o versión, estado de validación y una forma de corregirse.
{
"scope": "project:facturacion",
"key": "money_unit",
"value": "integer_cents",
"source": "schema.sql@sha256:...",
"status": "verified",
"supersedes": null
}
La referencia abreviada ilustra el campo; una implementación debe guardar el hash completo o una versión resoluble. La regla pertenece a facturación, no a todos los proyectos del usuario.
#RAG empieza antes de generar
El recorrido completo tiene varias etapas: incorporar documentos, dividirlos si hace falta, indexarlos, recuperar candidatos, filtrar permisos, seleccionar fragmentos y generar con citas. Si falla una etapa temprana, cambiar el prompt final puede no arreglar nada.
Para diez documentos, empieza con búsqueda textual. Es fácil de inspeccionar y responde bien a nombres exactos, códigos de error y funciones. Una base vectorial es una opción posterior, no un requisito para llamar RAG al mecanismo.
#Una recuperación pequeña que puedes entender
Este ejemplo completo usa re, el módulo de expresiones regulares de Python, para dividir texto en palabras. Cuenta coincidencias distintas y devuelve solo documentos autorizados. No implementa BM25 ni búsqueda semántica; su utilidad es hacer visibles ranking y filtrado.
import re
DOCUMENTOS = [
{"id": "d1", "tenant": "a", "title": "Backups",
"text": "El backup de PostgreSQL se verifica mediante una restauración."},
{"id": "d2", "tenant": "a", "title": "Cache",
"text": "Redis guarda respuestas temporales con caducidad."},
{"id": "d3", "tenant": "b", "title": "Backups privados",
"text": "Backup PostgreSQL del cliente b."},
]
def terms(text):
return set(re.findall(r"\w+", text.casefold()))
def buscar(query, tenant, limit=3):
wanted = terms(query)
ranked = []
for doc in DOCUMENTOS:
if doc["tenant"] != tenant:
continue
score = len(wanted & terms(doc["title"] + " " + doc["text"]))
if score:
ranked.append((score, doc["id"], doc))
ranked.sort(key=lambda item: (-item[0], item[1]))
return [doc for _, _, doc in ranked[:limit]]
results = buscar("backup PostgreSQL", tenant="a")
assert [doc["id"] for doc in results] == ["d1"]
print(results[0]["text"])
& calcula la intersección entre conjuntos. casefold normaliza mayúsculas. No elimina tildes ni entiende que «copia de seguridad» y «backup» son similares. Al aparecer ese fallo puedes añadir sinónimos, un motor textual mejor o embeddings, y comparar cuál lo resuelve con menor complejidad.
El filtrado ocurre antes de exponer títulos y textos. Recuperar primero de todos los tenants y pedir al modelo que ignore los no autorizados ya ha filtrado información.
#Dividir documentos sin destruir su significado
Un chunk es un fragmento indexado. Si es enorme, ocupa demasiada entrada; si es diminuto, pierde contexto. No hay un tamaño universal. Un manual puede dividirse por secciones; una API por endpoint; código por función o clase, conservando firmas e importaciones relevantes.
Cada fragmento debería guardar document_id, versión, título o ruta de sección, rango y texto. Un pequeño solapamiento puede evitar perder una frase entre fragmentos, pero también duplica evidencia. Deduplica al construir contexto y no confundas tres fragmentos del mismo documento con tres fuentes independientes.
Cuando una tabla relaciona un valor con una cabecera, cortar solo la fila puede cambiar su significado. Cuando el texto dice «esta opción», necesita el antecedente. Evalúa fragmentos leyéndolos aislados: si tú no puedes interpretarlos, el modelo tampoco tiene una base fiable.
#Qué añade un embedding
Un embedding convierte texto en un vector de números. La cercanía entre vectores puede aproximar similitud de significado según el modelo utilizado. Permite recuperar «copia de seguridad» para una consulta «backup», pero también puede dar una coincidencia semánticamente parecida que pertenece a otro producto o versión.
El pipeline de ingesta calcula y guarda un vector por fragmento. En consulta calcula el vector de la pregunta, busca vecinos y recupera sus textos. Los vectores deben pertenecer al mismo modelo y configuración; cambiar el modelo normalmente exige reindexar. Conserva esa versión en el índice.
La similitud coseno compara la dirección de dos vectores. No es probabilidad de verdad ni confianza de la respuesta. Un score 0,82 no significa «82 % correcto». Umbrales y número de candidatos se ajustan con ejemplos relevantes para tu corpus.
La búsqueda híbrida combina coincidencias textuales y semánticas. Un reranker vuelve a ordenar un conjunto pequeño de candidatos con una evaluación más costosa. Añade estas capas cuando puedas mostrar un fallo de recuperación que resuelven; de lo contrario solo añaden latencia y nuevas formas de equivocarse.
#Citar no es decorar una respuesta
Una cita debe apuntar a un fragmento que respalda la afirmación concreta. El modelo puede inventar una referencia con formato convincente. El verificador puede comprobar que el identificador pertenece a la evidencia entregada y que la versión existe; verificar que respalda semánticamente la frase requiere reglas de dominio, revisión o una evaluación adicional.
Cuando no hay evidencia suficiente, permite abstenerse. Obligar a producir una respuesta completa aumenta las invenciones. Una respuesta útil puede ser: «El manual explica la frecuencia del backup, pero no el procedimiento de restauración; falta ese documento».
#Evaluar recuperación por separado
Prepara preguntas y documentos relevantes conocidos. Recall@k mide qué proporción de los documentos relevantes aparece entre los primeros k; precision@k, qué proporción de esos candidatos resulta relevante. Si una pregunta tiene dos fuentes necesarias y recuperas una, el generador puede estar condenado aunque escriba muy bien.
Después mide si la evidencia llegó al contexto y si la respuesta la usa correctamente. Un experimento útil compara búsqueda textual y semántica con las mismas preguntas, permisos y presupuesto de entrada. No compares solo ejemplos bonitos elegidos después de ver resultados.
#Cuándo escribir memoria
No conviertas automáticamente cada frase del modelo en memoria. Una preferencia explícita del usuario puede persistirse con su ámbito. Una inferencia como «este proyecto parece usar UTC» debe quedar como hipótesis hasta comprobarla. Si el código cambia, esa memoria puede caducar o quedar invalidada por la nueva versión.
Mantén una política de sustitución: al actualizar una regla, conserva qué reemplaza, evita dos valores activos contradictorios y permite borrar información personal. La memoria es un dato de producto con mantenimiento, no un almacén infinito de texto.
#Referencias
- re, biblioteca estándar de Python: tokenización sencilla del ejemplo.
- SQLite FTS5: una evolución posible hacia índices de texto completo y ranking sin base vectorial.
- Retrieval-Augmented Generation, artículo original: fundamento del patrón de recuperación y generación; el ejemplo usa recuperación textual propia.