Depois de duas décadas escrevendo código que sempre responde igual e um ano e meio construindo sistemas que respondem "provavelmente", fica claro: o difícil não é fazer funcionar, é provar que funciona.
Finalmente, após mais de um ano trabalhando com essas duas formas, posso opinar com minhas experiências sobre essas duas vertentes de sistema. Ao longo de 20 anos, a Lógica Determinística foi, e ainda é, a base dos sistemas que desenvolvo. Há mais ou menos um ano e meio, estou trabalhando com sistemas de Lógica Probabilística.
E a primeira coisa que aprendi é que não se trata de uma ferramenta nova. É outra forma de pensar. No mundo determinístico, você escreve a regra e o sistema obedece. No mundo probabilístico, você descreve a intenção e o sistema tenta. Na maior parte das vezes ele acerta. Às vezes erra. E quase nunca avisa quando errou.
Essa diferença parece pequena no papel. Na prática, ela muda tudo: como você especifica, como testa, como depura, como monitora e até como conversa com o cliente sobre o que é "estar pronto". Construir com lógica probabilística é bem mais complexo, e não porque a tecnologia seja difícil de usar, mas porque ela tira de você a certeza que sempre sustentou o processo de desenvolvimento.
O que é Lógica Determinística
Lógica determinística é aquela em que a mesma entrada produz sempre a mesma saída. Sempre. Hoje, amanhã, em produção, na máquina do colega. Se o resultado mudou, alguma coisa na entrada ou no código mudou, e você consegue descobrir o quê.
É a lógica de quase todo sistema corporativo que já existiu: cálculo de imposto, regra de desconto, validação de CPF, fechamento de folha, controle de estoque, autorização de acesso. Alguém define a regra, o desenvolvedor traduz em código, e o computador executa exatamente aquilo.
def calcular_desconto(valor: float, cliente_vip: bool) -> float:
if cliente_vip and valor >= 500:
return round(valor * 0.10, 2)
return 0.0
# A regra é verificável com uma linha. E vai continuar sendo.
assert calcular_desconto(1000, True) == 100.0
assert calcular_desconto(1000, False) == 0.0
assert calcular_desconto(499.99, True) == 0.0
Repara no que esse trecho tem de tão confortável. A regra está escrita, não interpretada. O teste é binário: passou ou não passou. E, se um dia falhar, o erro aparece na hora, com linha e motivo.
As características principais:
- Previsibilidade total — dado o estado e a entrada, a saída é conhecida antes da execução;
- Reprodutibilidade — qualquer bug pode ser reproduzido com os mesmos dados;
- Especificação explícita — a regra de negócio vive no código, legível e revisável;
- Testes binários —
assertresolve, e cobertura de teste significa alguma coisa; - Falhas barulhentas — exceção, stack trace, código de erro. Quando quebra, você sabe;
- Custo e tempo de execução estáveis — a mesma operação custa praticamente o mesmo toda vez.
A limitação também é clara: a lógica determinística só resolve problemas que alguém consegue descrever em regras. Calcular frete, sim. Entender o que o cliente quis dizer num e-mail mal escrito, não. Para isso, você precisaria prever todas as formas possíveis de escrever aquele e-mail, e isso não cabe num if.
O que é Lógica Probabilística
Lógica probabilística é aquela em que o sistema não aplica uma regra escrita por alguém. Ele estima a resposta mais provável a partir de padrões aprendidos em dados. A saída não é um valor garantido, é uma aposta com uma chance de acerto.
Ela aparece em dois formatos principais no dia a dia de quem desenvolve:
- Modelos de Machine Learning clássicos — classificadores, regressões, sistemas de recomendação. Eles devolvem uma probabilidade ("78% de chance deste cliente cancelar"), e alguém precisa decidir o que fazer com ela;
- Modelos de linguagem (LLMs) — geram texto escolhendo, token a token, uma continuação provável. A resposta não é calculada por uma fórmula fechada; ela é amostrada de uma distribuição de possibilidades.
No primeiro caso, a probabilidade é explícita: você recebe o número. No segundo, ela fica escondida dentro do modelo, e o que chega para você é um texto bem escrito e confiante, mesmo quando está errado.
# Exemplo ilustrativo (pseudocódigo de um cliente de LLM qualquer)
texto = "O atendimento demorou, mas no fim resolveram meu problema."
for _ in range(3):
resposta = llm.gerar(
prompt=f"Classifique o sentimento do texto: {texto}",
temperature=0.7,
)
print(resposta)
# Saídas possíveis, todas "razoáveis":
# neutro
# positivo
# Sentimento: misto, com tendência positiva.
Mesmo prompt, mesmo modelo, três respostas diferentes. E nenhuma delas está exatamente errada. Esse é o ponto que mais incomoda quem vem do mundo determinístico: no probabilístico, "correto" deixa de ser um valor e passa a ser uma faixa.
No Machine Learning clássico, a variação aparece de outro jeito. O modelo é estável para a mesma entrada, mas a decisão final depende de um corte que você escolhe:
probabilidades = modelo.predict_proba(clientes)[:, 1]
LIMIAR = 0.65 # decisão de negócio, não da matemática
for cliente, prob in zip(clientes_ids, probabilidades):
if prob >= LIMIAR:
acionar_retencao(cliente)
Repara que, no fim, a probabilidade sempre vira uma decisão determinística. Alguém escolheu aquele 0.65. Baixar o limiar gera mais falsos positivos (você gasta esforço com quem não ia sair). Subir gera mais falsos negativos (você perde quem ia sair). Não existe valor "certo", existe o equilíbrio que o negócio aceita.
As características principais:
- Saída variável ou incerta — mesma entrada pode gerar respostas diferentes, ou uma resposta com grau de confiança;
- Comportamento aprendido, não programado — a "regra" está nos pesos do modelo, não num arquivo que você consegue ler;
- Lida bem com ambiguidade — linguagem natural, imagens, dados ruidosos, casos que nenhuma regra previu;
- Erra de forma silenciosa — a resposta errada chega com a mesma cara da resposta certa;
- Qualidade medida em taxa — você não diz "funciona", diz "acerta X% num conjunto de casos representativos";
- Custo e latência variáveis — no caso dos LLMs, dependem do tamanho da entrada e da resposta.
As diferenças lado a lado
| Aspecto | Determinística | Probabilística |
|---|---|---|
| Mesma entrada | Sempre a mesma saída | Saída provável, pode variar |
| Onde vive a regra | No código | Nos dados e nos pesos do modelo (e no prompt) |
| Especificação | Formal, sem ambiguidade | Exemplos, linguagem natural, métricas |
| Teste | assert: passou ou falhou |
Avaliação estatística sobre um conjunto de casos |
| Como o erro aparece | Exceção, stack trace | Resposta plausível e errada, sem aviso |
| Depuração | Reproduz, coloca breakpoint, corrige | Analisa padrões de falha, ajusta e mede de novo |
| Correção de bug | Resolve o caso e não quebra os outros (com testes) | Resolver um caso pode piorar outros |
| Dependências | Bibliotecas versionadas e estáveis | Modelo que pode mudar ou ser descontinuado pelo fornecedor |
| Custo por execução | Previsível e baixo | Variável (tokens, contexto, tentativas) |
| Definição de "pronto" | Todos os requisitos atendidos | Taxa de acerto aceitável para o risco do negócio |
Se tivesse que resumir a tabela em uma frase: no determinístico, você controla o comportamento; no probabilístico, você só consegue medir e cercar o comportamento.
Por que o desenvolvimento probabilístico é bem mais complexo
Fazer uma demonstração com um LLM leva uma tarde. Colocar o mesmo recurso em produção, com qualidade que dá para defender numa reunião, leva semanas. Essa distância entre a demo e a produção é onde mora a complexidade. E ela tem causas bem concretas.
O teste deixa de ser um assert e vira uma avaliação
No determinístico, um teste prova que um caso funciona. No probabilístico, um teste isolado não prova quase nada: o modelo pode acertar aquele caso hoje e errar amanhã, ou acertar aquele e errar o vizinho. A unidade de teste deixa de ser o caso e passa a ser o conjunto de casos.
Na prática, isso significa montar e manter um conjunto de avaliação (as famosas evals): dezenas ou centenas de entradas reais com a saída esperada, e uma métrica que diz se a qualidade está aceitável.
casos = carregar_casos("avaliacao_sentimento.jsonl") # texto + resposta esperada
acertos = sum(
1 for caso in casos
if classificar(caso["texto"]) == caso["esperado"]
)
taxa = acertos / len(casos)
# O teste não é "funciona", é "não piorou abaixo do aceitável"
assert taxa >= 0.90, f"Taxa de acerto caiu para {taxa:.0%}"
Parece simples, mas traz três problemas novos. Primeiro, alguém precisa construir esse conjunto, e ele precisa representar o mundo real, não só os casos fáceis. Segundo, quando a resposta é texto livre, comparar com o "esperado" não é trivial; muitas vezes você precisa de critérios, revisão humana ou de outro modelo atuando como avaliador, o que traz sua própria incerteza. Terceiro, rodar a avaliação custa dinheiro e tempo, então ela não roda a cada commit com a mesma leveza de um teste unitário.
O erro não avisa
No código determinístico, o pior bug é o silencioso, aquele que não lança exceção e devolve um valor errado. No probabilístico, todo erro é silencioso por padrão. Um LLM que inventa uma informação (a chamada alucinação) entrega o texto com a mesma gramática, o mesmo tom e a mesma confiança de uma resposta correta. Não tem stack trace. Não tem status 500. Tem um usuário recebendo uma informação falsa bem redigida.
Isso inverte o trabalho de qualidade. Em vez de esperar o sistema reclamar, você precisa construir os mecanismos que detectam o erro: validação de formato, checagem contra fontes, regras de negócio aplicadas depois da resposta, amostragem para revisão humana.
A especificação vira prompt, e prompt é ambíguo
No determinístico, a especificação é o código, e código não tem duplo sentido. Com LLM, boa parte da especificação vai para o prompt, escrita em linguagem natural. E linguagem natural é exatamente o terreno da ambiguidade.
Trocar uma palavra, mudar a ordem de duas instruções ou acrescentar um exemplo pode alterar o comportamento em casos que você nem estava olhando. Corrigir um caso pode quebrar outros três, e você só descobre se tiver o conjunto de avaliação do tópico anterior. Por isso prompt precisa ser tratado como código: versionado, revisado e avaliado antes de ir para produção.
Reproduzir o bug nem sempre é possível
O primeiro passo de qualquer depuração é reproduzir o problema. No probabilístico, isso pode simplesmente não acontecer: o usuário recebeu uma resposta ruim, você manda a mesma pergunta e o modelo responde bem.
Baixar a temperature para zero ajuda a reduzir a variação, mas não deve ser tratado como garantia de saída idêntica. Detalhes de infraestrutura do fornecedor e atualizações do modelo podem mudar o resultado. Sem o registro completo do que aconteceu (prompt montado, contexto, parâmetros, versão do modelo, resposta), o bug vira uma história que ninguém consegue verificar.
Você depende de um componente que não controla
Uma biblioteca determinística na versão 2.3.1 se comporta igual para sempre. Um modelo acessado por API pode ser atualizado, ter o comportamento ajustado ou ser descontinuado pelo fornecedor. O seu código não mudou uma linha e, mesmo assim, o sistema passou a responder diferente.
A defesa é a mesma de sempre, só que mais importante: fixar a versão do modelo quando o fornecedor permite, e rodar o conjunto de avaliação antes de qualquer troca de versão. Trocar de modelo é uma migração, não um ajuste de configuração.
A incerteza se multiplica em cadeia
Esse é o ponto que mais pega quem começa a montar agentes e fluxos com várias etapas. Se cada etapa acerta numa certa taxa, a chance do fluxo inteiro acertar é, numa aproximação simples (considerando as etapas independentes), o produto dessas taxas.
| Etapas no fluxo | Cada etapa acerta 95% | Cada etapa acerta 99% |
|---|---|---|
| 1 | 95,0% | 99,0% |
| 3 | 85,7% | 97,0% |
| 5 | 77,4% | 95,1% |
| 10 | 59,9% | 90,4% |
Os números são ilustrativos, mas a tendência é real: um fluxo de dez etapas "muito boas" pode errar perto de quatro vezes em cada dez execuções. No determinístico, encadear dez funções corretas resulta num fluxo correto. No probabilístico, cada etapa extra cobra pedágio. Isso explica por que vale a pena tirar do modelo tudo que pode ser resolvido com código comum.
A saída precisa ser domada antes de ser usada
Uma função determinística devolve o tipo que você declarou. Um LLM devolve texto. Mesmo pedindo JSON, você pode receber um campo a mais, um campo a menos, um valor fora da lista permitida ou um comentário simpático antes das chaves. Todo ponto onde a resposta do modelo entra no seu sistema precisa de validação, tratamento de falha e um plano B.
A entrada do usuário pode virar instrução
No determinístico, dado é dado e código é código, e boa parte da segurança (como prevenir SQL Injection) consiste em manter os dois separados. Num LLM, instrução e conteúdo chegam pelo mesmo canal: texto. Um documento, um e-mail ou uma mensagem do usuário pode conter frases que o modelo interpreta como ordens. É a prompt injection, e ela não tem uma correção definitiva como a query parametrizada tem para o SQL. A regra prática: nunca dê ao modelo uma permissão que você não daria ao texto mais malicioso que pode chegar até ele.
Custo, latência e observabilidade mudam de natureza
O custo de uma chamada depende de quantos tokens entram e saem. Um contexto que cresce, um usuário que cola um documento enorme ou um fluxo que faz novas tentativas multiplicam o custo sem que o código mude. A latência segue a mesma lógica.
E a observabilidade precisa ir além de "a requisição respondeu 200". Para um sistema probabilístico, o mínimo a registrar é:
- o prompt final montado (não só o template);
- a versão do modelo e os parâmetros usados;
- a resposta bruta e a resposta depois da validação;
- tokens de entrada e saída, tempo de resposta e número de tentativas;
- o caminho que o fluxo seguiu, quando há várias etapas.
Tudo isso com cuidado redobrado com dados pessoais, porque agora o log carrega o conteúdo das conversas.
O mundo muda e o modelo envelhece
No Machine Learning clássico, existe um problema a mais: o modelo aprendeu com dados do passado. Quando o comportamento dos clientes, o mercado ou o perfil dos dados muda (o chamado drift), a qualidade cai devagar, sem nenhum alerta. Uma regra determinística de desconto continua correta até alguém mudá-la. Um modelo preditivo pode ficar errado sozinho.
Existe uma frase atribuída ao estatístico George Box que resume bem essa parte:
Todos os modelos estão errados, mas alguns são úteis.
— George Box, estatístico
O trabalho de quem constrói com lógica probabilística é justamente manter o modelo do lado útil dessa frase, e saber quando ele deixou de estar.
O padrão que funciona: determinístico em volta do probabilístico
Depois de entender de onde vem a complexidade, a conclusão que mais me ajudou é simples: o modelo sugere, o código decide. A parte probabilística fica restrita ao que só ela resolve (interpretar, extrair, classificar, redigir), e tudo em volta continua determinístico: validação de entrada, validação de saída, regras de negócio, permissões e plano B.
from typing import Literal
from pydantic import BaseModel, ValidationError
class Classificacao(BaseModel):
sentimento: Literal["positivo", "neutro", "negativo"]
def classificar(texto: str, tentativas: int = 3) -> str:
# Entrada: regra determinística antes de gastar uma chamada
if not texto.strip():
return "neutro"
for _ in range(tentativas):
bruto = llm.gerar(prompt=montar_prompt(texto), temperature=0)
try:
# Saída: só aceita o que cabe no contrato
return Classificacao.model_validate_json(bruto).sentimento
except ValidationError:
continue
# Plano B determinístico: ninguém recebe lixo
return "revisao_humana"
Repara na estrutura. O modelo aparece numa única linha. O resto do código existe para garantir que, dê certo ou dê errado, o sistema se comporte de forma previsível. Uma resposta fora do contrato nunca chega ao usuário, e o pior caso é conhecido: o item vai para revisão humana.
Algumas regras práticas que saem desse padrão:
- Se dá para escrever a regra, escreva a regra. Não use LLM para validar CPF, calcular data de vencimento ou somar valores;
- Contrato de saída sempre. Schema, tipos e lista de valores permitidos, validados no código;
- Ações com efeito real passam por regra determinística. O modelo pode sugerir um reembolso; quem aprova é a regra de negócio (ou uma pessoa);
- Menos etapas probabilísticas encadeadas. Cada uma que vira código comum melhora a taxa de acerto do fluxo;
- Plano B definido antes de ir para produção. Fila de revisão, resposta padrão ou encaminhamento para atendimento humano.
Quando usar cada uma
Vá de lógica determinística quando:
- a regra pode ser escrita de forma completa;
- o erro tem custo alto ou consequência legal (cálculo financeiro, fiscal, permissões);
- o resultado precisa ser auditável e explicável linha a linha;
- a mesma entrada precisa gerar sempre a mesma saída.
Considere lógica probabilística quando:
- a entrada é ambígua ou não estruturada (texto livre, voz, imagem, documentos);
- o número de variações torna impossível escrever todas as regras;
- um acerto alto, mas não perfeito, já entrega valor ao negócio;
- existe um caminho seguro para os casos em que o modelo errar.
E, na maioria dos sistemas reais, a resposta é as duas: probabilístico no miolo que exige interpretação, determinístico em toda a borda.
Checklist antes de colocar lógica probabilística em produção
- Existe um conjunto de avaliação com casos reais, incluindo os difíceis, e uma taxa mínima de acerto acordada com o negócio?
- A saída do modelo é validada contra um contrato (schema, tipos, valores permitidos) antes de ser usada?
- Está definido o que acontece quando o modelo erra, falha ou demora demais?
- Prompt e versão do modelo estão versionados, e uma troca de qualquer um dos dois exige rodar a avaliação de novo?
- Tudo que pode ser regra virou regra, e o modelo ficou só com o que exige interpretação?
- Ações com efeito real (pagamento, exclusão, envio) passam por uma regra determinística ou por uma pessoa?
- O sistema registra prompt montado, parâmetros, resposta, tokens e latência, respeitando dados pessoais?
- Há monitoramento de qualidade ao longo do tempo, e não só de disponibilidade?
- O custo por requisição tem um teto conhecido?
- O conteúdo que chega ao modelo pode conter instruções maliciosas, e as permissões do modelo levam isso em conta?
Pra fechar
A lógica determinística não ficou velha. Ela continua sendo o chão onde todo o resto pisa. O que mudou é que agora existe uma segunda ferramenta, capaz de resolver problemas que nenhum if resolveria, mas que cobra um preço alto em incerteza.
Quem vem de anos de código determinístico tem uma vantagem enorme nessa transição, desde que não tente tratar o modelo como uma função comum. A habilidade nova não é escrever prompts. É desenhar sistemas que continuam confiáveis mesmo quando uma das peças não é.
