Testei 8 LLMs locais pra extrair memória das minhas sessões de agente. O modelo de 16GB ganhou do 70B - e um schema JSON ganhou de todos
Medi 8 LLMs locais nas mesmas 6 sessões pra extrair memória de agente. Um schema JSON ganhou de trocar de modelo, o llama3.3:70b deu OOM, e o vencedor foi um denso de 16GB que roda no M4 Pro.
9 min

TL;DR
Eu mantenho uma camada de memória self-hosted pros meus agentes de código. Um dos jobs usa um LLM local pra ler cada sessão encerrada e extrair “fatos” duráveis - decisões, preferências, configs e procedimentos passo a passo - pra próxima sessão já começar sabendo. As sessões têm dado pessoal e financeiro, então a extração precisa rodar na minha máquina. A pergunta era: qual modelo local?
Parei de chutar e medi 8 deles nas mesmas 6 sessões, mesmo prompt, num M4 Pro / 48GB. Quatro coisas me surpreenderam:
- O schema de saída importou mais que o modelo. O
format:"json"cru do Ollama fazia vários modelos retornarem um único fato em vez de uma lista - 1 fato por sessão. Forçar um schema de structured output levantou todos os modelos, inclusive o incumbente, de 1 pra 31 fatos nas mesmas 6 sessões. O maior ganho do exercício inteiro, e é uma linha de config. - O maior modelo nunca terminou. O
llama3.3:70b(34GB) deu OOM na terceira sessão. Um modelo denso de 16GB ganhou dele em qualidade e na capacidade de simplesmente rodar. - Mais extrações ≠ melhor. Um modelo produziu 120 fatos (4× o vencedor) - e eram fragmentos lixo. Uma métrica ingênua de “mais é melhor” teria coroado o pior modelo.
- O vencedor foi o mais barato que coube com folga na RAM.
qwen3.5:27b-int4, 16GB - menor que o modelo que ele substituiu.
Contexto: por que um modelo local
O projeto é o agent-memory-hub - um store self-hosted (Postgres + pgvector) que captura as minhas sessões de agente e injeta a fatia relevante de volta nas próximas. A camada de fatos é a parte interessante: quando uma sessão encerra, um LLM lê o transcript e emite afirmações tipadas e duráveis:
decision- “não sacar a previdência pra quitar o consignado”preference- “só investir depois da reserva de emergência cheia”config- “a previdência é PGBL na tabela progressiva”fact- “o consignado corre jul/26-mai/27 a ~2,49% a.m.”procedure- “pra fechar aperto de caixa: renegociar prazo com fornecedor, depois liquidar cripto/dólar, depois vender bolsa BR”
Esses transcripts estão cheios de detalhe financeiro e pessoal. Mandar pra uma API hospedada era inviável, então a extração roda num modelo local via Ollama. É essa a razão de o benchmark existir: eu não posso escolher “usa o melhor modelo da nuvem”. Eu preciso achar o melhor modelo que roda no hardware que eu tenho.
Método (pequeno de propósito, reproduzível)
- Corpus: 6 sessões reais (uma thread de planejamento financeiro - densa, cheia de procedimento - que é o caso mais difícil pro extrator).
- Mesmo prompt do extrator de produção, mesmo schema de structured output depois que achei o fix (mais abaixo).
- Máquina: MacBook M4 Pro, 48GB de memória unificada. Empiricamente ela apela pro swap bem antes de os 48GB nominais encherem, e isso acaba pesando muito nos modelos maiores.
- Sinais registrados por sessão: número de fatos, número de fatos
procedureespecificamente, se o JSON parseou, segundos de wall-clock - além de eu ter lido a saída de cada modelo. Contagem é proxy; a leitura manual é o desempate.
Isso não é MMLU. É um benchmark tosco e específico da tarefa, nos meus dados - que é exatamente o ponto. Qual o “melhor modelo” pra ler as minhas sessões de agente não é uma pergunta de leaderboard.
Como a rodada foi de verdade
A primeira passada quase me levou pro caminho errado. Todo modelo, menos o Qwen 27B denso, retornou exatamente um fato por sessão - Gemma, Mistral, até o meu MoE incumbente. A mesma sessão financial, rodada no Gemma 3 27B: um fato trivial, e só. Minha leitura honesta no primeiro momento foi “esses modelos são fracos demais pra extração”.
Aí caiu a ficha: não eram os modelos. O format:"json" do Ollama deixa o modelo devolver um objeto JSON quando o prompt pede um array - e Gemma e Mistral fizeram exatamente isso. O meu extrator de produção usava esse mesmo format:"json", ou seja, o primeiro eval era caladamente injusto: premiava o único modelo que por acaso cuspia arrays e punia o resto por uma mania de formatação. O fix não era um modelo mais forte - era um schema. Um schema de structured output que força {facts:[...]} dá a mesma chance pra todo modelo (e é uma melhoria real, então foi direto pra produção).
Rodando de novo com o schema justo, o benchmark começou a se matar sozinho. O processo do eval dava OOM atrás de OOM. Dois erros meus: eu carregava cada modelo novo por cima do anterior ainda residente, e todo modelo rodava com o contexto padrão do Ollama - que infla o KV-cache pra perto de 32k quando a extração precisa de ~4k (o extrator trunca a entrada em ~12k chars). Numa máquina de 48GB isso é crash garantido. Reescrevi o harness com três blindagens: num_ctx=8192, descarregar o modelo anterior antes de carregar o próximo, e checkpoint por modelo pra um OOM no modelo 7 não custar os resultados dos modelos 1-6. Só então a rodada terminou de ponta a ponta.
E com o schema justo, o ranking virou duas vezes. O MoE baseline “fraco” pulou pra 31 fatos / 9 procedimentos - nunca foi fraco, só estava mal medido. Aí a contagem crua mentiu na direção oposta: o command-r explodiu pra 120 fatos, uma sessão sozinha produzindo 68. Isso não é um modelo lendo bem; é um modelo picando o transcript. Foi aí que parei de confiar em contagem e comecei a ler a saída. Os achados abaixo são o que sobreviveu a isso.
Achado 1 - o schema ganhou de todo modelo
Essa primeira surpresa é o resultado mais importante daqui, então vale enunciar como regra, não como causo. O fix que a injustiça exigiu - um schema de structured output - é este:
FACTS_SCHEMA = {
"type": "object",
"properties": {
"facts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": {"type": "string",
"enum": ["preference", "decision", "config", "fact", "procedure"]},
"text": {"type": "string"},
},
"required": ["kind", "text"],
},
}
},
"required": ["facts"],
}
body = {"model": model, "prompt": prompt, "format": FACTS_SCHEMA,
"stream": False, "options": {"temperature": 0.2, "num_ctx": 8192}}
Mesmos modelos, mesmo prompt, agora obrigados a {facts:[...]} com um enum de kind. O MoE incumbente foi de 1 → 31 fatos nas mesmas 6 sessões. Todo modelo melhorou. O enum ainda matou de graça quase todo o ruído de “categoria errada”.
A lição vale além deste projeto: quando um modelo local “não consegue fazer extração estruturada”, cheque o seu contrato de saída antes de trocar de modelo. O gargalo era o harness, não os pesos.
Achado 2 - o maior modelo nunca terminou
A sabedoria convencional manda jogar o maior modelo possível num problema tipo-raciocínio. Então tentei o llama3.3:70b-q3 (34GB em disco).
Deu OOM na terceira sessão e não se recuperou. Numa máquina de 48GB que já apela pro swap bem antes de os 48GB estarem cheios, um modelo de 34GB mais o KV-cache simplesmente não tem espaço pra trabalhar. As duas sessões que ele completou eram medíocres de qualquer jeito (5 e 3 fatos, uma com zero procedimentos).
O gemma3:27b-it-q8_0 (29GB) fez o mesmo - OOM na sessão 6.
Enquanto isso um modelo denso de 16GB rodou as seis com folga e produziu a melhor saída. Nessa tarefa, “maior” perdeu duas vezes: não coube, e onde rodou, não foi melhor.
Achado 3 - o KV-cache era o verdadeiro comilão de memória
Aqui está a parte mais sutil da história do OOM. Os pesos do modelo não eram o problema todo - o KV-cache era. A janela de contexto padrão do Ollama (até 32k) infla o cache de forma absurda, e a extração só precisa de ~4k tokens de entrada.
Fixar num_ctx=8192 foi a diferença entre “modelos classe-27B crasham no meio” e “modelos classe-27B são estáveis”. Ou seja, parte do “o modelo grande não cabe” era na verdade um bug de configuração meu. Vale checar antes de concluir que o teu hardware é pequeno demais: uma janela de contexto do tamanho certo pode mover um modelo de OOM pra confortável.
Achado 4 - contagem é armadilha; qualidade é a métrica
O command-r:35b produziu 120 fatos - quase 4× o vencedor final. Se eu tivesse ranqueado por contagem crua, ele teria ganhado de goleada.
Aí eu li:
- “Seu problema é de fluxo.”
- “Cartão virou crédito, não meio de pagamento.”
- “Sacar a previdência custa ~R$ 6k a mais.”
Isso não é memória durável. É o transcript picado em frases de motivação - over-extraction que poluiria o recall em vez de ajudar. Uma sessão sozinha produziu 68 deles. A contagem subiu; o sinal desceu.
O erro inverso também apareceu. O gemma3:27b-it-qat produziu 38 fatos saudáveis - empatado com o vencedor em volume - mas só 1 procedimento nas 6 sessões inteiras. Pra um sistema cujo propósito é justamente transformar procedimentos em skills reutilizáveis do agente, um modelo que não enxerga procedimento é inútil, não importa quantos fact ele emita. Se você se importa com um subtipo específico de extração, meça esse subtipo, não o agregado.
Achado 5 - o vencedor
O qwen3.5:27b-int4 (denso, 16GB) levou:
- 38 fatos, 10 procedimentos nas 6 sessões - o maior número de procedimentos de todos.
- Afirmações self-contained (legíveis sem o transcript), idioma correto, bem categorizadas.
- 16GB de footprint - menor que o MoE de 23GB que ele substituiu. Não só ganhou em qualidade; aliviou a pressão de memória em vez de aumentar.
O tradeoff honesto: ele é mais lento. O MoE (3B de parâmetros ativos) fez média de ~16s/sessão; o denso 27B, ~60s. Num caminho interativo isso pesaria. Mas isso roda como batch noturno sobre sessões já encerradas, então troquei de bom grado ~44 segundos por sessão por fatos melhores e mais procedimentos. A variante int8 do mesmo modelo era marginalmente diferente em qualidade, ~86s/sessão, e 29GB em disco - bem no teto. Não compensa.
A tabela completa
Todos os tamanhos abaixo são o tamanho exato em disco pelo ollama list (o que o modelo ocupa de fato no SSD). A RAM em runtime é maior - pesos mais o KV-cache - que é justamente por que o teto pesa (ver Achado 3).
| Modelo | Fatos (6 sess) | Procedimentos | Tamanho (disco) | Notas |
|---|---|---|---|---|
| qwen3.5:27b-int4 (denso) | 38 | 10 | 16GB | Escolhido. Melhor qualidade, mais procedimentos, menor footprint |
| qwen3.5:35b-a3b MoE (incumbente) | 31 | 9 | 23GB | Competitivo após o schema; o mais rápido (~16s/sess) |
| gemma3:27b-it-qat | 38 | 1 | 18GB | Volume sem procedimentos |
| qwen3.5:27b-int8 | 33 | 10 | 29GB | Bom; 1 sessão retornou 0; ~86s/sess; perto do teto |
| mistral-small:24b | 29 | 4 | 14GB | Enxuto, fraco em procedimentos |
| command-r:35b | 120 | 22 | 18GB | Over-extraction - fragmentos, não memória |
| gemma3:27b-it-q8_0 | 28 (5/6) | 1 | 29GB | OOM na sessão 6 |
| llama3.3:70b-q3 | 8 (2/6) | 2 | 34GB | OOM na sessão 3 - nunca terminou |
O que NÃO funcionou / os limites
Quero ser direto sobre até onde isso generaliza:
- O N é pequeno. Seis sessões, uma rodada cada. Dá pra separar “ótimo / ok / quebrado”, não pra cravar um gap de 2 pontos de qualidade. Empates apertados (MoE vs qwen denso) eu resolvi na leitura manual, não na contagem.
- Sem gold set rotulado. “Qualidade” aqui é contagem de fatos + contagem de procedimentos + eu lendo a saída. Um gold set curado deixaria o ranking mais afiado; é trabalho futuro.
- Basicamente um idioma, um domínio. O corpus é em português, pesado em finanças. Um modelo que arrasa aqui pode capengar em sessões de código em inglês. Se os teus dados diferem, teu vencedor pode diferir - que é a tese inteira.
- Uma máquina. Todo veredito de OOM é relativo a 48GB de memória unificada. Numa placa CUDA de 24GB os tetos mudam; numa caixa de 128GB o 70B talvez termine (e talvez ainda perca em qualidade).
- Velocidade vs qualidade é uma bifurcação real. O MoE é ~4× mais rápido. Se isso fosse interativo, eu talvez tivesse escolhido ele. É batch, então não escolhi.
Takeaway
Antes de baixar um 70B ou comprar uma GPU maior pra uma tarefa de extração local:
- Conserte o seu contrato de saída primeiro. Um schema de structured output ganhou de qualquer troca de modelo que eu pudesse fazer. Se um modelo “não consegue extração estruturada”, desconfie do harness antes dos pesos.
- Defina qualidade; não conte. O modelo de maior volume era o pior. Decida o que é uma extração boa, e meça isso - incluindo o subtipo específico que você realmente quer.
- Ajuste a janela de contexto. O KV-cache, não os pesos, causou metade dos meus OOMs. Um
num_ctxdo tamanho da tarefa pode transformar “não cabe” em “confortável”. - Meça nos seus próprios dados. Nenhum leaderboard te diz qual modelo lê as tuas sessões melhor. Seis sessões e uma tarde disseram.
O modelo que lê a minha memória hoje é um int4 de 16GB que o instinto de “maior é melhor” teria pulado - e ele ganhou de um 70B que não conseguiu nem terminar a rodada.
O projeto inteiro é open-source e self-hosted; o extrator, o schema e o script de eval estão no repo. Link abaixo.
Repositório: github.com/carloshpdoc/agent-memory-hub