Aventuras com LLM local

Publicado em 28 de abril de 2026

Uso assistentes de IA todos os dias para programar — Claude Code, GPT, Gemini API. Ao longo do primeiro semestre de 2026, fui acumulando três frustrações com APIs em nuvem.

Pressão de tokens. Os planos pagos foram apertando os limites disponíveis a cada renovação. Cada projeto sério em algum momento batia no teto e exigia esperar o reset ou comprar pacotes adicionais. Custo crescente, controle decrescente.

Modelos somem. Em uma aplicação de auditoria de transformadores elétricos, o gemini-1.5-pro foi descontinuado da noite para o dia. O gemini-2.0-flash virou bloqueado para novos usuários. Sobrou migrar para gemini-2.5-flash enquanto durar. Quando o pipeline depende de um modelo na nuvem, ele tem prazo de validade.

Dados sensíveis fora de casa. Cada foto de placa de transformador enviada à API do Google é dado de cliente saindo da rede.

A resposta, em três projetos construídos no primeiro semestre de 2026, foi montar o stack em casa: um servidor LLM dedicado, um cliente que apontasse para ele em vez da API da Anthropic, e uma aplicação real que provasse que isso funciona em produção.

Existe um trade-off honesto: contexto custa memória. O combo modelo+contexto precisa caber na VRAM disponível, e essa equação restringe escolhas. Mas o ganho — controle do que roda, do que sai da máquina, e quanto custa por token (zero) — compensa.

Os três projetos: ancalagon-llm (setup do servidor com RTX 4070 Ti SUPER e llama.cpp tunado para MoE), local-claude (wrapper que faz Claude Code usar modelos locais) e ocr-pipeline-local (pipeline de visão substituindo a API do Gemini).


Números em Destaque

36.8 tok/s

Qwen3.6-27B local, 100% GPU

~6×

Speedup com speculative decoding (RTX 4070 Ti SUPER)

81.5 tok/s

Qwen3-Coder 30B (MoE tunado)

13 GB

Modelo cabendo em 16 GB de VRAM (TQ3_4S)

176

Testes automatizados no pipeline OCR

≥85%

Acurácia em campo (alimentador real)

5

Backends de inferência suportados

0

Chamadas a APIs externas em produção


O Servidor — ancalagon-llm

A primeira peça do stack é o servidor. O hardware é um PC dual-boot (Windows + Ubuntu Server 24.04) com Ryzen 7600X, 32 GB de RAM e RTX 4070 Ti SUPER 16 GB. O Mac acessa via Tailscale em 100.64.0.10.

Por que sair do LM Studio

O LM Studio era prático mas estava deixando metade dos tokens na mesa. Durante inferência, a GPU ficava em 30–34% de utilização, consumindo aproximadamente 70 W de um TGP de 285 W. Dois gargalos foram identificados:

  • Offload genérico não funciona para MoE. O LM Studio divide “N% das camadas na GPU”. Para modelos Mixture-of-Experts, o que importa é colocar attention/norm na GPU e os experts na CPU. A flag -n-cpu-moe do llama.cpp faz exatamente isso. O LM Studio não a expõe.
  • Modelo maior que VRAM. O Qwen3.6-27B Q4_K_M ocupa 17 GB — não cabe em 16 GB. Com o quant TQ3_4S (3-bit ternary, fork turbo-tan/llama.cpp-tq3) cai para 13 GB e cabe 100% em GPU.

Ganhos medidos

Configuração GPU util Power tok/s gen
LM Studio — qwen3-coder Q4_K_M 30% 67 W 64.5
llama.cpp upstream — coder -ncmoe 10 36% 101 W 81.5
LM Studio — Qwen3.6-27B Q4_K_M 34% 94 W 13.7
llama.cpp TQ3 fork — Qwen3.6-27B-TQ3_4S 96% 292 W 36.8

Três presets, uma porta

O servidor expõe três modelos via systemd, cada um em um service com Conflicts= declarado — apenas um sobe por vez, e o systemd para o anterior automaticamente:

  • Qwen3-Coder 30B — codificação (MoE com flag --n-cpu-moe 16, ctx 96K)
  • Qwen3.6-27B TQ3_4S — raciocínio/thinking (100% GPU, ctx 40K)
  • Gemma 4 26B-A4B — uso geral (MoE com --n-cpu-moe 8, ctx 96K)

