Conteúdo da trilha
PadrãoOperação e qualidade

Cascata de Modelos

Model Cascade

Cascata de Modelos (Model Cascade) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Operação e qualidade. Começar pelo modelo mais barato e subir de nível só quando necessário.

Elemento
67/70
Nível
Intermediário
Leitura
10 min
Atualizado
Nesta página 16
  1. 01Intenção
  2. 02Problema
  3. 03Solução
  4. 04Estrutura
  5. 05Pseudocódigo
  6. 06Cenários reais
  7. 07Aplicabilidade
  8. 08Quando não usar
  9. 09Como implementar
  10. 10Variações
  11. 11Custo, latência e falhas
  12. 12Como avaliar
  13. 13Em produção
  14. 14Prós e contras
  15. 15Código
  16. 16Teste seu entendimento
  17. ··Perguntas rápidas

Intenção

Começar pelo modelo mais barato e subir para um modelo maior só quando necessário. Serve para cargas de alto volume em que a maior parte das entradas é fácil e só uma fração precisa do modelo mais capaz.

Em uma frase
A triagem do pronto-socorro: o enfermeiro resolve o caso simples, e o especialista só é chamado quando precisa.

Problema

Bianca cuida da triagem de tickets de uma empresa de software. São dezenas de milhares de tickets por dia, e cada um precisa de fila, prioridade e produto. O sistema usa o maior modelo disponível em todos, porque nos testes ele errava menos.

A conta do mês passou do orçamento da área inteira. Quando Bianca olhou os tickets, viu que a maioria era trivial: “esqueci minha senha”, “quero a segunda via da fatura”, “o app não abre”. Qualquer modelo pequeno acerta esses. Uma fração pequena era difícil: tickets longos, com vários assuntos, ou ambíguos.

Um ticket difícil parece com isto (anonimizado):

Paguei a fatura via Pix ontem, o comprovante está no anexo, mas o
sistema bloqueou o acesso e agora a equipe inteira está parada no
fechamento. O CNPJ da nota é o da filial de Campinas.

Pix é o sistema brasileiro de pagamento instantâneo. Esse texto mistura financeiro, acesso e impacto em produção. Mandar para a fila de “acesso” com prioridade baixa é um erro caro. Mandar o ticket de senha para o mesmo modelo que desempata esse caso é outro erro, só que na fatura.

Trocar tudo pelo modelo pequeno derrubaria a qualidade justamente nos tickets difíceis, que são os que mais importam. Manter tudo no grande paga caro pelos fáceis.

A pergunta que este padrão responde: como pagar o preço do modelo grande só nas entradas que precisam dele?

Solução

Organize os modelos em degraus, do mais barato ao mais caro. Cada entrada começa no primeiro degrau. Depois de cada resposta, um critério decide se ela é boa o bastante para sair ou se a entrada sobe para o próximo degrau.

O critério é a parte difícil e a mais importante. Sem ele, você só tem dois modelos e um pedágio. Opções, das mais confiáveis às menos:

  • Verificação em código: a saída passa no schema, na validação de negócio, no teste. Um CNPJ com dígito errado, uma fila desconhecida, uma prioridade p1 em pedido de feature: o código recusa, e a entrada sobe.
  • Concordância: duas tentativas baratas concordam; se discordam, sobe. Combina com Votação.
  • Confiança declarada: o modelo devolve uma confiança junto com a resposta. Útil, mas mal calibrada se não for medida. Se o pequeno escreve 0,92 tanto quando acerta quanto quando erra, o número não filtra nada.
  • Classificador de dificuldade: um passo antes decide o degrau inicial. Aí o padrão se aproxima do Roteamento.

A diferença prática: roteamento escolhe o caminho antes da primeira tentativa; cascata tenta o barato e sobe se a tentativa não serve. Os dois combinam. Uma regra barata (ticket com mais de 300 palavras, menção a Pix e bloqueio ao mesmo tempo) pode pular o primeiro degrau. O resto entra na cascata.

O paper FrugalGPT (Chen, Zaharia e Zou, 2023) documenta a mesma ideia com modelos de APIs diferentes: tentar o barato e subir só quando a resposta não atinge um limiar de qualidade. A lição que importa não é o nome do paper. O ganho vem da fração que não sobe.

Checagem +20 XP de primeiraO modelo pequeno devolve fila, prioridade e produto no schema certo, com confiança 0,91. A regra de negócio diz que feature nunca é p1. A saída veio queue=feature e priority=p1. O que a cascata deve fazer?

Estrutura

flowchart LR
  IN(["1. Entrada"]):::io --> S(["2. Modelo pequeno"]):::llm
  S --> C1{"3. Passou no critério?"}
  C1 -->|sim| OUT(["6. Resposta"]):::io
  C1 -->|não| M(["4. Modelo médio ou grande"]):::llm
  M --> C2{"5. Passou?"}
  C2 -->|sim| OUT
  C2 -->|não| H((("Revisão humana"))):::human
  1. A entrada chega sem nenhuma suposição sobre dificuldade.
  2. O modelo mais barato tenta primeiro.
  3. Um critério, de preferência em código, decide se a resposta é aceitável.
  4. Se não for, a mesma entrada vai para o próximo degrau, às vezes com a tentativa anterior como contexto.
  5. O último degrau tem seu próprio critério.
  6. A resposta sai com o degrau registrado no trace. O que nenhum degrau resolve vai para uma pessoa.

Exemplo trabalhado

Dois tickets, o mesmo prompt, o mesmo valid(). Os JSONs abaixo são ilustrativos.

Ticket A. “Esqueci minha senha do app da loja.”

O modelo pequeno devolve:

{ "queue": "access", "priority": "p3", "product": "store-app", "confidence": 0.94 }

valid() passa: produto conhecido, fila conhecida, feature nenhuma. A confiança calibrada neste degrau, para a faixa acima de 0,85, acertava o bastante no eval. A triagem sai. Uma chamada barata.

Ticket B. O texto do Pix, do bloqueio e da equipe parada.

O modelo pequeno devolve:

{ "queue": "access", "priority": "p2", "product": "store-app", "confidence": 0.88 }

O schema passa. A confiança passa de 0,85. Se o critério for só isso, o ticket vai para acesso com prioridade média, e o financeiro não vê. A regra que Bianca colocou em código é outra: se o texto fala de pagamento e de bloqueio, a fila não pode ser só access. valid() falha. O ticket sobe.

O modelo grande devolve:

{ "queue": "billing", "priority": "p1", "product": "store-app", "confidence": 0.81 }

Fila financeira, prioridade de produção parada. O limiar do degrau grande é mais baixo (nele, 0,60), porque este modelo já é o último automático. Se valid() passar, sai. Se falhar de novo, vai para a fila humana com as duas tentativas no trace.

Compare os quatro jeitos de tratar o Ticket B:

Jeito O que acontece
Grande em tudo Fila certa, preço de grande no Ticket A também
Pequeno em tudo Fila errada, barato
Cascata só com schema e confiança Fila errada: o pequeno estava confiante e o schema passou
Cascata com regra de negócio Sobe, o grande acerta a fila

A cascata só é melhor que o pequeno sozinho se o critério pega o Ticket B. Senão você publicou erros baratos com um nome bonito.

Pseudocódigo

schema Triage { queue: string, priority: "p1" | "p2" | "p3", product: string, confidence: float }

TIERS = [
    { model: "small", min_confidence: 0.85 },
    { model: "large", min_confidence: 0.60 },
]

function triage(ticket):
    for tier in TIERS:
        result = llm(TRIAGE_PROMPT, ticket, model=tier.model) -> Triage     # 2 e 4
        trace.span("cascade." + tier.model, result)
        # 3 e 5. critério: validação em código + confiança calibrada
        if valid(result) and calibrated(result.confidence, tier) >= tier.min_confidence:
            return result                                                   # 6
    return human.queue(ticket)

Cenários reais

Aplicabilidade

  • Volume alto, em que o custo por chamada pesa na conta.
  • Distribuição desigual de dificuldade: a maioria é fácil.
  • Existe um critério de qualidade que dá para checar depois da resposta.
  • A latência extra de subir de degrau é aceitável para a fração que sobe.

Quando não usar

  • Volume baixo. A complexidade não se paga.
  • Não há critério confiável. Sem saber quando a resposta barata está errada, a cascata entrega erros baratos.
  • Antes de montar a cascata, meça a alternativa mais simples: o modelo mais capaz com esforço menor. Em muitos casos ele empata em custo e qualidade com um só modelo para manter, e o cache de prompt não se divide entre modelos.
  • Latência é crítica para todas as entradas. Quem sobe de degrau paga duas chamadas.

Erro comum: cascata como pedágio

Um time coloca o modelo pequeno na frente, sobe em qualquer dúvida, e quase tudo vai ao grande. No trace, cascade.tier aparece em todo span. Na fatura, a conta ficou pior: uma chamada extra em quase tudo, mais o grande.

Isso não é cascata. É o modelo grande com um pedágio na frente. A métrica que denuncia: fração que sobe. Se ela passa de dois terços no seu volume, volte ao eval e afrouxe o critério, ou desista da cascata.

Como implementar

  1. Rode o eval em cada modelo candidato e meça acurácia e custo por categoria. A média geral esconde o Ticket A e o Ticket B.
  2. Meça também o mesmo modelo capaz com esforço de raciocínio menor. Se empatar em qualidade e custo com a cascata, fique com um modelo só.
  3. Escolha o critério de subida e meça a calibração dele: quando o pequeno diz 90% de confiança, ele acerta 90%?
  4. Defina os limites de cada degrau a partir da calibração, não por intuição. Nunca escolha 0,7 porque “parece razoável”.
  5. Simule a cascata no dataset: fração que sobe, acurácia final, custo total, e os erros que o degrau barato aprovaria.
  6. Implemente com o degrau registrado em cada trace, e um destino final para o que nenhum degrau resolve.
  7. Recalibre quando trocar de modelo ou quando a distribuição de entradas mudar.

Como calibrar o limiar

Separe as saídas do degrau barato em faixas de confiança, por exemplo abaixo de 0,6, de 0,6 a 0,8, acima de 0,8. Em cada faixa, meça a acurácia real no eval.

O limiar útil é o ponto em que a faixa começa a acertar o bastante para o produto. Se “acima de 0,8” acerta muito e “de 0,6 a 0,8” acerta pouco, um corte perto de 0,8 faz sentido. Se todas as faixas acertam parecido, a confiança não carrega informação. Aí o critério precisa ser outra coisa: tamanho do texto, presença de um CNPJ, regra de negócio que falhou.

Suponha 10 mil tickets por dia e que o grande custe cerca de 10 vezes o pequeno por chamada. Se 80% ficam no pequeno, o custo diário cai para cerca de 28% do “grande em tudo”, e você ainda paga o grande nos 20% difíceis. Esses números são ilustrativos. Rode a conta com o preço e o volume do seu provedor.

A refatoração que chega neste padrão é Introduzir Cascata de Modelos.

Checagem +20 XP de primeiraO time quer ligar a cascata na semana que vem. Ainda não há eval por categoria, só a impressão de que o modelo grande 'erra menos'. Qual é o primeiro passo?

Variações

  • Cascata com verificação. O critério é uma checagem em código (schema, regra de negócio). O mais confiável.
  • Cascata por concordância. Duas respostas do modelo barato; se divergem, sobe. Combina com Votação.
  • Roteamento por dificuldade. Um classificador (ou uma regra barata) decide o degrau antes da primeira tentativa. Evita a chamada desperdiçada, mas erra o degrau às vezes. Use quando o eval mostrar um sinal barato e estável: tamanho do texto, presença de anexo, menção a Pix e bloqueio.
  • Cascata de esforço. Mesmo modelo, esforço de raciocínio crescente. Mantém um modelo só e o mesmo cache. Vale quando o eval da alternativa simples quase empatou, e um pouco mais de esforço nos casos que falham no critério fecha a diferença.

Custo, latência e falhas

Aspecto O que acontece Como aparece no trace
Chamadas Uma para quem fica no primeiro degrau, duas ou mais para quem sobe spans cascade.* por entrada
Custo Cai quando a maioria resolve no degrau barato custo médio por entrada e fração por degrau
Latência Maior para quem sobe duração somada dos degraus
Falha: confiança mal calibrada Erros com confiança alta passam no primeiro degrau acurácia do degrau 1 abaixo do esperado no eval
Falha: tudo sobe Critério rígido demais fração no degrau 2 perto de 100%
Falha: deriva Entradas novas mudam a distribuição fração que sobe mudando sem mudança de código

Quem sobe paga duas vezes o prompt. Modelos diferentes não compartilham cache de prompt, então a segunda chamada não aproveita o prefixo que a primeira já processou. Se 25% sobem, esses 25% custam pequeno + grande, não só o grande. Inclua isso na simulação.

Checagem +20 XP de primeiraA fração que sobe foi de cerca de 20% durante um mês e, sem deploy, passou a cerca de 70% numa terça. A acurácia geral ainda parece estável. O que você olha primeiro?

Como avaliar

  • Acurácia final da cascata comparada com o modelo grande sozinho e com o modelo capaz em esforço baixo.
  • Custo médio por entrada e fração por degrau.
  • Calibração do critério: curva de acurácia por faixa de confiança no degrau barato.
  • Erros escondidos: casos que o degrau barato aprovou com confiança alta e estavam errados.
{ "ticket": "Paguei a fatura mas o sistema bloqueou o acesso e agora a equipe inteira está parada", "expected": { "queue": "billing", "priority": "p1" }, "note": "deve subir de degrau" }

Monte o dataset com os dois tipos. Se só houver tickets fáceis, a cascata sempre “ganha”. Se só houver o Ticket B, ela sempre sobe e só acrescenta latência. A proporção do dataset precisa parecer com a produção, senão os limiares mentem.

Em produção

  • Atributo cascade.tier em todo trace, com painel de fração por degrau.
  • Alerta quando a fração que sobe muda muito de um dia para o outro.
  • Amostra semanal de respostas do degrau barato com confiança alta para revisão, para pegar erros confiantes.
  • Revisão dos limites a cada troca de modelo.
  • Destino final visível: fila humana com as tentativas anteriores, não um sumiço sem registro.

