Como versionar os critérios de decisão do Laya sem quebrar o histórico

Fluxo para versionar critérios do Laya com arquivo de criteria e checkpoint
Resposta rápida

Versionar critérios do Laya é uma prática de governança do seu time, não um recurso do modelo. Como o espaço de respostas é definido na requisição, o critério vive no seu código. Então: centralize cada decisão num arquivo com os criteria e o rótulo other, dê uma versão explícita e um hash do enunciado, grave versão, enunciado, checkpoint, resposta, answer_confidence e limiar junto de cada resultado, recalibre o limiar a cada versão e compare antes e depois reprocessando a mesma amostra com predict_batch, nunca comparando períodos diferentes

Fala aí, beleza? No Laya, mudar a definição de um rótulo é mudar a pergunta

E aí o seu histórico passa a misturar duas perguntas diferentes debaixo do mesmo nome, sem ninguém perceber 😛

Esse post é pra quem já roda decisões do Laya em produção e precisa ajustar critérios sem perder a capacidade de comparar resultados

Rapidinho, pra situar: o Laya é o modelo de decisão de pesos abertos da Convai Innovations, lançado em 18 de setembro de 2026 sob a licença Apache 2.0

Você entrega um estado (e-mail, ticket, documento JSON, tabuleiro de jogo) e algumas perguntas tipadas, e ele devolve respostas tipadas com uma probabilidade pra cada opção, numa única passada, sem gerar texto

Só que tem um detalhe importante: o Laya não guarda versão de critério pra você

Versionar é governança do SEU sistema, não um recurso do modelo

Bora ver como fazer isso direito?

O que você precisa antes de começar:

  • Laya instalado com pip install -U laya (isso também instala o comando laya pra teste local)
  • Uso via Python com laya.load, agent.predict, agent.predict_batch ou agent.decide com schema (JSON schema ou modelo pydantic)
  • Saber qual checkpoint está em uso: convaiinnovations/laya (base em inglês), convaiinnovations/laya-multilingual (pro que não é inglês) ou convaiinnovations/laya-typed-decisions (ajustado pra decisões tipadas), ou se é o router do repositório do Laya que escolhe o checkpoint por requisição
  • Um lugar onde as decisões já são gravadas: banco, log, data warehouse, o que o time já usa

Agora o porquê de tudo isso

No Laya, o espaço de respostas é definido no momento da requisição, então um schema novo não precisa de retreino

Isso é massa pra agilidade, mas tem um efeito colateral: o critério vive no seu código, não no modelo

Se ninguém versiona esse código como dado, a pergunta muda e o histórico não fica sabendo…

Se você já passou por isso com outro modelo do mesmo estilo, a lógica é bem parecida com a de versionar o schema de decisão do Jev

Passo a passo para versionar os critérios de decisão:

  1. Extraia as perguntas para um arquivo único por decisão

Cada decisão ganha um lugar só, com as perguntas, os criteria no formato {rótulo: definição de uma linha} e o rótulo other obrigatório nas perguntas choice

A estrutura abaixo é do seu time, não da API: depois você converte pro formato de requisição que a sua integração já monta

# criterios/triagem_ticket.py

TRIAGEM_TICKET = {
    "decisao": "triagem_ticket",
    "versao": 1,
    "checkpoint": "convaiinnovations/laya",
    "perguntas": [
        {
            "nome": "categoria",
            "tipo": "choice",
            "descricao": "Qual é a categoria principal deste ticket?",
            "criteria": {
                "cobranca": "Dúvida ou problema com fatura, pagamento ou reembolso",
                "acesso": "Cliente não consegue entrar na conta ou recuperar a senha",
                "bug": "Relato de erro ou comportamento inesperado do produto",
                "other": "Qualquer caso que não se encaixe nos rótulos acima",
            },
        }
    ],
}

O erro comum deste passo: definições espalhadas pelo código, uma string num serviço, outra num notebook, outra num prompt de teste

Aí ninguém sabe qual enunciado rodou de verdade em produção

  1. Atribua uma versão explícita e um hash do conteúdo do enunciado

A versão é o número que humanos leem, o hash é o que pega mudança que passou batida

import hashlib
import json

def hash_enunciado(decisao: dict) -> str:
    conteudo = {
        "perguntas": decisao["perguntas"],
        "checkpoint": decisao["checkpoint"],
    }
    bruto = json.dumps(conteudo, sort_keys=True, ensure_ascii=False)
    return hashlib.sha256(bruto.encode("utf-8")).hexdigest()

