Quando comecei o SynaBun, precisei escolher um modelo de embedding. O caminho padrão seria text-embedding-3-small ou ada-002. A OpenAI hospeda os dois. São bons. São fáceis.
Em vez disso, escolhi um modelo de 22MB chamado all-MiniLM-L6-v2. Ele roda na CPU do usuário. Não tem chave de API. Funciona dentro de um avião.
Este post é a versão longa do porquê, de quais são os trade-offs e de como ele se sai na prática.
O que é o all-MiniLM-L6-v2
O all-MiniLM-L6-v2 é um modelo sentence-transformers da biblioteca sentence-transformers, treinado originalmente pelo UKP Lab da TU Darmstadt. Os números que importam:
- Arquitetura: MiniLM de 6 camadas (uma variante destilada do BERT)
- Dimensões do embedding: 384
- Tamanho máximo da sequência: 256 tokens (entradas maiores são truncadas)
- Tamanho do modelo: ~22MB em disco, ~80MB de RAM com o tokenizer + runtime ONNX
- Licença: Apache 2.0
- Velocidade de inferência (CPU): ~12ms por trecho de 256 tokens em um MacBook Air M1
O corpus de treinamento tem mais de 1 bilhão de pares de sentenças de toda a web (Reddit, Quora, S2ORC, NLI, MS MARCO, Yahoo Answers e outros). É um encoder de uso geral, não um específico para código.
Por que rodar local importa para a memória MCP
O Model Context Protocol (MCP) permite que assistentes de programação com IA, como Claude Code, Codex, OpenCode e Gemini, chamem ferramentas externas. A memória é uma das ferramentas mais importantes, porque toda chamada de "lembre disso" ou "recupere o que eu disse sobre X" precisa ir e voltar de algum lugar.
Quando esse lugar é uma API na nuvem, três coisas acontecem:
- Custo em latência. Cada recuperação custa 200-400ms antes mesmo de o modelo ver o resultado.
- Custo financeiro. Embeddings são baratos um a um, mas caros no acumulado. Uma sessão pesada de programação pode chegar a 1.000+ operações de recuperação por dia.
- Custo em privacidade. Seu código, suas notas de projeto, seus rastros de depuração e suas decisões de design vão parar nas mãos de um terceiro.
Para quem usa o Claude Code 8 horas por dia, esses números se acumulam rápido. Só a latência já mata — não dá para manter o ritmo se cada recuperação de memória trava a sessão.
Embeddings locais eliminam os três. O custo é processamento no próprio dispositivo, que em um notebook moderno é praticamente de graça.
A alternativa da OpenAI — e por que ela perde
Aqui vai o confronto direto:
| all-MiniLM-L6-v2 | text-embedding-3-small | ada-002 | |
|---|---|---|---|
| Dimensões | 384 | 1.536 (pode ser reduzido até 512) | 1536 |
| Custo | $0 | US$ 0,02 por 1M de tokens | US$ 0,10 por 1M de tokens |
| Latência | ~12ms (CPU) | 200-400ms (rede) | 200-400ms (rede) |
| Offline | Sim | Não | Não |
| Chave de API | Não tem | Obrigatória | Obrigatória |
| Tamanho do modelo | 22MB | N/A (nuvem) | N/A (nuvem) |
| Licença | Apache 2.0 | Proprietária | Proprietária |
Para memória de desenvolvedor, o único argumento real a favor da OpenAI é a qualidade da recuperação. Então fiz um benchmark.
Como montei o benchmark
Montei um corpus de 12.000 memórias do SynaBun tiradas das minhas próprias sessões de desenvolvimento ao longo de 8 meses. Cada memória tem em média 280 caracteres e inclui coisas como:
- Descrições de correções de bugs
- Decisões de arquitetura
- Peculiaridades de APIs
- Padrões de código
- Resumos de sessões
- Notas por arquivo
Depois escrevi 200 consultas de recuperação para esse corpus. Cada consulta tem uma resposta correta conhecida no corpus (rotulada manualmente). Medi o recall@5 — a memória correta aparece entre os 5 primeiros 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 | Latência p50 | Custo 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 |
Três conclusões:
- O modelo v3 da OpenAI é realmente melhor — 5 pontos no recall@5, 7 pontos no recall@1.
- Essa diferença de 5 pontos custa 240ms de latência, 4 centavos de dólar a cada 1.000 consultas e uma conexão com a internet.
bge-small-en-v1.5egte-smallsão um pouco melhores que oall-MiniLM-L6-v2, mas exigem 130MB e 70MB, respectivamente. Para um pacote total de 5MB no servidor MCP em Node, o MiniLM vence.
Por que 384 dimensões bastam
O senso comum diz que "mais dimensões = melhor recuperação". Isso deixa de valer a partir de certo tamanho de corpus, e sempre cobra seu preço em memória + armazenamento + processamento.
Para um usuário típico do SynaBun, com menos de 100.000 memórias:
- 384 dimensões × 4 bytes × 100k memórias = 150MB de vetores
- 1.536 dimensões × 4 bytes × 100k memórias = 600MB de vetores
A diferença de 4x no armazenamento pesa quando o SQLite está varrendo a tabela inteira para calcular a similaridade de cosseno (mais sobre isso abaixo). Para corpora com menos de 1M de itens, usar 384 dimensões com similaridade de cosseno em um índice B-tree é mais rápido de ponta a ponta do que usar 1.536 dimensões em HNSW.
Armazenamento — SQLite puro em vez de um banco vetorial
O SynaBun guarda tudo em um único arquivo SQLite, usando o node:sqlite nativo do Node. Nada de Pinecone. Nada de Weaviate. Nada de Qdrant. Nenhuma extensão para compilar. Só um arquivo SQLite em ~/.synabun/mcp-data/memory.db.
Por quê:
- Zero infra. O usuário não precisa rodar nenhum serviço.
- Backup fácil. Copie o arquivo. Pronto.
- Dá para fazer diff. O SQLite é um banco de dados de verdade. Você pode rodar um
.dumpnele. - Dá para embutir. O servidor MCP em Node abre o arquivo nativamente.
O núcleo do schema é este (resumido):
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'
);
Os vetores ficam em uma coluna BLOB comum, e a similaridade de cosseno é calculada no processo do servidor. A partir do SynaBun 2.0.0 (Memory v2), a recuperação é híbrida por padrão: um canal de palavras-chave com FTS5, um canal de vetores de documento e uma passada de refinamento por trechos sobre os melhores candidatos, combinados com reciprocal rank fusion:
// each channel contributes by rank, not by raw score
score += 1 / (60 + rank);
Com a fusão de rankings, um identificador exato encontrado pela busca por palavra-chave e uma paráfrase encontrada pelo embedding aparecem juntos, sem que você precise colocar BM25 e cosseno na mesma escala. Se o modelo de embedding estiver ausente ou falhar, a recuperação por palavra-chave continua funcionando.
O reforço de recência
Um ranking puramente por relevância tem um grande ponto fraco quando se trata de memória para desenvolvedores: a decisão de ontem e uma decisão de 6 meses atrás parecem igualmente relevantes. O SynaBun adiciona um peso opcional de recência com meia-vida de 14 dias:
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)
Você liga ou desliga por consulta com recency_boost, e o padrão é configurável. Na recuperação híbrida, o reforço sobe uma memória recém-criada em até 15% e vai diminuindo conforme a meia-vida. O caminho antigo, só com vetores, mistura 55% de recência com 35% de similaridade, mais um pequeno bônus por número de acessos.
Limitações do all-MiniLM-L6-v2
Não é mágica. Três coisas para saber:
- Máximo de 256 tokens. O que passar disso é truncado. Ao salvar, o SynaBun divide memórias longas em trechos conferidos pelo tokenizer e mantém um vetor de documento agregado para a memória inteira.
- Focado em inglês. A recuperação em português, espanhol ou mandarim é visivelmente pior. Para quem trabalha em vários idiomas, o upgrade é o
paraphrase-multilingual-MiniLM-L12-v2(118MB). - Sem treinamento específico para código. Para busca pura em código (por exemplo, "encontre a função que faz X"), um encoder específico para código, como o
unixcoder-base, se sai melhor. Mas as memórias do SynaBun costumam ser anotações em inglês sobre código, não o código em si, então o encoder de uso geral funciona melhor na prática.
Quando vale usar a OpenAI
Não sou dogmático quanto a isso. Use embeddings na nuvem se:
- Seu corpus tem mais de 1 milhão de itens e a qualidade da recuperação é o seu gargalo.
- Você roda em uma infraestrutura que não consegue hospedar um modelo de 22MB (raro).
- Você precisa de recuperação entre idiomas e não quer distribuir o modelo multilíngue, que é maior.
Para 95% dos casos de uso de memória via MCP — um único desenvolvedor com alguns milhares de anotações de sessão — rodar localmente é a resposta certa.
Como usar no seu próprio servidor MCP
Se você está construindo seu próprio servidor MCP e quer embeddings locais, o caminho é curto:
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);
}
A integração é só isso. O modelo é baixado no primeiro uso (uma única vez, com cache em ~/.cache/transformers) e, daí em diante, toda chamada de embedding é local e rápida.
Para fechar
O padrão cloud-first das ferramentas de IA deixa de aproveitar vantagens reais. Um modelo de 22MB, que roda em qualquer notebook fabricado nos últimos 5 anos, entrega 95% da qualidade de recuperação com 0% do custo de latência e 0% do custo financeiro. Para memória de desenvolvedor em particular — em que o corpus é pequeno, as consultas são frequentes e a privacidade importa — o processamento local vence.
Se quiser ver isso funcionando, o SynaBun é open source sob a licença Apache 2.0. Todo o pipeline de memória está em mcp-server/src/memory/. Desmonte, faça um fork, publique o seu.
Leia também: