1

AI-native Architect: custos e capacidade — routing, cache e unit economics sob orçamento

AI-native Architect: custos e capacidade — routing, cache e unit economics sob orçamento

O time do RelayOps troca o modelo de extração por um mais barato por token. A fatura mensal da API cai — o dashboard de custo por chamada mostra a economia no mesmo dia. Duas semanas depois, alguém finalmente calcula o número que devia ter olhado desde o início: custo por documento efetivamente aceito. Ele subiu. O modelo mais barato erra o schema com mais frequência, cada erro vira uma nova chamada (que também é paga), e o documento — de um tenant crítico — ainda precisa de revisão humana antes de ser aceito. Este incidente é sintético; não descreve cliente, fatura real ou carga de produção, mas a estrutura do erro é comum o bastante para abrir o capítulo com ela: uma métrica caiu, mas não era a métrica que decidia se a troca valeu a pena.

A causa não é um erro de aritmética — é medir a coisa errada. Preço por token responde "quanto custa uma chamada". Custo por documento aceito responde "quanto custou o resultado que alguém realmente pôde usar", e essa segunda pergunta soma retries, revisão humana, armazenamento, egress e overhead de worker na mesma conta, sem contar nada duas vezes. As duas coisas variam de forma independente: um modelo pode ser mais barato por token e ainda assim mais caro por documento aceito, se ele erra schema com frequência suficiente para que os retries apaguem a economia — e, quando a revisão humana entra na conta, ela pode dominar tanto o total que a escolha de modelo vira um detalhe de segunda ordem perto da decisão de rotear ou não para humano. Este capítulo constrói a calculadora, a política de cache, a política de routing e o orçamento por tenant que tornam essa distinção verificável em vez de intuída.

Estado de entrada

O capítulo 5 fechou a camada de evals e observabilidade do RelayOps: dataset dev/holdout sem sobreposição de ID, graders determinísticos (schema, campo, política, sucesso da tarefa), um gate de release que reprova regressão em estrato crítico mesmo com média geral maior, e um relatório por estrato no formato { overall, n } — taxa de sucesso medida sobre uma amostra com tamanho conhecido. Este capítulo não reabre nem reimplementa nenhum arquivo daquela camada; ele assume que RelayOps já sabe medir se um resultado está certo, e faz uma pergunta diferente: dado que se sabe medir qualidade, qual routing, qual cache e qual orçamento mantêm essa qualidade dentro de um limite de gasto previsível por tenant — inclusive quando o tráfego não é a média estável de uma planilha? A única coisa que este capítulo reusa do capítulo 5 é a forma da evidência de estrato ({ overall, n }), como contrato de entrada da política de routing — nenhum número do capítulo 5 é copiado ou citado como se fosse deste capítulo.

O menor exemplo que separa preço de custo

Antes de qualquer calculadora, a distinção cabe em uma função:

export function tokenCallCost({ model, inputTokens, outputTokens, cacheReadTokens = 0 }, prices) {
  const rate = prices.models[model];
  const perMTok = (tokens, r) => (tokens / 1_000_000) * r;
  return perMTok(inputTokens, rate.input)
    + perMTok(outputTokens, rate.output)
    + perMTok(cacheReadTokens, rate.cacheRead);
}

Essa função responde "quanto custou esta chamada" — nada mais. Ela não sabe se a chamada precisou de um retry, não sabe se o documento foi para revisão humana, não sabe se o tenant tem orçamento para outra chamada. Cada uma dessas perguntas é uma camada separada: documentMarginalCost soma todas as chamadas de um documento (incluindo retries) mais overhead de tool/worker/storage/egress; documentAllocatedCost adiciona revisão humana só quando o routing decidiu por ela; e costPerAcceptedDocument divide o gasto total de um lote pela contagem de documentos que realmente chegaram a um estado aceito — nunca pela contagem de chamadas. Um documento rejeitado continua custando dinheiro e nunca é contado como gratuito; isso é um teste automatizado, não uma frase (a rejected document still costs money and is excluded from acceptedCount, never counted as free).

Intuição: o trade-off já tem nome

O próprio guia de engenharia da Anthropic sobre agentes afirma que "sistemas agênticos frequentemente trocam latência e custo por melhor desempenho da tarefa", e recomenda explicitamente rotear "perguntas fáceis/comuns para modelos menores e mais baratos... e perguntas difíceis/incomuns para modelos mais capazes" como o caso de uso central de um workflow de routing. O erro do time do RelayOps na abertura deste capítulo não foi usar um modelo mais barato — foi usar um só modelo mais barato para todo tipo de documento, sem medir se aquele tipo específico tolera o modelo mais barato. A intuição de custo/capacidade de sistemas convencionais também já resolveu um problema parecido: o livro de SRE do Google descreve proteção contra sobrecarga baseada em um sinal de utilização local, e um mecanismo de throttling adaptativo no qual cada cliente rastreia sua própria taxa de aceitação recente e se auto-regula antes que a fila compartilhada seja afetada — a mesma forma que este capítulo usa para orçamento por tenant: um sinal local de "quanto de orçamento resta" que desacelera o próprio tenant antes que ele consuma a capacidade de todos os outros.

