Choice, score e noul: quais são os tipos de pergunta do Laya e quando usar cada um?

diagrama mostrando os tipos de pergunta do Laya: choice, score e noul
Resposta rápida

Os tipos de pergunta do Laya são três primitivas tipadas: choice, score e noul, todas avaliadas sobre um mesmo estado em uma única passagem de inferência. O choice escolhe uma chave dentro de um dicionário de critérios e devolve a distribuição entre as opções mais a confiança. O score posiciona o estado numa rubrica ordinal (0, 1, 2 e por aí vai) e devolve o nível esperado com a distribuição. O noul é booleano e devolve P(true) de 0.0 a 1.0, com P(false) = 1 – P(true). A acurácia reportada por primitiva é noul 0.857, choice 0.733 e score 0.723

Fala aí, beleza? O Laya não escreve texto, ele responde perguntas tipadas sobre um estado

E isso muda TUDO na hora de desenhar as perguntas

Em vez de pedir pro modelo gerar um JSON e torcer pra ele vir bonitinho, você declara o que quer saber e recebe probabilidades calibradas de volta, sem nenhum token gerado. São três primitivas: choice, score e noul, todas avaliadas em um único forward pass sobre o mesmo estado (um texto, um e-mail, um ticket ou um documento JSON)

Escolher a primitiva errada é o que quebra o design das perguntas, e é um erro silencioso: o modelo responde, o número sai, só que ele não significa o que você acha que significa

Contexto de maturidade antes de seguir: o modelo é da Convai Innovations, que publica os pesos pela própria conta no Hugging Face, em huggingface.co/convaiinnovations/laya

Já o código vive numa conta pessoal no GitHub, o github.com/NandhaKishorM/laya, então não estranha ver os dois nomes: pesos pela empresa, repositório nessa conta. A licença é Apache 2.0 e o release mais recente listado no GitHub é o v0.3.5

Bora entender cada tipo? 🙂

Choice, score e noul lado a lado

Primitiva Que pergunta responde Formato da saída Como se lê Acurácia reportada
choice Qual das opções é esta? Chave escolhida + distribuição por opção + confiança answers['department']['choice'] 0.733
score Quanto disso tem aqui? Nível esperado + distribuição sobre os níveis + confiança answers['urgency']['score'] 0.723
noul É verdade ou não? P(true) de 0.0 a 1.0, com P(false) = 1 – P(true) answers['churn_risk']['noul'] 0.857
Formação Claude Code
Formação Recomendada

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 45min

Repara que o noul é o mais preciso dos três, com 0.857

Um aviso pra não te confundir mais pra frente: esses três números são acurácia POR primitiva, e não dá pra comparar direto com os números do benchmark typed-decisions que aparecem lá no fim do post. São recortes diferentes, medidos de jeito diferente

Faz sentido: ele só precisa separar duas margens de um eixo, enquanto o choice tem que distribuir massa de probabilidade entre várias opções que competem entre si e o score ainda carrega a ordem dos níveis por cima disso

Quanto mais espaço de resposta, mais lugar pro modelo errar, né?

Quando usar choice: roteamento e classificação por categoria

O choice escolhe UMA opção dentro de um dicionário de critérios

O exemplo do README é um ticket de suporte com a pergunta department e critérios billing, technical, sales e other. Clássico caso de roteamento: chegou a mensagem, pra qual time ela vai?

Só que a graça não está no rótulo vencedor

A saída traz a distribuição por opção e um score de confiança calibrado, e é aí que mora o valor real. Se billing deu 0.48 e technical deu 0.45, você não tem uma classificação, você tem um empate disfarçado. Esse é o ticket que merece revisão humana em vez de cair direto na fila errada

Com o rótulo cru você nunca enxergaria isso, porque billing é billing e pronto

