Inteligência artificial, sem ruído.
Tutoriais8 min

Otimizando SLMs: como restringir o espaço de saída elimina respostas malformadas

Troque a geração livre de texto por scoring restrito e torne erros de classificação estruturalmente impossíveis em modelos pequenos — até 30% mais rápido.

Otimizando SLMs: como restringir o espaço de saída elimina respostas malformadas

Por que isso importa agora

Modelos de linguagem pequenos (SLMs, na sigla em inglês) — como o Qwen2.5-0.5B, com meio bilhão de parâmetros — deixaram de ser curiosidade de laboratório para virar opção real de produção em tarefas de automação estreita: rotear tickets de suporte, classificar documentos, extrair campos. O problema é que a maioria das pessoas ainda os usa como se fossem um ChatGPT genérico: pede o texto, gera vários tokens e depois procura a resposta na string. Isso é lento e, pior, não confiável.

Há uma técnica que muda esse jogo: em vez de gerar texto e analisar o resultado, você restringe o espaço de saída do modelo à lista de rótulos válidos e lê a distribuição de probabilidade diretamente. O resultado, medido em benchmark, é até 30% mais rápido — e com erro de formatação estruturalmente impossível. Neste guia, mostramos como funciona, com código completo e explicado.

O que é “restringir o espaço de saída”

Uma tarefa de classificação tem um conjunto fixo de respostas. Se você roteia tickets em billing (cobrança), technical (técnico) ou account (conta), existem exatamente três saídas válidas — e nenhuma outra. Mesmo assim, o padrão comum é pedir para o modelo escrever a resposta, gerar alguns tokens e procurar algo reconhecível na string.

Isso falha de duas formas ao mesmo tempo. Primeiro, é lento: o generate() executa um forward pass sequencial por token, então pedir oito tokens custa cerca de oito vezes o custo de ler a resposta direto. Segundo, é instável: um modelo pequeno responde coisas como “Claro! Isso parece um problema de cobrança.” ou “Cobrança/Conta”, ou uma categoria que você nunca definiu. Cada uma dessas respostas exige uma regra de fallback, e cada fallback é um ponto de acúmulo de erro.

A correção: parar de gerar e começar a pontuar. Rode um único forward pass, leia a distribuição do próximo token e restrinja a decisão aos IDs de token dos seus rótulos candidatos. A resposta fica estruturalmente impossível de errar — e você ganha um score de confiança calibrado de brinde.

Requisitos e cenário de teste

ComponenteMínimoRecomendado
ModeloQwen2.5-0.5B-InstructQualquer SLM com tokenizer BPE
FrameworkHugging Face Transformerstransformers + accelerate
HardwareCPU (lento)Mac M-series / GPU
Python3.9+3.11+
Conhecimento prévioBásico de PyTorchNoção de logits e softmax
Para nivelar o campo, todos os benchmarks usam Qwen2.5-0.5B-Instruct em float16 via Transformers, em um MacBook Air M2 com 24 GB de RAM.

Comece preparando o ambiente:

pip install torch transformers accelerate

A versão ingênua: gerar texto e analisar depois

O código abaixo roda 600 tickets de exemplo e, para cada um, gera até 8 tokens e faz uma busca de substring contra a lista de rótulos:

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=torch.float32)
model.eval()

LABELS = ["billing", "technical", "account"]
tickets = [
    "My card was charged twice for the same invoice.",
    "The mobile app crashes whenever I open the settings page.",
    "I need to change the email address on my profile.",
] * 200

def build_prompt(ticket):
    messages = [
        {"role": "system", "content": "You classify support tickets. Answer with exactly one of: billing, technical, account."},
        {"role": "user", "content": f"Ticket: {ticket}\nCategory:"},
    ]
    return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

# Geração livre: o modelo escreve e o código procura o rótulo na string
predictions = []
for prompt in [build_prompt(t) for t in tickets]:
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
    with torch.inference_mode():
        output = model.generate(**inputs, max_new_tokens=8, do_sample=False)
    generated = output[0, inputs["input_ids"].shape[1]:]
    text = tokenizer.decode(generated, skip_special_tokens=True).strip().lower()
    predictions.append(next((lbl for lbl in LABELS if lbl in text), "UNPARSED"))

O resultado no teste do autor: os 600 tickets levaram 134 segundos, e a versão livre só “funcionou” por sorte — qualquer resposta criativa quebra o parser.

A versão otimizada: scoring restrito

Agora, em vez de gerar, o código faz um único forward pass, lê os logits do próximo token e aplica softmax apenas sobre os IDs dos rótulos:

# IDs do PRIMEIRO token de cada rótulo (veja o cuidado com tokenização abaixo)
label_first_ids = [tokenizer.encode(label, add_special_tokens=False)[0] for label in LABELS]
assert len(set(label_first_ids)) == len(LABELS), "Rótulos compartilham o primeiro token"
label_first_ids = torch.tensor(label_first_ids, device=model.device)

predictions, confidences = [], []
for prompt in [build_prompt(t) for t in tickets]:
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
    with torch.inference_mode():
        logits = model(**inputs).logits[0, -1, :]   # distribuição do próximo token
    probs = torch.softmax(logits[label_first_ids].float(), dim=-1)  # só os rótulos
    best = int(probs.argmax())
    predictions.append(LABELS[best])
    confidences.append(float(probs[best]))