Evolução RelayOps: da chamada ao orçamento por tenant

RelayOps ganha, neste capítulo, quatro peças novas, independentes da camada de evals e da camada de durabilidade dos capítulos anteriores:

  • Calculadora de custo (examples/costs/calculate.mjs) sobre um price fixture real e datado — preços de lista da API da Anthropic, capturados em 2026-09-28 — mais premissas operacionais explicitamente sintéticas (armazenamento, egress, worker, revisão humana), nunca misturadas na mesma tabela sem rótulo.
  • Política de cache (examples/src/cache-policy.mjs) com chave determinística por tenant + checksum do documento + versão de schema/prompt/modelo/policy, em dois namespaces que nunca colidem: exact (mesma resposta para o mesmo documento) e semantic (mesmo prompt/schema/policy compilado, reusado entre documentos diferentes do mesmo tenant e tipo).
  • Orçamento por tenant (examples/src/budget-policy.mjs) com reserva atômica na fronteira de execução, reconciliação pelo custo real após a chamada, e um sinal de backpressure que cresce conforme o orçamento de um tenant se esgota — para que um tenant barulhento nunca sufoque a fila compartilhada de outro.
  • Routing por evidência (examples/src/routing-policy.mjs) que decide entre nível barato, nível padrão e fallback humano usando três entradas determinísticas: risco do tipo de documento, orçamento disponível, e uma taxa de sucesso medida (nunca uma confiança relatada pelo próprio modelo).

Nenhuma dessas quatro peças chama um modelo real, um banco de dados ou a rede. Todas rodam offline, em Node puro, e os 24 testes que as cobrem rodam em menos de cem milissegundos.

O price fixture que alimenta a calculadora separa, de propósito, duas categorias de número que não podem viver na mesma tabela sem rótulo: os preços de Claude Opus 5.5, Claude Sonnet 5 e Claude Haiku 4.5 — reais, datados, copiados da documentação de preços da Anthropic em 2026-09-28 — e as premissas operacionais (armazenamento, egress, computação de worker, custo por minuto de revisão humana), que são inteiramente sintéticas, escolhidas para deixar a aritmética auditável à mão, não para modelar uma fatura de nuvem real. Misturar as duas categorias numa tabela só de números — sem dizer qual linha é fonte primária e qual é premissa de tutorial — é exatamente o tipo de erro que faz um leitor tratar um exemplo didático como um benchmark. prices-fixture.json marca cada bloco com um campo _type e um aviso explícito por esse motivo, e sources.md registra a URL e a data de acesso ao lado de cada preço real.

Por que reservar antes de gastar, não gastar e conferir depois

A ordem importa mais do que parece. Um sistema que primeiro faz a chamada e só depois verifica se havia orçamento sempre vai, eventualmente, gastar além do limite — a checagem chega tarde demais para impedir o próprio gasto que está checando. BudgetLedger.reserve() inverte essa ordem: o orçamento é retirado do saldo disponível do tenant antes de qualquer chamada acontecer, e só é convertido em gasto real (commit) ou devolvido (release) depois que o resultado da chamada é conhecido. Isso transforma "o orçamento estourou" de um fato que se descobre depois do gasto em uma condição que impede o gasto de acontecer — a diferença entre um alarme e um freio.

Contratos e trade-offs

O contrato mais importante deste capítulo é a diferença entre três números que soam parecidos e não são intercambiáveis:

  • Custo marginal: soma de tudo que foi efetivamente gasto processando um documento — cada chamada de LLM (incluindo retries), overhead de tool/worker, e a fração de armazenamento/egress atribuível àquele documento. Não inclui revisão humana.
  • Custo alocado: custo marginal mais revisão humana, quando o routing decidiu por ela. É "alocado" porque a mesma hora de revisor é, na realidade, compartilhada entre muitos documentos; este capítulo aloca 1:1 para manter a aritmética auditável, e diz isso explicitamente no código e na documentação — não finge que é uma alocação real de custo compartilhado.
  • Preço ao cliente — que este capítulo não implementa — seria um terceiro número, construído sobre o alocado, com margem e política comercial. Confundir esses três números é o próprio mecanismo da mandatory failure #1 abaixo: "preço por token" é ainda mais estreito que custo marginal, e tratá-lo como se fosse "custo por tarefa útil" é comparar a métrica errada.

