Blog

Orçamento de latência por etapa: onde cortar quando a resposta demora demais

A reclamação chega sempre no mesmo formato: "o bot está lento". O time olha o painel, vê o percentil noventa e cinco em nove segundos, e começa a otimizar o que for mais fácil de otimizar, que quase nunca é o que está consumindo o tempo. Duas semanas depois a latência caiu de nove para oito segundos e meio, e ninguém sabe explicar por que o esforço rendeu tão pouco. O problema não é falta de otimização, é falta de um orçamento: uma decomposição do tempo total em etapas com um teto declarado para cada uma, medida na mesma linha do tempo, de forma que a pergunta deixe de ser "como deixar tudo mais rápido" e passe a ser "qual etapa estourou o teto dela". Este artigo mostra como montar esse orçamento, por que a soma das etapas quase nunca fecha com o tempo total e o que esse buraco significa, como decidir entre cortar, paralelizar e adiar cada etapa, e por que a resposta certa muitas vezes é mudar a percepção do tempo em vez de reduzir o tempo.

2026-08-13 / IA Aplicada / 15 min

01

Um número agregado não é diagnosticável

A latência de ponta a ponta de uma resposta de agente é uma soma de coisas muito diferentes: espera em fila, resolução de intenção, geração de embedding, busca vetorial, reranking, montagem de prompt, tempo até o primeiro token do modelo, geração completa, validação de saída, execução de ferramentas e envio pelo canal. Cada uma dessas etapas tem uma causa de lentidão distinta, uma técnica de correção distinta e um custo de correção distinto. Ver só a soma é como receber a fatura do cartão sem a lista de compras: você sabe que gastou demais e não sabe onde cortar.

O primeiro passo é declarar um orçamento antes de medir. Isso parece invertido, mas é deliberado: se você mede primeiro e só depois define os tetos, os tetos vão nascer ancorados no que o sistema já faz, e o orçamento vira uma descrição do presente em vez de um contrato. Declarar antes força a conversa certa, que é sobre quanto tempo cada etapa merece dado o valor que ela entrega, não sobre quanto tempo ela leva hoje.

EtapaTeto sugerido (p95)Causa típica de estouroAção quando estoura
Espera em fila50 msConcorrência limitada por conexões ou trabalhadoresAumentar paralelismo ou aplicar prioridade por tipo de tráfego
Classificação de intenção150 msChamada a um segundo LLM para uma decisão bináriaTrocar por classificador leve ou por regra explícita
Embedding da consulta120 msModelo remoto sem pool de conexõesPool com keep-alive e cache de consultas repetidas
Busca vetorial200 mstopK alto demais ou índice frioReduzir topK e aquecer o índice antes de receber tráfego
Reranking300 msReranquear vinte candidatos quando cinco bastamCortar a lista de entrada antes do reranker
Tempo até o primeiro token900 msPrompt gigante sem cache de prefixoEstabilizar o prefixo e ativar cache de prompt
Geração completa2500 msResposta longa demais para o canalLimitar tokens de saída e instruir concisão
Validação e guardrails150 msValidação que chama outro modelo em sérieValidar por esquema e reservar o modelo para o caso duvidoso
Ferramentas externas800 msChamada a ERP ou CRM sem timeout próprioTimeout por ferramenta e resposta parcial quando estoura

Os números da segunda coluna não são universais e não devem ser copiados: eles são um ponto de partida para um agente de atendimento em texto, com resposta esperada em torno de cinco segundos. Um agente de voz precisa de tetos radicalmente menores porque o silêncio é insuportável ao telefone; um agente que gera relatório assíncrono pode ter tetos dez vezes maiores. O que importa é que os tetos existam, sejam escritos em algum lugar versionado e sejam revisados quando o produto muda de expectativa.

02

Medir por etapa na mesma linha do tempo

Medição por etapa costuma nascer errada de duas formas. A primeira é medir cada etapa com um cronômetro independente e somar depois, o que perde tudo que acontece entre as etapas: espera de agendamento, serialização, coleta de lixo, contenção de pool. A segunda é medir só as etapas que alguém lembrou de instrumentar, o que faz o tempo restante virar um resíduo invisível. As duas produzem o mesmo sintoma: a soma das etapas dá seis segundos e o cliente esperou nove.

A correção é tratar o buraco como um dado de primeira classe. Meça o total de ponta a ponta com um único relógio, meça cada etapa dentro desse mesmo intervalo, e reporte explicitamente a diferença como uma etapa chamada "não contabilizado". Quando essa etapa passa de dez por cento do total, ela deixa de ser ruído de medição e vira o maior item do orçamento, o que muda completamente onde o time deve olhar.

// telemetry/latency-budget.js
// Orcamento de latencia por etapa medido em uma unica linha do tempo.
// A diferenca entre o total e a soma das etapas e reportada como
// "unaccounted": e o item que mais revela problema de infraestrutura.

const BUDGET_MS = {
  queue_wait: 50,
  intent: 150,
  embed: 120,
  vector_search: 200,
  rerank: 300,
  llm_first_token: 900,
  llm_generation: 2500,
  validation: 150,
  tools: 800,
};

const TOTAL_BUDGET_MS = Object.values(BUDGET_MS).reduce((a, b) => a + b, 0);

export function createBudgetTracker({ metrics, logger, now = () => Date.now() }) {
  function start({ requestId, channel }) {
    const startedAt = now();
    const stages = [];

    async function stage(name, fn) {
      const from = now();
      try {
        return await fn();
      } finally {
        const durationMs = now() - from;
        stages.push({ name, durationMs });
        // Rotular com o teto permite alertar por etapa, nao pelo agregado:
        // uma etapa em 3x o teto some dentro de um p95 de servico saudavel.
        metrics.histogram('agent.stage.duration_ms', durationMs, {
          stage: name,
          channel,
          over_budget: durationMs > (BUDGET_MS[name] ?? Infinity) ? 'yes' : 'no',
        });
      }
    }

    function finish() {
      const totalMs = now() - startedAt;
      const measuredMs = stages.reduce((acc, s) => acc + s.durationMs, 0);
      // Positivo por construcao: espera de agendamento, serializacao,
      // coleta de lixo e contencao de pool vivem exatamente aqui.
      const unaccountedMs = Math.max(0, totalMs - measuredMs);

      metrics.histogram('agent.total.duration_ms', totalMs, { channel });
      metrics.histogram('agent.stage.duration_ms', unaccountedMs, {
        stage: 'unaccounted',
        channel,
        over_budget: unaccountedMs > totalMs * 0.1 ? 'yes' : 'no',
      });

      // So loga o detalhe quando estourou: log de caso feliz em volume
      // alto custa mais que a etapa que se quer observar.
      if (totalMs > TOTAL_BUDGET_MS) {
        const worst = [...stages].sort((a, b) => {
          const overA = a.durationMs - (BUDGET_MS[a.name] ?? 0);
          const overB = b.durationMs - (BUDGET_MS[b.name] ?? 0);
          return overB - overA;
        })[0];
        logger.warn('agent.budget.exceeded', {
          requestId,
          totalMs,
          budgetMs: TOTAL_BUDGET_MS,
          unaccountedMs,
          worstStage: worst?.name,
          worstOverMs: worst ? worst.durationMs - (BUDGET_MS[worst.name] ?? 0) : 0,
        });
      }

      return { totalMs, measuredMs, unaccountedMs, stages };
    }

    return { stage, finish };
  }

  return { start, BUDGET_MS, TOTAL_BUDGET_MS };
}

O detalhe que faz esse código valer é o rótulo de estouro por etapa. Sem ele, você tem nove histogramas e precisa olhar os nove para saber se algo saiu do lugar. Com ele, um único alerta sobre a proporção de requisições com alguma etapa fora do teto já responde a pergunta operacional, e o painel por etapa serve para o diagnóstico depois que o alerta disparou. É a diferença entre um painel que você consulta quando lembra e um que te chama.

Uma armadilha frequente aqui é medir o tempo da etapa incluindo o tempo em que ela esteve esperando um recurso compartilhado. Se a busca vetorial demora quatrocentos milissegundos, sendo trezentos esperando uma conexão livre no pool, atribuir isso à busca leva o time a otimizar o índice quando o problema é dimensionamento do pool. Vale separar o tempo de aquisição do recurso do tempo de trabalho efetivo sempre que a etapa depende de um pool, e essa separação costuma explicar sozinha metade dos estouros.

