Vicco LabsVicco Labs
Construindo um assistente conversacional em produção · Parte 7
Roteamento não é um único problema

Três routers, três problemas diferentes: DSPy, Semantic Router customizado e Aurélio AI

Antes de construir o customizado avaliei uma biblioteca open-source que quase entrou no projeto. Esse post é o comparativo que eu queria ter lido antes de tomar essas decisões.

1 ABR 2026·10 min de leitura·LLM Routing / Semantic Router / DSPy / Aurélio AI
LLM ROUTING

Nos posts anteriores mostrei como o DSPy substituiu o Agno/LangGraph como roteador cognitivo de um assistente, colocando contrato de output, métrica e processo de melhoria em cima do LLM.

O que não mostrei é que, no mesmo sistema, existe outro roteador com arquitetura completamente diferente. E que, antes de construir o customizado, avaliei uma biblioteca open-source que quase entrou no projeto.

Esse post é o comparativo que eu queria ter lido antes de tomar essas decisões.

O ponto de partida: roteamento não é um único problema

Quando você diz roteador de intenções, pode estar falando de coisas muito diferentes:

Problema A: O usuário digitou "quero cancelar meu pedido". É suporte, devolução, cancelamento pré-envio ou algo fora de escopo? Decisão entre poucos candidatos macro, com thresholds por intent e lógica de ambiguidade.

Problema B: O usuário digitou "tênis masculino Nike até R$ 300 com frete grátis e boa avaliação". É uma busca no catálogo. Mas com qual ferramenta? Com quais parâmetros? Scope + extração de 10+ filtros estruturados de uma frase livre.

Problema C: Você está começando um projeto, precisa rotear entre 8 intents e não quer construir nada do zero. Quer algo funcional em 20 minutos.

Cada um desses problemas pode tem um roteador diferente como resposta.

Roteador 1: Semantic Router customizado por embeddings

O que é

Um roteador que embeda a query do usuário, calcula similaridade coseno contra matrizes de exemplos positivos por intent, penaliza com exemplos negativos, e aplica uma pipeline de decisão com thresholds configuráveis.

Como funciona na prática

O scoring de cada intent:

Isso resolve o Problema A: classificação macro entre intents com semântica clara. Nenhuma chamada ao LLM, com latência de 20-50ms.

O pairwise reranker: onde o conhecimento de domínio entra

Para pares de intents que se confundem com frequência, um reranker heurístico decide com base em padrões de texto:

O pairwise reranker é o conhecimento de domínio codificado como regra. O embedding não distingue "quero ver o status do meu pedido" de "quero ver tênis masculino" - o score coseno dos dois pode ser próximo para busca_catalogo. A regra resolve isso de forma determinística e auditável.

CFG_PARAMS no Redis: hot reload sem deploy

Quando um comportamento de roteamento precisa ser ajustado em produção (um novo termo que começa a confundir intents, um threshold que precisa ser calibrado) você atualiza o CFG_PARAMS no Redis. Sem novo deploy, sem nova compilação.

O trabalho que ninguém documenta: calibração de thresholds

O semantic router customizado tem um problema que só aparece em produção: os thresholds são definidos manualmente, e a decisão de qual valor usar não tem respaldo matemático direto.

Como chegar nos valores certos? Trial and error com um conjunto de queries de teste, observando onde o roteador começa a aceitar queries que não deveria (threshold baixo demais) ou rejeitar queries legítimas caindo no fallback FAQ (threshold alto demais).

Isso é calibração manual. E tem um custo que piora com o tempo.

Quando o encoder muda - seja por novo modelo de embedding, nova versão do Azure OpenAI, migração de provider - a distribuição dos scores muda. Thresholds que funcionavam com text-embedding-3-small não funcionam com text-embedding-3-large. Você precisa recalibrar do zero.

Quando o catálogo de utterances cresce, acrescentando mais exemplos positivos por intent e mais diversidade semântica, a distribuição muda de novo.

O CFG_PARAMS no Redis resolve a operação de ajuste sem deploy. Não resolve o processo de descoberta do valor certo.