O smell que este padrão trata, no caso de volume alto, é Custo Sem Teto. A cascata não substitui um teto por execução. Ela reduz o custo médio. O teto continua existindo para a execução que dispara.

Prós e contras

Prós Contras
Custo menor em cargas de alto volume Depende de um critério confiável
Modelo grande reservado para casos difíceis Latência maior para quem sobe
Degraus podem mudar sem mexer no resto Vários modelos para avaliar e manter
O degrau registrado mostra onde está a dificuldade Cache de prompt não se aproveita entre modelos

Código

As duas abas tentam o modelo pequeno, checam validação e confiança, e sobem quando necessário.

Python. Use quando a triagem roda em Python e você quer os degraus numa lista configurável.

from typing import Literal

import anthropic
from pydantic import BaseModel

client = anthropic.Anthropic()


class Triage(BaseModel):
    queue: Literal["billing", "access", "bug", "feature", "other"]
    priority: Literal["p1", "p2", "p3"]
    product: str
    confidence: float


TIERS = [  # limites vindos da calibração no eval, não de intuição
    ("claude-haiku-4-5", 0.85),
    ("claude-opus-5-5", 0.60),
]

PROMPT = "Classifique o ticket em fila, prioridade e produto. Dê uma confiança de 0 a 1."


def valid(t: Triage) -> bool:  # regras de negócio em código
    return t.product in KNOWN_PRODUCTS and not (t.queue == "feature" and t.priority == "p1")


def triage(ticket: str) -> Triage | None:
    for model, min_conf in TIERS:
        t = client.messages.parse(
            model=model, max_tokens=300, system=PROMPT,
            messages=[{"role": "user", "content": ticket}], output_format=Triage,
        ).parsed_output
        trace_tier(model, t)  # registre o degrau no trace
        if valid(t) and t.confidence >= min_conf:
            return t
    return None  # fila de revisão humana

O que você ganhou: degraus e limites são dados, fáceis de mudar e testar. O que você perdeu: a confiança declarada só vale depois de calibrada; meça antes de confiar.

TypeScript. Use quando a triagem roda em Node e você quer o mesmo fluxo tipado.

import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";

const client = new Anthropic();

const Triage = z.object({
  queue: z.enum(["billing", "access", "bug", "feature", "other"]),
  priority: z.enum(["p1", "p2", "p3"]),
  product: z.string(),
  confidence: z.number(),
});
type Triage = z.infer<typeof Triage>;

const TIERS: [string, number][] = [
  ["claude-haiku-4-5", 0.85],
  ["claude-opus-5-5", 0.6],
];

const valid = (t: Triage) => KNOWN_PRODUCTS.has(t.product) && !(t.queue === "feature" && t.priority === "p1");

export async function triage(ticket: string): Promise<Triage | null> {
  for (const [model, minConf] of TIERS) {
    const { parsed_output: t } = await client.messages.parse({
      model,
      max_tokens: 300,
      system: "Classifique o ticket em fila, prioridade e produto. Dê uma confiança de 0 a 1.",
      messages: [{ role: "user", content: ticket }],
      output_config: { format: zodOutputFormat(Triage) },
    });
    if (t && valid(t) && t.confidence >= minConf) return t; // 3 e 5
  }
  return null; // revisão humana
}

O que você ganhou: a mesma cascata com tipos checados pelo compilador. O que você perdeu: o mesmo cuidado com calibração.

Teste seu entendimento

Checagem +20 XP de primeiraTickets com mais de 400 palavras quase sempre precisam do modelo grande no seu eval. Você ainda manda esses tickets para o modelo pequeno primeiro?

Checagem +20 XP de primeira75% das entradas ficam no modelo pequeno, mas o custo médio caiu menos do que a simulação previa. Qual contribuição é a mais fácil de ter esquecido na conta?

Checagem +20 XP de primeiraA amostra semanal do degrau barato achou três tickets de cobrança classificados como vendas, todos com confiança acima de 0,9. Qual é o próximo passo mais útil?

Perguntas rápidas

O que é Cascata de Modelos?

Cascata de Modelos (Model Cascade) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Operação e qualidade. Começar pelo modelo mais barato e subir de nível só quando necessário.

Quais smells o padrão Cascata de Modelos ajuda a curar?

Custo Sem Teto.

Onde Cascata de Modelos aparece na prática?

Nos cenários reais Triagem automática de tickets, Assistente de voz de call center, Moderação de conteúdo e Bot de suporte com escalonamento humano.

Para ir além

Relações

Smells que ajuda a curar 1
Refatorações que chegam aqui 1
Buscar