Como rodar muitas decisões do Jev em paralelo sem travar a sua aplicação?

diagrama mostrando decisões do Jev em paralelo sendo agrupadas e limitadas por semáforo
Resposta rápida

Rodar o Jev em paralelo é menos sobre abrir mil conexões e mais sobre a ordem das decisões: agrupar, limitar, isolar. Como todas as questões de uma requisição enxergam o mesmo state, juntar perguntas do mesmo documento numa chamada só já derruba custo e latência (no cookbook oficial, 13 questões ficaram 12,2x mais baratas e 10,0x mais rápidas). Depois disso você paraleliza entre states diferentes com o cliente assíncrono e um semáforo, respeitando os dois limites da API (250.000 tokens/s e 1.200 requisições/min), com backoff em 429 e efeito colateral idempotente do seu lado

Você manda um lote de alguns milhares de itens pro Jev classificar, e de repente a sua aplicação engasga

Request do usuário travando, timeout na tela, 429 pipocando no log

Aí bate aquela dúvida: será que o modelo é lento? Quase nunca é isso

Na maioria das vezes o gargalo está no SEU lado, no jeito como o cliente dispara as chamadas: tudo de uma vez, sem limite de concorrência, dentro do mesmo request HTTP que o usuário está esperando

Neste post a gente vai destrinchar isso em camadas: agrupar questões na mesma chamada, montar um pool com limite de paralelismo e tratar 429 com backoff, tudo no passo a passo

E depois, numa seção só dela, a camada que realmente destrava a tela: tirar o lote do caminho síncrono do usuário

Bora ver na prática? 🙂

O que você precisa antes de começar

Nada de PC da Nasa aqui, só o básico montado direito

  • SDK instalado. No Python: pip install typesafe-sdk (ou uv add typesafe-sdk), com Python >= 3.10. No JavaScript/TypeScript: o pacote @typesafe-ai/sdk, que expõe a classe TypeSafeClient e o método systemOne
  • Chave de API na variável de ambiente TYPESAFE_API_KEY. Os SDKs leem essa variável automaticamente quando você não passa a chave no construtor, então dá pra manter o código limpo
  • Modelo definido: jev-latest aponta sempre pro mais recente da família, e jev-1.13.0 é a versão fixada. Pra lote, pensa bem: versão fixada te dá resultado estável entre execuções
  • Entender o formato da requisição: três campos principais, state, model e questions. O questions é um mapa de questões tipadas, cada uma com um id escolhido por você, e a resposta volta com uma answer por id

Duas coisas que evitam dor de cabeça mais na frente

Primeira: a entrada do Jev é só texto (string ou dado estruturado enviado como state)

Segunda: aquele id que você dá pra cada questão serve só pra localizar a resposta correspondente, ele não é enviado ao modelo nem usado na inferência

Guarda essa segunda, ela volta mais na frente, lá no tópico sobre retry e efeito duplicado

Por que o Jev muda o desenho do lote:

Antes de sair codando, vale entender o que você tem em mãos

O Jev é o primeiro modelo System One da TypeSafe AI: em vez de gerar texto, ele recebe um estado não estruturado e devolve decisões tipadas (enum, booleano, score, categoria) com confiança calibrada

Se você conhece o fluxo de um LLM de chat, inverte a cabeça: aqui não tem streaming de token pra esperar, tem resposta tipada por questão

E tem um detalhe que é o coração deste post: o Jev ingere o state uma vez e avalia todas as questões contra ele em paralelo, numa única passada

Ou seja, já existe paralelismo DENTRO da chamada

Muita gente ignora isso e sai paralelizando por fora, do jeito mais caro possível…

Passo a passo: do disparo ingênuo ao pool controlado

  1. Faça a chamada base e entenda o contrato

O endpoint é POST https://api.typesafe.ai/v1/systemone, com os headers Authorization: Bearer <API_KEY> e Content-Type: application/json

curl -X POST https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @payload.json

O payload.json carrega os três campos: o state com o conteúdo a ser avaliado, o model (jev-latest ou jev-1.13.0) e o questions, o mapa com os ids que você escolheu

O erro comum deste passo: achar que o id da questão influencia a resposta e começar a escrever id gigante tipo questao_para_detectar_se_o_texto_e_spam_em_portugues. Não adianta, ele não vai pro modelo. Use id curto e estável, porque ele vai virar chave no seu banco depois

  1. Agrupe as questões do mesmo state numa chamada só

Essa é a otimização que a maioria pula

Uma chamada com N questões dá as mesmas respostas que N chamadas com uma questão cada