Quando usar

  • Classificação macro entre intents bem definidas (5-20 intents)
  • Latência é crítica (menos de 50ms por request)
  • Você precisa de hot reload de configuração sem deploy
  • Domínio tem pares de intents que se confundem de forma conhecida e previsível
  • Você tem exemplos positivos e negativos por intent, mas não tem dataset anotado suficiente para LLM

Roteador 2: DSPy

O que é

Um roteador LLM-first que usa uma Signature declarativa para classificar a intenção e extrair parâmetros estruturados em uma única inferência, compilado com exemplos supervisionados e uma métrica de avaliação assimétrica.

O que o diferencia concretamente

A saída do semantic router customizado é:

A saída do DSPy router é:

Mesma query de entrada, mas com output totalmente diferente. O DSPy não só classifica: ele extrai todos os parâmetros que a tool de busca precisa para executar a query no Redis Stack.

Fazer isso com embedding é inviável. Você não consegue extrair preco_max=300.0 de um vetor coseno.

A métrica assimétrica: onde a prioridade de negócio vira código

O BootstrapFewShot rejeita qualquer demo com scope errado, independente de quantos campos acertaram. Isso é inviável de implementar num semantic router por embeddings, onde o conceito de scope como critério zero-tolerância não existe na similaridade coseno.

Como o DSPy resolve o problema de calibração

No DSPy, o conceito de threshold não existe da mesma forma, porque a decisão não é "se o score está acima de X?". A decisão é "qual é o melhor RouterOutput dado o texto, o histórico e os demos compilados?".

O que funciona como calibração é a própria métrica com o BootstrapFewShot. O optimizer, ao selecionar quais demos incluir no prompt compilado, está implicitamente calibrando o comportamento do router para o domínio, sem que você defina um threshold explicitamente.

Se você adicionar novos exemplos ao dataset e recompilar, o modelo se recalibra com os novos dados. A limitação: recompilar tem custo de LLM. Não é uma operação que você faz com hot reload em produção. É um processo offline, controlado, com validação antes do deploy.

A coerção: proteção contra a imprevisibilidade do LLM

O semantic router nunca precisa disso. Embedding + dot product sempre retorna um float. LLM pode retornar markdown, JSON malformado ou texto livre.

Quando usar

  • O roteamento requer extração de parâmetros estruturados além da classificação de intent
  • Você tem dataset supervisionado (50+ exemplos já ajudam)
  • Precisa de portabilidade entre modelos (trocar GPT-4o por Claude sem reescrever)
  • O output do router precisa de contrato verificável e versionável
  • Latência de 2-3s por chamada é aceitável

Roteador 3: Aurélio AI semantic-router (avaliado, não implementado)

Antes de construir o roteador customizado, avaliei a biblioteca open-source do Aurelio AI, o grupo que inclui parte do time original do semantic-router. A API é limpa e o modelo mental é o mesmo: utterances por route, encoder configurável, similaridade coseno.

A biblioteca funciona. O que me fez construir o customizado foram três gaps específicos para o meu caso de uso:

  • Ausência de pairwise reranker configurável: para pares de intents que se confundem no domínio (busca_catalogo vs detalhe_produto quando o usuário menciona um produto específico), não existe um mecanismo estruturado para injetar a lógica de desempate. Dynamic Routes (com LLM) são a alternativa, mas adicionam latência que eu não queria no caminho crítico.
  • Sem hot reload de configuração: para calibrar thresholds, adicionar FAQ force terms ou ajustar o comportamento de intents ambíguas em produção, eu precisaria de um novo deploy. O CFG_PARAMS no Redis era um requisito do projeto.
  • Visibilidade reduzida da pipeline de decisão: o topk com scores de todos os candidatos, o pairwise reranker com sua regra, o disambiguation_rule no log estruturado... esses itens são importantes para debugar degradação em produção. A biblioteca retorna RouteChoice com similarity_score. Suficiente para muitos casos, mas não para o nível de auditabilidade que eu precisava.

O que a biblioteca resolve melhor: calibração automática de thresholds

