Pular para o conteúdo
João Netto.
PT EN
Artigos

O roteador que dobrava a conta

Toda mensagem do chat pagava duas chamadas ao modelo: uma para classificar, outra para responder. A página já sabia a resposta da primeira.

6 min
  • elixir
  • langchain
  • llm
Ler em English

Selecione um trecho para perguntar à assistente.

O chat deste site tem dois agentes: a assistente, que fala sobre carreira e contato, e o tutor, que explica os artigos e tutoriais publicados. Cada um tem o próprio prompt, as próprias tools e o próprio limite de assunto. Quando a fatura da Anthropic veio maior do que a conta de guardanapo previa, fui auditar a camada de IA — e o problema não estava no tamanho dos prompts. Estava no número de chamadas.

Toda mensagem pagava duas chamadas

Para decidir qual agente respondia, o site usava a RoutingChain do LangChain: uma chamada ao modelo que lê a pergunta, compara com a descrição de cada agente e devolve o nome de uma categoria. Só depois dessa resposta a pergunta seguia para o agente escolhido — a segunda chamada, a que o visitante de fato vê.

O roteador usava o claude-haiku-4-5, o modelo mais barato da Anthropic, e devolvia uma palavra. Parecia inofensivo. Mas o custo de uma chamada não é só a saída: o prompt de roteamento carregava as instruções da chain e a descrição dos dois agentes, algo entre 400 e 700 tokens de entrada em cada mensagem. Na prática, o roteamento respondia por 15% a 30% dos tokens de cada interação — e por metade das requisições.

E havia um custo que não aparece na fatura: as chamadas eram sequenciais. O streaming da resposta só começava depois que a classificação terminava. Cada mensagem carregava um segundo de silêncio comprado.

O detalhe que mais incomodava: o roteador rodava em toda mensagem, inclusive na quinta pergunta de uma conversa que já estava com o tutor desde a primeira. Uma classificação cujo resultado quase nunca muda, cobrada em dobro, mensagem após mensagem.

A página já é um classificador

A informação que o roteador tentava adivinhar já existia de graça: a URL. Quem está lendo um tutorial e abre o chat quase sempre quer perguntar sobre o tutorial; quem está na página sobre quer falar de carreira e contratação. O site inclusive já tinha essa função — ela escolhia o agente inicial do painel:

def agent_for_path(path) when is_binary(path) do
  case Portfolio.Paths.resolve(path) do
    {:ok, _content} -> :tutor
    :error -> :secretary
  end
end

Se o caminho aponta para um conteúdo publicado, o tutor responde; qualquer outra página fica com a assistente. Zero tokens, zero latência. O roteador LLM só existia para o caso raro: a pergunta fora do lugar, como alguém na página sobre perguntando sobre um tutorial.

O caso raro virou uma frase no prompt

Para esse caso, a resposta certa não era uma segunda chamada — era o próprio agente dizer que aquilo não é a área dele e apontar o caminho. Isso coube em uma regra da policy que já entrava no prompt:

9. Não cite títulos nem resuma artigos ou tutoriais do site, e nunca diga que
   um deles existe ou não existe. Essa área não é sua: direcione quem perguntar
   para [/pt/artigos](/pt/artigos) e [/pt/tutoriais](/pt/tutoriais), explicando
   que o assistente daquelas páginas responde sobre o conteúdo publicado.

O tutor tem a regra espelhada, apontando para a página sobre. O painel renderiza Markdown, então o link chega clicável. A pergunta fora do lugar continua custando uma chamada — a mesma que qualquer pergunta custa — e o visitante ganha um redirecionamento honesto em vez de uma segunda cobrança invisível.

Um ajuste acompanhou a mudança: a conversa não pode mais seguir o visitante entre seções. Antes, o id da conversa ficava no navegador e qualquer página retomava o mesmo processo. Agora a retomada confere o dono:

defp resume(socket, page_agent) do
  with true <- connected?(socket),
       id when is_binary(id) <- get_connect_params(socket)["chat_conversation_id"],
       ^page_agent <- AI.conversation_agent(id, page_agent),
       {:ok, turns} <- AI.transcript(id) do
    {id, Enum.map(turns, &entry(&1.role, &1.text))}
  else
    _fresh_start -> {nil, []}
  end
end

Repare no pin ^page_agent: se a conversa guardada pertence ao outro agente, o with cai no else e o visitante começa uma thread nova, com o agente que a página promete. O histórico antigo continua vivo na seção onde nasceu.

O cache que nunca existiu

A auditoria encontrou um segundo problema, mais silencioso. A configuração do modelo pedia prompt caching com TTL de uma hora — a Anthropic guarda o prefixo do prompt e cobra leituras a cerca de um décimo do preço. No papel, o system prompt de cada agente seria escrito uma vez e relido barato o dia inteiro.

Só que cada modelo tem um tamanho mínimo de prefixo cacheável, e no claude-haiku-4-5 esse mínimo é de 4.096 tokens. O maior prompt do site tem cerca de 1.400. Abaixo do mínimo a API não devolve erro: ela simplesmente ignora o pedido, e os campos cache_creation_input_tokens e cache_read_input_tokens ficam em zero. O cache que a configuração prometia nunca escreveu um byte.

A lição tem duas pontas. Primeiro: confie na telemetria, não na configuração — se cache_read_input_tokens é zero em requisições repetidas, o cache não existe, esteja lá o que estiver no código. Segundo: o TTL de uma hora cobra a escrita a duas vezes o preço de entrada, contra 1,25 vez do TTL padrão de cinco minutos. Se um dia um prompt crescer além do mínimo, o padrão mais barato já estará no lugar.

O que isso custa

Roteador LLM Agente da página
Chamadas por mensagem 2 1
Pergunta fora do lugar Resposta certa na hora Redirecionamento com link
Conversa entre seções Segue o visitante Fica na seção de origem
Qualidade do desvio Classificador dedicado Depende do prompt
Código +480 linhas Uma regra na policy

A troca real está na segunda linha: quem pergunta sobre tutorial na página errada agora recebe um link em vez da resposta — um clique a mais. Para um site de portfólio, é um preço baixo por cortar metade das requisições, 15% a 30% dos tokens e um segundo de espera de cada mensagem. A remoção ainda levou embora 480 linhas de código e os testes que as vigiavam.

A consequência prática vale além deste site: antes de pedir a um modelo que decida alguma coisa, olhe o que o contexto já decidiu de graça. A URL, o formulário, a sessão — quase sempre há um classificador gratuito parado do lado da chamada paga.

Escrito por

João Netto

Continue lendo

Todos os artigos