03

Cortar, paralelizar ou adiar: três respostas diferentes

Com o orçamento medido, cada etapa fora do teto admite três tratamentos, e escolher o errado é como o time desperdiça semanas. Cortar significa fazer menos trabalho: reduzir topK, encurtar a saída, remover uma chamada. Paralelizar significa fazer o mesmo trabalho ao mesmo tempo que outro. Adiar significa tirar a etapa do caminho crítico e executá-la depois que a resposta já saiu. Cortar reduz custo, paralelizar aumenta pressão sobre recursos e adiar muda a semântica do sistema.

A regra prática é procurar oportunidade de paralelismo antes de cortar qualidade, porque cortar sempre tem preço de resposta pior e paralelizar muitas vezes é de graça. Num agente típico há dois paralelismos evidentes que quase ninguém explora: gerar o embedding da consulta ao mesmo tempo que se busca o histórico da conversa, e disparar as ferramentas independentes juntas em vez de em cadeia. Nenhum dos dois muda uma vírgula da resposta.

Caminho critico de uma resposta de agente

  SEQUENCIAL (soma: 4.62 s)
  |-- fila 0.05
      |-- intencao 0.15
          |-- historico 0.18
              |-- embedding 0.12
                  |-- busca 0.20
                      |-- rerank 0.30
                          |-- ferramenta A 0.80
                              |-- ferramenta B 0.62
                                  |-- primeiro token 0.90
                                      |-- geracao 1.30

  PARALELIZADO (caminho critico: 3.20 s)
  |-- fila 0.05
      |-- intencao 0.15
          |-- [ historico 0.18 | embedding 0.12 ]      -> 0.18
              |-- busca 0.20
                  |-- rerank 0.30
                      |-- [ ferramenta A 0.80 | ferramenta B 0.62 ] -> 0.80
                          |-- primeiro token 0.90
                              |-- geracao 1.30

  Ganho: 1.42 s sem cortar nenhuma etapa e sem piorar a resposta.
  O que sobrou no caminho critico: geracao (1.30) e primeiro token (0.90),
  que juntos sao 69% do total. E ali que mora a proxima decisao dificil.

O diagrama mostra algo que se repete em quase todo sistema: depois de paralelizar o óbvio, o caminho crítico fica dominado pelo modelo. Isso é importante porque muda a natureza do trabalho restante. As etapas de recuperação respondem a engenharia clássica, com cache, índice e pool. O tempo do modelo responde a três coisas apenas: tamanho do prompt, tamanho da saída e escolha do modelo. Se o time continua otimizando busca vetorial depois de chegar nesse ponto, está otimizando doze por cento do problema.

TratamentoO que mudaGanho típicoO que você paga
Paralelizar etapas independentesOrdem de execução, não o trabalho feito20% a 35% do totalMais conexões simultâneas e código mais difícil de depurar
Reduzir topK e candidatos do rerankerVolume de dados processado150 a 400 msRisco de perder o documento certo em consultas raras
Cache de prefixo do promptQuanto o modelo reprocessa a cada chamada30% a 60% do tempo até o primeiro tokenDisciplina de ordenação do prompt para sempre
Limitar tokens de saídaTamanho da resposta geradaProporcional e imediatoRespostas truncadas se o limite for mal calibrado
Modelo menor para tarefas simplesQual modelo atende cada rota40% a 70% nas rotas roteadasUm classificador a mais para manter e avaliar
Adiar etapa para depois da respostaSemântica: deixa de ser síncronaRemove a etapa inteira do caminhoPrecisa de compensação quando a etapa adiada falha
Streaming da respostaPercepção, não o tempo totalTempo percebido cai para o primeiro tokenNão pode streamar o que ainda vai ser validado

A última linha merece atenção porque é a que mais confunde. Streaming não corta um milissegundo do tempo total: a resposta completa chega no mesmo instante que chegaria antes. O que muda é que o usuário para de olhar para um indicador de digitação em novecentos milissegundos em vez de em quatro segundos, e a percepção de lentidão desaparece mesmo com o número no painel intacto. Para um canal de texto isso frequentemente resolve o problema real, que era a reclamação, não o número.