# hashes já publicados, por versão (fica versionado no git junto dos critérios)
HASHES_PUBLICADOS = {
    ("triagem_ticket", 1): "cole_aqui_o_hash_da_v1",
}

def validar_versao(decisao: dict) -> None:
    chave = (decisao["decisao"], decisao["versao"])
    atual = hash_enunciado(decisao)
    publicado = HASHES_PUBLICADOS.get(chave)
    if publicado and publicado != atual:
        raise ValueError(
            f"{chave} mudou de conteúdo sem mudar de versão. Suba a versão."
        )

Roda o validar_versao no CI e pronto: ninguém mexe numa definição e esquece de subir a versão

O erro comum deste passo: versionar só o nome da pergunta (categoria_v2) e deixar as definições dos rótulos mudarem livremente embaixo dele

  1. Grave o contexto completo junto de cada resultado

Com cada decisão vão: versão, hash (referência imutável ao enunciado), checkpoint, resposta completa, answer_confidence e o limiar vigente naquele momento

from datetime import datetime, timezone

import laya

from criterios.triagem_ticket import TRIAGEM_TICKET

agent = laya.load(TRIAGEM_TICKET["checkpoint"])

def decidir_e_gravar(estado, perguntas_req, limiar, gravar):
    resultado = agent.predict(estado, perguntas_req)

    registro = {
        "decisao": TRIAGEM_TICKET["decisao"],
        "versao": TRIAGEM_TICKET["versao"],
        "hash_enunciado": hash_enunciado(TRIAGEM_TICKET),
        "checkpoint": TRIAGEM_TICKET["checkpoint"],
        "estado": estado,
        # resposta inteira, incluindo o answer_confidence e as probabilidades
        "resultado": resultado,
        "limiar": limiar,
        "gravado_em": datetime.now(timezone.utc).isoformat(),
    }
    gravar(registro)
    return resultado

Se o enunciado completo for grande pra repetir em toda linha, grava o hash e mantém uma tabela à parte com hash e enunciado (que nunca é editada, só recebe linha nova)

E se a sua integração processa em lote? Aí entra o agent.predict_batch(states, questions), e a regra é a mesma: um registro por estado, todos com a versão, o hash e o checkpoint daquele lote

def decidir_lote_e_gravar(estados, perguntas_req, limiar, gravar):
    resultados = agent.predict_batch(estados, perguntas_req)

    for estado, resultado in zip(estados, resultados):
        gravar({
            "decisao": TRIAGEM_TICKET["decisao"],
            "versao": TRIAGEM_TICKET["versao"],
            "hash_enunciado": hash_enunciado(TRIAGEM_TICKET),
            "checkpoint": TRIAGEM_TICKET["checkpoint"],
            "estado": estado,
            "resultado": resultado,
            "limiar": limiar,
            "gravado_em": datetime.now(timezone.utc).isoformat(),
        })
    return resultados

Esse mesmo predict_batch é o que vai fazer a comparação entre versões lá no passo 6

O erro comum deste passo: guardar só o rótulo vencedor

Sem a confiança e sem o limiar, você não consegue nem responder por que aquela decisão foi automática e não foi pra revisão humana

  1. Defina o que conta como mudança de versão

Sobe a versão quando:

  • muda a definição de qualquer rótulo
  • inclui ou remove rótulo
  • muda a escala de uma pergunta score
  • troca o checkpoint
  • muda a descrição da pergunta

Se liga nisso: nas perguntas choice, a descrição vira a instrução da pergunta, e uma boa descrição é o que torna a decisão precisa

Ou seja, reescrever uma definição "só pra ficar mais clara" muda o comportamento do modelo

E tem mais: a documentação recomenda manter perguntas choice abaixo de cerca de 20 opções, porque elas dividem um orçamento fixo de 256 tokens da cabeça e um espaço de rótulos grande derruba a precisão

Então adicionar rótulo não mexe só na pergunta, mexe também na precisão das outras opções

O erro comum deste passo: achar que reescrever uma definição é cosmético

Não é! É uma pergunta nova

  1. Recalibre o limiar a cada versão

O Laya é treinado com reinforcement learning contra regras de pontuação estritamente próprias (RLCD), o que força probabilidades calibradas e não só rótulos corretos

