# Treinamento do agente de atendimento (estágio 2) Fine-tuning (SFT com LoRA) do Llama-3.1-8B com os atendimentos reais do ifbot que foram **bem avaliados pelo estágio 1** (LLM-as-judge): nota do atendente ≥ 8, problema resolvido e cliente não-insatisfeito. O juiz funciona como filtro de qualidade do dataset — só "assim se atende" entra no treino. ## Fluxo completo ``` avaliações (estágio 1, MySQL) │ node backend/scripts/exportarDatasetTreinamento.js ▼ training/data/{train,valid}.jsonl ← formato chat {"messages":[...]} │ ./treinar_lora_mlx.sh ▼ training/adapters/ ← LoRA treinado (MLX, roda no M4 16GB) │ ./publicar_no_ollama.sh ▼ ollama: star-atendente ← modelo fundido + quantizado q4_K_M ``` ## Passo a passo 1. **Acumular avaliações** — o job do backend avalia sozinho; para acelerar: `cd backend && node scripts/avaliarTudo.js --lote 25` (~20s por atendimento no llama3.1). 2. **Exportar o dataset** (da pasta `backend/`): ``` node scripts/exportarDatasetTreinamento.js # score>=8, resolvido=sim node scripts/exportarDatasetTreinamento.js --score-min 7 --incluir-parcial # filtro mais frouxo ``` Gera `training/data/train.jsonl`, `valid.jsonl` e `manifest.json` (rastreia protocolos usados — `protocolosTrain`/`protocolosValid`, por atendimento — e filtros). Telefones são mascarados; mensagens do bot/sistema ficam de fora; cada amostra começa no cliente e termina na resposta do atendente. **Modo de amostragem (`--modo`)** — importa porque o `mlx-lm` com `--mask-prompt` só calcula loss no *último* turno de cada amostra: - `por-turno` (default, ciclo 5+): cada atendimento gera várias amostras, cada uma terminando num turno diferente do atendente (prefixo da conversa até ali). Sorteia até `--alvos-por-atendimento` (2) turnos com >= `--min-palavras-alvo` (8) palavras, mais acks/fechamentos curtos limitados a `--frac-triviais` (0,15) do total, pra o modelo ainda saber fechar conversa. `--max-alvo-repetido` (30) limita quantas vezes um mesmo texto de atendente ("Vou transferir seu atendimento…") pode ser alvo. O split treino/validação é por **atendimento**, então não há vazamento. - `ultimo-turno` (ciclos 1-4): 1 amostra por atendimento terminando no último turno. Nos ciclos 1-4 isso fazia o alvo ser quase sempre o fechamento ("Disponha!", mediana de 7 palavras, 43,6% com <=5) e o modelo aprendia a fechar conversa em vez de resolver. Mantido só pra reproduzir os datasets antigos. Com `por-turno` o dataset tem ~2x mais amostras que o `ultimo-turno`: prefira **1 época** (`./treinar_lora_mlx.sh 1`) pra manter o tempo de treino próximo ao dos ciclos anteriores. 3. **Treinar** (nesta máquina, Apple Silicon): ``` ./treinar_lora_mlx.sh # 3 épocas ./treinar_lora_mlx.sh 2 # 2 épocas ``` Usa `mlx-lm` com o modelo 4-bit (`mlx-community/Meta-Llama-3.1-8B-Instruct-4bit`), batch 1, seq 2048, gradient checkpointing — cabe nos 16 GB do M4, mas feche apps pesados e espere algumas horas com centenas de amostras. Acompanhe o `Val loss`: se começar a subir enquanto o train loss cai, reduza as épocas (overfitting). O script roda automaticamente, antes do treino: - `filtrar_por_tokens.py` — descarta amostras cujo tamanho real em tokens (tokenizer do próprio modelo) passa de 1600. `exportarDatasetTreinamento.js` só filtra por caracteres (`--max-chars`), que não é proxy confiável de tokens em pt-BR; uma amostra de 14-18 mil caracteres pode passar de 4000 tokens — bem acima do `--max-seq-length` de 2048 — e isso já causou OOM de GPU (e reinício da máquina) durante a validação em 04/08. - o treino roda via `run_lora_seguro.py`, que capa o `memory_limit`/`wired_limit` do MLX em 9 GiB antes de carregar o modelo. Por padrão o MLX permite até 1.5x o working set recomendado da GPU (~19 GiB neste M4), acima da RAM física (16 GiB) — sem teto explícito, um pico de memória derruba o sistema inteiro em vez de estourar uma exceção Python capturável. *Alternativa com GPU NVIDIA (mais rápido, datasets maiores):* os mesmos JSONL funcionam no [unsloth](https://github.com/unslothai/unsloth) ou axolotl — treinar lá, trazer o adapter PEFT e converter com `convert_lora_to_gguf.py` do llama.cpp. 4. **Testar o adapter antes de publicar**: ``` source .venv/bin/activate mlx_lm.generate --model mlx-community/Meta-Llama-3.1-8B-Instruct-4bit \ --adapter-path adapters --max-tokens 200 \ --prompt "Boa tarde, minha internet caiu" ``` 5. **Publicar no Ollama**: `./publicar_no_ollama.sh` — funde o LoRA, cria o modelo `star-atendente` quantizado e remove o intermediário fp16 (~16 GB temporários em disco). 6. **Validar e ativar**: compare respostas com o llama3.1 puro em perguntas típicas de suporte. O backend já usa `star-atendente` por default (`config.atendenteSugestao.model`, na feature de sugestão de resposta ao atendente) — só é preciso setar `OLLAMA_ATENDENTE_MODEL=star-atendente` no `backend/.env` se o modelo for publicado com outro nome. Não confundir com `OLLAMA_CHAT_MODEL`, que é o modelo do chat RAG geral. ## Avaliação (`eval/`) — rode antes de publicar qualquer modelo ou mudar o prompt da sugestão Harness com 30 cenários fictícios (`eval/cenarios.json`), sem dado de cliente: ``` # modelo MLX (base, ciclos, checkpoints) — da pasta training/, com a .venv HF_HUB_OFFLINE=1 .venv/bin/python eval/gerar_respostas.py --variantes base ciclo5 ciclo5@1400 --temp 0.3 --n-amostras 5 --saida eval/out/x.json # caminho de produção (Ollama + atendenteSugestaoService) — da pasta backend/ node training/eval/gerar_ollama.mjs --n 3 --saida training/eval/out/y.json \ --variante atual=prompt:atual,rag:nenhum,guard:1 --variante antiga=prompt:producao_v0.txt,rag:avaliacao,guard:0 # julgar (juiz llama3.1 local + regex de src/services/atendenteSugestaoGuardrails.js) node training/eval/juiz.mjs training/eval/out/y.json --saida training/eval/out/y.julgado.json node training/eval/juiz.mjs --calibrar # concordância do juiz com eval/vereditos_manuais.json ``` Cuidados de leitura: o juiz erra a fronteira adequada/parcial (69% de concordância exata com o manual, 88% em 3 níveis útil/fraca/afirma_falso) e tem falsos positivos de `afirma_falso` (recusas corretas); confira manualmente os casos marcados. Com n≈90 por variante, diferenças de poucos pontos são ruído. `ciclo5@N` é o arquivo numerado N da 2ª tentativa: iteração global = N + 1400. Resultados de 25/09/2026: o ciclo 5 ficou em 33% de respostas úteis contra 79% da base; nenhum checkpoint chegou perto da base. **Sugestão de resposta em produção (desde 25/09/2026): base `llama3.1`, não o LoRA.** Prompt com regras anti-invenção, guardrail pós-geração e RAG desligado (`ATENDENTE_SUGESTAO_RAG_TIPO=nenhum`; o RAG de avaliação elevava as afirmações inventadas de ~3% para ~10%). Chaves: `ATENDENTE_SUGESTAO_GUARDRAIL`, `ATENDENTE_SUGESTAO_RAG_TIPO` (`nenhum`|`avaliacao`|`conversa`), `ATENDENTE_SUGESTAO_RAG_ENABLED`. ## Quando re-treinar O dataset cresce conforme o sync importa protocolos novos e o juiz os avalia. Re-exportar + re-treinar vale a pena a cada leva significativa de amostras novas (ex.: +30%). O `manifest.json` diz o que entrou em cada rodada. ## Próximo estágio (futuro) Com volume maior, os scores viabilizam **DPO**: pares "atendimento nota alta" vs "nota baixa" para o mesmo tipo de problema, treinando por preferência em vez de imitação. Exige mais dados e curadoria — só faz sentido depois do SFT mostrar limite.