A diferença é que, quando um documento grande domina cada requisição, a chamada em lote envia o documento uma vez só

No cookbook de questões paralelas da TypeSafe, um briefing regulatório de 13 questões sobre o artigo da Wikipédia sobre GDPR ficou 12,2x mais barato e 10,0x mais rápido quando todas as questões foram agrupadas numa única chamada, sem mudança nas respostas

Insano, né? 😀

import os
from typesafe_sdk import TypeSafeClient

# o dicionário de questões tipadas fica no seu módulo,
# cada chave é o id que você escolheu
from questions import QUESTIONS

client = TypeSafeClient()  # lê TYPESAFE_API_KEY do ambiente

response = client.system_one(
    state=documento,
    model="jev-1.13.0",
    questions=QUESTIONS,
)

O erro comum deste passo: empilhar questão sem olhar o orçamento de tokens. São dois números diferentes: 64k cobre o state mais todas as questões somadas, e 32k se aplica ao state mais a questão mais longa. Documento grande com 40 questões estoura o primeiro fácil, e aí você precisa quebrar em grupos de questões, não em chamadas de uma questão cada

  1. Paralelize entre states diferentes, não entre questões do mesmo state

Agora sim: quando você tem mil itens distintos, cada um com seu próprio state, o paralelismo por fora faz sentido

No Python isso é o AsyncTypeSafeClient, com o método system_one recebendo state e questions

import asyncio
from typesafe_sdk import AsyncTypeSafeClient
from questions import QUESTIONS

client = AsyncTypeSafeClient()

async def avaliar(item):
    return await client.system_one(
        state=item["texto"],
        model="jev-1.13.0",
        questions=QUESTIONS,
    )

async def main(itens):
    return await asyncio.gather(*(avaliar(i) for i in itens))

O erro comum deste passo: rodar exatamente esse gather com a lista inteira de 5 mil itens e comemorar

Ele dispara TUDO de uma vez

Não é paralelismo, é enxurrada… e a conta chega em forma de 429

  1. Coloque um teto no paralelismo com um semáforo

O pool controlado é literalmente isso: um semáforo limitando quantas chamadas ficam em voo ao mesmo tempo

import asyncio
from typesafe_sdk import AsyncTypeSafeClient
from questions import QUESTIONS

client = AsyncTypeSafeClient()

async def avaliar(item, sem):
    async with sem:
        return await client.system_one(
            state=item["texto"],
            model="jev-1.13.0",
            questions=QUESTIONS,
        )

async def rodar_lote(itens, limite):
    sem = asyncio.Semaphore(limite)
    return await asyncio.gather(
        *(avaliar(i, sem) for i in itens),
        return_exceptions=True,
    )

Repara no return_exceptions=True

Sem ele, um item que falha derruba o gather inteiro e você perde o resultado dos que já tinham voltado

No JS/TS a ideia é a mesma, só muda a roupa: TypeSafeClient com o método systemOne, e você controla o teto processando a fila em fatias em vez de jogar um Promise.all na lista inteira

O erro comum deste passo: chutar o limite como número mágico. Ele não é um número universal, ele depende do tamanho médio do seu state. Item pequeno aguenta mais concorrência, documento gordo estoura tokens por segundo muito antes de estourar requisições por minuto

  1. Deixe o retry com backoff do SDK ligado

Os SDKs oficiais já fazem retry com backoff por padrão e respeitam o header retry-after quando a resposta traz um

No Python dá pra customizar isso passando uma RetryPolicy, e RetryPolicy(max_retries=0) desliga as tentativas

O erro comum deste passo: desligar o retry "pra ter controle" e não implementar controle nenhum. Só zera a RetryPolicy se você já tem a sua própria camada de fila e reprocessamento. Caso contrário, deixa o SDK trabalhar

Quando aparece 429 Too Many Requests (e como não voltar a ver)

Sintoma: o lote começa lindo, processa uns minutos, e do nada a taxa de erro dispara. 429 Too Many Requests em série, às vezes 529 Overloaded no meio

Causa: os limites de uso da API TypeSafe são medidos em DUAS dimensões, tokens por segundo e requisições por minuto

E uma requisição acima de qualquer um dos dois limites recebe 429

Os limites publicados por conta para o Jev são 250.000 tokens por segundo e 1.200 requisições por minuto

É aqui que quase todo mundo se ferra: dimensiona o pool contando requisição e esquece do token

Se o seu state médio é um documento grande, você bate os 250.000 tokens/s bem antes de chegar perto das 1.200 requisições/min