O ponto em que o Aurélio AI tem vantagem concreta sobre o customizado é exatamente o problema de calibração discutido acima. A biblioteca tem um método de fitting automático via dataset rotulado:

O fitting usa os próprios scores de similaridade para encontrar o ponto de corte ótimo por intent, sem trial and error manual. Quando o encoder muda, você roda o fitting de novo com o mesmo dataset e os thresholds se recalibram automaticamente.

A limitação: o fitting é tão bom quanto o dataset. Se ele não cobre os casos edge do domínio, os thresholds serão ótimos para o que você testou e silenciosamente ruins para o que não testou.

Para o meu caso, esse ganho não compensava os gaps nos outros três pontos. Para um projeto sem requisito de hot reload e com intents semanticamente bem separadas, pode ser suficiente.

Quando faz sentido

  • Adoção rápida com intents simples e bem separadas semanticamente
  • Volume baixo e poucos pares de intents confusos
  • Calibração automática de thresholds via dataset é prioridade sobre controle da pipeline
  • Time pequeno sem capacidade de manter pipeline de roteamento customizado
  • Dynamic Routes são suficientes para os casos edge

O comparativo de calibração lado a lado

O problema de calibração de thresholds aparece de formas diferentes nos três roteadores. Vale ver lado a lado:

O semantic router customizado coloca o trabalho de calibração no engenheiro. O Aurélio AI o automatiza, mas ainda depende da qualidade do dataset de treino. O DSPy o elimina como problema explícito, mas transforma em outro: manter um dataset supervisionado e um processo de recompilação.

Nenhum dos três é isento de manutenção. A diferença é onde o trabalho fica.

A tabela de comparação

Como os dois coexistem em produção

No assistente, os dois roteadores existem em camadas diferentes:

Camada 1 - Semantic router customizado no supervisor: decide se a mensagem é busca de catálogo, detalhe de produto, suporte, rastreamento de pedido, ou fora de escopo. Classificação macro, sem LLM, latência extremamente baixa.

Camada 2 - DSPy router dentro do subgrafo de busca: quando a Camada 1 roteou para busca_catalogo, o DSPy decide qual das tools chamar e com quais parâmetros. É aqui que "tênis masculino Nike até R$ 300 com frete grátis" vira:

A divisão não é por preferência, é por problema. A Camada 1 precisa de velocidade e hot reload. A Camada 2 precisa de extração estruturada com contrato verificável.

Os trade-offs que a tabela não captura

  • Degradação silenciosa: o semantic router customizado degrada quando os utterances ficam desatualizados em relação ao vocabulário real dos usuários. Você detecta pelo aumento da taxa de fallback. O DSPy degrada quando o modelo do provider é atualizado, você detecta rodando o validation set antes do deploy. O Aurélio AI degrada da mesma forma que o customizado, mas você tem fit() para recalibrar.
  • Custo operacional real: o semantic router customizado cobra embedding por request (barato e previsível). O DSPy cobra embedding + LLM, mas a extração de parâmetros na mesma inferência elimina a chamada subsequente ao LLM para parsear a query, o custo líquido às vezes é comparável. O Aurélio AI tem o mesmo custo do customizado fora dos Dynamic Routes.
  • Transferência de conhecimento: o semantic router customizado tem muitas linhas de pipeline de decisão. Cada linha é uma decisão com um motivo. Quando alguém novo entra no projeto, esse contexto precisa ser transferido explicitamente. O DSPy tem um JSON compilado e um dataset, o "porquê" está nos exemplos e nos pesos. O Aurélio AI tem documentação pública.

Resumindo

  • Semantic router customizado: velocidade, controle total da pipeline e hot reload. O preço é calibração manual de thresholds e custo alto de manutenção.
  • DSPy: quando a decisão de roteamento precisa extrair parâmetros estruturados, com contrato verificável, portabilidade entre modelos e calibração implícita via compilação.
  • Aurélio AI: quando você quer o modelo mental do semantic router sem construir a pipeline do zero, com calibração automática via fit(). Avalie se a ausência de hot reload e pairwise reranker é aceitável para o seu caso de uso antes de escolher.