Desempenho medido
Holdout de 536 casos, com ferramentas que o modelo nunca viu no treino (separação por
raiz semântica: nem o nome nem quase-sinônimos), catálogo de 1 a 6 ferramentas, posição da
correta sorteada.
Table with columns: s42, s43, s44, média | s42 | s43 | s44 | média |
|---|
| ferramenta correta | 80,0% | 84,1% | 83,8% | 82,6% |
| executou e cumpriu | 70,1% | 74,4% | 74,6% | 73,1% |
| over-calling (chamou quando não devia) | 17,5% | 16,0% | 18,3% | 17,3% |
Desvio-padrão entre sementes: 2,53 pp — da mesma ordem do erro amostral esperado em
n=536 (1,92 pp). Não há variância de treino anômala.
Table with columns: s42, s43, s44, média | s42 | s43 | s44 | média |
|---|
| ferramenta correta | 82,3% | 85,1% | 84,9% | 84,1% ± 1,6 |
| executou e cumpriu | 71,8% | 75,0% | 75,2% | 74,0% ± 1,9 |
| over-calling | 17,2% | 16,8% | 17,5% | 17,2% ± 0,4 |
Table with columns: ferramentas no catálogo, ferramenta correta| ferramentas no catálogo | ferramenta correta |
|---|
| 1–6 | 82,3% |
| 5 | 77,2% |
| 10 | 64,0% |
| 15 | 51,7% |
Este modelo não serve para catálogo grande. Escalar de 151M para 345M não conserta
(queda relativa 45% × 39%). ⭐ O que conserta é filtrar o catálogo antes de perguntar: um
recuperador lexical simples leva o acerto de 48,5% para 75,2% com top-3, sem treinar nada
— ver §Uso.
🔴 Este adapter destrói as outras capacidades do modelo base
Medido em 2026-08-30 com 3 sementes, em ChatML (o formato em que ele foi treinado). Ele
responde a um pedido de tradução com "não consigo traduzir com as ferramentas disponíveis":
Table with columns: modelo base, este adapter, piso trivial | modelo base | este adapter | piso trivial |
|---|
| resumo — cobertura | 84,0% | 12,4% ± 15,5 | — |
| resumo — respondeu (de 150) | 131 | 0 · 4 · 52 | — |
| tradução en→pt (chrF2) | 51,12 | 17,97 ± 0,78 | 21,54 (copiar a fonte) |
| tradução pt→en (chrF2) | 43,30 | |
⚠️ Nas duas direções a tradução fica ABAIXO do piso de copiar a fonte sem traduzir — ou
seja, pior que não fazer nada.
A causa está no corpus: 91,1% dos 4.421 exemplos negativos são recusas, e o modelo
generalizou "sem ferramenta → recuse" para qualquer tarefa.
⭐ Se você precisa que o modelo também converse, traduza ou resuma, use
BrCamp/bee-350m-pt-assistente —
mesmos prompts e mesma dose de negativos, com resposta útil no lugar da recusa. Ele custa 5,9 pp
de execução e 1,65 pp de macro, e devolve tradução (27,5), resumo (72,8% de cobertura) e
atendimento (30,9% de JSON), com 2,6 pp a menos de over-calling.
⚠️ Ressalvas — leia antes de usar o número
1. A folga sobre a versão anterior é +3,7 pp, e não a chamo de significativa — ela é medida
contra um modelo de 2 sementes, e comparar média de 3 com média de 2 é assimétrico.
⚠️ E vale como aviso de método: com duas sementes este modelo dava 70,1% e 74,4%, e eu
reportei que a folga (+2,9 pp) cabia dentro da própria amplitude. A terceira deu 74,6% — a
s42 era a ponta baixa de três, não instabilidade. Duas sementes não estimam variância, só
produzem uma diferença. É por isso que as três estão publicadas: para quem baixar verificar,
em vez de acreditar numa média.
2. Catálogo acima de 6 ferramentas é extrapolação. O treino não contém catálogos maiores.
3. O holdout tem 0,5% de e-mail e por isso quase não mede cópia de cadeia densa em pedido
natural. Esse eixo foi medido por sonda sintética, separadamente.
4. Argumentos de texto livre (assunto, corpo de mensagem, título) não entram no escore —
não há critério exato para julgá-los. A cobertura da pontuação vai impressa no avaliador.
O que este adapter conserta em relação ao anterior
Diversificação de cadeias arbitrárias no treino: os endereços de e-mail do corpus tinham
724 ocorrências e apenas 22 valores distintos (boss@company.com em 47%). O modelo não
aprendera a copiar — decorara. Trocando por 868 endereços inéditos, sem um exemplo novo:
Table with columns: cópia de e-mail inédito| cópia de e-mail inédito | |
|---|
| antes (22 distintos) | 41,7% |
| depois (868 distintos) | 53,7% nas duas sementes |
Pareado: +15/−2, McNemar p = 0,0024.
Uso
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
base = "BrCamp/bee-350m-pt-base"
tok = AutoTokenizer.from_pretrained(base)
modelo = AutoModelForCausalLM.from_pretrained(base, dtype="bfloat16")
modelo = PeftModel.from_pretrained(modelo, "BrCamp/bee-350m-pt-agentico")
sistema = """Você é um assistente AGÊNTICO. Você tem acesso às ferramentas abaixo.
FERRAMENTAS DISPONÍVEIS:
- send_email: Envie um email para um destinatário.
args: recipient (O endereço de e-mail do destinatário.), subject (O assunto.), content (O conteúdo.)
obrigatorios: recipient, subject, content
Responda com UM objeto JSON: {"tool": "<nome>", "args": {...}}. Se nenhuma ferramenta servir, responda em texto normal."""
msgs = [{"role": "system", "content": sistema},
{"role": "user", "content": "Envie um e-mail para ana@contoso.com com o assunto Reunião."}]
ent = tok(tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True),
return_tensors="pt")
saida = modelo.generate(**ent, max_new_tokens=320, do_sample=False,
eos_token_id=tok.convert_tokens_to_ids("<|im_end|>"))
print(tok.decode(saida[0][ent["input_ids"].shape[1]:], skip_special_tokens=True))
🔴 subfolder= NÃO funciona no PEFT 0.19.1
A versão anterior deste card mandava usar subfolder="seed-42", e isso falha: o PEFT acha o
adapter_config.json mas não propaga a subpasta para os pesos, dando
Can't find weights ... adapter_model.safetensors. Corrigido em 2026-08-30 — a semente 42
está na raiz do repositório e carrega com o código acima.
Para a 43 ou a 44:
from huggingface_hub import snapshot_download
d = snapshot_download("BrCamp/bee-350m-pt-agentico", allow_patterns="seed-43/*")
modelo = PeftModel.from_pretrained(modelo, f"{d}/seed-43")
⚠️ Duas coisas que mudam muito o resultado
O token de parada tem de estar ligado. Sem eos_token_id apontando para <|im_end|>, a
geração vai até o teto e o parser recebe várias chamadas concatenadas — o que se lê como 0% de
acerto num modelo que acerta.
Ligue as duas restrições de decodificação. Ambas em
comeia/eval/esquema.py:
- chave do argumento restrita ao esquema do prompt — o modelo às vezes escreve
receptor
onde o esquema diz recipient. +16,4 pp, +144 casos e −0.
- nome da ferramenta restrito ao catálogo — ele inventa nomes como
executar_program e
search_livros em 3% dos casos (catálogo 1–6) e 10% (catálogo 15). Restringir dá
+2,3 pp (4 células, pareado 38×2, p = 1,5e-09) e, sobretudo, zera saída inexecutável:
0 ferramentas inválidas em 1.730 chamadas.
⭐ Nas duas, nenhum caso legítimo é bloqueado — chave e ferramenta da referência sempre
vêm do catálogo.
Se o catálogo tiver mais de ~8 ferramentas, filtre antes de perguntar. Um recuperador
lexical (sobreposição de palavras com peso IDF) leva o acerto de 48,5% para 75,2% com
top-3, sem treinar nada — comeia/eval/recuperar_catalogo.py. ⚠️ Ele exige que a
descrição da ferramenta esteja no idioma do usuário: com descrições em outro idioma o
recall cai de 90,1% para 6,3%.
⚠️ Restringir o VALOR, não. Duas versões foram medidas e reprovadas (−9,0 pp e
−15,8 pp): o modelo não deixa de copiar por escolha, e restrição de decodificação não conserta
incapacidade — só troca a forma do erro. Ficam no código, desligadas, com os números no
docstring.
Treino
Table | |
|---|
| base | BrCamp/bee-350m-pt-base (345M, pré-treinado do zero em PT) |
| método | LoRA r=16 α=32, 1 época, lr 1,2e-3, batch efetivo 16 |
| dados | 11.160 exemplos (6.739 com chamada · 4.421 negativos) |
| hardware | RTX 5070 Laptop 8 GB · ~90 min por semente |
O corpus vem do gigaverbo e derivados,
com separação treino/teste por componente conexo de tudo que é compartilhado (pedido ou
tupla de argumentos) e verificação posterior sobre os arquivos finais.
Licença
Apache-2.0, como o modelo base.