04

O que adiar e como não quebrar nada ao adiar

Adiar é o tratamento mais poderoso e o mais perigoso, porque muda o contrato do sistema em vez de otimizá-lo. Uma etapa adiada é uma etapa que pode falhar depois que o cliente já recebeu a resposta, e o sistema precisa saber o que fazer nesse caso. A pergunta que separa o adiável do não adiável é direta: se essa etapa falhar depois da resposta já entregue, dá para compensar sem contradizer o que foi dito ao cliente?

  1. Registro em trilha de auditoria: adiável sem ressalva, porque nada na resposta depende de o registro já existir.
  2. Atualização de memória de longo prazo do cliente: adiável, já que ela só afeta a conversa seguinte e não a atual.
  3. Sincronização com o CRM: adiável com fila durável, porque o dado precisa chegar mas não precisa chegar antes da resposta.
  4. Cálculo de custo e métricas de qualidade: adiável e frequentemente melhor adiado, dado que roda em lote com mais contexto.
  5. Validação de política de conteúdo: não adiável, pois adiar significa entregar primeiro e descobrir o problema depois.
  6. Consulta de saldo ou estoque citada na resposta: não adiável, uma vez que a resposta afirma um fato que precisa ser verdadeiro no momento em que é dito.
  7. Confirmação de ação com efeito externo: não adiável, porque dizer que foi feito antes de fazer é a única falha que o cliente nunca perdoa.

Os três últimos itens definem a fronteira. Tudo que a resposta afirma como fato precisa ter sido verificado antes de a resposta sair. Tudo que é consequência da resposta pode acontecer depois, desde que aconteça de verdade, o que na prática significa fila durável com repetição e alerta quando a fila cresce. Adiar para uma chamada disparada e esquecida no mesmo processo não é adiar, é perder silenciosamente sob carga, e o momento em que isso acontece é justamente o pico em que a perda dói mais.

// pipeline/deferred.js
// Adia o que nao pertence ao caminho critico, com fila duravel.
// Regra: se a resposta afirma o fato, ele e verificado antes;
// se o fato e consequencia da resposta, ele pode ser adiado.

const CRITICAL = new Set(['policy_check', 'balance_lookup', 'action_confirm']);

export function createPipeline({ queue, metrics, logger }) {
  async function respond({ requestId, steps, deferred }) {
    // Etapas criticas: erro aqui interrompe e vira resposta de falha,
    // porque entregar uma afirmacao nao verificada e pior que nao responder.
    for (const step of steps) {
      if (!CRITICAL.has(step.name)) continue;
      await step.run();
    }

    const answer = await steps.find((s) => s.name === 'generate').run();

    // Etapas adiadas nunca usam disparo e esquecimento: vao para fila
    // duravel, porque perder isso sob carga e um bug silencioso.
    for (const task of deferred) {
      await queue.enqueue(task.name, {
        requestId,
        payload: task.payload,
        // Compensacao declarada junto com a tarefa: quem adia assume
        // a responsabilidade de dizer o que fazer quando ela falhar.
        onExhausted: task.onExhausted ?? 'alert_only',
        maxAttempts: task.maxAttempts ?? 5,
      });
      metrics.counter('agent.deferred.enqueued_total', 1, { task: task.name });
    }

    return answer;
  }

  // A fila adiada precisa de um observador proprio: crescimento sustentado
  // significa que o trabalho foi tirado do caminho critico e nunca feito.
  function watchBacklog({ thresholdAge, intervalMs = 60000 }) {
    const timer = setInterval(async () => {
      const oldestAgeMs = await queue.oldestPendingAgeMs();
      metrics.gauge('agent.deferred.oldest_age_ms', oldestAgeMs);
      if (oldestAgeMs > thresholdAge) {
        logger.error('agent.deferred.stale', { oldestAgeMs, thresholdAge });
      }
    }, intervalMs);
    timer.unref?.();
    return () => clearInterval(timer);
  }

  return { respond, watchBacklog };
}

