Pinhão: o adapter Gemma 4 que não finge ser modelo novo
No rastro da polêmica Rio 3.5, documentei o Pinhão: LoRA público sobre Gemma 4, dataset sintético, vault local, eval pequena e limites explícitos.

Começando pela polêmica
A semana do Rio 3.5 Open foi uma aula pública sobre uma coisa simples: em modelo aberto, procedência não é rodapé. É parte do artefato.
O caso virou barulho porque a release foi apresentada com ambição de foundation model brasileiro, e a comunidade foi atrás dos pesos. A issue da Nex acusou a publicação de ser uma mistura aproximada de 0.6 Nex-N2-Pro + 0.4 Qwen3.5-397B-A17B, com o modelo ainda se identificando como Nex quando removiam o prompt de sistema. Depois, a própria página do HF passou a dizer que o Rio 3.5 foi construído via merge de Nex e Qwen, seguido de distillation, e que houve upload incorreto da versão base mergeada em vez do modelo final. A nota do IplanRIO no X entrou nessa mesma tentativa de explicação pública.
Legalmente, tecnicamente, politicamente: dá para discutir muita coisa. Mas a lição operacional é menos glamourosa e mais útil:
Esse post é o meu anti-clímax dessa polêmica. Em vez de anunciar um “modelo novo”, eu treinei e publiquei o Pinhão Gemma 4 RAG Adapter: um LoRA/PEFT pequeno, sobre google/gemma-4-E4B-it, feito para melhorar formato de resposta num RAG local de Obsidian. Não é foundation model. Não é memória privada compactada. Não é soberania nacional em 20 steps. É só um adapter com recibo.
O que ele é
Pinhão é um adapter público para moldar comportamento de resposta em um fluxo de segundo cérebro:
- responder curto em PT-BR;
- citar fontes no formato
[S#]; - recusar exatamente com
não tenho contexto suficiente; - não inventar IDs de fonte;
- manter o conhecimento factual no RAG local, não nos pesos.
Essa última linha é a arquitetura inteira. O adapter não substitui o retrieval. Ele só tenta reduzir a quantidade de prompt necessário para manter o formato certo. A fonte de verdade continua sendo o Obsidian local, indexado em SQLite, com os trechos escolhidos pelo rag_ask.py.
Então a frase honesta é: Pinhão muda estilo e contrato de saída. Não adiciona memória privada.
O caminho de inferência que eu quis validar ficou assim:
Obsidian (local)
-> SQLite + embeddings (local)
-> rag_ask.py seleciona trechos [S1..Sn] (local)
-> túnel SSH (só prompt montado)
-> RunPod: Gemma 4 + LoRA Pinhão
-> resposta PT-BR com [S#]
Vault, índice e embeddings não cruzam o túnel. Só o contexto já recortado vai para a GPU.
Como o experimento nasceu
A restrição inicial era melhor que qualquer arquitetura bonita: não treinar ainda, não baixar modelo pesado localmente, não subir vault/SQLite/embeddings para RunPod, e não fabricar dataset com cara de nota privada.
O primeiro plano era mecânico — smoke de recibos para extrair JSON. Funcionava, mas era inútil para o problema real. A virada foi cortar esse rumo: recibos ficaram só como smoke de pipeline; o dataset útil virou um contrato sintético de RAG.
| Arquivo | Papel |
|---|---|
data/gemma4-receipts-train.jsonl | 200 linhas sintéticas para smoke mecânico |
data/gemma4-receipts-eval.jsonl | 40 linhas holdout para o smoke |
data/gemma4-rag-contract-train.jsonl | 160 linhas sintéticas para comportamento RAG |
data/gemma4-rag-contract-eval.jsonl | 40 linhas holdout para eval |
scripts/make_rag_contract_dataset.py | gerador do contrato sintético |
scripts/qlora_receipts_train.py --check-config | valida loader/config sem treinar |
Detalhe que parece pequeno e não é: os caminhos do dataset viraram synthetic/projects/..., não wiki/projects/.... O primeiro rascunho parecia nota real falsa. Dataset sintético tem que parecer sintético, inclusive no nome.
Os scripts e JSONLs ficaram no laboratório privado; o model card público descreve o dataset, a eval e um sketch mínimo de carregamento do adapter.
O dataset que vale
O contrato RAG ensina tarefas pequenas, mas importantes:
- responder só com suporte nos trechos dados;
- citar apenas
[S1],[S2],[S3]quando essas fontes existem; - recusar quando o contexto é fraco;
- dizer quando há conflito entre fontes;
- responder parcialmente quando só parte da pergunta tem suporte;
- manter resposta curta em português.
Isso é menos sexy que “treinei um modelo”. Também é mais real. Fine-tuning útil quase nunca começa com conhecimento novo. Começa com comportamento repetitivo que você está cansado de enfiar no prompt.
Uma linha típica do JSONL (simplificada) parece isto:
{
"messages": [
{"role": "user", "content": "Contexto:\n[S1] O smoke validou health e POST.\n[S2] Nada do vault saiu do laptop.\n\nPergunta: o que o smoke validou?"},
{"role": "assistant", "content": "Validou health e chat completions [S1]. Vault ficou local [S2]."}
]
}
Antes e depois
As métricas fazem mais sentido com um exemplo concreto. Mesmo tipo de pergunta, mesmo contexto sintético:
Contexto:
[S1] O serving smoke validou um servidor HTTP mínimo.
[S2] O mini-eval HTTP fez 5/5 no contrato estrito.
Pergunta: o que o smoke validou?
Base (padrão típico):
model
O smoke validou health check e o endpoint de chat.
thought
Resposta longa, sem [S1]/[S2] consistentes.
Adapter (servido, após cleanup):
Validou health e chat completions [S1, S2]. Nada do vault saiu da máquina [S1].
A base não inventou citação fantasma — isso já era 40/40 nos dois lados. O problema era formato: vazava trilha interna, resposta longa demais, citação inconsistente. Foi isso que o contrato de 160 linhas tentou comprimir nos pesos.
Prompt longo vs. LoRA
Na eval, base e adapter viram o mesmo contexto RAG montado no prompt. A base ficou em 18/40 no contrato de resposta; o adapter, em 37/40. Não rodei sweep exaustivo de system prompt — seria outro experimento — mas o gap persistiu mesmo depois de cleanup no serving.
Leitura pragmática: dá para empurrar parte do comportamento no prompt, sim. O LoRA comprou estabilidade de formato com menos texto de sistema repetido a cada chamada. Para um assistente que roda o dia inteiro em cima do vault, isso importa mais do que parece.
O treino
A primeira tentativa real foi no RunPod. A A5000 não estava disponível; a A40 de 48 GB resolveu. O treino em si levou ~119 s após cache; o pod ficou ligado mais tempo por download de pesos e setup — ordem de centavos no compute de treino, não de dezenas de dólares.
Escolhi google/gemma-4-E4B-it porque já era o candidato do laboratório para RAG local: instruction-tuned, cabível em 4-bit numa A40, e o wrapper multimodal exige cuidado — o LoRA precisa ficar só em model.language_model.*, longe das partes de visão.
| Item | Valor |
|---|---|
| Base | google/gemma-4-E4B-it |
| Formato | LoRA / PEFT (r=16, lora_alpha=32, dropout=0.05) |
| Alvo | módulos model.language_model.* (q/k/v/o + MLP) |
| GPU | RunPod A40 48 GB |
| Dataset | data/gemma4-rag-contract-train.jsonl |
| Linhas | 160 sintéticas |
| Steps | 20 |
| Runtime após setup/cache | 118,7 s |
| Train loss final | 1.341 |
| Stack | torch 2.11.0+cu128, transformers 5.12.1, TRL 1.6.0, PEFT 0.19.1 |
Gemma 4 aqui teve suas pequenas pontas. transformers 4.57.3 deu problema de processor/tokenizer. O caminho estável foi AutoModelForMultimodalLM, fallback de AutoTokenizer para texto, e LoRA restrito ao language model para não brigar com o wrapper multimodal.
O adapter subiu primeiro privado no Hugging Face. Depois virou público com model card e gráfico de eval: brunogonzaga/pinhao-gemma4-rag-adapter.
A parte que impede autoengano
Treinar é fácil de romantizar. Eval é onde a história perde maquiagem.
A avaliação comparou base vs adapter em 40 linhas sintéticas holdout. O resultado bruto:

| Métrica | Base | Adapter |
|---|---|---|
| Answer contract | 18/40 | 37/40 |
| Raw strict | 14/40 | 20/40 |
| Served strict com cleanup | 18/40 | 37/40 |
| Recusa exata | 33/40 | 40/40 |
| Citações esperadas | 27/40 | 37/40 |
| Citações inventadas | 0/40 | 0/40 |
Leitura honesta: o adapter melhora o contrato de resposta, principalmente recusa e citação. Mas a geração crua ainda vazava marcadores como model, thought e fragmentos finais de recusa — daí raw strict ficar em 20/40 mesmo com adapter.
Os 3 que sobraram
Dos 40 casos holdout, 3 ainda falharam no served strict com adapter. Concentraram-se nas tarefas mais difíceis do contrato: resposta parcial (pergunta composta com suporte só em metade) e conflito entre fontes (duas linhas [S#] com informação inconsistente). São exatamente os buckets com menos exemplos no treino de 160 linhas. Não é falha misteriosa — é amostra pequena encontrando o limite.
Cleanup no serving
Separar raw de served foi deliberado. Treino mede o adapter; produção mede o que o usuário vê depois de cortar lixo de geração.
O servidor HTTP mínimo aplicou três coisas:
- Stop markers — cortar em
model,thoughtou recusa duplicada no fim; - Token cap conservador — impedir resposta que recomeça o contrato inteiro;
- Strip de whitespace — normalizar o que sobrou antes de devolver JSON OpenAI.
Esqueleto do cleanup (simplificado):
STOP_MARKERS = ("model", "thought")
def serve_cleanup(text: str) -> str:
lowered = text.lower()
cut_at = len(text)
for marker in STOP_MARKERS:
idx = lowered.find(marker)
if idx != -1:
cut_at = min(cut_at, idx)
return text[:cut_at].strip()
Com isso, served strict subiu de 20/40 (raw adapter) para 37/40. Bom o suficiente para smoke. Não bom o suficiente para fingir produção sem eval maior.
O smoke de verdade
A parte que me interessava não era “consigo chamar o adapter?”. Era o caminho completo abaixo — local até o retrieval, remoto só na geração:
rag_ask.py local -> retrieval local -> túnel SSH -> RunPod adapter server

Depois disso, criei um servidor HTTP mínimo compatível com OpenAI e testei o caminho completo:
/healthrespondeu ok;POST /v1/chat/completionsrespondeu limpo;- mini-eval HTTP fez 5/5 no contrato estrito;
rag_ask.py --provider openairodou localmente;- o túnel SSH levou só o prompt com contexto selecionado;
- vault, SQLite, embeddings e notas reais ficaram locais;
- o servidor foi parado depois do smoke.
Esse é o ponto de privacidade que eu queria preservar desde o começo. Eu posso usar GPU remota para geração. Não preciso mandar meu segundo cérebro inteiro para ela.
Por que isso entra na polêmica
O problema de releases de IA não é usar base alheia. O ecossistema inteiro é empilhamento: base model, tokenizer, dataset público, dataset privado, SFT, DPO, merge, LoRA, quantização, serving hack. O problema é vender a pilha como se fosse uma pedra única caída do céu.
Pinhão é pequeno, então a régua é fácil de cumprir. Mesmo assim, a régua é a mesma:
| Pergunta | Resposta do Pinhão |
|---|---|
| Qual é o modelo base? | google/gemma-4-E4B-it |
| É modelo completo? | Não. É adapter LoRA/PEFT |
| Que dados usou? | 160 linhas sintéticas de contrato RAG |
| Usou vault real? | Não |
| O que melhora? | Formato, recusa, citação, brevidade |
| O que não melhora? | Conhecimento factual sem retrieval |
| Tem eval? | Sim, holdout sintético de 40 linhas |
| Tem limitação explícita? | Sim: pequeno, sintético, precisa cleanup, não produção |
Essa tabela é chata. Por isso ela importa.
Checklist anti-polêmica
Checklist genérico, com três itens que este experimento forçou na prática:
- Nomeie o base model — e linke o artefato, não só o nome.
- Diga se é full weights, merge, adapter, quantização ou serving wrapper.
- Separe métrica raw de métrica served; cleanup faz parte do produto.
- Publique o dataset ou descreva por que não pode (Pinhão: sintético, paths
synthetic/). - Liste explicitamente o que não entrou (vault, SQLite, embeddings, PII).
- Tenha holdout, mesmo pequeno — e diga onde ainda falha.
- Mostre métrica que ainda é feia depois do treino (raw strict do adapter: 20/40, apesar de 37/40 served).
- Desenhe o caminho de inferência real, incluindo o que fica local.
- Documente privacidade como arquitetura (túnel leva prompt, não vault).
- Escreva limitações antes de escrever marketing.
No Pinhão, o resultado mais importante não é 37/40. É a linha pública: adapter, dado sintético, formata resposta RAG; não contém meu vault.
Fechando
Eu gosto do nome Pinhão porque soa local sem performar grandiosidade. É semente, não floresta.
A polêmica do Rio 3.5 não deveria ensinar que ninguém pode remixar modelo. Deveria ensinar o oposto: pode remixar, adaptar, destilar, publicar — só não esconda a receita atrás do nome.
Pinhão é a minha versão pequena dessa regra: menos pose, mais procedência.
Artefato público: brunogonzaga/pinhao-gemma4-rag-adapter, com model card, gráfico de eval, descrição do dataset, sketch de carregamento, limitações e nota de privacidade. Scripts e JSONLs ficaram no laboratório privado; a procedência documentada é pública.