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
- 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". - 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.
- 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.