A função de observação da fila é a parte que costuma ficar de fora e é a que dá sentido à decisão de adiar. Quando você tira uma etapa do caminho crítico, ela some do painel de latência e passa a viver num lugar onde ninguém olha. A métrica de idade do item pendente mais antigo é o que impede que "adiado" vire "nunca feito", e ela precisa de alerta próprio, porque nenhum painel de latência vai avisar que a sincronização com o CRM está trinta e seis horas atrasada.

05

Orçamento por rota, não um número para o sistema inteiro

Um orçamento único para todo o sistema falha pelo mesmo motivo que um percentil único falha: ele mistura populações com expectativas diferentes. "Qual o horário de funcionamento" e "quanto eu devo, com detalhamento por nota fiscal" não deveriam ter o mesmo teto, e forçar o mesmo teto leva a duas decisões ruins ao mesmo tempo: a rota simples fica mais lenta que precisaria porque o teto é generoso, e a rota complexa vive estourando um teto que nunca foi realista para ela.

Classe de rotaTeto total (p95)Etapas no caminhoEstratégia dominante
Resposta de catálogo fixo600 msFila, intenção, templateCache de resposta com chave por intenção
Pergunta respondida por RAG3500 msRecuperação completa e geraçãoStreaming e cache de prefixo
Consulta a sistema externo4500 msRAG mais uma ou duas ferramentasParalelizar ferramentas e definir timeout por ferramenta
Ação transacional6000 msTudo acima mais confirmação e escritaConfirmação parcial em duas mensagens
Escalada para humano900 msIntenção e roteamentoNunca passar pelo modelo de geração

A última linha é a mais valiosa e a mais esquecida. Quando o cliente escreve "quero falar com um atendente", a única coisa certa a fazer é rotear, e o pior resultado possível é gastar três segundos gerando um texto elaborado explicando que vai transferir. Um caminho curto para essa intenção, sem passar pelo modelo de geração, transforma o pior momento da conversa em uma resposta quase instantânea, e é um dos poucos ajustes que melhoram latência e satisfação ao mesmo tempo sem nenhum contrapeso.

Com orçamento por rota, o alerta operacional muda de forma. Em vez de "o p95 do serviço passou de cinco segundos", ele vira "a rota de consulta externa está com trinta por cento das requisições fora do teto dela", o que já vem com o diagnóstico embutido e aponta o time direto para as duas ou três etapas daquela rota específica. É a diferença entre um alerta que inicia uma investigação e um alerta que inicia uma correção.

06

A sequência que costuma render mais

Nem toda técnica acima vale o mesmo esforço no mesmo momento. A ordem abaixo é a que costuma dar mais resultado por semana de trabalho em agentes de atendimento de porte médio, e cada passo depende do anterior porque sem medição por etapa você não sabe se o passo seguinte melhorou alguma coisa ou se você trocou um gargalo por outro.

  1. Declare os tetos por etapa e por classe de rota antes de medir, para que o orçamento seja um contrato e não uma descrição do presente.
  2. Instrumente na mesma linha do tempo, com a etapa "não contabilizado" explícita, e trate um resíduo acima de dez por cento como o item mais urgente do orçamento.
  3. Paralelize o que é independente antes de cortar qualquer coisa, já que esse é o único ganho que não tem contrapartida na resposta.
  4. Ligue streaming nos canais que suportam, porque ele resolve a reclamação mesmo sem mexer no número total.
  5. Estabilize o prefixo do prompt e ative cache, que é o maior ganho isolado no tempo até o primeiro token.
  6. Adie o que é consequência da resposta, sempre com fila durável e alerta sobre a idade do item pendente mais antigo.
  7. Só então corte qualidade, com topK menor e saída mais curta, medindo o efeito no eval antes de considerar o corte concluído.
  8. Crie um caminho curto para escalada humana que nunca toque o modelo de geração.

O item que mais gera resistência é o sétimo, e com razão. Cortar topK ou limitar a saída é a otimização mais fácil de implementar e a única da lista que pode piorar a resposta sem que ninguém perceba na semana seguinte. Por isso ela vem por último e por isso precisa passar pelo conjunto de avaliação antes de ser considerada pronta: um ganho de duzentos milissegundos que custa três pontos de acerto não é uma otimização, é uma troca que alguém precisa aprovar conscientemente.