Todos na mesma porta 1234 (idêntica à do LM Studio) — clientes existentes não precisam mudar URL. Um wrapper lmswitch alterna entre services com health-poll. Aliases SSH no Mac (llcoder, llq36, llgemma4, lloff) tornam o controle remoto trivial.

Fluxo de uso

Glaurung (Mac)                    Ancalagon (Ubuntu)
                                  100.64.0.10 / :1234
aliases:                          systemd --user:
  llcoder  ──ssh──>                 llama-coder.service ──┐
  llq36    ──ssh──>                 llama-qwen36.service ─┤ Conflicts=
  llgemma4 ──ssh──>                 llama-gemma4.service ─┘ (apenas um up)
  lloff    ──ssh──>                       │
                                          ▼
                                   :1234 (OpenAI-compat API)
                                          ▲
curl http://100.64.0.10:1234 ────────────┘

O Cliente — local-claude

Claude Code é um agente de codificação excelente, mas só fala com a API da Anthropic. Eu queria o agente, não o vendor lock-in. local-claude é um wrapper bash que injeta variáveis de ambiente (ANTHROPIC_BASE_URL, CLAUDE_CONFIG_DIR=~/.claude-local para isolar config) e roteia o tráfego para um servidor OpenAI-compatível local ou remoto. O claude original fica intocado.

Cinco backends

  • LM Studio — conecta ao servidor local
  • llama.cpp local — sobe e mata um llama-server automaticamente
  • llama.cpp remoto via SSH — mesmo, mas via SSH num host remoto (cenário Mac → Ancalagon)
  • remote — conecta a um servidor já em pé (ex.: o service do Ancalagon)
  • Apple Intelligence via apfel — modelo on-device do macOS Tahoe

Speculative decoding automático

Quando há um modelo menor da mesma família no diretório, o script ativa speculative decoding: um “draft” pequeno gera tokens candidatos que o modelo grande verifica em batch. Tokens aceitos são gratuitos; rejeitados são regerados normalmente.

Plataforma (Qwen2.5-7B Q8_0 + 0.5B Q8_0 draft) Sem draft Com draft Speedup
Apple M4 Pro (24 GB) 29 t/s 57 t/s ~2×
RTX 4070 Ti SUPER (16 GB) 29 t/s 177 t/s ~6×

Insight: o menor draft vence. O draft de 3B é mais lento que o de 1.5B apesar de ter taxa de aceitação maior — o overhead de verificação domina.

Apple Intelligence é caso especial

A janela de contexto da Apple Intelligence é de 4096 tokens. O system prompt + ferramentas do Claude Code sozinhos somam aproximadamente 27K tokens — sete vezes mais do que cabe. O backend roda em chat-only mode (--bare --tools ""): dá para conversar, mas o agente não pode usar ferramentas (editar arquivos, rodar comandos). É um lembrete físico de que contexto = memória.

Integração com o servidor

No Mac, o alias srl-coder conecta direto ao service que está no ar (specstory run claude -c "local-claude --backend remote --port 1234"). Zero gerenciamento de processo no cliente: o service do Ancalagon é a infraestrutura, o local-claude é só a ponte.


A Aplicação — Pipeline OCR Local

As duas primeiras peças são ferramentas de desenvolvimento. A terceira é uma aplicação concreta: um pipeline de visão que substitui a API do Google Gemini em um sistema de auditoria de transformadores elétricos. Fotos de campo de placas de equipamento entram, JSON estruturado com 11 campos sai (potência, fabricante, série, data de fabricação, elo fusível, tensões primária e secundária, fases, autoproteção, tombamento, matrícula).

Arquitetura

Servidor dedicado: outro PC com RTX 5070 12 GB, Ubuntu 24.04, CUDA 12.8. O pipeline tem dois estágios principais e dois auxiliares:

