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