Vale terminar com o desconforto que esse trabalho quase sempre revela. Depois de paralelizar, ligar streaming e cachear o prefixo, o orçamento fica dominado pela geração do modelo, e ali as opções são poucas e todas envolvem escolha de produto: resposta mais curta, modelo menor ou aceitar o tempo. Chegar nesse ponto não é fracasso do trabalho de latência, é o resultado dele. O orçamento serviu justamente para transformar uma reclamação vaga sobre lentidão numa decisão específica sobre quanto texto o cliente precisa receber e com qual modelo, que é uma decisão que o time consegue tomar.

FAQ

Perguntas frequentes

Por que a soma das minhas etapas não fecha com o tempo total medido?

Porque o tempo que não está em nenhuma etapa é real e normalmente é o mais informativo. Entre o fim de uma etapa e o começo da próxima cabem espera de agendamento do laço de eventos, serialização e desserialização de payload, pausas de coleta de lixo, contenção de pool de conexões e o tempo de rede que fica fora dos seus cronômetros. Se você mede cada etapa com um cronômetro separado e soma no fim, todo esse tempo desaparece e o painel mostra seis segundos enquanto o cliente esperou nove. A correção é medir o total de ponta a ponta com um único relógio, medir as etapas dentro desse mesmo intervalo e reportar a diferença como uma etapa explícita chamada não contabilizado. Quando esse resíduo passa de dez por cento do total, ele deixa de ser imprecisão de medição e vira o maior item do orçamento, apontando para dimensionamento de pool, saturação de CPU ou pressão de memória em vez de para qualquer componente de IA.

Streaming resolve latência ou é só maquiagem?

Ele não muda o tempo total em nada e resolve o problema real na maioria dos canais de texto, e as duas coisas são verdadeiras ao mesmo tempo. A resposta completa chega no mesmo instante em que chegaria sem streaming, então nenhum número do painel melhora. O que muda é que o cliente para de encarar um indicador de digitação em novecentos milissegundos em vez de em quatro segundos, e a reclamação que originou o trabalho era sobre isso, não sobre o número. Vale ser honesto sobre os limites: você não pode streamar conteúdo que ainda vai passar por validação de política, porque isso significaria mostrar e depois retirar, e em canais que não suportam entrega incremental o ganho é zero. Ainda assim, quando o canal permite, é a intervenção com melhor relação entre esforço e percepção de qualidade da lista inteira, e faz sentido tentá-la antes de cortar qualquer coisa que afete a resposta.

Quais etapas posso tirar do caminho crítico sem risco?

O critério é se a resposta afirma o fato ou se o fato é consequência da resposta. Tudo que a resposta afirma como verdadeiro precisa estar verificado antes de sair: saldo citado, estoque prometido, política aplicada, ação confirmada. Tudo que decorre da resposta pode acontecer depois: registro em trilha de auditoria, atualização da memória de longo prazo, sincronização com o CRM, cálculo de custo e métricas de qualidade. A ressalva importante é que adiar significa fila durável com repetição e compensação declarada, não uma promessa disparada e esquecida no mesmo processo, que sob carga vira perda silenciosa exatamente no pico em que ela mais dói. E como etapa adiada some do painel de latência, ela precisa de um observador próprio: a métrica de idade do item pendente mais antigo é o que impede que adiado vire nunca feito.

Latência se resolve por orçamento, não por esforço difuso

Um número agregado de latência não é diagnosticável: ele mistura nove etapas com causas, correções e custos diferentes, e leva o time a otimizar o que é fácil em vez do que pesa. Declarar tetos por etapa e por classe de rota antes de medir, instrumentar tudo na mesma linha do tempo com o resíduo não contabilizado explícito, paralelizar o que é independente antes de cortar qualquer coisa, ligar streaming onde o canal permite e adiar apenas o que é consequência da resposta, sempre com fila durável, é a sequência que transforma uma reclamação vaga em uma lista curta de decisões concretas. Ao final desse caminho o orçamento fica dominado pela geração do modelo, e é exatamente aí que a conversa deixa de ser técnica e vira produto: quanto texto o cliente precisa receber e com qual modelo. Posso instrumentar o orçamento de latência do seu agente, separar caminho crítico de trabalho adiável e apontar onde o corte compensa com dado medido em vez de palpite.