foto de placa ─▶ YOLO Stage 0 ─▶ Stage 1 (visão) ─▶ Stage 2 (raciocínio) ─▶ JSON
   (SMB ou           crop          Qwen3.5-VLM         qwen2.5:14b           ▲
    upload         da placa        9B (llama.cpp)     (Ollama)               │
    base64)            │              :8090            :11434                │
                       │                                  │                  │
                       ▼                                  ▼                  │
                 YOLO Stage 3 ◀─────── auditoria ─────────┘                  │
                  (revalida classes detectadas)                              │
                                                                             │
                                       cruzamento OCR × cadastro ────────────┘
  • Stage 0 (YOLO) — detecta a bounding box da placa_transformador, faz crop com 10% de padding e passa só a região da placa para o VLM em alta resolução. Fallback: redimensionar para 1600 px se YOLO falhar.
  • Stage 1 (visão) — Qwen3.5-VLM 9B via llama.cpp extrai todo texto visível da imagem cropada.
  • Stage 2 (raciocínio) — qwen2.5:14b via Ollama recebe o texto bruto e devolve JSON com 11 campos validados.
  • Stage 3 (YOLO) — auditoria pós-Stage 2: se o fabricante foi extraído mas a YOLO não viu nenhuma placa_identificacao, marca _needs_review=True para revisão humana.

Por que dois estágios e não modelo único

Testei sete modelos de visão (glm-ocr, qwen2.5vl, Qianfan-OCR, Gemma 4 E4B, entre outros). O Qwen3.5-VLM lê bem, mas não é confiável para emitir JSON estruturado direto — alucinou campos, inverteu valores, perdeu números. Separar OCR puro do raciocínio estruturado deu 11/11 campos corretos no conjunto de validação inicial (placas Romagnole 112.5 kVA).

Evolução em três semanas

O pipeline saiu do zero (v1.0) ao v3.1 com 11/11 campos em uma única sessão (06 de abril). Em 18 dias, evoluiu por v3.2 → v3.7 → v4.0 → v4.1-dev, terminando com 176 testes automatizados e ≥85.3% de acurácia em campo em um alimentador de rede elétrica (34 postes comparados com cadastro).

Descobertas práticas

  • Filtros em Python > regras no prompt. O qwen2.5:14b não segue regras complexas em linguagem natural. Toda vez que tive a tentação de adicionar regra ao prompt, regex em Python deu mais confiabilidade. Filtros como _classify_text e _filter_ocr_for_stage2 separam texto de placa, de medição, de gravação no poste e de telecom antes do Stage 2 ver.
  • YOLO como contexto, não só crop. A classe detectada (cls4=placa_identificacao vs cls6=placa_transformador) propaga um pole_id_context que controla filtros downstream. Detectar é fácil; usar a detecção para mudar comportamento do pipeline é o que gera ganho.
  • GPU semaphore. Stage 1 (~7 GB) + Stage 2 (~9 GB) ultrapassam os 12 GB da RTX 5070. Solução: threading.Semaphore(1) no FastAPI, serializando estágios dentro de uma mesma requisição.
  • banco_trafo via tags do banco de dados. Detectar banco de transformadores contando placas no mesmo frame YOLO não funciona — cada trafo do banco tem foto separada. Solução: contar imagens distintas com tag placa_transformador no banco. Se ≥ 2 → banco confirmado, e a comparação vira OCR_pot × N vs cadastro (±5%).

Integração com a aplicação consumidora

A API REST sobe em FastAPI na porta 8091 com envelope estilo Gemini (contents[].parts[].inline_data) para ser drop-in para o C# que antes chamava o Google. A aplicação dispara via dois tipos novos de agenda no scheduler existente:

  • TRA — processa um alimentador inteiro lendo as imagens via SMB.
  • ALT — envia imagens diretamente em base64 (lotes de até 50 postes / 30 imagens por poste / 200 MB).

Há ainda um endpoint OCR-only POST /api/v41/ocr/poste/{id}/texto para casos onde só o texto bruto interessa — usado para extração de dados de poste (separados dos dados do trafo, com filtros próprios).


O Preço do Contexto: Memória