Dois detalhes do v0.3.5 que vale ter na cabeça ao montar dicionários de opção:

  • o tratamento da descrição das opções mudou: agora só None e string vazia contam como ausência de descrição. Antes, qualquer valor falsy caía pra chave nua
  • o crash em perguntas choice com uma única opção foi corrigido nesse mesmo release

Ou seja: se você gera os critérios dinamicamente e às vezes o dicionário chega com uma opção só, atualize antes de sair culpando o seu código 😀

Quando usar score: rubricas ordinais com níveis

Use score quando as opções têm ordem natural

O score posiciona o estado em uma rubrica ordinal (níveis 0, 1, 2, …) e devolve o nível esperado, a distribuição sobre os níveis e a confiança. No exemplo do README, a pergunta é urgency, com níveis do tipo not urgent, soon e critical deadline or blocking issue

E qual a diferença prática de forçar isso num choice?

No choice, as três opções são estranhas entre si: errar de not urgent pra critical custa o mesmo que errar de soon pra critical. No score, a ordem existe, e a distribuição sobre os níveis te deixa tratar o meio termo de forma decente: um caso que fica espalhado entre soon e critical é um caso que sobe de prioridade, mesmo sem cravar o topo da rubrica

É isso que te permite definir limiar em cima de um eixo contínuo em vez de contar votos

Tome cuidado com um ponto: score é a primitiva com a MENOR acurácia reportada (0.723)

Então rubrica curta e com níveis bem descritos ajuda. Quanto mais parecido um nível é do vizinho, mais difícil pro modelo separar os dois

Quando usar noul: perguntas booleanas com probabilidade calibrada

O noul é a pergunta sim/não direta, tipo o churn_risk do exemplo do ticket

Mas atenção, porque aqui tem uma pegadinha de leitura: a saída não é um booleano

É uma probabilidade contínua, P(true) entre 0.0 e 1.0, com P(false) = 1 – P(true) por construção. Você recebe 0.62, não recebe True. Quem transforma isso em decisão é você, com o limiar que fizer sentido pro seu fluxo

E por que a calibragem importa tanto justamente aqui?

Porque o treinamento usa aprendizado por reforço contra strictly proper scoring rules, as tais regras de pontuação estritamente próprias, o que a documentação do modelo chama de RLCD. Traduzindo: o esquema de recompensa é montado de um jeito em que reportar a probabilidade honesta é o caminho de maior recompensa. Chutar 0.99 pra parecer confiante sai mais caro do que dizer 0.6 quando é 0.6

Se você conhece a ideia de um previsor de tempo bem calibrado (a chance que ele anuncia bate com a frequência real de chuva nos dias parecidos), é exatamente esse espírito

Dica de design: prefira noul a um choice de duas opções quando a pergunta é um gate

Você já tem um eixo contínuo pronto pra virar limiar, sem precisar comparar duas massas de probabilidade na mão

Como declarar as perguntas e ler as respostas no Laya

O fluxo é curtinho, se liga:

  1. Instale o pacote pelo PyPI
pip install laya
  1. Importe o Router, que é o ponto de entrada recomendado do pacote
from laya import Router
  1. Instancie com preload. O Router detecta script e idioma em menos de 1 ms e despacha pro checkpoint ideal
router = Router(preload=True)

O erro comum deste passo: instanciar dentro do handler da requisição e pagar carregamento toda hora. Suba o Router uma vez, no start da aplicação

  1. Monte o dicionário de perguntas, com o tipo, a instrução e os critérios em cada entrada
questions = {
    "department": {
        "type": "choice",
        "instruction": "Qual time deve receber este ticket?",
        "criteria": {
            "billing": "cobrança, fatura, reembolso",
            "technical": "erro, bug, indisponibilidade",
            "sales": "proposta, upgrade, novo contrato",
            "other": "qualquer outro assunto",
        },
    },
    "urgency": {
        "type": "score",
        "instruction": "Qual o nível de urgência deste ticket?",
        "criteria": {
            0: "not urgent",
            1: "soon",
            2: "critical deadline or blocking issue",
        },
    },
    "churn_risk": {
        "type": "noul",
        "instruction": "Este cliente demonstra risco de cancelar?",
    },
}