Mas o limiar de confiança é política sua, escolhida a partir da precisão medida naquela cobertura nos seus dados, não uma propriedade do modelo

Pega uma amostra rotulada por humanos, roda na versão nova e mede:

def precisao_por_cobertura(itens, limiares):
    # itens: lista de (confianca, acertou) da MESMA versão
    linhas = []
    total = len(itens)
    for limiar in limiares:
        aceitos = [acertou for conf, acertou in itens if conf >= limiar]
        if not aceitos:
            continue
        cobertura = len(aceitos) / total
        precisao = sum(aceitos) / len(aceitos)
        linhas.append((limiar, cobertura, precisao))
    return linhas

Aí o time escolhe o limiar que entrega a precisão que o negócio exige, e grava esse limiar junto da versão

O erro comum deste passo: herdar o limiar da versão anterior

Se a pergunta mudou, a distribuição de confiança mudou junto, e o limiar antigo não diz mais nada

  1. Compare antes e depois com a mesma amostra, não com períodos diferentes

Aqui o predict_batch brilha: em vez de chamar agent.predict estado por estado, tu pega uma amostra de estados históricos e roda o lote inteiro nas duas versões, pra comparar os resultados lado a lado

estados = carregar_amostra_historica()  # sua função, tamanho da amostra é decisão do time

res_v1 = agent.predict_batch(estados, perguntas_v1)
res_v2 = agent.predict_batch(estados, perguntas_v2)

# agora sim: mesmo estado, duas versões, lado a lado
for estado, a, b in zip(estados, res_v1, res_v2):
    comparar(estado, a, b)  # onde mudou? mudou pra melhor segundo o rótulo humano?

(os parâmetros perguntas_v1 e perguntas_v2 saem do mesmo arquivo de critérios, em versões diferentes, no formato que a sua integração já usa)

É a mesma cabeça de quem faz backup antes de atualizar sem quebrar fluxos: você quer poder olhar pro antes e pro depois com a mesma régua

O erro comum deste passo: abrir o dashboard de agosto, abrir o de setembro e declarar vitória haha

Por que comparar métricas de antes e depois de mudar um rótulo dá conclusão errada?

Essa é a armadilha mais comum de quem já está em produção, então vale destrinchar

O sintoma:

Logo depois de ajustar os critérios, a taxa de um rótulo (ou do other) sobe ou cai

E o time conclui que o mundo mudou, ou que a versão nova é MUITO melhor

A causa:

A pergunta mudou

Quando você reescreve uma definição ou inclui um rótulo, a probabilidade se redistribui entre as opções, e casos que antes caíam em bug agora podem cair no rótulo novo ou no other

Por cima disso, a população de estados também pode ter mudado no período: campanha, sazonalidade, produto novo e por aí vai

E a confiança não salva você aqui: ela ordena decisões, não prova que estão corretas

Confiança alta na versão nova não significa acerto maior

A solução:

  • compare métricas só dentro da mesma versão
  • pra decidir se a versão nova é melhor, faça comparação pareada: os mesmos estados rodados nas duas versões, avaliados contra rótulo humano

Como prevenir:

  • versão obrigatória em todo registro (registro sem versão nem entra no banco)
  • bloqueio de relatório que misture versões: se a consulta pega mais de uma versão, o relatório quebra ou separa por versão automaticamente

Parece burocracia, mas é o tipo de coisa que evita uma reunião inteira discutindo um número que não quer dizer nada 🙂

Quando criar uma nova versão de critério (e quando não precisa)?

Resumão por tipo de pergunta:

Situação Tipo Nova versão? Por quê
Triagem de ticket ganha um rótulo novo choice Sim O espaço de respostas mudou e os rótulos dividem o mesmo orçamento da cabeça
Definição de um rótulo foi reescrita choice Sim A descrição vira a instrução da pergunta
Escala de 0 a 3 vira 0 a 4 score Sim O valor é mínimo + argmax, então o mesmo número passa a significar outra coisa
Limiar de decisão sai de 0.5 pra outro valor noul Não muda a pergunta Muda a política: registre o limiar novo
Enum ou modelo pydantic do decide mudou schema Sim Enum/Literal viram perguntas choice, inteiro limitado vira score, booleano vira noul
Troca de checkpoint (ex: passar pro multilingual) todos Sim É outro modelo respondendo a mesma pergunta

Choice: triagem de ticket

Incluiu fraude na triagem? Nova versão

