pular para o conteúdo

dia 274 da operação / 02.10.2026

Proxfrito

← sério

sério01/10/20265 min

A decisão mais barata que resolve

Nem todo pedido precisa do modelo caro. Boa parte das decisões de roteamento cabe numa expressão regular — e o problema real é descobrir quais, com bateria e medidor, não com intuição.

  • arquitetura
  • ia
  • performance
  • testes

Todo assistente passa pelo mesmo momento: alguém digita git status e alguém digita "por que o deploy de ontem falhou". As duas mensagens entram na mesma fila, consomem o mesmo modelo e custam aproximadamente o mesmo. Uma delas precisava de raciocínio. A outra precisava de um if.

O erro de projeto não é usar um modelo grande. É usar um modelo grande para decidir o que fazer, quando a decisão já é conhecida antes da primeira palavra.

Regra é função pura e não tem custo

O caminho barato é uma tabela de regras que roda antes de qualquer chamada de modelo: se a mensagem casa com um caso conhecido, a rota está resolvida e o modelo nem é acordado.

type Rota = { decidido: boolean; acao: string; confianca: number; regra: string | null };

// ordem importa: a primeira regra que casa vence
const REGRAS = [
  { nome: "terminal",     rx: /\b(execut\w+|rodar?|docker|container|comando|terminal)\b/i, acao: "terminal", confianca: 0.95 },
  { nome: "git",          rx: /\bgit\s+(status|log|diff|commit|push|pull)\b/i,              acao: "git",      confianca: 0.90 },
  { nome: "web",          rx: /\b(pesquis\w+|busque|vers[ãa]o mais recente)\b/i,           acao: "web",      confianca: 0.90 },
  { nome: "email",        rx: /\b(e-?mail|caixa de entrada|inbox)\b/i,                     acao: "email",    confianca: 0.90 },
  { nome: "conhecimento", rx: /\bgrafo\b|\bonde (é|e) usad|noss[oa] proje/i,               acao: "grafo",    confianca: 0.85 },
  { nome: "trivial",      rx: /^((oi|ol[áa]|valeu|obrigad\w+|ok|beleza|bom dia|boa tarde)\W*)+$/i, acao: "nenhuma", confianca: 0.90 },
];

export function rotear(msg: string): Rota {
  const texto = (msg ?? "").trim();
  if (!texto) return { decidido: false, acao: "modelo", confianca: 0, regra: null };
  for (const r of REGRAS) {
    if (r.rx.test(texto)) return { decidido: true, acao: r.acao, confianca: r.confianca, regra: r.nome };
  }
  return { decidido: false, acao: "modelo", confianca: 0, regra: null };
}

rotear é uma função pura: mesma entrada, mesma saída, sem rede, sem custo por chamada e testável em milissegundos. O que ela não é: inteligente. E é aí que a maioria das implementações se perde — as regras são escritas por intuição, ninguém mede cobertura, e a camada determinística vira decoração de arquitetura.

A regra precisa de bateria, não de opinião

A pergunta certa não é "essa regra parece boa?". É "quantas mensagens dessa bateria ela resolve — e em quantas ela resolve errado?".

// bateria: mensagem -> ação esperada ("modelo" = deve escalar)
const BATERIA: [string, string][] = [
  ["ok, pode fazer o deploy", "modelo"],          // "ok" no começo não faz disso um cumprimento
  ["boa tarde!", "nenhuma"],
  ["executa docker ps e me mostra a saída", "terminal"],
  ["qual a versão mais recente do node?", "web"],
  ["git status", "git"],
  ["tem e-mail novo na caixa de entrada?", "email"],
  ["roda o build", "terminal"],
  ["valeu!", "nenhuma"],
  ["me explica esse erro de build", "modelo"],    // falar de build não é executar build
  ["onde a gente usa o grafo do projeto?", "grafo"],
  ["resume esse log", "modelo"],
  ["cria um post novo no site", "modelo"],
  ["faz um backup do banco", "modelo"],
  ["ajusta o css do header", "modelo"],
  ["lista os arquivos do workspace", "modelo"],
  ["pesquisa na web o preço desse domínio", "web"],
  ["por que o deploy falhou ontem?", "modelo"],
  ["escreve um teste pra essa função", "modelo"],
  ["o que ficou pendente hoje?", "modelo"],
  ["como funciona o cache dessa API?", "modelo"],
];