Solução: a orientação oficial para 429 e 529 é repetir a requisição com backoff exponencial, em vez de tentar de novo imediatamente

Retry imediato em cima de rate limit é jogar gasolina: você aumenta a pressão exatamente no momento em que precisava reduzir

Como prevenir:

  • Dimensione o pool pelos dois limites ao mesmo tempo. Estime o tamanho médio do seu state em tokens, multiplique pela concorrência pretendida e compare com o teto de tokens por segundo
  • Não trate esses números como pedra. A própria documentação avisa que os limites estão sendo ajustados dinamicamente e podem mudar sem aviso
  • Se o seu volume não cabe mesmo, limites maiores existem em planos custom e enterprise, via contato com [email protected]
  • E instrumente isso. Taxa de 429 por minuto e latência por item são o tipo de coisa que você quer no gráfico junto com as decisões do Jev em produção, não descobrindo por reclamação de usuário

Retry sem idempotência: quando a resposta certa vira efeito duplicado

Sintoma: você ativou o retry, o 429 sumiu, a felicidade durou um dia

Aí começam os relatos: registro processado duas vezes, e-mail duplicado pro mesmo usuário, contador de moderação inflado, item aparecendo repetido na fila de revisão

Causa: repetir a chamada ao Jev é seguro, ela é uma avaliação

O problema não é a chamada, é o EFEITO COLATERAL que você executa depois dela

Se o seu código faz "chama o Jev e já manda o e-mail", cada retry que atravessa metade do caminho pode disparar o efeito de novo

E atenção num ponto que confunde muita gente: o id da questão é só chave de correlação da resposta

Ele não é, e nunca foi, um mecanismo de deduplicação de requisição

Solução: separa em duas etapas bem marcadas

  1. Decidir: chama o Jev, pega as respostas tipadas
  2. Aplicar: grava e só então dispara o efeito

A gravação usa uma chave estável, montada por você, tipo id_do_item + id_da_questao

Com essa chave, a escrita vira idempotente: se o registro já existe com aquela chave, você atualiza e não reaplica o efeito

chave = f"{item['id']}:{'categoria'}"

if not repo.ja_processado(chave):
    repo.salvar(chave, resposta)
    aplicar_efeito(item, resposta)

Como prevenir: nunca coloque o efeito colateral dentro da mesma função que faz a chamada com retry

Essa separação é o que permite reprocessar backlog sem medo, e é exatamente o padrão que você quer no fluxo de moderar conteúdo enviado por usuários, onde aplicar a mesma punição duas vezes é problema de verdade

Como isolar o lote do caminho que o usuário vê na tela

Aqui vai o padrão que resolve o "minha aplicação travou"

O request HTTP do usuário não deveria esperar mil decisões acontecerem, simples assim

  1. O request do usuário só enfileira. Ele valida a entrada, grava um job com status pendente e devolve um identificador. Responde em milissegundos
  2. Um worker consome a fila com o pool limitado que você montou lá no passo 4. Ele roda fora do processo web, no ritmo que a API aguenta, não no ritmo da ansiedade do usuário
  3. O resultado é persistido por chave estável (aquele id_do_item + id_da_questao) e consultado depois, por polling ou por notificação

Por que isso importa tanto? Decisão do Jev que alimenta dashboard, triagem interna ou moderação em massa não tem nenhuma obrigação de bloquear o request HTTP

O usuário quer saber que o envio foi aceito, não quer segurar a conexão aberta enquanto 3 mil itens são avaliados

E quando vale fazer inline? Quando é UMA chamada única, com várias questões sobre o mesmo state

É justamente o caso barato e rápido: uma passada só, o documento enviado uma vez, todas as questões avaliadas de forma independente contra aquele mesmo state

Esse cenário cabe dentro do request sem drama

O que não cabe é laço de N chamadas dentro do controller… esse é o clássico que derruba o servidor em produção numa sexta à noite 😛

Três cenários de lote e o padrão certo para cada um

Cenário Formato do lote Padrão recomendado O que otimizar
A Um documento grande, muitas perguntas Agrupar tudo numa chamada só Custo de tokens de entrada
B Milhares de itens pequenos, mesmas questões Pool assíncrono com limite de paralelismo, uma chamada por item Latência total do lote
C Reprocessamento de backlog Fila, checkpoint por item e escrita idempotente Taxa de erro e retrabalho

Cenário A: um documento, muitas perguntas

Contrato, política interna, artigo comprido, relatório

Se você mandar uma chamada por pergunta, o documento inteiro viaja de novo em cada requisição

Agrupando, ele vai uma vez só

E como o Jev custa US$ 0,042 por 1 milhão de tokens de entrada, com tokens de saída não cobrados, o seu custo é basicamente o quanto de state você empurra

Mandar o mesmo documento 13 vezes é pagar 13 vezes por ele, não tem mágica

Só respeite os dois orçamentos: 64k pro state mais todas as questões, e 32k pro state mais a questão mais longa

Cenário B: milhares de itens pequenos

Comentários, tickets, mensagens, linhas de catálogo

Aqui cada item é um state diferente, então agrupar não se aplica: o caminho é o pool assíncrono com semáforo

O que você otimiza é latência total e estabilidade, calibrando o limite de concorrência contra tokens por segundo E requisições por minuto

Começa conservador, mede a taxa de 429, sobe aos poucos

Subir o limite até quebrar não é benchmark, é só quebrar

Cenário C: reprocessamento de backlog

Esse é o mais perigoso, porque você vai rodar o mesmo item de novo por definição

Fila, checkpoint por item e idempotência na escrita

Se o seu processo morre no item 2.847, ele precisa retomar dali sem reaplicar efeito nos 2.846 anteriores

Métrica que importa aqui não é velocidade, é taxa de erro e retrabalho

Tome cuidado com uma coisa: se você fixou jev-1.13.0 no lote original e reprocessa com jev-latest, você pode estar comparando resultado de modelos diferentes sem perceber

Conclusão

A regra que resume o post inteiro é uma ordem de decisões, não um truque

Agrupar antes de paralelizar: questões do mesmo state cabem numa chamada só, e o ganho de custo e latência é grande demais pra ignorar

Limitar antes de acelerar: semáforo no pool, sempre, com os dois limites da API na cabeça

Isolar antes de escalar: lote pesado sai do request do usuário e vai pra fila, com efeito colateral idempotente do seu lado

Próximo passo prático? Mede o que você tem hoje: custo e latência do lote atual, do jeito que ele roda agora

Depois testa o agrupamento por state e compara

E só então mexe no tamanho do pool, checando contra tokens por segundo e requisições por minuto ao mesmo tempo

Faça o teste e se liga nos números antes e depois, porque é aí que a diferença aparece =)

Até o próximo post!

Perguntas frequentes

Quantas requisições por minuto o Jev aguenta antes de retornar 429?

O limite publicado por conta é de 1.200 requisições por minuto e 250.000 tokens por segundo. Passar de qualquer um dos dois retorna HTTP 429 Too Many Requests. Vale lembrar que esses limites são ajustados dinamicamente e podem mudar sem aviso.

Vale a pena disparar uma chamada por questão pra paralelizar mais rápido?

Não, isso é o erro mais comum. Uma chamada com N questões dá exatamente as mesmas respostas que N chamadas com uma questão cada, só que mais lenta e mais cara quando o state é grande. No cookbook da TypeSafe, agrupar 13 questões numa chamada só ficou 12,2x mais barato e 10,0x mais rápido do que separar.

Como lidar com 429 sem travar a aplicação inteira?

A orientação oficial é repetir a requisição com backoff exponencial em vez de tentar de novo na hora. Os SDKs oficiais já fazem esse retry automaticamente e respeitam o header retry-after quando ele vem na resposta. Se quiser controlar isso na mão, dá pra passar uma RetryPolicy no SDK Python, inclusive desligando o retry com RetryPolicy(max_retries=0).

Preciso pagar por token de saída ao rodar muitas questões em paralelo?

Não, o Jev cobra só pelos tokens de entrada, US$ 0,042 por 1 milhão de tokens. Tokens de saída não são cobrados, o que ajuda bastante quando você tem muitas questões tipadas voltando resposta curta.

Dá pra estourar o limite de tokens mesmo rodando poucas questões?

Sim, existem dois orçamentos diferentes pra ficar de olho. O de 64k cobre o state mais todas as questões somadas, e o de 32k vale pro state mais a única questão mais longa daquele grupo. Um state grande com poucas questões enormes estoura o segundo limite mesmo sem chegar perto do primeiro.

O que muda entre jev-latest e jev-1.13.0 num lote grande?

jev-latest sempre aponta pro modelo mais recente da família Jev, enquanto jev-1.13.0 é uma versão fixada. Em lote, fixar a versão evita que o resultado mude entre execuções diferentes do mesmo pipeline.



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 Claude Code

Formação Claude Code

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

  • 120 aulas
  • 4 projetos
  • 9h 48min

Blog | Mais populares