Reescreveu a definição de acesso pra incluir autenticação em dois fatores? Nova versão também, mesmo que o nome do rótulo seja o mesmo

Score: escala ordinal

No score, o valor retornado é mínimo + argmax

Se a escala era 0 a 3 e virou 0 a 4, um "3" de antes e um "3" de agora não são a mesma coisa, então comparar média de score entre as duas é misturar régua

Noul: sim ou não

O noul devolve P(verdadeiro) de 0.0 a 1.0

Mudar o ponto de corte (sair de 0.5 pra outro valor) não muda a pergunta, mas muda o que vira "sim" no seu sistema

Não precisa subir a versão do enunciado, mas o limiar tem que estar gravado em cada registro, senão ninguém entende por que a taxa de "sim" mudou

Schema no decide

Se você usa agent.decide(state, schema=...) com pydantic ou JSON schema, o schema É o critério

Mexeu num Enum, num Literal, num limite de inteiro? Isso muda as perguntas geradas, então o schema entra no mesmo esquema de versão e hash

Troca de checkpoint

Passar do convaiinnovations/laya pro convaiinnovations/laya-multilingual (ou pro laya-typed-decisions) é trocar quem responde

Mesmo com enunciado idêntico, é nova versão, e se o router escolhe o checkpoint por requisição, grave o checkpoint usado em cada linha

Loop engineering: o mesmo princípio aplicado a vibe coding

Essa ideia de mudar uma coisa de cada vez, de forma controlada, sem quebrar o que já funciona, não é exclusividade do Laya

Pra começar do zero com loop engineering, este vídeo do canal mostra como a evolução do vibe coding passa por ciclos controlados que fazem a IA não quebrar nada

Próximo passo: auditar suas decisões atuais

Recapitulando: o Laya define o espaço de respostas na requisição, então o critério é código seu, e código seu precisa de versão

A ação concreta pra hoje é essa:

  1. levante todas as perguntas que já rodam em produção
  2. atribua versão 1 ao enunciado atual de cada uma, com hash
  3. comece a gravar versão, hash, checkpoint, resposta, answer_confidence e limiar junto de cada resultado, a partir de hoje
  4. só então mexa nos critérios

Tome cuidado com a ordem: se você ajustar o critério antes de começar a gravar a versão, perde a fronteira entre o antes e o depois

E histórico sem versão não se recupera depois, não tem script mágico que adivinhe qual enunciado rodou em cada linha…

Até o próximo post! 😀

Perguntas frequentes

O Laya guarda histórico de versões dos critérios sozinho?

Não, isso não é um recurso do modelo. Versionar critérios do Laya é responsabilidade do seu sistema: você extrai as perguntas pra um arquivo, atribui a versão e grava tudo junto com o resultado. O modelo só recebe o espaço de respostas na hora da requisição.

Preciso retreinar o Laya quando mudo a definição de um critério?

Não. O espaço de respostas é definido no momento da requisição, então um schema novo não exige retreino. Isso agiliza mudanças, mas também é o motivo pelo qual o histórico pode misturar perguntas diferentes se ninguém versionar o enunciado.

Só o número de versão já resolve, ou preciso do hash também?

O número de versão é o que humanos leem, mas ele não pega mudança silenciosa: alguém pode editar uma definição de rótulo e esquecer de subir a versão. O hash do conteúdo do enunciado serve exatamente pra travar isso no CI antes que vire produção.

Trocar o checkpoint do Laya sem mexer nas perguntas conta como mudança de versão?

Sim. O checkpoint (convaiinnovations/laya, laya-multilingual ou laya-typed-decisions) faz parte do conteúdo usado pra gerar o hash do enunciado. Trocar de checkpoint muda o comportamento da decisão mesmo com as perguntas idênticas, então isso também precisa subir a versão.

Quantas opções uma pergunta choice pode ter antes de perder precisão?

O recomendado é manter abaixo de cerca de 20 opções. As opções dividem um orçamento fixo de 256 tokens da cabeça de decisão, e um espaço de rótulos grande demais derruba a precisão da resposta.

Como escolher o limiar de confiança pra decisão automática do Laya?

O limiar não é uma propriedade do modelo, é uma política sua. Você usa o answer_confidence retornado pelo Laya e mede a precisão real naquela cobertura nos seus dados; a confiança ordena as decisões, mas não prova sozinha que estão corretas.




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 30min

Blog | Mais populares