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:

  1. Custo em latência. Cada recuperação custa 200-400ms antes mesmo de o modelo ver o resultado.
  2. 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.
  3. 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-v2text-embedding-3-smallada-002
Dimensões3841.536 (pode ser reduzido até 512)1536
Custo$0US$ 0,02 por 1M de tokensUS$ 0,10 por 1M de tokens
Latência~12ms (CPU)200-400ms (rede)200-400ms (rede)
OfflineSimNãoNão
Chave de APINão temObrigatóriaObrigatória
Tamanho do modelo22MBN/A (nuvem)N/A (nuvem)
LicençaApache 2.0ProprietáriaProprietá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

ModeloRecall@5Recall@1Latência p50Custo por 1k consultas
all-MiniLM-L6-v2 (CPU)78%54%12ms$0.00
text-embedding-3-small83%61%240ms$0.04
ada-00275%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:

  1. O modelo v3 da OpenAI é realmente melhor — 5 pontos no recall@5, 7 pontos no recall@1.
  2. 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.
  3. bge-small-en-v1.5 e gte-small são um pouco melhores que o all-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 .dump nele.
  • 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:

  1. 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.
  2. 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).
  3. 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: