Cuando empecé SynaBun tuve que elegir un modelo de embedding. Lo habitual habría sido text-embedding-3-small o ada-002. Los aloja OpenAI. Son buenos. Son fáciles.
En su lugar elegí un modelo de 22MB llamado all-MiniLM-L6-v2. Se ejecuta en la CPU del usuario. No necesita clave de API. Funciona en un avión.
Este post es la versión larga del porqué, de lo que se gana y se pierde, y de cómo rinde en la práctica.
Qué es all-MiniLM-L6-v2
all-MiniLM-L6-v2 es un modelo sentence-transformers de la biblioteca sentence-transformers, entrenado originalmente por el UKP Lab de la TU Darmstadt. Las cifras que importan:
- Arquitectura: MiniLM de 6 capas (una variante destilada de BERT)
- Dimensiones del embedding: 384
- Longitud máxima de secuencia: 256 tokens (las entradas más largas se truncan)
- Tamaño del modelo: ~22MB en disco, ~80MB en RAM con el tokenizador y el runtime de ONNX
- Licencia: Apache 2.0
- Velocidad de inferencia (CPU): ~12ms por fragmento de 256 tokens en un MacBook Air M1
El corpus de entrenamiento reúne más de 1 millardo (mil millones) de pares de oraciones de toda la web (Reddit, Quora, S2ORC, NLI, MS MARCO, Yahoo Answers y más). Es un codificador de propósito general, no uno específico para código.
Por qué importa lo local en la memoria MCP
El Model Context Protocol (MCP) permite que asistentes de programación con IA como Claude Code, Codex, OpenCode y Gemini llamen a herramientas externas. La memoria es una de las herramientas más importantes, porque cada llamada de «recuerda esto» o «recupera lo que dije sobre X» tiene que ir a algún lugar y volver.
Cuando ese lugar es una API en la nube, pasan tres cosas:
- Peaje de latencia. Cada recuperación cuesta 200-400ms antes de que el modelo llegue siquiera a ver el resultado.
- Peaje de costo. Los embeddings son baratos de uno en uno, pero caros en conjunto. Una sesión intensa de programación puede superar las 1.000 operaciones de recuperación al día.
- Peaje de privacidad. Tu código, las notas del proyecto, las trazas de depuración y las decisiones de diseño se envían a un tercero.
Para un desarrollador que usa Claude Code 8 horas al día, esas cifras se acumulan rápido. La latencia por sí sola ya lo arruina todo: no puedes mantener la concentración si cada recuperación de memoria frena la sesión.
Los embeddings locales eliminan los tres. El costo es el cómputo en el dispositivo, que en un portátil moderno es prácticamente gratis.
La alternativa de OpenAI y por qué pierde
Esta es la comparación directa:
| all-MiniLM-L6-v2 | text-embedding-3-small | ada-002 | |
|---|---|---|---|
| Dimensiones | 384 | 1.536 (reducible hasta 512) | 1536 |
| Costo | $0 | $0,02 por 1M de tokens | $0,10 por 1M de tokens |
| Latencia | ~12ms (CPU) | 200-400ms (red) | 200-400ms (red) |
| Sin conexión | Sí | No | No |
| Clave de API | No tiene | Obligatoria | Obligatoria |
| Tamaño del modelo | 22MB | No aplica (nube) | No aplica (nube) |
| Licencia | Apache 2.0 | Propietaria | Propietaria |
Para la memoria de un desarrollador, el único argumento real a favor de OpenAI es la calidad de la recuperación. Así que hice un benchmark.
Cómo monté el benchmark
Armé un corpus de 12.000 memorias de SynaBun a partir de mis propias sesiones de desarrollo a lo largo de 8 meses. Cada memoria tiene 280 caracteres en promedio e incluye cosas como:
- Descripciones de correcciones de errores
- Decisiones de arquitectura
- Peculiaridades de las API
- Patrones de código
- Resúmenes de sesión
- Notas a nivel de archivo
Después escribí 200 consultas de recuperación contra ese corpus. Cada consulta tiene en el corpus una respuesta correcta conocida (etiquetada a mano). Medí recall@5: ¿aparece la memoria correcta entre los 5 primeros resultados?
def evaluate(model, corpus, queries):
embeddings = model.encode([m.content for m in corpus])
correct = 0
for query, expected_id in queries:
q_emb = model.encode(query)
scores = cosine_similarity(q_emb, embeddings)
top5 = scores.argsort()[-5:][::-1]
if expected_id in [corpus[i].id for i in top5]:
correct += 1
return correct / len(queries)
Resultados
| Modelo | Recall@5 | Recall@1 | Latencia p50 | Costo por 1k consultas |
|---|---|---|---|---|
| all-MiniLM-L6-v2 (CPU) | 78% | 54% | 12ms | $0.00 |
| text-embedding-3-small | 83% | 61% | 240ms | $0.04 |
| ada-002 | 75% | 51% | 280ms | $0.20 |
| bge-small-en-v1.5 (CPU) | 79% | 56% | 14ms | $0.00 |
| gte-small (CPU) | 80% | 57% | 13ms | $0.00 |
Tres conclusiones:
- El modelo v3 de OpenAI es realmente mejor: 5 puntos en recall@5 y 7 puntos en recall@1.
- Esa diferencia de 5 puntos te cuesta 240ms de latencia, 4 centavos por cada 1.000 consultas y una conexión a internet.
bge-small-en-v1.5ygte-smallson un poco mejores queall-MiniLM-L6-v2, pero requieren 130MB y 70MB respectivamente. Para un paquete de 5MB en total dentro del servidor MCP en Node, gana MiniLM.
Por qué bastan 384 dimensiones
La creencia habitual es que «más dimensiones = mejor recuperación». Eso deja de ser cierto a partir de cierto tamaño de corpus, y siempre se paga en memoria, almacenamiento y cómputo.
Para un usuario típico de SynaBun con menos de 100.000 memorias:
- 384 dimensiones × 4 bytes × 100k memorias = 150MB de vectores
- 1.536 dimensiones × 4 bytes × 100k memorias = 600MB de vectores
La diferencia de 4x en almacenamiento pesa cuando SQLite recorre la tabla completa para calcular la similitud coseno (más sobre eso abajo). En corpus de menos de 1M de elementos, 384 dimensiones con coseno sobre un índice B-tree resultan más rápidas de extremo a extremo que 1.536 dimensiones sobre HNSW.
Almacenamiento: SQLite a secas en lugar de una base de datos vectorial
SynaBun guarda todo en un único archivo SQLite mediante node:sqlite, el módulo integrado de Node. Sin Pinecone. Sin Weaviate. Sin Qdrant. Sin extensiones que compilar. Solo un archivo SQLite en ~/.synabun/mcp-data/memory.db.
Por qué:
- Cero infraestructura. El usuario no tiene que ejecutar ningún servicio.
- Fácil de respaldar. Copias el archivo. Listo.
- Admite diff. SQLite es una base de datos de verdad. Puedes hacerle un
.dump. - Integrable. El servidor MCP de Node lo abre de forma nativa.
El núcleo del esquema se ve así (recortado):
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
vector BLOB NOT NULL, -- 384 floats packed as 1536 bytes
content TEXT NOT NULL,
category TEXT NOT NULL,
project TEXT NOT NULL,
tags TEXT NOT NULL DEFAULT '[]', -- JSON array
importance INTEGER NOT NULL DEFAULT 5,
created_at TEXT NOT NULL,
accessed_at TEXT NOT NULL,
access_count INTEGER NOT NULL DEFAULT 0
-- plus subcategory, source, related files, checksums, trash state
);
CREATE VIRTUAL TABLE memory_search USING fts5(
id UNINDEXED, content, category, project, tags, files,
tokenize='unicode61 remove_diacritics 2'
);
Los vectores viven en una columna BLOB común y la similitud coseno se calcula en el proceso del servidor. Desde SynaBun 2.0.0 (Memory v2), la recuperación es híbrida por defecto: un canal de palabras clave con FTS5, un canal de vectores de documento y una pasada de refinamiento por pasajes sobre los mejores candidatos, todo combinado con fusión de rangos recíprocos:
// each channel contributes by rank, not by raw score
score += 1 / (60 + rank);
Gracias a la fusión de rankings, aparecen tanto un identificador exacto encontrado por la búsqueda de palabras clave como una paráfrasis encontrada por el embedding, sin tener que poner BM25 y coseno en la misma escala. Si el modelo de embedding falta o falla, la recuperación por palabras clave sigue funcionando.
La prioridad a las memorias recientes
Un ranking de pura relevancia tiene un gran punto débil para la memoria de un desarrollador: la decisión de ayer y una de hace 6 meses parecen igual de relevantes. SynaBun añade un peso opcional por recencia con una vida media de 14 días:
def score(fused_score, memory, now):
age_days = (now - memory.created_at) / 86400
recency = 0.5 ** (age_days / 14) # half-life of 14 days
return fused_score * (1 + 0.15 * recency)
Se activa o desactiva en cada consulta con recency_boost, y el valor por defecto es configurable. Con la recuperación híbrida, el impulso eleva una memoria recién creada hasta un 15% y se desvanece según la vida media. La ruta anterior, solo vectorial, combina un 55% de recencia con un 35% de similitud, más un pequeño bono por número de accesos.
Limitaciones de all-MiniLM-L6-v2
No es magia. Hay tres cosas que debes saber:
- Máximo de 256 tokens. Todo lo que lo supere se trunca. Al guardar, SynaBun divide las memorias largas en pasajes verificados con el tokenizador y conserva un vector de documento agregado para la memoria completa.
- Muy centrado en el inglés. La recuperación en portugués, español o mandarín es notablemente peor. Para usuarios multilingües, la mejora es
paraphrase-multilingual-MiniLM-L12-v2(118MB). - Sin entrenamiento específico para código. Para búsqueda pura de código (por ejemplo, "encuentra la función que hace X"), un codificador específico para código como
unixcoder-baselo supera. Pero las memorias de SynaBun suelen ser notas en inglés sobre código, no código en sí, así que el codificador de propósito general funciona mejor en la práctica.
Cuándo conviene usar OpenAI
No soy dogmático con esto. Usa embeddings en la nube si:
- Tu corpus supera 1 millón de elementos y la calidad de la recuperación es tu cuello de botella.
- Trabajas sobre una infraestructura que no puede alojar un modelo de 22MB (algo poco común).
- Necesitas recuperación entre idiomas y no quieres distribuir el modelo multilingüe, que es más grande.
Para el 95% de los casos de uso de memoria con MCP (un solo desarrollador con unos pocos miles de notas de sesión), lo local es la respuesta correcta.
Cómo usarlo en tu propio servidor MCP
Si estás creando tu propio servidor MCP y quieres embeddings locales, el camino es corto:
import { pipeline } from '@xenova/transformers';
const embedder = await pipeline(
'feature-extraction',
'Xenova/all-MiniLM-L6-v2',
{ quantized: false }
);
async function embed(text) {
const output = await embedder(text, {
pooling: 'mean',
normalize: true
});
return Array.from(output.data);
}
Esa es toda la integración. El modelo se descarga en el primer uso (una sola vez, y queda en caché en ~/.cache/transformers); a partir de ahí, cada llamada de embedding es local y rápida.
Cierre
Al apostar por la nube por defecto, las herramientas de IA están desaprovechando valor real. Un modelo de 22MB que corre en cualquier equipo portátil fabricado en los últimos 5 años te da el 95% de la calidad de recuperación con un 0% del costo en latencia y un 0% del gasto. En la memoria para desarrolladores en particular, donde el corpus es pequeño, las consultas son frecuentes y la privacidad importa, lo local gana.
Si quieres verlo en acción, SynaBun es open source bajo Apache 2.0. Todo el pipeline de memoria vive en mcp-server/src/memory/. Desármalo, haz un fork y lanza el tuyo.
Lecturas relacionadas: