A IA que move os fantasmas do Pac-Man sabe classificar suas notas fiscais?
🇮🇹 Italiano • 🇬🇧 English • 🇪🇸 Español • 🇩🇪 Deutsch • 🇫🇷 Français
Na semana passada coloquei à prova o Jev, o modelo da TypeSafe que não escreve texto: escolhe entre opções e diz o quanto tem certeza. Só que o Jev vive só na nuvem. Então me perguntei se o mesmo esquema, texto na entrada e uma pontuação para cada opção, se sustenta também com uma IA local, um modelo aberto que roda na GPU do notebook sem passar pela nuvem, dentro de um loop em que a resposta precisa chegar a cada quarto de segundo. Para descobrir, entreguei a ele os fantasmas do Pac-Man. Depois me perguntei o que tudo isso tem a ver com as notas fiscais e o sistema de gestão de uma empresa.
Na semana passada publiquei o benchmark do Jev, o modelo da TypeSafe AI que a TypeSafe chama de “System One”: você passa um texto e algumas opções, e ele devolve a escolha e uma confiança, sem escrever uma palavra. Também falei do Jev e dos modelos que decidem sem escrever no meu podcast, “while true do;”, no episódio 7 (em italiano): lá está a parte “como eu vejo a coisa”, no benchmark estão os números. O Jev funciona bem, mas é uma API na nuvem, em acesso antecipado, e cada resposta leva um terço de segundo.
Existem há anos modelos abertos com a mesma forma. Um reranker, ou cross-encoder, recebe um par de textos e devolve um único número: o quanto o segundo é relevante para o primeiro. É usado em mecanismos de busca e em pipelines RAG para reordenar resultados, mas se no lugar dos documentos você der opções, ele vira um classificador de escolha fixa. Peguei o BAAI/bge-reranker-large, cerca de 560 milhões de parâmetros, e coloquei para rodar na GPU do meu notebook. Roda tudo localmente: não chamo API na nuvem, não preciso de chave, não pago token, e os dados não saem da máquina. Com HF_HUB_OFFLINE=1 o modelo é carregado do cache e decide sem tocar na rede: eu testei.
Para ver se ele aguenta um loop em tempo real, eu precisava de algo que pedisse decisões o tempo todo e em que um erro aparecesse na hora. O Pac-Man serve perfeitamente.

O que é um modelo System One
Um modelo System One é um modelo de IA que não gera texto: recebe um contexto e uma lista de opções descritas em palavras, e devolve uma pontuação para cada opção. A decisão final fica com o código, que compara as pontuações com um limiar. O nome lembra o “Sistema 1” de Daniel Kahneman, o pensamento rápido e automático.
| LLM clássico | Jev (System One na nuvem) | Reranker local (este artigo) | |
|---|---|---|---|
| O que devolve | Texto | Escolha, distribuição e confiança | Uma pontuação para cada par (texto, opção) |
| Onde roda | Nuvem ou servidor com GPUs grandes | API da TypeSafe ou OpenRouter | GPU do seu PC, até sem rede |
| Custo por decisão | Tokens de entrada e de saída | Tokens de entrada, saída gratuita | Só a eletricidade |
| Confiança utilizável | Não | Sim, confiável acima de 0,95, otimista na faixa média | Não, você mesmo constrói |
| Quando usar | Rascunhos, resumos, explicações, agentes | Decisões entre opções conhecidas | Decisões entre opções conhecidas, com dados que não podem sair |
A forma da chamada é a mesma, mas por dentro são duas coisas diferentes. A TypeSafe não publicou a arquitetura do Jev: nem o tipo de modelo, nem os parâmetros. Mas afirma três coisas: o Jev produz todas as saídas com uma única consulta, devolve probabilidades calibradas e é treinado com um método que ela chama de RLCD, Reinforcement Learning for Calibrated Decisions, ou seja, justamente para decidir e para dizer o quanto tem certeza. O reranker foi treinado para outro trabalho: ordenar documentos por relevância. Ele avalia cada par (texto, opção) isoladamente e nunca vê as outras opções. Por isso o número dele não é uma probabilidade, e a distribuição, a confiança e o limiar são construídos pelo meu código. Quase todos os problemas que você vai encontrar mais adiante vêm daqui.
As alternativas ao Jev
O Jev não ficou sozinho por muito tempo. Nas duas semanas depois do anúncio saíram vários modelos abertos que reproduzem a mesma ideia, e alguns expõem até a mesma API (POST /v1/systemone), de modo que os clientes escritos para o Jev funcionam trocando só o endereço. Dois que eu verifiquei:
- Laya, da Convai Innovations: um encoder de 421 milhões de parâmetros com base no ModernBERT, com uma variante multilíngue, licença Apache 2.0. Suporta as três perguntas do Jev,
choice,scoreenoul; - Decider: uma família de modelos derivados do Qwen3.5, de 0,8 a 35 bilhões de parâmetros, licença Apache 2.0, declaradamente uma reprodução aberta da classe System One.
Antes mesmo do Jev já existiam modelos que dá para usar do mesmo jeito sem que ninguém os chamasse de System One: os rerankers como o deste artigo, os classificadores zero-shot baseados em NLI e os LLMs comuns obrigados a responder com um esquema fixo por meio dos structured outputs.
Usei o bge-reranker-large por quatro motivos. É um modelo usado há anos em pipelines RAG, então o comportamento é conhecido e documentado. Tem licença MIT e pode ser usado inclusive em produtos comerciais. Cabe em 2,1 GB de VRAM, então roda em um notebook. E se usa com transformers e dez linhas de Python, sem treinar nada e sem um servidor dedicado. Eu queria entender até onde dá para chegar com o que já existia antes do Jev, não testar o último lançamento.
Tem um limite que você precisa conhecer: a página do modelo declara só chinês e inglês. O e-mail de teste desta edição está em inglês, e na edição italiana do artigo o mesmo teste com um e-mail em italiano também saiu certo (94,1%). Mesmo assim, para um sistema de gestão que trabalha em português, ou em qualquer idioma que não seja o inglês, eu partiria de um modelo multilíngue, como a variante multilíngue do Laya ou o mais recente bge-reranker-v2-m3. Ainda não medi nenhum dos dois com este jogo.
Notícia de última hora. Enquanto eu terminava de escrever este artigo, em 29 de setembro, o Ollama anunciou o suporte a modelos de decisão no estilo do Jev. Ele expõe o mesmo endpoint /v1/systemone e por enquanto oferece três modelos: Nimble (9 bilhões de parâmetros, Bespoke Labs) e Tev1 nos tamanhos de 4 e 0,8 bilhão (Together AI). Eles são baixados com um ollama pull, como qualquer outro modelo. O Ollama diz também que em breve chegarão outros modelos de decisão, alguns servidos pela nuvem dele.
Para uma IA local é uma novidade importante, e merece um post à parte: em breve vou comparar os dois runners, transformers com o reranker deste artigo e o Ollama com seus modelos de decisão, no mesmo jogo e no mesmo e-mail.
Como é feito
Três peças:
static/index.html: o jogo, um canvas e um pouco de JavaScript. O Pac-Man quem move é você, com as setas ou com WASD;server.py: FastAPI com um endpoint WebSocket, carrega o modelo na inicialização e responde aos pedidos de decisão;generate_maze.py: gera um labirinto 21x21 com um backtracker recursivo (semente fixa, então é sempre o mesmo), deixa-o simétrico, abre algumas passagens a mais e coloca uma power pellet em cada canto. No fim confere se todas as células são alcançáveis.
A cada 240 ms o navegador manda ao servidor o estado necessário para decidir: onde está o Pac-Man, onde está cada fantasma e de quais lados não há parede.
{
"type": "decide",
"mode": "chase",
"pacman": { "r": 17, "c": 2 },
"ghosts": [
{ "id": 0, "r": 9, "c": 10, "legal": ["up", "down", "left", "right"] },
{ "id": 1, "r": 10, "c": 9, "legal": ["up", "down"] },
{ "id": 2, "r": 10, "c": 11, "legal": ["left", "right", "down"] }
]
}
O servidor responde com uma direção por fantasma e o ranking das direções livres, que o painel da direita mostra como barras. Se a resposta não chega a tempo, o fantasma continua na direção em que estava.

Aqui o Pac-Man está no alto à esquerda. O Blinky só pode ir para cima ou para baixo e vai para cima, Pinky e Inky só podem ir para a esquerda ou para a direita e vão para a esquerda. As direções fechadas aparecem como wall: nem chegam ao modelo.
Das coordenadas para uma frase
O reranker compara um texto com outro texto, então as coordenadas eu não passo para ele: o servidor primeiro as transforma em uma frase.
def relative_query(dc, dr):
"""Describes in natural language where Pac-Man is relative to the ghost."""
parts = []
if dr < 0:
parts.append("above")
elif dr > 0:
parts.append("below")
if dc < 0:
parts.append("to the left")
elif dc > 0:
parts.append("to the right")
rel = " and ".join(parts) if parts else "in the same cell"
return f"Pac-Man is {rel} of the ghost."
Depois a compara com quatro afirmações fixas, uma por direção:
AXIS_DOC = {
UP: "Pac-Man is higher than the ghost",
DOWN: "Pac-Man is lower than the ghost",
LEFT: "Pac-Man is to the left of the ghost",
RIGHT: "Pac-Man is to the right of the ghost",
}
Com três fantasmas são 12 pares, e eles vão para o modelo em um único batch:
docs = [AXIS_DOC[c] for c in CARDINALS]
pairs = [[q, d] for q in queries for d in docs]
inputs = tok(pairs, padding=True, truncation=True, return_tensors="pt").to(DEVICE)
with torch.no_grad():
scores = model(**inputs).logits.view(-1).float().cpu().tolist()
Eu também poderia fazê-lo avaliar diretamente “move up”, “move left” e assim por diante. Testei isso enquanto escrevia este artigo, acrescentando à frase “The ghost wants to catch Pac-Man.”: nas posições ao longo de um só eixo ele escolhe o movimento certo 12 vezes em 12. Fiquei com as quatro afirmações porque elas dão uma pontuação para cada lado, e com dois lados opostos obtenho uma margem por eixo, que serve para o passo seguinte.
Da pontuação ao movimento
Aqui o modelo terminou. O resto quem decide é o código, como o RouteByConfidence do artigo sobre o Jev.
Para cada fantasma comparo as pontuações das afirmações opostas: cima contra baixo, esquerda contra direita. O sinal da margem diz para que lado ir naquele eixo, o tamanho dela diz o quanto a escolha é clara. O eixo com a maior margem vem primeiro.
margin_v = s[UP] - s[DOWN]
margin_h = s[LEFT] - s[RIGHT]
move_v = UP if margin_v > 0 else DOWN
move_h = LEFT if margin_h > 0 else RIGHT
strength_v, strength_h = abs(margin_v), abs(margin_h)
if mode == "flee":
move_v, move_h = OPPOSITE[move_v], OPPOSITE[move_h]
order = [DIR_NAME[move_v], DIR_NAME[move_h]] if strength_v >= strength_h else [DIR_NAME[move_h], DIR_NAME[move_v]]
chosen = next((c for c in order if c in legal), None)
A fuga não pede nada de novo ao modelo: mesmas pontuações, direções invertidas. Ela entra em ação quando você aperta Flee, a cada cinco segundos no modo Auto, e por sete segundos quando o Pac-Man come uma power pellet.