O erro comum deste passo: escrever o tipo errado na entrada (declarar choice numa pergunta que é gradação, por exemplo) e depois tentar ler por outra chave de tipo lá na saída. A leitura é sempre por tipo, então tipo trocado vira KeyError ou, pior, vira número que não quer dizer nada

  1. Chame o predict passando o estado e as perguntas. O estado pode ser um texto, um e-mail, um ticket ou um documento JSON
answers = router.predict(state, questions)
  1. Leia as respostas por chave e por tipo
print(answers["department"]["choice"])
print(answers["urgency"]["score"])
print(answers["churn_risk"]["noul"])

O padrão é sempre answers[<pergunta>][<tipo>], sem exceção

  1. Visualize as distribuições rodando o servidor de demonstração em examples/server.py, que renderiza choice, score e noul como barras de 0 a 100 e expõe uma API JSON. Ele depende do grupo opcional:
pip install "laya[server]"

O erro comum deste passo: rodar o exemplo sem o grupo opcional de servidor instalado e tomar erro de import achando que o pacote veio quebrado

Pra quem está começando do zero com a ideia de tipo e declaração antes de cair no dicionário de perguntas, este vídeo do canal mostra tipos de dados e declaração de variáveis na prática:

Por que seu choice piora quando o dicionário de opções cresce

Sintoma: você monta uma classificação com dezenas de rótulos e a acurácia despenca, mesmo com critérios bem escritos

Causa: as opções dividem um orçamento de 192 a 256 tokens. Com 77 opções, sobra coisa de 3 a 4 tokens por candidato, o que é basicamente nada pra descrever um rótulo. A degradação já começa a aparecer acima de umas 20 opções

Evidência: no Banking77 (77 rótulos) o Laya marcou 0.425, contra 0.870 do Jev, que é o baseline usado como comparação na própria documentação do modelo. Não é sutil, é metade do desempenho

Solução e prevenção: hierarquia em dois passos, o famoso coarse-to-fine

Quebre o espaço de rótulos: primeiro um choice grosso com poucos grupos, depois um segundo choice fino só dentro do grupo vencedor. Cada passagem fica com um dicionário pequeno, cada opção ganha tokens suficientes pra se descrever, e você para de brigar com o orçamento

Regra de bolso que dá pra tirar disso: se o seu choice passou de umas 20 opções, ele virou dois choice

O que esperar de acurácia antes de desenhar suas perguntas

Aqui vai a parte honesta, porque ela muda o seu planejamento

A acurácia zero-shot no benchmark typed-decisions é 0.362, ou seja, perto do acaso. Aquele 0.766 que circula por aí exige fine-tuning no split de treino do próprio benchmark. Não é plug and play, e fingir o contrário só gera frustração depois

Com o checkpoint ajustado, os resultados por fluxo ficam assim:

  • processamento de notas fiscais: 0.804
  • incidentes de segurança: 0.766
  • atendimento ao cliente: 0.764
  • observabilidade de traces de agente: 0.730

Do lado do custo, é onde a coisa fica interessante: a latência declarada é de 33 ms para uma pergunta e 7,2 ms por pergunta em lote, medida em GPU T4

Por checkpoint, em Tesla T4, dá cerca de 39,5 ms no modelo em inglês (ModernBERT-large, 421M de parâmetros e 512 tokens de contexto) e cerca de 32,8 ms no multilíngue (mmBERT-base, 322M de parâmetros, 1024 tokens e mais de 100 idiomas)

Veredito: faz sentido pra decisão tipada de alto volume, aquele fluxo que roda milhares de vezes por dia e precisa de número calibrado, não de texto bonito. Desde que você assuma o fine-tuning como parte do projeto, e não como um extra opcional