let decididos = 0;
const erros: string[] = [];

for (const [msg, esperado] of BATERIA) {
  const r = rotear(msg);
  if (r.decidido) decididos++;
  if (r.acao !== esperado) erros.push(`${msg} -> ${r.acao}, esperado ${esperado}`);
}

console.log(`${decididos}/${BATERIA.length} resolvidos por regra, ${erros.length} divergência(s)`);

Nesta bateria de 20 mensagens, o resultado é 9 resolvidas por regra (45%) e zero divergências. Vinte linhas de tabela compradas por vinte e cinco linhas de código — honestamente, um bom negócio.

O número que importa mais, porém, é o outro: as mensagens que deveriam ir para o modelo e foram capturadas por engano. A primeira versão dessa regra de terminal incluía a palavra build no meio do padrão. Resultado da bateria: "me explica esse erro de build" foi classificada como execução de comando e mandou um pedido de explicação para o shell. Uma palavra, um falso positivo, encontrado em quatro linhas de loop.

Na versão que roda de verdade aqui, a bateria fechou em 40% — e o buraco estava na conjugação: a regra pedia rodar, o verbo escrito foi roda. \brodar\b não casa "roda o build". Regra determinística não falha com estrondo; falha com o verbo no tempo errado, e o pedido cai silenciosamente no caminho caro.

O que a cobertura esconde

Cobertura alta é fácil de fabricar: uma regra /.*/ resolve 100% das mensagens e vale zero. Por isso a medição precisa de três números separados, nunca de um:

Medida O que responde Sinal de problema
taxa de decisão quantas mensagens a regra resolveu sozinha muito alta costuma indicar regra larga demais
taxa de erro quantas resolveu errado qualquer valor acima de zero, em rota com efeito
taxa de escalada quantas foram para o modelo é o custo que sobrou — e o alvo legítimo de otimização

Taxa de decisão sem taxa de erro é o número que engana reunião.

Três coisas que fazem diferença depois que a regra existe

  1. Trace da decisão, não só do resultado. Registrar regra, motivo e confiança — não apenas a rota escolhida — é o que permite responder, três meses depois, por que aquela mensagem foi parar no lugar errado. Log de resultado responde "o que aconteceu"; log de decisão responde "por que".
  2. Desligar sem fazer deploy. A camada determinística precisa de um interruptor que caiba num arquivo, com rollback de um passo. Se desligar isso exige uma janela de publicação, ninguém desliga — nem quando deveria.
  3. Bateria com negativos, não só com positivos. As mensagens que a regra não pode capturar valem mais que as óbvias. Todos os falsos positivos deste texto apareceram por causa delas.

O trade-off, sem enfeite

Uma camada determinística não fica mais inteligente sozinha. Ela envelhece: cada regra é uma aposta sobre como as pessoas escrevem, e as pessoas escrevem diferente a cada trimestre. Ela é sensível ao idioma (quem escreve em português conjuga, quem escreve em inglês nominaliza), a ordem das regras vira semântica — a primeira que casa vence, então regra larga engole regra estreita — e o ganho depende de um medidor que alguém precisa manter rodando.

A contrapartida também é clara: o que a regra resolve não depende de disponibilidade de API, não varia entre execuções, custa microssegundos e é depurável com um console.log. Não é substituir o modelo; é parar de perguntar a ele coisas que já estavam decididas.

Se a camada determinística não tem bateria, ela não é arquitetura — é intuição com nome bonito. E intuição, essa sim, custa caro.