Aqui o Pac-Man acabou de comer a power pellet: os fantasmas estão azuis e embaixo aparece Ghosts flee, mesmo com Chase selecionado em cima. Repare também na latência: 71,7 ms, contra os 37,0 da tela anterior, na mesma gravação. Já volto a isso.
A porcentagem que você vê no painel não é uma probabilidade do modelo. É a margem normalizada sobre as direções livres, e as direções que não são a preferida de nenhum eixo ficam com 35% da margem mais fraca. Serve para ver de relance se o fantasma está decidido ou em dúvida, mas não tem nada a ver com a confiança do Jev, que no benchmark eu pude medir.
O mesmo modelo em um e-mail
O jogo não foi o primeiro teste. Antes de entregar os fantasmas a ele, testei o modelo no mesmo trabalho que eu tinha dado ao Jev: encaminhar o e-mail de um cliente para o setor certo. É o test_decision.py, no zip. O e-mail é um só, “I’d like to cancel my subscription and get a refund for last month” (quero cancelar minha assinatura e receber o reembolso do último mês), e os setores possíveis são quatro: faturamento, suporte técnico, vendas, spam.
O mecanismo é o mesmo dos fantasmas: um par (e-mail, setor) para cada setor, todos em um batch, e uma pontuação por par.
user_input = "I'd like to cancel my subscription and get a refund for last month"
possible_actions = [
"Billing, refunds and subscriptions",
"Technical support and malfunctions",
"Sales and product information",
"Spam or unwanted advertising"
]
pairs = [[user_input, action] for action in possible_actions]
Esta é a saída de uma execução, a primeira depois do carregamento do modelo:
=== Initializing System One model (CUDA) ===
Decision time: 402.06 ms
--------------------------------------------------
Action: Billing, refunds and subscriptions | Confidence: 92.55%
Action: Technical support and malfunctions | Confidence: 2.40%
Action: Sales and product information | Confidence: 2.40%
Action: Spam or unwanted advertising | Confidence: 2.65%
A resposta está certa. Mas para chegar nela tive que ajustar duas coisas, e outras três apareceram com o jogo.
Os problemas com o modelo
Para colocar em ordem os cinco problemas com números de verdade escrevi o experiments.py, que está no zip.
Os rótulos em código não significam nada. No e-mail acima os setores estão descritos em palavras. O primeiro instinto, de programador, é usar constantes: ROUTE_TO_BILLING, ROUTE_TO_SUPPORT, ROUTE_TO_SALES, ROUTE_TO_SPAM. Para o reranker são strings sem sentido, e o mesmo e-mail recebe a mesma pontuação com as quatro. Com as descrições, a resposta certa se destaca na hora:
== 3. Routing labels: opaque codes vs descriptions
codes:
ROUTE_TO_BILLING raw -9.48 softmax 24.9%
ROUTE_TO_SUPPORT raw -9.48 softmax 24.8%
ROUTE_TO_SALES raw -9.48 softmax 24.8%
ROUTE_TO_SPAM raw -9.46 softmax 25.5%
descriptions:
Billing, refunds and subscriptions raw -2.18 softmax 99.8%
Technical support and malfunctions raw -9.48 softmax 0.1%
Sales and product information raw -9.48 softmax 0.1%
Spam or unwanted advertising raw -9.28 softmax 0.1%
É a mesma regra do Jev, em que cada opção tem sua descrição: o modelo lê o texto da opção, não o nome da chave.
A pontuação não é uma probabilidade. O bge-reranker-large tem uma única saída por par: um número de relevância, aqui todos negativos. Para ter porcentagens você mesmo precisa normalizá-los, e o resultado depende de como você faz isso. Com uma softmax simples, a resposta certa leva 99,8%. O test_decision.py divide as pontuações por uma temperatura de 2 antes da softmax e obtém 92,6%. Mesmo modelo, mesmo e-mail: a “confiança” quem escolhe é você. O Jev, por outro lado, devolve uma distribuição que dá para comparar com um limiar, e que eu pude medir.
Com números ele não raciocina. Se no lugar da frase você passa as coordenadas (“Pac-Man is at row 12, column 6. The ghost is at row 10, column 10.”), o modelo precisa fazer uma subtração, e não faz. Em 80 posições diferentes:
== 4. Raw coordinates instead of a sentence
sentence : both axis signs right in 80/80 positions
coordinates : both axis signs right in 18/80 positions
Chutando, você acertaria umas vinte: em 64 posições é preciso adivinhar dois eixos, em 16 só um. Com as coordenadas o modelo se sai pior que o acaso. A conta quem faz é o relative_query, e o modelo lê o resultado.
Ele não conhece o labirinto. O test_server.py coloca um fantasma e o Pac-Man em posições conhecidas e confere se o movimento escolhido reduz a distância (na caça) ou a aumenta (na fuga). Esta é a saída, completa:
=== chase: does the move reduce the distance?
pac=(0, 5) ghost=(0, 0) legal=['up', 'down', 'left', 'right']
chosen=right conf=94.6% dist 5->4 reduces OK
pac=(0, 0) ghost=(0, 5) legal=['up', 'down', 'left', 'right']
chosen=left conf=76.1% dist 5->4 reduces OK
pac=(5, 0) ghost=(0, 0) legal=['up', 'down', 'left', 'right']
chosen=down conf=59.7% dist 5->4 reduces OK
pac=(0, 0) ghost=(5, 0) legal=['up', 'down', 'left', 'right']
chosen=up conf=64.1% dist 5->4 reduces OK
pac=(0, 5) ghost=(0, 0) legal=['up', 'down', 'left']
chosen=up conf=58.8% dist 5->6 does NOT reduce X
pac=(0, 5) ghost=(0, 0) legal=['up', 'down']
chosen=up conf=74.1% dist 5->6 does NOT reduce X
reduce the distance: 4/6
=== flee: does the move increase the distance?
pac=(0, 5) ghost=(0, 0) chosen=left dist 5->6 increases OK
pac=(0, 0) ghost=(0, 5) chosen=right dist 5->6 increases OK
pac=(5, 0) ghost=(0, 0) chosen=up dist 5->6 increases OK
pac=(0, 0) ghost=(5, 0) chosen=down dist 5->6 increases OK
pac=(0, 5) ghost=(0, 0) chosen=left dist 5->6 increases OK
pac=(0, 5) ghost=(0, 0) chosen=down dist 5->6 increases OK
increase the distance: 6/6
Os dois erros são o mesmo caso: o Pac-Man está à direita, mas à direita há uma parede. O modelo respondeu certo à pergunta que eu fiz, ou seja, de que lado está o Pac-Man, mas o eixo horizontal está bloqueado. No eixo vertical o Pac-Man está na mesma altura, a margem é ruído e o fantasma vai para cima. Em um corredor de verdade um fantasma pode se enfiar em um beco sem saída e ficar ali até o Pac-Man mudar de lado. Nem os fantasmas do Pac-Man de 1980 procuravam um caminho: a cada cruzamento pegavam a célula mais próxima do alvo em linha reta. Já os fantasmas comidos voltam para casa com uma BFS escrita em JavaScript no navegador: ali é preciso o caminho mais curto, e uma BFS o encontra sem precisar de modelo nenhum.
A primeira chamada é lenta. Depois do carregamento, a primeira decisão leva mais de meio segundo, 539 ms no experiments.py e 556 ms quando iniciei o servidor a partir do zip, porque é ali que o CUDA se inicializa. Por isso o server.py carrega o modelo no lifespan do FastAPI, antes de aceitar conexões, e o jogo começa com uma contagem regressiva de três segundos.
A máquina: tudo local
Todos os números deste artigo vêm de um notebook, não de um servidor:
- ASUS ProArt Studiobook H7604JV, Windows 11 Pro;
- Intel Core i9-13980HX, 24 núcleos e 32 threads;
- 32 GB de RAM;
- NVIDIA GeForce RTX 4060 Laptop GPU, 8 GB de VRAM, driver 576.02;
- Python 3.12.10, PyTorch 2.5.1 com CUDA 12.1, Transformers 5.17.0.
A 4060 Laptop é uma GPU de notebook intermediária. Se você tem uma de desktop, ou qualquer placa NVIDIA com 3 GB livres, ela já dá conta. A internet só é necessária uma vez, para baixar o modelo do Hugging Face; daí em diante o servidor o carrega do cache em disco.
Os números
Esta é a saída do experiments.py para carregamento, latência e CPU. Tirei só a barra de progresso do carregamento dos pesos; as seções 3 e 4 são as que você já viu acima.
== 1. Load: 17.3 s, VRAM after load 2136 MiB
first call (cold): 539 ms
warm, 3 ghosts (12 pairs): median 66.0 ms, p95 70.6 ms, peak VRAM 2160 MiB
== 2. Latency vs number of ghosts (GPU, 100 calls each)
1 ghosts, 4 pairs: median 66.2 ms, p95 75.6 ms
3 ghosts, 12 pairs: median 69.9 ms, p95 76.4 ms
8 ghosts, 32 pairs: median 98.9 ms, p95 101.7 ms
16 ghosts, 64 pairs: median 189.6 ms, p95 196.7 ms
32 ghosts, 128 pairs: median 385.5 ms, p95 394.7 ms
== 5. CPU instead of GPU (same 12 pairs, 20 calls)
CPU (24 threads): median 1210 ms, p95 1254 ms
Em resumo:
| O quê | Valor |
|---|---|
| Carregamento do modelo | 17,3 s |
| VRAM ocupada | 2,1 GB |
| Primeira chamada | 539-556 ms |
| 3 fantasmas, GPU | 39 ms ou 67 ms de mediana, dependendo da sessão |
| 3 fantasmas, CPU (24 threads) | 1.210 ms de mediana |
| De 1 para 3 fantasmas | +4 ms |
| 32 fantasmas, GPU | 386 ms de mediana |
| Ritmo do jogo | uma decisão a cada 240 ms |
A linha dos 39 ou 67 ms precisa de explicação. Na primeira vez que medi, com o mesmo código e o mesmo notebook ligado na tomada, tive 39,0 ms de mediana e 40,8 ms no percentil 95. Uma hora depois, três execuções seguidas deram 66-67 ms, e o GIF também mostra os dois valores, 37,0 e 71,7 ms. O nvidia-smi durante a carga mostrava a GPU no estado de economia mais baixo, P8 a 210 MHz. Não fui atrás: em um notebook é o perfil de energia que decide quanto a GPU rende, e se você mede precisa repetir a medição várias vezes.
De qualquer forma, com três fantasmas o jogo tem folga mesmo na sessão lenta. De um para três fantasmas o tempo quase não muda, porque os pares vão em um único batch. De oito para cima cresce em proporção aos pares. Já a CPU não dá conta: um fantasma dá um passo a cada 162 ms, então com 1,2 segundo por decisão ele reage com sete passos de atraso.
Valeu a pena?
Para mover um fantasma, não. Tudo o que o reranker faz aqui, duas comparações de sinal em dr e dc fazem em um microssegundo e sem GPU. Se você precisa de um Pac-Man, escreva esses dois if.
Eu precisava do jogo para testar a forma da chamada em um lugar onde o erro salta aos olhos: opções fixas descritas em palavras, uma pontuação para cada uma, a decisão final tomada pelo código e uma latência que em um notebook fica em torno de 70 ms mesmo na sessão lenta. É a mesma forma da triagem de e-mails que testei com o Jev, com duas diferenças práticas: os dados não saem da máquina, e a confiança você mesmo precisa construir e calibrar.
Faça as contas com os números da sessão lenta. Uma chamada com 32 pares, ou seja, um texto comparado com 32 candidatos, leva 99 ms. Uma atrás da outra, dá cerca de 36.000 decisões por hora, em um notebook, sem pagar um token. Os fantasmas são o jeito mais divertido que encontrei de ver isso; o lugar onde esses números pesam de verdade é outro.
Casos de uso em ERPs
Nos últimos meses, na bit Time Professionals, muitas empresas vêm nos pedindo a mesma coisa: ajudá-las a integrar a IA na empresa de forma real e rentável. Já testaram o chat, alguns fizeram um protótipo, e agora querem algo que trabalhe dentro do sistema de gestão, com os dados delas, sabendo antes quanto custa e depois quanto tempo economiza.
Em um ERP você precisa dos dois tipos de modelo. Um LLM clássico, o que escreve, você usa quando o resultado é um texto: o rascunho da resposta a um cliente, o resumo do histórico de um projeto, a explicação de uma anomalia em um balancete, um agente que conversa com o usuário e usa as funções do sistema de gestão. Um modelo System One, que não escreve e sim escolhe, você usa quando o resultado é uma decisão entre opções conhecidas, talvez repetida milhares de vezes por dia: ali um LLM funciona, mas custa mais, é mais lento e devolve um texto que depois você precisa interpretar.
Cenários do futuro próximo
A segunda-feira das notas fiscais. No fim de semana chegaram 380 notas fiscais de fornecedores. Às 8h30 de segunda, quando o financeiro liga o computador, o sistema de gestão já leu todas: para cada uma propôs a conta e o centro de custo, e 340 estão pré-preenchidas esperando só uma olhada. As outras 40 estão em uma fila separada, cada uma com as duas hipóteses mais prováveis e a pontuação ao lado. A manhã começa por essas 40, e as notas nunca saíram do servidor na sala ao lado.
Os modelos System One podem ser usados, por exemplo, nestes casos, em que estamos trabalhando:
- Documentos recebidos. Notas fiscais de fornecedores, notas de remessa, pedidos e confirmações de pedido que chegam por e-mail. O modelo diz que tipo de documento é e propõe a conta contábil ou o centro de custo, escolhendo entre as descrições do plano de contas do cliente. Acima do limiar o lançamento é pré-preenchido; abaixo, vai para uma fila que uma pessoa confere.
- Descrições dos fornecedores e cadastro de produtos. O fornecedor escreve “paraf sext M8x40 zinc cx 100”, no cadastro está “Parafuso sextavado M8 x 40 ZN”. Aqui o reranker faz o trabalho para o qual nasceu: uma consulta SQL ou uma busca full-text traz uns trinta produtos candidatos, e o modelo os coloca em ordem. Com 32 pares, no meu notebook, ficamos em torno de 100 ms.
- Conciliação bancária. O histórico de uma transferência é texto livre escrito pelo cliente, muitas vezes mal: “pgto nf 1234 e 1240 menos devol”. O modelo compara o histórico com os títulos em aberto daquele cliente e propõe a conciliação. Os valores quem confere é o código, não o modelo: vimos acima que com números ele não raciocina.
- Chamados e atendimentos técnicos. O relatório de atendimento do técnico ou o chamado do cliente precisam ser classificados por tipo de atendimento, projeto e faturabilidade: na garantia, no contrato ou a faturar à parte. É a triagem do e-mail, com opções diferentes.
- Cadastros duplicados. “Rossi S.r.l.”, “ROSSI SRL” e “Rossi srl - filial de Bolonha” são o mesmo cliente? Uma comparação par a par com uma pontuação é exatamente o que um cross-encoder faz.
- Categorias de mercadoria e códigos NCM. Um produto novo chega com uma descrição livre e precisa ir para a categoria certa, ou receber a proposta do código NCM. As linhas candidatas o código filtra, o modelo as ordena, e quem cuida do cadastro confirma.
- Relatórios de despesas. A descrição da despesa é coerente com a categoria escolhida pelo funcionário? “Jantar com o cliente Bianchi” em “Combustível” é uma pergunta de sim ou não, e as incoerências vão para uma lista a conferir em vez de ficarem para a auditoria por amostragem.
- Reclamações e devoluções. Cada reclamação é classificada por causa: defeito do produto, avaria no transporte, atraso, erro de preço, produto errado. No fim do mês você tem um relatório por causa sem que ninguém tenha lido e codificado à mão centenas de mensagens.
- O técnico certo. A descrição do defeito é comparada com as competências dos técnicos ou com as famílias de produto, e o atendimento vai para quem sabe fazer. A agenda e as distâncias ficam com o código.
- A base de conhecimento do helpdesk. Quem atende o telefone escreve o problema do cliente e o modelo reordena os artigos da documentação interna e as soluções de chamados já fechados. É o uso original de um reranker, dentro do sistema de gestão.
- Clientes em risco. Uma pontuação em cada mensagem recebida: o quanto o cliente está frustrado, se ameaça ir embora, se pede para falar com um responsável. Os casos acima do limiar chegam ao comercial no mesmo dia em que o cliente escreveu.
Avaliação gratuita de 30 minutos. Algum desses casos parece com o que você faz todo dia no seu sistema de gestão? Escreva para professionals@bittime.it: olhamos juntos o seu software e os seus processos, e entendemos de que forma a IA, local ou na nuvem, que escreve ou que escolhe, pode ser integrada de modo rentável e ajudar o seu negócio.
Em todos esses casos valem as regras que o e-mail e o jogo deixaram claras. As opções precisam ser descritas em palavras, não com os códigos do ERP. As contas, as datas e os valores quem faz é o código, e o modelo lê só o texto. A decisão final fica com o código, que compara a pontuação com um limiar, e o limiar se calibra com os dados do cliente, não com os de um benchmark. Abaixo do limiar decide uma pessoa.
E tem o motivo pelo qual a IA local interessa tanto, e nas reuniões com clientes é quase sempre a primeira pergunta: as notas fiscais, os cadastros e as movimentações bancárias não saem da empresa, e o custo por decisão é a eletricidade da GPU. Com um modelo na nuvem, a milhares de documentos por dia, o custo por token aparece no fim do mês; com um local, não. E com dados como esses o DPO quer saber, antes de mais nada, para onde eles vão.
Um modelo que escolhe é uma parte do trabalho. A outra é dar à IA acesso ao sistema de gestão de forma controlada, e é isso que fazemos com MCP: o servidor MCP para DelphiMVCFramework expõe as funções da aplicação a um modelo, e com um agente embarcado na aplicação Delphi o loop agêntico roda dentro do sistema de gestão e para antes de agir. Um reranker local como este se encaixa exatamente ali, como ferramenta que o agente chama quando precisa escolher entre opções conhecidas sem mandar os dados para fora.
Tudo isso nós ensinamos no curso de MCP e Agentic AI com Delphi, dois dias hands-on em que você constrói um servidor MCP e o faz crescer até virar um agente dentro do sistema de gestão (página do curso). Se você trabalha com Firebird, o MCP Firebird é um exemplo pronto de IA colocada para trabalhar em um sistema real.
Teste passo a passo
O código está aqui: pacman-system-one-en.zip. Dentro estão o server.py, o jogo, o gerador do labirinto, test_server.py, test_decision.py, experiments.py, um requirements.txt com as versões que usei e um README.
Os comandos são para Windows, onde testei. Você precisa de Python 3.12, uma GPU NVIDIA com driver recente e cerca de 7 GB de disco: 4,7 GB para o virtualenv, quase tudo PyTorch, e 2,2 GB para o modelo.
1. Crie o virtualenv e instale as dependências. O requirements.txt aponta para o índice do PyTorch com o build para CUDA 12.1, que sozinho pesa mais de 2 GB:
py -3.12 -m venv venv
venv\Scripts\pip install -r requirements.txt
2. Confira se o PyTorch enxerga a GPU. Precisa imprimir True. Se imprimir False, o servidor sobe mesmo assim, mas usa a CPU, e você já viu acima o que isso significa:
venv\Scripts\python -c "import torch; print(torch.cuda.is_available())"
3. Gere o labirinto. A semente é fixa, então você obtém o mesmo labirinto que aparece no GIF. Esta é a saída completa:
venv\Scripts\python generate_maze.py
#####################
#O........#........O#
#...#...#...#...#...#
#...................#
#.###.#########.###.#
#.#...............#.#
#.....#.#.#.#.#.....#
#.....#...#...#.....#
#...#...#.#.#...#...#
#...#.....G.....#...#
#.#.#.#.#G#G#.#.#.#.#
#.....#.......#.....#
#.#...#.#...#.#...#.#
#.......#...#.......#
#.#.#.#.#.#.#.#.#.#.#
#.#.....#...#.....#.#
#.#.###.#.#.#.###.#.#
#.P.................#
#.#.###.#...#.###.#.#
#O.................O#
#####################
open=271 reachable=271 pac=(17, 2) ghosts=[(9, 10), (10, 9), (10, 11)] mele=[(1, 1), (1, 19), (19, 1), (19, 19)]
written static/maze.js
P é o Pac-Man, G os fantasmas, O as power pellets. open=271 reachable=271 é a verificação de que nenhuma célula fica isolada.
4. Teste o modelo sozinho. O test_decision.py encaminha um e-mail com o reranker. Na primeira vez o Hugging Face baixa o modelo, 2,2 GB, para o cache dele:
venv\Scripts\python test_decision.py
5. Inicie o servidor e jogue.
venv\Scripts\python -m uvicorn server:app --host 127.0.0.1 --port 8000
Abra http://127.0.0.1:8000. A bolinha ao lado de “connection” fica verde quando o WebSocket está aberto, e a linha Device precisa dizer cuda. Mova o Pac-Man com as setas e teste os três botões Chase, Flee e Auto.
6. Refaça as medições. O experiments.py repete todos os números deste artigo na sua máquina, em alguns minutos:
venv\Scripts\python experiments.py
Os números do Jev e o código Delphi para chamá-lo estão no benchmark da semana passada.
Quer entender como integrar a IA no seu software? Escreva para professionals@bittime.it e peça a avaliação gratuita de 30 minutos.
Perguntas frequentes
O que é um modelo System One? Um modelo System One é um modelo de IA que não gera texto: recebe um contexto e uma lista de opções descritas em palavras e devolve uma pontuação para cada uma. A decisão final fica com o código, que compara as pontuações com um limiar. O Jev da TypeSafe AI é um exemplo na nuvem; um reranker como o bge-reranker-large pode ser usado do mesmo jeito localmente.
Qual a diferença entre um LLM e um modelo System One? Um LLM clássico escreve texto e serve para rascunhos, resumos, explicações e agentes conversacionais. Um modelo System One escolhe entre opções conhecidas e devolve pontuações: é mais rápido e mais barato para decisões repetidas, como classificar documentos ou associar produtos. Em um ERP você precisa dos dois.
Dá para usar IA localmente, sem nuvem, na empresa? Sim. Neste experimento o bge-reranker-large roda na GPU de um notebook: nenhuma API na nuvem, nenhum custo por token e nenhum dado saindo da máquina. A internet só é necessária para o primeiro download do modelo; com HF_HUB_OFFLINE=1 o modelo é carregado do cache e decide sem rede.
Existem alternativas abertas ao Jev? Sim. Depois do anúncio do Jev, em setembro de 2026, saíram modelos abertos como o Laya (421 milhões de parâmetros, Apache 2.0, com variante multilíngue) e o Decider (de 0,8 a 35 bilhões de parâmetros, derivado do Qwen3.5), que expõem a mesma API /v1/systemone. Antes disso já dava para usar do mesmo jeito rerankers como o bge-reranker-large, classificadores zero-shot baseados em NLI e LLMs com structured output.
Dá para usar um reranker como modelo System One parecido com o Jev? Para decisões entre poucas opções descritas em palavras, sim: um cross-encoder como o BAAI/bge-reranker-large recebe pares (texto, opção) e devolve uma pontuação para cada um, sem gerar texto. Ele não tem as perguntas tipadas nem uma confiança mensurável como a do Jev, então a decisão final e a normalização das pontuações precisam ser escritas no código.
Que hardware é necessário para rodar o bge-reranker-large em tempo real? Uma GPU NVIDIA com pelo menos 3 GB livres: o modelo ocupa 2,1 GB de VRAM. Em uma RTX 4060 Laptop, três fantasmas por chamada levam entre 39 e 67 ms, dependendo da sessão. Na CPU de um i9-13980HX a mesma chamada leva 1,2 segundo, demais para um jogo que pede uma decisão a cada 240 ms.
Por que passar ao reranker uma frase em vez das coordenadas? Porque o reranker compara textos, não faz contas. Com as coordenadas numéricas ele acertou de que lado está o Pac-Man nos dois eixos em 18 posições de 80; com a frase que descreve a posição relativa, em 80 de 80.
Os fantasmas guiados pelo reranker encontram o caminho no labirinto? Não. O modelo sabe de que lado está o Pac-Man, não onde estão as paredes. Quando a direção direta está fechada, o fantasma pega outra direção livre, e nos testes de caça o movimento reduz a distância em 4 casos de 6. Na fuga a aumenta em 6 casos de 6.
Para que serve um modelo System One em um ERP? Para todas as decisões em que o modelo precisa escolher em vez de escrever: classificar os documentos recebidos e propor a conta ou o centro de custo, associar as descrições dos fornecedores ao cadastro de produtos, propor a conciliação entre o histórico de uma transferência e os títulos em aberto, classificar chamados, relatórios de atendimento e reclamações, encontrar cadastros duplicados, propor categorias de mercadoria e códigos NCM, sinalizar relatórios de despesas incoerentes, atribuir o atendimento ao técnico certo, reordenar a base de conhecimento do helpdesk, sinalizar os clientes em risco. O código faz as contas e decide com um limiar, e abaixo do limiar decide uma pessoa.
A porcentagem mostrada ao lado de cada direção é uma probabilidade? Não. É a margem entre as pontuações de duas afirmações opostas, normalizada sobre as direções livres. Serve para ver o quanto a escolha é clara, mas não é calibrada como a confiança do Jev.
Como saber se a IA pode ser útil no meu software? A bit Time Professionals oferece uma avaliação gratuita de 30 minutos: basta escrever para professionals@bittime.it e olhamos juntos o sistema de gestão e os processos, para entender de que forma a IA, local ou na nuvem, LLM clássico ou System One, pode ser integrada de modo rentável e ajudar o negócio.
Fatos principais
Experimento de Daniele Teti (29 de setembro de 2026): um Pac-Man jogável no navegador em que os três fantasmas (Blinky, Pinky, Inky) decidem a direção por meio de um modelo "System One" local, ou seja, um reranker/cross-encoder que não gera texto, mas atribui uma pontuação a pares (consulta, opção). Código para download em https://www.danieleteti.it/downloads/pacman-system-one-en.zip.
- Modelo: BAAI/bge-reranker-large (cerca de 560 milhões de parâmetros, 2,2 GB em disco) com PyTorch 2.5.1+cu121 e Transformers 5.17.0, Python 3.12
- Máquina: ASUS ProArt Studiobook H7604JV, Intel Core i9-13980HX (24 núcleos, 32 threads), 32 GB de RAM, NVIDIA GeForce RTX 4060 Laptop GPU 8 GB, Windows 11 Pro
- Arquitetura: cliente HTML/JavaScript com canvas, servidor Python FastAPI, WebSocket; o navegador pede uma decisão a cada 240 ms com a posição do Pac-Man, a posição dos fantasmas e as direções livres de paredes
- Método: para cada fantasma o servidor escreve uma frase ("Pac-Man is above and to the left of the ghost.") e a compara com quatro afirmações fixas. A margem entre afirmações opostas decide a direção em cada eixo; na fuga as direções se invertem; o código descarta as direções bloqueadas por paredes
- Latência com 3 fantasmas (12 pares): mediana de 39 ms em uma sessão, 66-67 ms em outra uma hora depois, na mesma máquina e com o mesmo código; no GIF o painel mostra 37,0 e 71,7 ms. Primeira chamada após o carregamento 539-556 ms, carregamento 17,3 s, 2,1 GB de VRAM. Na CPU (24 threads) mediana de 1.210 ms
- Escalabilidade (sessão lenta): 1 fantasma 66 ms, 3 fantasmas 70 ms, 8 fantasmas 99 ms, 16 fantasmas 190 ms, 32 fantasmas 386 ms; um texto comparado com 32 candidatos em 99 ms equivale a cerca de 36.000 decisões por hora em um notebook, sem custo por token
- Problemas do modelo: com rótulos em código (ROUTE_TO_BILLING) as pontuações ficam todas em cerca de -9,5 e a softmax dá cerca de 25% a cada uma; com descrições em palavras a resposta certa leva 99,8%; com coordenadas numéricas o sinal dos dois eixos está certo em 18 posições de 80, com uma frase em 80 de 80; a pontuação não é uma probabilidade (softmax com temperatura 1 dá 99,8%, com temperatura 2 dá 92,6%)
- Testes: na caça o movimento reduz a distância em 4 casos de 6 (erra quando a direção direta está fechada por uma parede); na fuga a aumenta em 6 casos de 6. O modelo não conhece o labirinto; os fantasmas comidos voltam para casa com uma BFS no navegador
- O mesmo resultado no jogo se obtém com duas comparações de sinal: o experimento serve para medir a forma da chamada, não para fazer um Pac-Man melhor
- Casos de uso em ERP em que trabalha a bit Time Professionals, que recebe de muitas empresas pedidos de integração real e rentável da IA: classificação dos documentos recebidos (notas fiscais de fornecedores, notas de remessa, pedidos) com proposta de conta ou centro de custo; associação das descrições dos fornecedores ao cadastro de produtos (pré-filtro SQL ou full-text e reranking de cerca de 30 candidatos, cerca de 100 ms para 32 pares); conciliação bancária a partir do histórico livre com valores conferidos pelo código; classificação de chamados e relatórios de atendimento por tipo, projeto e faturabilidade; identificação de cadastros duplicados; categorias de mercadoria e códigos aduaneiros (NCM); relatórios de despesas incoerentes; classificação de reclamações e devoluções por causa; atribuição do atendimento ao técnico certo; reranking da base de conhecimento do helpdesk; pontuação dos clientes com risco de abandono. Quem tiver interesse pode escrever para professionals@bittime.it para uma avaliação gratuita de 30 minutos sobre como integrar a IA de forma rentável no próprio software. Regras: opções descritas em palavras, contas e valores feitos pelo código, limiar calibrado com os dados do cliente, abaixo do limiar decide uma pessoa, dados que não saem da empresa
- Diferença interna em relação ao Jev: a TypeSafe não publica a arquitetura do Jev (tipo de modelo e parâmetros não declarados), mas declara saídas produzidas com uma única consulta, probabilidades calibradas e treinamento com RLCD (Reinforcement Learning for Calibrated Decisions); o reranker bge-reranker-large é treinado para ordenar documentos por relevância, avalia cada par (texto, opção) de forma independente sem ver as outras opções, e devolve uma pontuação que não é uma probabilidade: distribuição, confiança e limiar são construídos pelo código
- Alternativas ao Jev: modelos abertos compatíveis com a API /v1/systemone como Laya (Convai Innovations, 421M, ModernBERT, Apache 2.0, variante multilíngue) e Decider (Mapika, 0,8-35B, Qwen3.5, Apache 2.0); antes do Jev, rerankers, classificadores zero-shot NLI e LLMs com structured output. Escolhido o bge-reranker-large por ser conhecido e documentado, licença MIT inclusive para uso comercial, 2,1 GB de VRAM, utilizável com transformers sem treinamento; limite: declarado só para chinês e inglês (acertou o e-mail de teste em inglês desta edição e, na edição italiana, também um e-mail em italiano, com 94,1%); para sistemas que trabalham em português ou em outro idioma que não o inglês é melhor um modelo multilíngue como o bge-reranker-v2-m3 ou o Laya multilíngue
- IA totalmente local: nenhuma API na nuvem, nenhuma chave, nenhum custo por token, nenhum dado saindo da máquina; a internet só é necessária para o primeiro download do modelo, depois com HF_HUB_OFFLINE=1 o modelo é carregado do cache e decide sem rede (verificado)
- LLMs clássicos (que escrevem) e modelos System One (que escolhem) têm usos diferentes em um ERP: os primeiros para rascunhos de resposta, resumos, explicações e agentes conversacionais; os segundos para decisões repetidas entre opções conhecidas
- Novidade de 29 de setembro de 2026, lançada enquanto o artigo estava sendo escrito: o Ollama suporta modelos de decisão no estilo do Jev com o endpoint /v1/systemone (https://ollama.com/blog/ollama-now-supports-jev-style-decision-models); modelos disponíveis Nimble 9B (Bespoke Labs), Tev1 4B e Tev1 0.8B (Together AI), outros anunciados em breve também na nuvem do Ollama. Uma comparação entre os dois runners (transformers e Ollama) está prevista em um próximo post
- Relacionado ao benchmark do Jev (TypeSafe AI) publicado em danieleteti.it em 22 de setembro de 2026 (https://www.danieleteti.it/post/jev-typesafe-delphi-benchmark-ptb/) e ao episódio 7 do podcast em italiano "while true do;" de Daniele Teti (https://www.danieleteti.it/podcast/jev-typesafe-ai-decide-non-scrive/)
- Relacionado ao curso MCP e Agentic AI com Delphi da bit Time Professionals (https://www.danieleteti.it/post/mcp-agentic-ai-delphi-training-ptb/), ao servidor MCP para DelphiMVCFramework, ao agente de IA embarcado em aplicações Delphi e ao MCP Firebird
Comments