O Jev me fez repensar onde a IA deveria entrar no código
Nos últimos dois dias eu vi o Jev aparecer em praticamente todo lugar onde desenvolvedor discute IA.
20–200x mais rápido.
40–400x mais barato.
Output tokens grátis.
Não alucina.
Não gera texto.
O anúncio da TypeSafe passou de 1,8 mil pontos no Hacker News e, previsivelmente, começou a discussão sobre qual gaveta conhecida deveríamos usar para encaixar aquilo.
“É só um classifier.”
“Reinventaram classification models.”
“Isso já existe com structured output.”
“Se não gera texto, então não substitui um LLM.”
“Como pode dizer que não alucina se ainda pode responder errado?”
Eu acompanhei boa parte dessa discussão e acho curioso porque nenhuma dessas observações é exatamente absurda. O problema é tentar explicar o Jev por uma delas.
Começa pela comparação com LLM.
Jev não é um GPT que ficou 200 vezes mais rápido. A TypeSafe conseguiu esses números mudando o problema que o modelo resolve.
Você não manda:
Explique por que esse cliente provavelmente vai cancelar.
e fica esperando texto.
A interface se parece mais com isto:
state:
customer: ...
subscription: ...
usage: ...
questions:
will_churn?
needs_human_review?
customer_segment?
A resposta, conceitualmente:
will_churn: 0.82
needs_human_review: 0.34
customer_segment:
enterprise: 0.71
smb: 0.21
personal: 0.08
A descrição da própria TypeSafe é boa: entra estado não estruturado, saem decisões tipadas e probabilísticas.
A diferença parece pequena olhando apenas a API. Por baixo, é outra operação.
LLMs como GPT, Claude e Gemini foram construídos para prever o próximo token. Mesmo quando você obriga o modelo a devolver um JSON bonitinho, ainda existe geração autoregressiva acontecendo ali.
Jev dispensa a geração livre porque as respostas possíveis já foram definidas.
Isso explica boa parte dos números de performance sem precisar de mágica.
“Então é um classifier?”
Eu diria que sim, desde que a gente não esteja imaginando o classifier tradicional que treinamos para uma única tarefa.
Um modelo clássico pode receber um email e responder:
spam
not_spam
Outro modelo foi treinado para:
fraud
legitimate
Você coleta dados, prepara dataset, treina, valida, publica. Mudou o domínio de maneira relevante? Provavelmente haverá trabalho novo.
No Jev, a pergunta também chega em runtime.
Hoje você pode perguntar:
Esse ticket exige atendimento urgente?
Amanhã:
Esse texto parece uma tentativa de prompt injection?
Depois:
Qual dessas 40 skills atende melhor esta requisição?
É o mesmo modelo recebendo outros estados, perguntas e espaços de resposta.
Por isso “general-purpose semantic classifier” me parece uma descrição melhor do que tentar enfiar Jev na categoria de “novo LLM”.
Aliás, classifiers não ficaram obsoletos porque apareceu ChatGPT. Nós apenas passamos alguns anos resolvendo qualquer problema de IA com um modelo generativo porque modelos generativos ficaram absurdamente bons e fáceis de usar.
Nem sempre essa é a ferramenta mais eficiente.
E structured output?
Essa comparação faz sentido.
Hoje eu consigo pegar um modelo da OpenAI, Anthropic ou Google, entregar um JSON Schema e exigir:
{
owner: "billing" | "sales" | "engineering"
}
Então a novidade obviamente não é “uma IA conseguiu devolver um enum”.
O que muda é a quantidade de trabalho que o modelo precisa executar para chegar naquele enum.
Com um LLM você tem, grosseiramente:
contexto
↓
geração de tokens
↓
estrutura
↓
decisão
Jev pode trabalhar direto sobre o estado e o conjunto de decisões possíveis.
Segundo a TypeSafe, as respostas ficam entre aproximadamente 70 e 500 ms e o preço é US$ 0,042 por milhão de tokens de entrada.
Eu colocaria um asterisco grande nesses números porque são benchmarks e preços divulgados pela própria empresa. Quero ver uso real, gente reproduzindo teste, comportamento em domínios diferentes e principalmente onde o modelo começa a falhar.
Mas suponha que a ordem de grandeza se mantenha.
Aí começa uma mudança importante para quem escreve software.
Uma chamada de IA deixa de ser necessariamente aquele recurso caro que você usa uma vez no final de uma operação. Ela pode começar a aparecer várias vezes durante a execução.
E isso muda a arquitetura.
Tem outra frase do anúncio que precisa ser desmontada: “Jev não alucina”
Aqui acho que o marketing passou um pouco do ponto.
Se eu definir:
owner:
- billing
- sales
- engineering
Jev não vai responder:
owner: finance_department_2
Também não vai inventar uma quarta propriedade, uma função ou uma URL que não existe no schema.
Nesse sentido, a garantia existe por construção.
Só que isso não impede:
owner: billing
de estar errado.
Talvez aquele chamado claramente devesse ir para engineering.
Uma coisa é garantir que a saída pertence ao espaço permitido. Outra coisa é saber se o modelo escolheu corretamente dentro desse espaço.
Parece uma distinção banal, mas é exatamente o tipo de banalidade que vira incidente em produção.
Eu não gostaria de escrever isto:
const decision = await model.evaluate(state);
await execute(decision);
Prefiro tratar a saída do modelo como evidência para uma política que continua pertencendo à aplicação.
Por exemplo, uma escolha acompanhada da distribuição:
billing 0.48
engineering 0.44
support 0.08
conta uma história bem diferente de:
billing 0.96
engineering 0.03
support 0.01
Nos dois casos billing vence. Meu software não precisa reagir da mesma forma aos dois.
Foi aí que o anúncio começou a me interessar de verdade.
A TypeSafe publicou exemplos em que uma tarefa maior é quebrada em pequenos julgamentos independentes. Código normal coordena tudo.
Em vez de entregar um prompt enorme:
Analise todos estes dados, aplique estas regras, considere estas exceções, avalie dez critérios diferentes e no final me diga o que fazer.
você escreve o fluxo explicitamente.
Algo nessa linha:
carrega estado
avalia risco
se risco > limite:
pede revisão
classifica intenção
seleciona estratégia
aplica regra determinística
continua
Alguns desses passos são código comum. Outros são perguntas semânticas.
Isso me agrada muito mais do que a ideia de jogar o sistema inteiro dentro de um prompt e torcer para o modelo reconstruir corretamente a lógica da aplicação a cada chamada.
A TypeSafe também mostrou experimentos onde LLMs tradicionais tiveram resultados melhores quando usados nesse formato, resolvendo decisões menores, do que recebendo toda a tarefa de uma vez.
É benchmark deles. Novamente: cautela.
Mas a hipótese faz sentido.
Nós passamos bastante tempo tentando descobrir como escrever prompts cada vez maiores e mais sofisticados. Talvez parte desses prompts esteja grande simplesmente porque colocamos responsabilidade demais dentro do modelo.
Olha os usos que começaram a aparecer depois do lançamento.
Um desenvolvedor colocou Jev observando tool calls de um coding agent para avaliar se uma ação parecia segura.
Outro pensou em usar como router de modelos.
Outro entregou uma lista de skills e deixou o modelo escolher qual carregar.
Teve bot de Minecraft escolhendo ações pelo estado do jogo.
Outro projeto avaliava ideias executando vários julgamentos em paralelo.
Tem um padrão aí.
qual ferramenta?
qual categoria?
qual rota?
essa operação parece segura?
qual opção combina melhor com este estado?
São perguntas que aparecem naturalmente no meio de um algoritmo.
Quando percebi isso comecei a brincar com uma abstração em TypeScript.
Acabei publicando @nitoba/questions.
A motivação é simples.
Se eu começar a usar dezenas dessas decisões numa aplicação, não quero isto:
await jev.call(...)
espalhado por service, controller, job, worker e domínio inteiro.
Jev é um provider. Minha aplicação não deveria precisar pensar em termos de Jev.
O que ela está fazendo é uma pergunta sobre algum estado.
Então a API mais básica da lib ficou assim:
const blocked = await questions
.about(ticket)
.is("Esse incidente está bloqueando produção?");
Eu particularmente gosto de APIs que não precisam de três parágrafos para explicar o que estão fazendo.
Tem escolha:
const owner = await questions
.about(ticket)
.choose([
billingTeam,
platformTeam,
supportTeam,
]);
Tem score:
const impact = await questions
.about(ticket)
.score("Qual o impacto deste incidente?", [
"Baixo",
"Degradação parcial",
"Indisponibilidade",
]);
E dá para agrupar várias perguntas:
const assessment = await questions.about(ticket).ask({
urgent: "Esse incidente exige resposta imediata?",
owner: Question.choice("Qual time deve investigar?", {
billing: "Cobrança e pagamentos",
platform: "Infraestrutura, deploys e API",
support: "Atendimento ao cliente",
}),
impact: Question.score("Qual o impacto?", [
"Baixo",
"Serviço degradado",
"Indisponibilidade",
]),
});
O TypeScript infere:
{
urgent: boolean;
owner: "billing" | "platform" | "support";
impact: number;
}
No meio da implementação percebi que Zod já descreve boa parte da linguagem que eu precisava.
Por exemplo:
const triage = z.object({
urgent: z
.boolean()
.describe("Esse incidente exige resposta imediata?"),
owner: z
.enum(["billing", "platform", "support"])
.describe("Qual time deve investigar?"),
});
E então:
const result = await questions
.about(ticket)
.ask(triage);
O retorno é:
z.output<typeof triage>
Isso economiza abstração.
O schema que eu já usaria como contrato e validação runtime também consegue descrever as perguntas que serão feitas ao modelo.
E aqui eu tomei uma decisão importante no desenho da lib: questions não depende conceitualmente do Jev.
Hoje essa API pode trabalhar com Jev via TypeSafe/System One, com Vercel Evaluation ou com modelos generativos através do AI SDK.
Então:
questions.about(ticket).ask(triage)
pode acabar em Jev, Gemini, Claude ou GPT.
Só que eu não quis fingir que esses providers produzem a mesma coisa.
Se a distribuição veio realmente de um provider de decisão, a evidência registra:
probabilitySource: "provider"
Se eu precisei pedir para um LLM generativo estimar aquela distribuição:
probabilitySource: "estimated"
Essa distinção é proposital.
Se você pergunta para um LLM:
de 0 a 1, qual sua confiança nessa resposta?
e ele responde 0.94, aquilo não virou uma probabilidade calibrada só porque tem duas casas decimais.
Número com aparência científica ainda pode ser apenas texto gerado.
Também existe uma API mais detalhada:
const run = await questions
.about(ticket)
.run(triage);
que expõe:
run.value
run.evidence
run.diagnostics
run.operationId
A aplicação pode decidir sua própria política.
Talvez acima de certo nível de evidência você automatize. Numa região ambígua, manda para uma pessoa. Em outro caso, executa fallback ou consulta outro modelo.
O importante para mim é que essa política continue visível no código.
Tem ainda um caso que acho especialmente interessante para Jev: volume.
Imagine classificar cem mil registros.
Na lib dá para fazer:
const decisions = Streams
.from(tickets)
.map(
(ticket, { signal }) =>
questions.about(ticket).run(triage, { signal }),
{ concurrency: 16 },
);
Com bounded concurrency, backpressure, cancellation e Web Streams.
Aí começam a surgir pipelines assim:
dataset
↓
semantic classification
↓
filter
↓
batch
↓
regra da aplicação
↓
persistência
Para mim esse cenário é mais interessante do que perguntar se Jev vai “matar” GPT ou Claude.
Não vai.
Eu continuaria usando um modelo generativo para escrever código, resumir um documento, conversar, explicar uma ideia ou produzir texto. Jev abriu mão justamente dessa capacidade.
A pergunta que eu estou fazendo é outra: quantos ifs existem hoje nas nossas aplicações em que sabemos escrever perfeitamente o branch, mas não sabemos escrever a condição?
if (
await questions
.about(transaction)
.is("Essa transação parece fraudulenta?")
) {
await requireManualReview();
}
Escrever o if sempre foi fácil.
O problema está escondido dentro da pergunta.
Fraude, intenção, urgência, similaridade semântica, risco, roteamento, adequação. São coisas que um humano entende olhando contexto, mas que muitas vezes exigem uma quantidade ridícula de regras para serem codificadas de maneira determinística.
LLMs já nos permitiam atacar esses problemas. O custo e a interface, porém, empurravam muita coisa para prompts grandes.
Jev está testando uma ideia diferente: e se esse julgamento semântico for barato e rápido o suficiente para aparecer diretamente no fluxo normal do programa?
Eu quero explorar exatamente isso.
O fluxo continua escrito por mim. Banco, retry, autorização, regras de negócio e invariantes continuam sendo software convencional. Coloco o modelo apenas onde tenho uma pergunta que código comum não responde bem.
Foi dessa experiência que saiu @nitoba/questions.
bun add @nitoba/questions zod
Código:
github.com/nitoba/questions
Ainda quero descobrir até onde essa abstração faz sentido e onde ela começa a dar errado.
E, para ser sincero, essa segunda parte me interessa tanto quanto a primeira.