O ganho é duplo. O benchmark do autor apontou ~30% menos tempo (contra os 134s da versão livre) e 0 respostas não analisáveis em 600 — por construção, não por sorte. Além disso, o softmax restrito vira um score de confiança: você pode rotear qualquer predição abaixo de um limiar (comece com 0.6) para uma fila humana, em vez de deixar um rótulo de baixa confiança seguir pelo fluxo.

O detalhe que pega todo mundo: tokenização

A maioria dos tokenizers byte-level BPE trata " billing" (com espaço) e "billing" (sem espaço) como tokens distintos. Codifique a variante que o modelo realmente emitiria depois do seu prompt. Como o template de chat termina com uma quebra de linha antes da resposta, o próximo token não tem espaço inicial — por isso o código usa encode(label), e não encode(" " + label). Trocar isso faz o script rodar, mas você acaba pontuando três tokens que o modelo jamais emitiria.

Se dois rótulos compartilham o primeiro token (ex.: refund_request e refund_status), o assert dispara. Nesse caso, renomeie os rótulos para tokens únicos (como A, B, C) com uma legenda no prompt, ou pontue a sequência completa do rótulo em vez do primeiro token.

Por que isso funciona (a explicação por trás do código)

  • logits[0, -1, :] dá a distribuição não normalizada do próximo token — exatamente o que o generate() usaria depois, mas que é desnecessário quando a resposta é uma de três strings conhecidas.
  • Indexar em label_first_ids e aplicar argmax torna uma resposta fora do vocabulário estruturalmente impossível: o modelo deixa de ter liberdade criativa na formatação.
  • O softmax sobre os logits restritos é o score de confiança que permite roteamento por limiar.

Prós e contras da técnica

O que você ganha:

  • Respostas malformadas tornam-se estruturalmente impossíveis;
  • Menos forward passes → até ~30% mais rápido;
  • Score de confiança calibrado de graça;
  • Menos regras de fallback para manter;
  • Permite usar um SLM de 0.5B como opção real de produção.

⚠️ O que você não ganha:

  • Não serve para respostas abertas — só para conjuntos de saída fixos;
  • Exige atenção à tokenização (primeiro token de cada rótulo);
  • Rótulos que compartilham o primeiro token exigem tratamento extra.

Casos de uso reais

  • Roteamento de tickets: classificar suporte em billing/technical/account sem intervenção humana.
  • Triagem de e-mails: encaminhar para o departamento certo com limiar de confiança.
  • Moderação de conteúdo: aprovar/reprovar/encaminhar com score calibrado.
  • Extração de intenção em chatbots com vocabulário controlado.
  • Detecção de fraude em primeiro estágio, roteando casos duvidosos para revisão.

Troubleshooting

  • “O script roda, mas pontua tokens errados” → verifique espaço inicial: use encode(label), não encode(" " + label), depois do template de chat.
  • “AssertionError: labels share a first token” → renomeie rótulos para tokens únicos (A/B/C) ou pontue a sequência completa.
  • “O modelo ainda gera texto criativo” → você está chamando generate(); a técnica exige ler logits diretamente.
  • “Confiança baixa em tudo” → revise o prompt e confirme que os rótulos estão claros; considere rotear para humano.
  • “Roda devagar na CPU” → é esperado; o benchmark do autor usou 24 GB de RAM em Apple Silicon — em CPU, avalie lotes maiores ou um modelo menor.

FAQ

O que é um SLM? Um modelo de linguagem pequeno, como o Qwen2.5-0.5B, com bilhões (ou menos) de parâmetros, adequado a tarefas estreitas e de baixo custo.

Quando devo usar scoring restrito em vez de geração? Sempre que a tarefa tiver um conjunto fixo e conhecido de respostas válidas — classificação, roteamento, triagem.

Isso funciona com qualquer modelo? Sim, com qualquer modelo causal que exponha os logits do próximo token. O exemplo usa Qwen, mas a técnica é geral.

Como escolher o limiar de confiança? Comece em 0.6 como referência e ajuste com base nos erros observados; abaixo do limiar, envie para revisão humana.

Preciso de GPU? Não obrigatoriamente. O autor usou um MacBook Air M2, mas em CPU o processo fica consideravelmente mais lento.

O que esperar daqui para frente

À medida que os SLMs ficam melhores e mais baratos, a diferença entre um bom e um mau uso deles deixa de ser o modelo e passa a ser o código ao redor. Técnicas como o scoring restrito — que impõem um contrato de saída em vez de tratar o modelo como chatbot — são o que transformam um modelo pequeno de “compromisso” em “resposta óbvia” para automação estreita. Quem dominar isso em 2026 vai rodar automações de classificação em produção por uma fração do custo de uma API de modelo grande.



Descubra mais sobre noticiAI

Assine para receber nossas notícias mais recentes por e-mail.

R
Sobre o autorRedação Noticiai

Equipe editorial dedicada a explicar inteligência artificial com clareza, independência e contexto.