Na nuvem, contexto parece grátis (Claude tem 1 M, Gemini tem 2 M). Localmente, contexto é VRAM. A equação é simples:

VRAM = pesos do modelo + KV cache

O KV cache cresce linearmente com contexto e batch. Cada combinação modelo × ctx × quantização × KV quant ou cabe ou não cabe na placa. Na prática, foi assim nos três projetos:

Servidor VRAM Modelo Quant Ctx KV Observação
Ancalagon 16 GB Qwen3-Coder 30B (MoE) Q4_K_M 96K q4_0 experts na CPU (-ncmoe 16)
Ancalagon 16 GB Qwen3.6-27B TQ3_4S 40K q8_0 100% GPU, 96% util
Ancalagon 16 GB Gemma 4 26B (MoE) Q4_K_M 96K q4_0 -ncmoe 8
Servidor OCR 12 GB Qwen3.5-VLM + qwen2.5 Q4 / Q5 n/a serializado por semáforo
Mac (apfel) unified Apple Intelligence 4096 chat-only, sem ferramentas

Para Claude Code (system prompt ~27K + ferramentas), 4096 tokens não funciona. Mas 96K com modelo MoE de 30B em 16 GB de VRAM consumer-grade funciona — e era impensável dois anos atrás.


Lições Aprendidas

  • KV quant no LM Studio exige wrapper específico. Sem {checked: true, value: "q4_0"} o campo é silenciosamente ignorado e o cache fica em f16. Descoberto medindo VRAM antes e depois — nada na UI sinaliza.
  • K e V assimétricos quebram o coder. K=q8 / V=q4 → fallback CUDA catastrófico no Qwen3-Coder (3 tok/s). Iguale ou não use.
  • GPU hang na Blackwell. A RTX 5070 saiu do barramento PCIe sob carga intermitente. Cinco camadas de proteção: persistence mode no boot, pcie_aspm=off, semáforo no FastAPI, GpuHangError propagado para o cliente, e drop-in para parar processos concorrentes antes de subir o serviço principal.
  • gemini-1.5-pro 404 do dia para a noite. Sem aviso. Migração emergencial para gemini-2.5-flash em produção. Lembrete físico de por que vale a pena ter alternativa local.
  • File.Exists() silencioso em C#. Para path SMB inacessível, retorna false sem exceção. Resultado: zero imagens enviadas ao Gemini sem nenhuma mensagem de erro. Verifique acesso explicitamente em paths de rede.
  • Mistura de serializadores JSON em C#. [JsonPropertyName] (System.Text.Json) é ignorado pelo Newtonsoft.Json. Resultado: campos todos “NI” sem erro. Confira sempre qual serializador faz a desserialização de fato.
  • Filtros Python > regras no prompt. Modelos de porte médio não seguem regras complexas em linguagem natural. Quando regex resolve, regex resolve melhor.
  • YOLO como contexto, não só crop. A classe detectada propaga estado para filtros downstream e muda comportamento do pipeline. Detectar é fácil; usar a detecção é o que diferencia.

Reflexão

O ganho principal não é economia (embora seja real — zero custo por token marginal). É iterar sem medo.

Quando cada chamada custa, você poda experimentos antes deles começarem. Localmente, dá para rodar 4 configurações A/B no mesmo dataset (25 postes × 4 = 100 inferências) numa tarde sem pensar duas vezes — foi exatamente o que fiz para validar que o baseline Qwen3.5-VLM + qwen2.5:14b vence as alternativas (Qwen3-VL-8B, qwen3:14b com /no_think, qwen3:8b).

E quando o modelo é seu, ele não some. O Qwen3-Coder Q4_K_M de hoje vai estar rodando igualzinho em 2030, se eu quiser. Isso muda a escala de tempo dos projetos: dependências de modelo deixam de ser risco operacional.

O trade-off é real: contexto é finito. Para tarefas que exigem mais de 100K tokens (refator grande, base de código gigante), Claude na nuvem ainda ganha. Mas para 90% do que faço no dia a dia — feature focada, debug iterativo, OCR de placa, validação estruturada — o stack local resolve. E resolve sem que a próxima renovação de plano apertando os limites volte a ser uma fricção.