Como versionar o schema de decisão do Jev sem quebrar o que já está em produção?

O schema de decisão do Jev é um contrato: você define as chaves de questions, as opções da Choice e os níveis da Score, e a resposta volta exatamente dentro dessa forma. Para evoluir sem quebrar produção, fixe o ID da versão no campo model em vez do alias, grave um baseline com casos reais, escreva a mudança como uma pergunta NOVA rodando em paralelo, compare answer, probabilities e confidence, recalibre os thresholds e só então remova a pergunta antiga. Schema e versão de modelo nunca mudam no mesmo deploy, beleza?
Fala aí, beleza? Se você já tem uma chamada do Jev rodando em produção, então você já assinou um contrato com o seu próprio código
As chaves do mapa questions, as opções de uma Choice e os níveis de uma Score são definidos por VOCÊ antes da chamada, e a resposta volta sob as mesmas chaves que você enviou, com a distribuição restrita às opções que você declarou
Ou seja: mexeu no schema, mexeu no que chega lá no if que já está rodando do outro lado 😅
E aqui não tem mágica de migração automática. A compatibilidade é responsabilidade de quem consome a decisão, então versionar o schema de decisão do Jev é basicamente disciplina de deploy: uma mudança de cada vez, com baseline antes e comparação depois
Bora ver na prática?
O que você precisa ter antes de mexer no schema
Antes de sair editando criteria, confere essa listinha:
- Acesso ao Jev. Ele segue em early access por lista de espera em typesafe.ai, sem data anunciada de disponibilidade geral
- Uma chamada funcionando. Pode ser direto no HTTP (
POST https://api.typesafe.ai/v1/systemone, auth Bearer, corpo comstate,modelequestions) ou pelos SDKs oficiais de Python e JavaScript - O mapa de quem lê o quê. Quais chaves de
questionso seu código consome hoje, e qual threshold deconfidencecada caminho aplica. Se isso está espalhado em três serviços, esse é o primeiro problema a resolver - Casos reais guardados. Um punhado de
statede produção salvos em arquivo, pra servir de fixture. Sem isso você não compara nada, só acha - Opcional, mas ajuda muito: o jevcheck, projeto de terceiros (não é da TypeSafe) mantido pela conta
satharielsno GitHub, publicado no PyPI na versão 0.2.0. Ele grava um contrato de comportamento (modelo baseline + fixtures + respostas esperadas) e prova se um candidato ainda satisfaz esse contrato
Um detalhe que vale lembrar desde já: o Jev cobra só na entrada. No jev-1.13 o preço publicado é de US$ 0,042 por 1 milhão de tokens de entrada e US$ 0,00 na saída
Então cada opção que você adiciona numa Choice custa poucos tokens, e rodar duas versões da pergunta na mesma chamada tem custo. Pequeno, mas existe
Formação Claude Code
Domine Claude Code do absoluto zero até o avançado
- 120 aulas
- 4 projetos
- 9h 45min
Passo a passo para evoluir o schema sem derrubar o que já roda
A regra que costura tudo: nunca mude o schema e a versão do modelo no mesmo deploy
Se as duas coisas se movem juntas e o resultado muda, boa sorte descobrindo qual delas foi 🙃
- Congele a variável modelo. O SDK chama
jev-latestpor padrão, e hoje tantojev-latestquantojev-previewresolvem parajev-1.13.0. IDs versionados são aceitos no campomodelmesmo quando não aparecem na lista publicada, e a própria doc da TypeSafe orienta fixar o ID quando você já calibrou thresholds contra aquela versão. Fixe antes de tocar no schema
curl -X POST https://api.typesafe.ai/v1/systemone \
-H "Authorization: Bearer $TYPESAFE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "jev-1.13.0",
"state": { "ticket": "não consigo emitir a segunda via do boleto" },
"questions": {
"department": {
"type": "choice",
"instructions": "Para qual time este ticket deve ser roteado?",
"criteria": {
"billing": "cobrança, boleto, nota fiscal, reembolso",
"general": "dúvida de uso do produto, sem cobrança envolvida"
}
}
}
}'
O erro comum deste passo: trocar o alias por um pin concreto no mesmo commit em que você adiciona uma opção nova. Essa mesma lógica de separar as mudanças vale pra qualquer troca de modelo sem quebrar produção, não é exclusividade do Jev
- Grave o comportamento atual como baseline. Pegue as fixtures com
statereal e registre o que o schema de hoje responde. Com o jevcheck é um comando:
jevcheck record fixtures/tickets.json --out baseline-answers.json
O erro comum deste passo: gravar baseline apontando pra um alias. O CLI rejeita jev-latest e jev-preview sem --allow-unpinned, e com pin concreto ele exige que o response.model bata exatamente. Isso é feature, não chatice: baseline em cima de alias é baseline que muda sozinho
- Escreva a mudança como uma pergunta NOVA, não como edição da antiga. Perguntas da mesma requisição são avaliadas de forma independente e em paralelo, e o resultado de uma primitiva não vira contexto oculto que altera outra. Isso te dá um A/B honesto dentro da mesma chamada
from typesafe_sdk import Choice, TypeSafeClient
client = TypeSafeClient()
DEPARTMENTS_V1 = {
"billing": "cobrança, boleto, nota fiscal, reembolso",
"general": "dúvida de uso do produto, sem cobrança envolvida",
}
DEPARTMENTS_V2 = {
**DEPARTMENTS_V1,
"security": "acesso indevido, vazamento de dados, conta comprometida",
}
INSTRUCTIONS = "Para qual time este ticket deve ser roteado?"
resp = client.system_one(
model="jev-1.13.0",
state=ticket,
questions={
"department": Choice(instructions=INSTRUCTIONS, criteria=DEPARTMENTS_V1),
"department_v2": Choice(instructions=INSTRUCTIONS, criteria=DEPARTMENTS_V2),
},
)
O erro comum deste passo: editar o criteria da pergunta que já está em produção e torcer. Aí não tem baseline, não tem comparação, tem só surpresa
- Rode as duas lado a lado e logue tudo. A resposta traz
answerscom os mesmos ids que você enviou, mais omodelusado e ousagede tokens. NumaChoicevocê recebe a opção escolhida, as probabilidades de todas as opções e a confiança da escolhida. Guarde os três, não só o vencedor
v1 = resp.answers["department"]
v2 = resp.answers["department_v2"]
log.info(
"schema_shadow",
model=resp.model,
v1_choice=v1.choice, v1_conf=v1.confidence, v1_probs=v1.probabilities,
v2_choice=v2.choice, v2_conf=v2.confidence, v2_probs=v2.probabilities,
)
# quem decide continua sendo a v1 enquanto a v2 está em sombra
route_ticket(v1.choice if v1.confidence >= THRESHOLD else "human_review")
O erro comum deste passo: ligar a v2 pra valer no primeiro dia. Rodar em sombra primeiro é o mesmo espírito de um rollout gradual em produção: o tráfego só migra depois que o dado te deixa confortável
- Compare e decida com dado, não com vibe. O jevcheck compara o baseline gravado contra um candidato e devolve resultado inalterado, regressão de confiança ou virada de resposta, com diff exato:
jevcheck compare fixtures/tickets.json \
--from baseline-answers.json \
--to jev-1.13.0
# ou buscando os dois ao vivo
jevcheck eval fixtures/tickets.json --candidate-model jev-1.13.0
Falhou, ele sai com exit code diferente de zero, o que já te deixa plugar isso no CI
O erro comum deste passo: olhar só se a resposta mudou. Uma confidence que despenca de 0,94 para 0,71 sem virar a resposta é uma bomba-relógio, porque na próxima entrada parecida ela vira
- Recalibre os thresholds contra a forma nova antes de cortar o tráfego.
confidenceé uma estatística derivada da própria distribuição de probabilidades, colapsada num número de 0 a 1 pra você aplicar corte sem fazer a conta na mão. A doc recomenda começar conservador, testar com os seus dados e ajustar conforme o observado, porque o threshold certo depende do seu domínio
O erro comum deste passo: carregar o threshold antigo pra pergunta nova como se fosse a mesma régua. Mudou o conjunto de opções, mudou a distribuição, mudou a régua
- Remova a pergunta antiga só quando ninguém mais lê aquela chave. Grep no código, grep nos jobs, grep nos dashboards. Enquanto existir um caminho que faz
answers["department"], a chavedepartmentcontinua no ar
O erro comum deste passo: esquecer do consumidor indireto (relatório, fila de revisão humana, regra de negócio antiga) e derrubar exatamente ele
Os quatro tipos de mudança e o que cada um realmente quebra
Nem toda alteração tem o mesmo risco. Um resumão antes de destrinchar:
| Mudança | O que realmente quebra | Estratégia segura |
|---|---|---|
| Adicionar opção na Choice | redistribui as probabilidades e pode virar casos antigos | pergunta nova em paralelo + fallback para valor desconhecido |
| Renomear a chave da pergunta | o código lê uma chave que não volta mais | enviar chave antiga e nova juntas por um período |
| Remover opção morta | a massa de probabilidade vai para as opções restantes | recalibrar thresholds + fallback para dado histórico |
| Inserir nível no meio da Score | renumera os níveis e invalida comparação com dado antigo | acrescentar nos extremos ou versionar a pergunta inteira |
Adicionar uma opção nova em uma Choice:
Uma Choice aceita até 255 opções, e cada opção adicionada custa alguns tokens. Então o teto raramente é o problema
O problema é que a distribuição é sempre restrita ao que você declarou. Entrou security no criteria, a probabilidade que antes era dividida entre billing e general agora se espalha entre três, e casos que caíam confortáveis em general podem virar
E tem o lado do código: ele precisa lidar com um valor que ele ainda não conhece
HANDLERS = {"billing": route_billing, "general": route_general}
handler = HANDLERS.get(answer.choice)
if handler is None:
# opção nova que este deploy ainda não entende
route_human_review(answer)
else:
handler(answer)
Esse get com fallback é o que separa "apareceu uma categoria nova" de "o serviço explodiu às 3 da manhã"
Renomear um campo:
As respostas voltam sob as MESMAS chaves que você enviou em questions
Traduzindo: renomear a chave não é refactor cosmético, é troca de contrato. Quem lê answers["department"] simplesmente não acha mais nada
A saída segura é a mesma dos passos lá em cima: mandar as duas chaves juntas por um período, com o mesmo instructions e o mesmo criteria, e só aposentar a antiga quando o último consumidor migrar
questions = {
"department": Choice(instructions=INSTRUCTIONS, criteria=DEPARTMENTS_V1),
"routing_team": Choice(instructions=INSTRUCTIONS, criteria=DEPARTMENTS_V1),
}
Custa tokens a mais? Custa. Mas é entrada, e é por um período curto
Remover um valor morto:
Aquela opção que ninguém escolhe há meses parece candidata óbvia à faxina. Se liga no efeito colateral: como a distribuição só existe dentro do que você declarou, tirar uma opção empurra aquela massa de probabilidade pras que sobraram
Resultado: confidence de casos vizinhos muda, e o seu threshold calibrado ontem pode ficar frouxo ou apertado demais
E tem o histórico. Registro salvo no banco com o valor removido continua lá, então o código de leitura precisa de fallback pra valor desconhecido também na volta, não só na ida
Mexer em uma Score:
Essa é a mais traiçoeira. Numa Score, o criteria é um array ORDENADO de descrições de nível, do extremo baixo pro alto, e o número do nível é a posição no array começando em 0
Mínimo de 2 níveis, máximo de 10. Uma escala de três níveis vai de 0 a 2, e o score pode cair entre dois níveis
Então inserir um nível no meio renumera tudo o que vem depois. O 2 que você gravou mês passado não significa mais a mesma coisa que o 2 de hoje, e qualquer comparação histórica vira lixo silencioso
Mais seguro: acrescentar nos extremos, ou versionar a pergunta inteira com uma chave nova e migrar o dado antigo de forma explícita
Um detalhe que ajuda nesse processo todo: instructions, opções de Choice, níveis de Score e criteria de Noul aceitam estrutura JSON, não só string. A doc inclusive sugere passar o JSON ou os subcampos relevantes em vez de serializar tudo em template de texto
Isso é ótimo pra versionamento, porque você evolui a descrição como dado estruturado, versionado no repo, em vez de caçar f-string no meio do código 😀
Ah, e vale a mesma lógica pra Noul: ela é definida pelas instructions (a pergunta sim/não) e o criteria é opcional, servindo pra descrever o que conta como yes e o que conta como no. A resposta é uma probabilidade de 0 (no) a 1 (yes), e a boa prática é formular a pergunta pra que probabilidade alta signifique yes
Inverteu o sentido da pergunta num refactor? Você acabou de inverter todos os seus thresholds. Tome cuidado!
Sinais de que a migração quebrou algo (e como reagir)
Sintoma: a resposta virou de categoria
O clássico answer flip, tipo um caso que era billing e passou a voltar general
Causa provável: você adicionou ou removeu opção e a distribuição se redistribuiu. Como as probabilidades só existem dentro do conjunto declarado, qualquer mudança no conjunto mexe em todo mundo
Como diagnosticar: compare as probabilities completas das duas versões da pergunta, não só o choice. Se as duas primeiras opções estavam empatadas antes, a virada já estava contratada
Como reagir: volte o schema anterior no deploy, mantendo o ID de versão fixo, e traga a versão nova de volta em sombra. O jevcheck reporta esse tipo de diff de forma explícita (no repo tem exemplo de billing→general com confiança caindo de 0,94 para 0,71)
Sintoma: a confiança caiu e o volume de revisão humana explodiu
A resposta continua a mesma, mas a confidence afundou e passou a bater no seu corte
Causa provável: descrições novas competindo com as antigas, ou opções parecidas demais entre si. A doc de Score orienta usar descrições concretas por nível justamente pra o modelo conseguir diferenciar, e a lógica vale pras opções de Choice também
Como diagnosticar: olhe a distribuição inteira. Se a massa se espalhou entre duas opções vizinhas, o texto das duas está ambíguo pro modelo
Como reagir: deixe as descrições mais concretas e recalibre o threshold com os seus dados, começando conservador. E lembre que o certo aqui depende do seu domínio, não tem número universal pra copiar de ninguém
Sintoma: o código quebrou lendo a resposta
KeyError numa chave renomeada, ou um if que não trata a opção nova
Causa provável: o schema foi pra produção antes do consumidor
Como reagir: fallback pra valor desconhecido em TODO ponto que lê answers, e chave antiga convivendo com a nova durante a migração
Duas coisas que evitam sintoma antes de existir: não peça ao Jev o que ele não faz, e mantenha o resto no seu código
A doc de arestas do jev-1.13 registra que o modelo pode ser bastante literal na interpretação, tem dificuldade com tarefas que exigem precisão numérica e não é treinado pra gerar texto (encadear choices pra produzir texto não funciona bem e é lento)
E a orientação de construção é quebrar julgamento amplo em perguntas estreitas e tipadas, combinando os resultados com lógica no código, deixando controle de fluxo, regras determinísticas e efeitos colaterais do lado do seu programa
Schema pequeno e pergunta estreita é schema fácil de versionar. Pergunta gigante que decide tudo é a que te prende pra sempre
Vídeo: contexto sobre modelos e o cenário de IA
Pra começar do zero no assunto de troca de versão de modelo e no cenário atual, este vídeo do canal mostra o retorno do Fable 5 e o que esse movimento representa no ecossistema:
Próximo passo: transforme o schema em contrato versionado
Recapitulando o que realmente importa aqui:
Schema e versão de modelo mudam em deploys SEPARADOS
Baseline antes, comparação depois, sempre com o ID de versão fixo no campo model
Pergunta nova em paralelo em vez de edição na pergunta viva, aproveitando que as perguntas da mesma requisição são avaliadas de forma independente
E fallback pra valor desconhecido em todo lugar que lê a resposta
O próximo passo prático é bem pequeno: escolha dois ou três casos reais de produção, salve os state deles como fixture e grave o baseline HOJE. Só depois disso planeje a primeira alteração de Choice ou Score
Sem baseline, versionar o schema de decisão do Jev é chute com nome bonito 😛
E se você usa agente pra escrever esse código, a TypeSafe publica uma agent skill com contexto da API. No Claude Code:
claude plugin marketplace add typesafe-ai/skills
claude plugin install typesafe@typesafe-ai
Depois é só invocar com /typesafe:typesafe-ai
Em outros agentes:
npx skills add typesafe-ai/skills --skill typesafe-ai
Bora versionar direito e dormir tranquilo, até o próximo post!
Perguntas frequentes
Posso trocar de jev-latest para jev-preview sem me preocupar com quebra de schema?
Hoje os dois aliases apontam pra jev-1.13.0, então na prática não muda nada agora. Mas são aliases, e a TypeSafe pode fazer cada um resolver pra uma versão diferente no futuro. Se você já calibrou thresholds de confidence contra a versão atual, a própria doc orienta fixar o ID jev-1.13.0 em vez de depender do alias
Rodar duas versões da mesma Choice na mesma chamada custa mais caro?
Custa um pouco, sim. Cada opção adicionada numa Choice consome poucos tokens, e ter department e department_v2 na mesma chamada soma esse custo. No jev-1.13 o preço publicado é de US$0,042 por 1 milhão de tokens de entrada e US$0,00 de saída, então o extra do shadow test é pequeno
O jevcheck é uma ferramenta oficial da TypeSafe?
Não. É um projeto de terceiros mantido pela conta sathariels no GitHub, publicado no PyPI na versão 0.2.0. Ele grava um contrato de comportamento (modelo baseline, fixtures e respostas esperadas) e prova se um modelo candidato ainda satisfaz esse contrato antes da troca de versão
Por que o jevcheck recusa comparar baseline direto contra jev-latest?
Porque alias muda de versão sem avisar, e um baseline gravado em cima de alias vira um baseline que se move sozinho embaixo de você. O CLI rejeita jev-latest e jev-preview sem a flag –allow-unpinned, e com um pin concreto ele exige que o response.model bata exatamente
Dá pra editar direto o criteria de uma Choice que já está em produção?
Dá, mas é aí que mora o risco: as chaves de questions, as opções de Choice e os níveis de Score são definidos por você, então qualquer edição muda o que chega no if do outro lado. O caminho mais seguro é criar uma chave nova, tipo department_v2, rodar as duas em paralelo na mesma chamada e só depois trocar a antiga
O jev-1.13 consegue gerar o texto explicando por que tomou uma decisão?
Não é o uso pra ele. A doc de arestas do jev-1.13 registra que o modelo não é treinado pra gerar texto, e encadear várias Choices tentando montar uma explicação funciona mal e é lento. Ele foi pensado pra devolver probabilidade, escolha ou nível, e o texto explicativo fica por conta do seu código
Formações
Formação Vibe Coding
Do Prompt ao Produto: Crie Software Real com IA
- 474 aulas
- 20 projetos
- 39h 27min
Blog | Mais populares

Como montar um workflow de automação combinando decisões do Jev em código
Aprenda a montar um workflow com Jev: decisões tipadas encadeadas em código, alta confiança agindo sozinha e casos incertos escalando para revisão.

Para quem o Jev serve (e para quem não serve)?
Jev serve pra roteamento, scoring e guardrails em IA, não pra texto ou código. Veja pra quem o Jev serve e quando evitar.

Jev decide, LLM escreve: como dividir os papéis dentro de um agente de IA
Jev é o modelo que decide, não escreve: entenda como dividir papéis entre Jev e LLM dentro de um agente de IA e quando usar cada um.
