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.
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.
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.
A triagem do pronto-socorro: o enfermeiro resolve o caso simples, e o especialista só é chamado quando precisa.
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?
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:
p1 em pedido de feature: o código recusa, e a entrada sobe.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.
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
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.
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)
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.
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.
| 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.
{ "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.
cascade.tier em todo trace, com painel de fração por degrau.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 | 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 |
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 humanaO 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.
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.
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.