Se o plano era sair do zero-shot pra produção direto, melhor ajustar a expectativa agora 😛

Conclusão

O critério de escolha entre os tipos de pergunta do Laya dá pra resumir em três linhas:

  • pergunta booleana vira noul, e você trata a saída como probabilidade, não como sim/não
  • categoria sem ordem vira choice, de preferência curto (e hierárquico quando o espaço de rótulos for grande)
  • gradação com ordem natural vira score, com rubrica curta e níveis bem descritos

Próximo passo é simples: instala com pip install laya, reproduz o exemplo do ticket de suporte com department, urgency e churn_risk, e abre o examples/server.py pra ver as distribuições em barra em vez de olhar só o rótulo vencedor

Ver a massa de probabilidade se espalhando entre as opções é o que faz a ficha cair sobre qual primitiva você deveria ter usado

Os pesos estão em huggingface.co/convaiinnovations/laya e o código no repositório do projeto

Até o próximo post! 😀

Perguntas frequentes

Qual dos tipos de pergunta do Laya tem a maior acurácia reportada?

É o noul, com 0.857. Faz sentido: ele só precisa separar duas margens de um eixo (P(true) e P(false) = 1 – P(true)), enquanto choice (0.733) e score (0.723) lidam com mais opções competindo entre si. Só não confunda esses valores com os do benchmark typed-decisions: são recortes diferentes, reportados à parte.

Dá pra usar o tipo choice do Laya com muitas opções, tipo 77 rótulos?

Não é o ponto forte dessa primitiva. As opções dividem um orçamento de 192 a 256 tokens, e com 77 rótulos sobram só uns 3 a 4 tokens por candidato, o que derrubou o resultado no Banking77 pra 0.425, contra 0.870 do Jev, o baseline usado na comparação da documentação. A degradação já aparece acima de uns 20 rótulos, e a saída recomendada é uma hierarquia em dois passos (coarse-to-fine) em vez de um choice gigante.

O tipo noul do Laya devolve um true/false ou uma probabilidade?

Devolve uma probabilidade, não um booleano. É P(true) entre 0.0 e 1.0, com P(false) = 1 – P(true) por construção, então quem decide o limiar pra virar sim ou não é você.

Como fica a leitura de uma pergunta do tipo score no dicionário de resposta do Laya?

Vem pela chave da pergunta seguida de ‘score’, tipo answers[‘urgency’][‘score’]. Dentro dela está o nível esperado da rubrica ordinal, a distribuição sobre todos os níveis e a confiança calibrada.

O que o release v0.3.5 mudou no tipo choice do Laya?

Corrigiu um crash em perguntas choice com uma única opção e mudou o tratamento de descrição das opções: agora só None e string vazia contam como ‘sem descrição’, antes qualquer valor falsy caía pra chave nua. O mesmo release também ajustou o reconhecimento de idioma armênio e corrigiu um NameError no README.

Precisa treinar o Laya pra usar os três tipos de pergunta (choice, score e noul)?

Sem fine-tuning, a acurácia zero-shot no benchmark typed-decisions fica em 0.362, perto do acaso (esse recorte é diferente da acurácia por primitiva, que é reportada à parte e não serve de comparação direta). O número de 0.766 só aparece com fine-tuning no split de treino do próprio benchmark, e com o checkpoint ajustado os resultados variam por fluxo: 0.804 em notas fiscais, 0.766 em incidentes de segurança, 0.764 em atendimento ao cliente e 0.730 em observabilidade de traces de agente.




Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Subscribe
Notify of
guest

0 Comentários
Oldest
Newest Most Voted

Formações

Formação Vibe Coding

Formação Vibe Coding

Do Prompt ao Produto: Crie Software Real com IA

  • 474 aulas
  • 20 projetos
  • 39h 27min

Blog | Mais populares