Como usar o Jev para ordenar os resultados da busca interna do seu produto?

Busca interna boa é feita em duas etapas: o buscador recupera candidatos, o ranking decide a ordem. O Jev, o System One Model da TypeSafe AI, entra na segunda: você pergunta com uma primitiva Score (array ordenado de níveis, até 10) ou Choice (até 255 opções) e recebe uma decisão tipada com probabilities e confidence de 0 a 1. Normaliza com score / (len(criteria) - 1), combina as dimensões com pesos no seu código e ordena. Todas as perguntas do mesmo state vão numa requisição só, porque a cobrança é por token de input (US$ 0,042 por 1M, saída US$ 0,00).
Sabe aquela busca interna que devolve 40 resultados e o item certo aparece na posição 7?
O buscador não está quebrado, ele só está fazendo o trabalho dele: casar palavra-chave, filtrar, devolver candidatos
Quem decide ordem é outra etapa, e é justamente essa etapa que quase ninguém monta
O Jev é o primeiro modelo público da TypeSafe AI e foi apresentado como um "System One Model": em vez de devolver prosa, ele devolve decisão tipada com probabilidades calibradas
Traduzindo pro nosso problema: dá pra pedir uma nota de relevância item a item e receber um número utilizável, não um parágrafo que você vai ter que parsear na raça
Neste post a gente monta o fluxo inteiro: shortlist, pergunta, normalização, pesos e corte, com o ranking final montado no SEU código 🙂
O que você precisa antes de começar
A parte de ambiente é curta:
- Python >= 3.10
- SDK instalado com
uv add typesafe-sdkoupip install typesafe-sdk - a variável
TYPESAFE_API_KEYno ambiente, que é de onde o cliente do SDK lê a chave
Se tu não quer SDK, a API é um POST direto em https://api.typesafe.ai/v1/systemone, com header Authorization Bearer e Content-Type: application/json
O cliente chama jev-latest por padrão
Esse alias aponta hoje pra versão atual, a jev-1.13.0 (o jev-preview aponta pra mesma coisa), e aqui vai o primeiro tome cuidado do post: jev-latest se move quando sai versão nova, o que pode mudar as respostas
Pra brincar em dev, beleza
Pra ranking em produção, fixa a versão
E o pré-requisito de produto?
Esse é o que mais gente esquece: o Jev não substitui o seu buscador
Curso de Jev: Sistemas Mais Inteligentes, Rápidos e Robustos
Desenvolva sistemas mais inteligentes, rápidos e robustos com Jev
Você precisa já ter um retriever devolvendo uma shortlist, seja BM25, busca vetorial, Elastic, Postgres full text, o que for
O modelo entra depois, pontuando os candidatos que o retriever trouxe
Recuperar é uma coisa, ordenar é outra, e misturar as duas é o motivo de metade das buscas internas ruins por aí
Passo a passo: ranking da busca interna com o Jev
- Gere a shortlist com o buscador que você já tem
Não manda o catálogo inteiro pro modelo, manda o topo do que o retriever achou
Pra ter uma referência de tamanho: o cookbook de re-ranking da TypeSafe monta shortlists BM25 de 30 passagens para 40 consultas do dataset CLERC, e com o re-ranking a acurácia top-1 sai de 5% para 18% e a top-10 de 38% para 62%
O erro comum deste passo: querer ranquear tudo. Ranking é refino, não é recuperação
- Monte o state com a consulta e o candidato
O state é o contexto que o modelo enxerga pra responder
A janela do Jev 1.13 é de 32 mil tokens, e esse orçamento vale para o state mais a pergunta mais longa da requisição
# cliente e classes (AsyncTypeSafeClient, Score, Choice, Noul) vêm do typesafe-sdk
# confere os nomes exatos de import e de campo no quickstart oficial
client = AsyncTypeSafeClient() # lê TYPESAFE_API_KEY do ambiente
def monta_state(consulta, item):
return f"""Consulta do usuário: {consulta}
Título do resultado: {item['titulo']}
Seção: {item['secao']}
Trecho: {item['trecho'][:1200]}"""
O erro comum deste passo: despejar o documento inteiro no state. Custo é por token de entrada, então documento inteiro é dinheiro queimado em texto que não decide nada
- Escreva a pergunta Score com os níveis do baixo pro alto
A primitiva Score usa um array ordenado de descrições de níveis, do extremo baixo ao extremo alto, com até 10 níveis
Três já funciona bem, e honestamente três descritos com clareza rende mais que dez vagos
RELEVANCIA = [
"não tem nada a ver com a consulta",
"fala do assunto, mas não responde a dúvida",
"responde exatamente o que foi perguntado",
]
E como a gente vai combinar mais de uma dimensão lá na frente, já escreve as outras duas agora, no mesmo formato:
ESPECIFICIDADE = [
"fala do tema de forma genérica",
"cobre parte do caso específico da consulta",
"trata exatamente o caso específico que foi perguntado",
]
ATUALIDADE = [
"descreve um comportamento que não existe mais no produto",
"está desatualizado em alguns pontos",
"reflete a versão atual do produto",
]
Repara que são três listas independentes, cada uma julgando UMA coisa só
E todas as três vão virar pergunta na mesma requisição, já já te mostro o porquê
O erro comum deste passo: escrever níveis tipo "ruim, médio, bom". Isso não diz NADA pro modelo nem pro colega que vai mexer depois. Descreve o comportamento observável de cada nível
- Agrupe todas as perguntas do mesmo state numa requisição só
Essa é a dica que mais muda a sua conta no fim do mês
Uma chamada com N perguntas dá as mesmas respostas que N chamadas com uma pergunta cada, e a recomendação oficial é mandar junto tudo que compartilha o mesmo state
No cookbook de perguntas paralelas, uma chamada com 13 perguntas sobre o mesmo artigo custou US$ 0,000497 contra US$ 0,006090 de 13 chamadas separadas, ou seja, 12,2x mais barato, e levou 0,27 s contra 2,71 s somados, 10x mais rápido
resposta = await client.system_one(
state=monta_state(consulta, item),
relevancia=Score(criteria=RELEVANCIA),
especificidade=Score(criteria=ESPECIFICIDADE),
atualidade=Score(criteria=ATUALIDADE),
)
O próprio cookbook de re-ranking avisa isso na cara: ele fez uma pergunta por par só pra ficar didático, e uma aplicação real faria várias perguntas sobre o mesmo par em uma única chamada
O erro comum deste passo: montar um for que dispara uma chamada por dimensão. Mesmo state, mesma requisição
- Normalize o score
A resposta de um Score é uma posição na escala, e ela pode cair entre dois níveis
Junto dela vêm dois campos que você vai usar: probabilities, com a probabilidade de cada nível, e confidence, um número de 0 a 1 que resume a forma dessa distribuição
Guarda esse confidence junto do score normalizado, porque é ele que decide lá na frente o que sobe sozinho e o que vai pra revisão
Escala de quatro níveis devolve de 0 a 3, de três níveis devolve de 0 a 2
Pra deixar tudo numa régua de 0 a 1:
def normaliza(score, criteria):
return score / (len(criteria) - 1)
O erro comum deste passo: somar direto uma escala de 3 níveis com uma de 5 e achar que a segunda é "mais importante". Ela só tem número maior, e o seu ranking vira loteria
- Combine as dimensões com pesos no seu código
Aqui entra o padrão de composite scoring: quebra o julgamento em dimensões independentes, normaliza cada uma de 0 a 1 e combina com pesos
O exemplo da doc faz 0,6 × 0,62 + 0,3 × 0,64 + 0,1 × 1,0 = 0,664, arredondado pra 0,66
def score_final(rel, esp, atu):
return 0.6 * rel + 0.3 * esp + 0.1 * atu
Repara onde os pesos moram: no seu repositório, versionados, revisáveis em PR
É o mesmo raciocínio de quando você usa scores calibrados pra priorizar leads: o modelo julga cada dimensão, a política de negócio fica com você
O erro comum deste passo: pedir pro modelo "dar a nota final considerando tudo". Aí você perde a capacidade de debugar por que o item X subiu
- Ordene e aplique o corte
ranqueados = sorted(itens, key=lambda i: i["final"], reverse=True)
CORTE = 0.35 # número seu, calibre numa amostra real de consultas
visiveis = [i for i in ranqueados if i["final"] >= CORTE]
O erro comum deste passo: cortar antes de olhar a distribuição. Roda com uma amostra de consultas reais e escolhe o corte vendo o que some da tela
Usando Choice para ranquear muitos itens em uma requisição só
O fluxo de cima é par a par: uma consulta contra um candidato
Pra catálogo grande existe um caminho mais direto, que é jogar os itens como opções de uma pergunta Choice e deixar o modelo distribuir probabilidade entre eles
Uma pergunta Choice aceita até 255 opções, e cada opção adicionada custa poucos tokens
Por isso a doc recomenda passar a lista completa em vez de mandar um recorte: cortar opção pra "economizar" é economia de centavo que estraga o resultado
E aqui entra a terceira primitiva do Jev, que ainda não apareceu por aqui: a Noul
Pra fechar o combo: Choice escolhe dentro de uma lista de opções, Score dá nota numa escala que você descreve, e Noul devolve a probabilidade de uma afirmação ser verdadeira
No nosso caso a Noul responde uma pergunta bem prática: esse documento responde a consulta, sim ou não?
resposta = await client.system_one(
state=f"Consulta: {consulta}\n\n" + linhas_numeradas,
melhor_linha=Choice(options=ids_das_linhas),
documento_responde=Noul("o documento responde a consulta do usuário"),
)
O cookbook de busca linha a linha mostra exatamente isso: 218 IDs de linha dos Termos de Serviço do GitHub são pontuados contra uma consulta em linguagem natural em uma única requisição, com uma pergunta Choice ranqueando os IDs e uma Noul checando se o documento responde
A função find() de lá devolve a probabilidade de exists e um score de relevância por linha
Ou seja: a Choice te dá o ranking, a Noul te dá o "tem resposta aqui ou não tem?"
Comparando com o fluxo par a par do começo do post: o Score dá uma nota própria pra cada candidato, o que é ótimo quando você quer várias dimensões e um corte por nota
A Choice não dá nota individual, ela distribui probabilidade entre as opções e te devolve a ordem, com todos os itens numa requisição só
Regra de bolso: precisa de nota e de pesos? Score. Precisa só da ordem entre muitos itens, numa tacada só? Choice
E o Noul é uma primitiva versátil, é a mesma que serve pra checar tentativa de jailbreak antes do LLM responder
E quando o catálogo passa de 255 itens?
Particiona em lotes
Ranqueia cada lote, pega o topo de cada um e faz uma rodada final entre os finalistas
E fica de olho na janela de 32 mil tokens: o limite prático não é só a contagem de opções, é o quanto de texto você colocou em cada uma. ID curto mais um título enxuto rende muito mais item por requisição que parágrafo inteiro como opção
Como usar o confidence para decidir o que sobe e o que não aparece
Todo resultado vem com probabilities, a probabilidade de cada nível, e com confidence, um número de 0 a 1 que resume a forma dessa distribuição pra você aplicar um limiar sem fazer a conta na mão
No Score a confiança vem da distribuição entre os níveis
Distribuição mais achatada significa confiança menor, e isso faz todo sentido: se o modelo espalhou probabilidade entre "não tem nada a ver" e "responde exatamente", ele está em cima do muro
A doc sugere um padrão de três faixas, com limiares diferentes conforme o risco da ação:
| Faixa de confiança | O que a doc recomenda | Como aplicar no ranking |
|---|---|---|
| Alta | agir automaticamente | reordena o resultado normalmente |
| Média | seguir com cautela, confirmar ou revisar | mantém a posição original ou marca pra revisão |
| Baixa | não agir, mandar pra humano ou outro sistema | deixa o ranking do retriever mandar |
Repara na lógica do caso de baixa confiança: você não inventa nota, você simplesmente não mexe
O fallback do ranking com Jev é o ranking que você já tinha, e isso deixa o deploy MUITO menos assustador
E por que dá pra confiar nesse número em vez de tratar como enfeite? Porque modelos System One são treinados pra decisão calibrada, com as probabilidades otimizadas contra resultados pra refletir a incerteza (é o que a doc chama de RLCD, reinforcement learning for calibrated decisions)
Somando a isso, as saídas são restritas ao schema definido antes da chamada, então o modelo não consegue devolver valor inválido nem erro de tipo
Nada de json.loads dentro de try/except rezando pra dar certo 😛
Onde esse ranking com Jev faz diferença no produto
O mapa de casos de uso da TypeSafe cita explicitamente pontuar relevância consulta-candidato, reordenar resultados com comparações par a par e selecionar contexto pra pipelines de RAG
Na prática, alguns lugares onde isso rende rápido:
- Help center e documentação: usuário digita a dúvida com as palavras dele, a doc usa as palavras do produto. Keyword sofre nisso, julgamento de relevância não
- E-commerce: aqui o composite scoring brilha, porque relevância, disponibilidade e margem são dimensões diferentes e cada uma tem peso diferente na sua régua de negócio
- Seleção de contexto pra RAG: em vez de empurrar os top 10 do vetorial pro prompt, pontua e manda só o que passou do corte. Menos contexto irrelevante, resposta melhor
- Reordenação par a par: quando dois candidatos estão empatados no score, uma pergunta comparando os dois resolve o desempate
Um detalhe legal: em todos esses casos o modelo julga, mas a política continua sua. Quer que produto sem estoque nunca apareça no topo? Isso é if no seu código, não é prompt
Custo, latência e os erros que estouram a conta
Sintoma: a conta veio muito mais alta que o esperado
Causa: uma chamada por par, com o state sendo reenviado a cada chamada
O Jev é cobrado apenas por token de entrada, US$ 0,042 por 1M de tokens de input, com saída a US$ 0,00
Output de graça é ótimo, mas input repetido é input pago de novo
Solução: agrupar as perguntas do mesmo state
Os números do cookbook não deixam dúvida: 12,2x mais barato e 10x mais rápido na comparação de 13 perguntas
Sintoma: o ranking mudou sozinho entre um deploy e outro
Causa: jev-latest, que é o padrão do SDK, apontando pra uma versão nova do modelo
O alias se move, e quando ele se move as respostas podem mudar
Solução: fixar a versão em produção (hoje a atual é a jev-1.13.0) e subir versão de propósito, medindo antes e depois
Sintoma: estouro de contexto em alguns itens
Causa: state grande demais pro orçamento de 32 mil tokens, lembrando que esse orçamento cobre o state mais a pergunta mais longa da requisição
Geralmente é aquele item com página gigante que entra na shortlist e arrebenta tudo
Solução: enxugar o state pro trecho que realmente decide. Título, seção e um recorte do corpo costumam bastar pra julgar relevância
Como prevenir isso desde o começo
Define duas coisas antes de escrever a primeira linha:
- um orçamento por busca: quantos itens você aceita ranquear por consulta
- um teto de shortlist: 30 candidatos já é uma referência pública de fluxo que funciona
Depois é só monitorar tokens de input por busca, porque é ali que o custo mora
Conclusão
O ganho aqui não vem de trocar de buscador
Vem de separar recuperação de ordenação: o retriever acha os candidatos, o Jev pontua cada um com escala calibrada, e a decisão final é feita com pesos que vivem no seu código, versionados, testáveis e explicáveis pra quem perguntar por que o item subiu
Próximo passo que eu sugiro: pega uma amostra de consultas reais do seu produto, mede top-1 e top-10 com o ranking atual, roda o fluxo e mede de novo
Sem medir antes, você não tem como saber se melhorou ou se só ficou mais caro 😀
E se tu não quer plugar a API própria agora, o Jev também tem página de modelo na Cloudflare Workers AI e no OpenRouter, dá pra testar por onde já estiver na sua stack
Até o próximo post!
Perguntas frequentes
Dá pra usar o Jev sem trocar o buscador que a empresa já tem?
Dá, e é assim que o fluxo funciona. O Jev não faz recuperação, ele pontua os candidatos que o seu retriever (BM25, busca vetorial, Elastic, Postgres full text, o que for) já trouxe. Você mantém o buscador atual e adiciona uma etapa de ranking em cima.
Quanto custa usar o Jev pra ranquear resultados de busca?
O preço é só por token de entrada: US$ 0,042 por 1 milhão de tokens de input, com output a US$ 0,00. O mesmo valor aparece na página do Jev 1.13 no OpenRouter. Como a conta é por token de entrada, montar um state enxuto (sem despejar o documento inteiro) impacta direto no custo.
Quantos itens dá pra pontuar numa única chamada ao Jev?
Depende da primitiva. Uma pergunta Choice aceita até 255 opções na mesma chamada, e no cookbook de busca linha a linha da TypeSafe, 218 IDs de linha dos Termos de Serviço do GitHub foram pontuados numa requisição só, combinando uma pergunta Choice pra ranquear os IDs e uma Noul checando se o documento responde a consulta.
Como saber se posso confiar na nota que o Jev devolveu antes de usar no ranking?
O modelo devolve um campo confidence de 0 a 1 junto com a resposta, calculado a partir da distribuição de probabilidades entre os níveis do Score. A documentação sugere dividir isso em três faixas (alta, média, baixa) e definir o limiar de acordo com o risco da ação, então dá pra usar confidence baixa como sinal pra revisar manualmente um resultado antes de exibir no topo da busca.
O Jev só funciona pela API própria da TypeSafe?
Não, ele também está disponível via Cloudflare Workers AI e no OpenRouter, na página typesafe/jev. Pra quem já usa um desses provedores no stack, dá pra chamar o Jev por ali em vez de bater direto no endpoint da TypeSafe.
Vale a pena mandar uma pergunta por candidato ou agrupar tudo numa chamada?
Agrupar é a recomendação oficial sempre que as perguntas compartilham o mesmo state. No cookbook de perguntas paralelas, 13 perguntas sobre o mesmo artigo numa chamada custaram US$ 0,000497 contra US$ 0,006090 de 13 chamadas separadas, e o tempo caiu de 2,71 s somados pra 0,27 s.
Formações
Formação Claude Code
Domine Claude Code do absoluto zero até o avançado
- 120 aulas
- 4 projetos
- 9h 45min
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.

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.

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.