O segundo contrato é a chave de cache: tenantId + documentChecksum + schemaVersion + promptVersion + modelId + policyVersion, hasheada de ponta a ponta. Mudar qualquer um desses seis campos precisa produzir uma chave diferente — um teste percorre os seis campos, um de cada vez, e confirma isso (mandatory failure #2). O terceiro contrato é a reserva orçamentária: BudgetLedger.reserve() é síncrona e não tem await na seção crítica, de propósito — em Node, isso garante que duas reservas concorrentes para o mesmo tenant não possam passar pela checagem de orçamento ao mesmo tempo, sem precisar de um lock explícito. Essa garantia depende do event loop de um único processo; uma implantação real com múltiplos processos precisaria da mesma invariante garantida por uma transação de banco de dados ou por um script atômico em um armazenamento compartilhado — uma lacuna documentada, não resolvida aqui.

As quatro falhas obrigatórias

Falha 1 — preço por token virou unit economics

Sintoma: a fatura de API cai depois de uma troca de modelo; ninguém calcula custo por documento aceito antes de declarar vitória.
Causa: preço por token mede o insumo (uma chamada); unit economics real mede o resultado (um documento que alguém pôde usar). Um modelo mais barato por token que erra schema com mais frequência transforma cada erro em uma chamada extra — ainda paga — e pode, além disso, empurrar mais documentos para revisão humana, que costuma custar ordens de grandeza mais que a própria chamada de LLM.
Resposta: medir sempre costPerAcceptedDocument, nunca só costPerCall. No cenário reproduzido em verification.md, o modelo mais barato precisou de três chamadas (duas retries) para ser aceito, contra uma chamada do modelo mais caro para o mesmo tipo de documento; o custo somente de LLM do caminho "barato" ficou 52% mais alto em dólares absolutos que o caminho "caro" — o preço por token mentiu sobre a direção da economia assim que o retry entrou na conta. Quando a revisão humana some ao total (porque o tenant é crítico), ela domina o número final para os dois casos, o que ensina uma segunda lição: para esse tipo de documento, a alavanca de custo real não é qual modelo usar, é se a revisão humana é necessária — uma decisão de routing e eval, não de preço.

Falha 2 — cache ignora tenant ou versão de policy

Sintoma: um documento do tenant B recebe a resposta cacheada de um documento parecido do tenant A; ou uma resposta cacheada sob a policy antiga continua sendo servida depois que a policy mudou.
Causa: uma chave de cache construída só a partir do conteúdo do documento (por exemplo, só o checksum) ignora que a mesma entrada de conteúdo pode, legitimamente, precisar de respostas diferentes para tenants diferentes, ou para versões diferentes de schema/prompt/policy do mesmo tenant.
Resposta: a chave exact inclui os seis campos citados acima, e a leitura (CacheStore.get) exige o tenantId de quem está pedindo e lança CacheLeakageError se a entrada armazenada pertencer a outro tenant — mesmo que um bug upstream tenha, por acidente, produzido a mesma chave para dois tenants. Isso move a garantia de isolamento do momento de construir a chave para o momento de ler o cache, que é onde um bug de fato aparece. Um segundo namespace, semantic, existe só para reusar o prompt/schema/policy compilado de um tenant entre documentos diferentes do mesmo tipo — nunca a resposta de um documento específico — e os dois namespaces nunca colidem, mesmo construídos a partir dos mesmos seis campos (menos o checksum, que o semantic não usa).

Falha 3 — fallback sem orçamento

Sintoma: uma política de retry bem-intencionada continua tentando (ou caindo para um modelo de "reserva") depois que o orçamento do tenant já estourou.
Causa: lógica de retry e lógica de orçamento vivem em lugares diferentes do código, e ninguém garante que a primeira nunca rode sem passar pela segunda.
Resposta: withBudget() reserva orçamento antes de cada tentativa — incluindo retries — e só chama a função de tentativa depois que a reserva foi aceita; se a reserva falhar, a tentativa nunca roda (um teste confirma isso contando quantas vezes a função de tentativa foi de fato invocada). Cada tentativa que roda e falha na validação ainda teve um custo real — tokens foram gastos mesmo que o schema tenha saído errado — e esse custo é comitado no razão, nunca liberado de graça; só uma falha de infraestrutura anterior a qualquer gasto real (por exemplo, uma conexão recusada) libera a reserva sem custo. Um backpressureDelayMs() cresce conforme o orçamento restante de um tenant encolhe, e chega a infinito quando o orçamento acaba — o sinal existe para desacelerar o próprio tenant antes que ele precise ser bloqueado de vez, e para impedir que o esgotamento do orçamento de um tenant reduza o orçamento disponível de outro (noisy neighbor, testado explicitamente).

Falha 4 — a planilha usa a média estável para tráfego explosivo

Sintoma: um plano de capacidade calcula a taxa média de chegada de documentos ao longo de um período, conclui que o sistema está confortavelmente abaixo da capacidade, e é surpreendido por uma fila crescendo sem limite durante um pico real.
Causa: a Lei de Little (L = λW, número médio de itens no sistema igual à taxa de chegada vezes o tempo médio no sistema) e a fórmula de atraso de fila só valem — e só fazem sentido — para um sistema estável, isto é, com utilização menor que 1. A média de três períodos de demanda pode ser perfeitamente estável mesmo que um deles, isoladamente, não seja: a média nunca "vê" o pico, porque ela já misturou o pico com os períodos calmos antes de qualquer cálculo de fila acontecer.
Resposta: examples/costs/capacity-notes.md reproduz isso com números reais, não com afirmação solta. Com quatro workers e tempo de serviço de 1 segundo (capacidade agrupada de 4 documentos/segundo): o cenário de baixa demanda (0,5 doc/s) fica com utilização 0,125 e atraso de fila de 0,036s; o cenário base (1,5 doc/s) fica com utilização 0,375 e atraso de 0,15s; o cenário de burst (4,5 doc/s) tem utilização 1,125 — instável, sem atraso médio finito definido, fila crescendo sem limite enquanto o burst durar. A média dos três (2,1667 doc/s) resulta em utilização 0,542, perfeitamente estável, com atraso de fila de 0,295s e 2,8 documentos em média no sistema — exatamente o número que uma planilha de capacidade calcularia e aprovaria, sem nunca revelar que um dos três cenários que a compõem está fora da região onde a própria fórmula funciona.

O que a evidência de eval tem a ver com routing

routeDocument() não aceita nenhum campo que pareça uma confiança relatada pelo próprio modelo (confidenceScore, probability e variantes são rejeitados explicitamente por assertNoInventedConfidence) — porque uma pontuação que um modelo relata sobre a própria resposta não é uma probabilidade calibrada, é um número que o modelo produz junto com a resposta, sem garantia estatística nenhuma de que 90% de "confiança" corresponda a 90% de acerto real. A única evidência que a política aceita é uma taxa de sucesso medida, no mesmo formato { overall, n } que o gate de release do capítulo 5 já produz por estrato — e mesmo essa evidência só é usada quando a amostra é grande o bastante (n >= 8 neste capítulo, um piso didático, não calibrado contra histórico real de regressão). Um tipo de documento crítico sem amostra suficiente cai para humano mesmo com uma taxa medida alta e um risco aparentemente baixo — a ausência de evidência suficiente é, por si só, motivo de fallback seguro, independente do que qualquer número isolado diga.

Custo, segurança e reversibilidade

Custo e segurança se encontram na mesma decisão de routing: enviar um documento para o nível mais barato sem evidência suficiente não é só um risco de qualidade, é um risco de vazamento de dado se o schema daquele tipo de documento tiver campos sensíveis que o modelo mais barato erra com mais frequência (subestimando a necessidade de revisão humana justamente onde ela mais importa). O isolamento de tenant no cache (falha 2) é, ao mesmo tempo, uma questão de custo (uma chave errada gera uma resposta errada, que precisa de retry) e de segurança (uma resposta de um tenant vazando para outro é um incidente de confidencialidade, não só um bug de cache). Toda decisão deste capítulo é reversível por desenho: uma reserva de orçamento pode ser liberada (release) sem custo se a chamada nunca aconteceu; uma entrada de cache pode ser invalidada por tenant inteiro (invalidateTenant) sem afetar outros tenants; e uma decisão de routing é recalculada a cada documento, nunca fixada permanentemente — não existe, neste desenho, uma migração de dado irreversível de tenant nem um "modo econômico" que precise ser desligado manualmente para voltar ao estado anterior.

Checklist

  • Todo número de custo reportado tem unidade, rótulo (PROJECTION - local arithmetic...) e distingue marginal de alocado.
  • Nenhuma comparação de modelo é feita só por preço por token, sem contar retries e revisão humana no mesmo lote.
  • Toda chave de cache exact inclui os seis campos do contrato; toda leitura exige o tenantId de quem pede.
  • Toda chamada — incluindo retries — passa por uma reserva de orçamento antes de rodar; nenhum "fallback" contorna essa reserva.
  • Nenhum campo de confiança relatada pelo próprio modelo é usado como probabilidade em uma decisão de routing.
  • Todo plano de capacidade declara utilização e estabilidade por cenário, nunca só a média entre cenários.
  • node --test costs/*.test.mjs passa localmente antes de qualquer mudança de política de custo, cache, orçamento ou routing.

Exercícios

Carregando publicação patrocinada...